ARTICLE DETAIL

资讯详情

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

逆向分析实用技巧:APK定位、JS断点与混淆对抗

逆向分析实用技巧:APK定位、JS断点与混淆对抗 拿到一个加固过的APK或者一段怎么看都不太对劲的JS实现很多人的第一反应是从第一行开始读代码。我一开始也这样经常是读了半天连程序真正干活的函数在哪都没找到。后来慢慢发现逆向分析里最值钱的反而不是某个多高深的工具而是一套“从结果反推路径”的思维习惯。这篇就整理几则在日常样本分析中让我省过不少时间的逆向小技巧主要围绕四大块APK里怎么快速定位功能代码、JS调用链怎么用日志断点理清、控制流平坦化和数据平坦化这类混淆怎么对付、以及设备环境检测这类老熟脸问题该怎么处理。适合手头正好有样本、想提效的分析人员也适合刚装好工具链、不知道往哪下手的初学者。我尽量把思路和踩过的坑都写出来。1. 先看清擂台从热搜话题看逆向的几个主战场技术群和热搜榜上逆向相关的词条常年刷屏。光看这些关键词就能发现大家手头的目标基本集中在几个方向安卓APP的原生层和Java层、Web前端的JavaScript脚本、H5游戏、以及各种加固混淆方案。把这些词条分个类能看出一套很有意思的“技术版图”。方向典型场景主要卡点安卓APP某电商App、某本地生活App、某票务App加固壳、so层算法、签名校验、环境检测Web/JSjs逆向、akamai类云防护、某验证码产品参数加密、风控采集、JS虚拟机、混淆游戏/H5h5游戏逆向、Cocos/Laya资源资源加密、协议模拟、内存修改混淆/编译控制流平坦化、数据平坦化状态机混淆、字符串加密、静态分析失效跨端/框架uniapp逆向、Flutter产物框架特殊产物、工具链不通用我不建议一上来就啃那些防护强度最高的商业目标。不是说没用而是难度曲线太陡。比如akamai那套涉及动态设备指纹、JS虚拟机和大量环境校验没有足够前置经验直接冲很容易一周下来连关键逻辑在哪都没摸到。更合适的练手目标是自己写的Demo、开源APP、CTF公开题目以及公司给了书面授权的测试对象。把这些基础场景练明白再回头看商业样本至少知道该往哪个方向使劲。后面几节里提到的样本我也默认都来自这些合法授权来源。这个边界很重要做逆向的人尤其要分清分析自己手里的东西和试图突破别人的防线完全是两回事。2. APK定位功能代码把交叉引用思维从Windows搬到安卓热搜里有个问题我印象很深“apk逆向如何像windows逆向一样找功能代码”。这句话其实点破了很多刚转安卓的人真正的困惑。在Windows上用IDA打开一个PE文件看到可疑字符串Shift12打开字符串窗口双击进去按X看交叉引用然后一条条往上摸调用链。这个过程在安卓上完全可以复刻只是工具换成了jadx、GDA或者JEB思维内核一点没变。2.1 静态定位特征字符串是最快的地标最实用的第一招永远是搜字符串。比方说分析一个APK里的登录功能哪怕它有校验、有加密总会有一些提示文本、Toast文案或错误日志比如“登录失败请检查网络”。把APK丢进jadx搜索包含这段文本的引用跳转到对应Java代码位置再用Find Usage功能搜索这个函数被谁调用入口函数就出来了。具体操作路径大概是用jadx打开APK如果是加固包先脱壳或用frida-dexdump从内存中把dex拉出来再分析按字符串或正则搜索特征文本例如“登录失败”跳转到引用位置查看所在函数上下文对该函数执行Find Usage往上找调用者重复几次直到触达Activity/Fragment入口或某个后台任务入口。这一步本质上和Windows上用IDA的交叉引用完全一致只是Windows习惯按X看引用jadx里叫Find UsageGDA里也有类似入口。这里有个坑有时候中文提示词怎么都搜不到。不是因为字符串不存在而是dex里字符串可能是分片拼接的或者经过了轻微编码。我第一次遇到这情况卡了大半天后来换了一个思路——不搜用户可见文案改搜日志TAG、方法名、类名。很多开发者在写代码时会在关键函数前加一个独特的Tag比如LoginHelper、LoginActivity这些标识符被保留的概率比中文文案高得多定位反而更快。2.2 动态验证Frida主动调用确认逻辑静态分析只能确认“这段代码大概在做什么”不能保证100%准确。遇到重载、接口分发、反射调用时静态路径经常断掉这时候就得靠动态验证。我常用的方式是用Frida在Java层主动调用目标函数传入预期参数看返回结果和假设是否一致。下面是一个很通用的示例假设要主动调用某个类的静态方法Java.perform(function () { var Cls Java.use(com.example.demo.LoginHelper); // 假设函数是 checkLogin(String username, String password) var result Cls.checkLogin(test_user, test_pass); console.log([*] 主动调用结果: result); });如果是实例方法可以先通过Java.choose在堆里找一个现成对象再调用Java.perform(function () { Java.choose(com.example.demo.LoginActivity, { onMatch: function (instance) { var ret instance.checkLogin(test_user, test_pass); console.log([*] 实例方法返回: ret); }, onComplete: function () {} }); });主动调用的价值在于它能帮你快速确认某个函数的逻辑是否符合预期而不必完整跑一遍业务流程。比如怀疑某个加密函数是AES主动调用它加密一段已知明文看输出和标准AES结果是否一致立刻就能验证算法类型、KEY和IV是不是写死在代码里。这个技巧在Windows逆向里也有对应本质就是用脚本或调试器直接调用目标函数绕过前面一大堆干扰逻辑。2.3 Windows分析思维与安卓分析工具的映射很多刚转安卓的分析师不是不会逆向而是被新工具触发了陌生感。我整理过一个映射表把两边对应起来上手的心理负担会小很多分析场景Windows常用安卓常用静态反汇编/反编译IDA Pro、Ghidra、Binary Ninjajadx、GDA、JEB、IDA动态调试x64dbg、OllyDbg、WinDbgFrida、objection、调试器原生能力内存搜索Cheat EngineFrida内存遍历、GameGuardian字符串与引用IDA交叉引用jadx搜索Find Usage环境对抗ScyllaHide、TitanHideFrida检测绕过、自定义server这张表想说明一件事工具会变但“交叉引用”“断点回溯”“内存定位”这些底层思维是一模一样的。如果你在Windows平台上有过扎实的逆向底子转安卓缺的只是熟悉几个新工具入口而不是重新学一遍逆向。我见过太多人被“新平台”三个字吓住其实真动手起来半天就能上手。3. JS逆向的三点定位法与日志断点套路再看Web端。JS逆向最常见的场景是搞不清楚一串加密参数到底在哪生成、怎么生成。热搜里那么多“js逆向教程”本质上都在解决这个定位问题。我的习惯是先画三条线入口点、关键点、输出点。3.1 三个点入口、关键、输出入口点请求发出、事件绑定、定时器等“动作发生”的地方。在浏览器里最快的方法是给XMLHttpRequest或fetch的send函数下断点或者使用事件监听断点。关键点生成加密字段、校验字段、或改变请求行为的代码位置。输出点参数被赋值给请求头、请求体、DOM或某个全局变量的地方。一个很经典且高效的策略是先把输出点找到。也就是请求body里某个加密参数被生成的那一行。一般做法是下XHR断点或者直接搜索参数名。原因在于大部分JS做压缩混淆只会把变量名替换而请求参数名比如sign、token、payload往往会被保留因为它要被提交给服务端服务端还要按这个名字来解析所以混淆器通常不会动它。把输出点定位到之后再在DevTools里看它的调用栈就能一路回溯到入口点。这个过程和Windows逆向里“从一个API调用点往上翻调用栈”是完全相同的思路。3.2 日志断点比单步调试好用得多我观察到很多刚入门的朋友习惯F11一步一步跟遇到一个几千行的压缩JS函数跟到怀疑人生。我用得最多的其实是“日志断点”也就是在可疑函数入口打印参数、在return处打印返回值然后直接跑完整流程看日志里哪个函数收到什么、输出什么。这样整体脉络很快就清楚了不需要逐行走迷宫。在Chrome DevTools里给某个函数入口加日志断点很方便在调用栈展开中找到目标函数右键选择“Add log point”输入想输出的日志模板即可。但更可控的方式是在Console里直接覆写目标函数打印参数和返回值const _orig targetModule.encrypt; targetModule.encrypt function (...args) { console.log([encrypt] 入参:, args); const ret _orig.apply(this, args); console.log([encrypt] 返回:, ret); return ret; };再配合console.trace()或者new Error().stack打印调用堆栈就能一次性理清“谁调用了它”。这比逐行单步的效率高一个量级。特别当目标代码有几万行或者做了大量无用分支时先把日志铺出去看真实运行到底走了哪些分支再回来细看那些真正被走到的代码基本不会跑偏。这里有两点经验要提醒日志点不是越多越好。我曾经一次性给二三十个函数都加了打印结果主线程被日志刷到严重卡顿页面直接假死。后来学乖了只关注调用栈顶层的那几个函数先加少量日志确认方向再逐步细化。如果目标会检测Function.prototype.toString的指纹直接覆写函数很容易暴露。更隐蔽的做法是在函数外用一个代理拦截调用比如给某个对象方法包一层Proxy或者只在调用点附近打断点不碰函数自身。3.3 反调试检测与绕过的边界意识Web端的逆向还会遇到很多反调试定时器触发debugger、devtools检测、断点命中后卡死、内存被篡改等。这些机制可以识别也可以分析但我要把话说在前面对不属于你的在线系统做这些对抗既不合规也不安全。我只建议把这类技巧用在CTF题目、自己搭的靶场和本地Demo上。识别反调试的思路其实很通用搜索“debugger”关键字查看是否存在Function(debugger)一类的动态构造检查是否存在时间差检测比如检测debugger暂停导致的耗时异常。理解了这些机制之后在授权环境下可以通过删除调试分支、Hook关键函数等方式绕过。但如果你正在分析一个没有授权的目标请到此为止。4. 控制流平坦化与数据平坦化两层混淆一套打法热搜里“数据平坦化逆向”这个词挺考验功力的。混淆对抗是逆向里绕不开的大方向其中控制流平坦化是很多加固工具的看门本领数据平坦化则是相对少见但同样恶心人的一种思路。好消息是对付它们可以用同一套底层的动态插桩方法论。4.1 控制流平坦化的特征与麻烦控制流平坦化做的事就是把原本顺序执行的代码改造成一个状态机。典型特征是一个while(true)或while(dispatcher)循环里面有一个switch再配一个状态变量每次循环根据状态变量的值跳到下一个基本块。举个例子原始代码可能是这样的int logic(int a, int b) { int c a b; if (c 10) { return c - 5; } return c * 2; }经过控制流平坦化之后逻辑上等价但结构上变成类似这样int state 0; while (1) { switch (state) { case 0: state (a b 10) ? 1 : 2; break; case 1: return a b - 5; case 2: state 3; break; case 3: return (a b) * 2; } }这是最简化的示意真实混淆往往还会加上大量无意义分支、不透明谓词和复杂状态运算。这种代码静态看非常痛苦因为它把程序原有的“A调用B再调用C”的直观结构抹平了。这类代码在OLLVM混淆过的样本里很常见CTF出题人也很喜欢用。识别方法不复杂看反编译窗口里某个函数是否异常庞大且函数体内大量充斥着while switch 状态变量的结构。一旦确认是控制流平坦化就别再试图从头到尾静态读懂每个分支了。4.2 数据平坦化让静态分析失效的另一种手段数据平坦化在国外一些混淆器实现里也有核心思路是把原本清晰的数据访问全部摊平成“索引计算查表取值”让静态分析很难看出某个数据到底是从哪来的。很多混淆工具还会把字符串常量加密成一堆字节数组运行时通过解密函数还原。静态看代码只能看到decode(0x2a)这种调用完全不知道内容是什么只有运行起来之后才能在内存里看到还原后的明文。这种“让你看不见常量”的手法比控制流混淆更让人头疼。因为它连你唯一能抓住的“字符串地标”都给你拔了。控制流平坦化好歹还能靠函数边界切分数据平坦化直接让字符串引用变成一片黑箱。硬啃汇编是最不效率的路线正确的做法是让程序自己把“答案”交出来。4.3 动态插桩还原路径的操作框架针对这两类混淆我用的通用框架只有三步找到状态跳转或数据解密的关键函数控制流平坦化里的dispatcher、数据平坦化里的decode函数用Frida或Unicorn引擎动态插桩把每次状态转移的记录打印出来根据记录整合出真实执行路径或者把运行时解密出来的数据与调用点拼接回去。举个例子在Frida里打印控制流平坦化的状态变化大致思路如下。注意具体地址偏移要从自己样本里读取这里只是演示逻辑Interceptor.attach(Module.findBaseAddress(libtarget.so).add(0x1234), { onEnter: function (args) { // args[0] 是状态变量 console.log([state] - args[0].toInt32()); } });拿到一串状态序列之后再回到反汇编窗口里把state1 会跳转到哪些地址一一对应就能还原出真实的执行顺序。相当于把被打乱的代码“拉直”了后面继续分析就顺畅得多。对付数据平坦化我更喜欢用“主动解密”的方式Hook住解密函数拿到入参和返回值批量调用一次把所有字符串或数据的明文全部dump出来。后续分析直接看明文而不是看加密后的字节。这个思路同样适用于任何“运行时才还原常量”的样本记住一句话静态看不到的动态让它自己显示出来。5. 环境检测、反调试与加固体力活也有方法论分析到后来你会发现真正的算法逆向往往不是最耗时的最耗时的反而是“如何让样本在一个可调试的环境里跑起来”。设备检测、反调试、加固壳每一项都能拖慢进度。这里我不给具体的绕过代码因为很多绕过细节被写出来容易踩红线。我只讲识别方法论和一般性处理思路并再次强调这些操作只建议在拥有完全权限的设备、自己开发的程序或CTF题目中进行。5.1 常见的检测维度与识别方式TracerPid检测读/proc/self/status里的TracerPid如果非0说明有调试器挂接。用Frida或ptrace附加时很容易触发。ptrace反调试进程以自己为父进程发起trace阻止其他调试器再附加。Frida检测扫描内存特征字符串、检查默认端口27042、检查特殊线程名。Root/越狱检测检查su文件、Magisk包名、可写系统目录。时间差检测程序执行耗时超过阈值认为处于断点调试状态。识别方式是全平台通用的搜索特征字符串、Hook可疑的系统API观察返回值在正常环境和调试环境下是否不同。做这些事情的目的是为了让自己在合法样本上调试得更顺利而不是为了突破防护。5.2 一套比较稳妥的处理思路我自己的处理顺序一般是先静态搜特征字符串确认存在哪些检测再动态Hook那些检测函数把返回结果“修正”成预期值最后验证调试链路是否稳定。比如TracerPid检测一般思路就是找到读取该文件的代码Hook之后把TracerPid字段修正为0。但说句实话快速稳定且能覆盖不同加固方案的通用手段很难公开讲清楚因为每套加固的细节差异很大基本是个案处理。真正需要的时候我倾向于优先使用隔离环境、虚拟化调试和专门的逆向工具链而不是在真机上反复和反调试对抗。这里特别提醒一句很多加固方案和商业应用的检测逻辑本质是为了保护服务端和用户数据。不要用这些方法去试图突破不属于你的系统这是行业里应该守住的基本底线。5.3 加固壳与跨端产物的工具链选择面对加固APK我的建议是先确认壳的类型再选择对应工具。常见处理路径是先用脱壳工具一键脱壳再用修复工具修复dex里的指令最后丢进jadx分析。DexDump类方案配合Frida运行时从内存里把类定义抓出来对某些壳效果不错。判断壳的类型也有技巧很多加固包的入口点特征、so文件列表都很明显多试几次就能凭直觉说出个大概。H5游戏和uniapp这类跨端产物也有自己的一套工具链。前者很多是Cocos/Laya的产物资源文件往往带自定义头需要先识别格式再解包后者主要产物是APK壳或web资源包分析重点要先看它走的是原生调用还是内嵌webview。这类“先判断产物类型再选工具”的习惯本质上也是逆向方法论的一部分。最后说几句个人体会前阵子我在调试一段混淆得很厉害的样本时为了省时间把Frida脚本初稿和算法还原过程里的一些重复代码都交给了AI工具来写确实快了很多尤其是代码模板和正则表达式AI的效率明显比手写高。但AI能帮的是“怎么实现”不能替你做的是“该在哪里下断点”“哪个函数才是关键路径”。这些判断靠的仍然是经验、耐心以及对上面几套基础方法的熟练掌握。如果你想从零开始练逆向我仍然建议从最土的“字符串定位交叉引用”开始先把一条条调用链摸清楚再去看各种高深的脱壳和还原技巧。基本功扎实了后面出现的工具和新框架上手都很快。希望这几则小技巧能让你在下次对着样本发愁的时候少走一点弯路。
返回列表