Frida实战:动态脱壳360加固应用

1. 环境准备与工具安装

动态脱壳的第一步是搭建好工作环境。我们需要准备一台已经root的Android设备或者模拟器,我个人推荐使用真机,因为部分模拟器可能会被360加固检测到。另外还需要安装好Python环境和Node.js,因为Frida的很多工具链依赖这些环境。

Frida的安装其实很简单,直接用pip就能搞定:

pip install frida-tools

但这里有个坑要注意:frida-server的版本必须和本地frida-tools版本一致。我遇到过好几次因为版本不匹配导致连接失败的情况。可以去Frida的GitHub releases页面下载对应版本的frida-server,比如当前最新稳定版是15.1.27:

wget https://github.com/frida/frida/releases/download/15.1.27/frida-server-15.1.27-android-arm64.xz

解压后推送到手机:

adb push frida-server-15.1.27-android-arm64 /data/local/tmp adb shell chmod +x /data/local/tmp/frida-server-15.1.27-android-arm64

启动服务前记得先转发端口:

adb forward tcp:27042 tcp:27042 adb shell /data/local/tmp/frida-server-15.1.27-android-arm64

2. 目标应用分析与Hook点定位

拿到一个加固应用,首先要确定它的包名。这里有个小技巧,可以用frida-ps命令列出所有正在运行的进程:

frida-ps -Ua

找到目标应用后,我们需要分析它的内存加载情况。360加固通常会通过libart.so来加载原始DEX,所以我们的Hook点就选在DexFile类的OpenCommon方法上。这个方法在不同Android版本中的符号名会有些差异:

  • Android O:_ZN3art7DexFile10OpenCommonEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_PNS0_12VerifyResultE

  • Android P:_ZN3art13DexFileLoader10OpenCommonEPKhmS2_mRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_NS3_10unique_ptrINS_16DexFileContainerENS3_14default_deleteISH_EEEEPNS0_12VerifyResultE

实际操作中可以用下面这个脚本自动获取当前设备的函数名:

var moduleFuncName; var m = Module.enumerateExportsSync('libart.so'); m.forEach(function(m){ if(m.name.indexOf("OpenCommon") != -1){ moduleFuncName = m.name; console.log("module function name: "+ m.name); }else if(m.name.indexOf("OpenMemory") != -1){ moduleFuncName = m.name; console.log("module function name: "+ m.name); }; });

3. Frida脚本编写与DEX Dump

找到正确的Hook点后,就可以编写核心的脱壳脚本了。这个脚本主要做三件事:拦截DEX加载、读取内存中的DEX数据、保存到文件。下面是我在实际项目中验证过的脚本:

Interceptor.attach(OpenCommon,{ onEnter: function(args){ console.log("base: "+ args[1]); console.log("size: "+ args[2].toInt32()); // 打印前64字节的hexdump console.log(hexdump( args[1],{ offset: 0, length: 64, header: true, ansi: true })); var begin = args[1]; console.log("magic : " + Memory.readUtf8String(begin)) // 计算DEX文件大小 var address = parseInt(begin,16) + 0x20; var dex_size = Memory.readInt(ptr(address)); console.log("dex_size :" + dex_size); // 保存到文件 var file = new File("/data/data/com.target.app/" + dex_size + ".dex", "wb"); file.write(Memory.readByteArray(begin, dex_size)); file.flush(); file.close(); }, onLeave: function(retval){ console.log("Finished!!!"); } });

这里有几个关键点需要注意:

  1. DEX文件的魔数一般是"dex\n",可以用来验证是否捕获到正确的内存区域
  2. DEX文件大小存储在头部偏移0x20的位置
  3. 保存路径要确保有写入权限,建议直接使用目标应用的数据目录

4. 常见问题与解决方案

在实际操作中,你可能会遇到各种问题。下面分享几个我踩过的坑:

问题1:Frida连接失败可能原因包括:

  • 端口未正确转发(检查adb forward)
  • frida-server版本不匹配(用frida --version检查)
  • 手机未root或selinux限制(尝试setenforce 0)

问题2:Hook不到OpenCommon这种情况通常是因为:

  • 函数符号名不匹配(先用脚本动态获取)
  • 加固壳做了反调试(尝试在非调试模式下启动应用)
  • Android版本太新/太旧(需要调整Hook点)

问题3:dump出的DEX无法解析可能原因是:

  • 捕获的时机不对(有些壳会多次加载)
  • 内存数据被修改(尝试不同的Hook点)
  • 文件头损坏(手动修复magic和checksum)

对于360加固,我建议在应用刚启动时就附加Frida,因为它的壳加载比较靠前。如果遇到反调试,可以试试下面这个绕过脚本:

var pthread_create = Module.findExportByName(null, "pthread_create"); Interceptor.replace(pthread_create, new NativeCallback(function (a, b, c, d) { return 0; }, 'int', ['pointer', 'pointer', 'pointer', 'pointer']));

5. 后续分析与处理

成功dump出DEX后,还需要做一些后续处理。我通常会用dex2jar转换成jar文件:

d2j-dex2jar.sh dumped.dex

然后用JD-GUI或者直接使用jadx-gui查看代码。如果发现代码逻辑不完整,可能是多dex的情况,需要重复dump过程。

对于360加固,有时候会碰到抽取指令的情况,这时候需要结合动态分析和静态分析。我常用的方法是:

  1. 用Frida trace关键方法调用
  2. 对比原始DEX和运行时内存
  3. 用IDA Pro分析so层逻辑

最后提醒一点,脱壳后的代码可能会有一些反调试陷阱,在分析时要特别注意那些看起来不合理的条件判断和异常处理。