1. 米斗APP逆向分析的整体思路与方案选型
1.1 为什么选择从加固识别入手
拿到一个APK,第一步永远不是急着拖进反编译工具,而是先搞清楚它到底穿了什么“衣服”。米斗APP这个样本,我最初用常规的apktool反编译,出来的classes.dex只有几百KB,打开一看全是壳代码——典型的DEX整体加固特征。再用jadx-gui直接打开APK,搜索Application类的attachBaseContext方法,发现里面调用了大量反射和native方法,基本可以确认是360数字壳。
为什么先做加固识别?因为不同加固方案的脱壳策略完全不同。360壳、腾讯御安全、梆梆、爱加密,它们的DEX加载时机、内存布局、反调试手段都不一样。你如果连壳都没认准就上手脱,轻则脱出来的DEX残缺不全,重则触发反调试导致设备被标记。我个人的习惯是:先看AndroidManifest.xml里的application标签,再看assets目录下有没有可疑的.so或加密文件,最后用frida挂载看DexClassLoader的调用栈。这三步走完,基本能锁定加固厂商。
1.2 脱壳方案对比:内存dump vs 主动调用
确认是360壳之后,脱壳路线有两条:内存dump和主动调用。内存dump的思路是在DEX被加载到内存但还未被完全加密时,把内存中的DEX结构体抠出来;主动调用则是通过HookDexFile的loadDex或openDexFile方法,在系统加载DEX的瞬间拦截并保存。
我最终选了内存dump为主、主动调用为辅的组合方案。原因很直接:360壳对DexFile相关方法做了inline hook,单纯主动调用容易被绕过;而内存dump虽然粗暴,但只要找到合适的时机——通常是attachBaseContext执行完毕、onCreate开始之前——就能拿到相对完整的DEX。具体工具上,frida-dexdump和FRIDA-DEXDump这两个脚本我都试过,前者对360壳的兼容性更好,后者在多DEX场景下更稳。
注意:脱壳环境建议用真机而不是模拟器。360壳对x86模拟器的检测非常敏感,我一开始在AVD上跑,frida刚attach上去进程就崩了。换到一台Android 10的物理机后,同样的脚本一次跑通。
1.3 工具链的取舍与版本坑
工具版本这块我踩过不少坑。frida建议用15.2.2或16.1.4这两个版本,太新的版本对老壳的兼容性反而差,太老的版本又不支持Android 12以上的设备。frida-dexdump用GitHub上star最多的那个Python脚本就行,但它依赖frida-tools的版本,装的时候最好用pipenv隔离环境,不然很容易和系统里的其他Python包冲突。
反编译工具方面,jadx用来快速看Java层逻辑,GDA用来分析native层和壳的交互,IDA Pro则是在需要深入.so文件时才会用到。这里要提一句:不要迷信单一工具。同一个方法,jadx反编译出来可能是乱码,换GDA就能看到清晰的逻辑,这是因为不同工具对DEX指令的解析策略不同。多工具交叉验证是逆向分析的基本功。
2. 360数字壳的核心细节与脱壳实操要点
2.1 360壳的DEX加载流程拆解
360数字壳的DEX加载大致分三个阶段:壳DEX启动、解密真实DEX、替换DEX数组。第一阶段,壳的Application类在attachBaseContext里通过System.loadLibrary加载一个叫libjiagu.so的native库,这个库负责后续所有解密操作。第二阶段,native层从assets目录或APK的尾部读取加密的DEX数据,用AES或自定义算法解密到内存。第三阶段,通过反射修改ActivityThread的mPackages字段,把解密后的DEX路径塞进去,让系统以为这就是原始的DEX。
理解这个流程对脱壳至关重要。因为你要找的“脱壳时机”,就是在第二阶段完成、第三阶段刚开始的那个窗口期。太早,DEX还没解密完;太晚,壳可能已经做了二次加密或内存抹除。我实测下来,在frida里Hookandroid.app.Instrumentation的callApplicationOnCreate方法,在这个方法执行前做内存搜索,成功率最高。
2.2 内存dump的具体操作与参数调优
具体操作上,我用的命令是:
frida -U -f com.example.midou -l dexdump.js --no-pause其中dexdump.js是frida-dexdump的核心脚本。这里有几个参数需要根据设备情况调整:
- 扫描范围:默认是
0x0到0xFFFFFFFF,但在64位设备上这个范围太大,容易OOM。我一般改成从0x70000000到0x7FFFFFFF,这是Android应用DEX常见的映射区间。 - 匹配特征:DEX文件头是
dex\n035或dex\n037,但360壳有时会把文件头改掉。我加了一个模糊匹配,同时搜索dex\n和dex\n0两种模式。 - 超时时间:默认3秒,但米斗APP的DEX有多个,解密耗时较长,我调到了8秒。
跑完之后,脚本会在/data/local/tmp下生成一堆.dex文件。这时候别急着高兴,先检查文件大小和数量。米斗APP正常应该有3个DEX,主DEX大概4MB左右。如果脱出来只有几百KB,说明时机不对,得重新调整Hook点。
2.3 脱壳后的DEX修复与验证
脱出来的DEX往往不是完美的。常见问题有三个:文件头损坏、DEX尾部缺失、指令集错乱。文件头损坏的话,用010 Editor手动把前8个字节改成dex\n035\0就行。DEX尾部缺失比较麻烦,需要用dexfixer这类工具尝试修复,但成功率不高,很多时候只能重新脱。
验证DEX是否完整,我一般用两个方法:一是用dexdump命令看能否正常解析出类列表,二是用jadx打开看关键类(比如MainActivity)的方法体是否完整。如果jadx里看到的方法全是native或者空实现,那说明DEX还是壳的,真实代码没脱出来。
实操心得:脱壳过程中,手机最好开飞行模式,关掉所有后台应用。我遇到过好几次因为系统自动更新或其他应用抢占内存,导致脱出来的DEX不完整。另外,
frida-server要以root权限运行,并且和frida客户端的版本严格一致,否则连不上。
3. 米斗APP核心逻辑的逆向分析与还原
3.1 从入口Activity追踪业务逻辑
DEX脱干净之后,用jadx打开,先找AndroidManifest.xml里注册的入口Activity。米斗APP的入口是com.midou.app.SplashActivity,里面主要做初始化工作和路由跳转。顺着SplashActivity往下追,会看到一个RouterManager类,它根据服务器下发的配置决定跳转到登录页还是主页。这个设计在现在的APP里很常见,目的是为了动态控制功能入口,方便做A/B测试或灰度发布。
继续追RouterManager的调用链,会发现它依赖一个ConfigManager,而ConfigManager的数据来源是一个叫/api/v1/config的接口。到这里,Java层的逻辑基本清晰了:APP启动后先请求配置接口,拿到配置后再决定后续流程。这个过程中,网络请求用的是OkHttp,但做了证书绑定(SSL Pinning),直接抓包会失败。
3.2 native层关键函数的定位与分析
Java层逻辑不复杂,说明核心逻辑大概率在native层。用GDA打开libjiagu.so,搜索JNI_OnLoad,找到注册的native方法列表。其中有一个叫nativeCheckSign的方法,一看就是做签名校验的。还有一个nativeGetKey,返回一个字符串,大概率是用于网络请求加密的密钥。
分析nativeGetKey时,我用IDA Pro加载.so,定位到函数入口,发现它内部调用了AES_set_decrypt_key和AES_cbc_encrypt,说明密钥是AES解密出来的。密钥的密文硬编码在.rodata段,IV是固定的16字节。把密文和IV抠出来,用Python的pycryptodome库解密,得到一串32位的字符串——这就是网络请求的AES密钥。
from Crypto.Cipher import AES import binascii key = binascii.unhexlify('你的密文hex') iv = binascii.unhexlify('你的IV hex') cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = cipher.decrypt(binascii.unhexlify('你的密文数据')) print(plaintext)3.3 网络协议与数据加密的还原
拿到AES密钥后,抓包就简单了。用mitmproxy配合frida的ssl unpinning脚本,绕过证书绑定,就能看到明文请求。米斗APP的请求体是JSON,但关键字段(如userId、token)用AES加密后再Base64编码。响应体也是同样的处理。这意味着你光抓包没用,还得把加解密逻辑还原出来。
我写了一个Python脚本模拟它的加解密流程,核心就是上面那段AES代码。需要注意的是,它的AES模式是CBC,填充方式是PKCS7,这些在IDA里看AES_cbc_encrypt的调用参数就能确认。另外,它的token有有效期,过期后会返回401,需要重新登录获取。这个逻辑在TokenInterceptor类里,用jadx能直接看到。
常见问题:脱壳后的DEX里,有些方法被
nop掉了,或者指令被替换成了goto。这是360壳的“指令抽取”保护。遇到这种情况,要么用frida在运行时Hook这些方法,直接读返回值;要么用IDA分析native层的对应实现。我一般优先选前者,因为快。
4. 逆向过程中的典型问题与排查技巧实录
4.1 反调试检测的绕过方法
360壳的反调试手段主要有三种:ptrace检测、frida检测、调试端口检测。ptrace检测是检查/proc/self/status里的TracerPid字段,如果不是0就说明被调试了。绕过方法是在frida脚本里Hookfopen或open,把TracerPid的值改成0。frida检测是扫描进程内存里有没有frida相关的字符串,比如frida-agent、gum-js-loop。这个可以用frida的--rename参数把agent改名,或者用magisk模块隐藏frida。
调试端口检测是检查adb是否开启。这个最简单,脱壳时把USB调试关掉,用frida的-U参数通过USB连接就行。但要注意,有些壳会检测adb的默认端口5555,如果发现端口开放就主动退出。我一般用adb tcpip换个端口,或者直接用frida的-H参数走网络连接。
4.2 DEX脱壳不完整的修复思路
脱壳不完整是家常便饭。我遇到最多的情况是:DEX文件头正常,但string_ids或method_ids区域全是0。这说明壳在内存里把DEX的某些区域抹掉了。修复思路有两个:一是从/proc/pid/maps里找到DEX的内存映射,用dd命令直接dump整块内存,再用dexfixer重建DEX结构;二是用frida的Memory.readByteArray在DEX加载的瞬间读取,这时候数据还没被抹。
第二种方法成功率更高,但需要精确的Hook点。我一般Hookdalvik.system.DexFile的<init>方法,在方法进入时读取this对象的mCookie字段,这个字段指向DEX在内存中的起始地址。然后根据DEX文件头里的file_size字段,读取整个DEX。这个方法对360壳特别有效,因为它是在DEX完全加载后才做抹除的。
4.3 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| frida attach后进程崩溃 | 壳检测到frida | 查看logcat是否有SIGSEGV | 用magisk隐藏frida,或改名agent |
| 脱出的DEX只有几百KB | Hook时机太早 | 检查DEX文件头是否完整 | 调整Hook点到onCreate之前 |
| jadx打开DEX全是native方法 | 指令抽取保护 | 用GDA查看方法体 | 运行时Hook或IDA分析native层 |
| 抓包全是乱码 | 数据加密 | 搜索AES相关字符串 | 还原AES密钥和IV |
| 请求返回401 | token过期 | 检查请求头里的token | 重新登录获取新token |
反编译报错Invalid DEX | DEX文件头损坏 | 用010 Editor查看前8字节 | 手动修复为dex\n035\0 |
4.4 独家避坑技巧
第一个技巧:脱壳前先备份原始APK。我见过太多人脱壳脱到一半,把原始APK覆盖了,结果连壳都找不回来。备份的时候连split APK一起备份,有些APP的核心DEX在split包里。
第二个技巧:用多个frida脚本交叉验证。frida-dexdump和FRIDA-DEXDump各有优劣,我一般两个都跑一遍,对比脱出来的DEX数量和大小。如果一致,说明脱干净了;如果不一致,以大的那个为准,小的那个大概率是残缺的。
第三个技巧:关注/data/data/包名/下的文件变化。有些壳会把解密后的DEX临时写到应用私有目录,脱壳时用inotifywait监控这个目录,能直接抓到DEX文件。这个方法对腾讯御安全和梆梆壳特别有效,360壳偶尔也能用。
第四个技巧:不要忽视oat和vdex文件。Android 8.0以上,系统会对DEX做预编译,生成oat和vdex文件。这些文件里可能包含完整的DEX代码,直接用vdexExtractor就能提取。我遇到过好几次,壳脱不出来,但从vdex里直接拿到了完整DEX。
5. 逆向分析后的延伸思考与合规提醒
5.1 从逆向结果看APP的安全设计
米斗APP的整体安全设计在同类产品里算中等偏上。它用了360壳做基础防护,网络层做了证书绑定和AES加密,native层有签名校验和密钥保护。但它的弱点也很明显:AES密钥硬编码在.so里,虽然做了简单的字符串混淆,但用IDA的strings窗口一搜就能找到线索。另外,它的token有效期太长,我测试时发现一个token能用72小时,这给了攻击者很大的窗口期。
从防御角度看,如果我是这个APP的开发者,我会把AES密钥改成动态下发,每次启动时从服务器获取,并且用白盒加密保护。签名校验也应该放到native层,并且和关键业务逻辑绑定,而不是单独一个nativeCheckSign方法。当然,这些都是后话,逆向分析的目的不是攻击,而是理解它的安全边界在哪里。
5.2 逆向分析的合规边界
这里必须说清楚:逆向分析仅限用于学习研究和安全评估。我写这篇东西,目的是分享技术思路和实操方法,不是教人去破解或盗取数据。实际操作中,我全程用的是自己的测试账号,没有触碰任何真实用户数据。如果你要分析某个APP,请确保你有合法的授权,或者至少是在自己的设备上、用自己的账号做研究。
另外,脱壳后的DEX和还原的密钥,不要公开传播,更不要用于商业用途。我一般分析完就把这些文件删掉,只保留分析笔记。技术本身没有对错,关键在于用它的人。
5.3 后续可以深入的方向
如果你对米斗APP的逆向还有兴趣,可以继续往这几个方向挖:一是它的so文件里还有几个未分析的native方法,比如nativeEncryptData和nativeDecryptData,大概率是更底层的加密逻辑;二是它的assets目录下有一个config.dat文件,看起来是加密的,可以尝试用脱壳时拿到的密钥解密;三是它的推送和埋点逻辑,这部分用了第三方SDK,分析起来相对独立。
我个人在实际操作中的体会是,逆向分析最耗时的不是脱壳本身,而是脱壳后的代码梳理和逻辑还原。一个中等规模的APP,脱壳可能只要半小时,但把核心业务逻辑理清楚,往往需要好几天。所以耐心和细致比工具更重要。最后再分享一个小技巧:分析过程中随时记笔记,把关键类名、方法名、密钥、接口地址都记下来,不然过两天再回头看,你自己都忘了当时是怎么找到的。