
做逆向这几年我最大的感受是真正难的不是某个高深算法而是大量细碎的“卡点”。比如一个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逆向最常用的定位方式就三招。第一招全局搜字符串。比如看到请求参数里有sign、token、encrypt、sessionid这类关键词直接在DevTools的Sources里搜索通常能跳到定义的地方。第二招看调用栈。Network面板里找到目标请求点一下“Initiator”或“Call Stack”浏览器会告诉你这个请求从哪个函数发出去的。第三招基于包结构反推。请求负载里的字段名和顺序往往对应代码里对象的构造顺序搜其中一个特殊字段名常能一抓一个准。拿我调试过的一个案例说当时一个请求里有个fgid参数长度五个字符在代码里搜这个词命中了三十多处。这时候别一个个看直接在搜索框里输入fgid:、fgid快速过滤赋值语句。很快定位到一个工具函数里面调用了某个哈希算法。核心思路就是用“字段名加赋值上下文”缩小范围而不是满世界翻文件。如果你的目标JS做过代码压缩或者混淆变量名都变成a、b、c这种搜索字段名的思路依然有效。因为字符串常量无法被混淆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依赖window、document、navigator这些全局对象在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加密传回。定位的时候我发现请求里有w和bg两个字段搜索bg很快就找到了轨迹采集代码。再搜AES加密的关键特征比如CryptoJS、enc.Base64就找到了加密入口。把入口函数抠出来以后剩下的就是补canvas的toDataURL方法因为代码需要通过它里的像素信息生成指纹。我补了一个固定返回值的假canvas加上轨迹数组用随机数模拟结果后端照样校验通过——因为真正校验的是轨迹是否像人类操作而不是像素内容。这个案例说明逆向不是把每个逻辑都还原而是找到服务端真正校验的那部分针对性处理。有时候你花几天还原一个算法最后发现参数根本不会被校验白费功夫。3. 安卓逆向实用技巧3.1 用jadx快速定位Java层代码安卓逆向和JS逆向最大的不同是多了一层Java代码和一个Native世界。拿到APK以后我习惯先把APK拖进jadx直接看反编译后的Java代码。jadx的好处是能够以类名和包名浏览像读源码一样。定位功能和Windows逆向确实很像先搜字符串再找引用。比如某个功能会在跳转时带上一个加密参数那就在jadx里搜索参数名如果搜索不到就搜索接口的URL字符串再从OkHttp或者Retrofit的调用点回溯。另外一个技巧是看AndroidManifest.xml入口Activity、Service、动态广播都在这份文件里可以从入口开始追。对于有加固的Appjadx打开会看到一堆壳类这时候就先脱壳后面第五节再说。如果是用uniapp、Flutter这类跨平台框架写的AppJava层可能只是一个容器真正的业务逻辑在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]是JNIEnvargs[3]是jstring。把入参和返回的字节dump出来能帮你快速验证so是否就是加密核心。比起纯静态看汇编动态确认输入输出和静态还原逻辑结合才是完整路径。3.3 像Windows逆向一样找功能代码有朋友问我APK逆向能不能像Windows逆向那样看到一个关键字符串就顺藤摸瓜找到功能代码答案是能而且思路完全一样。Windows程序里程序员调用MessageBoxA(注册成功)你会看到那个字符串被压在栈里函数引用清晰地指向处理逻辑。安卓里也一样jadx中搜索“注册成功”会搜到R.string.xxx再到值引用处就能看到这段UI处理代码。唯一不同的是布局资源把字符串抽走了所以优先搜资源ID或硬编码的URL。实在找不到字符串就抓运行时的提示框利用Frida去HookToast.makeText或Log方法打印调用栈。这种“从结果倒推代码路径”的办法比从入口硬撸整个Activity效率高得多。还要注意App常常把关键算法做成动态加载字符串被分割或加密。遇到这种就需要先在内存里把字符串还原出来再去定位引用。可以用Frida扫描内存里的Unicode字符串搜目标URL或参数名的子串命中后打印周围内存经常能扒出一片完整逻辑。3.4 脱壳的基本思路现在很多App都做了加固dex壳会把真实的类藏起来。脱壳的本质是把内存中已经解密/还原的dex dump出来。常用的两个路子一是利用Frida在运行时dump dex文件二是用现成的脱壳机比如frida-dexdump或定制系统里的dump脚本。我自己的习惯是先识别加固厂商查壳的静态特征再选对应方案。大部分加固在内存里都会有完整的dexdump出来之后用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, datadata) 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补一个带getItem、setItem方法的对象补环境时不要一个报错补一个补完就跑直到下一个报错。这是最低效的方式。更好的做法是先在代码里搜索所有window.、document.、navigator.的引用点把所有用到的属性列个清单一次性建一个对象。这样会减少很多的来回。5.4 用AI辅助逆向的几个姿势最近总有人问“怎么让codex帮我逆向提示词”我的看法是AI可以帮你在思路上加速但不能替你判断。我现在常用的场景有三个。第一把一段混淆后的JS或反汇编伪代码贴给AI让它总结逻辑流程这比自己硬读快不少。第二让AI生成补环境的模板代码省去写基础框架的时间。第三让AI解释某段算法的数学含义比如TEA加密的轮函数、魔数变换作为参考非常方便。但要注意AI并不真正理解程序的运行上下文你让它改代码或补全某个闭包变量经常会出现幻觉。所以所有AI给的结果都要跑一遍验证不能直接采信。把它当成一个能快速组织语言的“搜索引擎”而不是权威专家。5.5 提升效率的几个小习惯尽最大努力记录下每次分析时用到的脚本片段比如Hook模板、环境模板、Frida启动脚本。这些代码可能是几百次调试打磨出来的存成个人仓库比收藏几十篇教程有用的多。另外逆向一个目标时顺手开一个文本文件把关键偏移、函数名、URL、参数顺序都记下来数据分析完笔记也成了。这个习惯能让你在返工或复盘时事半功倍。逆向这行说到底就是不断地尝试、记录、再尝试没有太多神话关键是耐心和积累。最后再分享一个小技巧我在分析一个比较复杂的so时经常会先把每个可疑函数的调用次数统计出来配合Frida打印函数名就能快速知道主流程里哪些函数是高频执行的。这个思路很简单却多次帮我从一堆混淆代码里找到真正的核心逻辑。希望这些几则小技巧能帮你在逆向路上少走两步弯路。