记一次对加固 Android 应用进行脱壳

前言

既然着手开发 Xposed 插件,那有些 apk 是需要脱壳,目前加固厂商有很多,就随便找一家尝试下,正好有个项目想 hooks 里面的业务,就寻思脱壳记录下:

做之前在网上搜了相关资料,很多资料已经老了,对于新的加固方式只能走 Frida 定位追踪脱壳了。

Frida 安装

Frida 是一种动态插桩工具,可以插入一些代码到原生app的内存空间去,动态地监视和修改其行为,这些原生平台可以是WinMacLinuxAndroid或者iOS。至于其中的原理就不管了,毕竟只关心使用,安装 Frida 也很简单,就是准备个手机和电脑装好对应的环境就行。

电脑是 macOS 自带 Python 环境直接使用 pip3 安装 Frida 环境就好了:

pip3 install frida-tools

如果控制台报环境配置的警告添加环境变量:

# Python3
export PATH="$HOME/Library/Python/3.9/bin:$PATH"

好了之后可以直接输出版本号就正常了:

jaxson@iHomer ~ $ frida --version
16.3.3

电脑端装好了之后手机也需要配置下,在逆向开发需要 root 环境的手机是必备的,我的手机就是一加系列的 OnePlus 11 并且已经做了 KernelSU 内核 root 包括安装了 LSPosed 环境。如果自带的系统有问题的话可以去利用系统的 DSU动态系统更新 特性安装 DSU-Sideloader 软件来实现 GSIs 系统沙盒环境,减少国产 ROM 的干扰。
这里就不再详细描述怎么装 DSU 系统了,Android 安装 Frida 环境很简单参考官网文档即可:

unxz frida-server.xz # https://github.com/frida/frida/releases
$ adb root # might be required
$ adb push frida-server /data/local/tmp/
$ adb shell "chmod 755 /data/local/tmp/frida-server"
$ adb shell "/data/local/tmp/frida-server &"

然后链接电脑输入:frida-ps -U 输出手机应用说明环境配置成功了。

后面我用了 (MagiskFrida)[https://github.com/ViRb3/magisk-frida] 模块,刷入一直以为是生效的,给我排查很久原因才发现根本没启动,可能是不兼容 KernelSU 导致的,还是老老实实用上面的方法吧。

尝试脱壳

在各大论坛搜了下资料发现能够兼容新系统且新版本 Frida 脱壳脚本就:FART 后续就打算使用这个脚本脱壳,不过遇到对抗问题。

先把上面的脱壳脚本放一边,处理对抗问题:

jaxson@iHomer ~ $ frida -U -f com.iiong.example.app
     ____
    / _  |   Frida 16.3.3 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to PHB110 (id=c842ab07)
Spawned `com.iiong.example.app`. Resuming main thread!
[PHB110::com.iiong.example.app ]-> Process terminated
[PHB110::com.iiong.example.app ]->

Thank you for using Frida!

应用直接打开后中断调试,看来是对此做了 Frida 检测了,所以下面需要做一些 hook 来绕过 Frida 检测,直接使用 android_dlopen_ext 函数来加载是什么,其实不说也知道上面截图展示是爱加密加固,那就是 so 搞的鬼:

function hookDlopen(libraryName = "") {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var libraryPathPointer = args[0];

      if (libraryPathPointer !== undefined && libraryPathPointer !== null) {
        var libraryPath = ptr(libraryPathPointer).readCString();
        console.log(libraryPath);
      }
    },
  });
}

setImmediate(hookDlopen, "");

然后执行调试:

jaxson@iHomer ~/Downloads/FART-Fix-main $ frida -U -f com.iiong.example.app -l test.js 
     ____
    / _  |   Frida 16.3.3 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to PHB110 (id=c842ab07)
Spawned `com.iiong.example.app`. Resuming main thread!
[PHB110::com.iiong.example.app ]-> libstats_jni.so
/system/framework/oat/arm64/org.apache.http.legacy.odex
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/oat/arm64/base.odex
/data/user/0/com.iiong.example.app/files/libexec.so
/data/user/0/com.iiong.example.app/files/libexecmain.so
Process terminated
[PHB110::com.iiong.example.app ]->

Thank you for using Frida!

多试了几次从 Process terminated 得知是 libexecmain.so 内部函数做了反调试处理,继续编写脚本来进一步定位:

var baseAddress = null;

function hookDlopen(targetLibrary = "") {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var libraryPathPointer = args[0];

      if (libraryPathPointer !== undefined && libraryPathPointer !== null) {
        var libraryPath = ptr(libraryPathPointer).readCString();
        console.log(libraryPath);
        if (libraryPath.indexOf(targetLibrary) !== -1) {
          this.shouldHook = true;
        }
      }
    },
    onLeave: function (retval) {
      if (this.shouldHook) {
        baseAddress = Module.findBaseAddress(targetLibrary);
        hookPthreadCreate();
      }
    },
  });
}

function hookPthreadCreate() {
  Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), {
    onEnter: function (args) {
      var threadFunctionAddress = args[2];

      var offset = threadFunctionAddress.sub(baseAddress);
      console.log("The thread function offset is " + offset);
    },
  });
}

// Start hooking
setImmediate(hookDlopen, "libexecmain.so");

执行脚本返回:

/data/user/0/com.iiong.example.app/files/libexec.so
/data/user/0/com.iiong.example.app/files/libexecmain.so
The thread function address is 0x6c8328e4
The thread function address is 0x6c8328e4
The thread function address is 0x6c8328e4
The thread function address is 0x6c8328e4
The thread function address is 0x6c8328e4
The thread function address is 0x6c8329cc
The thread function address is 0x6c8329cc
The thread function address is 0x6c8329cc
The thread function address is 0x6c8329cc
The thread function address is 0x6c8329cc
The thread function address is 0x6c8454ac
The thread function address is 0x6c8454ac
The thread function address is 0x6c8454ac
The thread function address is 0x6c8454ac
The thread function address is 0x6c8454ac
The thread function address is 0x6c845f18
The thread function address is 0x6c845f18
The thread function address is 0x6c845f18
The thread function address is 0x6c845f18
The thread function address is 0x6c845f18
The thread function address is 0x6c847640
The thread function address is 0x6c847640
The thread function address is 0x6c847640
The thread function address is 0x6c847640
The thread function address is 0x6c847640
The thread function address is 0x6c847b88
The thread function address is 0x6c847b88
The thread function address is 0x6c847b88
The thread function address is 0x6c847b88
The thread function address is 0x6c847b88
Process terminated

不停的把 App 杀掉重新调试发现毫无规律之言,索性把 so 换成 libexec 再看看:

/data/user/0/com.iiong.example.app/files/libexec.so
/data/user/0/com.iiong.example.app/files/libexecmain.so
The thread function address is 0x328e4
The thread function address is 0x328e4
The thread function address is 0x328e4
The thread function address is 0x328e4
The thread function address is 0x328e4
The thread function address is 0x329cc
The thread function address is 0x329cc
The thread function address is 0x329cc
The thread function address is 0x329cc
The thread function address is 0x329cc
The thread function address is 0x454ac
The thread function address is 0x454ac
The thread function address is 0x454ac
The thread function address is 0x454ac
The thread function address is 0x454ac
The thread function address is 0x45f18
The thread function address is 0x45f18
The thread function address is 0x45f18
The thread function address is 0x45f18
The thread function address is 0x45f18
The thread function address is 0x47640
The thread function address is 0x47640
The thread function address is 0x47640
The thread function address is 0x47640
The thread function address is 0x47640
The thread function address is 0x47b88
The thread function address is 0x47b88
The thread function address is 0x47b88
The thread function address is 0x47b88
The thread function address is 0x47b88
Process terminated

发现无论怎么调试 0x328e4 这个偏移地址是固定的,应该是 frida 的反调试现成,所以直接把该地址换成空函数,来达到绕过:

var soaddr = null;
function hook_dlopen(soName = "") {
  Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"), {
    onEnter: function (args) {
      var pathptr = args[0];

      if (pathptr !== undefined && pathptr != null) {
        var path = ptr(pathptr).readCString();
        if (path.indexOf(soName) != -1) {
          this.hook = true;
        }
        console.log(path);
      }
    },
    onLeave: function (ret) {
      if ((this.hook = true)) {
        soaddr = Module.findBaseAddress(soName);
        hook_pthread_create(soName);
      }
    },
  });
}

function hook_pthread_create(soName) {
  Interceptor.attach(Module.findExportByName("libc.so", "pthread_create"), {
    onEnter(args) {
      var func_addr = args[2];

      var offes = func_addr.sub(soaddr);
      // console.log("The thread function address is " + offes);
      if (offes == 0x329cc || offes == 0x454ac) {
        Interceptor.replace(
          func_addr,
          new NativeCallback(
            function () {
              console.log("in replaces");
              setTimeout(function () {
                console.log("脱壳中。。。");
                // setImmediate(hookFunc);
              }, 1000 * 10);
            },
            "void",
            []
          )
        );
      }
    },
  });
}
setImmediate(hook_dlopen, "libexec.so");

这时候再看看效果:

jaxson@iHomer ~/Downloads/FART-Fix-main $ frida -U -f com.iiong.example.app -l test.js
     ____
    / _  |   Frida 16.3.3 - A world-class dynamic instrumentation toolkit
   | (_| |
    > _  |   Commands:
   /_/ |_|       help      -> Displays the help system
   . . . .       object?   -> Display information about 'object'
   . . . .       exit/quit -> Exit
   . . . .
   . . . .   More info at https://frida.re/docs/home/
   . . . .
   . . . .   Connected to PHB110 (id=c842ab07)
Spawned `com.iiong.example.app`. Resuming main thread!
[PHB110::com.iiong.example.app ]-> libstats_jni.so
/system/framework/oat/arm64/org.apache.http.legacy.odex
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/oat/arm64/base.odex
libframework-connectivity-jni.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libhtsfx.so
/data/user/0/com.iiong.example.app/.tcache/libs/libhts.so
libSchedAssistJni.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libijm-emulator.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libmmkv.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libumeng-spy.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libencrypt.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libGenX.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libscheduler.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libRemoteParkAssist.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libmangocracker.so
/data/app/~~LtHZTgnVAXMR3WItyKi-NQ==/com.iiong.example.app-dZs0vWoXniKdWGmLznkxHA==/base.apk!/lib/arm64-v8a/libBugly_Native.so
/my_product/lib64/libcolorx-loader.so
liboplushwui_jni.so
liboplusgui_jni.so
/vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so
/vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so
/vendor/lib64/hw/gralloc.default.so
/vendor/lib64/hw/android.hardware.graphics.mapper@4.0-impl-qti-display.so
[PHB110::com.iiong.example.app ]->                                                  
[PHB110::com.iiong.example.app ]-> Frida
{
    "version": "16.3.3"
}
[PHB110::com.iiong.example.app ]->

确实成功了,中间在做自己想要的 Hoook 事件,在那边 FART脚本就可以对应用进行脱壳了。要注意创建对应的文件夹和权限。

总结

在做这篇文章的时候参考阅读很多大佬逆向解析思路以及对应的策略方法,想写出来源之处无奈隔几天忘了在哪看了,有时候不得不感谢大佬,大佬的前途永远是披荆斩棘的!

不过后续又看到 hluda-server 魔改的 frida 也是可以规避应用检测,不过也没尝试去做,下次再试试。