大概两年前,我接手一个Web端安全评估的项目,重点之一就是某OTA平台的风控token。第一次在接口响应里看到code: 1002, msg: token invalid的时候,说实话我愣了一下。网上的说法五花八门:有人说1002是token过期,有人说是签名被篡改,有人说是设备环境异常。这些说法其实都沾点边,但都不完整。后来我把整个分析过程跑完,回头再看这个1002,才意识到一个关键问题:对一个风控token做算法分析,最重要的不是猜它用什么哈希,而是先搞清楚它在请求链路里到底扮演什么角色。
这篇文章就以"携程token1002 算法分析"为切入点,聊一聊我是怎么拆解这类前端风控token的。不会给你一个直接复制就能跑的脚本——那既不负责也不安全——而是分享一套可以复用到其他平台的分析方法论:怎么确认1002的真实身份,怎么拆token字段,怎么推导签名算法,怎么验证你还原的算法是对的。适合正在做安全测试、前端功能开发,或者对风控对抗机制感兴趣的工程师参考。
1. 先搞清楚 token1002 到底是什么
1.1 它可能是错误码,也可能是参数标识
我见过太多人一上来就奔着算法去了,结果连目标都没搞清。1002这个数字,在携程这类平台的请求体系里,至少有两种身份。
第一种是错误码。接口返回的JSON里出现code: 1002,msg里面写着token相关文案,这代表token已经生成并且提交上去了,但服务端校验没通过。这种情况下算法本身可能没毛病,问题出在校验环节:时间窗口超了、参数被改过、或者设备和cookie对不上。第二种是参数标识。某些内部接口会把场景ID或版本号塞进token相关参数里,1002可能是某个风控场景编号,比如"搜索场景"或"下单场景"。
判断方法很简单:打开DevTools的Network面板,找到报错的那条请求,分别看请求参数和响应体。如果请求参数里有token相关字段而响应体里返回1002,那它多半是错误码;如果token字段本身带着1002这种可辨识的子串,那更可能是场景标识。这一步不做,后面全是空中楼阁。
1.2 风控token在请求链路中的位置
搞清楚1002的身份之后,第二步是定位token在整个请求链路里的位置。前端风控token的生成时机通常有两种:页面初始化就生成,或者某个关键事件触发后才生成。搜索框聚焦、滑块校验、提交订单,都是常见的触发点。
以OTA平台的典型链路来说,大概是这样一个流程:页面加载时,前端SDK先采集设备信息,包括UA、屏幕分辨率、Canvas指纹、时区、语言环境;然后SDK会在后台持续采集用户行为,比如鼠标移动轨迹、点击坐标、停留时长;等到要发核心请求时,把这些信息连同时间戳一起交给一个token生成函数,产出最终token,塞进请求头或请求体。服务端拿到token后,先做签名校验,确认参数没被篡改;再看时间戳,确认请求在有效窗口内;最后看设备指纹和风控策略,决定放行还是返回1002。
这个链路理解透了,你就知道分析的重点应该放在哪个环节。如果1002出现在所有请求上,问题大概率在基础指纹采集;如果只出现在特定接口,那问题就在那个接口的专用逻辑里。
提示:分析任何站点前,先确认自己是否有授权。合规是底线,也是保护自己的方式。下面的方法都默认你在做授权范围内的安全评估或功能调试。
1.3 分析前的材料准备
动手之前,把工具和材料备齐,能省下一半的返工时间。
- 浏览器DevTools:抓请求快照、打断点、搜索JS源码都用它。
- 抓包工具:Charles或Fiddler都行,用来做更细粒度的请求对比。
- Node.js环境:后期要把前端JS函数拉出来单独跑,没有Node寸步难行。
- Python环境:写脚本做样本对比、字段统计、自动化验证。
材料方面,至少记录三样东西:第一,报错发生时的完整请求URL和请求头,注意里面有没有token相关字段;第二,响应体原文,1002的具体msg文案越长越好;第三,时间点,精确到秒,因为后面做时间窗口分析时要用。
很多人在这一步随手截个图就完事了,我强烈建议你保存成文本文件,按时间命名。后面做样本对比时,一份干净的请求日志比任何逆向工具都值钱。
2. 拆解token的组成:先看,再猜,后验
2.1 第一梯队:肉眼能识别的字段
拿到真实token样本后,先别急着上工具,用肉眼做一轮初筛。这一步能过滤掉一半以上的无意义猜测。
先看长度和字符集。如果token是30位左右的十六进制字符串,里面只有0-9a-f,大概率是某种哈希值;如果出现/、+、=这类字符,大概率是Base64编码过的数据;如果能看到点号、下划线、横杠之类的分隔符,那基本是多个字段的拼接体,分隔符就是天然的分段边界。
举个例子,我教学时经常用这样一个构造样本(注意,这不是真实平台的token,只是用来演示特征):
a3f8c1e2.1700000000.9b2f4d17e8c3a1b6.5f7a2c9d四段之间用点号隔开。第一段a3f8c1e2是8位hex,短小,可能是一个固定算法的前8位或设备指纹的摘要前缀;第二段1700000000是标准的10位Unix时间戳,肉眼就能认出来;第三段9b2f4d17e8c3a1b6是16位hex,像某个哈希值;第四段5f7a2c9d又是8位hex。有了这个初步判断,后面的分析就有了方向:时间戳部分可以直接验证,哈希部分去定位算法,短前缀部分去追踪设备指纹。
2.2 第二梯队:需要还原的字段
肉眼能看出来的只是皮毛。风控token里真正有信息量的,是那些看起来像乱码的部分。它们通常承担三种职责。
第一种是签名。签名的作用是防篡改,服务端会用同样的算法对请求参数重新计算一遍,比对不上就返回1002。签名的特征是长度固定、对输入极其敏感,你改一个字节,输出会变得面目全非。第二种是设备指纹。这类字段通常是把UA、Canvas指纹、屏幕参数等信息经过哈希或编码后的结果,短时间内在同一设备上保持不变,换设备或换浏览器就会变。第三种是行为特征,鼠标轨迹、点击热区、键盘输入节奏都被编码成紧凑的字节序列,这个最难还原,因为它依赖一套完整的行为采集SDK。
这三类字段往往混在同一个token里,需要通过控制变量法把它们一个个剥离开。
2.3 用分组对比法确定字段边界
控制变量法是拆字段最靠谱的手段,比读几千行混淆代码高效得多。
操作流程是这样的:固定账号、固定设备、固定浏览器,在同一个页面URL下连续发起十次请求,记录每次的token。然后只改一个变量,比如换一个网络环境,再发十次;再换一个浏览器指纹,再发十次。把这三组样本按位置对齐,用表格列出来:
| 样本组 | 第1段 | 第2段(时间) | 第3段 | 第4段 |
|---|---|---|---|---|
| 组A-1 | a3f8c1e2 | 1700000000 | 9b2f4d17e8c3a1b6 | 5f7a2c9d |
| 组A-2 | a3f8c1e2 | 1700000001 | 9b2f4d17e8c3a1b6 | 5f7a2c9d |
| 组B-1 | b7c29d18 | 1700000010 | 6e1a90b3c4d5e7f8 | 5f7a2c9d |
| 组C-1 | f3a5e182 | 1700000020 | 8d2c4b6a1f9e7d3c | 5f7a2c9d |
看这张表,规律很清楚:第1段在换设备后变了,说明和指纹相关;第2段每次都按时间递增,是时间戳没跑;第3段跟着变,既受时间影响也受指纹影响,极大概率是参与签名的哈希;第4段雷打不动,可能是固定盐值或其他常量。这样一轮下来,字段边界和职责基本就清晰了。
3. 从已知到未知:签名算法的推导路径
3.1 参数排序与拼接:服务端是怎么验签的
签名是整个token的核心,也是"算法分析"这四个字最集中的体现。要推导签名算法,得先理解服务端的验签逻辑。大多数验签流程都遵循一个朴素原则:将要校验的参数按固定顺序拼接成字符串,串上盐值,做一次哈希,得到签名,再和客户端传上来的签名比对。
这个"固定顺序"通常是参数名的字典序,也就是按key排序后逐个拼接。我用Python写一个最典型的示意结构(仅演示通用逻辑,不是任何平台的真实代码):
import hashlib import time def generate_sign(params: dict, salt: str) -> str: # 按key排序并拼接 sorted_keys = sorted(params.keys()) raw = "&".join(f"{k}={params[k]}" for k in sorted_keys) raw = raw + salt return hashlib.md5(raw.encode()).hexdigest()理解这个结构之后,你在前端JS里找对应逻辑时就有了搜索方向:找排序函数、找字符串拼接模板、找hash调用点。如果平台用的是HMAC而不是普通哈希,结构会略有不同,通常是先构造好消息串,再以某个密钥做HMAC计算,但分析思路完全一致。
3.2 哈希算法的指纹识别
怎么判断对方用的到底是MD5还是SHA系列?三个特征就够了。
第一是输出长度。32位hex基本就是MD5或其变种;40位hex是SHA1;64位hex是SHA256。第二是字符集。纯hex输出说明经过了hexdigest处理;如果是Base64格式的定长字符串,比如28个字符的Base64,很有可能对应20字节的SHA1或HMAC-SHA1输出。第三是输入构造。如果你已经能控制输入的部分参数,可以通过稍微改动输入观察输出的雪崩效应强度,侧面推断哈希函数的类型。
这里有个容易踩的坑:很多平台不会直接用标准哈希,而是先对字符串做一次性编码,比如以Base64作为中间层,再做哈希;或者在哈希之前先压缩、反转、截断。所以我一直强调,哈希算法指纹判断只是一个方向,最终确认还得靠"自己拼串算一遍,看能不能对得上"。
3.3 盐值从哪里来:三种常见来源
签名里的盐值是整个算法还原里最容易卡住的一环。它一般有三个来源。
第一种是硬编码在前端JS里。这种情况下盐值就在某个JS文件的字符串常量里,格式化代码后直接搜索可疑字符串就能找到。特点是对所有用户都一样。第二种是服务端动态下发。页面初始化时会额外发一个接口,返回一个盐值或密钥,前端用它参与签名。这种设计比硬编码安全一个档次,因为盐值可以定期轮换。第三种是绑定设备指纹。盐值本身是设备指纹的某一个派生值,每台设备不同,这会让拿到代码的人也未必能算出有效的token。
判断盐值来源有点小技巧:在控制变量法那一步,如果第4段(固定不变的部分)在不同账号、不同设备上都一样,那它很可能是硬编码;如果换个会话就变,那基本是动态下发,你得把获取盐值的那个接口也纳入分析范围。
4. 还原token生成的四个实操阶段
4.1 定位调用链:从触发点到生成函数
前面做的都是静态分析,这一步开始进入动态调试。目标是找到token生成函数在JS代码里的确切位置。
我最常用的方法是调用栈回溯。在DevTools里找到发请求的XHR/fetch调用,在Network面板右键那条请求,选择"Break on fetch/XHR",然后刷新页面或重新触发请求。请求发出去之前会断下来,这时候看调用栈,一层层往下点,通常就能看到一个函数的名字比较可疑,比如generateToken、buildSign、getSecurityToken这类的。
如果断点没断到,就用关键词搜索。把JS源码格式化之后,在Sources面板里全局搜索token、sign、1002这些关键词,定位所有出现的位置,再逐个跳到上下文去看。压缩混淆过的代码可读性很差,但变量名再怎么混淆,字符串常量往往变不了,原始字段名还是会出现在字符串里。
4.2 断点与Hook:让算法自己开口说话
找到生成函数后,直接读代码有时候很难受,因为一屏放不下,逻辑跳来跳去。更高效的办法是Hook关键的原生函数,让算法在我们眼皮底下运行。
以JavaScript为例,Date.now()和Math.random()是token生成里最常被用到的两个函数。我在调试环境下会先挂上这样的Hook:
const _now = Date.now; Date.now = function () { const ts = _now(); console.log("[Date.now]", ts); return ts; }; const _random = Math.random; Math.random = function () { const r = _random(); console.log("[Math.random]", r); return r; };这样token每生成一次,时间戳和随机数就会原样打印在控制台里,配合你抓包拿到的token样本,能迅速确认哪一段对应时间戳、哪一段是随机数参与的结果。同理,你还可以HookJSON.stringify、crypto.subtle.digest、window.btoa,观察字符串拼接前的对象结构。
提示:Hook代码要放在页面加载之前才有效。用DevTools的
Overrides功能,或者在Sources面板里用Script snippet在页面初始化阶段注入,都是可行的办法。
4.3 最小复现环境与补环境
前端JS函数通常依赖浏览器环境里的window、document、navigator、localStorage等一系列对象。要想在Node.js里单独运行这个函数,就得做环境补齐。
我的建议是:先拉到Node里跑一下,看它报什么错,缺什么补什么。报window is not defined,就定义一个global.window;报navigator.userAgent不存在,就给navigator补上UA字段。不用追求100%还原浏览器,只需要补到函数能正常走完就行。这一步能大幅提升分析效率,因为你可以像调普通Node脚本一样反复执行token生成函数,批量制造样本。
一个典型的补环境脚本长这样:
global.window = global; global.navigator = { userAgent: "Mozilla/5.0 ...", language: "zh-CN", platform: "Win32" }; global.document = { cookie: "", referrer: "" }; const { generateToken } = require("./extracted_token.js");补环境的技术含量不高,但很耗耐心。我的经验是先从报错最密集的地方补起,比如canvas相关操作,通常需要mock一个HTMLCanvasElement.prototype.getContext返回固定数据,否则在不同环境下生成的指纹会不一致。
4.4 验证还原结果:时间偏移与参数篡改
算法还原到什么程度算成功?标准只有一个:你独立生成的token,能在授权范围内通过服务端的基本校验。
验证方法有三类。第一类是时间偏移测试。把系统时间往前调30秒、1分钟、5分钟,分别生成token并发送请求,观察哪个时间点开始报1002,从而推断token的有效窗口。第二类是参数篡改测试。在请求里改动一个非token字段,比如把业务参数从A改成B,看token是否失效。如果失效,说明该字段参与了签名,你的签名算法必须包含它;如果没失效,说明服务端没有把这个字段纳入验签范围。第三类是重放测试。把同一个token重复发送两次,如果第二次被1002拦截,说明服务端有防重放机制,token是一次性的。
这三类验证结果,直接决定了你还原出来的算法能不能作为后续工作的基础。
5. 分析结果如何验证与落地
5.1 用控制变量法给字段语义下结论
到这一步,你手上应该有了一个可运行的token生成函数。但"能跑"不等于"看懂了"。我建议再回头做一轮完整的控制变量实验,给每个字段的语义下最终结论。
操作方法是:固定其他所有条件,每次只改一个输入,观察生成的token里哪一段会变。整理成一张表,以后复盘时一目了然:
| 改变的变量 | 受影响字段 | 结论 |
|---|---|---|
| 修改URL参数 | 第3段 | 第3段参与签名,验签范围包含URL参数 |
| 修改系统时间 | 第2段、第3段 | 时间戳纳入签名,第2段是明文时间戳 |
| 修改设备指纹 | 第1段、第3段 | 设备指纹参与签名,第1段是指纹摘要 |
| 清空Cookie | 全部无变化 | 该token与Cookie无关 |
| 更换账号 | 第4段 | 第4段可能与账号ID绑定,需进一步确认 |
这张表做完,整个token的算法结构才算真正落到纸面上。
5.2 自动化回归:用数据说话
人工验证几个样本不够,我会用脚本批量生成几百个token,做三个统计指标。
第一个是正确率。把生成的token逐个发送到授权测试环境,统计通过率,低于95%说明还原不完整,还有隐藏参数没纳入签名。第二个是重复率。在相同输入下连续生成100次,如果token完全相同,说明没有随机因子,这可能被服务端的防重放机制直接拦截。第三个是时间戳分布。统计token里时间戳字段的解析结果,如果出现大量未来的时间戳或负数,说明字段边界判断错了。
这三个指标能帮你快速定位问题出在随机数处理、字段拼接还是签名范围上。自动化脚本本身不复杂,就是把生成函数循环跑,把结果写进CSV,用Python做统计。
5.3 合规边界与结果的使用方式
最后这部分我必须认真说。整个分析流程强调了很多次授权,这里再展开一下我个人的做法。
分析结果应该用于三类场景:一是授权范围内的安全评估,把发现的问题写成漏洞报告提交给平台安全团队;二是自己团队产品的风控自测,理解现有token机制在什么条件下会失效;三是纯粹的技术学习,把分析过程写成文章或教程。这三类场景都建立在"你对该平台拥有合法测试权限"的前提上。
反过来,用这类分析去批量抓取他人数据、绕过风控刷接口、干扰正常用户服务,既违反平台规则,也可能触及法律红线。我见过同行在这上面栽跟头,代价远超收益。
分析完一个token之后,我通常还会顺手做一件事:把整个分析过程整理成一份带时间线的复盘文档,记录每个阶段的样本、假设和验证结果。下次再遇到其他平台的token,直接套这套流程,效率能提升一大截。
最后再分享一点个人体会。这类风控token的分析,真正难的地方从来不是某个哈希算法本身,而是你能不能沉住气把样本收集够、把字段边界切分对。我见过太多人在猜算法上花掉一周,回头发现不过是设备指纹不一致导致的全盘皆错。先花半小时把样本和链路摸清楚,往往比任何奇技淫巧都管用。