背景与问题
很多同学第一次做 Web 逆向时,最容易卡住的不是“接口在哪”,而是“参数怎么来的”。
表面上看,浏览器发出的请求只是一个普通 POST,但你一打开抓包工具就会发现:
- 请求体里出现了看不懂的
sign、token、ciphertext - 同一个接口,参数每次刷新都不一样
- 直接复制请求重放,服务端返回“签名错误”或“非法请求”
- 页面代码被打包、混淆,搜索关键字几乎找不到入口
这类问题的本质,不是“接口不能调”,而是前端在发请求前,对原始参数做了加工。常见加工形式包括:
- 时间戳拼接
- 字段排序
- MD5/SHA/HMAC 摘要
- AES/RSA 加密
- Base64/Hex 编码
- 动态 token 注入
- 指纹、环境校验参与签名
这篇文章我会从浏览器开发者工具出发,带你完整走一遍定位链路:
- 先在 Network 里确认哪个请求有加密参数
- 再在 Sources 里断到“参数生成前后”
- 识别是编码、摘要还是对称/非对称加密
- 最后用可运行代码把参数还原出来
重点不是“背某个站的答案”,而是掌握一套可迁移的方法论。
前置知识与环境准备
建议你至少具备这些基础:
- 会使用 Chrome/Edge 开发者工具
- 了解 HTTP 请求结构
- 会读一些 JavaScript
- 知道常见加密/编码概念:MD5、SHA、AES、RSA、Base64
建议准备环境:
- 浏览器:Chrome 最新版
- Node.js:18+
- Python:3.10+(可选,用于辅助验证)
- 编辑器:VS Code
- 一个练手页面或你自己的测试站点
说明:本文讨论的是合法授权范围内的技术分析,例如自研系统调试、接口联调、安全测试、学习研究。不要将方法用于未授权目标。
核心原理
前端“加密参数”的生成,通常可以抽象成一个固定流水线:
flowchart LR
A[用户输入/业务参数] --> B[字段整理]
B --> C[附加动态值<br/>timestamp nonce token]
C --> D[排序/拼接/序列化]
D --> E[摘要或加密]
E --> F[编码转换]
F --> G[发送请求]
真正实战时,不要一上来就盯着“加密算法”。我更建议按下面顺序判断:
-
先看参数形态
- 32 位十六进制:可能是 MD5
- 40 位:可能是 SHA1
- 很长的 Base64:可能是 AES/RSA 输出
- JSON 包一层后再整体编码:可能是自定义封装
-
再看是否有动态因子
- 时间戳是否参与
- 随机数是否参与
- Cookie / localStorage / sessionStorage 中的 token 是否参与
- 浏览器指纹是否参与
-
最后才看具体实现
- 是库函数调用,还是项目自定义封装
- 是纯前端生成,还是先请求一个 seed/token 再签名
一个常见参数生成链路
sequenceDiagram
participant U as 用户操作
participant P as 页面脚本
participant S as 签名函数
participant N as Network请求
U->>P: 点击查询
P->>P: 收集 keyword/page
P->>P: 读取 timestamp/token
P->>S: buildSign(payload, token, ts)
S-->>P: 返回 sign/data
P->>N: 发起 POST 请求
N-->>P: 服务端响应
方法总览:如何用 DevTools 定位加密参数
我通常会按下面这条路径走,效率比较高。
第一步:在 Network 确认目标请求
打开开发者工具,切到 Network:
- 勾选
Preserve log - 勾选
Disable cache - 触发一次页面操作
- 找到对应接口,重点看:
- Request URL
- Method
- Query String Parameters
- Payload / Form Data
- Request Headers
你需要先回答两个问题:
1)哪些字段“像加密结果”?
例如请求体长这样:
{
"data": "U2FsdGVkX1+8L2l2Wk...",
"sign": "5f4dcc3b5aa765d61d8327deb882cf99",
"t": 1721900000000
}
这里通常可以初步判断:
t是时间戳sign像 MD5data像 Base64 编码后的密文
2)原始业务参数还在不在?
如果你输入的关键字是 "laptop",而请求里完全没有 "laptop" 的明文,那说明它大概率被封进了 data 里。
第二步:从发起请求的位置反查调用栈
在 Network 中点开请求,看 Initiator 或右键:
Open in Sources panelBreak on request- 查看调用栈
Call Stack
如果站点没严重混淆,这一步往往能直接把你带到:
axios.interceptors.request.use(...)fetch(...)包装函数encrypt(data)、sign(params)之类的工具函数
这一步的目标不是一次找到算法,而是找到发请求前最后一跳。
第三步:在 Sources 里打断点,观察“加密前”和“加密后”
这一招非常关键,也是很多人容易忽略的点:
不要试图直接读懂整个混淆文件,先断住再看变量。
推荐的断点位置:
- 请求发送函数前
JSON.stringify(...)附近CryptoJS.*、encrypt、sign、digest附近setRequestHeader、fetch、XMLHttpRequest.send附近
你要重点观察三类变量:
- 原始参数
- 中间态字符串
- 最终请求参数
例如:
payload = { keyword: "laptop", page: 1 }
baseStr = "keyword=laptop&page=1&t=1721900000000"
sign = md5(baseStr + secret)
只要能看到这三者之间的关系,还原基本就成功一半了。
第四步:识别算法类型
这里给一个实战中非常好用的判断表。
| 现象 | 高概率类型 | 排查方式 |
|---|---|---|
| 32位小写十六进制 | MD5 | 搜 md5 / hex_md5 / CryptoJS.MD5 |
| 固定长度更长哈希 | SHA 系列 | 搜 SHA1 / SHA256 / digest |
| 明显 Base64 字符串 | Base64/AES/RSA 输出 | 先尝试 Base64 解码 |
| 很长十六进制密文 | AES/自定义字节流编码 | 看是否有 key/iv |
| 每次都变但业务数据一致 | 加了时间戳/随机数 | 找 Date.now() / Math.random() |
| 请求头里带 sign | 摘要验签 | 看拦截器或 header 注入逻辑 |
实战演示:从页面请求还原 sign 参数
下面我用一个简化但贴近真实项目的例子演示。假设页面发送的请求如下:
{
"keyword": "laptop",
"page": 1,
"t": 1721900000000,
"sign": "e2d8f3c6b7d5a1c3..."
}
经过 DevTools 断点,我们观察到前端逻辑是:
- 收集业务参数:
keyword、page - 加上时间戳
t - 按 key 排序后拼接为查询字符串
- 在末尾追加固定密钥
secret - 计算 MD5,生成
sign
也就是说,签名规则是:
sign = MD5(sort(params).join("&") + secret)
页面中的原始逻辑(示意)
function buildSign(params, secret) {
const keys = Object.keys(params).sort();
const str = keys.map(k => `${k}=${params[k]}`).join("&");
return md5(str + secret);
}
用 Node.js 还原签名
先安装依赖:
npm init -y
npm install crypto-js
编写 sign.js:
const CryptoJS = require("crypto-js");
function buildSign(params, secret) {
const sortedKeys = Object.keys(params).sort();
const base = sortedKeys.map(key => `${key}=${params[key]}`).join("&");
const sign = CryptoJS.MD5(base + secret).toString();
return {
base,
sign
};
}
const params = {
keyword: "laptop",
page: 1,
t: 1721900000000
};
const secret = "my_private_key_123";
const result = buildSign(params, secret);
console.log("base string:", result.base);
console.log("sign:", result.sign);
运行:
node sign.js
如果你的输出和浏览器请求里的 sign 一致,就说明你已经完成了签名还原。
实战演示:还原被 AES 加密的 data 参数
真实站点中,除了 sign,还有一类常见情况是把整个业务 JSON 加密后塞进一个 data 字段。
假设我们在断点中看到如下逻辑:
- 原始对象:
{ keyword: "laptop", page: 1 } JSON.stringify后得到明文- 使用 AES-CBC 加密
- key 和 iv 写在前端代码里
- 最后转为 Base64
那么还原时只要把这条链路复刻出来即可。
前端逻辑示意
function encryptData(obj) {
const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");
const text = JSON.stringify(obj);
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
Node.js 可运行还原代码
const CryptoJS = require("crypto-js");
function encryptData(obj) {
const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");
const text = JSON.stringify(obj);
const encrypted = CryptoJS.AES.encrypt(text, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return encrypted.toString();
}
function decryptData(cipherText) {
const key = CryptoJS.enc.Utf8.parse("1234567890abcdef");
const iv = CryptoJS.enc.Utf8.parse("abcdef1234567890");
const decrypted = CryptoJS.AES.decrypt(cipherText, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
});
return decrypted.toString(CryptoJS.enc.Utf8);
}
const payload = {
keyword: "laptop",
page: 1
};
const cipherText = encryptData(payload);
const plainText = decryptData(cipherText);
console.log("cipherText:", cipherText);
console.log("plainText:", plainText);
参数定位到还原的完整路径
flowchart TD
A[Network 发现 data/sign 异常字段] --> B[查看 Initiator]
B --> C[Sources 断点到请求发送前]
C --> D[观察原始对象与中间变量]
D --> E[识别算法与 key/iv/secret]
E --> F[在 Node.js 复刻逻辑]
F --> G[对比浏览器请求结果]
G --> H[完成参数还原]
逐步验证清单
实战里我很少一口气把整条链路写完,而是会按下面清单逐步验证。这样出错时更容易定位。
验证 1:业务参数是否一致
先确认你自己代码里的原始参数,与浏览器断点看到的一致:
{
keyword: "laptop",
page: 1
}
不要忽略:
- 空字符串和
null - 数字和字符串类型差异
- 布尔值序列化差异
- 字段是否缺省
验证 2:动态参数是否一致
例如:
- 时间戳
t - nonce
- session token
- cookie 中某个字段
这一步我踩过很多坑。你明明算法写对了,但时间戳取值不同,最终签名就完全不一样。
验证 3:拼接规则是否一致
重点检查:
- 是否按 key 排序
- 是否过滤空值
- 是否 URL 编码
- 是否用
&拼接 - 是否末尾多拼了
secret - 是否有固定前缀/后缀
验证 4:编码格式是否一致
例如:
- UTF-8 还是 UTF-16
- Base64 还是 Hex
- AES key 是否先
Utf8.parse - RSA 明文是否分段
很多“差一个字符”的问题,本质上就是编码不一致。
常见坑与排查
这一节很重要。很多同学不是不会写代码,而是卡在细节里。
1. 只看 Network,不看调用栈
很多人抓到请求后,开始手动搜 sign。如果代码混淆严重,这样效率很低。
更好的办法是直接从 Initiator / Call Stack 倒推。
建议: 永远先找“谁发了这个请求”,再找“这个参数怎么算的”。
2. 把“编码”误判成“加密”
比如一个字段看起来很乱:
eyJrZXl3b3JkIjoibGFwdG9wIiwicGFnZSI6MX0=
这其实很可能只是 Base64,不是 AES。
先尝试解码,再判断是不是密文。
可以用浏览器控制台快速试:
atob("eyJrZXl3b3JkIjoibGFwdG9wIiwicGFnZSI6MX0=")
3. 忽略拦截器
很多项目不会在业务函数里直接写签名,而是统一在:
- Axios request interceptor
- fetch wrapper
- 公共请求模块
里追加 header、token、sign。
现象: 你在页面代码里看不到 sign 的生成逻辑,但 Network 中它确实存在。
排查方向: 全局搜索 interceptors.request.use、setRequestHeader、fetch =、XMLHttpRequest.prototype.send。
4. 混淆后函数名不可读
比如你看到的是:
a.b(c(d(e)))
别硬啃全文。我的经验是:
- 在关键调用处打断点
- 看实参和返回值
- 给变量改“心里名字”
- 一层层缩小范围
真正有价值的是数据流,不是函数名。
5. 时间戳单位搞错
常见有两种:
- 秒级:
Math.floor(Date.now() / 1000) - 毫秒级:
Date.now()
差 1000 倍,签名必错。
6. AES 模式、填充方式、iv 任一不一致
AES 不是只知道 key 就行,还要确认:
- ECB / CBC / CTR
- Pkcs7 / ZeroPadding
- iv 是否固定
- 输出是否 Base64
有一次我就踩在这里:key 对了,但漏了 iv,结果解出来全是乱码。
7. 参数顺序不一致
有些签名逻辑对字段顺序极其敏感:
a=1&b=2
和
b=2&a=1
哈希结果完全不同。
安全/性能最佳实践
这一部分既适合做逆向分析时提高效率,也适合前端/后端做防护时理解边界。
1. 不要把“前端加密”当成真正安全边界
只要算法、密钥、调用路径都在前端执行,理论上就可以被观察、复刻。
所以:
- 前端加密更像“增加门槛”
- 真正的权限控制、风控校验必须在服务端完成
- 不要把核心密钥硬编码在前端当作最终防线
2. 优先分析数据流,不要迷信反混淆
大文件、压缩代码、webpack 打包很常见。
如果你一上来就想“全部反编译看懂”,很容易陷进去。
更高效的方式:
- 从请求出发
- 沿调用栈回溯
- 只抓关键中间变量
- 复刻最小闭环逻辑
3. 复刻代码时尽量保持“同构”
比如浏览器里用的是 CryptoJS,那你在 Node.js 里也优先用 crypto-js。
这样可以减少:
- 编码差异
- 输出格式差异
- mode / padding 差异
等拿到稳定结果后,再考虑换成原生 crypto 做性能优化。
4. 建议做“最小验证样本”
我一般会先固定一组参数:
{
keyword: "laptop",
page: 1,
t: 1721900000000
}
只要这组样本能稳定复现,就说明链路正确。
之后再扩展到动态请求。
5. 关注浏览器存储与运行时环境
有些参数不直接写在代码里,而是来自:
localStoragesessionStoragedocument.cookiewindow.__INITIAL_STATE__- 某个接口预先下发的 seed
所以不要只看函数体,还要看它依赖的上下文。
一个更完整的实战模板
如果你已经定位出规则,可以用下面这个模板快速拼装自己的还原脚本。
const CryptoJS = require("crypto-js");
function normalizeParams(params) {
const result = {};
for (const key of Object.keys(params)) {
const value = params[key];
if (value !== undefined && value !== null && value !== "") {
result[key] = value;
}
}
return result;
}
function buildBaseString(params) {
const normalized = normalizeParams(params);
return Object.keys(normalized)
.sort()
.map(key => `${key}=${normalized[key]}`)
.join("&");
}
function buildSign(params, secret) {
const base = buildBaseString(params);
return CryptoJS.MD5(base + secret).toString();
}
function encryptPayload(obj, keyText, ivText) {
const key = CryptoJS.enc.Utf8.parse(keyText);
const iv = CryptoJS.enc.Utf8.parse(ivText);
const text = JSON.stringify(obj);
return CryptoJS.AES.encrypt(text, key, {
iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}).toString();
}
function buildRequestParams(input) {
const t = input.t || Date.now();
const biz = {
keyword: input.keyword,
page: input.page
};
const data = encryptPayload(biz, "1234567890abcdef", "abcdef1234567890");
const signParams = {
data,
t
};
const sign = buildSign(signParams, "my_private_key_123");
return {
data,
t,
sign
};
}
const req = buildRequestParams({
keyword: "laptop",
page: 1,
t: 1721900000000
});
console.log(req);
这个模板覆盖了两种常见场景:
- 业务参数整体加密成
data - 再对
data + t生成签名sign
很多站点虽然细节不同,但结构上就是这个思路。
排查思路图:请求失败时怎么快速定位
stateDiagram-v2
[*] --> 检查原始参数
检查原始参数 --> 检查动态参数: 原始参数正确
检查原始参数 --> 修正业务字段: 原始参数错误
检查动态参数 --> 检查拼接规则: 动态参数正确
检查动态参数 --> 修正时间戳nonce: 动态参数错误
检查拼接规则 --> 检查编码加密: 拼接正确
检查拼接规则 --> 修正排序过滤规则: 拼接错误
检查编码加密 --> 请求成功: 算法一致
检查编码加密 --> 修正keyivmode: 算法不一致
总结
前端加密请求参数的还原,核心不是“会某一种算法”,而是掌握一条稳定路径:
- 在 Network 找到异常参数
- 从 Initiator / Call Stack 反查请求入口
- 在 Sources 关键位置打断点
- 观察原始值、中间值、最终值
- 识别排序、拼接、摘要、加密、编码规则
- 用 Node.js 复刻最小闭环
如果你让我把整篇文章压缩成一句实战建议,那就是:
不要先研究混淆代码,要先抓住数据是怎么流到请求里的。
最后给几个可执行建议:
- 第一次还原时,优先选一个参数简单、触发链路短的接口
- 每次只验证一个环节:原始参数、动态值、拼接串、sign、data
- 复刻代码尽量与前端使用相同库,先求“结果一致”,再谈优化
- 如果请求仍失败,优先怀疑动态参数、排序规则、编码格式,而不是马上怀疑算法错了
边界条件也要明确:
- 如果签名依赖硬件指纹、浏览器环境、WASM、服务端下发一次性令牌,难度会显著上升
- 如果密钥完全不在前端,而是服务端参与协商,那么前端复刻空间会小很多
- 单纯“前端加密”不能替代后端鉴权,但对调试、联调、分析链路非常有帮助
只要你能在浏览器里把“加密前”和“加密后”都看见,剩下的工作通常只是耐心复刻。这个思路,我自己在很多项目里反复验证过,基本都能落地。