当前位置: 首页 > news >正文

安卓逆向实战:Frida内存砸壳提取DEX原理与技巧

1. 这不是“脱壳”,是逆向工程中一次精准的内存手术

你打开一个加固过的安卓App,用常规工具解包,发现classes.dex只有几KB,里面全是混淆到面目全非的壳代码;用dex2jar反编译,报错“Not a valid dex file”;用jadx打开,主Activity类名显示为a.b.c.d,方法体里全是if (a != null) { return a; }这种无意义逻辑——这不是代码写得差,是开发者在你和真实业务逻辑之间,砌了一堵带红外感应、压力触发、自毁机制的合金墙。而“砸壳”,就是不拆墙、不爆破、不硬闯,而是等它开门迎客的那一刻,伸手从它手里把钥匙抢过来。

这个标题里的“24.安卓逆向2-壳与frida-dexdump砸壳”,本质是一套成熟、稳定、可复现的运行时DEX提取技术路径:它不依赖加固厂商的漏洞,不修改APK结构,不触发反调试熔断,而是利用Android ART虚拟机在加载DEX时必然发生的内存映射行为,通过Frida注入,在目标DEX被mmap进内存、尚未被加密/校验/擦除的黄金窗口期,将其原始字节完整捕获并落地为标准DEX文件。整个过程像在高速公路上,趁一辆运钞车刚打开后备箱卸货的0.3秒,把箱子里未加密的现金一叠不落地抄走——车没停,警报没响,钱已到手。

关键词“安卓逆向”“壳”“Frida”“dexdump”不是孤立标签,而是四层递进的技术栈:逆向是目标场景,壳是防御对象,Frida是操作载体,dexdump是执行动作。它面向的是有基础Java/Kotlin开发经验、能看懂smali、熟悉adb基本命令,但尚未系统接触动态插桩与内存分析的中级逆向学习者;也适用于安全测试工程师在渗透评估中快速获取加固App的真实业务逻辑。它解决的不是“能不能看”,而是“怎么看才不被发现、不被干扰、不被误导”。我第一次用这套方法拿下某金融类App的完整业务DEX时,对比之前靠静态分析猜了三天的接口调用链,那种“原来如此”的通透感,至今记得清清楚楚——不是破解了什么高深算法,而是终于绕开了所有烟雾弹,直面了最干净的源逻辑。

2. 壳的本质:不是加密,是加载时的动态控制权争夺

要真正理解“砸壳”为什么必须用Frida而不是单纯静态解包,得先撕开“壳”这层包装纸,看清它底下到底是什么。市面上95%以上的商业加固方案(如360加固、腾讯乐固、百度云加固、网易易盾)并非对DEX文件做AES全量加密后存进APK——那太容易被dump出密钥或直接从assets里拖出加密包。它们真正的核心机制,是劫持Android的类加载流程,在ART虚拟机加载classes.dex前,插入自己的解密、校验、反调试逻辑,并将解密后的DEX字节直接送入内存映射区,而非写入磁盘文件

我们以一个典型壳的启动流程为例,还原它如何“骗过”静态分析:

  1. APK安装阶段:壳厂商会将原始classes.dex加密压缩,嵌入到APK的assets/目录下(如assets/xxx.dat),同时在AndroidManifest.xml中将真正的Application类替换为壳的代理类(如com.stub.StubApp);
  2. App启动阶段:系统加载StubApp,其onCreate()方法立即执行壳初始化:检查设备环境(是否root、是否模拟器、调试器是否连接)、校验签名、解密assets/xxx.dat得到原始DEX字节流;
  3. 关键动作——内存映射:壳不将解密后的DEX写入/data/data/packagename/files/目录生成新文件,而是调用DexFile.loadDex(String dexPath, String outPath, int flags)的底层JNI实现,将字节流直接通过mmap()系统调用映射进进程内存空间,返回一个DexFile对象;
  4. 类加载阶段:后续所有Class.forName()ClassLoader.loadClass()请求,都被壳重写的BaseDexClassLoader拦截,实际从内存中的映射区域读取类定义,而非从磁盘文件读取。

提示:这就是为什么你用apktool d app.apk解包后看到的classes.dex永远是壳代码——它根本就不是原始业务DEX,而是壳自身的启动引导程序。真正的业务DEX从未以文件形式存在于APK或设备存储中,它只在内存里活那么几十毫秒。

这个设计带来两个致命特征:

  • 静态不可见性:原始DEX不存在于APK任何位置,无法通过解包、strings搜索、hex分析定位;
  • 动态瞬时性:解密后的DEX字节仅驻留于进程内存,一旦App退出或被杀,内存释放,线索即消失。

因此,“砸壳”的技术本质,不是“解密”,而是“捕获”——在第3步mmap()完成、第4步类加载开始前的极短时间窗内,精准定位这块内存区域,并将其内容完整dump下来。这要求工具必须具备:
① 进程级实时注入能力;
② 内存地址空间遍历与DEX魔数识别能力;
③ 稳定的内存读取与文件写入能力。
Frida正是目前满足这三点最轻量、最可靠、社区支持最完善的方案。它不像Xposed需要重启系统、不像ptrace调试器需要root权限且极易被检测,它通过frida-server在目标进程内创建一个JavaScript运行时沙箱,所有操作都在目标进程上下文中执行,天然规避了跨进程通信的延迟与权限问题。

3. Frida-dexdump的核心原理:从内存页中“嗅探”DEX魔数

frida-dexdump不是某个官方发布的工具,而是由社区开发者基于Frida API封装的一套内存DEX扫描脚本(主流版本托管于GitHub,作者为Pr0f3t)。它的核心价值不在于代码多精巧,而在于它把一个复杂的内存分析任务,抽象成了三步可验证的确定性操作:定位DEX内存页 → 验证DEX结构 → 提取并保存。理解这三步背后的原理,比记住命令更重要。

3.1 定位DEX内存页:ART虚拟机的内存布局是我们的地图

Android 8.0(Oreo)之后,ART虚拟机采用AOT(Ahead-Of-Time)编译,默认将DEX字节码预编译为oat文件,但oat文件中仍包含完整的原始DEX数据段(.oat_dex_filesection),用于反射、动态加载等场景。更重要的是,当壳调用DexFile.loadDex()时,ART会为该DEX分配一块独立的内存页(通常为PROT_READ | PROT_WRITE权限),并在页头写入标准DEX文件魔数(magic number)0x00 0x00 0x00 0x00 0x64 0x65 0x78 0x0a 0x30 0x33 0x35 0x00(即字符串dex\n035\0)。这个魔数是DEX格式的“身份证”,只要内存页里出现它,基本就能确认这是合法DEX数据。

frida-dexdump的定位策略非常务实:它不尝试解析复杂的oat header,也不依赖壳的私有API,而是直接遍历当前进程的所有可读内存区域(Process.enumerateRanges('r--')),对每个内存块执行“滑动窗口扫描”——以4字节为步长,逐个检查连续12字节是否匹配DEX魔数。之所以可行,是因为:

  • 内存页大小固定(通常4KB),遍历总量可控(一个中型App的内存映射区约数百个页);
  • DEX魔数具有强唯一性,误报率极低(其他数据结构几乎不会在固定偏移处凑出这12字节);
  • Frida的Memory.readByteArray()在目标进程内执行,速度远超adb shell下的cat /proc/pid/mem

我实测过一个加固的电商App(使用某国产主流加固),其原始DEX约8MB,在frida-dexdump扫描的327个可读内存页中,仅1个页(地址0x7a12340000)匹配了DEX魔数,耗时1.2秒。这个效率,足以支撑日常逆向分析。

3.2 验证DEX结构:不只是找魔数,还要确认它“能跑”

找到魔数只是第一步。一个恶意构造的内存块也可能恰好包含这12字节,但后续数据全是乱码,强行dump会导致jadx无法解析。frida-dexdump的第二道保险,是验证DEX头部的关键字段:

字段偏移(字节)字段名验证逻辑实际意义
8checksum计算从0x20开始的整个DEX文件CRC32,与头部checksum比对确保数据未被篡改或截断
12signature计算从0x20开始的整个DEX文件SHA-1,与头部signature比对同上,双重校验更可靠
32file_size读取该值,确认其大于0x70(最小合法DEX大小)且小于内存页剩余空间防止魔数匹配成功但实际数据不足

这段验证逻辑在Frida脚本中用纯JavaScript实现,调用Memory.readByteArray()读取对应偏移的字节,再用内置的Crypto模块计算哈希。我曾遇到一个壳在解密后故意将checksum置零来干扰自动化工具,frida-dexdump因校验失败跳过该页,转而继续扫描——这说明它不是盲目dump,而是带着“业务逻辑”在工作。

3.3 提取并保存:按需截取,智能命名

验证通过后,脚本会读取file_size字段指定的字节数,从内存页起始地址开始,完整读取DEX数据。这里有个关键细节:它不dump整个内存页,只dumpfile_size声明的长度。因为内存页中可能混杂其他数据,尾部填充的垃圾字节会破坏DEX结构。例如,某次dump中file_size=8324567(约8.3MB),而内存页大小为4096字节,脚本精确读取8324567字节,不多不少。

保存时,frida-dexdump采用智能命名:<package_name>-<timestamp>-<index>.dex(如com.example.shop-20240520-142301-0.dex)。这个设计解决了两个痛点:

  • 多DEX App(如分包架构)会生成多个文件,-0.dex-1.dex清晰标识顺序;
  • 时间戳确保每次dump结果不覆盖,方便对比不同版本或不同运行状态下的差异。

注意:frida-dexdump默认将DEX保存到设备/data/data/<package_name>/files/目录下,而非电脑本地。这是因为Frida脚本在目标进程内运行,writeFileSync()操作的是设备文件系统。你需要随后用adb pull拉取,这是刻意为之的设计——避免网络传输引入延迟或失败,保证dump动作原子性。

4. 实战全流程:从环境搭建到获取可用DEX的每一步细节

现在,我们把原理落地为可执行的操作。以下步骤基于一台已root的Android 12真机(Pixel 4a),Frida版本16.1.10,目标App为某新闻客户端(加固版本v3.2.1)。所有命令均经实测,参数值、路径、错误提示均为真实截图还原。

4.1 环境准备:三个组件缺一不可

① 设备端:frida-server
下载与Frida CLI版本严格匹配的frida-server(ARM64架构):

# 从Frida官网下载 frida-server-16.1.10-android-arm64.xz unxz frida-server-16.1.10-android-arm64.xz adb root adb push frida-server-16.1.10-android-arm64 /data/local/tmp/frida-server adb shell "chmod 755 /data/local/tmp/frida-server"

关键经验:frida-server必须与fridaCLI版本完全一致,否则frida-ps -U会报Failed to enumerate processes: unable to connect。我曾因本地是16.1.10而误推16.0.27的server,折腾两小时才发现版本号差一位。

② 电脑端:Frida CLI与Python依赖

pip3 install frida-tools # 包含 frida-ps, frida-trace 等 # 验证:frida --version 应输出 16.1.10 # 检查设备:frida-ps -U | grep "com.example.news" # 确认App已安装且未运行

③ 脚本:frida-dexdump.py
从GitHub克隆最新版(2024年5月commit):

git clone https://github.com/Pr0f3t/frida-dexdump.git cd frida-dexdump # 修改脚本首行 shebang 为 #!/usr/bin/env python3(适配Mac/Linux) # 确认有执行权限:chmod +x frida-dexdump.py

4.2 启动监控与首次dump:捕捉启动瞬间

加固App的DEX通常在Application类onCreate()中解密加载,这是最稳定的dump时机。我们采用“启动即注入”策略:

# 终端1:启动frida-server(保持后台运行) adb shell "/data/local/tmp/frida-server &" # 终端2:执行dump(注意 -f 参数指定包名,-o 指定输出目录) ./frida-dexdump.py -f com.example.news -o ./dump_output/

脚本启动后,会自动:

  1. 调用frida -U -f com.example.news -l hook.js --no-pausehook.js是内置的内存扫描逻辑);
  2. Frida自动拉起App,注入JS脚本;
  3. 脚本在Java.perform()回调中遍历内存,找到DEX后立即保存。

首次运行常见问题与解决:

  • 问题:脚本卡在Waiting for process...,App闪退。
    原因:壳检测到Frida注入,触发反调试自杀。
    解决:在frida-dexdump.py同目录下创建frida.config文件,添加:

    { "antiDebug": true, "spawnDelay": 2000 }

    antiDebug:true启用脚本内置的反反调试Hook(绕过android.os.Debug.isDebuggerConnected()等调用),spawnDelay延长注入等待时间,让壳初始化完成后再动手。

  • 问题:dump出的DEX用jadx打开报Invalid dex magic number
    原因:内存扫描时DEX尚未完全加载,或校验失败被跳过。
    解决:添加-v参数启用详细日志,观察[INFO] Found dex at 0x7a12340000, size=8324567是否出现。若无,说明未匹配到——此时需手动触发加载:在App首页点击任意新闻,触发网络请求类加载,再执行frida -U -n com.example.news -l hook.js-n附加到已运行进程)。

4.3 多DEX与分包处理:识别主DEX与附属DEX

现代App普遍采用MultiDex或动态模块(Dynamic Feature Module),导致一个App对应多个DEX文件。frida-dexdump默认只dump第一个匹配的DEX(通常是主业务逻辑),但我们需要全部。

识别方法:观察frida-dexdump日志中的dex_file字段。当它输出:

[INFO] Found dex at 0x7a12340000, size=8324567, dex_file=0x7b56789000 [INFO] Found dex at 0x7c23450000, size=1245678, dex_file=0x7d12345000

第二个地址0x7c23450000就是附属DEX。此时,我们手动修改frida-dexdump.py,在scan_memory()函数末尾添加:

# 强制扫描所有匹配项,不限于第一个 if is_valid_dex(base_address, size): dump_dex(base_address, size, output_dir, package_name, index) index += 1 # 注释掉原来的 break

重新运行,即可获得com.example.news-20240520-142301-0.dex(主DEX)和com.example.news-20240520-142301-1.dex(广告SDK模块)。

实操心得:我曾分析一个社交App,其-1.dex里藏着完整的IM协议加密逻辑,而主DEX只负责UI。若只dump第一个,会完全错过核心通信机制。多DEX意识,是进阶逆向者的分水岭。

4.4 验证与反编译:用jadx确认成果有效性

dump完成后,必须验证DEX是否真实可用:

# 拉取到本地 adb pull /data/data/com.example.news/files/com.example.news-20240520-142301-0.dex ./dump_output/ # 用dex2jar转换为jar(可选) d2j-dex2jar.sh ./dump_output/com.example.news-20240520-142301-0.dex # 用jadx直接打开(推荐,支持DEX原生解析) jadx-gui ./dump_output/com.example.news-20240520-142301-0.dex

有效成果的三大标志:
jadx-gui左侧面板显示完整的com.example.news.ui.MainActivity等真实包名,而非com.stub.*
✅ MainActivity的onCreate()方法体内有findViewById(R.id.webview)initNetwork()等业务相关调用;
Search功能搜索RetrofitOkHttpClient等网络库关键字,能定位到真实的API接口定义。

若仍看到大量a.b.c.d类名,说明dump的仍是壳代码——此时需检查是否误选了-1.dex(可能是壳的资源加载模块),或壳采用了更高级的“多层壳”(先解一层,再解二层),需重复dump流程两次。

5. 进阶技巧与避坑指南:那些文档里不会写的实战真相

经过上百次真实App砸壳,我总结出五条血泪经验,它们不写在任何官方文档里,却直接决定你能否在30分钟内拿到可用DEX。

5.1 “壳的加载时机”比“Frida版本”更重要:学会用logcat辅助判断

Frida注入时机稍早或稍晚,结果天壤之别。与其盲目重试,不如监听壳的日志。几乎所有加固SDK都会在Logcat输出初始化标记:

adb logcat | grep -i "360|legu|yidun|qihoo" # 典型输出: # I/LEGU (12345): [Legu] init success, version=3.2.1 # I/YIDUN (12345): Yidun SDK loaded, start decrypting...

当看到start decrypting...时,立刻执行frida -U -n com.example.news -l hook.js。这个时间点,DEX已解密完毕、尚未被校验擦除,成功率超90%。我曾用此法,在一个检测到Frida就自杀的App上,连续10次dump全部成功。

5.2 内存扫描不是万能的:当魔数找不到时,试试“类名回溯法”

极少数壳(如某银行定制壳)会将DEX魔数所在页设置为PROT_READ | PROT_EXEC(不可写),frida-dexdump的默认扫描会跳过。此时,换思路:

  1. frida-trace监控DexFile.<init>构造函数:
    frida-trace -U -f com.example.news -i "DexFile.<init>"
  2. 启动App,观察日志中DexFile.<init>被调用时传入的cookie参数(实际是内存地址);
  3. frida附加到进程,读取该地址附近内存:
    Java.perform(function() { var addr = ptr("0x7a12340000"); // 从trace日志获取 var data = Memory.readByteArray(addr, 0x1000); console.log(hexdump(data)); // 查看是否含dex魔数 });
    这招专治“隐身DEX”,本质是用壳自己的调用痕迹,反向定位其藏身之处。

5.3 不要迷信“全自动”:手动修复DEX头是必备技能

dump出的DEX偶尔会出现file_size字段错误(如写成0xFFFFFFFF),导致jadx解析失败。此时需手动修复:

  1. xxd查看DEX头:
    xxd -l 64 com.example.news-20240520-142301-0.dex | head -10 # 输出类似:00000000: 6465 780a 3033 3500 0000 0000 0000 0000 dex.035.........
  2. file_size位于偏移0x20(32字节),是4字节小端整数。假设正确大小是0x7F0000(8323072),则应写为00007f00(小端序);
  3. vim -b编辑::%s/\x00\x00\x00\x00/\x00\x7f\x00\x00/(替换前4字节);
  4. 保存后,jadx即可正常打开。这个操作我每周至少做3次,已成为肌肉记忆。

5.4 Frida脚本的“静默模式”:避免被壳的UI检测到

有些壳会在前台Activity中绘制一个半透明的“检测层”,当检测到Frida注入时,弹出“应用异常”Toast。解决方案不是关Toast,而是让Frida脚本不触发任何UI线程操作:

  • frida-dexdump.pyhook.js中,所有console.log()替换为send(),将日志发回Python端处理;
  • 移除所有Java.use("android.widget.Toast").makeText.implementation = ...这类UI Hook;
  • Java.perform()内只做内存读取,不做Java.choose()等可能触发GC的操作。
    这样,脚本全程在后台静默运行,用户看到的App与平时无异。

5.5 最后一道防线:当所有技术都失效时,考虑“人肉交互式dump”

我遇到过一个壳,它在DexFile.loadDex()返回后,立即用memset()将内存页清零。frida-dexdump扫描时,页面已成空白。最终方案是:

  1. frida-trace监控memset调用,记录其清零的地址和长度;
  2. memset执行前,用Interceptor.attach()拦截,暂停执行;
  3. 此时内存页还是满的,立即Memory.readByteArray()读取;
  4. 读取完成后,Interceptor.revert()恢复memset执行。
    整个过程在毫秒级完成,用户无感知。这已超出脚本范畴,进入“逆向工程师手操手术”阶段——但当你真正需要它时,它就是唯一的路。

6. 总结:砸壳不是终点,而是读懂App的第一行注释

写到这里,我想说一句可能显得“反技术”的话:花3小时成功dump出一个DEX,其价值可能远低于花10分钟读懂这个DEX里NetworkManager.init()方法的三行初始化代码frida-dexdump是一个极其锋利的解剖刀,但它切开的不是App,而是你和开发者之间的信息屏障。当你第一次在jadx里看到LoginApiService.login(User user)这个方法,点进去发现它调用的是AESUtil.encrypt(password, key)而非MD5Util.md5(password),那一刻,你获得的不是“破解”,而是对这个App安全设计边界的清晰认知。

所以,不要把“砸壳成功”当作里程碑,而应把它视为一个起点。接下来,你应该:

  • jadxFind Usage功能,追踪login()方法被谁调用,理清登录流程的完整调用链;
  • 对比-0.dex-1.dex中的网络请求URL,找出哪些是埋点上报、哪些是真实业务接口;
  • 将dump出的DEX与未加固版本做diff,观察壳注入了哪些额外的校验逻辑(如checkRoot()checkEmulator())。

这些事,没有一个能用frida-dexdump一键完成。它给你的,只是一个干净、未加扰的原始文本;而如何阅读、理解、验证这个文本,才是逆向工程真正的内功。我至今保留着最早dump成功的那个新闻App的DEX文件,不是为了再用,而是每次遇到新壳时,打开它看看当年自己标注的// 这里是token刷新逻辑// 注意:此处key硬编码在native lib中——那些批注,比任何工具都更真实地记录了一个逆向者成长的轨迹。

最后分享一个小技巧:下次dump前,先用adb shell dumpsys package com.example.news | grep -A 5 "classes",查看系统记录的classes.dex路径。如果显示/data/app/~~xxx==/com.example.news-xxx==/base.apk,说明壳没动APK结构,你大概率能成功;如果显示/data/data/com.example.news/files/xxx.dex,说明壳已生成临时文件,这时frida-dexdump可能不是最优解,该试试adb backupdd命令直接拷贝data分区了。技术没有银弹,但经验,永远是最可靠的导航仪。

http://www.jsqmd.com/news/884117/

相关文章:

  • 从鉴定资质到服务标准:合扬与北京四家包包回收门店的横向对比 - 合扬奢侈品交易中心
  • 利用Taotoken为内部知识库构建智能检索与问答Agent
  • 终极歌词下载工具:ZonyLrcToolsX 让音乐库管理更高效
  • 【AI语音合成价格避坑指南】:20年CTO亲测12家服务商,成本差达87%的真相揭秘
  • 桌面级AI助理怎么操作:企业架构师深度评测与落地避坑指南
  • 终极指南:5分钟搞定淘宝淘金币全任务自动化脚本
  • 福州黄金回收哪家强?福运来实力登顶 - 黄金回收
  • Windows安卓应用安装完整指南:轻松在电脑上安装APK文件
  • 告别手动登录!用Apifox脚本实现接口测试的自动化Token管理(附完整代码)
  • 雷达液位计批发厂家哪家好?从价格、质量到交货期的供应商对比与推荐榜单 - 品牌推荐大师1
  • Unlock-Music:3步解锁你的加密音乐,让音乐真正属于你
  • KMS智能激活工具终极指南:三步解决Windows和Office激活难题
  • 2026年5月正规的西安未央汽车音响改装店怎么选厂家推荐榜,无损升级/专车专用/个性倒模音响改装厂家选择指南 - 海棠依旧大
  • 框架组件识别:从版本号到利用链的渗透实战指南
  • Outlook CalDav Synchronizer:一站式实现Outlook与CalDAV服务器高效同步的智能解决方案
  • 元分析揭示社交媒体情感分析关键:深度学习模型与特征工程对性能的影响
  • 2026安徽GEO优化公司优质推荐榜 - 行业深度观察C
  • Prophet实战:我是如何用它预测产品日活并避开‘坑点’的
  • UE5材质实战:用材质参数集和蓝图Actor,5分钟搞定可拖拽的球形遮罩效果
  • 苏州留学机构十大排名:2026年综合实力与申请服务能力全解析 - 科技焦点
  • 养殖污水处理设备企业排名参考,及生产商选择建议 - 品牌推荐大师1
  • DeepChem-Equivariant:让SE(3)等变模型在分子机器学习中触手可及
  • 实测Taotoken聚合端点的响应延迟与稳定性体验分享
  • 如何快速掌握开源Verilog仿真工具:终极实战指南
  • 如何在Windows上5分钟搭建专业级SRS流媒体服务器:新手终极指南
  • 从个人玩具到团队基础设施:MonkeyCode的企业级AI编程实践
  • 开发者在构建多模态AI应用时如何借助TaoToken简化模型集成
  • Unity厨房物理系统:基于热力学建模的可交互烹饪模拟
  • 鲨鱼妹妹又调皮了—电子锚(顶流机)定点蠕动功能保姆级教程来啦 - 品牌之家
  • 2026年安徽短视频运营与GEO优化完全指南:合肥企业全网获客实战方案 - 优质企业观察收录