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-arm642. 目标应用分析与Hook点定位
拿到一个加固应用,首先要确定它的包名。这里有个小技巧,可以用frida-ps命令列出所有正在运行的进程:
frida-ps -Ua找到目标应用后,我们需要分析它的内存加载情况。360加固通常会通过libart.so来加载原始DEX,所以我们的Hook点就选在DexFile类的OpenCommon方法上。这个方法在不同Android版本中的符号名会有些差异:
Android O:
_ZN3art7DexFile10OpenCommonEPKhjRKNSt3__112basic_stringIcNS3_11char_traitsIcEENS3_9allocatorIcEEEEjPKNS_10OatDexFileEbbPS9_PNS0_12VerifyResultEAndroid 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!!!"); } });这里有几个关键点需要注意:
- DEX文件的魔数一般是"dex\n",可以用来验证是否捕获到正确的内存区域
- DEX文件大小存储在头部偏移0x20的位置
- 保存路径要确保有写入权限,建议直接使用目标应用的数据目录
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加固,有时候会碰到抽取指令的情况,这时候需要结合动态分析和静态分析。我常用的方法是:
- 用Frida trace关键方法调用
- 对比原始DEX和运行时内存
- 用IDA Pro分析so层逻辑
最后提醒一点,脱壳后的代码可能会有一些反调试陷阱,在分析时要特别注意那些看起来不合理的条件判断和异常处理。