ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android APK解固脱壳实战:加固原理、工具选型与DEX修复

Android APK解固脱壳实战:加固原理、工具选型与DEX修复 做 App 安全分析或者移动端技术评估的人应该都遇到过这么个闹心场景费劲拿到一份 APK 样本拖进 jadx 一看核心业务类全是壳子入口指向一个叫com.xxxx.Application的类真正的逻辑影子都看不到。你要是手头有恶意 App、合规检查或者自家 App 安全测试的任务不把这一层壳去掉后边的代码分析根本没法往下做。这时候就需要走一趟解固脱壳的流程。所谓“解固”和“加固”是成对出现的。加固厂商把原始 DEX 文件加密、抽取、藏到各种奇怪的位置目的就是让静态工具看不到真实代码。而解固脱壳就是在授权分析的前提下把壳在运行时解密到内存里的那一份“真实的 DEX”抓出来让 Java 层的代码重新变回可读状态。这篇文章把我实际用过的几套脱壳思路、工具选型和踩坑经验完整整理一遍适合刚开始接触 App 安全分析、经常要处理带壳样本的朋友参考。1. 加固与脱壳的核心逻辑1.1 加固到底对 APK 做了什么要搞清楚脱壳从哪下手先得知道加固在安装包里动了哪些手脚。一个正常 APK核心代码都在classes.dex里用 jadx 打开就能直接看。加固器介入之后流程大致变成这样把原始classes.dex用某种算法加密或者干脆按照方法维度抽取变成一段不完整的“残缺代码”。生成一个新的、专门负责解密的壳入口类替换掉原来的Application入口。加密后的原始 DEX 被塞到 APK 的assets目录、lib目录的 so 文件里或者通过云端下发。App 启动时壳入口类的attachBaseContext和onCreate会先把加密的数据解密出来再用DexClassLoader或自定义 ClassLoader 加载到虚拟机里最后才真正启动原来的业务入口。打个比方正常 APK 是直接把一摞文件摊在桌面上你可以随便翻加固之后的 APK 就像把文件锁进了保险柜柜门上只留了一个小窗口柜子会自己打开一点让你看到里面的东西但没法直接把整摞文件抽出来。脱壳要做的就是在“柜子自己打开”的那一刹那把里面真实的文件捞出来。这里有个关键点不管壳做得再复杂它最终必须把原始 DEX 完整地交给虚拟机执行。既然要执行运行时就一定会有一份明文的 DEX 或者至少是完整的方法指令出现在内存里。这就是脱壳可能成立的根本原因。1.2 脱壳的本质从运行时抓明文脱壳技术的切入点总结起来大概三条路主动 hook 壳的 ClassLoader 加载过程在DexClassLoader加载完解密后的 DEX 之后直接从内存或者参数里把 DEX 对象拿回来。扫描目标进程的内存区域通过 DEX 文件头特征也就是dex\n035\0、dex\n037\0、dex\n039\0这类 magic 值把内存里存在的所有 DEX 数据块整体 dump 出来。修改 Android 虚拟机源码在解释执行或者 dex2oat 过程中把加载过的类和方法指令记录下来再从内存中重建出完整 DEX。这三类思路对应市面上常见的脱壳工具。整体加固壳也就是把整个 DEX 加密隐藏的用前两种基本就够抽取型加固壳也就是把方法体单独抽走的就要用到第三种思路后面详细说。2. 动手前先判断壳的类型和加固厂商2.1 三种快速判断加固特征的方法拿到一个 APK不要急着上工具先花两分钟做静态观察能省掉后面一堆无用操作。第一种方式直接用 jadx 或 jadx-gui 打开 APK看 AndroidManifest.xml 里android:name指定的 Application 入口。正常 App 的入口一般是com.自己的包名.MainApp之类的类。如果看到一个和你预期完全不相干的类名或者一个看起来就很有“壳味”的类名基本可以断定是加固壳。另外如果 jadx 打开classes.dex之后只看到少量类且大部分类是stub、proxy、guard、shell那更是壳没跑了。第二种方式解压 APK看lib/目录下的 so 文件命名。加固厂商基本都有自己的特征 so比如libjiagu.so、libDexHelper.so、libsecneo.so这种一眼就能看出是谁家的壳。同时看一眼assets/目录如果里面有一堆没有正常命名的bin、dat、pkg文件这些八成就是加密后的原始 DEX 数据。第三种方式运行 App 后抓 logcat看启动早期加载了哪些 so、ClassLoader 加载了什么路径。这个方法在新版加固上更靠谱因为实力强的加固方案会随机化 so 名称来规避静态特征。2.2 常见加固厂商特征对照我自己整理了一份特征对照表不是百分百准确因为厂商随时更新但作为初始判断足够用加固方案常见特征 so / 文件360 加固libjiagu.so、libjiagu_a64.so梆梆加固libDexHelper.so、libDexHelper-x86.so腾讯乐固libshell*.so、libtprt.so爱加密libsecneo.so、libprotectclass.so网易易盾libnesec.so、libnesec_x64.so娜迦加固libchaosvmp.so、libddog.so顶象加固libx3gso.so这个表的核心作用是帮你确定后续用什么工具更合适。比如看到libsecneo.so说明是爱加密整体加密型壳BlackDex 或者 frida-dexdump 大概率能直接解决如果看到libchaosvmp.so这类带 VMP 字样的说明里面有虚拟机保护普通脱壳流程只会出来一堆乱逻辑。2.3 判断是“加密壳”还是“抽取壳”除了判断厂商还要判断壳的类型。这是选择工具的最关键依据。加密壳原始 DEX 整体被加密运行时会先把整个 DEX 解密再加载。这种壳只要能在运行时抓明文就能还原出完整代码。抽取壳原始 DEX 里的一部分方法体被抽走单独加密保护。运行时壳会在类加载或者方法执行前把被抽走的方法指令动态回填到内存。如果只是普通 dump DEX出来的文件虽然能打开但关键方法体往往是一堆空壳或者跳转指令根本分析不了。VMP代码被转换到自定义指令集在虚拟机上解释执行DEX 层已经看不到真实逻辑。这种只能靠动态跟踪常规脱壳工具基本不够用。实际分析中处理加密壳是最开心的处理抽取壳就得升级工具处理 VMP 则要做好长期作战的准备了。3. 主流脱壳工具选型与场景分析3.1 BlackDex无 root 场景下的快速选择BlackDex 是目前社区里使用率非常高的一个脱壳工具它最大的优点是部署简单可以在部分未 root 设备上直接运行。它的原理说起来不复杂自身会作为宿主 App 启动然后把目标 APK 置于可控沙箱环境中运行在 ART 层 hook ClassLoader 和 DexFile 相关流程等到壳解密并加载真正的 DEX 后直接把 DEX 数据落地到本地文件。实际操作流程大致是安装 BlackDex打开它把目标 APK 添加进去点启动让它跑起来。启动后目标 App 会正常进入主界面等它稳定运行十几秒回到 BlackDex 查看提取结果。输出文件一般在/sdcard/BlackDex/或者应用外部存储目录的BlackDex文件夹下不同版本路径稍有差异找不到就全局搜*.dex文件。BlackDex 对绝大多数整体加密壳效果很好速度快不用配置 Frida 环境非常适合第一次处理带壳样本。它的短板也很明显面对抽取壳dump 出来的 DEX 里方法体是残缺的看起来能打开实际上分析不了面对 VMP 就基本没用。3.2 frida-dexdump最通用的内存扫描方案Frida 本身是一个动态插桩框架可以用来 hook 运行中的 Appfrida-dexdump是社区开发的一个专门脚本基于 Frida 去扫描目标进程内存里的 DEX 数据块。它和 BlackDex 的差异在于BlackDex 是用一个 App 在外部包装启动目标frida-dexdump 是直接注入到目标进程内部做扫描。所以它对壳的动态加载更敏感只要是进程内存里存在的 DEX magic 数据都能被识别并 dump 出来。使用前需要在电脑上装 Frida 命令行工具在设备上跑一个 frida-server。基础命令如下pip install frida-tools frida-dexdump adb push frida-server /data/local/tmp/ adb shell chmod x /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server # 启动目标 App 并脱壳 frida-dexdump -U -f com.example.target -o ./dump/命令执行后目标 App 会启动frida-dexdump 会在短时间内扫描进程内存。扫描到的每个 DEX 都会单独保存为dump_*.dex文件输出目录自己指定。这个方案的好处是通用性强不管壳是哪一家的只要内存里有完整 DEX 就能抓坏处是如果壳检测到了 Frida 服务并主动退出或者进程有反调试逻辑需要先处理这些对抗因素对新手不太友好。3.3 FART抽取型加固的克星前面提到抽取壳普通 dump 解决不了因为方法体被抽走内存里的 DEX 只是一副空骨架。FART 就是为了解决这个问题出现的。FART 全称是“Frida ART”或者更准确地说是“基于定制 ART 的全自动脱壳机”常见产出是一个定制的 Android 模拟器镜像。这个镜像里的 ART 虚拟机被修改过在类加载、方法解释执行阶段会把被壳回填过的完整方法指令连同 DEX 文件一起导出。我在实际使用 FART 的时候流程大体是准备一个刷入 FART 的模拟器把目标 APK 安装进去运行 App等 App 跑起来后FART 会在/data/data/包名/目录下生成 dump 文件夹里面既有 DEX 文件也有以.bin结尾的方法指令数据。把这些文件拉出来用配套脚本把指令文件重新回填到 DEX 中再拖进 jadx 就能看到相对完整的方法实现了。FART 对目前市面上一大批抽取壳效果不错但它要求使用定制模拟器If 你的分析环境是实体手机很多场景不方便直接上 FART。版本兼容方面也要注意FART 镜像一般基于特定 Android 版本遇到高版本系统上才运行的新壳可能跑不起来。3.4 其他工具FDex2 和 YouPK除了上面三个主流的还有两个也值得提一下。FDex2 是一个 Xposed 模块原理是 hook ClassLoader 的loadClass或者DexFile构造方法将 DexFile 对象通过反射转储到本地。它适合 Android 7 以下的旧环境配置了 Xposed 的模拟器上用起来方便但现在新系统上基本不更新了。YouPK 是另一个比较全能的脱壳工具能处理很多抽取壳和一部分加固对抗输出修复后的 DEX配合 Frida 生态使用。它的上手难度比 BlackDex 要高但处理新版加固时经常有惊喜。如果你手头工具链齐全可以把 YouPK 作为 FART 之外的备选方案。3.5 工具取舍总结为了便于选择我整理了一个表格工具运行环境适用壳类型上手难度BlackDexAndroid 本机 App整体加密壳低frida-dexdump电脑 Frida 环境整体加密壳、动态加载 dex中FART定制模拟器抽取壳中高FDex2Xposed 环境旧版整体加密壳低YouPK电脑 Frida 环境抽取壳、部分复杂壳高一般建议是先 BlackDex 跑一遍能出完整 DEX 就先用它不行再上 frida-dexdump 扫内存遇到 dump 出来但方法体完整的文件就是运气好如果方法体是空的再上 FART。工具不是越高级越好能解决问题的那套就是最合适的。4. 一次完整的脱壳实操记录4.1 环境准备和样本信息这里用我最近分析的一个恶意样本为例包名com.mock.sample用 jadx 打开时只有一个壳入口业务逻辑全不可见。分析目标是确认它到底有没有调用短信拦截和上传通讯录的行为。准备环境是一台 Android 8.1 的模拟器已 root。电脑安装了 jadx、adb。设备上安装了 BlackDex并且准备好了 frida-server。样本 APK 已经安装。特别提醒一句跑样本前建议先断网或者直接开飞行模式。很多加固壳启动时会拉取云端配置如果检测到异常环境或者下发新的对抗策略会影响脱壳过程。断网可以减少干扰变量。4.2 BlackDex 快速脱壳全过程先打开 BlackDex主界面会列出设备上已安装的 App找到com.mock.sample点击它。接着 BlackDex 会重新启动目标 App并在后台观察 dex 加载情况。这一步要有点耐心不要在 App 闪出图标后立刻切走等它主界面真正加载出来过十五秒左右再返回 BlackDex。很多整体加固壳是分多个阶段解密 dex 的启动的前几秒是加密壳入口真正的业务 dex 可能在 home 界面渲染完成后才被加载。回到 BlackDex 看任务状态如果显示提取完成打开输出目录。这个版本把结果放到了/sdcard/BlackDex/com.mock.sample/下面里面能看到几个 dex 文件。我用 adb 把它们拉到电脑adb pull /sdcard/BlackDex/com.mock.sample/ ./dump_blackdex/然后用 jadx 打开其中一个体积最大的 dex竟然能看到不少业务类了像com.mock.sample.core.*、com.mock.sample.net.*这些。说明这一层整体加密壳已经拿下了。不过 jadx 里有个别类还是有报错提示 dex 校验失败。这也是常见问题很多直接从内存 dump 出来的 dex头部 checksum 和 signature 是坏的需要在本地修复下面单独讲。4.3 frida-dexdump 作为第二道保险BlackDex 的结果不错但我不太放心因为有些嵌入式 dex 是在子进程里面加载的BlackDex 未必全抓到。既然环境里有 frida-server就再用 frida-dexdump 补一道frida-dexdump -U -f com.mock.sample -o ./dump_frida/这条命令会重新启动目标 App同时开始扫描进程内存。大约等了十几秒dump 目录下多出七个 dex 文件其中有两个和 BlackDex 抓到的文件名完全一致另有三个是之前没见到的应该是运行时动态加载的插件 dex。所以说工具配合使用还是很有必要的。BlackDex 能给你一个非常干净的初始结果frida-dexdump 则能补漏动态加载的部分。两者并不冲突反而是互补关系。唯一需要注意的是frida-dexdump 跑的时候目标 App 对 Frida 是有感知的。如果 shell 立即崩溃就得考虑换一个隐藏性更好的 Frida 变体或者干脆先在静态环境把行为摸清楚再上动态 hook。4.4 修复 dump 出来的 DEX 文件内存 dump 得到的 DEX经常会在头部留下坏校验导致 jadx 打不开或者说“corrupt dex”。原因很简单DEX 文件头部 0x08 处的 checksum 字段和 0x0C 处的 SHA-1 signature 字段在内存里可能没有被完整更新尤其是一些加固壳会主动破坏头部字段来干扰内存 dump。修复方法并不神秘按 dex 文件格式重新计算这两个字段就行。我自己写了一个简单的 Python 脚本处理大部分情况import sys import hashlib import zlib def fix_dex(path): with open(path, rb) as f: data bytearray(f.read()) if not data.startswith(bdex\n): print(f{path} 不是合法的 dex 文件) return # 清空 signature 字段位置0x0C 开始长度 0x14 data[0x0C:0x20] b\x00 * 0x14 # 清空 checksum 字段位置0x08 开始长度 0x04 data[0x08:0x0C] b\x00 * 0x04 # signature 是 0x20 到文件末尾的 SHA-1 signature hashlib.sha1(data[0x20:]).digest() data[0x0C:0x20] signature # checksum 是 0x0C 到文件末尾的 adler32 data[0x08:0x0C] (zlib.adler32(data[0x0C:]) 0xffffffff).to_bytes(4, little) out path.replace(.dex, _fixed.dex) with open(out, wb) as f: f.write(data) print(f已修复: {out}) if __name__ __main__: fix_dex(sys.argv[1])用法很简单python3 fix_dex.py dump_001.dex修复完之后重新用 jadx 打开之前报错的类很多就能正常反编译了。这是我的固定操作凡是内存 dump 出来的 dex 都默认先跑一遍修复脚本省得一边分析一边被解析错误打断。修复时还有一个细节如果先计算 signature 再计算 checksumchecksum 计算范围要包含已经写好的 signature。上面脚本里先写 signature再把从 0x0C 开始的字节丢给 adler32这里的顺序不能搞反。4.5 jadx 验证脱壳结果修复完 dex全部拖进 jadx。这时能看到完整的包结构包括原来壳隐藏的 Application、四大组件、网络层和各种工具类。样本里也确认了存在短信管理器SmsManager的设备管理权限申请代码基本坐实了恶意行为。到这里脱壳这一步就顺利完成了接下来进入常规的代码审计和分析阶段。5. 脱壳后的代码分析思路5.1 拿到真实 DEX 后先看什么脱壳只是第一步真正的重头戏是分析。我自己一般按这个顺序来先看Application子类了解 App 启动时做了哪些初始化特别是是否有诡异的后台服务注册。再搜权限相关关键字比如PackageManager、DevicePolicyManager、SmsManager、GET_ACCOUNTS这些往往是恶意行为的高发点。接着看网络请求地址确认 App 会把数据发到哪里如果是恶意样本这一步直接决定后续取证方向。最后再看动态注册的 Receiver很多恶意 App 的业务逻辑藏在广播接收器里。脱壳之后你要当心一件事文件里的代码是能看懂了但运行时行为可能还有壳在做手脚。所以不要只看静态代码动态 hook 验证结果往往更可靠。5.2 用 Frida 验证关键逻辑当你从 dex 里发现一个疑似关键方法比如sendSmsBySelf可以用 Frida hook 这个方法实时看参数和返回值。frida-ps -U frida -U -f com.mock.sample -l hook.js基础 hook 脚本大概是这种结构Java.perform(function () { var targetClass Java.use(com.mock.sample.core.SmsManagerWrapper); targetClass.sendSmsBySelf.implementation function (number, content) { console.log([hook] sendSms number: number); console.log([hook] sendSms content: content); return this.sendSmsBySelf(number, content); }; });这一步能拿到具体的短信内容和接收人信息比纯粹盯着反编译代码猜要直观得多。hook 过程要注意如果 App 检测到 Frida 存在会主动崩溃或者走一条混淆分支。处理这种对抗最直接的办法是先不 hook让 App 正常跑起来再通过 frida attach 进去而不是启动时直接注入。但这也不是万能的具体要随机应变。6. 常见问题与避坑经验6.1 脱壳问题速查现象可能原因处理方法BlackDex 提取结果很少只有一两个 dex壳把 dex 放到了子进程或者有延迟加载用 frida-dexdump 扫全进程或者多等一段时间dump 出的 dex 打不开提示 corruptDEX 头校验字段损坏用上面的 Python 脚本修复 checksum 和 signaturedex 能打开但方法体都是空实现抽取壳代码被抽走了上 FART 或 YouPK 进行指令回填目标 App 检测到 Frida 后崩溃加固有反调试用更隐蔽的 Frida 变体或者先静态分析再处理模拟器上跑不起来壳检测到模拟器环境换一台有系统镜像的物理测试机这张表基本覆盖了我日常遇到的大多数问题。你实际分析的时候如果发现某个现象不在表里先不要慌多半是壳的个性对抗把 logcat 拉出来仔细看通常能在启动早期找到相关输出。6.2 抽取壳为什么只普通 dump 不够上一节提到普通 dump 遇到抽取壳只能拿到空壳方法这里具体说明一下原理。抽取壳在加固阶段会把原始 dex 里一部分方法指令从code_item区域抹掉替换成空指令或者跳转指令。真正的方法指令被加密保存到壳的数据区。运行时壳在类加载阶段或者某个类第一次被调用时才把对应方法的指令回填到内存里的 dex 结构里。所以普通内存 dump 得到的 dex可能只是部分方法被回填过的“中间态”。运气好能恢复一部分方法运气不好关键方法全部是空的。FART 这种工具解决的就是这个问题它修改了 ART 的解释器在解释到某个方法并触发指令回填之后立刻把当前方法的完整code_item记录下来最后重新拼装成一个修复后的 dex。这就解释了为什么 FART 对于抽取壳特别有效因为它就是冲着“运行时机”这个点位去的。6.3 处理反分析机制的一些个人习惯最后分享几个实战中摸索出来的习惯。第一跑带壳样本之前一定要断网。我遇到过不止一次App 启动时壳从服务器拉取策略发现当前环境是模拟器或者有 hook 痕迹后直接走另一个恶意分支把分析环境带偏了。断网之后很多壳只能走本地默认逻辑分析结果更干净。第二脱壳工具顺序要讲究。不要一上来就上 FridaFrida 本身暴露面太大了容易触发壳的反调试。先用 BlackDex 这种“宿主型”工具拿整体代码再考虑用 frida 做点补充和动态验证成功率会高很多。第三多 DEX 的情况不要只盯着一个文件。加固之后的 App 往往有多个 class loader下载的插件 dex、壳动态解密后生成的其他 dex都需要专门去抓。frida-dexdump 会按内存地址把多个 dex 一起 dump 出来拉到本地后逐个用 jadx 翻别只看体积最大的那个。第四也是最重要的所有脱壳操作都要确保有授权背景不管是自己公司的 App、客户委托的测试样本还是公开的恶意样本分析。分析目的应该放在安全评估和研究上不是用来突破别人的保护机制。脱壳这件事说到底就是一场“你藏我找”的猫鼠游戏。壳的对抗手段越来越强脱壳工具也在不断迭代。我的经验是不要迷信某一个工具能通杀所有样本而是把 BlackDex、frida-dexdump、FART 这些组合在一起形成一个自己的工具链遇到具体样本再灵活选用。这个策略帮我处理了不少难缠的壳希望这篇记录也能让你少走点弯路。
返回列表