三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

CTF实战:Frida+DexDump动态脱壳破解安卓加固

CTF实战:Frida+DexDump动态脱壳破解安卓加固

1. 项目概述:从一道CTF赛题看安卓动态脱壳的核心价值

最近在复盘去年安恒杯春季赛的一道安卓逆向题“shield”,这道题可以说把安卓加固和动态脱壳的核心攻防点展现得淋漓尽致。题目本身是一个经过商业加固保护的APK,静态分析工具打开后,关键的classes.dex文件要么被加密、要么被隐藏,常规的静态反编译手段基本失效,看到的只是一些壳的引导代码。这正是目前移动应用安全,尤其是CTF竞赛和实际安全评估中越来越常见的场景——静态防御越来越强,动态攻防成为突破口。

这道“shield”题目的核心,就是要求我们绕过加固保护,从运行中的应用内存里,把原始的、已解密的DEX文件给“掏”出来,也就是所谓的“脱壳”。而完成这个任务,我选择并最终验证有效的“黄金搭档”就是FridaDexDump。Frida这个动态插桩框架,让我们能够像手术刀一样精准地切入到目标应用的运行时进程,而DexDump则是专门为Frida打造的一把“内存提取器”,它能识别并导出内存中完整的DEX结构。这个过程不仅仅是点几下鼠标,它涉及到对安卓ART/Dalvik虚拟机内存管理机制的理解、对加固壳加载时机的把握,以及一套稳定的动态分析环境搭建。

如果你正在学习安卓逆向,或者对CTF中的移动安全题目感到头疼,觉得加固像一堵密不透风的墙,那么这次通过“shield”实战总结出的Frida-DexDump动态脱壳与源码还原全流程,或许能给你提供一个清晰的破局思路。它不仅适用于CTF解题,其原理和方法同样适用于对加固应用进行安全研究、漏洞挖掘的场景。接下来,我就把这套从环境准备、脱壳操作到源码还原分析的完整经验,毫无保留地拆解给你看。

2. 核心思路与工具选型:为什么是Frida+DexDump?

面对一个被加固的应用,我们首先要理解对手是怎么工作的。现代安卓加固技术(俗称“壳”)的核心思路大同小异:在原始APK的外层包裹一层自定义的代码(壳代码)。当应用启动时,首先执行的是这层壳代码。壳代码的责任是负责在内存中解密被加密或混淆的原始DEX文件,然后通过动态加载技术(如DexClassLoader)将其加载到虚拟机中执行,同时尽可能抹去磁盘和内存中的解密痕迹。这就导致我们直接解压APK得到的classes.dex是无效的,静态分析工具(如Jadx、GDA)看到的是壳的逻辑。

因此,脱壳的突破口就在于“运行时内存”。既然壳最终要把原始代码交给虚拟机执行,那么在某个时刻,解密后的、完整的DEX字节码必然以某种结构存在于应用进程的内存空间中。我们的目标就是抓住这个时机,把这个内存镜像完整地提取出来。基于这个思路,动态脱壳工具应运而生,而FridaDexDump的组合,因其灵活性和针对性,成为了当前最主流的手动脱壳方案之一。

2.1 Frida:动态分析的“瑞士军刀”

Frida不是一个专门的脱壳工具,而是一个强大的动态代码插桩框架。它允许你将一段JavaScript(或Python)代码注入到目标进程(无论是本地还是远程)中,从而能够Hook函数、调用方法、读写内存,甚至修改逻辑。在脱壳场景下,Frida的核心价值在于:

  1. 进程附着与控制:我们可以将Frida的Agent注入到目标安卓应用的进程中,获得对其运行时状态的完全控制能力。
  2. 精准Hook关键点:我们可以编写脚本,Hook住壳在解密和加载DEX时必经的Android系统API或自定义函数。例如,Hookdalvik.system.DexClassLoaderjava.lang.ClassLoaderloadClass方法,在关键逻辑执行前后进行拦截和内存扫描。
  3. 内存操作能力:Frida提供了强大的内存读写API(如Memory.scan,Memory.readByteArray),使得我们能够主动搜索和提取内存中的特定数据块(如DEX文件头dex\n035\0)。

选择Frida而不是其他工具(如Xposed)的原因在于其跨平台(支持Android/iOS/Windows/macOS/Linux)、脚本化(无需重启设备或应用)以及对Native层和Java层无差别的Hook能力,这为应对复杂的、涉及Native代码的加固壳提供了可能。

2.2 DexDump:专为Frida打造的内存DEX提取器

理论上,只用Frida的API,我们通过扫描内存、识别DEX头、计算长度也能把DEX文件抠出来。但这个过程繁琐且容易出错,尤其是面对多DEX或内存中有多个DEX副本的情况。DexDump的出现,完美地解决了这个痛点。

DexDump本质上是一个开源的Frida脚本(通常是一个.js文件)。它的工作原理非常聪明:

  1. 遍历内存映射:它利用Frida的Process.enumerateRangesAPI,获取目标进程所有的内存区域(ranges)。
  2. 智能识别DEX结构:它不仅仅搜索dex\n035dex\n038这样的文件头魔数。一个有效的DEX在内存中不仅有文件头,其内部结构(如字符串池、类型池、方法池)的偏移和索引也必须自洽。DexDump会进行初步的校验,过滤掉那些只是偶然出现魔数但实为无效数据的区域。
  3. 重建与导出:对于识别出的、可能是有效DEX的内存区域,DexDump会尝试按照DEX文件格式将其数据完整地拷贝出来,保存为本地文件(如dump_0xXXXX.dex)。

在“shield”这道题中,直接使用DexDump脚本,往往就能在应用启动后的几秒内,自动完成对内存中所有潜在DEX的扫描和导出,极大提高了脱壳效率。它的优势在于“开箱即用”,省去了手动编写复杂内存扫描和校验逻辑的麻烦。

2.3 环境搭配与选型考量

在实际操作中,我通常使用以下环境组合:

  • 测试设备:首选安卓模拟器(如雷电模拟器、夜神模拟器)。原因在于模拟器环境纯净、可快照恢复、易于Root,且屏幕录制和文件传输方便。对于“shield”这类CTF题目,模拟器完全够用。当然,真机(需Root)也是可以的。
  • Frida环境:在电脑(攻击机)上安装Frida的Python包(pip install frida-tools),同时需要将对应版本的frida-server推送到安卓设备(模拟器)中并运行。这里有一个关键点:Frida客户端与Server的版本必须严格匹配,否则会出现连接失败或协议错误。
  • DexDump脚本:从GitHub获取最新版的dexdump.js

这个组合的选型逻辑很清晰:Frida提供底层能力和入口,DexDump提供针对性的、高层的自动化操作。两者结合,形成了一个从侵入到提取的完整管道。对于初学者,理解这个分工协作的关系,比死记硬背命令更重要。

3. 实战环境搭建与前期准备

理论清晰了,接下来就是动手搭建一个稳定的“作战平台”。这个过程有些繁琐,但每一步的稳定性都直接关系到后续脱壳能否成功。我会以在Windows 11系统下,使用雷电模拟器(Android 9)为例,详细演示整个环境搭建过程。

3.1 模拟器配置与Root

  1. 安装与设置模拟器:下载并安装雷电模拟器。安装完成后,进入其设置界面。最关键的一步是开启Root权限。在雷电模拟器的“系统设置”或“属性设置”中,通常有明确的“Root开关”,将其打开。然后重启模拟器。
  2. 验证Root:重启后,安装一个终端应用(如Termux)或者通过ADB连接后执行adb shell,然后输入su命令。如果命令提示符从$变成了#,并且没有弹出权限拒绝的提示,说明Root成功。
  3. 调整网络(可选但重要):为了让电脑和模拟器处于同一网络便于通信,建议将模拟器的网络设置从默认的“桥接”或“NAT”改为“网络桥接模式”,并指定一个与电脑主机在同一网段的静态IP,或者确保电脑能通过ADB可靠连接。更简单通用的方法是直接使用ADB。

3.2 Frida环境部署:客户端与Server端

这是最容易出错的一环,务必仔细。

  1. 电脑端安装Frida:在电脑的命令行(CMD或PowerShell)中,执行pip install frida-tools。这会同时安装fridafrida-tools(包含frida-psfrida等命令行工具)。安装完成后,用frida --version检查版本,例如输出16.1.4
  2. 获取匹配的frida-server:访问Frida的GitHub Release页面。根据你模拟器或真机的CPU架构(通常安卓模拟器是x86或x86_64,真机多是arm或arm64)以及上一步查到的Frida版本号,下载对应的frida-server-xx.x.x-android-x.x.x.xz文件。对于雷电模拟器Android 9,通常下载x86_64架构的版本。
  3. 推送并运行frida-server
    • 解压下载的.xz文件,得到一个名为frida-server-xx.x.x-android-x86_64的可执行文件。
    • 使用ADB命令将其推送到模拟器的/data/local/tmp目录,并赋予执行权限。
    adb push frida-server-xx.x.x-android-x86_64 /data/local/tmp/frida-server adb shell su cd /data/local/tmp chmod 755 frida-server
    • 在模拟器的/data/local/tmp目录下,以后台方式运行frida-server:./frida-server &。注意观察命令行,如果没有报错且光标停留在新的一行,通常表示启动成功。
  4. 验证连接:新开一个电脑端的命令行窗口,执行frida-ps -U。这个命令的意思是列出通过USB(-U)连接的设备上的所有进程。如果一切正常,你会看到一个长长的进程列表,其中包含com.android.settingscom.tencent.mm(如果你装了微信)等。看到这个列表,就证明Frida环境打通了

注意:每次重启模拟器后,frida-server进程会关闭,需要重新进入/data/local/tmp目录执行./frida-server &来启动。你可以编写一个简单的脚本来自动化这个过程。

3.3 目标应用与脱壳脚本准备

  1. 安装目标APK:将“shield”题目的APK文件(假设名为shield.apk)拖入模拟器界面安装,或者使用adb install shield.apk命令安装。
  2. 获取DexDump脚本:从GitHub(例如hluwa等作者维护的仓库)下载dexdump.js脚本,保存到电脑的某个目录,例如D:\CTF\Tools\dexdump.js
  3. 初步静态分析(可选):用解压软件打开shield.apk,查看lib目录下有无可疑的so文件(可能是加固壳的Native组件),用文本编辑器打开AndroidManifest.xml查看入口Activity。这一步不是为了破解,而是为了对目标有个基本认知,比如入口是com.xxx.xxx.MainActivity

环境至此准备完毕。总结一下关键检查点:模拟器Root成功、frida-server在设备上运行、frida-ps -U能列出进程、目标应用已安装、DexDump脚本在手。

4. 动态脱壳操作全流程实录

环境就绪,真正的“狩猎”开始。动态脱壳的核心在于时机,我们要在壳完成解密、DEX已加载到内存但尚未被虚拟机完全优化或销毁之前,完成内存抓取。下面是最常用的两种方法,我会结合“shield”题目进行说明。

4.1 方法一:应用启动时附着并脱壳(通用方法)

这是最直接、最常用的方法,适用于大多数在应用启动初期就完成解密加载的壳。

  1. 启动Frida并附着目标应用:我们使用frida -U命令在应用启动时注入我们的脚本。命令格式如下:

    frida -U -f com.anheng.shield -l D:\CTF\Tools\dexdump.js --no-pause
    • -U: 使用USB连接设备。
    • -f com.anheng.shield:-f后面跟的是目标应用的包名(假设为com.anheng.shield),这个参数会让Frida先启动这个应用。
    • -l D:\CTF\Tools\dexdump.js:-l指定要加载的JavaScript脚本,这里就是我们的DexDump脚本。
    • --no-pause: 立即启动应用,不要暂停。对于脱壳来说,我们需要应用尽快跑起来,让壳代码执行。
  2. 观察脚本输出与交互:执行上述命令后,Frida会启动目标应用,并将dexdump.js脚本注入进去。脚本会自动开始工作。你会在命令行中看到大量的输出信息,这是DexDump在遍历内存区域、识别和导出DEX。

    [雷电模拟器::com.anheng.shield]-> [DEXDump] Found target [com.anheng.shield] (pid: 12345) [DEXDump] Enumerate memory ranges... [DEXDump] Filter range: 0x1000 - 0x2000 (r-x) [anon:linker_alloc] [DEXDump] Filter range: 0x7fe1234000 - 0x7fe1255000 (rw-) [anon:libc_malloc] [DEXDump] Dump dex from range: 0x7fe1234000 - 0x7fe1255000, size: 0x21000 [DEXDump] Dex size: 0x1F800, Save to: dump_0x7fe1234000.dex [DEXDump] Dump dex from range: 0x7ff5678000 - 0x7ff569a000, size: 0x22000 [DEXDump] Dex size: 0x20800, Save to: dump_0x7ff5678000.dex

    输出中会显示它过滤了哪些无关的内存区域,以及在哪个内存地址范围发现了可能是DEX的数据,并将其保存为文件。这些dump_xxxxx.dex文件默认会保存在运行Frida命令的当前目录下。

  3. 手动触发关键逻辑(如果需要):有些壳可能不会在应用一启动就解密所有代码,而是等到用户点击某个按钮、进入某个界面时才动态加载。如果启动时脱壳得到的DEX不完整或不是核心逻辑,你需要让应用运行起来,手动操作到那个关键界面,然后在Frida会话仍然连接的情况下,在命令行中手动执行DexDump脚本中的扫描函数(如果脚本提供了相应的导出函数,例如scandex())。或者,更简单的方法是,在应用进入关键界面后,按Ctrl+C中断当前Frida会话,然后重新执行附着命令(但不用-f参数,而是用-n附着到正在运行的进程),再次运行脱壳脚本。

4.2 方法二:Hook关键类加载函数(精准打击)

对于某些狡猾的壳,或者你想更深入地理解脱壳过程,可以尝试手动编写Frida脚本,Hook特定的类加载函数。这种方法更有针对性,但需要一些编程知识。

  1. 编写简易Hook脚本:创建一个新的JS文件,比如hook_dexload.js
    Java.perform(function() { // Hook java.lang.ClassLoader 的 loadClass 方法 var classLoader = Java.use(\"java.lang.ClassLoader\"); classLoader.loadClass.overload('java.lang.String').implementation = function(name) { console.log(\"[*] ClassLoader.loadClass called for: \" + name); // 在这里可以调用DexDump的逻辑,或者直接进行内存扫描 // 例如,可以调用一个全局的dump函数 if (name.indexOf(\"com.anheng.shield\") !== -1) { // 过滤特定包名 console.log(\"[!] Target class loading triggered, dumping memory...\"); // 这里可以嵌入或调用DexDump的核心扫描代码 } return this.loadClass(name); // 继续执行原方法 }; // 也可以Hook DexClassLoader 或 PathClassLoader 的构造函数 var dexClassLoader = Java.use(\"dalvik.system.DexClassLoader\"); dexClassLoader.$init.overload('java.lang.String', 'java.lang.String', 'java.lang.String', 'java.lang.ClassLoader').implementation = function(dexPath, optimizedDirectory, librarySearchPath, parent) { console.log(\"[*] DexClassLoader created for path: \" + dexPath); console.log(\"[*] This is a strong indicator of dynamic loading! Time to dump!\"); // 立即执行内存dump // ... 调用dump逻辑 ... return this.$init(dexPath, optimizedDirectory, librarySearchPath, parent); }; });
  2. 结合DexDump功能:上面的脚本只是打印日志。为了真正脱壳,你需要将DexDump脚本中扫描和保存DEX的核心函数(通常是一个名为dumpscan的函数)整合进来,或者在Hook到关键点后,直接执行外部DexDump脚本。一种更实用的方式是:先运行dexdump.js脚本,它通常会导出一个全局函数(如scandex)。然后在你的Hook脚本中,在合适的时机(如loadClass被调用时)通过setTimeout或直接调用的方式,触发这个全局函数。

在“shield”的实战中,使用方法一(直接运行dexdump.js)通常就足够了。当应用启动后,脚本会自动跑完,在当前目录生成若干个.dex文件。你需要做的就是从这些文件中找到那个正确的、包含主要业务逻辑的DEX

4.3 脱壳后的文件筛选与初步验证

DexDump可能会生成多个dex文件,因为内存中可能存在系统库的dex、壳自身的dex、以及多个业务dex。如何找到我们想要的?

  1. 看大小:壳的dex通常较小(几十到几百KB),而主要业务dex会比较大(可能几MB)。shield题目脱出来的主要dex可能在1MB以上。
  2. 用工具验证:最直接的方法是用反编译工具(如Jadx-gui)逐个打开这些dex文件。打开后,查看包结构。真正的业务dex,其包名层级会与目标应用相关(如com.anheng.shield),并且内部会有大量的自定义类和方法。而壳的dex或系统dex,包名通常是com.wrappercom.shellandroid.*java.*等。
  3. 定位关键代码:在Jadx中打开疑似正确的dex,搜索题目可能相关的字符串(如“flag”、“check”、“success”、“error”等),或者查看MainActivity之类的入口类。如果能找到清晰可读的业务逻辑代码,说明脱壳成功。

5. 源码还原分析与Flag获取

成功脱出正确的DEX文件,就像拿到了一个加密盒子的钥匙。接下来就是用这把钥匙打开盒子,解读里面的秘密。对于CTF题目,最终目的是找到Flag。

  1. 使用反编译工具静态分析:将筛选出的主DEX文件拖入Jadx-gui。Jadx会尝试将其反编译成尽可能接近原始Java代码的形式。浏览MainActivity或主要的逻辑处理类。
  2. 分析“shield”题目逻辑:在“shield”这道题中,经过脱壳和反编译,我们可能会发现核心的验证逻辑。例如,代码可能包含一个checkPassword函数,它将用户输入与一个经过复杂运算(可能是AES、DES、或自定义算法)的字符串进行比较。或者,Flag可能被拆分隐藏在资源文件、Native So库,或需要满足特定条件才能触发显示。
  3. 动态调试验证(可选但推荐):静态分析得出的结论有时需要动态验证。我们可以再次使用Frida,但这次是用于主动调用和调试。例如,如果我们静态分析发现一个getFlag()方法,可以直接写Frida脚本去调用它:
    Java.perform(function() { var MainActivity = Java.use(\"com.anheng.shield.MainActivity\"); // 假设getFlag是静态方法 var flag = MainActivity.getFlag(); console.log(\"[*] The Flag is: \" + flag); });
    或者,Hook某个判断函数,直接修改其返回值,让程序走入显示Flag的分支。
  4. 处理Native层加固(进阶):有些强壳会把关键逻辑放到Native层(C/C++代码编译的.so文件里)。如果反编译Java层后找不到核心逻辑,就需要分析lib目录下的.so文件。这时,可以使用IDA Pro、Ghidra等工具进行逆向分析,同时结合Frida去Hook so文件导出的JNI函数(如Java_com_anheng_shield_MainActivity_encrypt)或内部函数,来动态追踪数据流和逻辑。这属于更高级的范畴,但思路是一致的:动静结合,用Frida去验证静态分析的猜想。

在“shield”的实战中,通过上述步骤,最终在脱壳后的DEX里,通过Jadx分析MainActivity,发现了一个简单的字符串比较逻辑,将输入与一个硬编码的、经过Base64编码的字符串进行比较,解码后即可获得Flag。整个过程的关键突破口,就在于成功使用Frida+DexDump完成了动态脱壳,让被隐藏的代码“原形毕露”。

6. 常见问题、排查技巧与避坑指南

在实际操作中,你几乎一定会遇到各种问题。下面是我踩过坑后总结的一些常见问题及解决方案,希望能帮你节省大量时间。

6.1 Frida连接与注入失败

  • 问题:执行frida-ps -U提示Failed to enumerate processes: unable to connect to remote frida-server
  • 排查
    1. 检查设备连接adb devices确认设备已连接。
    2. 检查frida-server:在设备shell中执行ps | grep frida-server,看进程是否存在。如果不存在,回到/data/local/tmp目录重新启动。
    3. 检查端口冲突:frida-server默认使用TCP端口27042。确保该端口没有被占用。可以尝试在设备上用netstat -tulpn | grep 27042查看。
    4. 检查版本匹配:这是最常见的问题!务必用frida --versionfrida-server --version(在设备上运行./frida-server --version)检查两端版本号是否完全一致。
    5. 关闭冲突软件:某些电脑安全软件或防火墙可能会拦截ADB或Frida通信,尝试暂时关闭。

6.2 DexDump脚本运行无输出或找不到DEX

  • 问题:脚本执行了,但输出很少,也没有生成dump文件,或者生成的dex用Jadx打开是空的或错误的。
  • 排查
    1. 时机不对:壳可能还没有完成解密加载。尝试让应用完全启动,并手动操作到核心界面后再运行脚本,或者使用setTimeout延迟执行脚本中的扫描函数。
    2. 内存搜索范围或特征问题:有些壳会修改DEX文件头魔数,或者将DEX结构打散。可以尝试使用更新版或修改版的DexDump脚本,它们可能包含了更多的特征码或更智能的搜索算法。也可以手动编写Frida脚本,搜索dex\n035dex\n038或壳可能使用的自定义特征。
    3. 应用有反调试/反Frida检测:这是高级壳的常见手段。应用会检测Frida的存在(如检测端口、进程名、文件特征等),如果发现则退出或执行垃圾代码。对策包括:
      • 使用隐蔽模式:Frida有--enable-soft-ptrace等参数,或使用frida-server的改名版本。
      • Patch反检测代码:静态分析找到检测点,用Frida Hook并绕过。
      • 使用其他工具辅助:如objection(基于Frida)的anti-anti-frida插件。

6.3 脱出的DEX反编译后代码混乱或不全

  • 问题:Jadx打开dex后,看到很多类名是乱码,或者方法体是空的、只有return语句。
  • 原因与对策
    1. 抽取型加固:这是目前最棘手的加固方式之一。壳并不完整加密整个DEX,而是在运行时动态地“抽取”每个方法的字节码,只在执行前才还原。内存中dump下来的DEX,其方法体(CodeItem)可能是空的或被填充了无意义指令。对付这种壳,需要在每个方法被解释执行或JIT编译的瞬间去Hook并dump其完整的CodeItem。这需要更精细的Frida脚本,或者使用专门的工具如FRIDA-DEXDump的增强版、Youpk等。
    2. 字符串加密:类名、方法名、字符串常量被加密。脱壳只解决了代码结构问题,但字符串内容仍是密文。这需要进一步分析解密函数,并用Frida动态执行解密逻辑,或者写IDAPython脚本在静态分析时进行解密。
    3. DEX优化格式:ART虚拟机下,DEX可能被优化成OAT或VDEX格式。DexDump有时能直接dump出DEX,有时dump出的是OAT,需要工具(如oat2dex)进行转换。

6.4 性能与稳定性问题

  • 内存占用过大:DexDump扫描整个内存空间可能比较慢,在低配设备上可能导致应用卡顿甚至崩溃。可以尝试在脚本中限制扫描的内存区域范围,或者选择在应用启动后、界面完全加载的相对空闲期进行dump。
  • 脚本崩溃:复杂的Frida脚本可能与目标应用或Frida自身版本存在兼容性问题。简化脚本逻辑,确保错误处理(try-catch),并更新到稳定的Frida版本。

最后的个人心得:安卓脱壳是一场与加固方案的“猫鼠游戏”。没有一成不变的银弹。Frida+DexDump这套组合拳,攻防的是2020-2022年间大部分主流加固的基础版本。面对不断升级的壳(尤其是VMP、深度抽取),需要更深入的理解和更定制化的工具。我的建议是,从“shield”这类基础题目入手,彻底吃透内存dump的原理,然后尝试分析一些带有简单反Frida检测的样本,学习如何绕过。当你能够熟练地运用Frida去Hook、修改、追踪内存中的关键数据时,你就已经掌握了动态分析最核心的武器,足以应对大部分CTF逆向题目和相当一部分商业应用的初步安全分析了。记住,思路比工具更重要,理解“为什么这么做”远比记住“怎么做”有价值。

← 返回列表