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

《Web逆向实战:从请求链路分析到签名参数复现的中级方法论》

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

Web逆向实战:从请求链路分析到签名参数复现的中级方法论

做 Web 逆向时,很多人一上来就盯着“加密函数”本身,结果越看越乱。真正高效的方式,往往不是先解算法,而是先把请求链路理清:参数从哪里来、什么时候组装、在哪一层被改写、最终如何落到网络请求里。

这篇文章我会按一个更接近实战的顺序,带你从 请求链路分析 走到 签名参数复现。重点不是某个具体站点,而是一套中级阶段非常实用的方法论:先定位链路,再缩小范围,最后最小化复现

边界先说清:本文讨论的是安全研究、接口调试、自动化测试、数据抓包分析等合法场景。请仅在你有权限的系统中使用。


背景与问题

一个典型场景是这样的:

  • 页面里某个接口请求返回 401 / 403
  • 你复制请求头、Cookie、Query 参数去重放,仍然失败
  • 多了几个看不懂的字段,比如:
    • sign
    • token
    • x-s
    • traceid
    • nonce
    • timestamp

这时很多人会立刻搜“JS 逆向 sign”,然后在 Sources 里全局搜 md5sha256encrypt。这当然可能有效,但中级阶段更应该建立一个稳定套路:

  1. 先确认哪个参数真的是校验核心
  2. 再确定参数生成时机
  3. 再确定参与签名的原始数据
  4. 最后才是还原算法和执行环境

如果这四步顺序反了,往往会陷入两个坑:

  • 误把业务参数当签名参数
  • 误把加密函数当核心,而忽略前置拼接、排序、编码、环境注入

我当时踩过一个很典型的坑:盯着一个 sha1() 半天,最后发现真正影响结果的是调用前对对象做了字段排序 + URL 编码 + 空值过滤。哈希函数本身根本不重要,重要的是“喂进去的字符串到底是什么”。


前置知识

如果你已经具备下面这些基础,阅读会顺很多:

  • 会用浏览器开发者工具看 Network / Sources / Application
  • 知道 XHR / Fetch 的基本区别
  • 理解 Query 参数、Form、JSON Body
  • 对 JavaScript 闭包、异步 Promise、Hook 有基本认知
  • 能运行 Node.js 或 Python

环境准备

建议准备以下工具:

  • Chrome / Edge DevTools
  • Node.js 16+
  • Python 3.9+
  • 可选:
    • Charles / Fiddler / mitmproxy
    • JS 格式化工具
    • source map 查看工具

我个人偏好是:

  • 浏览器里定位链路
  • Node.js 里复现 JS 签名
  • Python 里做自动化请求

这样职责清晰,不容易把“分析环境”和“请求环境”混在一起。


核心原理

1. Web 逆向的重点不是“算法”,而是“数据流”

签名参数一般是由以下几部分拼出来的:

  • 固定盐值或版本号
  • 时间戳
  • 随机数 / nonce
  • 设备指纹 / 环境特征
  • 业务参数(如页码、关键词、商品 ID)
  • Cookie / LocalStorage / SessionStorage 中的令牌
  • 服务端下发的动态挑战值

所以你要还原的其实不是单个函数,而是这条数据流:

flowchart LR
  A[页面交互/初始化] --> B[业务参数收集]
  B --> C[环境参数注入]
  C --> D[签名串拼接]
  D --> E[哈希/加密]
  E --> F[请求头或请求参数]
  F --> G[发送请求]

2. 请求链路分析的目标

请求链路分析不是为了“看懂所有代码”,而是为了回答这几个关键问题:

  • 请求是谁发起的?
  • 参数在哪一层组装?
  • 签名在发送前最后一次修改发生在哪里?
  • 哪些字段是稳定的,哪些字段是一次一变的?
  • 是否依赖浏览器环境对象?

3. 典型签名生成位置

常见位置通常有三层:

  1. 业务层
    • 页面代码直接拼接参数
  2. 请求封装层
    • axios/fetch 二次封装、request interceptor
  3. 安全 SDK 层
    • 专门负责 sign/token/fingerprint

中级实战里,一个很重要的意识是:
真正的签名逻辑往往不在发请求的页面代码里,而在拦截器或 SDK 里。


从请求链路入手的分析框架

第一步:在 Network 里确认“目标请求”

先找出最值得分析的那个请求,通常满足:

  • 只有它失败,其他静态资源正常
  • 重放时服务端会校验
  • 请求里带有明显动态字段
  • 接口响应对业务价值高

拿到目标请求后,优先记录:

  • 请求方法
  • URL
  • Query 参数
  • Header
  • Body
  • Cookie
  • 请求发起时间
  • 调用栈 Initiator

第二步:按“发起点”反查调用链

Chrome DevTools 的 Initiator / Call Stack 很关键。
你要找到的是:

  • 谁调用了 fetch
  • 谁调用了 XMLHttpRequest.send
  • 谁在请求发送前改写 headers/body

可以把调用链抽象成这样:

sequenceDiagram
  participant U as 用户操作
  participant P as 页面业务代码
  participant R as 请求封装层
  participant S as 签名模块
  participant N as Network

  U->>P: 点击/滚动/加载
  P->>R: request(config)
  R->>S: buildSign(config, env)
  S-->>R: sign/header/token
  R->>N: fetch/xhr send

第三步:不要急着搜 md5,先搜“参数名”

如果请求中有 signx-signsignature,优先全局搜索:

  • 参数名本身
  • 设置 header 的方法
  • setRequestHeader
  • fetch(url, config)headers
  • axios 请求拦截器

我很多次都是这样定位到的,而不是直接搜加密函数。
因为加密函数可能被混淆成 a.b(c),但参数名通常要落到请求里,藏不住。

第四步:确认“签名原文”

这是最容易被忽略、也是最重要的一步。

你需要确认:

  • 参与签名的字段有哪些
  • 字段顺序是否固定
  • 是否过滤空值
  • 是否把对象转为 JSON 后再签
  • 是否 URL 编码
  • 是否拼接 secret
  • 是否区分大小写
  • 时间戳单位是秒还是毫秒

很多“明明算法一样却签不对”的问题,都出在这里。


一套可执行的中级方法论

我习惯把整个过程拆成 5 个层次:

层 1:抓到“最终请求”

目的是拿到结果,而不是理解代码。

关注:

  • 最终发出的 headers
  • 最终 body
  • 动态参数随时间是否变化

层 2:定位“最后写入点”

目的是找到“是谁把 sign 写进去的”。

常用手法:

  • fetch 打断点
  • 对 XHR 的 send 打断点
  • Hook setRequestHeader
  • Hook JSON.stringify

层 3:定位“签名原文组装点”

目的是弄清楚进入加密函数前的字符串。

常用手法:

  • 在疑似函数前后打印入参与出参
  • CryptoJS.MD5sha256btoa 等做 Hook
  • 断点观察调用栈和闭包变量

层 4:抽离“最小可复现逻辑”

目的是在页面外运行。

抽离时只保留:

  • 必需参数
  • 必需依赖函数
  • 必需环境对象

不要一开始就复制整份混淆代码,那样后期很难维护。

层 5:重放验证与批量化

目的是确认复现结果真的有效。

验证顺序建议是:

  1. 浏览器控制台内调用函数,结果是否一致
  2. Node.js 中运行,结果是否一致
  3. 替换到真实请求里,是否通过
  4. 批量请求时,是否稳定通过

实战案例:复现一个常见签名参数

下面用一个教学用简化案例演示完整流程。
假设某接口请求如下:

GET /api/list?page=1&keyword=phone&ts=1665830000&sign=?

页面逻辑大致是:

  • pagekeyword
  • 加上当前时间戳 ts
  • 按键名排序
  • 拼成 keyword=phone&page=1&ts=1665830000
  • 末尾追加固定盐值 secret=demo123
  • 做 MD5,得到 sign

现实中往往会更复杂,但分析路径是一样的。


实战代码(可运行)

1. 浏览器中 Hook 请求链路

先在控制台里注入 Hook,观察请求从哪里发出、sign 在哪里出现。

(function () {
  const originalFetch = window.fetch;

  window.fetch = async function (...args) {
    const [url, config] = args;
    console.log('[fetch url]', url);
    console.log('[fetch config]', config);

    if (config && config.headers) {
      console.log('[fetch headers]', config.headers);
    }

    const resp = await originalFetch.apply(this, args);
    return resp;
  };

  const originalOpen = XMLHttpRequest.prototype.open;
  const originalSend = XMLHttpRequest.prototype.send;
  const originalSetHeader = XMLHttpRequest.prototype.setRequestHeader;

  XMLHttpRequest.prototype.open = function (method, url, ...rest) {
    this._method = method;
    this._url = url;
    return originalOpen.call(this, method, url, ...rest);
  };

  XMLHttpRequest.prototype.setRequestHeader = function (key, value) {
    if (!this._headers) this._headers = {};
    this._headers[key] = value;
    return originalSetHeader.call(this, key, value);
  };

  XMLHttpRequest.prototype.send = function (body) {
    console.log('[xhr]', this._method, this._url);
    console.log('[xhr headers]', this._headers || {});
    console.log('[xhr body]', body);
    return originalSend.call(this, body);
  };

  console.log('hook installed');
})();

这段代码的作用很直接:

  • fetch
  • 抓 XHR
  • 看最终 URL、Header、Body

如果你已经在 Network 中确认了目标请求,这一步就能帮助你判断:
sign 是在请求创建前就存在,还是在发送前临时加进去。


2. 在 Node.js 中复现签名

下面给出一个可以直接运行的签名示例。

const crypto = require('crypto');

function buildSign(params, secret) {
  const filtered = Object.keys(params)
    .filter((key) => params[key] !== undefined && params[key] !== null && params[key] !== '')
    .sort()
    .map((key) => `${key}=${params[key]}`)
    .join('&');

  const plainText = `${filtered}&secret=${secret}`;

  const sign = crypto
    .createHash('md5')
    .update(plainText, 'utf8')
    .digest('hex');

  return {
    plainText,
    sign,
  };
}

const params = {
  page: 1,
  keyword: 'phone',
  ts: 1665830000,
};

const secret = 'demo123';

const result = buildSign(params, secret);
console.log('plainText:', result.plainText);
console.log('sign:', result.sign);

运行方式:

node sign.js

如果你已经在浏览器里拿到了签名前原文,就可以对照:

  • 原文是否一致
  • 哈希结果是否一致

3. 用 Python 重放请求

签名常常在 Node.js 里算,但请求批量化更适合用 Python。

import hashlib
import time
import requests


def build_sign(params, secret):
    filtered = {k: v for k, v in params.items() if v not in (None, '')}
    plain = '&'.join(f'{k}={filtered[k]}' for k in sorted(filtered.keys()))
    plain = f'{plain}&secret={secret}'
    sign = hashlib.md5(plain.encode('utf-8')).hexdigest()
    return plain, sign


def main():
    params = {
        'page': 1,
        'keyword': 'phone',
        'ts': int(time.time())
    }
    secret = 'demo123'
    plain, sign = build_sign(params, secret)
    print('plain:', plain)
    print('sign:', sign)

    query = params.copy()
    query['sign'] = sign

    url = 'https://httpbin.org/get'
    resp = requests.get(url, params=query, timeout=10)
    print(resp.status_code)
    print(resp.text[:500])


if __name__ == '__main__':
    main()

这段代码主要演示两个点:

  • 签名逻辑如何在请求前拼接
  • 如何把签名插回请求中做重放验证

逐步验证清单

做签名复现时,我建议你按下面顺序验证,能少走很多弯路。

清单 1:字段层验证

  • 字段名完全一致
  • 字段值类型一致(字符串 / 数字)
  • 字段顺序一致
  • 空值过滤规则一致
  • 时间戳单位一致

清单 2:编码层验证

  • 是否使用 UTF-8
  • 中文是否先 URL 编码
  • 空格是否变成 %20+
  • JSON stringify 是否去空格
  • Unicode 转义是否一致

清单 3:算法层验证

  • MD5 / SHA1 / SHA256 是否判断正确
  • 输出是 hex / base64 / 大写 / 小写
  • 是否截断
  • 是否二次哈希
  • 是否有 HMAC key

清单 4:环境层验证

  • 是否依赖 window
  • 是否依赖 document.cookie
  • 是否依赖 localStorage
  • 是否依赖 Canvas / WebGL 指纹
  • 是否依赖浏览器时区、语言、UA

更复杂一点:签名不只是哈希

现实站点里,签名常见会发展成这种结构:

  1. 先收集业务参数
  2. 再读取 Cookie / token
  3. 再加上随机数和时间戳
  4. 再做排序或序列化
  5. 再经过加密函数
  6. 结果写到 Header
  7. 服务端还会校验请求频率、指纹一致性

可以抽象为下面这个状态过程:

stateDiagram-v2
  [*] --> 收集业务参数
  收集业务参数 --> 注入环境参数
  注入环境参数 --> 生成签名原文
  生成签名原文 --> 执行哈希或加密
  执行哈希或加密 --> 写入请求
  写入请求 --> 服务端校验
  服务端校验 --> [*]

这也是为什么中级实战不能只盯着“某个加密函数”。


常见坑与排查

1. 看到 sign 就以为它是唯一校验项

有些接口表面上校验 sign,实际上还会同时校验:

  • timestamp
  • nonce
  • trace-id
  • Cookie 中的会话值
  • 请求头中的设备标识

排查建议:

  • 先固定除 sign 外的所有参数,观察单变量变化
  • 再逐个删字段测试哪个会导致失败

2. 忽略“最终请求”与“源码逻辑”不一致

源码里你看到的是一套参数,但真正发出去时,可能被拦截器改过。

例如:

  • Query 被追加公共参数
  • Header 被统一注入 token
  • Body 被重新序列化
  • 时间戳在发送前才生成

排查建议:

  • 以 Network 中的最终请求为准
  • fetch / xhr.send 处断点看最终值

3. 哈希结果对不上,其实是拼接原文错了

这是最高频的坑。常见错法:

  • 排序规则错
  • 漏掉某个字段
  • 1'1' 混用
  • 参数值含空格或换行
  • 中文编码方式不同
  • JSON key 顺序变化

排查建议:

把注意力放到“签名前原文”上,而不是只盯最终 sign。
只要原文一致,算法大概率就对了。


4. 浏览器能算,Node.js 算不出来

这通常意味着签名逻辑依赖浏览器环境。

典型依赖包括:

  • window.navigator
  • document.cookie
  • atob / btoa
  • canvas.toDataURL()
  • crypto.subtle
  • DOM 事件时间序列

排查建议:

  • 先在浏览器控制台直接调用目标函数
  • 再一点点迁移到 Node.js
  • 缺什么环境,就 mock 什么环境
  • 能最小化就不要全量移植

5. 过度依赖格式化后的代码结构

很多混淆代码格式化后看着“像能读懂”,但变量名没意义,容易让人误判。

排查建议:

  • 先看数据流,不急着看控制流
  • 先确认入参和出参,再看函数内部
  • 多用断点和运行时打印,少靠静态猜

安全/性能最佳实践

这一节不只是给“做逆向”的人,也是给“做接口安全”的人一点反向视角。

1. 最小化分析范围

不要试图一次看完整站点 JS。
高效做法是:

  • 只围绕一个目标请求
  • 只追一个签名参数
  • 只抽一条最小链路

这样你能更快形成闭环验证。


2. 建立“样本对照组”

至少准备 3 组请求样本:

  • 相同参数、不同时间
  • 相同时间、不同业务参数
  • 相同业务、不同会话

有了对照组,你更容易判断:

  • 哪些字段参与签名
  • 哪些字段只是随请求变化
  • 哪些字段和用户会话绑定

3. 自动化时控制请求频率

真实系统往往不只校验 sign,还会做风控:

  • IP 限流
  • 会话频率异常
  • Header 指纹异常
  • 行为轨迹不自然

因此自动化测试时要注意:

  • 控制并发和间隔
  • 保持 Header 一致性
  • 会话与指纹不要频繁切换
  • 合法测试要提前获授权

4. 抽离签名模块时保留可观测性

我建议把复现代码写成这种形式:

  • 能输出签名原文
  • 能输出参与字段
  • 能输出最终 sign
  • 能单独校验每一步

这样后面站点升级时,你会非常容易定位是哪一步变了。

例如:

function debugBuildSign(params, secret) {
  const keys = Object.keys(params).sort();
  const pairs = keys.map((k) => `${k}=${params[k]}`);
  const plain = `${pairs.join('&')}&secret=${secret}`;
  const sign = require('crypto').createHash('md5').update(plain).digest('hex');

  return {
    keys,
    pairs,
    plain,
    sign,
  };
}

5. 对防守方的建议

如果你是接口安全或前端安全侧,单纯把算法藏在前端并不稳妥。更合理的是:

  • 不把核心密钥完全暴露在前端
  • 让服务端参与校验挑战
  • 结合时效、会话、设备、行为综合判断
  • 对重放攻击和批量请求做限速与告警

前端签名能提高滥用成本,但不能替代服务端安全策略。


一个中级读者值得养成的思维方式

到这个阶段,我很建议你把“逆向”理解成三件事:

1. 先找边界

不是所有代码都需要懂。
你真正需要的是:

  • 输入是什么
  • 输出是什么
  • 中间最关键的变换在哪

2. 先还原流程,再还原细节

先知道:

  • 请求怎么发
  • sign 什么时候生成
  • 原文怎么拼

再去看具体算法。
这比一开始就冲进混淆代码高效很多。

3. 最终目标是“稳定复现”,不是“看懂全部源码”

很多项目代码量巨大,完全看懂不现实。
但你只要能做到:

  • 找准请求链路
  • 还原签名原文
  • 稳定复现参数

就已经完成了大部分实战目标。


总结

这篇文章想传递的核心只有一句话:

Web 逆向里,签名参数复现的关键,不是先破算法,而是先理清请求链路和数据流。

一个更稳的中级方法论是:

  1. 在 Network 中锁定目标请求
  2. 沿 Initiator 和调用栈回溯到请求封装层
  3. 找到 sign 的最终写入点
  4. 确认签名前原文,而不是只盯哈希函数
  5. 抽离最小可运行逻辑到 Node.js / Python
  6. 用样本对照做持续验证

如果你已经能熟练完成这些步骤,那么后面遇到更复杂的场景——比如混淆、环境依赖、动态 token、多段签名——也只是工作量更大,而不是思路完全失效。

最后给一个很实用的建议:
每次分析都保存“请求样本 + 签名原文 + 复现代码 + 验证结果”四件套。
这会让你的逆向过程从“碰运气”变成“可复盘、可维护”的工程化流程。


分享到:

上一篇
《AI 应用中的 RAG 实战:从向量检索、重排序到效果评估的完整落地指南》
下一篇
《集群架构中基于 Kubernetes 的高可用控制平面设计与故障切换实战》