逆向实战指南:JS与安卓逆向实用技巧与排坑手册
2026/9/17 5:34:27 网站建设 项目流程

做逆向这几年,我最大的感受是:真正难的不是某个高深算法,而是大量细碎的“卡点”。比如一个JS加密函数定位不到,一个so层的控制流被混淆到没法看,一个App明明打开却抓不到包,其实都是小问题叠加。今天分享几则我实战里反复用到的逆向小技巧,覆盖js逆向、安卓逆向、协议分析和常见坑点处理。

这篇内容适合三类人:刚入行想系统学习逆向的朋友,已经在跟某个目标但卡壳的,以及单纯想看看“逆向分析”到底在做什么的人。我没有按教科书的方式讲,全部以实际分析路径为主,你看到的是我平常打开工具之后真实会做的动作。

1. 逆向工程的核心思路与准备

1.1 逆向不是破解:先想清楚目标

这里先把边界划清楚。逆向技术本身是中性的,安全研究、漏洞挖掘、病毒分析、数据恢复、旧系统兼容都需要逆向。如果你拿它去绕过付费、破解别人的商业系统,那不是技术问题,是合规风险。所以我下面所有技巧,都默认用在你拥有权限或已获得授权的目标上,希望你也按这个标准来。

回到技巧本身。拿到一个目标,先回答三个问题。第一,目标是Web前端、App客户端、还是纯协议服务?这三个方向的工具链差异很大,不要混着用。第二,你要找的是签名参数、加密数据、还是某个功能入口?目标不同,定位路径完全不同。第三,你打算在哪个层停下?比如只想调通接口,那通过补环境生成加密参数就够了;想把算法完整还原成Python代码,那就得进到汇编层。目标不清晰,后面全是白费功夫。

我见过太多人一上来就反编译,结果在一个无关紧要的类上耗了半天。原因就是没先问“我到底要什么”。所以这一小节虽然啰嗦,但值得你花十秒想一想。

1.2 工具链别贪多,顺手最重要

很多新手喜欢把Frida、Objection、jadx、Ghidra、IDA、Burp全装一遍,结果每个都不熟。我的建议是:按链路搭一套最小工具链。Web端,Chrome DevTools加Node.js,再配任意抓包工具;安卓端,jadx看Java层、Frida做运行时Hook、IDA或Ghidra看so层、Objection做常用免root操作;协议阶段,用Burp或Charles抓包,再用Python的requests做模拟请求。

工具不是越多越好,是用到哪个就练透哪个。以Frida为例,先不要说那些花哨的脚本,你只需要会两件事:启动目标进程并注入脚本、附加到指定方法打印调用栈和参数。能把这两件事做好,很多问题自然就解开了。

另外提醒一句,工具版本要固定。我踩过最大坑是Frida服务端和客户端版本不匹配,导致一直报unable to connect,排错花了一下午。后来经验是升级前先看Release日志,能不动就不动。

1.3 先抓包,再动刀

无论是Web还是App,第一步永远是看数据。很多人拿到App直接反编译,看完一堆类就懵了。不如先打开抓包工具,跑一遍核心业务流程,观察哪些请求是加密的、哪些参数是动态变化的、服务端返回了什么。抓包能帮你建立“数据流转”的地图,后面定位代码时就有方向。

这里有个细节:App如果没有在系统中信任你装的CA证书,HTTPS会抓不到明文。常见做法是把App的网络安全配置临时改为允许用户证书,或者在一个可控的测试环境里调试。这一步不要去动别人的正常软件,只对你自己完全控制的测试样本操作。抓完包以后,把关键请求按业务流程排个序,哪些参数由前一个响应决定、哪些是本地生成的,一目了然。

2. JS逆向实用技巧

2.1 搜索特征,先定位加密函数

做js逆向,最常用的定位方式就三招。第一招,全局搜字符串。比如看到请求参数里有signtokenencryptsessionid这类关键词,直接在DevTools的Sources里搜索,通常能跳到定义的地方。第二招,看调用栈。Network面板里找到目标请求,点一下“Initiator”或“Call Stack”,浏览器会告诉你这个请求从哪个函数发出去的。第三招,基于包结构反推。请求负载里的字段名和顺序,往往对应代码里对象的构造顺序,搜其中一个特殊字段名,常能一抓一个准。

拿我调试过的一个案例说,当时一个请求里有个fgid参数,长度五个字符,在代码里搜这个词,命中了三十多处。这时候别一个个看,直接在搜索框里输入fgid:"fgid",快速过滤赋值语句。很快定位到一个工具函数,里面调用了某个哈希算法。核心思路就是:用“字段名加赋值上下文”缩小范围,而不是满世界翻文件。

如果你的目标JS做过代码压缩或者混淆,变量名都变成abc这种,搜索字段名的思路依然有效。因为字符串常量无法被混淆,URL、参数名、提示语、JSON的key,都是天然锚点。

2.2 Hook关键函数,把黑盒变白盒

定位到函数后,下一步是看它在运行时的输入输出。最粗暴的方法是打断点,但如果有加密逻辑被分散在多个函数里,断点效率不高。我更推荐用Hook的方式,直接在Console里改写函数,包一个壳:

let originalFunc = window.someEncryptFunc; window.someEncryptFunc = function(...args) { console.log("args:", args); let result = originalFunc.apply(this, args); console.log("result:", result); return result; };

这段代码的意思是:保留原函数,在每次调用时打印入参和返回值。如果是放在闭包里的局部函数,外衣访问不到,就在对应的源码行号位置手动加断点,或者用Frida等工具去Hook更高层的调用点。Hook的核心是“拦截-记录-放行”,不要一上来就想改逻辑,先把数据拿到手。

补充一个经验:Hook的目标函数如果会被调用几百次,建议加一个计数器,只打印前几次和最后几次,避免刷屏。同时打印new Error().stack,能看到调用方来源,对梳理函数关系极有帮助。

2.3 补环境,让JS代码在Node里跑起来

很多时候,光看参数还不够,最好能把JS代码直接搬到本地跑一遍。问题来了:浏览器里的JS依赖windowdocumentnavigator这些全局对象,在Node.js里统统没有。于是就要“补环境”。我见过不少同学在这一卡就是两三天,其实有套路。

先写一个最基础的全局对象替身,比如:

global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 ...' , platform: 'Win32', language: 'zh-CN' }; global.document = { createElement: function() { return { getContext: () => ({ measureText: () => ({ width: 0 }) }) }; }, cookie: '', // ...按需补充 };

缺什么补什么,但重点不是“全”,而是“够用”。怎么知道缺什么?把目标JS文件挂到Node里跑,看报错。报xxx is not defined,就补一个xxx占位对象;报xxx is not a function,就补一个空函数。一圈跑下来,你会发现真正要补的东西不超过二十个。

推荐用vm模块把JS代码放在沙箱里执行,避免污染全局变量:

const vm = require('vm'); const sandbox = { window: {}, navigator: {}, console }; vm.createContext(sandbox); vm.runInContext(jsCode, sandbox);

这样做的好处是每次都能重置环境,调试效率高很多。补环境有个常见误区:追求完美还原浏览器。其实后端只关心你算出来的结果对不对,有些DOM操作直接返回空值完全够用。

2.4 真实案例:一个滑块验证码的加密参数

之前分析过一个滑块验证的生成参数,这里不提具体厂商,流程大致是:前端收集轨迹和设备指纹,在canvas上画一次图形,再把各项信息拼成一个字符串,最后用AES加密传回。

定位的时候,我发现请求里有wbg两个字段,搜索bg很快就找到了轨迹采集代码。再搜AES加密的关键特征,比如CryptoJSenc.Base64,就找到了加密入口。把入口函数抠出来以后,剩下的就是补canvastoDataURL方法,因为代码需要通过它里的像素信息生成指纹。我补了一个固定返回值的假canvas,加上轨迹数组用随机数模拟,结果后端照样校验通过——因为真正校验的是轨迹是否像人类操作,而不是像素内容。

这个案例说明,逆向不是把每个逻辑都还原,而是找到服务端真正校验的那部分,针对性处理。有时候你花几天还原一个算法,最后发现参数根本不会被校验,白费功夫。

3. 安卓逆向实用技巧

3.1 用jadx快速定位Java层代码

安卓逆向和JS逆向最大的不同是多了一层Java代码和一个Native世界。拿到APK以后,我习惯先把APK拖进jadx,直接看反编译后的Java代码。jadx的好处是能够以类名和包名浏览,像读源码一样。定位功能和Windows逆向确实很像:先搜字符串,再找引用。

比如某个功能会在跳转时带上一个加密参数,那就在jadx里搜索参数名,如果搜索不到,就搜索接口的URL字符串,再从OkHttp或者Retrofit的调用点回溯。另外一个技巧是看AndroidManifest.xml,入口Activity、Service、动态广播都在这份文件里,可以从入口开始追。对于有加固的App,jadx打开会看到一堆壳类,这时候就先脱壳,后面第五节再说。

如果是用uniapp、Flutter这类跨平台框架写的App,Java层可能只是一个容器,真正的业务逻辑在JS包或二进制里。遇到这种情况,先去assets目录找app-service.js或者libapp.so,再按对应方向的逆向思路走。很多跨平台App的网络请求参数和Web端写法高度相似,直接按JS逆向处理反而更快。

3.2 从Java层的线索进入So层

Java层往往只负责调用一个native方法,真正的算法在so里。当你看到类似System.loadLibrary("security")private native String sign(String data)这样的代码时,就该打开IDA或Ghidra了。

在so文件里定位函数,最有效的方式还是看导出表和字符串引用。一般Java_com_example_Security_sign这种命名规则会把Java层和Native层直接对应起来。如果没有导出符号,就在Frida里动态注册时抓一下方法地址。进到汇编层后,读起来确实难受,先从函数头部的栈操作、尾部的返回值汇编看起,配合F5伪代码,能恢复个大概。别指望一次看懂,先把输入输出确认了,再逐段理解。

我还习惯用Frida去Hook Native函数,直接在参数和返回值上打日志。比如:

Interceptor.attach(Module.findExportByName("libsecurity.so", "Java_com_example_Security_sign"), { onEnter(args) { console.log("arg0:", hexdump(args[2]), "arg1:", hexdump(args[3])); }, onLeave(retval) { console.log("retval:", hexdump(retval)); } });

这里args[2]是JNIEnv,args[3]是jstring。把入参和返回的字节dump出来,能帮你快速验证so是否就是加密核心。比起纯静态看汇编,动态确认输入输出和静态还原逻辑结合,才是完整路径。

3.3 像Windows逆向一样找功能代码

有朋友问我,APK逆向能不能像Windows逆向那样,看到一个关键字符串,就顺藤摸瓜找到功能代码?答案是能,而且思路完全一样。Windows程序里,程序员调用MessageBoxA("注册成功"),你会看到那个字符串被压在栈里,函数引用清晰地指向处理逻辑。安卓里也一样,jadx中搜索“注册成功”,会搜到R.string.xxx,再到值引用处,就能看到这段UI处理代码。

唯一不同的是布局资源把字符串抽走了,所以优先搜资源ID或硬编码的URL。实在找不到字符串,就抓运行时的提示框,利用Frida去HookToast.makeTextLog方法,打印调用栈。这种“从结果倒推代码路径”的办法,比从入口硬撸整个Activity效率高得多。

还要注意,App常常把关键算法做成动态加载,字符串被分割或加密。遇到这种,就需要先在内存里把字符串还原出来,再去定位引用。可以用Frida扫描内存里的Unicode字符串,搜目标URL或参数名的子串,命中后打印周围内存,经常能扒出一片完整逻辑。

3.4 脱壳的基本思路

现在很多App都做了加固,dex壳会把真实的类藏起来。脱壳的本质是把内存中已经解密/还原的dex dump出来。常用的两个路子,一是利用Frida在运行时dump dex文件,二是用现成的脱壳机,比如frida-dexdump或定制系统里的dump脚本。

我自己的习惯是先识别加固厂商,查壳的静态特征,再选对应方案。大部分加固在内存里都会有完整的dex,dump出来之后用jadx重新分析即可。注意,脱壳必须在你自己控制的手机或模拟器里,配合合法测试包进行,否则就是在破坏他人系统,还会带来法律风险。分析脱壳后的dex,经常能看到一些被混淆的类名,这时不要慌,只要逻辑完整,按照前几节的定位方法一样能找到功能代码。

脱壳之后如果发现代码还是很乱,很多类名都是a.b.c这种,那就优先看入口类和Application类,那里常会做初始化工作,把关键逻辑串起来。

4. 协议逆向与数据平坦化

4.1 协议逆向的通用套路

所谓协议逆向,其实就是搞清楚客户端和服务端之间“说了什么、按什么格式说”。有时候你根本不用关心客户端里具体怎么算,只需要能模拟出一个合法的请求。

先抓包,把请求头和请求体全部保存下来。然后分字段看变化:哪些字段是固定的、哪些是时间戳、哪些是随机数、哪些是签名。最常用的验证方法就是改字段值,比如把时间戳改成昨天的,看返回是否变化。如果返回值提示签名不过,说明这个字段参与签名。再把签名算法在代码里定位,基本就打通了。

对于格式类的逆向,比如librecad解析dwg这类问题,其实也属于协议逆向的范围。文件格式本身就是一种数据协议,你通过观察数据分布、查找结构体对齐特征、分析不同记录的类型字段,把文件格式还原出来,这就是逆向。理解这一点,就不会纠结“解析一个文件格式到底算不算逆向”了。无非是目标从网络字节流变成了文件字节流。

4.2 控制流平坦化的识别与还原

热词里有个“数据平坦化逆向”,在二进制里对应最多的其实是“控制流平坦化”(Control Flow Flattening)。这是一种编译器级别的混淆,常用于安全防护产品,用来把正常的if/else/while结构打散成一个巨大的switch-case循环。逆向工程师一看伪代码就头大,全是一个状态循环,逻辑被压成扁平的。

识别它很容易:函数里有大量形如while (1) { switch (someVar) { case 0: ... } }的结构。还原的核心是找到状态变量和分析状态之间的跳转关系。可以用angr、Triton这些符号执行工具自动推,也可以人工维护一个状态表。我建议新手先别追求全自动,先手动跟几个case,把状态转移画出来,再交给脚本处理。这个过程挺考验耐心,但你只要把状态变量和case对应关系列成表格,逻辑会迅速清晰。

举个小例子,一个正常的函数可能是:

if (a > 0) { b = 1; } else { b = 2; }

经过平坦化之后会变成:

while (1) { switch (state) { case 0: if (a > 0) state = 1; else state = 2; break; case 1: b = 1; state = 3; break; case 2: b = 2; state = 3; break; case 3: goto end; } }

你只要把state的取值和每个分支的副作用列出来,还原原来的if/else并不难。关键是耐下性子。

4.3 把抓到的请求转成可重放的脚本

完成协议逆向以后,最后一个动作往往是写一段Python脚本,把请求重放出来。我的建议是直接用requests库,把抓包得到的headers、cookies、请求体原样搬过来,先确保能跑通,再逐步替换动态参数。

import requests, time, hashlib url = "https://example.com/api/get" data = { "timestamp": str(int(time.time())), "nonce": "abc123", "data": "test" } data["sign"] = hashlib.md5( (data["timestamp"] + data["nonce"] + data["data"] + "salt").encode() ).hexdigest() resp = requests.post(url, data=data) print(resp.text)

这一步看着简单,真正坑的是服务端会校验User-Agent、Referer、参数顺序、甚至TLS指纹。所以重放时先保持和客户端一致,能跑通后再简化,别一上来就追求“极简请求”。如果遇到需要复杂TLS指纹的,可以试试用Python的curl_cffi库保持浏览器指纹,这个库可以模拟Chrome的TLS握手特征,大多数时候能解决。但还是要强调,只在你自己的测试环境里做。

5. 常见问题与排查技巧实录

5.1 抓不到包怎么办

抓不到包的原因通常有三类。第一,代理设置不对,App没走系统代理;第二,证书校验没过,TLS握手就断了;第三,请求走了Socket长连接,不走HTTP代理。第一类检查WiFi代理是否生效,第二类给测试机安装CA证书并修改网络安全配置,第三类就要用Frida去Hook底层发送点,或者用可以截获TCP的抓包工具。

如果只针对某个App抓包,还可以用adb把目标App的流量转发到代理端口,或者用tcpdump抓网卡数据。抓包不一定要看到明文,有时候分析重传长度、连接频率也有价值。这一步的关键是判断目标到底用了什么通信方式,而不是死磕一个工具。

5.2 Hook没生效的几个原因

排查Hook问题时,先确认进程和脚本是否连接成功,再确认函数签名是否匹配,特别是重载函数要指定参数类型。有时候是函数被内联优化了,Hook不到,就得找上层调用的入口。记住一点:Hook不到不代表函数不存在,很可能是绑错了位置。

另一个常见问题是作用域。Java层Hook的时候,如果类被自定义ClassLoader加载,需要用Java.enumerateClassLoaders找到正确的loader,再Java.classFactory.loader = loader切换。这个问题在加固App里尤其常见,很多新手在这里莫名其妙地找不到类,其实就是loader没切换。

5.3 补环境常用报错速查

报错信息原因解决方案
navigator is not defined缺少navigator对象global.navigator = {...}
window is not defined缺少window对象global.window = global,或按需赋值
document.createElement is not a functiondocument补得不完整document增加对应方法
Cannot read property 'xxx' of undefined某个全局对象没补在报错行打日志,定位到具体属性
WebSocket is not defined代码里用了WebSocket补一个假的WebSocket类,直接返回空对象
localStorage is not defined代码里用了localStorage补一个带getItemsetItem方法的对象

补环境时不要一个报错补一个,补完就跑,直到下一个报错。这是最低效的方式。更好的做法是先在代码里搜索所有window.document.navigator.的引用点,把所有用到的属性列个清单,一次性建一个对象。这样会减少很多的来回。

5.4 用AI辅助逆向的几个姿势

最近总有人问“怎么让codex帮我逆向提示词”,我的看法是,AI可以帮你在思路上加速,但不能替你判断。我现在常用的场景有三个。第一,把一段混淆后的JS或反汇编伪代码贴给AI,让它总结逻辑流程,这比自己硬读快不少。第二,让AI生成补环境的模板代码,省去写基础框架的时间。第三,让AI解释某段算法的数学含义,比如TEA加密的轮函数、魔数变换,作为参考非常方便。

但要注意,AI并不真正理解程序的运行上下文,你让它改代码或补全某个闭包变量,经常会出现幻觉。所以所有AI给的结果都要跑一遍验证,不能直接采信。把它当成一个能快速组织语言的“搜索引擎”,而不是权威专家。

5.5 提升效率的几个小习惯

尽最大努力记录下每次分析时用到的脚本片段,比如Hook模板、环境模板、Frida启动脚本。这些代码可能是几百次调试打磨出来的,存成个人仓库,比收藏几十篇教程有用的多。另外,逆向一个目标时顺手开一个文本文件,把关键偏移、函数名、URL、参数顺序都记下来,数据分析完,笔记也成了。这个习惯能让你在返工或复盘时事半功倍。

逆向这行,说到底就是不断地尝试、记录、再尝试,没有太多神话,关键是耐心和积累。最后再分享一个小技巧:我在分析一个比较复杂的so时,经常会先把每个可疑函数的调用次数统计出来,配合Frida打印函数名,就能快速知道主流程里哪些函数是高频执行的。这个思路很简单,却多次帮我从一堆混淆代码里找到真正的核心逻辑。希望这些几则小技巧能帮你在逆向路上少走两步弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询