二代壳脱壳实战:函数抽取加固原理、Frida Hook抽取函数、DexHunter工具使用

各位同学,今天我们来啃一块硬骨头——二代壳(函数抽取壳)。说实话,我在刚入行那会儿,第一次遇到这种壳,整个人是懵的。Dump出来的Dex文件,打开一看全是空方法,代码逻辑不翼而飞。嗯,这就是抽取壳的“杰作”。

你想想看,一代壳只是把整个Dex加密存起来,运行时解密加载。但二代壳不一样,它更“聪明”——它把每个方法的真正代码抽走,只留一个空壳。运行时再动态填回去。这就导致你光Dump内存中的Dex文件,拿到的只是一堆空壳方法。

那怎么办?别急,今天我们就来彻底搞定它。

函数抽取加固的核心原理

说白了,函数抽取壳的核心就两步:

  1. 编译阶段:把Dex里每个方法的CodeItem(真正的字节码)抽走,存到另一个地方(可能是so文件,也可能是加密后的数据区)。
  2. 运行时阶段:当方法第一次被调用时,壳把抽走的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的步骤很简单:

  1. 下载DexHunter的源码(GitHub上就有)
  2. 编译成so文件
  3. 通过Xposed或Frida注入到目标App
  4. 运行App,触发各种功能
  5. 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流程图:二代壳脱壳核心逻辑

二代壳脱壳核心逻辑流程图 1. 识别壳类型 PKID / AppInfo 查壳 2. 触发方法还原 Frida主动调用 / UI操作 3. Dump完整Dex DexHunter / Frida脚本 三种核心脱壳方法 方法A:Frida主动调用 • 枚举所有类和方法 • 触发方法执行 • 捕获还原后的CodeItem 方法B:DexHunter工具 • Hook ArtMethod::Invoke • 自动捕获CodeItem • 输出完整Dex文件 方法C:内存Dump • 搜索Dex魔数 • Dump内存区域 • 修复Dex结构 最终输出:完整可反编译的Dex 三种方法可组合使用,提高脱壳成功率

常见问题与解决方案

在实际操作中,你可能会遇到各种问题。我把最常见的几个列出来:

  • 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