news 2026/10/17 15:25:58

前端签名参数逆向实战:定位Acs-Token与ab_sr生成链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
前端签名参数逆向实战:定位Acs-Token与ab_sr生成链路

1. 先搞清楚这两个参数到底在防什么

1.1 抓包看到的第一印象

接到“目标系统”这个接口安全评估任务的时候,我第一个动作和大多数做前端安全的人一样:打开抓包工具,先跑一遍核心业务链路,看请求长什么样。结果没跑几步就看到每个请求头里都带着两个陌生的东西:一个是Acs-Token,一个是ab_sr。值看起来像是一长串十六进制加字母的混合体,长度不固定,字符集也明显不是普通的 Base64。

我习惯先别急着搜 JS 里的参数名,而是先观察这两个值的行为规律。这一步看起来不起眼,但往往能决定后面定位的效率。我手动改了一下请求体里的某个业务字段,服务端直接返回“参数校验失败”;再把这个请求原封不动复制一份,隔几分钟重新发送,服务端又返回了“请求已失效”之类的内容。这两组实验基本说明了一件事:Acs-Token不是写死的静态令牌,它既和请求内容相关,又和时间戳或者防重放机制有关,典型的动态签名参数。

1.2 Acs-Token 和 ab_sr 为什么总成对出现

很多刚接触这类参数的朋友会问:为什么一个系统要搞两个签名参数,一个不行吗?实际上,这两者在大多数前端风控体系里承担的职责是不一样的。Acs-Token更像是一张“请求完整性封条”,它把业务参数、时间戳、随机数、设备信息等拼在一起,用某个摘要或加密算法算出一个值。服务端拿到请求后,用约定好的算法重新算一遍,比对不一致就拒绝。这就是为什么我修改请求体之后回包会直接报参数校验失败。

而ab_sr更偏“行为环境标记”。它通常聚合了浏览器指纹、运行环境特征、用户操作轨迹或者页面生命周期里的一些状态。服务端通过它判断请求是不是来自一个看起来正常的浏览器环境,而不是某个自动化脚本或伪造客户端。如果给这两者做个类比:Acs-Token像是寄快递时箱子上的防拆封条,保证内容没有被换过;ab_sr则像是快递物流记录里的轨迹信息,判断整个过程是不是反常。封条完好只能说明东西没被调包,但一个脚本发出的请求在外面看起来“封条”也可能完好,这时候就需要ab_sr这类风控信息辅助识别。

1.3 大多数前端签名参数的通用设计套路

理解参数背后的设计套路,等于提前拿到了整个调试地图。我拆过不少类似的签名体系,发现它们的骨架高度相似。前端在发起请求前,会先准备一组数据:请求方法、路径、业务参数、时间戳、随机数、客户端指纹,可能还有服务端下发的某个种子值。这些数据按约定好的规则拼接,有的还会做编码或压缩,然后传给一个签名函数。签名函数内部可能使用摘要算法,也可能是对称加密或非对称加密,最终把结果放在请求头或者请求体里提交给服务端。

服务端验签并不神秘,它不需要解出所有字段的原始含义,只要能用相同的算法,基于请求中的内容算出同样的签名,再核对时间窗口和随机数是否新鲜,就可以决定是否放行。这类设计要防的无非是两件事:第一,请求参数被篡改;第二,请求被恶意重放。为了加强第二点,通常还会加入风控字段,ab_sr里的那些指纹和状态信息就是干这个的。搞懂这一层设计后,逆向定位的方向就很清楚了:我们不需要从头到尾阅读整份混淆 JS,只需要找到三个关键点——签名函数在哪里、它是基于哪些输入计算的、计算过程有没有用到动态环境值。

2. 从请求入口反推签名逻辑的几条思路

2.1 先全局搜索参数名,还是先 Hook

如果刚接触这类问题,最常见的做法是在浏览器开发者工具里对Acs-Token做全局搜索,因为在不少系统中,请求头名称是直接以字符串形式出现在网络封装层里的。我个人的经验是:可以搜,但不能只依赖这一条路。搜索命中的位置往往不止一处,有的是在请求封装模块里设置 header,有的是在某个环境检测代码里读取同名变量,还有的是在混淆代码里被动态拼接出来的片段。直接在这些命中点里盲猜,容易绕远。

更稳妥的做法是先通过抓包观察到的特征做一个小判断:如果参数名能搜到,优先从“设置 header”的那个点入手;如果搜不到,直接上 Hook。搜索的价值在于快速确认“这个字符串的拼装点在哪里”,Hook 的价值在于从运行时行为上锁定“这个值到底是在哪一步被写到请求头里的”。两条线同时推进,通常半小时内就能定位到核心模块。

2.2 调用栈回溯:从发送点往回找

我见过不少朋友上来就在 Sources 面板里到处翻代码,想把整个前端资源全部读一遍,这种思路在面对小型站点时还行,碰到大型系统基本是浪费时间。正确姿势是反着来:不要从加密函数往后找,而是从请求发出的那一刻往前找。因为签名函数无论写得多么隐蔽,它最终产生的值一定会被某个代码路径带到XMLHttpRequest.setRequestHeader或者fetch的参数里。我们在发送点打断点,等请求触发后,调用栈自然会把这条路径完整地展示出来。

实际操作用的是 Chrome DevTools 的 XHR/fetch 断点:在 Sources 面板右侧的 XHR/fetch Breakpoints 区域添加一个 URL 关键字,然后点击页面里的某个按钮触发请求。命中断点后打开 Call Stack,就能看到一串很长的调用函数列表。前几个是框架内部的封装,比如请求库、拦截器,继续往下翻,会出现业务代码里比较有语义的函数名。签名函数就藏在这些业务函数附近,通常就是我们肉眼能看到的第一个“比较可疑”的模块。

2.3 监听对象属性,谁读谁负责

还有一种场景比较棘手:参数名在 JS 资源里搜不到,Hook 请求头也看不到明显规律,只知道最终写给服务端的 header 里有一个对象,比如config.headers,它的某个属性被赋成了签名值。这时候可以换个思路,用Object.defineProperty监听这个属性,当代码读取它的时候打印调用栈,谁能读到它,谁就是使用者;谁给这个属性赋了值,谁就是生成者。

我通常会在页面初始化的时候执行一段探针脚本,把这个属性改造成访问器属性,get和set里都加日志。这样接下来不管代码是读还是写,控制台都会输出当时的调用位置。这种方式尤其适合定位那种被模块间反复传递的参数对象。用的时候注意别在正式环境留探针,调试完成记得刷新页面清理掉。

// 探针脚本示例:监听某个请求配置对象的 header 访问情况 (function () { const target = window.someRequestConfig; // 替换成你实际观察到的对象 Object.defineProperty(target, 'headers', { get() { console.trace('[getter] headers 被读取'); return target.__realHeaders; }, set(v) { console.trace('[setter] headers 被赋值', v); target.__realHeaders = v; }, configurable: true, }); })();

2.4 工具准备与工作目录安排

在开始动代码之前,把工具和工作目录整理好能省很多事。我常用的组合是:浏览器开发者工具负责动态调试,抓包工具负责观察请求结构变化,Node.js 作为最后把签名函数迁移过去的运行环境。建议单独建一个目录,把从浏览器里保存下来的相关 JS 文件放到本地,使用格式化功能先美化一遍,再统一搜索关键字符串。格式化这一步很关键,混淆后的代码往往是一整行或者极紧凑的写法,不格式化根本没法做静态阅读。

有一点要提醒:不要一上来就依赖各种自动化逆向工具。那些工具在处理简单场景时效率高,但遇到大型混淆系统容易黑盒化,你只知道结果却说不清中间过程。手工断点加调用栈回溯虽然慢一点,但能把整个签名逻辑的链路彻底摸透,后面不管是要写防护建议还是做加固方案,心里都会更有底。

3. 实战定位 Acs-Token 与 ab_sr 的生成函数

3.1 在 setRequestHeader 打点

定位的核心第一步,是在请求头写入的位置设下埋伏。以XMLHttpRequest为例,我推荐直接在原型链上修改setRequestHeader,给它包一层日志逻辑。这样无论代码从哪个角落发起请求,只要它尝试写Acs-Token或ab_sr,控制台就会立刻打印出参并输出调用栈。用console.trace能直接看到当前调用链,比自己一层层翻调用栈更快。

// 在页面加载后尽快执行这段脚本 (function () { const originalSetHeader = XMLHttpRequest.prototype.setRequestHeader; XMLHttpRequest.prototype.setRequestHeader = function (key, value) { if (key === 'Acs-Token' || key === 'ab_sr') { console.log('[capture]', key, value); console.trace('[trace] 写入请求头的位置'); } return originalSetHeader.call(this, key, value); }; })();

如果目标系统用的是fetch而不是 XHR,就去改写window.fetch,把传入的headers参数完整打出来。这里的思路是完全一样的:凡是签名值最终要经过的通道,都值得插一根探针。需要留意的是,有些请求是在页面加载早期发出的,探针脚本必须放在所有业务脚本之前执行,可以用浏览器扩展或者 DevTools 的 Overrides 功能实现,手动在控制台粘贴往往来不及。

3.2 跟着调用栈走进加密模块

探针一旦生效,点击页面上的某个查询按钮,控制台就会输出类似[capture] Acs-Token: 6f2a...,紧接着的console.trace会给出调用链。我当时的调用栈简化后大致是:

setRequestHeader getHeader (网络拦截层) request (请求封装库) interceptors.request (请求拦截器) handleClick (业务代码) _sign (可疑模块) fingerprint (环境指纹采集)

从这段调用链能看出,签名不是由请求封装库自动生成的,而是业务代码在点击事件处理函数里先调用了_sign,然后把结果塞进 header。所以下一步就是回到 Sources 面板,搜索_sign这个函数名,在它的定义处打断点。等断点命中之后,观察它的入参和返回值:入参里有一个业务参数对象、一个时间戳、一个随机字符串,返回值就是最后写进请求头的那串Acs-Token。到这里,整个签名函数的边界已经非常清楚了。

接着再往fingerprint这个函数里钻,能看到ab_sr的数据来源。这类函数往往读取了canvas、navigator、screen等浏览器环境信息,然后拼接成一个固定格式的字符串。ab_sr和Acs-Token虽然都在这条调用链上,但一个更偏算法校验,一个更偏环境采集,职责分得比较开。

3.3 处理混淆 JS 时的定位技巧

如果遇到参数名被混淆,全局搜Acs-Token搜不到怎么办?这里有几种实操中很管用的处理方式。第一,搜索字符串的十六进制或 Unicode 变体。Acs-Token变成\x41\x63\x73\x2d\x54\x6f\x6b\x65\x6e之后,特征完全不一样,在搜索框里输入Acs或Token这种片段,反而可能命中。第二,把搜索范围缩小到格式化后的代码里,再根据探针日志中记录的调用函数名去反向定位。第三,如果整个脚本做了控制流平坦化,大量代码块被打散,我不建议逐行读懂,而是只看数据流:找尾部返回字符串的函数,看它读哪些变量。

还有一个很小的经验:直接在控制台执行某个可疑函数的toString(),能绕开一部分变量名混淆。因为混淆通常只改源码文本,函数内部的逻辑结构还是会以字符串形式暴露出来。特别是那些体积不大但被包装得很深的签名函数,toString()之后往往能一眼看出它拼接了哪些字段、调用了哪个算法。

4. 把签名函数搬到 Node.js 里跑通

4.1 抽离 Webpack 模块

定位到加密函数之后,很多人会直接在浏览器里继续调试,但更实用的做法是把它搬到一个可以自由控制的 Node.js 环境里。目标系统采用的是模块化打包,我在浏览器执行window['webpackChunkxxx'],看到了它的模块加载器。Webpack 模块的运行机制其实不复杂:每个模块是一个函数,模块间通过__webpack_require__相互引用。我们不需要把整个 webpack 运行时全搬回去,只需要把签名函数所在模块以及它依赖的几个兄弟模块抽出来。

实际操作中,我会在浏览器控制台里先执行模块加载器暴露的入口,找到对应模块的索引号,然后用一个自定义的加载器壳子把模块函数包起来,在一个全局对象上挂载__webpack_require__实现模块间互调。判断模块依赖最直接的方式是看这个函数内部使用了哪些__webpack_require__(编号),把编号一个个复制出来。随后在本地建一个modules.js,按编号存储模块函数,再用一个简单的加载器把它们串起来。

// 本地简易 webpack 模块加载器(教学示例) const modules = { 100: function (module, exports, require) { // 这里是签名模块的真实逻辑,按浏览器中断点处的源码搬入 }, 200: function (module, exports, require) { // 这是签名模块依赖的工具函数 }, }; const cache = {}; function localRequire(id) { if (cache[id]) return cache[id].exports; const module = { exports: {} }; cache[id] = module; modules[id](module, module.exports, localRequire); return module.exports; }

搬代码的时候有一个原则:不要贪多。很多模块虽然被签名函数间接引用,但真正影响签名输出的只有几个。把依赖圈定得越小,后面补环境就越轻松。

4.2 补齐缺失的浏览器环境

签名函数从浏览器搬到 Node.js 最常见的报错就是“某个全局变量不存在”。这类函数为了生成指纹,往往直接读取window、document、navigator、screen等对象。我这边遇到的是函数内部读navigator.userAgent拼进指纹串,还读document.cookie里的某个标识。解决办法是在 Node.js 启动阶段把这些全局对象 mock 成和浏览器环境一致的值。

最简单的做法是给这些全局对象赋值一个最小可用的假对象,只填充签名函数依赖的字段。比如:

global.window = global; global.navigator = { userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', language: 'zh-CN', platform: 'Win32', }; global.document = { cookie: 'user_id=test_abc; session_id=123456', }; global.screen = { width: 1920, height: 1080, colorDepth: 24, };

如果签名函数里还有更复杂的 DOM 操作,不要用手写 mock 硬扛,直接用 jsdom 模拟一个完整文档对象更省力。但对于canvas指纹这类复杂逻辑,我建议直接手动 mock 成一个固定返回值,因为 Canvas 在 Node 里的渲染结果很难做到和浏览器完全一致,它对我们的验证目标并不重要。

4.3 用 Proxy 兜底,缺什么打什么

补环境最怕的是“闭着眼睛补”:你不知道代码会访问什么,只能靠报错一点点试。这里有个非常高效的技巧——用 Proxy 包一层全局对象,任何属性访问只要没命中就打印一条日志。这样跑一次脚本,所有缺失的字段就全部暴露出来了,不用反复试错。

const missingCache = new Set(); const fakeWindow = new Proxy(globalThis, { get(target, prop, receiver) { if (!(prop in target) && !missingCache.has(prop)) { missingCache.add(prop); console.warn('[missing]', prop.toString()); } return Reflect.get(target, prop, receiver); }, }); global.window = fakeWindow; global.navigator = fakeWindow.navigator; global.document = fakeWindow.document;

跑第一轮的时候,控制台会刷出一堆[missing] canvas、[missing] chrome、[missing] devicePixelRatio。此时不要慌,逐个过滤这些字段在签名函数里到底起什么作用:如果只是被读取并拼进字符串,mock 成固定值就行;如果参与了算法计算,就要保证和浏览器里的取值一致。这种“缺什么打什么”的迭代方式,比我一开始手写几十行 mock 靠谱得多。

5. 高频问题与调试思路速查

5.1 “xxx is not defined” 的环境补齐问题

在 Node.js 里执行搬过来的签名函数,最常遇到的就是xxx is not defined。第一反应不要急着补这个变量,而是先想清楚它属于哪一类。如果它是一个浏览器 API,比如window、document、navigator,那就按前面说的方式 mock 对应字段;如果它是一个自定义函数名,那说明模块依赖没有抽完整,需要回到浏览器确认这个函数是从哪个模块导出的,再把对应模块补进本地加载器。

这里容易踩一个坑:某些环境检测代码会故意访问一个不存在的变量,如果补出来的值不对,反而走错分支。遇到这种情况,我建议在浏览器里先确定这个报错变量的真实取值,再拿到本地 mock。比如navigator.webdriver在正常浏览器里是false或undefined,但自动化环境可能是true。签名函数如果把这个值拼进了ab_sr,两边不一致就会导致签名结果不同。

5.2 无限 debugger 与反调试

调试过程中最烦人的不是加密难,而是反调试。我遇到过的无限 debugger 实现通常是往代码里插入debugger;语句,再用定时器反复触发。在 DevTools 里打开文件会被断住,特别是一段混淆代码里埋了好几个debugger,光手动点掉就耗尽耐心。

处理办法是在 Sources 面板里定位到debugger语句所在的那一行,右键选择“Never pause here”,把断点禁用掉。如果反调试逻辑是动态生成的,右键禁用可能不够,我有时会直接把脚本内容下载到本地,批量把debugger替换成空语句,再用 Overrides 功能重写到页面里。这样不影响线上逻辑,只影响本地调试视图。还有一个细节:有些反调试会判断窗口大小或是否打开了 DevTools,不要在页面加载前就开着控制台,先把逻辑跑通再开调试器。

5.3 全局搜索搜不到参数名

如果你把整个前端资源都翻遍了还是搜不到Acs-Token,别急着怀疑方向,先检查是不是字符串被做了变形。我以前遇到过一种写法,字符串被拆成"Acs" + "-Token"再拼回去,直接搜完整名称当然搜不到。这种场景下,搜单个片段能提高命中率。

另一种可能是参数名本身不是常量,而是由某个函数从一个配置表中动态取出来的,比如getHeaderName('token')。搞清楚这一点需要结合探针打印的调用栈,看看写 header 的那个函数是否接收了一个逻辑名称。只要探针日志完整,顺着调用栈往上找,一般都能定位到转换逻辑。

5.4 本地签名结果和浏览器不一致

把签名函数搬到 Node.js 后,最令人头疼的就是同样一套代码,两边算出来的值不一样。出现这种情况,先不要怀疑算法搬错,优先怀疑环境差异。签名输入里只要有时间戳、随机数或者浏览器环境值,任何一点细微差别都会让结果完全不同。

我的排查思路是先做“变量固定”:在浏览器调试时,把签名函数接收的时间戳和随机数手动替换成固定值,观察输出;再在 Node.js 里用同样的固定值跑一遍,逐一比对每个中间变量。如果中间值对不上,就查看它依赖的环境字段是什么,去浏览器里读取同名字段的值,再回来补齐 mock。还有一个容易被忽略的点:有些签名函数内部会读取performance.now()或Date.now()这样的动态时间,这类字段无法通过简单 mock 完全固定,需要在对比时尽可能在同一个时间窗口内执行。

现象可能原因排查方向
报错某个浏览器全局变量不存在环境缺字段用 Proxy 日志定位缺失字段,再 mock 浏览器取值
本地运行被反复 debugger 卡住插入的反调试逻辑搜 debugger 并禁用,或用本地文件替换
参数名全局搜索搜不到字符串被拆分或动态拼接搜片段、查看十六进制变体、利用探针调用栈
Node 与浏览器签名不一致环境字段差异或动态时间固定时间戳和随机数,逐层打印中间值对比
模块间提示 require 方法未定义模块依赖抽取不完整回到浏览器确认模块依赖关系,补全模块函数

调试这类前端签名参数,我个人最大的体会是:不要一上来就盯着加密算法本身,先把请求发送链路摸清楚。Acs-Token和ab_sr这类参数看着唬人,本质上只是一个校验体系的出口,真正值得花时间的其实是“它从哪来、输入是什么、依赖哪些环境”。把这些搞明白,无论后面是要做自己系统的安全加固,还是评估现有防护是否可靠,都能对症下药。另外老生常谈一句:所有调试行为都必须限定在自己拥有权限或获得明确授权的系统范围内,这套分析思路是用来保护系统、找出防护弱点的,不是用来绕过别人防线偷数据用的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 15:25:09

计算机网络概述与体系结构:分层模型、核心协议与实战应用

在计算机网络这个方向上摸爬滚打这些年,我最大的感受就是:很多人一开始就扎进各种协议细节里,反而越学越乱。真正让我把整个知识框架串起来的,恰恰是回过头来重新审视“网络概述与体系结构”这一块基础内容。这个主题乍一看平淡无…

作者头像 李华
网站建设 2026/10/17 15:22:13

PCB安规设计:爬电距离与电气间隙的确定与Layout实操

简介:这份PDF资料聚焦PCB Layout设计中的爬电距离与电气间隙确定方法,面向硬件设计工程师、PCB设计初学者及需要应对电气安全标准的从业者,帮助解决绝缘配合计算与安全距离取值问题。内容系统梳理了爬电距离与电气间隙的定义差异,…

作者头像 李华
网站建设 2026/10/17 15:22:08

工业大模型车间落地:从演示到数字员工的完整路径与实战拆解

1. 为什么工业大模型必须变成"车间里的工种"才能活这几年做工业数字化项目,被身边同行问得最多的一个问题已经变了:大模型到底能不能在车间里真的干活,而不是只活在演示PPT上。这个问题的答案,不取决于模型本身跑分多高…

作者头像 李华
网站建设 2026/10/17 15:21:16

OpenHarness与Hermes Agent:任务箱加工具闸阀,打造稳定可控的AI智能体

1. Hermes Agent 到底解决什么问题 先把这个标题拆开看。OpenHarness 不是一个普通的脚本仓库,也不是一个简单的机器人框架,它更像是一套“给智能体穿上的操作台”。Hermes Agent 则是跑在这个操作台上的具体智能体实现。很多人一上来就盯着“Agent”三个…

作者头像 李华
网站建设 2026/10/17 15:20:32

Flash凸透镜成像课件失效?原理拆解、转视频与HTML5仿制全攻略

简介:《探究凸透镜成像规律》flash动画PPT课件,是一份面向初中物理教师、初二学生及光学知识自学者制作的多媒体教学资源,用于直观演示凸透镜对光线的折射作用以及不同物距下的成像性质,解决传统板书或静态图片难以呈现的动态光线…

作者头像 李华