ARTICLE DETAIL

资讯详情

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

Android应用脱壳实战:从加固原理到Frida动态Dump全流程解析

Android应用脱壳实战:从加固原理到Frida动态Dump全流程解析 逆向这份工作干久了你慢慢就会发现拦在你面前的不一定是奥数题而是一道道厂商精心打磨的“壳”。不管是做自家App的安全防护验证、恶意样本分析还是纯粹研究Android加固技术Apk脱壳都是绕不开的一环。而梆梆加固作为国内老牌的移动应用加固方案出现频率一直很高不少刚入门的兄弟都在问怎么拆掉这一层保护。这篇文章我准备把“如何从梆梆加固的APK里拿到原始Dex”这件事讲透——从加固原理入手再到环境搭建、两条脱壳实操路径快速扫描与精确Hook、最后是Dex修复与常见坑位排查。适合刚接触移动逆向的新手也适合想系统性理解脱壳原理的进阶玩家看完之后至少能动手跑通一个完整流程。1. 先看懂“壳”是怎么套上的1.1 加固到底做了什么普通APK的Dex是明文躺在classes.dex里的用jadx、GDA这类工具直接就能解析出Java层逻辑。加固做的事情可以用一句话概括把原来能直接看的Dex藏起来替换成一个“启动器”。这个启动器在App运行时申请一块大内存从so库里解密出真正的Dex文件再交给ART虚拟机加载。以我测过的梆梆加固样本为例它的核心机制大致是这样的原始Dex加密后被塞进assets目录或者藏在某些自定义后缀的资源文件里新APK里的classes.dex是“壳Dex”体积很小作用只是引导壳so完成初始化。这个壳so在Application的attachBaseContext阶段或者JNI_OnLoad阶段用自研的解密算法常见组合有AES、异或、RC4把原始Dex解密出来然后调用ART底层的DexFile::OpenMemory把Dex装载进运行时。这一步走通之后真正要分析的业务代码只在内存里有明文。你直接用jadx打开加固后的APK看到的只有一堆壳相关类以及完全不可读的密文。这就是为什么很多人拿到加固样本会一脸懵不是工具不行是因为目标压根不在磁盘上。1.2 脱壳的本质把“内存里的明文”截下来既然运行时Dex终归要出现在内存里脱壳的思路就非常直白——在正确的时间点把内存中的完整Dex捞出来。实际操作中难点就两个一是时间点。要在加固解密完成之后、Dex被载入ClassLoader之前或之后立刻抓取。时机太早Dex还是密文太晚有的加固会把解密后的缓冲区主动擦掉或者通过内存重映射增加dump难度。二是完整性。Dex在内存中的布局和文件不一样。有的加固会在内存里对Dex的部分区域做延迟解密也就是常说的“函数抽取”单纯dump内存拿到的可能是残缺的Dex方法体全是空壳或跳转桩。好在梆梆加固的多个版本都偏向整体加密经典版本并不依赖函数抽取整体dump往往可行。这也是为什么不少人都说拿梆梆练手脱壳是新手友好路线。当然不同版本难度差异很大新版加了反调试、反Frida检测之后这条路就不是几分钟能走通的了。2. 战前准备一套能跑的脱壳环境2.1 模拟器、root与Frida——三项缺一不可脱壳本质是动态分析用静态工具直接读APK没有意义。你需要准备这样一套环境模拟器或真机推荐Android 7到Android 9的系统。这个区间ART内部结构相对稳定Frida的适配也成熟很多早期加固的兼容性正好落在这个区间。root权限Frida需要注入目标进程没有root会卡在权限限制上连接和注入都会失败。Frida环境宿主机安装frida-python设备上装对应版本的frida-server。这里有一个最常见也最容易踩的坑frida-server版本必须和宿主机frida版本严格对应不一致会报protocol error连不上设备。脱壳工具下面会给两条路线第一条用现成的frida-dexdump做扫描第二条手写Hook脚本做精确dump。2.2 怎么确认目标确实是梆梆加固拿到一个APK不要急着搭环境。先去确认加固类型否则后面所有操作都可能跑偏。最土的办法是用jadx打开APK搜索特征字符串。梆梆加固的壳类一般长这样com.secneo.apkwrapper.ApplicationWrapper。看到这个Application类基本可以断定是梆梆加固。与此同时去lib目录下找特征so通常会出现libSecShell.so文件。assets里大概率还有一些看不出格式的加密文件。这几个特征同时出现就可以放心按梆梆的思路去处理。顺带提醒一句不同代际的梆梆加固在特征细节上会变化有的新版本会把特征字符串做混淆但Application被替换成Wrapper这件事骗不了人依然是快速识别的信号。2.3 搭建ROP连招host与设备连通环境搭建我用模拟器举例。假设你已经装好了夜神或者雷电模拟器或者Genymotion里的Android 7/8镜像接下来要给模拟器装Frida。# 先把frida-server推到设备里。注意模拟器CPU架构。 # 如果是x86架构的模拟器选frida-server的x86版本 # 如果是ARM盒子和真机选arm64版本。 adb push frida-server-16.x.x-android-x86 /data/local/tmp/ # 给执行权限跑起来 adb shell chmod 755 /data/local/tmp/frida-server adb shell /data/local/tmp/frida-server # 宿主环境验证是否连通 frida-ps -U看到进程列表输出说明Frida已经能用了。这里再强调一次版本匹配问题宿主机执行pip install frida16.x.x和frida-tools设备上就放对应版本的frida-server千万别混。3. 第一条路扫描方案极速拿Dex3.1 frida-dexdump的工作原理frida-dexdump是一个基于Frida的Python工具它的思路是“地毯式搜索”不是去Hook某个特定函数而是把目标进程的整个内存空间扫一遍寻找Dex文件的头部特征——dex\n035\0这种魔数。发现匹配之后把内存块整体dump下来。这样做的好处非常实际不需要知道当前加固具体调用了哪个ART版本、哪个函数符号只要App运行起来Dex在内存里被解密过它就极大概率能扫出来。对梆梆加固这种整体加密型方案来说这条路的成功率相当高而且速度很快适合先拿结果、再分析。3.2 实操命令安装工具和执行都很快pip install frida-dexdump然后有两种启动方式。推荐先启动App再附加进程因为有些加固的类加载是异步的先启动能让所有解密逻辑跑完# 方式一直接冷启动并自动附加 frida-dexdump -U -f com.example.app # 方式二App已经打开按进程名附加 frida-dexdump -U -n 应用进程名跑完之后当前目录下会生成一堆类似dump_xxx.dex的文件。这些就是扫描到的所有Dex镜像。3.3 从一堆Dex里找到关键的那个扫描方案经常一次性dump出好几个Dex这并不代表全部是核心业务Dex。其中至少有一个是正确的核心Dex其他的可能是壳自身Dex、多Dex工程里的分包或者扫描到的残留数据。怎么判断哪个是核心最直接的方式是逐个用jadx打开看类结构。核心业务Dex通常体积最大包名和类名符合原始App的业务特征。另外也可以直接在dump目录里grep业务包名关键词。只要Dex没被破坏jadx打开后就能看见熟悉的包路径。这里有句经验之谈如果扫描出来的Dex用jadx打开是空的、或者只有一个Application类说明dump时机不对或者壳做过二次处理。这时候不要硬刚扫描方案直接转下一章的Hook方案。4. 第二条路精确Hook在ART加载点截胡4.1 为什么还需要Hook方案扫描方案虽然快但本质是“盲扫”可能在内存持久化、防dump处理前抓不到也可能抓进来一堆垃圾数据。更关键的是它只告诉你“哪里有Dex”不告诉你“加固是怎么加载它的”。如果碰到加固在Dex加载完成后做了内存擦除、重映射扫描方案就会失效。精确Hook是另一个维度的思路不猜直接在Dex被ART解析的那一刻从函数参数里把完整数据拿到。这就像等在唯一出入口看到一个亲笔签名直接把包裹截下来。4.2 找到ART的Dex加载入口ART虚拟机里Dex进入加载流程通常经过DexFile::OpenMemory或DexFile::OpenCommon函数。不同Android版本函数符号有差异mangled name也不同。好在用Frida可以直接遍历libart.so的导出表动态匹配函数名。下面这段脚本用来在目标进程里查看本机ART导出的相关符号// 仅用于查看当前Android系统上的ART符号 // 保存为 find_symbols.js Java.perform(function() { var art Process.findModuleByName(libart.so); if (!art) { console.log(libart.so not found); return; } art.enumerateSymbols().forEach(function(s) { if (s.name.indexOf(OpenMemory) ! -1 || s.name.indexOf(OpenCommon) ! -1) { console.log(s.name s.address); } }); });把这段脚本跑一下你就能看到当前系统实际可用的符号名。在Android 7/8上比较常见的mangled name里会有OpenMemory和参数类型Android 10以后ART改动大搜索逻辑可能要扩展到DexFile构造函数相关的导出符号。4.3 一个可商用的Hook脚本找到符号后用Interceptor在onEnter阶段读取参数。以老标准为例OpenMemory的参数里第一个参数是指向Dex文件内容的内存指针第二个参数是Dex大小。onEnter时拿指针onLeave或onEnter后立刻读内存把数据发回宿主机保存。下面是一个可直接运行的示例脚本我建议用Python来做接收和落盘比直接在Frida脚本里写文件更稳定import frida import sys import time package_name com.example.app script_source use strict; function findOpenMemory() { var art Process.findModuleByName(libart.so); if (!art) return null; var symbols art.enumerateSymbols(); for (var i 0; i symbols.length; i) { var name symbols[i].name; if (name.indexOf(OpenMemory) ! -1 || name.indexOf(OpenCommon) ! -1) { return {name: name, addr: symbols[i].address}; } } return null; } var target findOpenMemory(); if (target) { console.log([*] found: target.name); Interceptor.attach(target.addr, { onEnter: function(args) { // 第二个参数是 dex 长度 this.bufPtr args[0]; this.size args[1].toInt32(); }, onLeave: function(retval) { if (this.bufPtr ! null this.size 0) { send({ type: dex, name: dex_ Date.now() .dex, size: this.size }, this.bufPtr.readByteArray(this.size)); } } }); } else { console.log([-] OpenMemory/OpenCommon not found); } def on_message(message, data): if message[type] send: payload message[payload] if payload[type] dex: filename payload[name] with open(filename, wb) as f: f.write(data) print([] saved:, filename) else: print([*], message) device frida.get_usb_device() session device.attach(package_name) script session.create_script(script_source) script.on(message, on_message) script.load() # 给dump一些时间 time.sleep(3) session.detach()这段脚本会把hook到的Dex直接保存到当前目录。用的时候注意两点一是size参数在个别版本里可能不是第二个参数遇到问题要先打印所有args比对二是不要只等一个Dex可以多触发几次App的页面跳转、Activity加载让ART加载更多分包然后一起dump。4.4 Hook跑通的坑一个符号吃遍所有版本是不可能的在Android 8/9上ART导出符号还算规矩按关键词匹配OpenMemory就能找到。到了Android 10以上ART内部改得比较多函数名字可能变成DexFile::DexFile或者别的内部符号参数偏移也会变。所以千万不要以为找到一个符号就能通吃全家桶。我个人的处理习惯是先扫描出符号列表再对照当前Android版本的源码确认参数个数和顺序后改脚本。这也是为什么实际工程里很多人最后会选FART、Youpk、FDex2这类开源框架——它们把不同系统版本的兼容问题做了大量适配你只要跑到对应系统上按下按钮就行。5. 常见问题与脱壳后处理5.1 Dump出来的Dex是空的、乱码的、还是打不开的这里把我在实际操作中遇到最多的情况整理成一张速查表方便你排查定位现象可能原因处理方式文件只有几百字节抓到壳Dex或无效内存块过滤后重新dump筛选体积较大的文件jadx打开没有任何类Dump时机过早壳还没完成解密等待App冷启动完成后再dump方法体是空的、只有桩代码加固启用了抽取保护扫描和简单Hook不够需要上FART这类的主动调用脱壳文件能打开但乱码内存中Dex被二次修改或Dump到了非连续内存用Dex修复工具处理按Dex header的file_size重新截断Dump了多次都定位不到关键类关键逻辑不在Java层而在so里换IDA分析so或先找到so加载的关键函数再继续5.2 Dex修复和验证如果是从OpenMemory直接拿的数据结构一般比较完整用jadx打开就能用。但如果是扫描方案拿到的可能带有内存中的其他数据需要修复几步先检查Dex头部魔数确认是dex\n035\0。然后读取偏移0x20处的file_size字段对照实际文件大小如果文件比期望大就按file_size截断。再检查header里的data_off、map_off字段如果指向明显异常就需要用工具重建map。常见工具包括dex-fixer这类Python脚本以及baksmali与smali的重新回编译流程。验证方式很简单用jadx打开修复后的Dex能正常看到业务包名、类名、方法就说明脱壳链路已经打通。5.3 模拟器上秒退、被杀、闪退怎么处理加固产品普遍有反调试和反模拟器能力你在模拟器上跑脱壳环境时经常遇到App启动后几秒就退出。这种情况有两类原因一类是反Frida检测检测到进程被Frida注入另一类是模拟器环境特征被识别。如果是为了分析自己写的APK最干净的建议是在自己测试样本里暂时把加固的检测逻辑注释掉重新打包再测。这种操作在授权范围内非常有效也不用跟驱动层的反调试死磕。如果是分析已经拿到授权的样本更稳妥的方式是换真机并且在受控环境中做分析。这种时候不要一门心思去改Frida的隐藏插件先确认分析授权再决定投入多少精力对抗检测。毕竟对大多数学习场景来说验证原理比追求无限制绕过更重要。5.4 遇到函数抽取型加固怎么办如果Dump出来的Dex方法体全是空壳说明这个版本不是纯粹的整体加密很可能走了函数抽取。所谓抽取就是先把每个方法对应的代码块抽走运行时由壳so在方法第一次执行前再填回去。这种情况下普通整体dump拿到的是“骨架”没有血肉。要对付抽取型加固技术路线就得升级使用FART这类主动调用脱壳机在ART内部遍历每一个方法并通过反射主动调用让被抽取的code item在方法执行前被真实代码回填再在内存中dump完整Dex。这条路的实现复杂度要高一个量级但原理依然是“等它把内容吐出来再截获”。对梆梆新一代的部分版本这条路就是主要的突破口。6. 脱壳之后真正的逆向才刚开始拿到原始Dex很多人觉得战斗结束了其实这只是下一阶段分析的前置条件。用jadx打开脱壳后的Dex你可以开始快速定位关键类、入口Activity、网络协议、加密逻辑。但别忘了很多App的核心算法并不在Java层而是在so层。脱壳解决的是Java层可见性问题如果关键函数在so里用了VMP指令集翻译那后续要面对的就是指令集层面的还原与trace跟踪难度会再上一个台阶。就我个人的体会来说脱壳这件事很少是一次到位的。第一次抓到的Dex往往会缺文件、缺方法体甚至根本打不开全靠反复调试和验证轮着试。等到你把不同Android版本、不同加固方案的差异整理成自己的笔记之后你才会真正觉得“壳的思路我大概摸清了”。建议新手从梆梆这种整体加密的样本入手扫描方案快速拿结果精确Hook方案理解原理之后再一步步过渡到抽取壳和VMP加固。说到底任何逆向分析都要守住授权边界只对你有权分析的样本动手这条路才能走得远。
返回列表