APK 脱壳这个话题凡是做过安卓逆向的人迟早都要跟它正面碰上。你可能只是想看清楚一个自己参与开发、但源码已经丢失的旧应用里到底写了什么逻辑也可能是在做安全评估时拿到一个被加固过的样本jadx打开一片空白dex里就一个StubApp心里直骂人。我第一次遇到加固壳的时候也是这个反应——反编译工具显示没报错打开却只有壳的这一层代码真正干活的业务逻辑一个字节都看不到。后来才知道这层看不见的东西就是壳而把壳里的真实代码抠出来就是所谓的脱壳。这篇东西我打算以市面上比较有代表性的爱加密加固为样本从头讲清楚加固和脱壳这两件事的底层逻辑把整套流程、工具选型、参数取舍和踩过的坑都摊开说。整个内容偏实战工具会给到具体命令原理会讲到能自己判断为止。不管你是刚入门看不懂StubApplication的新手还是已经会跑现成脚本、但想搞明白为什么这么脱的老手应该都能从里面捞到点有用的东西。需要先说清楚的是下面所有操作都建立在这样一个前提下你分析的样本是你自己开发的、或者你有明确授权去研究的目标脱壳是用来学习和安全分析的不是拿来干别的。1. 先搞清楚壳到底是什么加固和脱壳的基本盘在动手之前如果不把加壳和脱壳这两件事的原理理顺你后面照抄命令大概率也是懵的。很多人对壳的理解停留在代码被加密了其实远没有这么简单。要真正脱掉一个壳你得先站在加壳方的角度去想他们到底做了哪些事才能既保证 App 能正常跑起来又保证你把 APK 拖进反编译工具里看不到真实逻辑。1.1 加固厂商到底在防什么一个 APK 本质上是压缩包里面最核心的是classes.dex可能还有classes2.dex、classes3.dex……这些 dex 文件里存放着编译后的字节码AndroidManifest.xml里声明四大组件resources.arsc和res放着资源。正常打包出来的 APK把这个 dex 丢进jadx或者baksmali源码逻辑基本就一览无余了。加固厂商要做的就是让你拖进去也白拖。他们干的第一件事是加密原始 dex。真实业务代码被打包成一个或多个源 dex然后整体加密再塞进 APK 的 assets 目录或者伪装成别的资源文件命名往往很随意比如ijiami.dat这种一看就有猫腻的名字。同时APK 里保留一个可以正常被系统识别的壳 dex里面只有极少数几个类——一个入口 Application、一个负责解密和加载的类加载器、若干工具类。这样系统启动 App 时先跑壳 dex壳 dex 再在运行时把真正的业务 dex 解密、还原出来、扔给虚拟机执行。第二件事是替换类加载器。安卓加载 dex 靠的是DexClassLoader或者PathClassLoader加固壳会自己搞一个继承自DexClassLoader的加载器绕过系统的加载路径从内存里直接把解密后的 dex 喂进去。这也是为什么很多脱壳思路的核心就是在类加载器加载的那一刻去拦截。第三件事也是爱加密这类商业壳的进阶手段——指令抽取俗称抽取壳、Dex2C 的一部分。它不只是整体加密而是把每个方法的方法体字节码在打包时抽走运行时再动态回填。这意味着就算你把内存里的 dex dump 出来很多方法的code_item也是空的反编译出来是native或者空方法。这就是为什么脱壳远不止 dump 内存那么简单还得处理方法体修复。注意搞明白这三层防护dex 加密、类加载器劫持、指令抽取你在遇到脱不干净、脱出来是空方法、反编译报错之类问题时才有方向去排查。上来就找脚本乱跑只会浪费时间。1.2 脱壳到底在脱什么一句话概括脱壳的本质是拿到运行时真正被执行的那份可读 dex。不管用什么工具最终目标都是把经过解密、回填、加载到内存里的 dex 完整地提取出来让它能被jadx、GDA这些工具正常解析。这里要区分两种壳对应两种完全不同的脱壳难度壳的类型防护方式脱壳难度典型表现整体加密壳一代壳源 dex 整体加密运行时一次性解密加载较低dump 内存即可拿到完整 dex抽取壳二代壳/指令抽取方法体按需回填静态时方法体为空较高dump 出来方法体缺失需要主动调用补齐爱加密早期的产品偏整体加密后期版本普遍上了抽取这也是为什么很多人用现成 dump 工具跑爱加密得到的 dex 反编译后一堆空方法——不是工具不行是壳的形态变了思路也得跟着变。脱壳这件事最容易被忽略的一点是脱壳不是解密文件而是抓运行时状态。文件层面的解密密钥你可能一辈子挖不出来但没关系因为 App 自己会解密你只要在它解密之后、执行之前或者执行之中把内存里的那份抓下来就行。理解这一点后面所有工具的使用逻辑就通了。2. 爱加密加固的特征识别与结构拆解动手之前先学会认壳这一步很多人偷懒跳过结果用错方法。拿到一个 APK第一件事不是直接扔脱壳工具而是先判断它是什么壳、什么版本、上没上抽取。2.1 静态特征从 Manifest 和文件结构入手把 APK 解压直接改后缀为 zip 或者用unzip看几个关键位置。正常的 APKclasses.dex会有一定体积而加固后的 APK 里classes.dex往往小得可怜只有几百 KB 甚至几十 KB因为它只是壳。真正的业务 dex 藏在别处。用unzip -l先看清单unzip -l target.apk | sort -k1 -n -r | head -20看体积最大的几个文件。爱加密常见的特征包括classes.dex体积异常小里面只有壳类assets/目录下存在命名奇怪的.dat、.bin、.so或者随机字符串文件体积很大lib/目录下有针对性的.so负责解密和加载常见名字里带ijiami、exec、jiagu之类AndroidManifest.xml里的application标签android:name指向一个明显不是业务命名的类比如s.h.e.l.l.S这类不同壳不同。再把壳 dex 用jadx打开你会看到入口 Application 的attachBaseContext里做了几件事加载 so、调用 native 方法、把 Application 换掉。爱加密典型的手法是在attachBaseContext中通过反射替换ActivityThread里的mInitialApplication接管整个应用的生命周期。// 壳 Application 的典型结构示意非真实代码 protected void attachBaseContext(Context base) { super.attachBaseContext(base); // 加载解密 so System.loadLibrary(xxx); // 调用 native 解密 decryptAndLoad(base); }看到这种结构基本可以确定是商业加固壳了。具体是哪家结合 so 名字、assets 里的文件命名、壳类包名综合判断爱加密有自己的固定特征。2.2 运行时特征类加载器和内存布局静态看不出全貌就要上动态。最直接的办法是把 App 跑起来然后看它的进程内存和类加载器。这里frida是绝对主力。一个非常好用的探测脚本就是枚举当前进程里所有的类加载器以及它们加载的 dexJava.perform(function () { var loaders Java.enumerateClassLoadersSync(); loaders.forEach(function (loader) { try { var pathList loader.pathList; console.log([Loader] loader); // 进一步取 dexElements } catch (e) { console.log(skip: e); } }); });爱加密运行时的一个显著特征是业务代码被加载进一个自定义的DexClassLoader或者其子类里这个 loader 不在系统默认的PathClassLoader链上而是被塞进了某个mClassLoader字段。你能在内存里找到一堆已经解密、但没落地的 dex它们躺在 Java 堆或者 native 内存里。/proc/pid/maps也是个好帮手跑起来之后看进程映射爱加密往往会把解密后的 dex 映射成匿名内存rw-p且无对应文件这就是 dump 的目标区域。用frida配合/proc/self/maps扫描匿名可读可执行段是脱壳脚本的常见套路。心得识别阶段花上十几分钟能省下后面几小时的瞎折腾。我见过太多人拿着一个抽取壳用整体 dump 工具跑dump 出十几 MB 文件jadx打开全是空方法然后开始怀疑工具坏了——其实方向从一开始就错了。3. 环境准备与工具选型别一上来就上重型武器脱壳工具现在多如牛毛从一键脚本到需要自编译 AOSP 的重型方案都有。选型的核心原则是能用轻的绝不上重的能黑盒解决的绝不碰源码。下面按从轻到重排一遍。3.1 设备与系统选型脱壳对运行环境有要求核心是两点能 root能自由读写进程内存。真机首选有 root 权限的安卓真机最稳尤其做内存 dump 和主动调用时兼容性最好。注意高版本安卓比如 Android 12 以上对/proc/pid/mem的访问限制越来越严很多老工具会失效建议备一台 Android 8 到 10 的机器或者用对应版本的模拟器。模拟器推荐 x86 或 arm 镜像的模拟器root 方便快照方便折腾坏了直接回滚。但要注意部分壳检测到模拟器会拒绝运行或者改变行为这时候只能上真机。云真机/云手机不方便备设备时的一个选项但涉及内存操作的工具在云端往往受限适合验证不适合攻坚。系统版本和架构要和目标 APK 对齐。一个只含arm64-v8a的 APK你拿 x86 模拟器装上去要么装不上要么跑的是兼容层native 层脱壳会出问题。装之前用aapt看一眼架构aapt dump badging target.apk | grep native-code3.2 核心工具清单与各自定位工具没有银弹每一款都有它擅长的壳形态。我按实际使用频率列一下工具定位适合壳型是否需 rootBlackDex免 root 一键脱壳集成在 App 内整体加密壳为主否FARTAOSP 定制 ROM主动调用脱壳抽取壳是刷机Youpk类似 FART 的主动调用方案抽取壳是刷机frida-dexdumpfrida 脚本扫描内存中的 dex整体加密壳是Frida 自写脚本最灵活可 hook 类加载器各种壳是FRIDA-DEXDumpfrida-dexdump 的原版脚本整体加密壳是选型逻辑是这样的先拿BlackDex这种免 root 工具试一把。它能脱则皆大欢喜脱不了抽取壳再看下一步。整体加密壳用frida-dexdump扫内存成功率很高几分钟出结果。抽取壳就得上FART / Youpk这类需要刷机、靠主动调用回填方法体的方案成本高但效果好。极端情况多层壳、VMP只能针对性写 frida 脚本动态 trace 解密点。3.3 基础环境搭建以 frida 方案为例把环境搭起来。设备上装frida-server版本要和 PC 端frida对齐PC 端装 Python 环境和工具# PC 端 pip install frida frida-tools frida-dexdump # 推送 frida-server 到设备需 root adb push frida-server-16.x.x-android-arm64 /data/local/tmp/frida-server adb shell su -c chmod 755 /data/local/tmp/frida-server adb shell su -c /data/local/tmp/frida-server # 验证连通 frida-ps -Ufrida-ps -U能列出设备上的进程就说明通了。这一步如果卡住先排查frida-server版本和frida客户端版本是否一致、设备架构是否对得上别急着往下走。注意frida-server对很多商业壳是敏感的爱加密有一定的反调试和反注入能力跑起来后进程可能崩溃或者行为异常。这时候可以试试给frida-server改名、换端口或者用 gadget 方式静态注入。这类对抗是脱壳过程中最耗时的部分后面单独讲。4. 动态脱壳实操从免 root 到 frida 内存扫描环境齐了进入正题。我按由易到难的顺序把完整流程走一遍。4.1 免 root 方案先行BlackDex 快速验证BlackDex 的思路很聪明它不依赖 root而是通过精心构造的方式在应用进程内触发 dex 的加载然后自行 dump。使用上简单到离谱——装一个 BlackDex 的 App把目标 APK 装进手机并运行一次再回到 BlackDex 里选择目标应用点脱壳等结果。实际操作要注意几点目标 App 必须至少运行过一次否则壳还没解密dump 出来是空的有些壳会检测 BlackDex 的加载环境导致目标 App 起不来这时候就得换方案dump 出来的 dex 文件会以cookie或时间戳命名导出到/sdcard下再用adb pull拉回来。拉回来后用jadx打开验证。如果是整体加密壳这时候jadx里就能看到比较完整的业务代码了。如果是抽取壳你会看到大量方法体是nop或者干脆报错那就说明得进入下一节。adb pull /sdcard/Android/data/com.wrbug.dexextractor/ ./这一步的意义是快速定性花五分钟就知道这个壳是整体加密还是抽取值不值得投入后续重武器。别小看这个定性方向对了后面就顺。4.2 frida 内存扫描脱壳整体加密壳的主力打法整体加密壳的脱壳逻辑很清晰壳在运行时会把解密后的 dex 加载到内存我们只要在内存里把这些 dex 找出来、按合法格式 dump 下来即可。frida-dexdump就是干这个的它扫描进程内存识别 dex 魔数dex\n035/dex\n036/dex\n037等然后按header里记录的file_size完整 dump。# 先让目标 App 处于运行状态然后 frida-dexdump -U -f com.target.package-f表示启动应用spawn这样能保证从进程一启动就开始监控防止 dex 还没加载完就错过。如果 App 已经跑起来了用-n加上进程名附着即可。脚本跑完会在当前目录下生成一批classes.dex之类的文件。这里有几个关键点决定成败时机dex 是分阶段加载的有些壳会延迟解密。如果 dump 太早拿到的可能不完整。可以让 App 多操作几下触发更多类加载再 dump。去重和过滤内存里会有一堆相似的 dex 片段甚至包括壳自己的 dex需要人工过滤。frida-dexdump会尽量按魔数和校验来筛但重名冲突时得自己合并。完整性校验dump 出来的 dexjadx能不能打开、方法体全不全是唯一的验收标准。遇到打不开的用baksmali反汇编看看是不是checksum或signature头损坏。dump 完的验证命令# 用 baksmali 反汇编能过说明 dex 结构基本合法 java -jar baksmali.jar disassemble classes.dex -o out_smali # 或者直接用 jadx 图形界面打开看方法体4.3 绕过反注入让 frida 能稳定附着爱加密这类壳对 frida 是有防守的。最常见的对抗是我一 attach目标进程就闪退或者frida-ps能看到进程但注入就崩。第一招是改头换面。很多壳通过进程名、端口号、默认路径来检测frida-server。把 server 改名随意一点换个监听端口adb shell su -c mv /data/local/tmp/frida-server /data/local/tmp/fs adb shell su -c /data/local/tmp/fs -l 0.0.0.0:6666 frida -H 127.0.0.1:6666 -f com.target.package -l hook.js第二招是用 gadget 静态注入。把frida-gadget.so改个名塞进 APK 的lib目录然后在壳 Application 之前加载它。这种方式的隐蔽性比动态 attach 高但对 APK 要重新签名壳的完整性校验可能会发现。签名校验本身又是一场猫鼠游戏通常需要用工具把校验点 nop 掉。第三招是延迟附加。有些壳只在启动阶段检测注入等 App 稳定运行后再 attach成功率会高一些。用frida -U -n package -l hook.js附着到已启动进程而不是 spawn。我踩过最坑的一次frida 一 attach 就崩折腾半天发现是壳在attachBaseContext里就做了反调试。后来是把注入时机往后挪绕过启动检测才成功。所以遇到一 attach 就闪退别硬怼先想想是不是时机问题。4.4 用 hook 类加载器的方式精准 dump比盲扫内存更优雅的方法是 hook 类加载的关键函数在 dex 被喂进加载器的那一刻精准抓取。核心 hook 点是DexFile.loadDex或openDexFile这类 native 方法以及BaseDexClassLoader的构造函数。Java.perform(function () { var DexFile Java.use(dalvik.system.DexFile); // hook 加载入口打印或转储文件路径/内容 DexFile.loadDex.overload(java.lang.String, java.lang.String, int) .implementation function (src, dst, flags) { console.log([loadDex] src src dst dst); var res this.loadDex(src, dst, flags); return res; }; });很多时候壳把解密后的 dex 写到临时文件比如应用私有目录下的/data/data/pkg/xxx.dex再加载那直接 hook 完就能拿到文件路径用 root 权限adb pull出来比内存 dump 干净得多。这是我最推荐的思路——顺着壳自己的流程走而不是跟内存里的字节流死磕。顺带提一句有一部分爱加密版本会把解密后的 dex 直接在内存里构造DexFile对象不落地文件这时候 hookopenInMemoryDexFileAndroid 8.0 新接口会更有效。具体用哪个接口取决于目标系统的 API 级别。5. 抽取壳攻坚方法体为什么是空的怎么补回来到这里整体加密壳基本能拿下了。但爱加密较新版本普遍上了指令抽取这时候上面所有 dump 手法都会遇到同一个尴尬dex 结构是完整的类和方法都在但方法体是空的jadx打开显示native或者{}。这不是 dump 失败而是我们脱的时机不对——壳只回填了当前要执行的那几个方法。5.1 抽取壳的运行机制抽取壳的原理是打包时把每个方法的字节码从code_item里抠掉只留下方法签名和结构同时在壳里记录这个方法体在哪、怎么还原。运行时当某个方法即将被调用壳的加载逻辑先把它的方法体解密、回填到内存中的 dex 里再交给虚拟机执行。所以你 dump 得再完整也只能拿到已经执行过、已经回填过的那些方法没执行过的永远是空的。这就解释了一个现象如果你只启动 App 不做任何操作dump 出来的方法体寥寥无几如果你把 App 里每个功能都点一遍dump 出来的方法体就多很多。这本身就是一种暴力穷举式的脱壳——把所有入口都触发一遍让壳把所有方法都回填。5.2 主动调用把等它执行变成我让它执行暴力点功能效率太低而且有 UI 的操作未必能覆盖所有方法。真正的解法是主动调用写代码遍历 dex 里所有类、所有方法逐个主动触发它们的加载/回填壳就会把对应方法体补上。FART、Youpk 这类方案的核心就是这个思路。它们在 AOSP 源码层面改动了art的方法调用入口在方法被调用时把回填后的CodeItem转储出来再在脱壳结束后把所有转储片段合并成完整的 dex。自己用 frida 也能做简化的主动调用思路是枚举已加载的所有类对每个类的每个方法构造一个不执行真实逻辑的调用比如 hook 住方法本身在进入时立即返回一旦方法被调用触发壳就会回填方法体此时再从内存中 dump。Java.perform(function () { Java.enumerateLoadedClasses({ onMatch: function (name) { try { var clazz Java.use(name); var methods clazz.class.getDeclaredMethods(); methods.forEach(function (m) { // 记录方法签名构造触发逻辑 console.log([method] name . m.getName()); }); } catch (e) {} }, onComplete: function () {} }); });实际操作中主动调用触发会遇到大量参数错误对象为 null的异常需要一套容错逻辑每个方法用 try-catch 包住失败了跳过成功了就说明这个方法体已经被回填可以 dump。FART 的价值就在于它在系统层把这个过程做得足够稳、足够全。用 FART dump 出来的结果通常是一堆碎片文件需要用官方配套脚本合并、去重最后生成可反编译的 dex。这个过程比较机械按文档走就行但它对环境和版本很敏感刷机前一定确认好你的 AOSP 版本和 FART 版本匹配。心得做抽取壳之前先确认它是不是真的抽取。有些壳只是整体加密 少量反编译对抗用整体 dump 就完全够。识别方法很简单——dump 后随机挑几个业务方法看方法体在不在全空才是真抽取。别为了不存在的问题上重武器。6. 常见问题与排查技巧实录脱壳过程中遇到报错是常态整理几个高频问题附排查思路。这些坑绝大部分都是环境问题或时机问题不是工具本身不行。现象可能原因排查/解决frida 一 attach 进程就崩壳有反注入/反调试改 server 名、换端口、延迟附加、gadget 静态注入dump 出来 dex 打不开header 损坏 / dump 不完整用 baksmali 看反汇编是否报错检查 file_size重新 dumpdex 能开但方法体全空抽取壳dump 时机太早触发功能或上主动调用方案BlackDex 脱壳失败壳检测环境或抽取换 frida 方案确认壳类型App 装上跑不起来模拟器/架构不匹配、壳检测换真机、对齐 abi、检查 native 库frida-ps 连不上server 版本/端口/架构不符版本对齐-l指定端口确认 abi脱出来的方法名乱码壳做了符号混淆正常现象结合字符串、调用关系还原几个我实际踩过的细节dump 的 dex 有重复内存里同一个 dex 可能因为类加载器的多次加载而存在多份合并时按类去重别把两份拼一起导致重复类名报错。/proc/pid/mem读不了Android 高版本对内存读取做了限制要么降版本要么改走 frida 的Memory.readByteArray接口它绕过了部分限制。native 层加密有些爱加密样本把核心逻辑放进.sodex 只是壳。这时候脱 dex 拿不到核心逻辑得转向 native 逆向静态分析 so 用IDA动态用frida hooknative 函数。这条路更硬需要单独规划。排查的总原则是先定性壳类型再定位失败环节注入/加载/dump/合并最后针对性解决。别在一堆工具之间反复横跳浪费时间还理不清头绪。7. 我给准备入坑的人几句实话做 APK 脱壳这件事工具只是表面真正拉开差距的是对安卓运行时、类加载机制和加固原理的理解。你把DexClassLoader的加载流程、art里DexFile的内存布局搞明白了很多壳你会知道该在哪里下手而不是依赖别人的脚本碰运气。脚本会过期原理不会。我自己现在拿到新壳第一反应已经不是用哪个工具而是它到底在哪一步做了手脚这个思路的转变比学会十个工具都值钱。另外提醒一句脱壳过程里你会遇到大量加密、签名校验、反调试的对抗心态上要做好打持久战的准备一个样本卡几天是常事。最后还是那句老话所有操作请限定在你自己的样本或者有明确授权的目标上技术本身是中性的用在哪、为什么用才是真正需要自己想清楚的事。