Web端JS逆向实战:断点调试与签名算法分析全流程
2026/9/15 1:06:47 网站建设 项目流程

1. 我为什么要做这次Web端逆向分析

做前端性能优化或者数据采集的同学,应该都遇到过这样的情况:网页上明明能看到数据,但接口返回的内容却带着一堆看不懂的加密参数。你要么放弃,要么就得去扒这些参数是怎么算出来的。

我这次处理的目标是某书的Web端,也就是标题里说的“小某书”。它属于典型的“外层看着简单、内部塞满加密逻辑”站点。为什么选择拿它做案例?因为它足够有代表性:页面功能不少,前端打包产物做了细致的代码分割,接口请求里塞了好几个签名相关的自定义Header,同时它在不同版本的迭代里调整过参数的生成方式。这意味着,你可以用一套非常通用的JS逆向流程去分析它,而这套流程放到其他同类站点上也照样能用。

本文把我从抓包、定位、断点调试,到最终梳理出签名生成链路的过程完整记录下来。适合刚接触JS逆向、想建立一套可复用调试流程的人,也适合已经能看代码但经常断点断不明白的朋友。先说清楚边界:这篇文章只做技术分析和学习分享,不针对任何平台做破解、绕过或攻击,所有涉及目标站点的内容都以公开页面和前端资源为分析对象。

很多人一听“逆向”两个字就紧张,总觉得是灰产或者黑产才做的事。其实“JS逆向”本身是个中性词,本质上就是通过可执行的前端代码去倒推它的运行逻辑。这和你在GitHub上读一个开源项目的源码没有本质区别,只不过这个项目的代码是压缩混淆过的,你只能通过动态调试去理解它。

我常跟团队里新人说一句话:写代码是把想法变成指令,逆向是把指令还原成想法。你日常开发时调试自己的Bug,其实也在做类似的事,只不过对象变成了别人的代码。带着这种心态去做分析,你会少很多心理负担,也多很多耐心。

2. 逆向分析的整体思路拆解

2.1 先做黑盒观察,不要急着上断点

很多新手一上来就打开DevTools到处点,看到有报错就打断点,结果断点命中了也不知道自己在哪。我建议的流程是先做黑盒观察,把目标当做一个不透明的系统,只看输入和输出。

比如某书Web端首页的信息流接口,你往下滑动页面,Network面板里就会出现一批请求。先别管加密参数怎么来的,你先看这几个东西:

  • 请求URL是什么,路径里带了哪些参数;
  • 请求方法是GET还是POST,参数在Query里还是在Body里;
  • 请求头里有哪些自定义字段,哪些字段每次请求都变,哪些不变;
  • 响应数据里有没有你想要的核心信息,是明文还是加密。

这一步的目的是建立“最小可复现请求”。你先通过复制cURL或者直接改参数重放,确认这个接口在什么条件下能返回数据。如果少了某个签名头就报错,那么这个签名头就是你需要逆向的目标。

2.2 从输入到输出,建立自己的“调用链地图”

有了最小可复现请求之后,逆向的整体思路就非常清晰了。你需要沿着请求的触发路径,找到加密参数生成的那一段代码,然后把这段代码从浏览器环境中剥离出来,在Node里模拟执行,最终实现不依赖浏览器就能生成合法请求。

这个流程可以拆成四步:

  1. 找到触发请求的入口逻辑,无论是按钮点击、滚动加载还是定时器;
  2. 在该入口附近对网络请求相关方法做断点,拿到完整的调用栈;
  3. 在调用栈中寻找生成签名参数的函数,分析它的依赖数据;
  4. 把分析得到的逻辑移植到本地环境,用同样的输入做验证。

整套思路说白了就是一句话——“先看数据从哪里来,再看参数在哪里被算出来”。如果把加密参数比作一盘菜,那么抓包等同于看成品菜的外观,调用栈就是后厨的监控录像,你顺着录像就能看到厨师从哪个冰箱取了什么食材、用哪口锅炒、放了多少调料。

2.3 为什么大多数攻略都强调“最新版”三个字

标题里写了“最新版”,这其实非常关键。Web端逆向有一个特征:时效性极强。今天能用的签名算法,可能过两周就换了,也可能同一天在不同地区的用户身上返回不同的结果。你在网上搜到的老教程采用的是某个固定版本代码片段,甚至连参数名都和现在完全不同,照着抄基本不可能成功。

所以我这次刻意以“最新版”为分析基调,也建议你以后看到类似题目时保持怀疑:与其找一个旧代码复制粘贴,不如现场从当前的页面资源里重新定位。逆向分析最宝贵的不是最终那几行算法,而是你定位算法的速度。版本越新,市面上可参考的资料越少,对你基本功的考验也就越大。

3. 工具与准备环境

3.1 工具清单

工欲善其事,必先利其器。做JS逆向用得最频繁的工具其实就那几样,不需要装一堆花里胡哨的软件。我在这台机器上长期保留的是下面这套组合:

工具用途替代方案
Chrome DevTools主战场,断点调试、观察调用栈、编辑代码Edge DevTools、Firefox
Fiddler/Charles抓包、重放、修改请求响应Whistle、Mitmproxy
Node.js本地模拟运行逆向出来的算法Python的PyExecJS、quickjs
VS Code整理代码逻辑、写验证脚本Sublime、WebStorm

如果你只想从零开始,Chrome DevTools加Node.js这两样就够用。Fiddler主要解决的是移动端或者其他浏览器环境的抓包需求,纯Web端调试用不上它。

3.2 环境配置的几个细节

很多人在本地运行逆向代码时报各种错,回头发现是环境变量的问题。这里说几个我踩过坑的点。

第一个是浏览器版本和Node版本之间的差异。现在前端构建环境大多支持ES6甚至ES2020以上的语法,你从压缩代码里摘出来的函数大概率也会用到?.??BigInt这类特性。本地Node版本如果太老,语法解析阶段就会报错。建议把Node升到LTS版本以上,最好直接用18或者20。

第二个是“补环境”的问题。浏览器里的代码会依赖windowdocumentnavigator这些全局对象,你把函数抽到Node里跑,通常得临时造一个模拟环境。常见的做法是用jsdom把基础的DOM API补齐,或者在代码入口处判断一下全局变量是否存在。需要注意,这一步只是为了让你自己的验证脚本能跑通,不是在教你伪造请求内容。

第三个是数据流的一致性问题。很多算法的输出依赖输入,而输入里面可能包含时间戳、随机数、页面上下文里的状态值。你在Chrome DevTools里断点命中的那一刻,和你在Node里运行脚本的那一刻,哪怕相差一秒钟,生成的结果都不相同。所以在做对比验证时,不能只看最终输出的字符串是否相等,而是要把输入固定成同一个值,或者把随机部分处理好再比对。

4. 核心环节:定位加密参数与断点调试

4.1 如何快速锁定参数位置

拿到一个请求后,我通常会先看URL和Header里有哪些非常规字段。比如某书Web端请求头里会有类似x-sx-t这种自定义签名Header,一眼就能看出来是前端加上的。

这时候直接在Sources面板里按Ctrl+Shift+F全局搜索参数名。如果站点没有做严格的字符串拼接或编码,你会直接在某个.js文件里定位到对应的赋值语句。

但实际经验是,很多站点不会把参数名明文写死。我之前遇到过一种情况:参数名是由一个数组拼接出来的,数组里的字符串片段分散在不同模块里,全局搜索完整参数名完全没结果。这种时候就要换个思路,在Network面板里把请求头整个复制出来,搜索请求头里的“值”,而不是“键”。因为值通常是一个动态结果,你搜索值的时候,如果能命中某个变量名,说明这个变量就是该参数的上游来源。

另一个很实用的技巧是使用XHR断点。在Network面板里右键你要分析的请求,选择“Add XHR/fetch breakpoint”,输入一个URL片段,当代码发起匹配该片段的网络请求时,浏览器会自动暂停。此时你切换到Sources面板,就可以从Call Stack面板里看到一串调用过程。

我自己的习惯是先在XHR断点处暂停,然后看调用栈底部的几层,找到发请求的入口函数,再在入口函数里手动打断点,刷新页面重新触发一次。这样做的目的是缩小范围,不让自己深陷到库代码的迷宫里去。

4.2 断点调试的三种常用姿势

断点不是越多越好,关键是断在正确的位置。我常用的断点姿势有这三种:

第一种是普通行断点。找到可疑代码行,点击行号即可。这个是所有方法的基础,适合你已经定位到某个函数,但想知道它内部每一步执行结果时使用。配合Watch面板添加表达式,可以在不打断代码节奏的情况下观察关键变量的变化。

第二种是条件断点。如果代码在一段循环里执行了很多遍,或者某个函数被频繁调用,普通断点会让你按到怀疑人生。右键点击行号,选择“Add conditional breakpoint”,填入一个判断表达式,比如i === 3 && this.value > 100,满足条件时才会暂停。这个技巧对过滤无关调用特别有用。

第三种是Logpoint。它其实是断点加控制台输出的融合体,在右键菜单中选择“Add logpoint”,填入一段表达式,浏览器不会暂停,但会在Console里输出表达式的结果。适合你需要观察一个值的平缓变化过程,又不想一次次手动恢复执行的情况。我在分析长列表滚动加载的请求时经常用这个方法。

4.3 面对压缩混淆代码怎么办

压缩后的代码确实难看。变量名全是abc,函数名完全没有语义,但压缩混淆不等于不可读。Webpack打包的产物通常有固定的模块结构,只要定位到模块加载器,就能按模块去整理逻辑。

一个常见做法是先点击DevTools里的“{}”按钮,让浏览器帮把压缩代码格式化。格式化之后代码的可读性会提升不少,但变量名不会变好。你还需要做二次整理:把频繁出现的关键函数重命名成你能理解的名字,把关键变量重新命名为timestampsignatureInput这类标识符。

Webpack的模块列表也有迹可循。模块加载器通常是webpackJsonp.push或者一个中心化的__webpack_require__函数。如果你搜索某一个比较特殊的字符串能命中多个模块,说明这些模块可能共享同一个依赖,而你定位的函数大概率就活跃在这个依赖关系网里。

还有一个常见套路:很多站点会把核心算法放到一个独立的IIFE(立即执行函数)中,外部只暴露一个入口。此时的代码边界会比较清晰,你要做的就是找到这个IIFE的返回值,看看它被谁接收、最终赋给了哪个变量。知道了入口和出口,算法内部就算再像是天书,你也只需要关注和加密参数有关的几个分支。

5. 实操记录:以某书Web端签名头为例

5.1 线索溯源:从一个请求头开始

我随便打开某书Web端,刷新首页,Network面板里看到信息流请求路径大概是/api/sns/web/v1/homefeed这类的结尾。现有的请求头里除了常规的user-agentreferer之外,还带了一个自定义签名Header,每次刷新值都不一样。

我的第一步不是搜索这个Header名,而是先把它当前的值复制出来,放到Sources面板里全局搜索。这个值通常不是固定常量,搜索值一般也不会直接命中源文件,但搜索之后可能会得到零结果,这本身就是有用信息——说明该参数不是直接以字符串形式出现在代码里的。

于是我把策略调整为搜索Header名。这次命中了一段代码:某个对象里定义了从请求上下文读取某些字段,再经过一个函数处理,最终把返回值赋给这个Header。顺着赋值语句向上追踪,我看到它依赖了路径、查询参数、时间戳以及一个固定密钥相关的变量。

看到依赖项之后,我做了两件事:第一,用Network面板重新发一次同样的请求,将签名Header值复制进本地临时文件里存档;第二,在赋值函数那行打上断点,刷新页面,等代码暂停后,逐个查看Watch里的变量值。

这个过程听起来行云流水,实际会有很多反复。因为函数调用堆栈可能有三四十层深,而且大量代码是异步的,断点很可能命中在无关的Promise回调里。我的做法是宁可多刷新几次,也要保证每次断点命中都能记录下调用栈、当前作用域和闭包变量,凑三次以上再去归纳规律。

5.2 断点命中与调用栈分析

断点命中那一刻,Chrome会在Sources面板右侧展示当时的Call Stack。栈顶是当前正在执行的函数,往下是调用者。我在分析某书Web端时发现,签名Header的生成并不是在XHR发送的最内层完成的,而是在一个更早的请求包装层就被计算好,然后作为Header一起传递给底层的fetch方法。

这说明什么?说明签名生成和请求发送是分开的。底层请求模块只管把Header带上,实际签名逻辑在业务代码层早就已经算好。于是我的关注点从“发送请求前在哪里加密”切换到“业务代码里哪个模块调用了签名函数”。

我选择在Call Stack中部的一帧里停下来,往上层看调用者,很快看到一个工具函数模块,函数名不能说和签名完全无关,但也不是明晃晃的createSignature这种风格,而是更偏向于缩写。这时候我使用了本地重写功能:在Sources面板里选中这个工具函数所在文件,右键选择“Override content”,把这行函数在执行后把入参输出到console里,保存后再次刷新页面,console里就能看到签名函数接收到的完整参数对象。

得到入参后,我打开浏览器的“Event Listener Breakpoints”,把FetchXHR两类断点全部取消,避免后面调试时频繁被无关请求打断。接着在工具函数内部的关键分支逐行单步执行,同时用Watch面板观察一个对象变量从空对象一步步变成包含时间戳、路径、查询参数、密钥的完整结构。到这一步,签名算法的输入输出全都在手里了。

5.3 在本地环境里跑通整套逻辑

有了输入和输出,接下来要做的就是把函数依赖的代码从浏览器中复制到本地。复制的时候不能只复制签名函数自身,它引用到的辅助函数、常量、编码方法都要一起带上。最稳妥的方式是借助Source Map或者模块列表,把依赖模块逐个拷贝出来。

我在某书Web端这个例子里需要的依赖包括:一个MD5相关的摘要函数、一个时间戳格式化函数、一个对查询字符串做排序的工具函数,以及一个密钥常量。把这些抽取到一个Node脚本后,我构造同样的入参,执行签名函数,将输出的值和浏览器里抓到的值做批量比对。

第一次比对就发现不一致,这是正常的。我后来查证发现,时间戳字段在签名逻辑里用的是毫秒级,但请求Header里暴露的是秒级;另外,部分英文字母在排序时要统一转成小写。这种边界情况不看实际执行结果根本发现不了。

环境补齐也是个问题。某书Web端的代码里会读取location.pathname,但我在Node里没有这个对象。我用global.window = {}global.location = { pathname: '/' }这样的模拟方式先把缺失变量补上。请记住,这个动作是为了让研究代码能跑起来,不代表你应该伪造任何敏感请求。

最终跑通之后,我写了一小段验证脚本,用最近几次抓包得到的请求路径和查询参数作为输入,自动生成签名Header,再通过Node发起真实的HTTP请求去和浏览器行为做对比。需要强调的是,我的验证请求发送得非常克制,没有做循环采集、没有污染线上数据,仅仅为了校验签名算法是否能复现。

6. 常见问题与排查技巧

6.1 新手最容易踩的五个坑

排查问题这件事,最大的成本往往不是修复,而是定位。以下是我做JS逆向以来遇到的高频问题,基本能覆盖80%以上的卡壳场景。

现象可能原因处理办法
全局搜索参数名搜不到参数名被拼接、编码或拆分成多段搜索参数值,或用XHR断点直接拦截请求
断点能命中,但调用栈里全是库代码实际业务函数在异步回调里切换Event Listener断点,只看Fetch或XHR
本地Node执行结果和浏览器不一样缺少“环境”,或依赖了随机数用jsdom补环境,或固定时间戳后再比对
同一个参数的版本变化太快服务端动态下发逻辑或灰度配置多抓几次请求,观察规律,不要依赖单次样本
页面检测到调试就自动跳出站点存在反调试逻辑使用Logpoint代替断点,避免长时间暂停

6.2 反调试的几种常见手法与应对思路

某书Web端这一类站点多少会带一点反调试逻辑,最常见的是在源码里埋一个debugger语句,配合无限循环,让你一打开DevTools就卡死。还有一种是监听Console是否被打开,改变页面行为或输出干扰信息。

面对debugger卡死,我自己的解决办法是不用普通断点,改在Function.prototype.constructor或者关键函数调用处打断点。还可以用Never pause here右键菜单把某个debugger位置标记为永不暂停,这样就不会被反复弹窗打扰。

另外,有些反调试逻辑会检查Date.now的变化频率。长时间在断点里停留,会让时间戳出现异常跳变。如果发现签名结果在本地死活复现不了,可以先检查一下是不是这个原因。我的习惯是提前在脚本里写好一个“时间冻结”的工具函数,在需要复现时把Date.now临时替换成固定值。

6.3 关于“签名算法失效”的排查思路

服务端更新算法是常规操作。标志性现象就是,你本地跑得好好的签名脚本,过了一晚突然生成出来的Header不被接受。这时候不要慌,按三个方向去排查:

第一,检查请求参数是否有变化。服务端可能新增了一个必选参数,你没带上去,签名值自然就无效。你把浏览器里最新抓到的请求体和老请求体做diff,一眼就能看出差异。

第二,检查密钥是否过期。有些签名算法会用服务端下发的临时token作为密钥的一部分,token过期之后,算法再正确也没用。这种时候先看页面里是否有新的token被注入到HTML或异步接口里,再进行二次组合。

第三,检查是否引入了新的反爬策略。比如服务端开始校验完整的Header顺序,或者要求请求必须携带特定的Cookie。别只盯着签名Header,把整个请求的Header列表都对比一遍。

6.4 一条争议边界:什么样的分析行为是安全的

写到这里,我总觉得应该把边界问题说透。JS逆向这个技术本身没有原罪,但它确实可能被用来做自动化脚本、绕过风控、批量爬取用户数据。这些用途在法律和道德上都有很大风险。

我自己内部有一个安全红线清单:

  • 只分析自己有合法权限的页面和接口,比如公开页面、自有站点或获得明确授权的项目;
  • 不将逆向结果用于批量采集、价格监控、恶意请求、撞库等行为;
  • 不以任何方式获取或存储用户隐私数据,哪怕这些数据在公开接口里能够拿到;
  • 不在生产环境反复测试签名接口,避免给平台带来压力或影响真实用户体验。

这篇文章里写的所有内容,本质上都是“如何阅读一段JavaScript代码”的延伸。如果你拿它去做正当的前端安全测试、性能优化或者是学习研究,那是好事;如果你拿它去打灰色擦边球,那请你立刻关上页面。

7. 写在最后:这个能力应该怎么用

如果你只想要一份可以抄的代码,这篇文章可能会让你失望。但如果你想要的是面对陌生站点时的一套分析思路,我相信前面这些内容足够你上手了。

我做JS逆向这几年,最大的收获不是“能搞定某个平台的加密参数”,而是练出了一种习惯:遇到问题先看现象、再列假设、然后用断点和调试逐一验证。这种习惯让我在做普通前端开发时也受益匪浅——很多东西我只会在真实执行时暴露问题,而动态调试正是发现这类问题的最高效方式。

对于想入门的朋友,我的建议是不要从某书这种级别的大站开始。你可以先找一个没有混淆、没有反调试的小网站练手,比如一个简单的后台管理系统,试着找到登录接口的加密方式,再逐步增加难度。先把定位参数的流程走顺,再去面对代码混淆和反调试。

如果你已经有一定基础,不妨试着把你逆向出来的逻辑整理成一份技术笔记,记录你当时是怎么找到入口的、踩过哪些坑、最终如何验证。这个整理过程会逼迫你把模糊的判断变成清晰的流程,下一次再遇到类似站点,你的速度会快上很多。

最后说一句我一直挂在嘴边的话:前端代码是跑在用户设备上的代码,它注定是透明的。它不是用来做隐藏的工具,而是用来做验证的入口。真正重要的,是你看待它的时候,遵守规则、保持克制。

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

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

立即咨询