零基础Android逆向:360加固脱壳实战指南
很多人接触Android逆向第一道坎就是碰上加固的App。光拿jadx打开一看全是壳代码真正的业务逻辑影子都见不着。今天这篇就专门解决“零基础怎么脱360加固”这个问题把完整思路和实操过程都摊开讲清楚。我自己第一次处理这种样本的时候也是一头雾水后来反复对比了几十种方案最后沉淀出一套稳定好用的流程这篇就按这个流程来讲。先说清楚一个概念加固包说白了就是把原本的dex文件加密藏起来等App运行的时候再在内存里解密。咱们要做的事情就是在它解密完、还没开始执行业务逻辑的那一刻把内存里的dex“截胡”下来这个过程在圈里叫“脱壳”。这篇内容适合谁对被加固App束手无策的初学者、想搞懂Dex加载原理的开发者、以及做安全研究的朋友。不需要你有多深的底层功底但至少得看得懂Java或Kotlin的基础代码知道Android四大组件是什么概念。后面涉及的工具都是免费开源的跟着一步步装好就能跑起来。1. 整体思路拆解为什么硬碰硬不行得绕道内存1.1 加固包的本质外衣底下藏着什么360加固的实现方式和市面上大多数商业加固方案思路一致原始的classes.dex会被加密成一段密文放进so文件或者assets目录然后用一个新的、体积很小的classes.dex替换掉原来的入口。这个新的dex里只有一个壳的Application类真正的业务代码全部“休眠”状态。所以直接用反编译工具打开一个加固App你看到的通常长这样只有几个类比如com.stub.StubApp或者com.qihoo.util.StubApplicationAndroidManifest.xml里的Application被替换成壳的入口业务类一个都看不到这个阶段好比快递包裹只看到了外层的纸箱里面的商品在另一个仓库里存着。1.2 脱壳的三个流派静态、动态、混合老手们在长期实战中总结出了三条路每个方案都有它的优缺点我列个表对比一下你可以根据自己的设备情况选。方案原理优点缺点适用场景静态脱壳直接解析so文件还原解密函数速度快、不依赖运行环境加壳版本更新后算法经常变设备情况有限、需要批量处理动态dump运行App后在内存中抓取dex通用性强、操作直观需要root或模拟器环境学习原理、分析单个样本Hook脱壳用Frida等工具hook关键函数可控性强、可定制需要编写脚本、对新手稍难过反调试、对抗强化壳我建议零基础的人直接从方案二动态dump入手因为它的成功率高逻辑也好理解。等弄明白了再进阶去玩Frida那套。1.3 为什么选择模拟器而不是真机动态脱壳理论上真机也能跑但实操过程中特别容易翻车。调试器一挂上去壳的反调试策略就触发了App直接闪退。模拟器相对而言可控性强得多还能随时保存快照搞坏了大不了恢复一下。模拟器选型上推荐用Android Studio自带的AVD或者第三方模拟器都可以。不过说实话后面步骤里我会把Frida的server用root权限跑起来这三者配合的坑还是不少。这里先给个结论优先用Android Studio自带的AVD系统镜像选Android 7.0或8.0ARM架构这套组合在兼容性和反调试对抗上平衡得最好。2. 工具清单与环境搭建把“手术台”先支起来2.1 必须准备的工具和各自的作用脱壳这条流水线上每个工具都干一件事缺一不可。整理一份我日常用的清单工具版本建议用途获取方式Android Studio最新稳定版创建模拟器、看日志官网下载jadx1.4.7及以上反编译查代码GitHub开源Frida14.x或15.xHook内存、dump dexpip安装dex2jar最新转换dex为jarGitHub开源010 Editor任意版本查看二进制、修复文件头官网Apktool2.7.0资源解码、Manifest查看GitHub开源工具不需要贪多这6个就能覆盖整个流程。后面每一个的作用都会在对应步骤里说明这里先装好。2.2 一步步搭建脱壳环境第一步找到Android Studio的SDK Manager安装一个Android 7.0的模拟器镜像。我的安装经验是别装最新的API Level太新的系统对frida-server的兼容性反而不好Android 7.0API 24或Android 8.0API 26都行。注意模拟器的CPU架构务必选择x86_64如果选了arm架构在PC上跑起来会巨卡无比。第二步用pip install frida-tools装Frida的命令行工具然后去Frida的GitHub仓库里下载和你模拟器架构匹配的frida-server。注意版本必须一一对应比如frida-tools是15.1.0那frida-server也得是15.1.0。第三步写一段简单的Python脚本验证frida和模拟器连通。这里有个小坑模拟器里的frida-server要以root权限跑起来否则后面可能没有权限读进程内存。连不上的话多半是adb端口转发没做。给一个最简单的连通性测试脚本:import frida # 连接模拟器的frida-server device frida.get_device_manager().add_remote_device(127.0.0.1:8888) print(device.name) # 列出正在运行的进程测试是否连通 processes device.enumerate_processes() for p in processes[:10]: print(p.name, p.pid)如果这段脚本能正常列出进程环境就通了。2.3 为什么要在模拟器里先跑一遍App真正脱壳之前我习惯先在干净的模拟器里把目标App安装运行一次。这一步看着不起眼实际上有三个好处确认App在你的环境里能正常运行很多加固App检测到模拟器就直接闪退早发现早解决触发加固壳的完整初始化流程让它在内存里留下解密后的dex顺便把日志输出出来观察壳的运行状态为后续找时机做准备如果App检测模拟器闪退后面处理方案会单独讲先按正常流程走。3. 核心细节剖析Dex在内存里到底长什么样3.1 Dex文件格式的关键特征想在内存海里精准找到解密后的dex必须先知道它在二进制层面有什么标志。每个dex文件的文件头是一段魔数字节64 65 78 0A 30 33 35 00对应ASCII码就是dex\n035\0。这个魔数序列堪称“定海神针”后面咱们在内存里搜索dex就是靠它来定位。整个dex文件的文件头占112个字节里面记录了解释器所需的各种偏移量比如string_ids、type_ids、class_defs这些表的偏移和大小。Dex文件是分区排布的头部之后依次是字符串表、类型表、原型表、字段表、方法表、类定义表和数据区。也正是因为这种高度结构化的布局脱壳dump出来的dex还需要拿到真实的内存地址和大小才能完整还原。3.2 脱壳的黄金时机onCreate还是handleLoadDex脱壳成败的关键在于你什么时候去内存里捞dex。太早了壳还没解密完全太晚了dex可能被内存回收或者被壳自己的保护机制覆盖。360加固壳的加载流程大致是这样的public class StubApp extends Application { protected void attachBaseContext(Context context) { // 真正的Application在这里加载dex从so里解密后载入 nativeReloadDex(); super.attachBaseContext(context); } }核心动作在attachBaseContext阶段壳会调用native方法解密并加载真正的dex。这时候内存里会短暂出现一个完整的、未受保护的dex镜像。零基础做脱壳我的建议是观察ClassLoader的加载时机——在onCreate之前出手成功率最高。3.3 为什么要同时监控多个内存区域一些新手脱壳时只盯着Java堆内存找。但实际情况是壳可能把dex放在native堆区或者匿名内存映射区域。只盯一个区域很可能就漏掉了。我在实战中的做法是用Frida注入后扫描/proc/pid/maps里面所有可读且可执行的区域逐一检查。这就要用到下面这个核心原理——通过扫描内存块地址来定位dex。4. 实操过程全记录从dump dex到jadx打开可读代码4.1 用Frida脚本找到内存里的dex魔数先写好一个Frida脚本核心逻辑是在目标进程的内存空间里搜索刚才说的那个dex魔数一旦命中就把整个dex块dump下来。这个脚本我一直在用已经在各种加壳样本上验证过多次。// attach.js function findAndDumpDex() { // 打开进程的maps文件读取内存映射 var maps Process.enumerateRanges(r--); for (var i 0; i maps.length; i) { var range maps[i]; if (range.file range.file.path range.file.path.indexOf(/data/app/) -1) { continue; } // 以只读方式打开内存区域 var r new MemoryRange(range.base, range.size); r.region.matchAll(6465780A33353500, { onMatch: function(match) { // 命中魔数后尝试从当前地址dump整个dex var dexAddr match.address; try { // dex header里的file_size字段偏移0x20处 var dexSize Memory.readU32(dexAddr.add(0x20)); if (dexSize 0x100 dexSize 0x10000000) { var file new File(/tmp/dump_ dexAddr .dex, wb); file.write(Memory.readByteArray(dexAddr, dexSize)); file.close(); console.log([] Dumped dex at dexAddr , size: dexSize); } } catch (e) { // 有些内存区域不可读跳过 } } }); } } setImmediate(findAndDumpDex);这段脚本放在脱壳流水线里核心作用就是扫描和dump。接下来说说它的执行思路先遍历内存映射只挑那些和App数据目录挂钩的区域然后搜魔数。命中后从dex文件头的0x20位置读出file_size字段校验一下合法范围再存文件。提示dump出来的dex可能不止一个一般会有多个。都需要保留后面逐个检查哪个是真正的业务dex。4.2 执行完整的脱壳流程整个流程我梳理成五步每一步都有明确的输入输出照着做基本上不会跑偏。第一步启动模拟器确认frida-server在跑然后安装目标App。我这里用一个测试样本演示你们用自己手里的App就行。adb install target.apk # 启动frida-server注意端口转发 adb forward tcp:8888 tcp:8888 adb shell su -c /data/local/tmp/frida-server -l 0.0.0.0:8888 第二步写一个Python脚本负责拉起Frida并加载上面的attach.jsimport frida import sys def on_message(message, data): if message[type] send: print(message[payload]) device frida.get_device_manager().add_remote_device(127.0.0.1:8888) pid device.spawn([com.sample.target]) session device.attach(pid) with open(attach.js, r, encodingutf-8) as f: script session.create_script(f.read()) script.on(message, on_message) script.load() device.resume(pid)这里用了device.spawn先挂起进程等Frida注入完成后再resume目的是确保壳在跑起来之前咱们的工具就已经就位。第三步等待App完全启动脚本会在内存里扫到dex并自动dump到/tmp目录。如果这一步没输出说明时机不对你得在模拟器里手动点几下界面触发壳的加载逻辑。第四步把dump下来的dex文件拉回本地adb pull /tmp/dump_0x7f001234.dex ./第五步用jadx打开看效果。这里有个细节jadx可以直接打开dex文件不用非得转jar。命令行打开或者GUI打开都行jadx-gui dump_0x7f001234.dex如果一切正常你会看到原本被藏起来的业务代码全部现身了。4.3 如果dump下来的dex是坏的dex修复三板斧第一次做脱壳十有八九dump出来的dex打开是报错的。常见报错就三种对应的修法也不太一样报错现象原因修复方法文件头不对dump起点偏了往前找对齐的dex魔数首地址文件大小异常读到的file_size不准确用dex文件头辅助字段推算大小类结构错乱dump过程中内存被修改在壳解密后的稳定期再dump文件头不对这个问题最常见的场景是内存里存在多个魔数挨得很近脚本命中第一个但位置不对。我的处理方式是在命中魔数后往前偏移64字节扫描一遍看是否是完整的dex头部。dex大小异常的情况需要手动用010 Editor打开对比dex header的file_size和data_off字段如果明显小于dump出来的实际内容长度直接修正文件头里的file_size即可。这个修法虽然不够“优雅”但dumped dex大多都能救回来。4.4 从dex到可读源码jadx的进阶使用技巧jadx打开dex只是第一步很多时候你还需要从“能看”到“看得懂”。下面这几个技巧让我节省了大量时间在Preferences里打开Show inconsistent code选项遇到反编译失败的方法能显示为字节码用CtrlL跳转到某个类时可以直接输入完整类名jadx会智能匹配按ShiftF3全局搜索字符串可以快速定位敏感API比如网络请求地址、密钥在代码中的位置如果代码里大量出现try/catch包裹可能是控制流混淆这时候看smali可能比看Java更清晰jadx只是一个大前提具体能不能把脱出来的dex还原成可读的Java代码还取决于壳有没有做额外的混淆。360的基础版一般不做重度代码混淆所以脱完基本能看懂。如果遇到其他厂商的壳后续可能还得配合去混淆一起做。5. 高频问题与真实踩坑实录给新手的一条避坑路线图5.1 零基础最经常碰到的三类问题我见过太多新人在同一批坑里反复跌倒这里集中整理一下按出现的频率排序。问题一Frida连不上模拟器这个九成是端口或版本不一致。frida-server跑起来了但Python端连不上先检查两点服务端启动时有没有加-l 0.0.0.0:8888参数frida版本和frida-tools版本是不是严格一致。版本错位是最隐蔽的坑我遇到过14.x和15.x混用脚本死活跑到一半报错。问题二无法Attach到App进程这个多半是因为App启动了反调试。观察一下是不是App启动几秒后自动退出如果是就得在壳初始化之前用spawn方式抢跑或者用-f参数冷启动。问题三dump下来的文件打开报错正如上一节说的先尝试修复文件头。如果修不好重新运行一次App换个时机再dump。多试几次总能找到正确时机。5.2 360加固不同版本的差异和应对策略360加固作为一个持续更新的产品不同版本的脱壳难度差异很大。我这边实操过的版本情况旧版本2.x之前没有反调试直接frida就能怼3.x版本加入了反调试要用spawn方式抢跑脚本也要精简避免暴露特征4.x之后的新版本部分样本会做vmp保护纯dump的内存数据需要再修复才能用面对新版壳我建议多关注加固壳App启动时加载的so文件列表很多时候壳的真正解密逻辑在子so里而不是主so里。配合Frida跟踪一下Native函数的调用能少走很多弯路。5.3 高性价比学习路线会脱壳之后该学什么脱壳只是逆向的入门仪式真正难的是脱完之后的代码分析。我给零基础的学习路线建议是第一阶段把jadx玩溜。对着脱壳后的代码搞清楚Activity跳转、API调用、资源获取第二阶段自己写一个小App用360加固加一遍再来脱。这样你能对照原始代码验证脱壳是否正确第三阶段深入理解Android类加载机制搞懂为什么加固壳要这么设计对你理解系统底层大有裨益这样学的好处是每一步都有确定性的反馈不会卡在某个环节盲目乱试。5.4 最后的经验之谈脱壳这门手艺说白了就是“对抗与反对抗”的拉锯战。你在找壳的破绽壳的开发者在不断堵漏洞。我的切身体会是不要在某个版本的工具上死磕随时关注社区动态新版本壳出来之后过段时间就会有对应的脱壳方案出现。平时多存几个备用工具和脚本关键时候能派上大用场。另外说句实在话学习和研究逆向技术请一定把目光放在自己开发的App、有授权的样本、以及公开的开源项目上。把技术用在正道上才能在遇到问题时和人交流时获得更多帮助。我整理的这套流程本质上是让你理解Android运行机制的一把钥匙而不是教你怎么去搞别人的东西。工具本身没有立场用的人有分寸就好。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →