ARTICLE DETAIL

资讯详情

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

从原理到实战:SO层不固定位置动态加密的逆向分析框架

从原理到实战:SO层不固定位置动态加密的逆向分析框架 上周有个学弟准备Android逆向面试拿着“so层不固定位置的动态加密怎么解决”这道题过来找我。他原话是面试官问完静态分析之后突然转到这一题自己脑子一片空白只记得见过加壳so、脱壳dex但“动态加密”这四个字一出来就不知道怎么下手了。这道题表面是一道问答题实际上是整套native层逆向基本功的浓缩考察你要懂so是怎么加载的、为什么代码能运行时变来变去、怎么用工具在正确的时间抓到正确的内存还得能把“不固定位置”这种看似随机的情况抽象成一套稳定解法。这篇文章我把整套思路拆开讲从原理到实战脚本再到面试时应该怎么组织语言一次说完。目标是让看完的人既能应对面试官的三连追问也能真的去分析一个动态加密的so。1. 先把题目翻译成人话so层动态加密到底在防什么1.1 一句话理解“so层不固定位置的动态加密”先拆词。so层指的是Android里用C/C编译出来的native库一般核心算法、反调试逻辑都放在这一层。动态加密不是apk整体加固那种而是so文件本身被处理过文件里一部分代码以密文形式存在程序运行到某个时刻先在内存里把密文还原成真正的机器码或数据再继续执行。所谓“不固定位置”是题眼意思是这个加密点不是写死的某个函数或某一段可能是随机选择、不同设备不同位置、每次启动不同也可能是多个加密块由一张索引表动态驱动。一句话概括程序运行时代码是“现炒现卖”的。你不能把so直接丢进IDA静态分析因为你看到的文件内容和它真实运行的机器码根本是两份东西。这种做法的目的很直接抬高逆向门槛。常规native逆向是“拿so拖进IDA找到目标函数按F5看伪代码”。如果把关键逻辑加密存储、运行时才还原静态分析能看到的只有残缺函数、跳板指令、垃圾字节。这道题问的就是文件不可信了作为分析者你还能怎么办。1.2 “不固定位置”常见的三种实现方式第一种是分段加密。编译完正常so后把最核心的若干个函数机器码抽走替换成桩代码或随机字节运行时由一个解密函数按索引表恢复。索引表可能是全局数组也可能藏在.init_array里甚至由Java层传入参数决定解密哪一块。第二种是随机加密。位置不写死每次打包时从多个候选函数里随机选几个做加密或者同一个函数里有多个候选分块启动时随机选一块解密执行。这种属于“打游击”分析者很难预判下一次解密发生在哪个偏移。第三种是配合外层壳的二次修改。so被壳解出来后加载过程中又对代码段动手常见的是修改函数前几个字节、插入跳板、对数据段运行时解压。这类更像动态编码和自修改代码SMC是同一族。这三种形态经常混着用。面试时你不用去强行分辨它具体是哪一种但你要表现出“我知道有哪些可能每种对应什么切入口”。1.3 面试官真正在考察什么说结论这道题考的不是某个工具而是四层能力。一是静态分析能力能不能在IDA/Ghidra里梳理出解密例程二是动态调试能力能不能用Frida/lldb在正确时机断下来三是工程抽象能力遇到“不固定”时能不能找到统一打点的位置四是对抗被动检测的能力遇到反调试、自校验、模拟器检测这些障碍时能不能绕过去。还有一个隐藏考点表达。很多人做了大量逆向但讲不清楚面试官要的是结构化思路不是全部调试记录。该按什么顺序答我建议是解密逻辑必然存在 - 如何定位解密入口 - 如何抓解密后的内存 - 如何处理不固定位置 - 如何修复成可分析文件。这一套选择题答下来基本就是满分大纲。2. 解密逻辑再藏也有三个藏不起来的锚点2.1 锚点一解密行为必然落在内存管理API上任何一个把so里密文变成可执行代码的动态加密都逃不开两个动作写入和改变权限。写入用mmap、open、read这类文件IO改变权限用mprotect或直接调syscall。绝大多数开发者不会手写内联汇编绕过libc所以mprotect是最高频的突破口。实战里我有两个固定习惯。第一个习惯是挂上Frida后先全局hook一组系统函数open、read、mmap、mprotect、dlopen、dlsym、pthread_create。先不急着认哪个是解密函数把行为记录打全观察哪些地址在运行过程中被改了权限、被写了数据。第二个习惯是hook完之后专门把带了PROT_EXEC的mprotect调用单独打印。因为只有涉及可执行权限的改动才可能是SMC解密只改数据段读写的可以往后排。这里有个容易踩的坑很多so会主动对整个代码段调一次mprotect变成RWX保证执行到加密块时不缺页崩溃而不是一块一块改。这种情况mprotect只能看到一次大范围改权限看不到每次具体解密的信息。所以mprotect只是入口真正关键的是改权限之后谁在写、写进了哪个地址。2.2 锚点二加解密算法必然引用密钥和密文源从算法层面想一个动态加密要成立必须有三个东西密文、密钥、算法逻辑。密文一般以二进制数据存在so的数据段或独立资源文件里密钥可能硬编码在代码里也可能由Java层传下来算法可能是AES、RC4也可能只是一个异或循环。分析时不要一上来就抠算法先找“源数据”和“目标数据”的对应关系。这个对应关系通常体现在一个大循环里外层遍历加密块的索引表内层对每个块的密文做变换再写回地址。静态分析的最优路径不是读伪代码而是先确认“由谁驱动”驱动源就是索引表。小技巧在Frida里直接扫描只读数据段里看起来像地址数组的表然后逐个尝试把它们当索引表来遍历命中率不低。因为索引表本质上就是一组离散的偏移量它一定会被解密函数按顺序访问你一旦hook住解密函数就能把索引表里所有块的位置全部拉出来。2.3 锚点三解密出来的东西终究要被执行密文范围再广、密钥再复杂最终目标都是让CPU执行解密后的机器码。所以还有一个兜底思路不找解密函数直接找执行点。用硬件断点或软件断点对可疑内存区下断等程序运行到那里自然触发再回看是谁改写了这些字节、执行流从哪条路径跳进来的。这个思路简单粗暴但很有效尤其适合加密块分散多处、每次执行路径都不同的情况。执行路径追踪可以配合trace工具。PC端用IDA的debuggerAndroid上可以用Frida的Stalker插桩把执行流打出来。Stalker速度偏慢但分析一个几MB的so、梳理重点函数的调用链完全够用。当你看到某个地址一开始是垃圾字节、被某段循环写完后作为跳转目标被执行整条解密链路基本就清楚了。3. 实战路线静态定位解密入口动态dump解密结果3.1 第一步用IDA/Ghidra把骨架摸清楚我不建议一上来就动态调。先把so丢进IDA用“反编译so”的思路快速过一遍骨架信息。第一步看导出表JNI_OnLoad必看绝大多数native加解密逻辑的初始化入口都在这里。第二步看.init_arrayso加载时就会执行的构造函数列表。第三步看dlsym动态查找过的符号很多解密核心逻辑通过动态获取函数指针来间接调用静态看不到完整调用关系。如果走到这里已经看到一个明显的“循环写内存”结构就深入函数细看如果找不到说明加密逻辑是运行时临时生成或藏得比较深需要转到动态分析。我还会把能识别出来的函数名列一遍重点关注名字里带dec、decode、unpack、verify、key、crypt这类字段的符号。加密方虽然会主动去掉符号但早期版本和很多外包方案里保留命名的情况并不少。3.2 第二步Frida hook内存管理API抓解密时机骨架确认后上Frida。我第一次解决类似问题时用的就是最简单的mprotect hook脚本效果意外地好。核心思路hook mprotect当参数包含PROT_EXEC时等调用返回后把这段内存读出来落盘。以下是一个可以直接跑的基础版本目标进程名替换成你要分析的App即可。import frida import sys target com.example.target device frida.get_usb_device() pid device.spawn([target]) session device.attach(pid) script session.create_script( Interceptor.attach(Module.findExportByName(null, mprotect), { onEnter: function(args) { this.addr args[0].toInt32(); this.len args[1].toInt32(); this.prot args[2].toInt32(); // PROT_EXEC 4 if ((this.prot 4) ! 0) { console.log(mprotect exec: this.addr.toString(16) size this.len prot this.prot); } }, onLeave: function(retval) { if ((this.prot 4) ! 0) { try { var bytes Memory.readByteArray(ptr(this.addr), this.len); var file new File(/data/local/tmp/dump_ this.addr.toString(16) .bin, wb); file.write(bytes); file.close(); console.log(dump ok, size this.len); } catch (e) { console.log(read failed: e); } } } }); ) script.load() device.resume(pid)注意这个版本没有对抗反调试。如果so做了Frida端口扫描、文件检测这类对抗直接挂载很可能闪退。后面会在常见问题里讲对应的处理思路。3.3 第三步dump下来的bin如何变成能看的sodump只是第一步接下来要让IDA认识这份数据。最常见的问题是dump出来的内容没有ELF头是一段裸二进制IDA直接打不开。解决办法有两个。第一个办法是根据原始so的ELF头在dump缓冲区里重建文件结构把原文件头覆盖到dump结果起始位置。第二个办法是分析时手动指定加载基址用IDA的“Load additional binary file”把dump加载到内存偏移处。操作上我更推荐第二种错误概率小。如果是整段加密导致文件头都有问题那就先回到进程内存里找完整映射的so基址Frida里可以直接用Process.findModuleByName(libxxx.so)拿到模块信息再按基址加模块大小把整段内存dump出来。这样得到的文件虽然有运行时痕迹但ELF结构完整IDA可以直接加载。dump完再做一次对比比对dump前和dump后的文件哪些位置被改写过那些位置就是解密过的块通常也是核心逻辑所在。3.4 第四步架构差异和模拟器兼容性处理很多测试环境是x86模拟器但目标so是arm架构。这时候要特别注意CPU架构翻译层的存在。部分so在arm下的解密逻辑依赖ARM指令特征迁移到x86环境后会多出一层二进制翻译直接跨架构dump出来的机器码不能直接在IDA里当x86分析。建议是能上真机的尽量上真机必须用模拟器时优先用支持ARM翻译的模拟器先把“模拟器 frida so”这套链路跑通确认Frida注入成功后再做hook。另外“.so从x86迁移arm文件”这类场景本质上是在说不同ABI的so差异分析前先确认当前加载的是哪个架构的镜像别拿x86的hook脚本硬套arm样本。面试时如果能把这个问题讲出来比如“我分析时发现模拟器和真机dump结果不一致最后定位是架构翻译导致的”会是很好的加分项。这个问题也说明动态分析并不是“脚本一跑出结果”这么简单工具链的可信度本身就需要验证。4. 真正应对“不固定位置”的通用解法把单点断点升级成全量打点4.1 先找调度层再管执行层“不固定位置”看着难其实有一个共性位置可以随机但随机逻辑本身需要一个调度器。调度器负责决定选哪个块解密、按什么顺序解密、什么时候触发。找调度器的思路很简单先随便hook住任意一个解密块往回追它的调用来源多追几次就能看到一个共同的上级函数那个上级函数就是调度层。在调度层下断点比在每个解密块下断点高明得多因为你不关心这次解密发生在哪个偏移你只关心“每次要解密了我要不要抓”。这就是应对不固定位置的核心手段不做点对点分析做事件级全量分析。我见过一个实际案例对方把解密块的选择跟设备ID绑定每台手机启动后解密的块完全不一样。如果按固定点思路分析量会非常大但如果只在调度层打一个断点收集所有被解密块的地址日志原本可能要几天的分析换个思路几分钟就定位完成了。这就是工程抽象能力带来的效果。4.2 用unidbg做脱离真机的批量解密调试器很好用但一些高强度的so在真实设备上有大量反Frida检测Frida还没注入就退出了。这时候可以考虑unidbg思路是模拟执行。unidbg用Java写一个模拟的JNI环境把要分析的so加载进来手动调用JNI_OnLoad或指定导出函数在模拟执行过程中hook系统API。它不依赖真机、不依赖Frida很多反调试点在unidbg环境里根本不生效因为unidbg的返回值完全由你控制。用法上先创建AndroidEmulator实例加载so文件配置虚拟的JavaVM把需要执行的方法注册进去再在需要hook的地址上设置断点。代码运行到断点时会停住此时可以读内存、改寄存器、看调用栈。对解决“不固定位置”问题unidbg还有一个好处可以在每次解密循环的结尾打印被解密块的地址清单完全复现整个动态加密过程。我一般会在批量样本分析时用它先把逻辑跑通再回真机验证。不过unidbg的学习曲线不低需要懂JVM、JNI和模拟执行原理。它不是开箱即用的工具但它是面试里的高级话题能说出来并讲对原理就能和普通候选人拉开差距。4.3 自动化dump和patch的完整闭环定位到所有加密块之后最后一步是自动化处理。思路是找到调度器里负责写回内存的函数hook它“写完后”的返回点把目标地址的字节完整保存到本地等所有加密块都触发完毕后按地址合并再把合并结果patch回原始so文件。这一步可以写成Frida脚本也可以写成unidbg脚本核心逻辑是相通的解密事件来了就抓、抓完就存、最后统一组装。patch时有一个关键工作量地址对齐。so文件的虚拟地址和文件偏移之间有复杂的对齐关系不能直接拿虚拟地址往文件里写。需要先把虚拟地址转成文件偏移也就是找到对应段的p_offset和p_vaddr做差值再在文件对应位置覆盖。实际操作中我会写一个Python小脚本辅助处理转换公式很简单但坑很多比如不同段的文件对齐和内存对齐粒度不一样遇到不规则的段要单独处理。5. 常见问题与排查技巧实录实战里我踩过的坑5.1 问题速查表下面这些是我在动态加密so分析中反复遇到、也经常帮别人排过的问题整理成速查表。现象原因解决思路dump下来的机器码在IDA里显示为乱码解密时机太早代码还没写完把dump时机从解密函数调用前改到调用后或直接在onLeave里dumpmprotect只触发一次整个模块权限变成RWX开发者用了大范围权限修改单点hook不够要跟踪写内存的源头或直接定位解密循环Frida脚本一执行就闪退存在反调试、Frida检测换临时端口、移除特征字符串、配合反检测模块或改用unidbgso在模拟器上能跑真机上崩溃架构ABI差异检查当前分析的so是arm还是x86不要跨架构直接套脚本还原后的so仍然逻辑不完整加密块不是一次性解密而是懒解密收集所有加密块触发事件不能只dump一次就完事5.2 独家避坑心得第一不要一上来就想把整个so完全还原。现代加固方案很多时候核心逻辑运行时才生成即使dump下来也未必能直接逆出完整源码。目标应该是“够用”能还原关键流程、能看清加密算法、能模拟调用参数。我见过不少人在这个项目上卡死都是因为追求100%还原结果时间翻倍、收益递减。第二优先判断目标是不是商业加固方案。如果是搜一下这个加固厂商的公开特性很多商业方案有固定模式知道模式之后解法是现成的。如果是私有自研的加密再走完整的动态分析流程。第三面试沟通时要表现出“解法有条理、工具选择有理由”。比如你说“我用Frida而不是直接上frida-server”说明你知道这里的区别你说“我选unidbg是因为它有跨平台模拟能力”说明你理解原理。只背脚本没有用面试官追问两句就会露馅。6. 直接可用的面试应答框架6.1 高分回答模板面试时建议按四段式回答每段两三句不拖泥带水。第一段是复述问题并分类“so层动态加密我理解本质是SMC的一种。常见有固定点解密和随机点解密您说的不固定位置我倾向认为是随机索引表或分层加密。”第二段讲定位思路“我会先静态看JNI_OnLoad和.init_array再动态hook mprotect、mmap、open这些内存相关API先确定解密事件发生在哪个地址范围。”第三段讲抓捕手段“确定解密入口后重点找调度器把所有解密块统一打点。解密完成后按虚拟地址转文件偏移再patch回原so。”第四段讲工具与扩展“工具上我常用Frida做动态分析、IDA/Ghidra做静态分析、unidbg处理反调试强的样本。遇到架构问题会专门处理arm/x86差异必要时换真机验证。”这套模板的逻辑是闭合的答了什么、用什么答、遇到对抗怎么办每个问题都有落点。6.2 加分细节与反问技巧面试时还有几个加分点。一是主动提到SMC概念证明有底层知识储备。二是提到手工修复ELF对齐的坑证明真的做过而不是只会背书。三是提到模拟器Frida和架构迁移问题证明有全平台经验。如果面试官让你反问可以问“你们目前的so加密是自研还是商业方案有没有反调试需求”这一问既能展示专业度也能顺带了解对方技术栈。最后再分享一点个人体会。我第一次实际排查这类动态加密so的时候花了整整三天时间在mprotect hook上反复调却始终抓不到完整逻辑后来才反应过来思路从一开始就偏了只盯着单次解密事件没有往上找调度层。从那以后我遇到“不固定位置”就换个问法“什么逻辑在驱动这次解密”而不是“这个位置什么时候解密”。这道题走到最后拼的其实不是工具熟练度而是能不能把“不固定”抽象成“统一事件”这个思维转换。能想通这一点你离问题的答案就不远了。
返回列表