安卓逆向工程 - QT

dev 2023-01-14 21:33:19 #逆向 1459

这是我第一次接触安卓逆向,我的需求很简单,仅是修改应用程序中的文本和图像资源,不涉及功能性修改。在 Google 一番后,我自信以为只需要用 Apktool 修改下资源文件即可。于是,便直接上手开始了尝试......

工具

使用 Apktool 逆向第三方、封闭的二进制 Android 应用程序,它可以将资源解码成接近原始的形式,并在进行一些修改后重建它们。

使用 JADX 反编译 Android Dex & Apk 为 Java 源代码。

使用 Ghidra 反汇编二进制文件。

使用 IconKitchen 用于生成 Icons 资源。

使用 NativeScript Image Builder 用于生成 Logo 资源。

此外,可以在此查看安卓逆向工具汇总:github/android-reverse

重新签名

由于 Apktool 被设计得非常便于使用,且有相关教程资源,所以这一过程便不多做叙述。需要注意的是,为了运行 Apktool 重建的应用程序,需要对其进行签名。

首先,使用 keytool 创建密钥:

keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000

使用 jarsigner 进行签署:

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore my-app.apk alias_name

值得注意的是,在较高版本的 Android 版本中,需要使用 apksinger 进行签署,jarsigner 仅是设计用于签名 JAR 文件,它并不关心 APK 签名的技术细节。例如 Android 7.0 (Nougat) 引入了新的 APKv2 签名方案;在 API17 或更低级别上运行的 APK 不能在其签名中使用 SHA-256 摘要等等。[stackoverflow]

在 Mac 系统中,apksigner 位于 Android SDK (>=24.0.3) 的 build-tools 目录:

/Users/ganxiaozhe/Library/Android/sdk/build-tools/33.0.0/apksigner

进行签署:

apksigner sign --ks-key-alias alias_name --ks my-release-key.keystore my-app.apk

进行校验:

apksigner verify my-app.apk

Qt for hate

这时,构建好的 APP 已可以正常在实机上安装运行,但同时我之前在解包时的疑惑(找不到相关文本内容)也得到了应验:该 APP 并不是原生 Android 应用,而是通过 Qt for Android 构建的,资源被编译为了二进制文件。

由于我没有任何 Qt 开发经验,在使用 Apktool 解包后修改资源文件时发现 res/values/* 下没有相关文本内容时,下意识以为是 Apktool 的 aapt 版本造成的资源文件缺失。但当我使用了 Android SDK build-tools aapt\aapt2 dump 之后,发现 Apktool 并没有问题。接着我以为文本被写死在了代码中,便用 JADX 进行 DEX to Jar 的反编译,只在项目包里发现了蓝牙库和 QtProject 的相关代码。不过,在后者的 bindings/QtLoader 类中,发现了一些可疑代码:

private void loadApplication(Bundle bundle) {
    Resources resources = this.m_context.getResources();
    String packageName = this.m_context.getPackageName();
    try {
        if (bundle.getInt(ERROR_CODE_KEY) != 0) {
            AlertDialog create = new AlertDialog.Builder(this.m_context).create();
            create.setMessage(bundle.getString(ERROR_MESSAGE_KEY));
            create.setButton(resources.getString(17039370), new DialogInterface.OnClickListener() { // from class: org.qtproject.qt.android.bindings.QtLoader.1
                @Override // android.content.DialogInterface.OnClickListener
                public void onClick(DialogInterface dialogInterface, int i) {
                    QtLoader.this.finish();
                }
            });
            create.show();
            return;
        }
        ArrayList<String> arrayList = new ArrayList<>(prefferedAbiLibs(resources.getStringArray(resources.getIdentifier("bundled_libs", "array", packageName))));
        if (this.m_contextInfo.metaData.containsKey("android.app.lib_name")) {
            bundle.putString(MAIN_LIBRARY_KEY, this.m_contextInfo.metaData.getString("android.app.lib_name") + "_" + this.preferredAbi);
        }
        bundle.putStringArrayList(BUNDLED_LIBRARIES_KEY, arrayList);
        DexClassLoader dexClassLoader = new DexClassLoader(bundle.getString(DEX_PATH_KEY), this.m_context.getDir("outdex", 0).getAbsolutePath(), bundle.containsKey(LIB_PATH_KEY) ? bundle.getString(LIB_PATH_KEY) : null, this.m_context.getClassLoader());
        Object newInstance = dexClassLoader.loadClass(bundle.getString(LOADER_CLASS_NAME_KEY)).newInstance();
        if (!((Boolean) newInstance.getClass().getMethod("loadApplication", contextClassName(), ClassLoader.class, Bundle.class).invoke(newInstance, this.m_context, dexClassLoader, bundle)).booleanValue()) {
            throw new Exception("");
        }
        QtApplication.setQtContextDelegate(this.m_delegateClass, newInstance);
        if (!((Boolean) newInstance.getClass().getMethod("startApplication", new Class[0]).invoke(newInstance, new Object[0])).booleanValue()) {
            throw new Exception("");
        }
    } catch (Exception e) {
        e.printStackTrace();
        AlertDialog create2 = new AlertDialog.Builder(this.m_context).create();
        create2.setMessage(resources.getString(resources.getIdentifier("fatal_error_msg", "string", packageName)));
        create2.setButton(resources.getString(17039370), new DialogInterface.OnClickListener() { // from class: org.qtproject.qt.android.bindings.QtLoader.2
            @Override // android.content.DialogInterface.OnClickListener
            public void onClick(DialogInterface dialogInterface, int i) {
                QtLoader.this.finish();
            }
        });
        create2.show();
    }
}

例如 bundle.getString() 和 resources.getString() 等函数方法,并且在 assets 目录下有名为 android_rcc_bundle.rcc 的文件,我怀疑它便是 QT 项目的资源文件。于是我找到了两个工具用于反编译 .rcc 文件:zedxxx/rccextended 和 pgaskin/qrc,我尝试了它们两者,但得到的只是一堆嵌套的目录名,以及名为 qmldir 的文件,并无任何其他文本内容。

至此,到这一步我似乎被困住了,于是我下载了 Qt Creator 并尝试构建一个应用进行逆向。发现包构造和之前的相差无几,那么结果就显而易见的指向了我最不想面对但又不得不面对的二进制&汇编,也就是 lib/arm64-v8a 目录下的 .so 文件:

MacBook-Pro:arm64-v8a ganxiaozhe$ ls -lh | awk '{print $5,$9}'

15K libQt6Concurrent_arm64-v8a.so
5.3M libQt6Core_arm64-v8a.so
7.0M libQt6Gui_arm64-v8a.so
1.5M libQt6Network_arm64-v8a.so
484K libQt6OpenGL_arm64-v8a.so
761K libQt6QmlModels_arm64-v8a.so
68K libQt6QmlWorkerScript_arm64-v8a.so
4.2M libQt6Qml_arm64-v8a.so
285K libQt6QuickControls2Impl_arm64-v8a.so
51K libQt6QuickControls2_arm64-v8a.so
189K libQt6QuickLayouts_arm64-v8a.so
252K libQt6QuickShapes_arm64-v8a.so
2.2M libQt6QuickTemplates2_arm64-v8a.so
5.8M libQt6Quick_arm64-v8a.so
6.6M libc++_shared.so
1.3M libGxzTest_arm64-v8a.so ### 项目文件 ###
27K libplugins_imageformats_qgif_arm64-v8a.so
30K libplugins_imageformats_qico_arm64-v8a.so
227K libplugins_imageformats_qjpeg_arm64-v8a.so
33K libplugins_networkinformation_qandroidnetworkinformation_arm64-v8a.so
447K libplugins_platforms_qtforandroid_arm64-v8a.so
212K libplugins_qmltooling_qmldbg_debugger_arm64-v8a.so
107K libplugins_qmltooling_qmldbg_inspector_arm64-v8a.so
19K libplugins_qmltooling_qmldbg_local_arm64-v8a.so
18K libplugins_qmltooling_qmldbg_messages_arm64-v8a.so
37K libplugins_qmltooling_qmldbg_native_arm64-v8a.so
52K libplugins_qmltooling_qmldbg_nativedebugger_arm64-v8a.so
188K libplugins_qmltooling_qmldbg_preview_arm64-v8a.so
139K libplugins_qmltooling_qmldbg_profiler_arm64-v8a.so
27K libplugins_qmltooling_qmldbg_quickprofiler_arm64-v8a.so
98K libplugins_qmltooling_qmldbg_server_arm64-v8a.so
16K libplugins_qmltooling_qmldbg_tcp_arm64-v8a.so
83K libplugins_tls_qcertonlybackend_arm64-v8a.so
301K libplugins_tls_qopensslbackend_arm64-v8a.so
2.5M libprotocore.so
3.1M libprotoqml.so
8.7K libqml_QtQml_Models_modelsplugin_arm64-v8a.so
8.8K libqml_QtQml_WorkerScript_workerscriptplugin_arm64-v8a.so
8.6K libqml_QtQml_qmlplugin_arm64-v8a.so
64K libqml_QtQuick_Controls_Basic_impl_qtquickcontrols2basicstyleimplplugin_arm64-v8a.so
1.0M libqml_QtQuick_Controls_Basic_qtquickcontrols2basicstyleplugin_arm64-v8a.so
144K libqml_QtQuick_Controls_Fusion_impl_qtquickcontrols2fusionstyleimplplugin_arm64-v8a.so
825K libqml_QtQuick_Controls_Fusion_qtquickcontrols2fusionstyleplugin_arm64-v8a.so
162K libqml_QtQuick_Controls_Imagine_impl_qtquickcontrols2imaginestyleimplplugin_arm64-v8a.so
1.7M libqml_QtQuick_Controls_Imagine_qtquickcontrols2imaginestyleplugin_arm64-v8a.so
202K libqml_QtQuick_Controls_Material_impl_qtquickcontrols2materialstyleimplplugin_arm64-v8a.so
973K libqml_QtQuick_Controls_Material_qtquickcontrols2materialstyleplugin_arm64-v8a.so
108K libqml_QtQuick_Controls_Universal_impl_qtquickcontrols2universalstyleimplplugin_arm64-v8a.so
864K libqml_QtQuick_Controls_Universal_qtquickcontrols2universalstyleplugin_arm64-v8a.so
9.0K libqml_QtQuick_Controls_impl_qtquickcontrols2implplugin_arm64-v8a.so
21K libqml_QtQuick_Controls_qtquickcontrols2plugin_arm64-v8a.so
9.0K libqml_QtQuick_Layouts_qquicklayoutsplugin_arm64-v8a.so
9.0K libqml_QtQuick_Shapes_qmlshapesplugin_arm64-v8a.so
9.7K libqml_QtQuick_Templates_qtquicktemplates2plugin_arm64-v8a.so
10K libqml_QtQuick_Window_quickwindowplugin_arm64-v8a.so
9.0K libqml_QtQuick_qtquick2plugin_arm64-v8a.so

其中,根据文件的命名规则,可得知项目资源文件是 libGxzTest_arm64-v8a.so,通过 Ghidra 进行分析后找到相关语言文件:

u_1_63c2aeb87e693_1715x1080.jpeg

值得一提的事,在 StackExchange 上八年前便有人提出过这样的问题:How do I reverse engineer .so files found in android APKs?,同时你也能在 HackTricks 上找到更为详尽的技术原理:Reversing Native Libraries,感谢他们的无私奉献。

所以接下来只需研究如何修改 ELF 文件 (.so etc.),问题应该就能得到解决。