跳转到内容
123xiao | 无名键客

《Web逆向实战:中级开发者如何定位并复现前端签名参数生成逻辑》

字数: 0 阅读时长: 1 分钟

Web逆向实战:中级开发者如何定位并复现前端签名参数生成逻辑

很多中级开发者第一次做 Web 逆向时,最容易卡住的点不是“不会写代码”,而是找不到真正的签名生成位置。页面里一堆打包压缩后的 JavaScript,网络请求又带着 signtokennoncetts 之类参数,看起来每个都像关键点,最后很容易陷入“全都像,又全都不是”的状态。

这篇文章我会按我自己实战里最常用的一条路线,带你完整走一遍:如何定位前端签名参数的生成逻辑、如何验证自己的判断、以及如何在本地稳定复现。重点不是某个具体站点,而是一套可迁移的方法。

说明:本文内容用于前端调试、接口联调、安全研究与教学分析。请在合法合规、获得授权的前提下使用。


背景与问题

典型场景一般长这样:

  • 页面请求某个接口时会带上 sign
  • 你直接复制请求去重放,返回“签名错误”或“非法请求”
  • 刷新页面后同样的业务参数,sign 却每次都不同
  • 前端代码经过 webpack 打包、混淆甚至加了简单反调试

对于中级开发者来说,真正的难点通常有三个:

  1. 不知道从哪里下手
  2. 定位到疑似代码后,无法确认是不是最终签名点
  3. 即使找到了算法,也复现不出与浏览器一致的结果

所以本文的目标很明确:

  • 学会从请求出发,倒推签名生成位置
  • 理解前端签名常见组成方式
  • 用可运行代码复现一个典型签名流程
  • 知道排查失败时优先看什么

前置知识

开始之前,建议你至少熟悉这些内容:

  • Chrome DevTools 基本使用
  • JavaScript 基础语法
  • 浏览器 Network / Sources / Debugger 面板
  • 常见摘要算法概念:MD5 / SHA256 / HMAC
  • Node.js 基本运行方式

如果你已经做过接口联调、看过 webpack 打包产物,那阅读会很顺。


环境准备

本文建议准备以下环境:

  • Chrome 浏览器
  • Node.js 16+
  • 一个可编辑的本地目录
  • DevTools 打开 Source map(如果目标站没关的话会轻松很多)

安装 Node 后可直接测试:

node -v
npm -v

如果你需要在 Node 中计算哈希,可直接使用内置 crypto,不必额外安装依赖。


核心原理

前端签名本质上通常不是“神秘黑魔法”,而是下面几类材料的组合:

  • 业务参数,如 page=1&keyword=test
  • 时间戳,如 ts=1710000000
  • 随机串,如 nonce=abc123
  • 固定盐值,如某个常量字符串
  • 用户态信息,如 tokenuid
  • 某种序列化规则,如按 key 排序后拼接
  • 最后套一层摘要算法,如 md5(...)sha256(...)

很多站点会把逻辑写成:

sign = hash(sort(params) + secret + ts + nonce)

也可能是:

sign = hash(hash(body) + token + path + ts)

或者:

sign = HMAC(secret, method + url + canonicalQuery + bodyDigest + ts)

一个关键认知

真正难的不是算法本身,而是“输入材料”和“拼接顺序”。

我见过很多同学已经猜到了是 md5,但一直复现不出来,最后发现只是因为:

  • 参数排序不一致
  • 数字和字符串类型不一致
  • URL 编码时机不同
  • 时间戳单位是秒不是毫秒
  • 某些空值字段前端偷偷过滤掉了

所以做逆向时,要把关注点放在:

  1. 参与签名的字段有哪些
  2. 字段进入签名前是否被转换
  3. 字段顺序如何确定
  4. 最终调用的 hash/hmac 是什么

定位思路总览

先给你一张总图,建立整体路线感。

flowchart TD
    A[抓到目标请求] --> B[观察 Query/Header/Body 中可疑参数]
    B --> C[全局搜索 sign/token/nonce/ts]
    C --> D[在 XHR/fetch 发送前断点]
    D --> E[回溯调用栈]
    E --> F[找到参数拼接函数]
    F --> G[确认摘要算法与输入顺序]
    G --> H[本地复现并对比]
    H --> I[逐项排查差异]

这张图里最重要的两步是:

  • 发送前断点
  • 回溯调用栈

很多时候你不需要一开始就硬啃整个混淆包,直接在请求即将发出时拦住它,往上追调用链,效率会高很多。


背景与问题:如何判断哪个参数是真签名

一个请求里可能有好几个“看起来像签名”的参数,例如:

GET /api/search?q=phone&page=1&ts=1710000000&nonce=8f3a2c&sign=5f4dcc3b5aa765d61d8327deb882cf99

这里通常可以这样判断:

  • ts:大概率时间戳
  • nonce:大概率随机串
  • sign:大概率最终校验值
  • 也有可能 token 并不只是登录态,而是参与签名的材料

经验上,优先关注“变化但不可读”的字段
比如 sign 看起来像 32 位十六进制串,很可能就是 MD5;64 位可能是 SHA256;带 =/+ 的可能是 Base64 编码结果。


核心原理:从请求发送点往回追

前端请求常见由这几类 API 发出:

  • fetch
  • XMLHttpRequest
  • axios
  • jQuery.ajax
  • 某些站点自封装请求层

最稳的一招:在请求发送处下断点

如果页面使用 XHR,可以在 DevTools 中:

  • 打开 Sources
  • 找到右侧 XHR/fetch Breakpoints
  • 添加目标接口关键字,比如 /api/search
  • 触发页面请求

浏览器会在请求发出前暂停。此时你要做的是:

  1. 看当前作用域变量
  2. 看调用栈
  3. 往上逐层点,找到构造参数对象的地方

下面用时序图表示这个过程:

sequenceDiagram
    participant U as 用户操作
    participant P as 页面业务代码
    participant S as 签名函数
    participant R as 请求封装层
    participant N as 网络层

    U->>P: 点击搜索
    P->>S: 传入业务参数
    S->>S: 排序/拼接/摘要
    S-->>P: 返回 sign
    P->>R: 组装 headers/query/body
    R->>N: fetch/XHR 发出请求

你真正要抓住的是 P -> SS -> S 这两段。


逐步实战:定位签名生成逻辑

下面用一个典型示例来演示完整过程。假设页面请求参数如下:

GET /api/search?q=phone&page=1&ts=1710000000&nonce=8f3a2c&sign=xxxxxxxx

第一步:从 Network 看请求差异

先手动发两次相同请求,观察:

  • qpage 不变
  • ts 变化
  • nonce 变化
  • sign 变化

初步判断:

  • sign 很可能依赖 tsnonce
  • 也可能依赖 qpage
  • 可能还隐式依赖 token 或 cookie

建议做个小表格:

字段是否变化猜测作用
q业务参数
page业务参数
ts时间戳
nonce随机数
sign签名结果

第二步:全局搜索关键词

Sources 里优先搜索:

  • sign
  • nonce
  • ts
  • 请求路径关键字,如 /api/search
  • md5sha1sha256
  • CryptoJS
  • digest
  • Hmac

如果搜 sign 命中太多,可以改搜请求路径,通常能更快定位到请求封装处。

第三步:在 fetch/XHR 断住

一旦断住,重点看:

  • 当前发送的 URL 是在哪里被拼出来的
  • sign 是提前挂到 params 上的,还是在请求拦截器里统一追加的
  • axios 场景下常出现在:
    • request interceptor
    • transformRequest
    • 单独的 sign() 工具函数

第四步:回溯调用栈

这一阶段我常做的事是:

  • 从当前栈帧往上点
  • 找到第一次出现“可读变量名”的地方
  • 观察这个函数的输入和输出

例如你可能看到类似逻辑:

const params = {
  q: keyword,
  page,
  ts: Date.now(),
  nonce: genNonce()
}
params.sign = makeSign(params)

这时候基本已经进入核心区域了。


实战代码(可运行)

下面我构造一个“典型前端签名”的示例,模拟网页中的生成逻辑,然后在 Node.js 中复现。你可以直接运行,帮助建立“定位后该怎么验证”的感觉。

示例签名规则

假设站点前端的规则是:

  1. 取业务参数对象
  2. 过滤掉值为 undefined/null/'' 的字段
  3. 按 key 字典序排序
  4. 拼成 k=v&k=v...
  5. 末尾拼接固定盐值 &secret=demo_secret
  6. 对完整字符串做 MD5
  7. 得到最终 sign

浏览器中的前端代码示意

function normalizeParams(params) {
  const clean = {};
  Object.keys(params).forEach((key) => {
    const value = params[key];
    if (value !== undefined && value !== null && value !== '') {
      clean[key] = String(value);
    }
  });
  return clean;
}

function buildSignString(params) {
  const clean = normalizeParams(params);
  const sortedKeys = Object.keys(clean).sort();
  const pairs = sortedKeys.map((key) => `${key}=${clean[key]}`);
  return `${pairs.join('&')}&secret=demo_secret`;
}

假设最终签名调用的是一个 MD5 实现:

function makeSign(params) {
  const raw = buildSignString(params);
  return md5(raw);
}

Node.js 复现版本

新建 sign-demo.js

const crypto = require('crypto');

function normalizeParams(params) {
  const clean = {};
  Object.keys(params).forEach((key) => {
    const value = params[key];
    if (value !== undefined && value !== null && value !== '') {
      clean[key] = String(value);
    }
  });
  return clean;
}

function buildSignString(params) {
  const clean = normalizeParams(params);
  const sortedKeys = Object.keys(clean).sort();
  const pairs = sortedKeys.map((key) => `${key}=${clean[key]}`);
  return `${pairs.join('&')}&secret=demo_secret`;
}

function md5(text) {
  return crypto.createHash('md5').update(text, 'utf8').digest('hex');
}

function makeSign(params) {
  const raw = buildSignString(params);
  return md5(raw);
}

function main() {
  const params = {
    q: 'phone',
    page: 1,
    ts: 1710000000,
    nonce: '8f3a2c',
  };

  const signString = buildSignString(params);
  const sign = makeSign(params);

  console.log('signString =', signString);
  console.log('sign =', sign);
}

main();

运行:

node sign-demo.js

为什么这段代码重要

因为它对应了逆向中的两个关键验证动作:

  1. 还原签名前原始字符串
  2. 确认摘要结果是否与浏览器一致

很多时候你以为“算法没问题”,其实只要把 signString 打印出来和浏览器现场值对比,就能马上发现差异。


如何在浏览器里验证你找到的函数

定位到疑似函数后,不要急着抄代码。先做现场验证。

方法一:在函数入口打断点

如果你找到 makeSign(params),就在函数第一行断住,观察:

  • 入参 params 是什么
  • 有没有额外上下文参与,比如 token
  • 有没有对值做字符串化

方法二:在函数返回前打印结果

你可以在 Console 中临时调用:

copy(buildSignString(params))

或者:

console.log(buildSignString(params))
console.log(makeSign(params))

如果页面作用域里访问不到该函数,可以在断点暂停时用当前作用域直接执行表达式。

方法三:hook 摘要函数

如果全局能接触到 CryptoJS 或某个 md5 函数,可以临时 hook:

const oldMd5 = window.md5;
window.md5 = function(input) {
  console.log('[md5 input]', input);
  const result = oldMd5(input);
  console.log('[md5 output]', result);
  return result;
};

如果页面没暴露全局 md5,而是模块作用域内部函数,那就需要在具体调用点断住看参数。


更复杂一点:请求拦截器中的统一签名

很多现代前端项目并不会在业务页面里显式写 params.sign = ...,而是放在统一请求封装层。

比如 axios 场景:

service.interceptors.request.use((config) => {
  const ts = Date.now();
  const nonce = randomString(6);

  const params = {
    ...(config.params || {}),
    ts,
    nonce
  };

  const sign = makeSign(params);

  config.params = {
    ...params,
    sign
  };

  return config;
});

这种情况下,你在业务代码搜索 sign 可能搜不到,或者只有少量命中。
这时搜请求实例名、搜 interceptor、搜 config.params 会更有效。

下面这张类图能帮助你把“业务层、签名层、请求层”的关系看清楚:

classDiagram
    class PageLogic {
      +search(keyword, page)
    }

    class RequestClient {
      +request(config)
      +useRequestInterceptor(fn)
    }

    class SignService {
      +normalizeParams(params)
      +buildSignString(params)
      +makeSign(params)
    }

    PageLogic --> RequestClient : 调用请求
    RequestClient --> SignService : 请求前生成签名

逐步验证清单

做复现时,我建议你按下面顺序验证,不要一上来就只盯着最终 sign

1. 参数全集是否一致

检查:

  • query 参数
  • body 参数
  • header 中是否有参与签名的字段
  • cookie / localStorage / sessionStorage 中是否有 token 被读取

2. 参数值类型是否一致

比如:

  • 1'1'
  • true'true'
  • null 是否被过滤
  • 空字符串是否被保留

3. 排序规则是否一致

常见情况:

  • ASCII 字典序
  • 大小写敏感
  • 只排序业务参数,不排序系统参数
  • body JSON 内部字段也可能要排序

4. 编码方式是否一致

比如:

  • encodeURIComponent 是在拼接前还是拼接后
  • 空格是 %20 还是 +
  • 中文是否先 UTF-8 编码

5. 时间戳单位是否一致

经常踩坑:

  • 前端用毫秒:Date.now()
  • 服务端要求秒:Math.floor(Date.now() / 1000)

6. 摘要函数是否一致

常见误判:

  • 以为是 MD5,实际是 HMAC-MD5
  • 以为是 SHA256,实际是 sha256(secret + text)
  • 结果输出是 hex 还是 base64

常见坑与排查

这一节很关键,我把实战里最常见的问题集中说一下。

坑一:只看最终请求,不看发送前变量

有些参数在 Network 中已经是处理后的结果,比如:

  • body 被序列化了
  • header 被统一注入了
  • URL 被重新编码了

所以只看最终报文是不够的
一定要在“发送前一刻”断住。

坑二:忽略了隐藏输入

签名可能依赖这些你一开始没想到的东西:

  • navigator.userAgent
  • 当前 URL path
  • 环境标识,如 appId
  • 登录 token
  • 某个固定版本号
  • body 的摘要值

排查办法:
在疑似签名函数中观察闭包变量、模块常量、调用方上下文。

坑三:参数顺序搞错

这是最常见的失败原因之一。

例如你以为前端是按对象插入顺序拼接,实际是:

Object.keys(params).sort()

或者你以为连 sign 自己也参与签名,结果前端签名时明确排除了它。

坑四:误把“加密”当“签名”

很多前端字段看起来像一串乱码,但实际上可能只是:

  • Base64 编码
  • URL 编码
  • JSON 字符串压缩
  • AES 加密后的密文

而真正用于校验的 sign 是另一个字段。
不要把“看不懂”直接等同于“签名”。

坑五:浏览器环境依赖导致 Node 复现失败

前端函数可能用了:

  • window
  • document
  • btoa/atob
  • TextEncoder
  • crypto.subtle

如果你直接把函数拷到 Node 里运行,很可能报错。
这时建议:

  • 先把纯算法部分抽离
  • 需要时做最小 polyfill
  • 或者直接在浏览器控制台先复现一版

坑六:被反调试干扰

部分站点会做这些动作:

  • 无限 debugger
  • 控制台检测
  • Sources 格式化后仍极难读
  • 关键逻辑动态拼接执行

我的建议是:

  • 先用 XHR/fetch 断点抓调用链
  • 少从“完整读懂全站代码”入手
  • 必要时对关键函数做局部 hook,而不是硬解所有混淆

一个完整的排查示例

假设你本地复现出来的 sign 不对,可以按下面顺序排查:

flowchart TD
    A[sign 不一致] --> B{原始拼接串一致吗?}
    B -- 否 --> C[排查参数过滤/排序/编码/类型]
    B -- 是 --> D{摘要算法一致吗?}
    D -- 否 --> E[确认 md5/sha256/hmac/输出编码]
    D -- 是 --> F{是否有隐藏输入?}
    F -- 是 --> G[检查 token/path/header/body 摘要]
    F -- 否 --> H[检查时间戳单位与随机串生成]

这套顺序能帮你避免“全都怀疑”的混乱状态。


安全/性能最佳实践

虽然本文讲的是“如何定位并复现”,但从工程和安全角度,也有几个很值得记住的点。

1. 不要高估前端签名的安全性

前端代码运行在用户浏览器里,逻辑最终可被观察、调试、hook。
所以前端签名更适合:

  • 提高滥用门槛
  • 限制脚本小子直接重放
  • 做请求完整性校验的补充

不适合作为核心安全边界
真正关键的风控、权限、数据校验,仍应放在服务端。

2. 签名材料尽量最小化

如果你是业务开发者,设计签名时建议:

  • 明确参与字段
  • 固定排序规则
  • 保持序列化稳定
  • 避免把不必要的大对象都纳入签名

这样不仅减少性能开销,也方便联调排障。

3. 避免在高频路径重复做重计算

例如列表滚动时每个请求都做大型 JSON 深排序再 SHA256,大概率会影响前端性能。
实践中可考虑:

  • 只签关键字段
  • 预计算固定片段
  • 在请求封装层统一处理,避免重复实现

4. 调试时注意脱敏

你在 Console、日志、抓包工具中打印签名输入时,可能会包含:

  • token
  • uid
  • secret
  • 业务敏感参数

建议:

  • 本地临时调试后及时清除
  • 不要把敏感日志直接提交仓库
  • 团队共享样例时做脱敏处理

5. 复现代码要和线上隔离

如果你是为了联调、测试、研究而写复现脚本,建议:

  • 放在单独目录
  • 不混入业务生产代码
  • 用环境变量管理敏感配置
  • 记录脚本适用版本和时间点

因为前端签名逻辑很可能会变,混在正式项目里后续会很难维护。


给中级开发者的实用建议

如果你已经不是新手,但还没形成稳定的方法论,我建议你把下面几条变成习惯:

先抓“发送前现场”,再读源码

这是效率最高的路线。
不要一上来就对着压缩代码海量搜索。

先还原“原始签名串”,再算 hash

只比最终 sign 很难定位问题,
先把进入 hash 前的字符串搞对,成功率会高很多。

优先怀疑“排序、过滤、编码、时间戳”

这四类问题比“算法本身猜错”常见得多。

遇到混淆,不求全懂,只求打穿链路

你不需要还原整个工程,只需要知道:

  • 请求在哪发
  • 参数在哪补
  • sign 在哪算
  • 输入是什么
  • 输出是什么

做到这一步,绝大多数联调和分析任务已经够用了。


总结

前端签名逆向,真正可复制的方法不是“背某种算法”,而是掌握一条稳定路径:

  1. 从 Network 找到目标请求
  2. 用 XHR/fetch 断点卡在发送前
  3. 回溯调用栈,定位参数补充与签名函数
  4. 搞清楚参与字段、过滤规则、排序方式、编码时机
  5. 在本地先复现原始签名串,再复现最终摘要
  6. 若失败,优先排查类型、顺序、时间戳、隐藏输入

如果你把这套流程练熟,面对大多数中等复杂度的前端签名场景,都会比“全局盲猜”快很多。

最后给一个边界条件判断:

  • 如果站点只是普通打包混淆、统一请求封装,这套方法通常足够
  • 如果站点叠加了强反调试、WASM、动态环境校验、设备指纹深度绑定,那就需要更进一步的动态 hook 与环境模拟能力

但无论复杂度怎么变,“先抓发送前现场,再回溯签名输入” 这个思路,几乎一直都有效。


分享到:

上一篇
《自动化测试中的测试数据管理实战:从用例隔离到环境一致性保障》
下一篇
《Web3 中级实战:基于 Solidity 与 The Graph 构建可查询的链上积分系统》