ARTICLE DETAIL

资讯详情

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

Android逆向实战:梆梆加固脱壳全流程解析

Android逆向实战:梆梆加固脱壳全流程解析 1. 为什么需要脱壳一个逆向分析老兵的日常困境作为一个常年跟Android样本打交道的逆向狗我手机里永远躺着两台真机、一个专门跑样本的类原生系统平板以及一台顶着32G内存跑虚拟机的主力电脑。不是装备党而是被逼的。安卓逆向的日常就是和各种各样的App过招但真正让人头疼的从来不是代码逻辑有多复杂而是那一层裹在外面、油盐不进的加固壳。先说说我自己的真实场景。上个月接了一个业务安全风控的项目需要分析某款App里一个比较敏感的算法实现逻辑。按常规流程把Apk拿到手扔进jadx或者用GDA打开结果摆在我面前的是一段完全没法看的代码——看不到任何业务逻辑只剩下几个类无非是Application的壳子、加固SDK自己的初始化调用以及一堆看起来像是加密过、完全没法直接读的byte数组和字符串。再看一眼so文件好家伙又是加固厂商喜欢抽空Dex、把内容挪到native层的套路。这个场景熟悉吗太熟悉了。做过移动端渗透测试、业务风控分析、恶意样本逆向的同学十有八九都撞上过加固壳这道墙。墙后面才是真正想拿到的东西——原始的Dex文件、真实的类结构、业务逻辑源码级别的蛛丝马迹。而我们常说的脱壳字面意思就是把外面这层保护硬壳打碎把被藏起来的核心Dex重新捞出来恢复到可以进行静态分析和动态调试的正常状态。这篇文章我主要拿“梆梆加固”作为研究对象来走一遍完整流程。不是因为某一家特别好欺负而是这个壳在市面上覆盖面广企业用户多而且它的整体思路和Dex保护机制具备非常典型的参考价值。把它的脱壳流程跑通一遍你对市面上75%的同类加固方案基本上都能举一反三。后面涉及的脱壳演示都将严格限定在已获得授权的样本、自研App或者公开的正向安全研究用途中。逆向是门手艺前提是别拿手艺干违法的事。在开始动手之前需要先明确一个问题我们说的“脱壳”到底是在脱什么很多新手刚开始用工具的时候以为脱壳就是执行一条命令然后看着屏幕上跳出“Success就行了。想得太简单了。脱壳的本质是在正确的时间、正确的位置把壳在内存中还原出来的完整Dex镜像想办法弄出来。为了做到这一步你需要理解Dex文件的加载流程、Android类加载机制、壳的入口逻辑、甚至底层so文件的行为特征。不是一个工具能解决的事而是一套方法论。下面先把加壳与脱壳这两边的攻防思路摊开来看清楚只有知道对手把东西藏在了哪里才知道该去哪里翻箱倒柜。2. 加固前后的Apk差异Dex去哪了2.1 一个正常Apk的长相正常的Apk本质上是一个ZIP压缩包里面包含了AndroidManifest.xml、classes.dex、resources.arsc、META-INF签名目录以及各种res资源文件和lib目录下的so文件。我们做静态分析主要目的就是把classes.dex里的结构还原出来——Dex文件里保存了App全部Java/Kotlin层的类定义、方法、字段和字节码指令。jadx这类反编译工具做的事情是把Dex字节码翻译成等价可读的Java代码。正常情况下一个没有加固的Apk你拿jadx打开看到的类层次清清楚楚业务代码是怎么写的几乎可以像读源码一样去读。这也是为什么很多开发者会用混淆、字符串加密、so保护等各种手段来增加分析难度——因为一旦Dex直接暴露在静态分析工具面前代码基本等于裸奔。2.2 加了“梆梆加固”之后发生了什么现在看加壳后的Apk。用解压工具打开你发现原本的classes.dex不再是平滑的、尺寸合理的、可以正常被jdax解析的文件。它可能是几百字节到几KB的打酱油小文件真正的Dex内容不知道跑到哪里去了而被替换成一个经过处理的、加了密的加密数据块。“梆梆加固”这类方案的整体思路是这样的加固厂商会提供一个壳的SDK里面包含了一套专门的解密和加载逻辑。开发者将自己的原始Apk通过加固平台处理加固平台会将原始Dex文件提取出来加密这里一般是AES加密配合自定义异或、偏移处理重新封装成一个包含了加固SDK和加密数据的新的Apk。原来的classes.dex位置会替换成一个壳入口类也就是你在jadx里看到的那几个可怜的类。当你运行加固后的Apk时壳的入口代码会被最先执行。它一般不会出现在普通的Application类里而是用了一个AndroidManifest里的android:name指向的壳加载器在attachBaseContext或者onCreate阶段抢先执行。接下来就是一套标准的招数读取加密数据、解密还原出原始Dex的字节数组、通过自定义的ClassLoader——通常是修改过的DexClassLoader或者InMemoryDexClassLoader——把还原后的Dex加载进内存替换掉系统默认的类加载来源。这就是关键点原始Dex在内存中是真实存在过的。不管你加密算法多么复杂壳无论如何都要在某个执行阶段把完整的Dex内容还原到内存否则方法就没法被解释执行或者编译执行。这也是所有基于“整体加固”思路的壳最大的宿命。我们做脱壳核心目标就是捕捉到这个还原的瞬间从内存里把Dex抠出来。2.3 梆梆加固的双层Dex结构根据近期对多款“梆梆加固”样本的观察此类加固有一个非常明显的特征它往往会构造一个双层Dex的加载结构。第一层是一个极其轻量的入口Dex负责壳自身逻辑和加密数据的解密入口第二层则是真正包含业务逻辑的原始Dex被加密后放在Apk文件的其他位置可能藏在了assets目录下某个看似无害的二进制文件里也可能是文件末尾追加的隐藏数据区。这种结构的好处是对加固厂商来说它可以把原始Dex藏得相对隐蔽给静态分析制造很大的成本。但对于熟悉脱壳的人来说这也等于你事先就知道该去哪里找加载入口、去找哪段解密逻辑。你不需要真的去逆向解密算法本身只需要hook住最终的加载函数在原始Dex刚被解密和加载、尚未被壳做额外处理时把内存中的镜像抓出来即可。3. 脱壳的武器库工具、环境与踩过的坑3.1 工具选型为什么我没有选“一键脱壳”网上常见的脱壳工具多如牛毛早期很流行FART、Youpk、DexDump后面又有黑科技类的FRIDA-DEXDump、oasis、Youpk这一类主动调用型工具。“梆梆加固”的脱壳方法最无脑的可能是拿一个基于定制系统或者脱壳机直接跑一下。但我不建议一上来就上重型工具原因有三重型工具的兼容性不可控。很多脱壳工具依赖特定的Android版本和内核沙箱而加固厂商的检测手段恰恰喜欢盯着常见的脱壳环境特征做对抗——比如检测模拟器、检测Frida特征、检测Xposed框架。一键脱壳是黑盒黑盒意味着你拿到壳里的Dex之后并不理解它为什么能成、为什么会失败。一旦遇到变种束手无策。梆梆加固自身也升级过好几代早期的整体加密脱壳脚本早就失效了现在的版本加入了反调试、反内存dump检测必须用精细化对抗的方式处理。所以我自己的日常方案是组合工具配合手动确认。核心武器如下工具/环境用途备注真机Android 9/10动态运行目标App比模拟器更加接近真实环境降低环境特征检测触发Magisk 隐藏Root提供调试权限但可以隐藏Root状态使用Magisk内置的MagiskHide或者Shamiko模块Frida Objection动态插桩、hook关键加载函数版本尽量新一些且要有对抗Frida检测的方案r0capturedump内存中Dex参考文献式脚本结合Frida运行一键dump classloader里的Dex010 Editor / binwalk对dump文件做手工修复修Dex头、魔数、checksum等字段GDA / jadx静态查看Dex内容检验脱壳结果的完整度3.2 环境准备里最容易忽略的细节第一件要提醒的事情是用真机不要用模拟器。模拟器的硬件特征非常明显Build.FINGERPRINT、CPU指令集、传感器列表任何一个都能被加固SDK以极低的成本识别出来。一旦识别到模拟器壳经常会在解密阶段前就退出或者是给你一个看起来运行正常的假界面但Dex内容实际上根本没有完整解密。我在早期就因为图省事用Nox模拟器卡了整整两天后来换到一台旧的Pixel 3上一次性就跑通了。第二点是Root的隐藏策略。壳不可能完全禁止Root设备运行App——这会丢失大量用户——但它会检测Root状态来决定是否启用更严格的反调试逻辑。所以在真机上要装好Magisk框架并开启MagiskHide或使用Shamiko。另外Frida默认的端口和D-Bus通信协议特征在加固面前等于是穿着“我来了”的衣服大摇大摆。需要一个对抗手段比如用强混淆版的Frida Gadget配合自定义端口和连接方式或者干脆在Frida服务端加一层反检测。第三点是脱壳时机的选择。很多人上来就hook了DexClassLoader构造方法等了半天没动静。原因很简单安装Apk后的首启阶段壳会做完整的解密和加载但如果你让App已经跑了一段时间Dex已经在内存里了再hook加载函数自然没反应。正确做法是先把Frida的脚本准备好在App启动的极早期完成注入然后冷启动目标App。我会把注入Deamon放在zygotefork出去后的第一刻或者用frida -f com.target.app -l hook.js这种以spawn方式拉起App的模式启动这样能保证在Application入口创建前就建立好hook点。3.3 Frida环境搭建中的两个实际教训教训一Frida版本与目标机的匹配。老生常谈但每次都会坑到人。Frida服务端和客户端版本必须严格对应另外Android版本不同可用的Frida指令集也有差异。我一般用Frida 15.x以上的版本配合最新的Gadget兼容性较稳。教训二注入方式。对加壳App做spawn注入时壳会发现/data/local/tmp目录存在可疑文件和进程环境不对导致闪退。我自己的实践是使用Frida Gadget混入App的lib目录方式让壳把Frida当成App的一部分来加载这样大部分检测逻辑就失效了。这些准备工作看着琐碎但它们决定了脱壳动作能不能顺利跑完。任何一步出错后面全部白搭。4. 实战脱壳定位加载点、内存镜像与Dex还原4.1 静态侦察找到壳在Manifest中设置的加载入口拿到一个“梆梆加固”包装的Apk不要急着上Frida先用jadx裸看一圈Apk里能静态看到的东西。虽然业务逻辑都看不到但壳的入口Application类是暴露在AndroidManifest.xml明文里的。用aapt dump badging或者直接jadx打开AndroidManifest.xml看application标签下的android:name属性。正常情况下这里会指向某个看起来奇怪的类比如com.secshell.app.SecShellApplication。对“SecShell”这类命名特征就是梆梆自己的。这一类的入口就是我们要盯住的第一站。在jadx里追踪这个类通常会看到一个比较精简的结构。里面有attachBaseContext(Context)和onCreate()重写它们会调用一个Native方法——一般是来自libSecShell.so或者类似名称的so文件。这个so文件是实现Dex解密和动态加载的核心。静态上来看这里的逻辑基本就是个空壳真正的实现全部隐藏在so层。到这里静态分析能提供的线索已经用得差不多了。接下来要实时追踪解密和加载这个动作得请出Frida。4.2 动态插桩把DexClassLoader和InMemoryDexClassLoader一起hook住市面上的脱壳脚本有很多变体但最核心的hook点其实就是两个类dalvik.system.DexClassLoaderdalvik.system.InMemoryDexClassLoader此外还有一个容易被遗漏的点android.app.Application类中的attachBaseContext以及PathClassLoader的构造过程。老版本壳喜欢走DexClassLoader新版本尤其是Android 8.0以上的环境很多壳开始改用InMemoryDexClassLoader来加载解密后的Dex——此时Dex不是从文件路径加载而是直接从一个ByteBuffer内存区域加载。这个改动对脱壳脚本的影响很大因为如果你只hook了文件路径版本的类加载器是捕捉不到内存中Dex的。我的Frida脚本核心逻辑如下这里给出关键片段不是完整代码但思路是照着可以直接用的Java.perform(function() { var DexClassLoader Java.use(dalvik.system.DexClassLoader); DexClassLoader.$init.overload(java.lang.String, java.lang.String, java.lang.String, java.lang.ClassLoader) .implementation function(dexPath, optimizedDirectory, librarySearchPath, parent) { console.log([DexClassLoader] loading: dexPath); // 这里是关键在你自己的tmp目录里复制一份dex文件副本 try { var file new File(dexPath, r); var content file.readBytes(); // 写入指定目录 } catch(e) { console.log(e); } return this.$init(dexPath, optimizedDirectory, librarySearchPath, parent); }; var InMemoryDexClassLoader Java.use(dalvik.system.InMemoryDexClassLoader); InMemoryDexClassLoader.$init.overload(java.nio.ByteBuffer, java.lang.ClassLoader) .implementation function(buffer, parent) { console.log([InMemoryDexClassLoader] loading from ByteBuffer); // 将buffer内容dump出来 var bufferData Java.array(byte, buffer.array()); // 写入文件 return this.$init(buffer, parent); }; });这段脚本的核心意义是在壳调用类加载器的一瞬间把Dex文件路径/内存内容截获并复制出来。但要注意Dex文件不是一步到位地完整暴露的。4.3 用dex特征定位内存中的Dex镜像不论加固方案多复杂最终放进类加载器里的Dex文件一定符合Dex文件格式的基本约束。Dex文件有一个固定的magic number——十六进制的64 65 78 0a 30 33 35 00也就是字符串dex\n035\0。新版本也可能是dex\n037或dex\n038等但前四个字节的dex\n是稳定的。所以另一个更暴力的思路是直接在App运行起来之后扫描它的整个堆内存找到所有符合Dex魔数的内存区域把它们整体抠出来。这就是一些DexDump工具的工作原理。拿Frida做这件事核心思路是读取/proc/pid/maps找到可读写的堆内存区域遍历并搜索dex\n035特征命中后把整个内存页dump下来。难点在于Dex文件可能横跨多个内存页没有从页边界开始实际的文件大小在头部字段中也有记录file_size字段位于Dex头偏移0x20位置。所以dump时不能简单只抓一页而是要根据扫描到魔数之后读取头部0x20偏移处的file_size值再从魔数位置为起点一次性读取完整的file_size字节。这一步在实战中经常遇到一个问题由于InMemoryDexClassLoader加载的ByteBuffer可能不是完整的Dex文件原始数据壳还会做一步“Dex头部修正或者数据偏移修正”导致直接从内存抠出来的镜像里file_size字段与实际加载到内存的缓冲区长度不一致。此时就需要手工尝试——要么通过修正file_size来匹配实际dump长度要么在内存里搜索真实的Dex尾部通常file_size实际值偏大内存里根本不存在那么长一块完整区域。这个问题我们在下一节展开。4.4 梆梆加固下的一种特殊情况壳把Dex切成了多段接下来这段是本次实战里我认为最有价值的一手经验。脱“梆梆”的时候我按照4.3节的方式扫描到了两个Dex魔数区但dump出来之后发现解析不完整。一个文件能用jadx打开但类不全另一个文件干脆校验失败。这说明什么呢说明壳对原始Dex做了分块处理。具体来说加固壳会把原始Dex的完整结构打散只保留一个最基础的数据块加载到内存中其他部分则延迟解密或者在方法第一次被调用时才动态去解密并回填。这种技术现在被各路加固厂商广泛采用目的就是让传统“整体dump”的策略失效。应对思路也比较明确我们不能只依靠dump一次而是要主动触发方法调用让壳完成更多解密工作。这就要用到Frida的另一个玩法——主动调用遍历。简单说主动调用脱壳的原理是在Dex加载完成后获取到所有已加载的类和方法然后通过反射或者直接调用的方式让每个类的每个方法都执行一遍或者至少触发它的初始化。这样壳必须为这些方法完成解密和补全我们就能在内存中看到完整的Dex内容。这里可以参开FART和Youpk这类主动调用工具的公开思路自己写一个精简版。用Frida遍历classloader中的类调用getDeclaredMethods()逐个setAccessible(true)然后invoke(对象)或者直接method.invoke(null)对于静态方法。当方法被执行时如果壳是在这一层做的解密那么调用之后就能在内存中看到真实数据了。5. 反调试与完整性校验脱壳路上最大的几只拦路虎5.1 反调试有多常用从TracerPid到ptrace自检查先说结论现在“梆梆加固”基本都是多层反调试叠加。最基础的一个检测手段是读取/proc/self/status中的TracerPid字段。正常情况下如果没有任何调试器附加到这个进程上TracerPid的值是0一旦有调试器比如IDA远程调试、gdb、frida-server自身也存在被检测到的可能TracerPid就是调试进程的PID。壳还会使用ptrace(PTRACE_TRACEME)方式自我附加。Linux下同一个进程只能被一个进程ptrace跟踪一旦壳自己先ptrace了自己外部再想attach上去就会失败。这个方法简单粗暴但相当有效。对付这类反调试常见的思路是用Frida在native层hookptrace函数让它直接返回0相当于壳的自我附加动作“假成功”实际并未生效。另外还有更隐蔽的时间差检测壳解密Dex之后设置一个定时器如果解密完成后某些标志位没有在预期时间内被消费掉就判定有调试行为则直接abort退出。这类检测绕过起来更麻烦需要对so层的逻辑做比较精细的逆向分析。5.2 反Frida的检测与对抗让我差点翻车的地方在这次脱壳的过程中让我差点翻车的不是Dex解密部分而是反Frida检测。我最初在目标App上直接用frida-server注入一启动App就闪退。后来经过排查发现壳的so层用了多种方式检测Frida检查/proc/self/maps中是否存在frida-agent的映射。扫描/data/local/tmp/目录下是否存在frida-server。扫描TCP端口27042Frida默认端口。通过入侵检测库 hook 掉pthread_create来发现可疑线程。应对方案将Frida server改名自定义端口通过-l指定端口。使用Frida Gadget通过修改Apk包内lib目录添加libgadget.so并在smali中注入加载逻辑让App自己把Frida给加载进来这样一来壳看到的只是一个普通的第三方native库。或者可以用更强力的Frida变种比如强混淆版本基本没有公开特征。我的选择是修改目标Apk、注入Gadget然后将AndroidManifest中application类替换为自定义的入口类在onCreate里先加载libgadget.so再启动原始壳入口。整个过程可以写成一个小工具下次碰到同类壳就直接用。这样一定程度上绕开了Frida特征检测保障了后续dump的稳定。5.3 完整性校验明明脱出的Dex是完整的App就是要闪退另一个常见现象是你dump出来的Dex看起来是完整的用jadx或者GDA打开类都有了代码也能反编译出来。但你一旦尝试把脱壳后的Dex重打包成新的Apk——也就是修复加固包并重新签名安装——App在启动阶段直接就闪退了。这可能涉及到多重校验加固SDK初始化时会回调服务端鉴权同时校验Apk签名和文件哈希。业务自定义的JNI_OnLoad里会拿到当前宿主Apk的sourceDir路径并对自身做一次SHA256比对一旦发现文件跟原始包不一致就拒绝继续运行。对于这类完整性校验最有效的办法通常不是去硬刚签名校验逻辑而是在重打包时同步hook掉校验函数或者在App启动早期提前加载一个Xposed模块对校验方法进行桩替换。我自己在操刀时一般喜欢先用Frida把所有文件访问相关函数open、stat、fopen的返回值打印出来看看它到底对比哪些文件。大多数情况下壳就会自己暴露校验路径。后续要么在smali层把返回值写死要么在native层用一个inline hook把校验结果垫掉。6. 脱壳后的验证与修复如何确认自己拿到的Dex是真的6.1 jadx静态验证最直接的完整性检验dump出来的Dex合不合格第一关是静态检查工具能不能正常解析。用jadx打开dump文件如果直接报“Dex parse failed”多半是文件头部有严重问题。常见的修复手段如下检查魔数Dex的开头必须是64 65 78 0a 30 33 35 00如果被篡改直接补回。file_size字段偏移0x204字节是否与实际文件大小一致如果不一致就修正它。header_size偏移0x24一般固定为0x70。checksum偏移0x08和signature偏移0x0C是SHA-1和Adler32校验静态分析时不需要和文件内容一致也被很多工具容忍但如果要重打包就必须修复。反编译后重点看几个标志性内容是否能看到MainActivity等业务类、是否有App自己的包名目录结构而不仅仅是壳相关的com.secshell等类、是否能搜到你关心的字符串资源。如果这些条件都满足大概率就是拿到了真实内容。6.2 动态验证从行为反推Dex完整性静态可以过只能说明Dex结构上没问题。更保险的做法是把脱出来的Dex和运行中的类加载器做一次交叉验证用Frida获取当前ClassLoader中加载的类列表跟dump出的Dex中类列表做比对看看有无缺失。另外还有一个简单但实用的判断直接看dump出的Dex中是否有App所有业务的关键类。比如你要分析支付类App那PayActivity、SignHelper这类类如果存在说明核心业务逻辑没有丢失要是这些类一个都搜不到只有壳类和SDK类那基本上就是dump时机太早或者内存中Dex并不是完整状态——你需要重新回到4.4节提到的主动调用路线再跑一轮。6.3 离线修复Dex结构恢复和重打包如果静态验证时发现Dex里某些类的内容还是空缺的方法体是空的、所有函数都返回默认值那说明壳用了抽取型的保护策略抽取壳。这种情况下dump出来的Dex只是空壳骨架真正的方法代码在运行时才动态从native层还原回填到内存。对这类文件纯静态修复的难度要大很多可能需要配合指令修复或者运行时的回填数据采集。我的个人建议是对于抽取壳第一优先方案是直接用定制的Frida脚本在类方法执行完成后dump整个Dex镜像这比离线修复的成功率要高得多。工具方面也可以参考Youpk的思路它本质上就是在脱壳过程中结合主动调用和内存修复把抽取的部分也一并拉回来。“梆梆加固”目前的一些版本已经引入抽取壳特性所以如果你发现自己脱出来的文件出现空方法的情况不要惊讶走主动调用这条路就是了。6.4 保持心态变种与迭代是常态脱壳从来没打“一劳永逸”的算盘。加固厂商会持续更新隔几个月可能就换了一套反调试策略或者把Dex的加密方式改一下。这次写的思路是基于当前观察样本的通用打法过一段可能部分细节就不适用了但核心思想不会变以类加载器为锚点以内容加载时机为突破口。只要它还要在内存中还原Dex就一定有迹可循。你需要做的是不断更新自己的检测脚本跟上加固厂商的步伐。这场猫鼠游戏没有终点但这正是移动逆向有意思的地方。结合我自己多次的脱壳实验分享几条最朴素但却最有效的经验先判断壳版本和加固特征不要上来就套老脚本。保持Frida环境低调反调试检测是最大的变数。dump两次不如主动调用一次不要心疼时间该主动触发就主动触发。拿到Dex后不要急着分析业务代码先做完整度校验确认没有缺类再往下走。文章写到这里技术层面的核心内容已经全部讲完了。我在实际项目中靠这套思路完成了多个加固样本的分析也帮助团队解决过不止一次业务安全上的“疑难杂症”。脱壳说起来是个很“偏门”的技术但真正跑通一遍之后你对Android运行时的理解会上升一个台阶再回头看你写的App很多问题会看得更通透。最后再提一句老生常谈的话研究脱壳最好手里只放自己拥有授权或者公开安全研究用途的样本这条路才会走得持久、走得安心。
返回列表