APP脱壳重打包实战:从内存Dump到签名绕过全流程避坑指南
我这两年做移动端安全测试遇到一个很有意思的需求客户自己公司的一款应用市场反馈说新版本启动慢、功耗高、部分机型闪退。他们怀疑是加固方案和某些低端机兼容性出了问题但加固服务商已经不维护了只能自己拿回去分析。这就涉及到给APP脱壳、重新打包做回归验证的完整流程。整个过程踩了不少坑今天完整复盘一下给同样做APP安全测试、逆向分析、或者需要处理遗留加固项目的朋友一个参考。先说清楚界限这篇文章讲的是对已经获得授权自有应用、客户书面授权测试、CTF/漏洞挖掘学习样本的APP做脱壳与重打包研究。技术上能做的事情很多但用在哪里、合不合法责任完全在操作者自己。我不会讨论任何绕过付费、盗版分发、破解他人商用应用的场景这类需求请直接关掉页面。1. 这次分析任务的起点为什么必须走脱壳-重打包这条路客户给的APK加固过具体加的是什么壳不知道无外包无文档。正常情况下我们拿到一个APK第一步就是扔进jadx或者GDA直接看源码快速定位关键逻辑。但这个包一打开classes.dex只有几千行入口被替换成一个壳的Application类真正的业务代码全被抽走压缩到了so文件或者加密文件里。也就是说静态分析路径直接断掉了。这时候摆在面前的有两条路动态调试在模拟器或真机上跑起来用Frida hook关键函数看运行时的行为。优点是灵活缺点是如果壳检测到调试器会主动退出而且hook是零散的没法看到全貌。脱壳后静态分析在运行过程中把内存里解密后的完整dex抓出来修复后扔进jadx像分析普通APK一样看全部源码。这次场景必须选第二条因为我们要回归验证的改动涉及启动流程、页面初始化逻辑、还有几个核心配置项。这些东西用动态hook去试等于盲人摸象只有把真正的dex还原出来才能精确修改再重新打包测试。另外要说明的一点是脱壳本身不是破解。壳保护的是代码静态内容的安全脱壳是把APP运行时的真实代码还原出来这是移动安全测试、恶意代码分析、漏洞挖掘里的标准前置步骤。很多安全厂商的自动分析平台就是靠这一套原理批量跑样本的。2. 加壳方案识别与脱壳原理拆解2.1 先认清楚对方用的是什么壳脱壳之前先得知道壳的类型不同壳的方案差异很大乱试工具浪费时间。识别壳的方法很直接看几个特征APK根目录下的so文件不同加固厂商的so文件名有特征比如libDexHelper.so基本就是腾讯乐固libjiagu.so是360加固libSecShell.so是爱加密libDexHelper.so有时候梆梆也在用更准确的是看so的内部导出符号。入口Application类名壳会替换入口比如com.tencent.StubShell.TxAppEntry就是乐固的特征com.qihoo.util.StubApplication是360的特征。classes.dex文件大小如果一个小体量APP的dex只有几十KB那大概率是壳的加载器真正的业务dex被加密存到assets或so里了。assets目录下的加密文件很多壳会把真正的dex加密后放到assets目录文件名可能是随机生成的也可能是带特征的比如tinker、packer等。我这边的样本特征很明确libDexHelper.so存在入口是com.tencent.StubShell.TxAppEntryassets里有一个很大的.dat文件。基本锁定是腾讯乐固系。这里有个细节腾讯乐固的版本差异很大老版本可以直接用FDex2这类工具dump新版本尤其是企业版会对内存dex做完整性校验dump出来的dex直接打开全是乱码需要先修一下内存里的dex头。2.2 脱壳到底在脱什么明白原理很重要。加壳不是把代码加密就完了最终APP要运行系统需要加载dex并执行。壳做的事情是APP启动时先加载壳自己的Application。壳在Application的attachBaseContext或onCreate里解密真正的dex数据。壳把解密后的dex数据写入内存或者写入私有目录然后通过自定义ClassLoader加载。业务代码的类实际是从这个解密后的dex里加载的。所以无论壳怎么加密在APP运行起来、业务类被加载之后内存里一定存在一份完整的、可被ClassLoader解析的dex数据。脱壳的本质就是在合适的时机把这块内存数据抓出来。这里有个关键点不是只要在运行中随便dump就行。如果dump太早业务dex还没被解密加载太晚某些类已经被加载完但dex数据在内存中可能还是完整的因为Art虚拟机的ClassTable里会持有DexFile对象内存中那份dex的镜像还在。所以脱壳工具一般会Hook ClassLoader的loadClass或者dex文件的打开过程在dex被加载进内存的第一时间点dump这样最完整。2.3 现有工具的能力与局限目前主流的脱壳工具我大概列一下工具/方案原理优势痛点Frida-DEXDump遍历内存中dex魔数dex\n035\0并dump简单粗暴兼容性好部分壳会抹掉内存中的dex头部导致dump失败BlackDex利用Android系统启动时加载dex的时机在非root环境直接脱不需要root操作简单对新版壳、指令抽取型壳支持一般FART主动调用每个类的方法让类初始化并dump完整dex对抽取型壳效果好需要定制ROM环境搭建麻烦Youpk基于FART思路改进自动脱抽取壳自动化程度高需要特定模拟器版本维护更新慢手工dump修复定位内存地址手工提取dex并用脚本修复最灵活技术要求高费时我这个样本用Frida-DEXDump跑了两次第一次dump出来一批dex但里面混着很多脱壳器自己创建的中间dex真正的主dex有两个其中一个打开后类全是空的。这就是典型的抽取型加固——dex在内存里只是一个空壳真正的方法体指令在运行时才被so文件填回去。抽取型加固是这几年脱壳的主要难点。它比早期整体加密更进一步不是把dex封装起来而是让dex正常加载但把类的方法指令抽走存到native层。这样dump出来的dex虽然结构完整但方法体是空壳jadx打开后函数体全是空的或者只有return void。针对这种情况光靠内存dump是不够的得用主动调用Active Call思路或者针对性修复。3. 脱壳实操从内存dump到还原可读dex3.1 环境准备一条命令能起的环境最省心我的操作环境是这么搭的一台Android 8.1的模拟器Genymotion因为FART/Youpk类工具对低版本适配更好而且老版本模拟器跑加固APP不容易触发反调试。真机一台Pixel 3Android 10已root用于交叉验证脱壳结果。Frida 14.x 对应版本的frida-server。宿主机的Python 3 frida-tools以及objection备用。为什么用模拟器而不用真机有些壳会检测模拟器直接退出运行所以反而在真机上更稳。但模拟器有快照功能脱壳过程中如果APP崩了或者dump时机错过了直接回滚快照重新来比真机重装省太多时间。所以我的策略是模拟器优先崩了就回滚模拟器跑不起来的样本再上真机。安装frida-server的时候有个老生常谈的坑frida-server版本必须和主机端frida一致。我之前用frida 15.1.0的客户端往设备上推了15.2.0的server结果session一直创建失败报错信息还特别隐晦不提示版本不一致。后来统一换成15.1.0才正常。3.2 Dump时机早和晚都不行Frida-DEXDump的使用很简单默认就是遍历进程内存找dex魔数。但实际执行时要注意Dump时机我先启动APP让它跑过启动页进入主界面这时候业务类基本都已经加载完了。再附加进程执行frida-dexdump -U -f com.example.app -o dump/。但跑下来发现一个问题dump出来的dex数量很多但真正包含业务代码的dex只有两个一个是肉眼看体积就觉得挺大的主体dex另一个是比较小的子dex。这个时机最稳。不要一启动就直接dump很多壳的初始化还没完成也不要在一个页面停留太久有些壳有反内存dump的定时器运行久了之后会抹掉dex头或者主动释放掉解密后的内存。dump完成后当前目录下会出现一堆dex文件。先别急着全打开写个小脚本批量检查一下文件头和结构完整性。用010 Editor的DEX模板可以快速看dex的文件头、string_ids、class_defs这些关键段的偏移如果class_defs.off指向的位置超过文件大小说明这个dex就是个半成品。3.3 抽取壳的方法体修复最花时间的一步前面说主dex的类方法是空壳这里具体说说长什么样。用jadx打开主dex找到MainActivity跳到一个方法发现方法体只有一行return void。再去看反汇编的smali也只有return-void指令。这就是抽取型加固的典型特征方法指令被抽走了运行时壳会把真正的指令填回ArtMethod里。针对这种壳的修复思路有两条基于主动调用的dump用FART这类工具在定制ROM里遍历ClassLoader加载的所有类逐个调用每个方法。方法被调用的过程中壳的指令还原逻辑会先触发把真正的指令填好这时候把整个方法体的指令从内存里dump出来。这个方案对大多数抽取壳有效但需要定制ROM环境搭建成本高。基于DexDump指令回填先从内存里dump出包含了真实指令的dex可能是某个片段然后对比空壳dex和真实指令dex的差异定位到被抽走的方法指令再回填到空壳dex里。这个方案对工具要求高但对特定壳效率不低。我这次用的是第二个思路。具体做法是dump出来的空壳dex记为dex_a里面方法体是空的。用Frida脚本在运行时找到MainActivity的ArtMethod读取它方法体的实际字节码CodeItem保存到JSON文件。写一个Python脚本把JSON文件里的方法指令按方法索引和偏移回填进dex_a的对应位置。回填完后用baksmali重新把dex转为smali再转回dex让结构重新归一。这个过程说起来简单实际操作的时候坑非常多。最典型的一个ArtMethod的内存布局里方法体有一个映射关系同一个方法在dex里的method_idx和在运行时ClassTable里的index不完全一致如果按错索引回填轻则方法逻辑错乱重则认为dex校验失败直接崩溃。所以回填前一定要通过MethodHelper这类工具把运行时ArtMethod的name和dex里的method_idx对应上用方法名做中间验证确保不会填错位置。修复完后重新用jadx打开MainActivity的方法体就完整可读了而且反编译的Java代码逻辑和预期一致。到这里脱壳的核心目标达成。4. 修改与重打包dex替换、签名绕过与重签名的全套动作脱壳完了真正要动刀了。客户的回归测试需求是调整启动流程里的几个参数实际上就是一个很小的改动在Application里加一个配置分支。但就是这个小改动在加固重打包场景下牵出了一堆问题。4.1 改写dex的正确姿势尽量改smali别改Java拿到可读dex后正常的修改流程是用apktool解包原APK把我们要改的dex替换成修复后的版本。用baksmali把dex转成smali代码。修改smali。用smali工具把修改后的smali文件夹转回dex。把新dex放回APK重新打包重新签名。有个很重要的经验直接改smali别想着在jadx里把Java改好再编译回dex那个还原度做不到。jadx反编译出来的Java有损改完再编译回去等于脱了裤子放屁——不仅方法签名可能对不上泛型信息、注解、内部类全部会出问题。我自己改的是一个判断逻辑。原逻辑是读一个设备型号字符串如果是特定机型走A分支否则走B分支。我需要改成全部走B分支。对应的smali是# 原始逻辑 invoke-static {p0}, Lcom/example/Cfg;-getDeviceModel()Ljava/lang/String; move-result-object v0 const-string v1, SM-G975F invoke-virtual {v0, v1}, Ljava/lang/String;-equals(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_branch_a # 修改后——直接强制走B分支 const/4 v0, 0x0 if-nez v0, :cond_branch_a生产里的真实改动比这个复杂一些还涉及一个SharedPreferences的写入位置调整但核心方法论是一样的定位关键分支用smali等价改写。4.2 签名校验一个足以让人崩溃的隐藏地雷dex改完APK重新打包签名安装点击图标。黑屏然后退出。没有crash日志没有ANR就是直接秒退。第一反应是dex改坏了方法体回填有问题。于是把原始未修改的修复dex放回去重新打包签名安装——同样秒退。这下问题明确了不是dex改动的问题是重建后的APK在启动阶段就被干掉了。排查思路缩小到签名校验和完整性校验。加固APP基本都会做签名校验因为脱壳重打包必然导致签名变化签名校验就是防脱壳重打包的最后一道防线。有两个方案可以处理定位校验点并绕过用Frida hook PackageManager的getPackageInfo或者查Signature把返回值替换成原始签名。这个方法对调试有效但不能根治因为每次启动还是会被检测。将重打包后的签名改成和原包一致技术上做不到签名是私钥生成的我们没有原私钥。降级校验找到校验逻辑在smali层面把它改成恒真。这个方法最彻底。签名的处理实际操作上用的方案是通过hook去确认了壳确实在校验签名之后用jadx在脱壳后的dex里全局搜Signature和getPackageInfo的调用点定位到一个工具类里面写了一大段签名校验逻辑。修改方案就是把这个工具类的check方法改成直接返回合法状态。这里有个值得说的细节签名校验的触发分两种一种是壳自身逻辑写在so里一种是业务代码校验写在dex里。我这次遇到的是业务代码校验所以脱壳后能找到修改点。如果校验藏在so里就得在native层patch或者hook工作量和难度会明显上一个台阶。4.3 重签名的选型v1/v2/v3的选择题重签名用的是uber-apk-signer这是个很顺手的工具参数简单自动选择签名方案。但我建议在重签名前先确认清楚APK的targetSdkVersiontargetSdkVersion 30及以上系统要求必须使用v2或v3签名Android 11以上还会校验v2签名的Block部分。如果只用v1签名低版本能装高版本直接拒绝安装。因此不能只图省事用v1。uber-apk-signer默认行为会对合适的目标自动使用v2方案实际测试下来没出问题。原始APK如果是v1v2签名重打包后建议也保留v1v2。只签v2的话Android 7.0以下设备安装会失败虽然测试机都是新机但保不齐以后要在老设备上验证。签名完成后用apksigner verify --print-certs检测签名结果确认签名信息正常。5. 重打包后闪退的完整排查链路一个隐藏的崩溃点重打包完成后在测试机安装启动这时候遇到一个比签名校验更隐蔽的坑。5.1 现象新的崩溃点和旧的不一样签名校验绕过之后APP能进入启动页了但走到界面跳转时又开始闪退。这次有日志了通过adb logcat抓到了关键异常AndroidRuntime: FATAL EXCEPTION: main AndroidRuntime: java.lang.VerifyError: Verifier rejected class com.example.core.CfgHolder AndroidRuntime: at com.example.app.MainActivity.onCreate(...)VerifyError类校验失败。这个问题在普通开发中很少见但在脱壳重打包的场景下很常见。为什么Android 7.0及以上默认使用ART虚拟机它会在类加载时做字节码校验Verifier检查类型安全、分支跳转地址合法性、寄存器类型等。如果smali修改后的字节码存在类型不匹配或者跳转目标错误ART会在运行时直接拒绝加载这个类抛出VerifyError。5.2 定位过程从报错类名逆向追踪拿到异常信息定位到com.example.core.CfgHolder这个类。用baksmali反编译看看.method public static getFlag()Z .registers 2 # 新加的指令 const/4 v0, 0x1 return v0 .end method乍一看没问题类型也匹配。但注意CfgHolder是个静态内部类外部引用它时会有synthetic字段和方法。最可能出现问题的地方是修改smali时我们把CfgHolder的方法改了但类文件的字段表或者方法表里某个访问标志没对齐。于是用了debug模式重新跑adb shell am start通过valgrind不需要那么重。用dmesg和logcat配合看日志里直接给出了具体拒绝原因AndroidRuntime: class com.example.core.CfgHolder: rejected: this argument of CfgHolder.getFlag()Z is not compatible with CfgHolder这个错误的意思是调用getFlag时传入的this引用类型不匹配。说明外部调用它的smali代码里move-object指令加载的对象类型被标记成了父类或其它不相关类型。查调用点发现MainActivity的onCreate里变量寄存器v0被之前的逻辑占用存了一个字符串对象又一个本地变量错位。回到smali看清楚invoke-direct {p0}, Lcom/example/App;-init()V invoke-static {v0}, Lcom/example/core/CfgHolder;-getFlag()Z这里有个致命错误getFlag是静态方法不需要传this但我改smali时把之前的move-result-object v0放在了invoke之前v0里是一个Ljava/lang/String对象却作为第一个参数传给了静态方法。ART校验器认为参数类型不匹配直接拒掉。这就是VerifyError的直接原因。修复方式很简单删掉多余的move-result-object v0确保invoke-static的寄存器内容正确。这个坑很值得单独拿出来说因为很多新手改smali时只关注逻辑对不对忽略了寄存器值复用导致的类型冲突。ART的字节码校验比JVM严格得多遇到VerifyError千万别慌先去核对寄存器类型和调用指令的参数列表。5.3 彻底杀掉残留的完整性校验还有一类崩溃不会立刻出现而是在APP运行一段时间后突然闪退难定位得多。这类是壳在native层埋的完整性校验检测assets目录文件的改动。我们重打包时apktool会重新压缩资源。如果原包assets目录下有一个和壳相关的.dat文件即使内容没变压缩率变了文件字节也可能不一样。壳在so层写死了某个文件的hashhash对不上就主动崩溃。怎么查用Frida hook一下文件读取相关API或者更直接把原始APK和重打包APK的assets目录逐文件对比sha256发现确实有一个.dat文件变了。怎么让它不变用apktool的-c参数但在较新版本中加载不到特定的压缩方式。更稳妥的方案是用apktool解包后手工把assets目录替换回原始APK里的原始文件重新打包时压缩也会把数据原样拷贝。实测下来这个方案能解决绝大多数因资源压缩导致的完整性校验失败。另外要注意如果原APK里的资源使用了flattenResources之类的特性重打包时要把resource.arsc以及res目录整体保留不要改任何资源只改dex。一次正常的脱壳重打包只应该动到dex和签名两部分其它文件尽量保持原样这样能避开大部分坑。6. 逆向分析的合规边界与长期有用的三个习惯6.1 什么能做、什么不能做心里得有数脱壳重打包是一个技术能力但这个能力是有清晰边界的能做的场景自己开发或公司自有APP做合规性检查、安全自测、回归验证。拿到客户书面授权的渗透测试中对目标APP做逆向分析。开源软件、明确声明允许二次开发分析的APP。CTF比赛、漏洞挖掘平台提供的分析样本。学术研究、移动安全教学中的样例分析。不能做的场景破解并分发他人的付费APP。绕过商业APP的付费验证、会员体系。篡改应用内容后伪装成原版发布。将脱壳技术用于批量盗版和恶意植入。我这次做的是第一种场景客户带着盖公章的授权委托书来的。合规是一切技术操作的大前提技术圈里很多人不是不会脱壳而是不知道脱壳之后转身把分析结果用在非法用途上最后进了名单。这不是危言耸听是有真实案例的。6.2 习惯一每次操作都要留痕归档脱壳分析的每一步都值得留档源APK sha256、脱壳时间戳、使用工具版本、dump出的关键dex、修改了哪些smali文件、签名证书指纹。这些信息在做安全报告时是必须的原材料也是未来追溯问题比如某个修改导致奇怪的bug时的依据。我的做法是每个样本建一个目录里面放README.md把所有操作记录按时间顺序写清楚包括命令和参数。6.3 习惯二修改前先做基线对比在动任何smali之前先把脱壳后未修改的dex文件用jadx整体导出一次保留一份代码快照。这样后续修改出问题时可以快速对比原逻辑定位是哪一步改出了偏差。而且脱壳后的代码快照是分析报告中重要的过程证据它是脱壳成功的一个直观展示也是追溯问题的基础。这次改启动逻辑时我就因为快照齐全省掉了不少回滚验证的时间。6.4 习惯三善用脱壳分析后的副产品脱完后得到的完整dex不只是修改重打包这个用途。换个角度想很多安全问题在这时候也藏不住了。比如AndroidManifest里导出的组件、数据库明文存储、接口地址硬编码等等。客户后来拿着我脱出来的代码做了一个小的安全巡检发现两个接口没有鉴权参数当场就让研发修了。这才是脱壳分析在企业服务里真正值钱的地方——重打包只是手段发现并解决实际问题才是目的。最后再分享一个纯实操的小技巧重打包完的APK安装前强烈建议用apktool的d命令再回解一遍确认xml资源、AndroidManifest没有因为打包过程损坏。有两次我重打包后闪退最后发现是AndroidManifest里一个application的name被apktool重写时弄丢了。先验证包完整性再装进手机能省掉一半的排查时间。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →