二代壳脱壳实战:函数抽取加固原理、Frida Hook抽取函数、DexHunter工具使用
各位同学,今天我们来啃一块硬骨头——二代壳(函数抽取壳)。说实话,我在刚入行那会儿,第一次遇到这种壳,整个人是懵的。Dump出来的Dex文件,打开一看全是空方法,代码逻辑不翼而飞。嗯,这就是抽取壳的“杰作”。
你想想看,一代壳只是把整个Dex加密存起来,运行时解密加载。但二代壳不一样,它更“聪明”——它把每个方法的真正代码抽走,只留一个空壳。运行时再动态填回去。这就导致你光Dump内存中的Dex文件,拿到的只是一堆空壳方法。
那怎么办?别急,今天我们就来彻底搞定它。
函数抽取加固的核心原理
说白了,函数抽取壳的核心就两步:
- 编译阶段:把Dex里每个方法的CodeItem(真正的字节码)抽走,存到另一个地方(可能是so文件,也可能是加密后的数据区)。
- 运行时阶段:当方法第一次被调用时,壳把抽走的CodeItem填回原来的方法结构体里,然后执行。
这里有个关键点——方法只有被调用时才会被还原。没被调用的方法,永远是空壳。我在项目中遇到过一款金融App,登录功能正常,但某个隐藏的调试接口一直报空指针。后来才发现,那个方法从来没被触发过,所以代码一直是空的。
核心数据结构:CodeItem
在Dex文件格式中,每个方法都有一个CodeItem结构体,里面存放着真正的Dalvik字节码。抽取壳就是把这个结构体里的insns(指令数组)清空或替换成占位指令。
Frida Hook抽取函数:主动触发还原
既然方法只有被调用时才还原,那我们的思路就很清晰了——主动调用所有方法,让壳被迫还原代码,然后我们再Dump内存。
我个人习惯用Frida来做这件事。为什么?因为Frida可以在运行时枚举所有类和方法,然后挨个调用。代码很简单,但效果拔群。
// Frida脚本:主动调用所有方法,触发抽取壳还原
Java.perform(function() {
var ClassLoader = Java.use('java.lang.ClassLoader');
var BaseDexClassLoader = Java.use('dalvik.system.BaseDexClassLoader');
// 获取当前App的ClassLoader
var currentClassLoader = ClassLoader.getSystemClassLoader();
// 枚举所有已加载的类
Java.enumerateLoadedClasses({
onMatch: function(className) {
try {
var targetClass = Java.use(className);
var methods = targetClass.class.getDeclaredMethods();
methods.forEach(function(method) {
try {
// 设置可访问
method.setAccessible(true);
// 尝试调用静态方法
if (java.lang.reflect.Modifier.isStatic(method.getModifiers())) {
method.invoke(null);
}
} catch(e) {
// 调用失败很正常,忽略即可
}
});
} catch(e) {
// 类加载失败,跳过
}
},
onComplete: function() {
console.log('[+] 所有类方法已尝试调用');
}
});
});
避坑指南:我曾经因为主动调用导致App崩溃了十几次。后来发现,有些方法不能随便调——比如初始化UI的方法、网络请求的方法。建议你只调用那些看起来是“数据处理”或“逻辑判断”的方法。或者,你可以先Hook住方法入口,只触发还原,不真正执行。
更好的做法是:Hook ArtMethod的入口,在方法被调用前Dump它的CodeItem。这样既触发了还原,又不会真正执行方法逻辑。
// 更优雅的方案:Hook方法解析过程
var ArtMethod = Module.findExportByName(null, '_ZN3art9ArtMethod14InvokeThreadIdEPNS_6ThreadEPj');
if (ArtMethod) {
Interceptor.attach(ArtMethod, {
onEnter: function(args) {
// args[1] 是ArtMethod指针
// 从这里读取CodeItem并Dump
console.log('[+] 方法被调用,准备Dump CodeItem');
}
});
}
DexHunter工具:专为抽取壳而生
手动写Frida脚本虽然灵活,但每次都要重复造轮子。这时候,DexHunter就派上用场了。这是我早期做脱壳时最常用的工具之一,专门针对函数抽取壳设计。
DexHunter的原理其实不复杂:它Hook了Android运行时中与方法解析和执行相关的关键函数。当壳把抽走的CodeItem填回方法结构体时,DexHunter就能捕获到这个瞬间,然后把完整的CodeItem保存下来。
DexHunter的核心Hook点:
ArtMethod::Invoke—— 方法被调用时触发ArtMethod::GetCodeItem—— 获取CodeItem时触发ClassLinker::LoadMethod—— 方法被加载时触发
使用DexHunter的步骤很简单:
- 下载DexHunter的源码(GitHub上就有)
- 编译成so文件
- 通过Xposed或Frida注入到目标App
- 运行App,触发各种功能
- DexHunter会自动Dump出完整的Dex文件
# 编译DexHunter
git clone https://github.com/你的仓库/DexHunter.git
cd DexHunter
ndk-build
# 注入到目标App(以Frida为例)
frida -U -l dexhunter_inject.js com.target.app
注意:DexHunter在Android 8.0以上的版本可能需要适配。因为Art运行时内部结构发生了变化。我曾在Android 9上踩过坑,DexHunter直接崩溃。后来自己改了源码里的偏移量才搞定。
实战流程:从Dump到修复
好了,理论讲完了,我们来走一遍完整的实战流程。这是我个人总结的一套标准操作:
| 步骤 | 操作 | 工具/方法 |
|---|---|---|
| 1 | 确定壳类型 | 查壳工具(PKID、AppInfo) |
| 2 | Dump内存Dex | Frida脚本或DexHunter |
| 3 | 触发方法还原 | 主动调用或UI操作 |
| 4 | 提取完整Dex | DexHunter自动输出 |
| 5 | 修复Dex结构 | 010 Editor + Dex模板 |
| 6 | 验证完整性 | jadx打开查看代码 |
这里我要特别强调第5步——修复Dex结构。很多时候,DexHunter虽然Dump出了CodeItem,但Dex文件的头部信息、偏移量可能已经损坏。你需要用010 Editor配合Dex模板来修复。
我曾经遇到一个案例:Dump出来的Dex有3000多个方法,但jadx只能解析出500个。后来发现是string_ids和type_ids的偏移量被壳篡改了。手动修正后,所有方法都恢复了。
SVG流程图:二代壳脱壳核心逻辑
常见问题与解决方案
在实际操作中,你可能会遇到各种问题。我把最常见的几个列出来:
- Dump出来的Dex打不开:大概率是Dex头部被篡改了。用010 Editor打开,对照标准Dex格式手动修复magic、checksum、signature等字段。
- 方法体是空的:说明壳还没还原这个方法。多触发一些功能,或者用Frida主动调用。
- App闪退:可能是Hook点选错了,或者主动调用触发了敏感操作。建议先Hook住异常,看看是哪行代码导致的。
- DexHunter没输出:检查一下Android版本。DexHunter在Android 7.0以下最稳定,高版本需要适配。
我的经验:如果你遇到一个特别顽固的壳,怎么都脱不下来。不妨试试“组合拳”——先用DexHunter跑一遍,再用Frida脚本补漏,最后手动修复Dex结构。三种方法配合,成功率能到90%以上。
好了,关于二代壳脱壳的内容就讲到这里。函数抽取壳虽然比一代壳难搞,但只要掌握了原理和工具,其实也就那么回事。记住核心思路:触发还原 → 捕获CodeItem → 修复Dex。下次遇到类似的壳,你心里就有底了。
公众号:蓝海资料掘金营,微信deep3321