FART+Frida动态脱壳:Android加固应用逆向分析的利器
1. 项目概述为什么我们需要“FARTFrida”在移动安全分析尤其是Android应用逆向的日常工作中脱壳一直是个绕不开的“硬骨头”。你肯定遇到过这种情况拿到一个App用静态分析工具打开一看核心的DEX文件要么被加密得面目全非要么干脆就是个空壳真正的逻辑代码在运行时才会被动态加载。这时候传统的静态分析工具就束手无策了你必须得让App跑起来在内存里“抓住”那个解密后的、活生生的DEX。FARTFix ART就是为解决这个问题而生的一个经典工具它通过修改Android运行时ART在应用执行时自动将内存中的DEX文件“吐”出来实现脱壳。但FART本身更像一个“沉默的捕手”它把DEX文件写到了设备的/data/data/包名目录下你需要手动去拉取而且对于更复杂的、多层壳或者有反调试、反脱壳检测的应用FART有时会显得力不从心。这就是Frida登场的时候。Frida是一个动态插桩框架它允许你将自己的JavaScript脚本注入到目标进程的内存中去Hook函数、修改逻辑、甚至实时与内存交互。如果把FART比作一个设置好的捕兽夹那Frida就是一位手持万能钥匙和探测器的特工。将两者结合我们就能打造一个更强大、更灵活、更智能的动态脱壳分析环境。Frida可以帮我们绕过一些简单的反调试可以在关键的解密函数被调用时发出通知甚至可以实时修改内存数据辅助FART更精准、更完整地捕获到目标DEX。这个组合拳能让分析效率提升好几个档次尤其适合对付那些“狡猾”的加固应用。2. 环境搭建与工具选型解析2.1 核心组件FART与Frida的版本匹配工欲善其事必先利其器。搭建环境的第一步是确保核心组件的兼容性。FART本身不是一个独立App它是一套需要编译进Android系统镜像通常是AOSP的补丁。这意味着你需要一个已经集成了FART的Android系统环境。对于大多数研究者最实际的选择是使用一台已经Root的Android真机并刷入集成了FART的定制ROM例如基于Android 8.1或9.0的版本。网上有一些热心开发者编译好的镜像你可以根据自己手头的设备型号去寻找。这里有个关键点FART的版本或者说其适配的Android API级别需要与你使用的Frida-server版本大致匹配。虽然不要求绝对一致但如果你在一个Android 9API 28的FART环境下强行运行一个为Android 12API 31编译的frida-server很可能会遇到兼容性问题导致崩溃。我的建议是优先确定你的FART环境所基于的Android版本。然后去Frida的官方GitHub Release页面下载对应架构通常是arm或arm64和对应Android版本的frida-server。例如你的设备是arm64-v8a架构Android 9.0那么就下载类似frida-server-16.1.4-android-arm64.xz这样的文件。版本号不必追求最新稳定兼容更重要。我个人的经验是选择一个比你的Android版本稍早但仍在活跃维护的Frida大版本如15.x, 16.x通常兼容性最好。2.2 辅助工具链准备除了FART环境和Frida一个顺畅的分析流程还需要其他工具辅助ADBAndroid Debug Bridge这是与设备通信的生命线。确保你的电脑上安装了最新版的Android SDK Platform-Tools并配置好环境变量。通过adb devices命令能正常识别到你的设备是第一步。Python环境与Frida-tools在电脑端你需要Python环境3.7并通过pip安装frida-toolspip install frida-tools。这为你提供了frida-ps、frida命令行工具等用于列出进程、注入脚本。代码编辑器与调试终端推荐使用VS Code或PyCharm来编写和管理你的Frida JavaScript脚本。同时准备至少两个终端窗口一个用于执行ADB命令另一个用于运行Python脚本或Frida CLI。逆向分析主力工具如JADX-GUI、Ghidra、IDA Pro等用于分析脱壳后得到的DEX或so文件。注意整个环境搭建过程请务必在合规合法的测试环境如自己的测试设备、授权的测试应用中进行。所有技术讨论仅限用于安全研究、学习交流目的。3. FART脱壳原理与基础操作复盘在引入Frida之前我们必须先吃透FART本身是怎么工作的。知其然更要知其所以然这样才能知道在哪个环节引入Frida能产生“化学反应”。3.1 FART的核心Hook点LoadMethod与DexFileFART的魔法主要施加在ART虚拟机的两个关键环节。ART虚拟机在加载和执行一个DEX文件中的方法时会经历DexFile的解析和LoadMethod的过程。FART的补丁正是在这些关键路径上插入了“钩子”。DexFile的Dump当ART加载一个DEX文件无论是主DEX还是动态加载的时FART的代码会介入将内存中已经解密、准备被虚拟机使用的DexFile结构体完整地拷贝出来写入到文件。这个文件通常以包名_类名.dex的格式命名保存在应用的数据目录。这是获取完整DEX的基础。LoadMethod的CodeItem Dump这是FART更精妙的一步。有些加固方案不会一次性解密整个DEX而是采用“方法级”的加密即用到某个方法时才解密该方法的字节码CodeItem。FART在ART的LoadMethod函数中插入逻辑每当一个方法被首次加载和执行时就将其对应的CodeItem包含具体的操作指令dump下来。这些零散的CodeItem后期可以通过FART提供的工具如fart进行合并重组还原出可被反编译工具识别的DEX。3.2 标准FART脱壳流程标准的、不结合Frida的FART脱壳流程是这样的刷机与启动将集成了FART的ROM刷入测试机并启动。安装目标应用通过adb install安装待脱壳的APK。运行应用触发脱壳启动目标应用并尽可能多地遍历其功能界面。这一步的目的是触发尽可能多的类和方法被加载让FART有机会dump下更多的CodeItem。对于简单的壳可能启动完主界面完整的DEX就已经被dump到/data/data/包名目录下了。提取脱壳文件应用运行一段时间后通过adb shell进入设备找到应用数据目录将里面生成的.dex和.binCodeItem文件拉取到电脑。adb shell su cd /data/data/com.target.app ls -la *.dex *.bin exit adb pull /data/data/com.target.app .合并与修复使用FART工具包里的fart工具或配套的Python脚本处理拉取的.bin文件将它们合并到对应的.dex中最终生成一个完整的、可被反编译的DEX文件。静态分析用JADX等工具打开修复后的DEX文件进行分析。这个流程在对付常规加固时是有效的但它被动且“笨拙”。如果应用有启动检测在FART完全生效前就崩溃了怎么办如果某些关键方法只有在特定分支条件下才会被加载而你在手动遍历时漏掉了呢这时我们就需要Frida来赋予这个流程“主动性”和“智能”。4. Frida赋能动态干预与精准脱壳Frida的介入可以从多个维度增强FART脱壳流程的鲁棒性和精准度。下面我们分场景来看。4.1 场景一绕过基础反调试与反脱壳检测许多加固会在应用启动初期进行反调试检测如果发现调试器包括Frida的注入行为或运行环境异常如检测到FART相关的痕迹就会主动退出或执行垃圾代码干扰。Frida可以在应用进程启动的极早期注入并Hook这些检测函数使其失效。例如常见的检测点包括android.os.Debug.isDebuggerConnected() 检测是否被调试。System.getProperty查询ro.debuggable等 检测系统是否可调试。遍历/proc/self/status或/proc/self/task查看TracerPid 检测是否有跟踪进程。我们可以编写一个Frida脚本在应用启动时立即注入并Hook这些函数Java.perform(function () { // Hook isDebuggerConnected固定返回false var Debug Java.use(android.os.Debug); Debug.isDebuggerConnected.implementation function () { console.log([反调试绕过] isDebuggerConnected() 被调用返回 false); return false; }; // Hook System.getProperty对特定查询返回“安全”值 var System Java.use(java.lang.System); var originalGetProperty System.getProperty.overload(java.lang.String); originalGetProperty.implementation function (key) { var result originalGetProperty.call(this, key); if (key ro.debuggable) { console.log([反调试绕过] 查询 ro.debuggable返回 0); return 0; } // 可以继续处理其他敏感属性 return result; }; });使用Frida命令注入这个脚本frida -U -f com.target.app -l anti-anti-debug.js --no-pause。-f表示启动应用--no-pause表示立即启动主线程这对于早期注入至关重要。这样就能为FART的脱壳过程创造一个“安全”的运行环境。4.2 场景二监控与触发关键解密流程有些高级壳并非在应用启动时一次性解密所有代码而是在运行时当某个类或方法被首次访问时才调用底层的Native函数进行解密。FART的LoadMethodHook虽然能捕获结果但如果我们能知道“解密发生在何时、何处”就能更主动地触发它。假设通过初步分析我们怀疑解密逻辑在Native层的一个叫JNI_OnLoad的函数里或者在一个名为OLLVM混淆过的decrypt_data函数中。我们可以用Frida的Interceptor来监控这些函数的调用。// 监控 Native 函数 Interceptor.attach(Module.findExportByName(libtarget.so, decrypt_data), { onEnter: function (args) { console.log([解密监控] decrypt_data 被调用); console.log(参数1 (数据地址): args[0]); console.log(参数2 (数据长度): args[1]); // 可以在这里打印堆栈看看是谁调用的解密 console.log(Thread.backtrace(this.context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join(\n)); }, onLeave: function (retval) { console.log([解密监控] decrypt_data 返回返回值: retval); // 返回后解密后的数据应该已经在内存中此时FART的LoadMethod Hook很可能被触发 } });更进一步我们可以主动调用某些Java方法来触发解密流程。例如发现应用有一个LicenseManager.check()方法调用后才会初始化核心模块。我们可以用Frida脚本在合适的时机主动调用它Java.perform(function () { setTimeout(function(){ // 延迟几秒等待应用基本初始化完成 Java.choose(com.target.app.LicenseManager, { onMatch: function(instance) { console.log([主动触发] 找到 LicenseManager 实例调用 check()); instance.check(); // 主动调用触发后续解密 }, onComplete: function() {} }); }, 5000); });4.3 场景三内存检索与DEX文件定位辅助有时候FART虽然dump了文件但我们可能想确认dump是否完整或者想直接内存中搜索DEX的魔术头dex\n035或dey\n035for cdex。Frida可以轻松扫描进程内存。function scanForDexInMemory() { console.log([内存扫描] 开始扫描 DEX 魔术头...); Process.enumerateRanges(r--).forEach(function (range) { // 只扫描可读的内存区域 var magic range.base.readCString(4); // 读取前4个字节 if (magic dex\n || magic dey\n) { console.log([内存扫描] 发现 DEX 区域地址: range.base , 大小: range.size); // 可以将这块内存直接dump到文件与FART dump的进行比对 var dexBytes range.base.readByteArray(range.size); var dexFilePath /data/local/tmp/dex_from_memory_ range.base .dex; var file new File(dexFilePath, wb); file.write(dexBytes); file.close(); console.log([内存扫描] DEX 已保存至: dexFilePath); } }); } // 在合适的时机调用比如在触发解密后 Java.perform(function () { setTimeout(scanForDexInMemory, 8000); });这个脚本能帮助我们发现那些可能被FART遗漏的、或者以非标准方式映射到内存中的DEX片段是FART脱壳结果的有效补充和验证。5. 一体化自吐Frida脚本设计与实现将上述能力整合我们可以设计一个“一体化”的Frida脚本。这个脚本的目标是一键注入后自动完成反调试绕过、监控解密、触发关键流程、扫描内存并在检测到DEX被加载时甚至可以尝试通过Frida直接调用FART的内置功能如果FART环境提供了JNI接口或者通知我们手动操作。5.1 脚本架构设计一个健壮的一体化脚本应该包含以下模块初始化与配置模块定义目标包名、关键函数签名、触发时机等。反检测模块实现常见的Java层和Native层反调试、反注入绕过。监控模块Java层HookClassLoader.loadClass、DexClassLoader构造函数等监控类加载行为。Native层Hookdlopen、dlsym以及疑似解密的函数。触发模块在监控到特定事件如某个类加载失败或定时器到期后主动调用特定的初始化方法。内存辅助模块定时或事件驱动地扫描内存中的DEX结构并可与FART输出进行比对。日志与输出模块将关键事件、内存地址、调用堆栈等格式化输出到控制台或文件便于分析。5.2 核心代码片段示例下面是一个高度整合的示例脚本框架// config var TARGET_PACKAGE com.target.app; var TRIGGER_CLASS com.target.app.Initializer; var TRIGGER_METHOD initCore; // 主函数 function main() { console.log([*] 开始注入目标应用: TARGET_PACKAGE); // 1. 反检测 antiDetection(); // 2. 监控类加载 monitorClassLoading(); // 3. 监控Native解密 monitorNativeDecrypt(); // 4. 延迟后尝试主动触发 setTimeout(activeTrigger, 3000); // 5. 定期扫描内存 setInterval(scanForDexInMemory, 10000); } function antiDetection() { Java.perform(function () { // ... 实现上述反调试Hook代码 ... console.log([] 基础反检测措施已部署); }); } function monitorClassLoading() { Java.perform(function () { var ClassLoader Java.use(java.lang.ClassLoader); ClassLoader.loadClass.overload(java.lang.String).implementation function (className) { // 过滤掉系统类减少日志噪音 if (!className.startsWith(android.) !className.startsWith(java.)) { console.log([类加载监控] 尝试加载: ${className}); } try { return this.loadClass.call(this, className); } catch (e) { console.log([类加载监控] 加载失败 ${className}: ${e}); // 这里可以加入触发逻辑比如当某个关键类加载失败时主动调用初始化 if (className TRIGGER_CLASS) { setTimeout(activeTrigger, 500); } throw e; } }; }); } function monitorNativeDecrypt() { // 遍历所有模块寻找可疑导出函数进行Hook Process.enumerateModules().forEach(function (module) { if (module.name.indexOf(libshield) ! -1 || module.name.indexOf(libprotect) ! -1) { console.log([] 发现可疑模块: ${module.name} (${module.base})); // 可以尝试Hook这个模块的所有导出函数或者通过模式匹配寻找解密函数 } }); // ... 具体的Interceptor.attach代码 ... } function activeTrigger() { Java.perform(function () { try { var Initializer Java.use(TRIGGER_CLASS); // 假设initCore是静态方法 console.log([主动触发] 尝试调用 ${TRIGGER_CLASS}.${TRIGGER_METHOD}()); Initializer[TRIGGER_METHOD](); } catch (e) { console.log([主动触发] 调用失败: ${e}); // 如果静态方法失败尝试寻找实例 Java.choose(TRIGGER_CLASS, { onMatch: function(instance) { console.log([主动触发] 找到实例调用方法); instance[TRIGGER_METHOD](); }, onComplete: function() {} }); } }); } // 执行主函数 setTimeout(main, 0);5.3 脚本使用与调试心得将上述脚本保存为unpacker.js。使用Frida注入时有几个实用技巧保持会话使用frida -U -f com.target.app -l unpacker.js并保持终端打开实时查看日志。脚本重载在Frida CLI中如果修改了脚本可以使用%load命令重新加载无需重启应用。崩溃诊断如果注入后应用立即崩溃可能是Hook点不对或脚本有误。可以尝试先注释掉所有Hook然后逐一启用定位问题点。Frida的-D参数可以输出更详细的设备端日志。性能考虑Hook太多函数尤其是高频函数如ClassLoader.loadClass可能会拖慢应用速度甚至导致超时或行为异常。在实际使用中要尽量精准Hook或者添加更严格的过滤条件。6. 实战问题排查与进阶技巧即便有了FART和Frida的组合实战中依然会遇到各种“妖魔鬼怪”。下面分享一些我踩过的坑和总结的技巧。6.1 Frida连接失败或进程崩溃这是最常见的问题之一。问题现象可能原因排查步骤与解决方案frida-ps -U无输出或报错1. USB调试未开启/未授权2. frida-server未运行3. 设备未Root或权限不足4. 端口冲突默认270421.adb devices确认设备在线并弹窗授权电脑。2.adb shell后su提权执行/data/local/tmp/frida-server 确保已push。3. 检查ps | grep frida确认进程存在。4. 尝试重启adbdadb kill-server adb start-server。注入时目标进程崩溃1. Frida版本与系统/应用不兼容。2. 脚本Hook了不稳定的早期函数。3. 应用有强力的反Frida检测。1. 更换Frida-server版本尝试更旧或更测试版。2. 使用--no-pause让应用先启动再注入或使用setTimeout延迟注入脚本主体。3. 加强反检测脚本或使用Frida的隐身模式如修改特征字符串但高级壳仍可能检测。出现Security Violation等提示应用集成了商业级加固检测到Frida运行环境。1.修改Frida特征这是猫鼠游戏。可以尝试使用开源工具如frida-skeleton或自行编译修改frida-gum库中的特征字符串。2.绕过时机尝试在应用完成检测之后再注入Frida。这需要精确计时或找到检测完成后的一个稳定Hook点。3.使用替代方案考虑使用ptrace或LD_PRELOAD等更底层的注入方式但这超出了本文范畴且复杂度极高。6.2 FART脱壳不完整或失败现象拉取到的目录下没有.dex文件或者只有很小的一个。排查首先检查应用是否真的启动了。查看logcat日志过滤FART相关的tag如ActivityThreadfart等看是否有错误信息。可能FART的补丁没有生效或者ROM刷写有问题。解决确保刷入的ROM确实包含FART并且版本匹配。对于某些加固可能需要更彻底地遍历应用。结合Frida你可以编写脚本自动点击、跳转页面比手动操作更高效。现象有.dex文件但用JADX打开后很多方法体是空的或显示“nop”。排查这说明FART只dump了DexFile结构但没有捕获到方法的CodeItem。这通常发生在“函数级”抽取壳上。你需要确保相关方法被执行过。解决这就是Frida大显身手的地方。用Frida脚本更全面地触发应用功能特别是那些隐藏在深层逻辑、特定条件分支下的方法。可以Hook一些UI控件的监听器模拟用户交互去触发。6.3 内存扫描与DEX修复的进阶操作定位动态加载的DEX有些壳不会通过标准的DexClassLoader加载而是直接调用dalvik.system.DexFile.loadDex或Native方法mmap一个文件。Frida可以Hook这些底层函数。var DexFile Java.use(dalvik.system.DexFile); DexFile.loadDex.implementation function (sourcePathName, outputPathName, flags) { console.log([Dex加载监控] loadDex被调用: ${sourcePathName}); var result this.loadDex.call(this, sourcePathName, outputPathName, flags); // 此时DEX文件可能已被解密并映射到内存 setTimeout(scanForDexInMemory, 1000); // 延迟扫描内存 return result; };手动修复DEX结构即使FART合并工具有时也可能失败。你需要了解DEX文件格式。使用010 Editor配合DEX模板可以手动分析和修复文件头、method_ids、class_defs等结构。这需要较强的耐心和基础知识。处理OAT或VDEX在高版本Android上可能会遇到OAT或VDEX格式。FART通常也支持dump出对应的.odex或.vdex文件但分析它们需要不同的工具链如oat2dex、vdexExtractor。Frida可以帮助你确认运行时加载的到底是哪种格式。6.4 保持环境稳定与可复现动态分析环境非常“娇气”。一次成功的脱壳后建议做好快照。对于模拟器使用VirtualBox或VMware的快照功能在干净FART环境配置好后保存一个状态。对于真机刷入稳定ROM后使用TWRP等Recovery做一个完整的系统备份NANDroid Backup。脚本管理将好用的Frida脚本进行版本管理如Git并写好注释。记录下成功脱壳某个版本应用时使用的脚本版本、Frida版本和ROM信息。打造“FARTFrida”的动态脱壳环境是一个从被动接受到主动干预的思维转变。FART提供了强大的底层捕获能力而Frida赋予了我们在应用运行时进行侦查、干预和控制的灵活性。两者结合不仅能提高脱壳的成功率更能让我们深入理解加固方案的工作原理。这个过程没有一成不变的银弹需要根据目标应用的特点灵活组合监控、触发、绕过等技术。最重要的永远是耐心、细致的观察和基于理解的尝试。当你亲手从一片混沌的内存中提取出清晰的Java代码时那种成就感就是驱动安全研究者不断向前的最大乐趣。