
逆向圈子里但凡做过安卓样本分析的人基本都绕不开一个词脱壳。不管是分析恶意软件、做合规审计还是研究自家App的加固强度你在拿到一个加壳样本时第一步想的往往就是怎么把真正的dex从内存里捞出来。Frida-unpack这个动态脱壳工具就是专门干这事的——它依托Frida的插桩能力在App运行时从内存中还原并转储dex文件帮你绕开壳的静态保护直接看到业务代码本身。这篇内容会从原理讲到实操再聊到常见坑适合刚接触脱壳的新手也适合已经用Frida但还没系统梳理过脱壳流程的进阶读者。我会把自己在实际使用中踩过的坑、试过的方案、验证过的参数一并写出来你可以直接照着复现。1. 脱壳这件事到底在解什么1.1 加壳的本质让静态分析失效先理清一个基础问题壳做了什么。APK本质上是一个zip包里面最核心的是classes.dex——Dalvik字节码文件也就是App的业务逻辑所在。加固厂商做的事情简单说就是把原本的classes.dex加密或隐藏起来替换成一个壳的loader通常是新的classes.dex或so库真正的业务dex只在App运行时才被解密并加载进内存。静态分析时你解压APK看到的是壳的入口、壳的so库、加密后的文件而不是真正的dex。这时候想要分析业务逻辑要么静态逆壳研究壳的加载流程找到解密点要么让App自己跑起来在它解密dex加载进内存之后直接从内存里把数据捞出来——这就是动态脱壳的基本思路。动态脱壳的优势在于不依赖壳的具体加密算法。不管壳是几层、用什么加密方式最终它都要把dex加载进ART虚拟机而DexFile对象、ClassDef、方法区这些结构一定会出现在内存里。你要做的就是在合适的时机dump内存再做修复。1.2 Frida为什么适合干这件活Frida是一个动态插桩框架它能在App运行时注入JavaScript代码hook住Java层、Native层的方法读写内存、调用函数。它不是重打包、不是静态修改而是运行时注入所以不太受签名校验、完整性校验的干扰——只要App能正常跑起来Frida就能介入。脱壳工具用Frida来实现核心就是把hook点集中在ART虚拟机的加载函数上比如OpenMemory、DexFile::OpenFile、DefineClass这些。壳调用这些函数加载真正dex的时候Frida的hook就能把dex数据截获下来写入本地文件。相比自己写ptrace方案的脱壳机用Frida开发快、迭代快、跨机型适配相对容易这也是Frida-unpack这类工具能流行起来的原因。注意动态脱壳有两个前置条件——App能正常运行、设备能注入Frida。如果App有反调试、反Frida检测你需要先处理好对抗问题否则后面所有步骤都无从谈起。2. 工具选型与核心设计拆解2.1 常见动态脱壳方案对比在聊Frida-unpack之前先横向对比一下主流方案方便你理解为什么Frida路线值得优先尝试。方案类型代表工具优势劣势Frida脚本脱壳Frida-unpack、各类开源dump脚本部署快、上手门槛低、跨版本相对灵活容易被检测、需要对抗反Frida定制ROM脱壳刷入定制系统改ART源码稳定、不易被App感知环境重、限设备、维护成本高内核级脱壳通过内核模块抓取文件读写隐蔽性强开发量大、兼容性差脱壳机基于定制虚拟机各类商业脱壳机功能全、自动化高贵、闭源、样本数据易泄露个人经验如果你只是偶尔分析一两个样本Frida脚本路线性价比最高如果要做批量脱壳流水线可以考虑定制ROM方案。Frida-unpack定位就是前者——一套脚本、一台root过的设备就能完成大部分脱壳需求。2.2 Frida-unpack的工作流程我用的这个frida-unpackGitHub上常见的那套也有多个fork版本核心流程如下注入Fridahook住ART虚拟机中加载dex的入口函数。当壳调用加载函数时Frida在JavaScript层拿到dex文件的内存地址和大小。读取内存数据通过send或者rpc回调将数据传回主机端。主机端Python脚本接收dex的二进制数据按照dex魔数dex\n验证后写入文件。对所有dump出来的dex做批量修复修正checksum、signature、data_offset等字段。关键设计点在于hook哪个函数、在哪个时机dump。早期工具大都hookDexFile::OpenFile或DexFile::OpenMemory这两个函数在壳load dex时一定会被调用。后来高版本安卓Android 8.0以上ART改动有的需要hookDexFile::DexFile构造函数或OpenCommon否则可能会漏dex。2.3 为什么选择“内存Dump 数据修复”组合有些新手会问内存dump出来直接就能用吗不一定。壳在加载dex时ART虚拟机为了优化性能可能对dex做了一些调整比如数据区偏移被修改、class_defs顺序被调整更常见的是checksum和signature字段因为运行时被改写而失效。直接用十六进制工具拉出来的dex拖进jadx或者GDA里往往会报错提示“checkSum mismatch”之类的问题。所以脱壳完整链路必须是dump出来之后再用脚本修正dex头部的校验字段。修复工具一般会按dex文件格式解析header重新计算checksum采用Adler32算法和signature采用SHA-1并把file_size修正为实际读取长度。这一步是决定你dump出来能不能用的关键。3. 完整实操从环境搭建到拿到可用dex3.1 环境准备与基础依赖我使用的环境如下你可以作为参考分析机Ubuntu 20.04.xWindows 也可但Python和adb命令在Linux下更顺手目标设备Pixel系列手机系统Android 911已rootMagiskFrida工具链frida-tools 15.x frida-server 15.x注意要和主机端版本一致Python环境Python 3.8及以上需要安装frida、frida-tools、androguard部分修复脚本会用到安装Frida命令如下pip install frida-tools # 查看本机frida版本 frida --version手机上启动与主机端同版本的frida-serveradb push frida-server /data/local/tmp/ adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server -l 0.0.0.0:6666 提示frida-server版本必须与主机端frida版本严格一致。我遇到过一次主机15.2.0、设备15.1.24结果大部分hook都注册失败报错完全没有提示排查了半天。3.2 核心脚本hook点设计Frida-unpack的脚本核心部分我用一个尽量精简的版本做演示。以下脚本hook了OpenMemory和OpenFile两个入口适配大多数壳// frida_unpack.js function dumpDex(startAddr, size) { var file new File(/data/local/tmp/dump_ Date.now() _ size .dex, wb); var content startAddr.readByteArray(size); file.write(content); file.close(); send({type: dump, size: size, path: /data/local/tmp/dump_ Date.now() _ size .dex}); } // Hook Art DexFile::OpenCommon 适配高版本 var openCommon Module.findExportByName(libart.so, _ZN3art7DexFile10OpenCommonEPKhmRNS_6MemMapERKNS_10OatDexFileE); if (openCommon) { Interceptor.attach(openCommon, { onEnter: function(args) { var begin args[0]; var size args[1].toInt32(); if (size 0 size 100 * 1024 * 1024) { // 过滤异常大小 try { dumpDex(begin, size); } catch (e) { console.log([*] dump error: e); } } } }); } // Hook DexFile::OpenMemory var openMemory Module.findExportByName(libart.so, _ZN3art7DexFile10OpenMemoryEPKhmRKS6_NS_12DexFileVariantE); if (openMemory) { Interceptor.attach(openMemory, { onEnter: function(args) { var begin args[0]; var size args[1].toInt32(); if (size 0 size 100 * 1024 * 1024) { try { dumpDex(begin, size); } catch (e) { console.log([*] dump error: e); } } } }); }这段脚本的思路通过Module.findExportByName从libart.so里找导出函数的符号地址。你可能会问为什么符号名这么长这是C的name mangling机制Android ART源码里的DexFile::OpenCommon方法编译到release版本后符号就是_ZN3art7DexFile10OpenCommonE...。不同Android版本的符号名可能不一样建议先用adb shell grep或者frida-trace扫一下当前设备上libart.so的导出符号。dump下来的dex不会立刻写到主机端我习惯先在设备上落盘然后统一adb pull回本地。这样比较稳脚本里如果用rpc逐块回传大数据量时容易丢包。3.3 主机端Python调度与文件修复设备端负责dump主机端负责调度与修复。我用一个Python脚本启动Frida attach到目标App进程并加载JS脚本# unpack_main.py import frida import sys import os import time device frida.get_usb_device() session device.attach(com.example.target) # 目标包名 with open(frida_unpack.js, r, encodingutf-8) as f: script_code f.read() script session.create_script(script_code) def on_message(message, data): if message[type] send: print([], message[payload]) elif message[type] error: print([*], message[stack]) script.on(message, on_message) script.load() print([*] Hook loaded. Waiting for dex dump...) time.sleep(300) # 根据App启动流程调整挂机时间这个脚本一般配合手动操作启动脚本后再去App上点击需要触发的页面。有些壳的dex是在App冷启动时就解密有的则是等某个功能页面打开时才加载。所以我一般建议先冷启动一遍再热启动一遍再手动触发几个核心页面保证各类时机覆盖。拿到dump文件后批量修复dex头。这里用到一个比较轻量的修复脚本# fix_dex.py import hashlib import zlib import struct import sys def fix_dex(file_path): with open(file_path, rb) as f: data bytearray(f.read()) if data[:4] ! bdex\n: print(f[!] {file_path} is not a dex file) return file_size struct.unpack(I, data[32:36])[0] # 如果文件大小字段与实际不符修正 if file_size ! len(data): data[32:36] struct.pack(I, len(data)) file_size len(data) # 修正签名 (SHA-1 hash of all bytes after signature field) data[12:32] b\x00 * 20 signature hashlib.sha1(data[32:]).digest() data[12:32] signature # 修正checksum (Adler32 over entire file) data[8:12] b\x00 * 4 checksum zlib.adler32(data) data[8:12] struct.pack(I, checksum) with open(file_path.replace(.dex, _fixed.dex), wb) as f: f.write(data) print(f[] Fixed: {file_path}) if __name__ __main__: for path in sys.argv[1:]: fix_dex(path)修复的核心逻辑有三步修正file_size字段dex头部偏移为32字节处存放文件大小以data[32:36]读取。如果实际读取大小和头部记录不一致统一为实际大小。修正signaturedex头部偏移12字节处是20字节的SHA-1签名它计算的是从第32字节开始到文件末尾的哈希值。所以先把原签名清零再重新计算。修正checksum偏移8字节处是4字节Adler32校验值计算范围为整个文件。同样先清零再计算。这三步做完修复后的dex就可以直接用jadx、GDA或者baksmali打开反编译了。3.4 使用frida-unpack脱壳的完整流程演示将整个流程串起来我惯用的操作顺序如下准备阶段安装App到设备确保能正常打开。跑一次frida-ps -U确认frida-server连接正常、能列出目标包名。启动脚本python3 unpack_main.py冷启动触发通过adb shell am force-stop 目标包名杀掉进程然后手动点击App图标启动观察控制台是否输出dump信息。热启动与页面遍历脚本保持挂机状态在App内多点点几个页面尤其是有登录、支付、列表刷新等功能的页面这些地方往往隐藏着动态加载的dex或加固分包。拉取dump文件adb pull /data/local/tmp/ 本地保存路径/批量修复python3 fix_dex.py dump_*.dex反编译验证用jadx打开修复后的dex文件确认能正常解析。实际操作中一个常规加固App通常在冷启动阶段能dump到13个dex其中主dex是业务代码其余可能是壳的壳dex或空壳dex。判断哪个是真正业务dex可以看文件大小——动辄几十MB的是业务dex几KB的是壳类。4. 常见问题与排查技巧实录4.1 高频问题速查我把自己和身边人用frida-unpack时踩过的坑整理了一张速查表按出现频率排序问题现象可能原因解决方案Unable to attach: process not found包名错误或App未启动frida-ps -U查看准确包名先启动App再attachHook注册成功但无dump输出壳开放在其他进程检查是否有子进程尝试frida-trace排查加载函数dump到大小异常如0字节hook点不对或时机过早尝试hook其他ART函数延长脚本等待时间dex打开报checkSum mismatch修复脚本未执行或修复有误确认修复脚本正确读取dex头先验证魔数设备端崩溃/重启frida-server被检测或版本不兼容更换frida版本或者用-l 0.0.0.0避免默认端口特征dump出来的dex反编译后内容极少那可能不是业务dex检查大小或者尝试内存搜索dex\n魔数手动dump4.2 三种脱壳失败场景的深度排查场景一App有反Frida检测现在市面上的加固方案几乎都内置了反调试、反Frida。比较常见的手段是检测/data/local/tmp/frida-server、检测端口27042、检测/proc/self/maps中是否有frida相关so以及hook某些Java方法来探测。排查思路先用frida-ps -U确认注入层是否能正常工作如果App直接闪退大概率踩上了检测。此时可以考虑临时改frida-server的文件名或者用frida-gadget注入方案把frida库打包进App内替换原so。更进阶的做法是hook掉检测函数的返回值——但这类操作比较复杂一句两句说不清建议先从改文件名和换非默认端口入手。场景二壳动态加载时机晚有些壳的dex不是冷启动就加载而是在某个业务模块首次使用时才解密。比如一个游戏App主界面没问题点进战斗场景才dump到新dex。遇到这种情况不要只等着。我习惯的做法是脚本挂机的同时用adb模拟点击或monkey遍历页面。也可以配合frida-trace先跟踪一下ClassLoader的加载路径确认业务代码到底在什么时机被加载。经验搞不定脱壳时机时先别慌着写复杂hook。先在App里把能点的页面全点一遍把登录、切换账号、刷新列表这几个动作做完往往就有收获。场景三修复后反编译报错几天前我在分析某社交App时dump出来一个30MB的大dex修复后jadx能打开但反编译到某个类直接报“class not found”之类错误。后来查明原因是壳在加载dex时对部分class_def做了延迟初始化运行到对应类时才在内存中patch。这种情况比较棘手简单的dump修复顶不住。建议方案有两个一是用frida-dexdump这类带内存扫描功能的工具主动从堆上搜dex\n魔数并dump二是在App运行过程中使用ART的DexFile数量统计确认是否有多个dex叠加。再有就是针对特定壳去买商业脱壳机或者定制ROM纯Frida路线可能需要接受“能dump大部分但个别类残缺”的现实。4.3 几项提升脱壳成功率的独家细节分享几个我在实践中验证过的小细节常规教程不会写。第一多dump几次再修复。同一个App、同一个页面不同时间点dump出来的dex可能不一样。我遇到过壳在第一次启动时加载了完整业务dex第二次启动时就改成动态加载分包的情况。所以脱壳不要只跑一次多跑几轮把不同状态下的dump文件都存下来再做修复。第二hook点不要只盯libart.so。有些加固方案的dex加载并不走标准的ART接口而是绕过ART自己用C内存映射方式加载。碰到这种就需要hookmmap、memcpy这些底层函数或者干脆用Frida的Memory.scan对整个进程内存扫魔数。Frida的Memory.scan在大范围内存扫描时速度并不快但针对性扫描某个模块区间时表现还不错。第三学会给dump脚本做“节流”。如果dump太频繁设备IO和内存压力会变大导致App卡死甚至被壳检测。我一般在dump函数里加个简单同步锁每dump一个文件后sleep一小段。牺牲一点速度换来稳定性。第四关注OatDexFile。Android 9以上ART更倾向于把dex转成oatAOT编译。有些壳会先把dex编译成oat再执行这种情况下直接dump内存dex可能拿到的是oat文件而非dex。你需要先用oatdump工具把oat转回dex或者hook住脱壳更上游的LoadDexFile接口在oat编译之前拿到原始dex。5. 进阶思路从一次性脱壳到自动化分析如果你已经把Frida-unpack跑顺了下一步自然是想把整套流程自动化。这个方向值得投入因为现代App更新频繁每次更新壳都可能变化人工跑脱壳效率太低。我的自动化方案是用frida-server常驻设备通过Python脚本批量管理目标App。准备一个手机集群几台不同Android版本的机器装好同一个样本的不同版本。每次新样本进来先冷启动脱壳dump成功则修复并自动提交到分析平台。若dump失败自动记录hook日志、Capture调用栈供后续人工分析。这套流水线的核心价值不是省那几分钟而是帮你积累“壳行为特征库”——哪些壳在哪个版本、哪个时机加载dexhook哪个符号最优。这些经验数据多了之后新样本进来几乎可以秒判定走哪条脱壳路线。自动化过程中需要注意设备状态的监控。机器跑时间长了frida-server可能被系统杀掉、设备温度过高导致App自启失败这些都需要定时巡检。我在实践中加了两个简单的判断定期检查frida-ps -U的返回状态如果设备掉线就自动重连每次dump完对比文件大小是否异常异常则重试。6. 脱壳后的下一步反向利用与安全分析脱壳拿到dex只是第一步后面的事情才真正检验功力。大多数人的实际场景是恶意样本分析——从脱壳后的代码里找C2服务器地址、找敏感权限调用链、找加密算法实现。这时候Frida同样有用你可以在干净环境跑起脱壳后的样本hook掉关键的字符串比较、加解密函数把样本的行为链路完整画出来。另一种场景是做自家App的安全自查。开发团队想知道自己的加固方案是否能扛住市面上的通用脱壳工具会专门用Frida-unpack打一遍自己的App。脱壳成功并不意味着绝对不安全指标是脱壳后你能在多短时间内定位到核心算法。比如一个金融App如果脱壳后反编译能找到登录加密逻辑的入口说明加固力度不够如果找到了加密函数但核心密钥藏在Native层说明加固是有层次的。这里我想强调的是脱壳技术没有善恶之分关键看用途。安全研究、漏洞挖掘、合规审计都是正当方向拿到脱壳能力后更应该用于发现和修复问题而不是去做不该做的事。最后一个实际建议把每次脱壳的样本、dump记录、修复脚本版本都留档。你会遇到一个App脱壳成功后更新两个版本又脱不出来的情况——这时候翻旧档对照上一次用的hook点和修复脚本能省下大量返工时间。工具会过时但你的排查流程和经验积累不会过时。这些拿时间堆出来的判断力才是比任何脚本都值钱的东西。