比 dex 加密更狠的防护普通加固（360/梆梆/乐固）保护的是 dex 文件——运行时解密加载，dump 内存就能拿到明文。但 dex2c 和 VMP 加固玩的是另一个维度：直接把 Java 方法翻译成 native 代码，dex 里只剩一个空壳方法（native 声明或 throw 占位），真实逻辑编译进了 SO。dump 出 dex 也没用——方法体是空的。这就是为什么这类加固被称为"脱壳的终点"。识别：怎么判断中招了jadx 打开脱壳后的 dex，看到这些特征就是 dex2c/VMP：// 特征1：方法体是 native 声明，但 Java 层从没这样写过 public static native String getSign(String s); // 特征2：方法体只有一行 throw（占位） public String getSign(String s) { throw new RuntimeException("stub"); } // 特征3：伴随加载奇怪的 SO static { System.loadLibrary("protect"); }再结合 SO 文件大小异常（几 MB 甚至十几 MB 的 libprotect.so），基本实锤。dex2c 的对抗思路dex2c（如某加固的"Java2C"功能）把 Java 方法逐条翻译成等价的 C 代码编译成 SO。对抗分三层：第一层：动态执行拿结果（首选）不试图理解 native 代码，直接让 App 自己执行：// Frida 直接调这个"native 化"的方法，和普通方法调用没区别 Java.perform(function() { var SignUtils = Java.use('com.target.app.SignUtils'); var result = SignUtils.getSign("test_input"); console.log(result); });dex2c 翻译后的方法对调用者完全透明——Java 层该怎么调还怎么调。Frida RPC 方案不受任何影响。第二层：unidbg 模拟执行需要脱离设备时，unidbg 加载那个巨大的 libprotect.so。注意 dex2c 的 SO 重度依赖 JNI 函数表（它要模拟 Java 语义：创建对象、调方法、访问字段），补环境工作量大，但 unidbg 的 AbstractJni 覆盖了大部分。第三层：静态还原（最后手段）真要读懂算法，面对 dex2c 的 SO 有技巧：它生成的代码是模式化的——每个 Java 字节码对应固定的 native 代码模板。认识这些模板（比如 aload 对应局部变量读取的固定指令序列），就能反向映射回 Java 逻辑。社区有 dpt-shell 等工具针对特定 dex2c 方案做自动化还原。VMP 加固：自定义指令集的终极形态VMP（虚拟机保护）比 dex2c 再进一步：Java 方法被编译成自定义虚拟机的字节码，SO 里内置一个解释器来执行。你看到的 native 代码只是解释器，真实逻辑是数据段里的一堆自定义字节码。对抗思路：别碰解释器，碰输入输出：Frida 调方法或 hook 解释器的"取指-译码-执行"循环的边界，记录每条虚拟指令的操作数——这是"指令 trace"路线还原虚拟机架构：分析解释器的译码逻辑，搞清自定义指令集（有多少种操作码、操作数格式），然后写反汇编器把字节码翻译成可读伪码——工作量以周计，只有高价值目标值得执行流录制：用 Stalker 记录方法执行的所有 native 指令轨迹，从轨迹里提取数据流（输入怎么变成输出），跳过理解指令集精髓：成本决策树面对 dex2c/VMP，按这个顺序决策：Frida RPC 直调（成本：小时）→ 90% 的场景到这里就够了unidbg 模拟（成本：天）→ 需要规模化、离线化时指令 trace + 数据流分析（成本：周）→ 需要理解算法原理时完整还原虚拟机（成本：周~月）→ 学术研究或极高价值目标逆向的终极目标不是"读懂每一行代码"，而是"让目标能力为我所用"。能调用就别还原，这是面对强防护的第一原则。总结dex2c/VMP 让 dex 方法变空壳，dump dex 路线终结但调用接口不变——Frida 直调完全不受影响，这是首选unidbg 能跑 dex2c 的 SO，补环境是主要成本静态还原 VMP 是周级工程，非必要不启动交流微信：run1255
腾讯乐固脱壳：dump dex 的时机选择决定成败
乐固的特点：懒加载与分片解密腾讯乐固（legu）的识别特征：入口 com.tencent.StubShell.TxAppEntry，assets 里有 libshell.so、libshella-x.so 等。它和 360/梆梆最大的不同是解密策略：乐固不是启动时一次性解密全部 dex，而是按需解密、分片加载——用到哪个类才解密哪部分。这个差异决定了脱壳策略：启动后立刻 dump，只能拿到不完整的 dex；必须把 App 的主要功能都点一遍，触发所有类的加载，再 dump。正确的脱壳流程第一步：spawn 启动，绕过反调试frida -U -f com.target.app -l dump.js --no-pause乐固的反调试相对温和（主要是 TracerPid 和端口检测），标准绕过脚本即可。第二步：自动化遍历触发类加载手动点太慢，写个简单的遍历：启动后通过 monkey 或 UI Automator 随机点击几分钟：# 另开一个终端跑 monkey，覆盖主要页面 adb shell monkey -p com.target.app --pct-touch 80 --pct-motion 20 -v 500第三步：dump// dump.js Java.perform(function() { // 延迟 30 秒，期间配合 monkey 遍历 setTimeout(dumpAllDex, 30000); }); function dumpAllDex() { var count = 0; Process.enumerateRanges('r--').forEach(function(range) { if (range.size < 4096 || range.size > 30 * 1024 * 1024) return; try { Memory.scan(range.base, range.size, '64 65 78 0a 30 33 35 00', { onMatch: function(addr) { var size = addr.add(0x20).readU32(); if (size > 1024 && size < 50 * 1024 * 1024) { count++; console.log('[' + count + '] dex @ ' + addr + ' size=' + size); send({type: 'dex', index: count}, addr.readByteArray(size)); } }, onComplete: function() {} }); } catch(e) {} }); console.log('共 dump ' + count + ' 个 dex'); }精髓一：判断 dump 是否完整乐固脱壳最常见的失败是"dump 了但类不全"。验收方法：# 把所有 dump 出的 dex 合并分析 jadx -d out dump1.dex dump2.dex dump3.dex # 检查 AndroidManifest 里声明的 Activity 是否都有对应类对照原 APK 的 AndroidManifest.xml，逐个检查四大组件的类是否存在且有方法体。缺哪个页面的类，就回去多点哪个页面再 dump。精髓二：乐固的字符串与资源加密乐固对部分字符串做了加密，dump 出的 dex 里会看到 TxAppEntry 相关的解密调用。处理思路：运行时批量调用解密函数建映射表（Frida hook 解密函数，遍历所有密文）写 jadx 插件或后处理脚本把 decrypt(0x1234) 替换成明文精髓三：64 位与 32 位的差异乐固在 64 位设备上加载 libshella-x.so（64 位壳），部分老脚本只 hook 了 32 位路径。Frida 脚本里注意：// 确认当前进程位数 console.log(Process.arch); // arm64 还是 arm // libart 的符号在两种位数下参数布局不同，dump 脚本的参数索引要适配与其他壳的对比速查壳入口特征解密策略脱壳要点360 加固StubApp启动全量解密启动后即可 dump梆梆ApplicationWrapper启动解密+反调试强先过反调试腾讯乐固TxAppEntry按需分片解密遍历后再 dump总结乐固的特征是按需解密，启动即 dump 必不完整monkey 遍历主要页面触发全量类加载，再动手 dump验收对照 Manifest 的组件清单，缺类补点注意 32/64 位壳的符号差异，脚本要做适配交流微信：run1255
梆梆的特征与防护思路梆梆加固（bangcle）的识别特征：入口是 com.secneo.apkwrapper.ApplicationWrapper 或 MyWrapperProxyApplication，assets 里有 libsecexe.so / libsecmain.so。它的防护比 360 更"黏"一点——除了 dex 加密，还有反调试、模拟器检测、dex 加载点混淆的组合拳。脱壳前先过反调试，否则 Frida 一 attach 就闪退。姿势零：先绕过反调试梆梆的反调试常见手段：检测 TracerPid、检测 frida 端口（27042）、检测 frida-agent 的内存特征。通用绕过：// 1. 绕过 TracerPid 检测：hook open/read 伪造 /proc/self/status var openPtr = Module.findExportByName('libc.so', 'open'); Interceptor.attach(openPtr, { onEnter: function(args) { var path = args[0].readCString(); if (path.indexOf('/proc/') >= 0 && path.indexOf('status') >= 0) { this.isStatus = true; } }, onLeave: function(retval) { this.isStatus = false; } }); // 配合 hook read，把 TracerPid: xxx 改成 TracerPid: 0 // 2. spawn 模式启动，抢在反调试初始化之前 // frida -U -f com.target.app -l bypass.js --no-pause姿势一：hook dex 加载点（推荐）梆梆最终也要把明文 dex 交给系统加载。Android 运行时的必经之路是 libart.so 里的 DexFile 相关函数：function hookDexLoad() { var patterns = ['*!*OpenMemory*', '*!*OpenCommon*', '*!*DexFile*']; var resolver = new ApiResolver('module'); patterns.forEach(function(p) { resolver.enumerateMatches('exports:' + p).forEach(function(m) { if (!m.name.includes('libart')) return; try { Interceptor.attach(m.address, { onEnter: function(args) { // 参数布局随 Android 版本不同，一般前两个参数是 base+size for (var i = 0; i < 4; i++) { try { var addr = args[i]; if (addr.readU32() === 0x0a786564) { // "dex\n" 小端 var size = addr.add(0x20).readU32(); console.log('dex @ ' + addr + ' size=' + size); send({type: 'dex'}, addr.readByteArray(size)); } } catch(e) {} } } }); } catch(e) {} }); }); }在参数里找 dex 魔数，找到就按头部 fileSize 字段完整 dump。姿势二：内存扫描兜底加载点 hook 失败（符号混淆严重）时，退回内存扫描：setTimeout(function() { Process.enumerateRanges('r--').forEach(function(range) { if (range.size > 20 * 1024 * 1024) return; // 跳过超大区域防卡死 try { Memory.scan(range.base, range.size, '64 65 78 0a 30 33 35 00', { onMatch: function(addr) { var size = addr.add(0x20).readU32(); if (size > 1024 && size < 50 * 1024 * 1024) { // 尺寸合理性校验 send({type: 'dex'}, addr.readByteArray(size)); } }, onComplete: function() {} }); } catch(e) {} }); }, 5000);尺寸校验很重要——内存里有大量误命中，fileSize 在合理区间（1KB~50MB）才是真 dex。姿势三：hook 文件操作抓落地文件梆梆某些版本会把解密后的 dex 写到 /data/data/包名/ 下的临时文件再加载。hook 文件 API 直接截胡：var open = Module.findExportByName('libc.so', 'open'); Interceptor.attach(open, { onEnter: function(args) { var path = args[0].readCString(); if (path.indexOf('.dex') >= 0 || path.indexOf('.jar') >= 0) { console.log('[open] ' + path); // 记下路径，稍后直接 adb pull } } });看到落地路径后 adb pull 即可，连内存 dump 都省了。精髓：dump 后的验收清单数量对不对：梆梆可能把原 dex 拆成多个，dump 出的 dex 数量要和 jadx 打开 APK 看到的"缺失程度"对照——业务类全了才算完。方法体空不空：梆梆企业版有方法体抽取，jadx 里看到大量 throw new RuntimeException() 占位就是中招了，需要主动调用回填（Youpk/Fart 路线）。Manifest 里的入口要改回：重打包时把 Application 从壳的 ApplicationWrapper 改回原 App 的入口（从 dump 出的 dex 里找）。总结先过反调试（TracerPid/frida 特征），否则 attach 即闪退首选 hook libart 加载点，内存扫描兜底，文件截胡看运气dump 后验收三件套：dex 数量、方法体完整性、Manifest 入口方法体抽取是企业版分水岭，中招就得上主动调用式脱壳交流微信：run1255
先认清壳的结构360 加固（jiagu）的核心思路：原 dex 加密后藏在壳 dex 的资源/资产里，运行时壳先启动，解密原 dex 并通过自定义 ClassLoader 加载。脱壳的本质就是等它解密加载完成后，从内存里把明文 dex 抠出来。判断是不是 360 加固：jadx 打开 APK，入口 Application 是 com.stub.StubApp，assets 里有 libjiagu.so / libjiagu_a64.so，基本实锤。脱壳时机：等"解密完成"这个窗口壳的工作流程：StubApp.attachBaseContext → 解密 dex → 替换 ClassLoader → 启动原 Application。明文 dex 在内存中的存活窗口从解密完成开始，到进程结束。所以脱壳脚本要在原 Application 的 onCreate 之后动手——这时所有 dex 都已解密映射。方案一：FRIDA-DEXDump 暴力搜内存最省事的方案，用 FRIDA-DEXDump 工具直接扫内存里的 dex 魔数：frida -U -f com.target.app -l agent.js # 进入 REPL 后// agent.js 核心逻辑：搜内存里的 "dex\n035" 魔数 Java.perform(function() { setTimeout(function() { // 等壳解密完成 Process.enumerateRanges('r--').forEach(function(range) { try { Memory.scan(range.base, range.size, '64 65 78 0a 30 33 35 00', { onMatch: function(address, size) { console.log('发现 dex @ ' + address); // 读 dex 头里的 fileSize 字段（偏移 0x20） var fileSize = address.add(0x20).readU32(); var dex = address.readByteArray(fileSize); send({type: 'dex', addr: address}, dex); }, onComplete: function() {} }); } catch(e) {} }); }, 3000); });Python 侧接收内存块存成 .dex 文件。搜出来的通常有多个：壳自己的 dex、原 App 的 dex（可能拆成多个）、框架 dex。按大小和包名分辨。方案二：hook 解密函数精准拿360 加固的解密在 native 层（libjiagu.so），但 dex 最终要交给 Android 运行时。hook 运行时的 dex 加载点更精准：// hook libart 的 OpenMemory / DexFile 构造 var openMemory = Module.findExportByName('libart.so', '_ZN3art7DexFile10OpenMemoryEPKhmRKNSt3__112basic_stringIc...'); // 符号名随 Android 版本变化，用 frida 的 ApiResolver 模糊匹配： var resolver = new ApiResolver('module'); resolver.enumerateMatches('exports:*!*OpenMemory*').forEach(function(m) { if (m.name.includes('libart')) { Interceptor.attach(m.address, { onEnter: function(args) { var base = args[1]; // dex 内存基址 var size = args[2]; // 大小 send({type: 'dex'}, base.readByteArray(size.toInt32())); } }); } });精髓：dump 之后的修复才是关键内存 dump 出来的 dex 经常不能直接分析，360 加固会做两类破坏：方法体抽取（VMP 级防护）：部分方法的 CodeItem 被清空，运行时才回填。dump 出来的 dex 里这些方法是空的（No instructions）。修复办法是 hook ArtMethod 的 invoke，在方法被调用时把 CodeItem dump 出来回填——这就是"主动调用式脱壳"，工具如 Youpk、Fart 走这个路线。字符串加密：jadx 打开看到 StubApp.getString(12345) 之类的调用，真实字符串运行时才解密。批量修复要写脚本模拟解密函数回填，或直接用 Frida 在运行时批量调用 getString 建映射表。验证脱壳成功# 看 dex 里有没有业务包名 strings dump.dex | grep "com/target/app" | head # jadx 打开看入口 Activity 是否有真实代码 jadx-gui dump.dex总结360 加固特征：StubApp 入口 + libjiagu.so脱壳时机：原 Application onCreate 之后，dex 全部解密映射暴力方案搜 dex\n035 魔数，精准方案 hook libart 的 OpenMemorydump 只是第一步，方法体抽取和字符串加密才是修复的主战场交流微信：run1255
frida-ios-dump 砸壳实战：从 App Store 包到可分析 IPA
为什么 iOS 逆向第一步是砸壳App Store 下载的 IPA 里，二进制被苹果的 FairPlay DRM 加密——直接拖进 IDA/Hopper 看到的代码段全是密文，无法静态分析。砸壳（decrypt）的本质：利用系统加载器解密后的内存镜像，把明文 dump 出来重打包。前提：一台越狱设备（iOS 13-15 的 checkra1n/palera1n 时代设备最顺手）。frida-ios-dump 标准流程# 1. 设备上起 frida-server（iOS 版），USB 连接 # 2. 电脑上 pip install frida-ios-dump cd frida-ios-dump # 3. 端口转发（USB 映射 frida 端口） iproxy 2222 22 # 或 usbfluxd # 4. 列出设备上的 App python3 dump.py -l # 5. 砸壳（-H 指定本机，走 SSH 隧道） python3 dump.py -H 127.0.0.1 -p 2222 com.target.app跑完得到脱壳后的 IPA。原理：Frida attach 到目标进程，等系统完成 FairPlay 解密（进程启动时内核自动解密代码段），然后从内存把明文的 __TEXT 段抠出来，写回二进制文件，同时修改 cryptid 标志为 0（告诉系统"这是未加密二进制"）。验证砸壳成功# 解开 IPA 看主二进制 unzip -o dumped.ipa -d dumped/ cd dumped/Payload/Target.app # 检查加密标志：cryptid 0 = 已脱壳 otool -l Target | grep -A 4 LC_ENCRYPTION_INFO # 输出 cryptid 0 即成功 # 拖进 Hopper 能看到正常汇编，class-dump 能出头文件 class-dump -H Target -o headers/精髓一：framework 和扩展也要砸主二进制砸完只是开始——现代 App 大量代码在动态库里：Payload/Target.app/Frameworks/*.framework：Swift 库、三方 SDKPayload/Target.app/PlugIns/*.appex：分享扩展、小组件frida-ios-dump 会尝试一起砸，但经常漏。检查方法：对每个 framework 二进制跑 otool -l | grep cryptid，非 0 的单独补砸。漏砸的 framework 在静态分析时是黑盒，关键逻辑（比如签名）很可能就藏在里面。精髓二：砸壳时机的坑FairPlay 解密发生在代码段被加载进内存时——懒加载的代码页在第一次访问时才解密。frida-ios-dump 通过遍历内存映射强制加载所有页来规避，但极端情况下（超大 App、内存压力导致页被换出）会砸出不完整的二进制。对策：砸壳前在 App 里把主要功能页面都点一遍（让更多代码页驻留内存），再执行 dump。精髓三：M 系列 Mac 的"免越狱"路线没有越狱设备时的替代方案：M1/M2/M3 Mac 可以直接运行 iOS App（开发者允许的话），用 poormans 类工具或 bagbak`（需越狱）……实际上 M 系列上的主流方案是：用 Apple Configurator 2 或第三方工具从 App Store 抓 IPA（加密状态）在 M 系列 Mac 上运行该 IPA（需要绕过签名限制）用 frida macOS 版 attach 进程，从内存 dump 解密后的二进制这条路绕开越狱但限制多（App 开发者可以禁止 Mac 运行），能走越狱设备还是优先越狱设备。砸壳之后干什么拿到明文二进制 + class-dump 头文件，iOS 逆向才算真正开始：头文件是地图：SignManager.h、CryptoUtils.h 这种类名直接指路Hopper 伪代码：OC 方法按 F5 看伪 C，比汇编友好十倍Frida hook OC 方法：ObjC.classes.SignManager['- getSign:'] 直接拦截总结FairPlay 加密的 IPA 必须砸壳才能静态分析，原理是 dump 解密后的内存镜像frida-ios-dump + 越狱设备是标准流程，cryptid=0 验证成功framework 和 appex 要逐个检查补砸砸壳后 class-dump 头文件是分析地图交流微信：run1255