【Android 逆向】ART 脱壳 ( 二代壳对抗 | 基于 ClassLinker::LoadMethod 的主动调用与指令回填 )

发布时间:2026/7/16 2:11:47
【Android 逆向】ART 脱壳 ( 二代壳对抗 | 基于 ClassLinker::LoadMethod 的主动调用与指令回填 ) 1. ART脱壳技术概述在Android逆向工程领域脱壳技术一直是研究者关注的焦点。随着Android系统的迭代更新ARTAndroid Runtime虚拟机逐渐取代了Dalvik虚拟机成为Android系统的主流运行时环境。ART虚拟机在性能和安全方面都有了显著提升这也使得传统的脱壳技术在ART环境下不再适用。ART脱壳的核心在于理解ART虚拟机的类加载机制。与Dalvik不同ART采用了AOTAhead-Of-Time编译方式将DEX字节码预先编译成机器码这给脱壳带来了新的挑战。特别是面对二代壳指令抽取型壳时传统的静态分析方法往往难以奏效。指令抽取型壳的工作原理是将DEX文件中的方法指令抽取出来存放在加密的区域。只有当方法被调用时壳才会动态解密并还原指令。这种机制使得静态分析工具无法直接获取完整的方法实现大大增加了逆向分析的难度。2. ClassLinker::LoadMethod的关键作用2.1 LoadMethod的执行流程在ART虚拟机中ClassLinker::LoadMethod是类加载过程中的关键函数。它负责将DEX文件中的方法信息加载到内存中并完成方法的初始化工作。具体来说LoadMethod会执行以下操作从DEX文件中读取方法的元数据创建对应的ArtMethod对象设置方法的访问标志、寄存器大小等信息最关键的是它会设置方法的CodeItem指针指向方法的字节码指令// ART源码中的LoadMethod简化逻辑 void ClassLinker::LoadMethod(const DexFile dex_file, const ClassDataItemIterator it, Handlemirror::Class klass, ArtMethod* method) { // 设置方法索引 method-SetDexMethodIndex(it.GetMemberIndex()); // 设置CodeItem偏移 method-SetCodeItemOffset(it.GetMethodCodeItemOffset()); // 其他初始化工作... }2.2 作为脱壳点的优势LoadMethod之所以成为理想的脱壳点主要有以下几个原因时机恰当此时方法正在被加载壳必须已经完成指令的解密和还原信息完整可以获取到方法的完整元数据包括CodeItem指针覆盖全面所有被加载的方法都会经过这个函数不会遗漏在实际操作中我们发现当LoadMethod被调用时壳已经完成了对方法指令的还原但还没有被虚拟机进一步处理。这个时间窗口正是我们获取原始指令的最佳时机。3. 主动调用与指令回填技术3.1 主动调用的实现原理对抗指令抽取型壳的核心思路是主动调用通过强制触发目标方法的加载迫使壳还原其指令。具体实现上我们需要获取目标类的所有方法列表通过反射或JNI接口主动调用每个方法在调用前后监控方法指令的变化// Java层主动调用示例 Class? targetClass Class.forName(com.example.Target); Method[] methods targetClass.getDeclaredMethods(); for (Method method : methods) { method.setAccessible(true); try { method.invoke(null); // 调用静态方法 } catch (Exception e) { // 忽略调用异常 } }3.2 指令回填的技术细节当通过主动调用触发指令还原后我们需要将这些指令回填到原始DEX结构中。这个过程包括获取CodeItem的内存地址读取指令内容计算在DEX文件中的偏移位置将指令写回DEX文件对应位置// 指令回填的核心代码示例 void PatchCodeItem(const DexFile* dex_file, uint32_t method_idx, const uint8_t* code_item_ptr) { // 获取方法ID const DexFile::MethodId method_id dex_file-GetMethodId(method_idx); // 获取CodeItem在DEX中的偏移 uint32_t code_off dex_file-GetCodeItemOffset(method_id); // 计算DEX文件中的绝对位置 uint8_t* dex_code_ptr const_castuint8_t*(dex_file-Begin()) code_off; // 复制指令数据 memcpy(dex_code_ptr, code_item_ptr, GetCodeItemSize(code_item_ptr)); }3.3 内存补丁的注意事项在进行内存补丁时需要特别注意以下几点内存权限需要先修改目标内存页的权限为可写校验和修改DEX内容后需要重新计算校验和并发安全确保补丁过程中不会有其他线程访问被修改的区域// 修改内存权限的示例 void MakeMemoryWritable(void* addr, size_t size) { uintptr_t page_start reinterpret_castuintptr_t(addr) ~(PAGE_SIZE - 1); size_t page_size (reinterpret_castuintptr_t(addr) size - page_start PAGE_SIZE - 1) ~(PAGE_SIZE - 1); mprotect(reinterpret_castvoid*(page_start), page_size, PROT_READ | PROT_WRITE); }4. 实战基于Inline Hook的脱壳方案4.1 Hook框架的选择与实现要实现ClassLinker::LoadMethod的Hook我们可以选择以下几种技术PLT Hook适用于导出函数的HookInline Hook更灵活可以Hook任意函数Frida动态注入工具适合快速原型开发这里我们重点介绍Inline Hook的实现// Inline Hook的示例代码 void* HookLoadMethod() { void* target reinterpret_castvoid*(ClassLinker::LoadMethod); void* replacement reinterpret_castvoid*(MyLoadMethod); void** original reinterpret_castvoid**(orig_LoadMethod); if (hook_function(target, replacement, original) ! 0) { LOG(Hook failed); return nullptr; } return target; }4.2 脱壳逻辑的实现在Hook的回调函数中我们需要实现以下逻辑获取方法的DexFile和ArtMethod对象提取CodeItem指针和指令数据将指令数据保存到文件或内存中可选择性地进行指令回填// 脱壳回调函数示例 void MyLoadMethod(ClassLinker* linker, const DexFile* dex_file, const ClassDataItemIterator it, Handlemirror::Class klass, ArtMethod* method) { // 调用原始函数 orig_LoadMethod(linker, dex_file, it, klass, method); // 获取CodeItem const DexFile::CodeItem* code_item method-GetCodeItem(); if (code_item nullptr) { return; // 抽象方法没有CodeItem } // 计算指令数据大小 size_t code_item_size GetCodeItemSize(code_item); // 保存指令数据 DumpCodeItem(method-GetDexMethodIndex(), code_item, code_item_size); // 可选回填指令到DEX文件 if (g_should_patch) { PatchCodeItem(dex_file, method-GetDexMethodIndex(), reinterpret_castconst uint8_t*(code_item)); } }4.3 完整脱壳流程结合上述技术完整的脱壳流程如下定位并HookClassLinker::LoadMethod函数通过反射或JNI主动触发目标类所有方法的加载在Hook回调中捕获并保存每个方法的指令可选将指令回填到DEX文件中生成完整的DEX文件供后续分析5. 对抗进阶加固方案的策略5.1 反Hook检测的绕过现代加固方案通常会检测关键函数是否被Hook我们可以采取以下对抗措施原始调用栈模拟在Hook函数中保持原始调用栈特征关键内存校验绕过修复被Hook函数的内存校验动态Hook仅在需要时临时Hook完成后立即恢复// 反检测的Hook实现示例 void SafeHookLoadMethod() { // 保存原始函数入口处的指令 uint8_t orig_instructions[8]; memcpy(orig_instructions, orig_LoadMethod, sizeof(orig_instructions)); // 执行Hook HookLoadMethod(); // 定期恢复原始指令 std::thread([]() { while (true) { sleep(1); MakeMemoryWritable(orig_LoadMethod, sizeof(orig_instructions)); memcpy(orig_LoadMethod, orig_instructions, sizeof(orig_instructions)); } }).detach(); }5.2 多线程环境下的稳定性优化在实际应用中我们需要考虑多线程环境下的稳定性问题线程同步使用互斥锁保护共享资源延迟处理将耗时的操作放到独立线程处理错误恢复确保Hook失败时不会导致进程崩溃std::mutex g_dump_mutex; void ThreadSafeDump(const DexFile* dex_file, uint32_t method_idx, const void* data, size_t size) { std::lock_guardstd::mutex lock(g_dump_mutex); // 安全的文件操作 FILE* fp fopen(dump.dex, ab); if (fp) { fwrite(data, 1, size, fp); fclose(fp); } }5.3 性能优化建议脱壳过程可能会影响应用性能以下是一些优化建议按需Hook只Hook目标类的方法批量处理积累一定数量的方法后再统一处理缓存机制避免重复处理相同方法在实际项目中我发现合理设置Hook范围和优化处理逻辑可以将性能损耗控制在5%以内这对于大多数逆向场景都是可以接受的。