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

《Web逆向实战:从抓包定位到参数还原,系统破解前端加密接口的中级方法论》

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

背景与问题

做 Web 逆向时,最让人头疼的往往不是“有没有接口”,而是“接口明明抓到了,参数却还原不出来”。

典型场景一般长这样:

  • 页面请求里有 signtokenciphertextenc_data 之类的字段
  • 参数每次都变,直接重放一定失败
  • 前端代码被压缩、混淆,关键逻辑埋在一大坨 bundle 里
  • 你明明知道“加密一定在前端做过”,但就是找不到入口

这类问题如果只靠“到处搜关键字”“盲断点”“碰运气 hook”,效率会很低。真正能提高成功率的,是建立一套从抓包定位到参数还原的系统方法论。

这篇文章我想从架构化分析的角度来讲,不只讲“怎么解一个站”,而是讲一套中级阶段可复用的路径:

  1. 先从网络层确定目标参数和调用链
  2. 再从运行时定位参数生成点
  3. 最后把前端逻辑抽离成可独立运行的代码
  4. 在此基础上做稳定性、性能和排障优化

注意,本文讨论的是授权测试、学习研究、CTF/靶场、企业自测等合法场景。不要用于未授权目标。


先建立整体视角:逆向的不是“密文”,而是“生成系统”

很多人一开始会盯着那个 sign 值本身,比如:

  • 它是 MD5 吗?
  • 是 AES 吗?
  • 是 RSA 吗?
  • 里面是不是时间戳拼接了盐?

这些猜测有时有帮助,但更有效的思路是:

不要先猜算法,先找“这个值在前端系统里是如何被生产出来的”。

也就是说,我们逆向的对象不是一个孤立参数,而是一条生产链:

flowchart LR
    A[抓包定位请求] --> B[识别关键参数]
    B --> C[关联调用栈]
    C --> D[定位生成函数]
    D --> E[分析输入来源]
    E --> F[抽离依赖环境]
    F --> G[本地复现生成逻辑]
    G --> H[接口稳定重放]

真正困难的地方,往往不在“加密算法很复杂”,而在于它混合了这些因素:

  • 请求体规范化:排序、过滤空值、字段拼接
  • 时间戳、随机数、nonce
  • 浏览器环境依赖:windownavigatordocumentlocalStorage
  • token、cookie、session 参与计算
  • wasm、web worker、第三方加密库
  • 混淆和反调试

所以中级阶段最重要的能力,不是会背几个算法,而是会把整个参数生产过程拆开。


核心原理

1. 从抓包视角看:先区分“哪类参数值得追”

拿到一个请求后,不是所有参数都值得逆。优先级建议如下:

第一类:决定成败的校验参数

例如:

  • sign
  • signature
  • token
  • x-s
  • x-sign
  • auth
  • verify

这些往往是服务器验签的核心字段,不还原就没法稳定调用。

第二类:看起来像密文的业务参数

例如:

  • data
  • payload
  • enc_data
  • params

它们可能是:

  • 明文 JSON 再 base64
  • AES/RSA 加密串
  • URL-safe 编码
  • 压缩后再编码

第三类:环境协同参数

例如:

  • 时间戳
  • nonce
  • traceId
  • deviceId
  • session 派生值

这类参数本身不一定复杂,但常常是验签输入的一部分。

我通常会先做一个表,把请求字段分成三类:

字段表现形式是否变化猜测角色
timestamp数字每次变化时间因子
nonce随机字符串每次变化防重放
sign十六进制/长串每次变化核心校验
database64/密文按业务变化业务载荷

这样后续分析更有方向,不容易在杂项里迷路。


2. 从运行时视角看:参数一定经历了“明文到目标值”的转换

不管代码混淆得多厉害,参数如果来自前端,就几乎一定要经过以下某种链路:

sequenceDiagram
    participant U as 用户操作
    participant P as 页面业务代码
    participant E as 参数处理层
    participant C as 加密/签名函数
    participant X as XMLHttpRequest/fetch
    participant S as 服务端

    U->>P: 触发搜索/翻页/提交
    P->>E: 组装业务参数
    E->>C: 排序/拼接/加密/签名
    C-->>E: 返回 sign/data
    E->>X: 发送请求
    X->>S: HTTP Request

这个链路的意义在于:
即使你看不懂整个项目,也可以在这条链路上找切入点。

常用切入点

  1. XHR/fetch 发送前
    • 看最终 body、headers 是怎么来的
  2. 加密库入口
    • 比如 CryptoJS.MD5AES.encryptJSEncrypt.prototype.encrypt
  3. 序列化入口
    • JSON.stringify
    • encodeURIComponent
    • btoa
  4. 时间和随机数
    • Date.now
    • Math.random
    • crypto.getRandomValues

很多时候,我并不是先去读 bundle,而是先在这些 API 上做 hook,让运行时自己“吐出”关键线索。


3. 参数还原的本质:算法 + 输入 + 环境

一个参数能否成功复现,通常由三件事决定:

算法

例如:

  • MD5/SHA 系列
  • HMAC
  • AES/RSA
  • 自定义字符置换
  • base64/urlencode/压缩

输入

例如:

  • 请求参数原文
  • token/cookie
  • 时间戳
  • 固定盐值
  • 页面埋点值
  • 设备指纹

环境

例如:

  • 浏览器对象
  • localStorage 中缓存
  • JS 执行上下文
  • WebAssembly 实例
  • 某些初始化流程

很多“明明算法抠出来了却跑不通”的问题,本质上不是算法错了,而是输入漏了或者环境不对


方案对比与取舍分析

在实际项目里,前端加密接口的处理方式大致有三条路:

方案 A:浏览器内直接复用页面环境

做法:

  • 打开页面
  • 在控制台或注入脚本中直接调用前端现成函数
  • 借助 Playwright/Puppeteer 做自动化

优点

  • 环境最真实
  • 成功率高
  • 对复杂站点、强环境依赖站点很友好

缺点

  • 性能一般
  • 批量调用成本高
  • 页面升级后容易失效
  • 自动化特征更明显

方案 B:抽离 JS 逻辑,在 Node.js 中本地运行

做法:

  • 从页面中提取核心函数
  • 补齐依赖
  • 在 Node 中生成参数

优点

  • 性能较好
  • 易集成到脚本和服务
  • 便于做批量请求

缺点

  • 环境补齐成本高
  • 对混淆/动态加载代码不够友好
  • 维护要求更高

方案 C:重写算法,用 Python/Go/Node 重新实现

做法:

  • 读懂原始逻辑
  • 完全脱离前端,自己实现签名和加密流程

优点

  • 最稳定
  • 性能最好
  • 最适合长期维护

缺点

  • 前期成本最高
  • 复杂站点难度大
  • 漏掉边界条件就会验签失败

我个人的建议

中级阶段推荐采用两段式策略

  1. 先用方案 A/B 快速打通
    • 核心目标是拿到可用参数生成链
  2. 再视收益决定是否走方案 C
    • 如果接口长期要用,再做重写和工程化

可以简单理解为:

  • 验证期:优先“快”
  • 稳定期:优先“准”
  • 规模期:优先“稳 + 省资源”

实战代码(可运行)

下面给一个可运行的教学级示例。它不是针对某个真实站点,而是模拟一个典型前端签名流程:

  1. 业务参数按 key 排序
  2. 去掉空值
  3. 拼接时间戳和 token
  4. 生成 sign
  5. 构造请求体

这样能把“参数还原”这件事讲透。


示例一:Node.js 还原签名逻辑

const crypto = require('crypto');

/**
 * 规范化参数:
 * 1. 去掉 null/undefined/""
 * 2. key 按字典序排序
 * 3. 统一拼接成 k=v&k2=v2
 */
function normalizeParams(params) {
  return Object.keys(params)
    .filter((key) => params[key] !== undefined && params[key] !== null && params[key] !== '')
    .sort()
    .map((key) => `${key}=${String(params[key])}`)
    .join('&');
}

/**
 * 模拟前端 sign 生成逻辑
 * sign = md5(normalized + "|" + timestamp + "|" + token + "|" + salt)
 */
function buildSign(params, timestamp, token) {
  const salt = 'mid_level_reverse';
  const normalized = normalizeParams(params);
  const raw = `${normalized}|${timestamp}|${token}|${salt}`;
  return crypto.createHash('md5').update(raw, 'utf8').digest('hex');
}

/**
 * 构造最终请求体
 */
function buildPayload(params, token) {
  const timestamp = Date.now();
  const sign = buildSign(params, timestamp, token);

  return {
    data: params,
    timestamp,
    sign
  };
}

// 测试运行
const token = 'test_token_123';
const params = {
  keyword: '逆向',
  page: 1,
  pageSize: 20
};

const payload = buildPayload(params, token);
console.log('payload =>', payload);

这个例子虽然简单,但已经覆盖了实战里最常见的几个要素:

  • 参数排序
  • 空值过滤
  • 时间戳参与计算
  • token 参与计算
  • 固定盐值参与计算

很多真实站点,本质也不过是这个框架再叠加几层编码或加密。


示例二:Python 复刻同样逻辑

当你需要把结果接入爬虫、测试脚本或后端服务时,Python 版很实用。

import hashlib
import time


def normalize_params(params: dict) -> str:
    items = []
    for k in sorted(params.keys()):
        v = params[k]
        if v is None or v == "":
            continue
        items.append(f"{k}={v}")
    return "&".join(items)


def build_sign(params: dict, timestamp: int, token: str) -> str:
    salt = "mid_level_reverse"
    normalized = normalize_params(params)
    raw = f"{normalized}|{timestamp}|{token}|{salt}"
    return hashlib.md5(raw.encode("utf-8")).hexdigest()


def build_payload(params: dict, token: str) -> dict:
    timestamp = int(time.time() * 1000)
    sign = build_sign(params, timestamp, token)
    return {
        "data": params,
        "timestamp": timestamp,
        "sign": sign
    }


if __name__ == "__main__":
    token = "test_token_123"
    params = {
        "keyword": "逆向",
        "page": 1,
        "pageSize": 20
    }
    payload = build_payload(params, token)
    print(payload)

示例三:浏览器中 hook fetch 观察参数生成结果

如果你还没定位到生成函数,不要急着读代码。先 hook 一下请求发送层,往往更快。

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

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

    if (options) {
      console.log('[fetch options]', options);

      if (options.body) {
        try {
          console.log('[fetch body raw]', options.body);

          if (typeof options.body === 'string') {
            try {
              console.log('[fetch body json]', JSON.parse(options.body));
            } catch (e) {}
          }
        } catch (e) {
          console.warn('body parse error', e);
        }
      }
    }

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

这段代码适合在浏览器控制台执行。它的价值在于:

  • 你能直接看到最终提交的 body
  • 能快速识别 signtimestampnonce
  • 能判断加密发生在 fetch 之前还是更早

示例四:hook 常见加密入口

如果你怀疑站点使用 CryptoJS,可以这么观察:

(function () {
  if (!window.CryptoJS) {
    console.warn('CryptoJS not found');
    return;
  }

  const rawMD5 = CryptoJS.MD5;
  CryptoJS.MD5 = function (data) {
    console.log('[MD5 input]', data && data.toString ? data.toString() : data);
    const result = rawMD5.apply(this, arguments);
    console.log('[MD5 output]', result.toString());
    return result;
  };

  console.log('CryptoJS.MD5 hooked');
})();

我自己在分析一些站点时,最常见的突破口就是这种“先 hook 再回溯调用栈”的方式。
因为你不需要先搞懂整套混淆代码,只需要让关键函数先现身。


从抓包到还原:一条更稳的实战路径

这一部分给出一套更贴近项目的操作顺序。

第一步:抓包确认最小目标

先明确三件事:

  • 真正有价值的是哪个接口
  • 哪个参数导致重放失败
  • 请求依赖哪些 header/cookie

建议重点记录:

  • URL
  • Method
  • Query 参数
  • Body 结构
  • 请求头
  • 响应码和错误信息

如果接口返回类似:

  • sign error
  • invalid token
  • timestamp expired
  • illegal request

那排查范围会收缩得很快。


第二步:固定变量,制造对照组

这是很多人忽略但非常关键的一步。

例如同一个请求,尽量只改一个字段:

  • 只改 page
  • 只改 keyword
  • 只改时间间隔
  • 不改 cookie,只重放 body

观察哪些字段随之变化。

你想得到的结论

  • sign 是否只和 body 相关?
  • 是否和 header 里的 token 相关?
  • 是否和 cookie 绑定?
  • timestamp 是否参与验签?
  • nonce 是否必须唯一?

这一步其实是在做“黑盒因果分析”。


第三步:从发送层向前追溯

如果抓包看到最终发出的 body 里有:

{
  "data": "abcxyz...",
  "sign": "9f8e7d...",
  "timestamp": 1680000000000
}

那你的追踪顺序最好是:

  1. fetch/xhr.send 处下断点
  2. 看是谁调用了它
  3. 回到上一层找到 body 构造位置
  4. 再往前看 sign 是哪个函数返回的
  5. 再看这个函数的输入是谁传进去的

这个顺序比“全局搜 sign”通常更稳。
因为很多站点压缩后变量名根本不是 sign,而是 _0x3fa2b1 这种鬼名字。


第四步:抽离最小可运行单元

定位到核心函数后,不要急着整包复制。
先做最小抽离

  • 只保留生成参数所需的函数
  • 逐步补齐依赖
  • 每补一层就验证一次输出

这个阶段最怕“一次性复制一万行代码”,后面只会更乱。


第五步:做结果校验

参数还原成功,不是“看起来像”,而是要满足:

  • 本地输出和浏览器输出一致
  • 接口可成功返回业务数据
  • 多组输入下都稳定有效

建议至少验证三组不同参数,不要只测一组就下结论。


常见坑与排查

这部分我尽量写得实战一点,因为很多时间都是花在这里。

1. 只抄了算法,没抄输入预处理

最典型的坑:

  • 前端会先对参数排序
  • 会过滤空值
  • 布尔值转字符串
  • 数字转字符串
  • 数组先 JSON.stringify
  • 中文编码方式不同

最终表现就是:
“我明明也用了 MD5,为什么 sign 不一样?”

排查方式

把原始输入串打印出来,对比:

  • 浏览器端签名前原文
  • 本地还原签名前原文

不要只比最终 hash,要比 hash 之前的明文。


2. 忽略环境变量

比如某些站点会把这些值混进签名:

  • localStorage.token
  • document.cookie
  • navigator.userAgent
  • window.location.pathname

你如果只复制函数,不模拟环境,结果一定错。

排查方式

在关键函数里打印所有入参和依赖对象;
如果在 Node 中运行,逐项 mock:

global.window = {};
global.navigator = {
  userAgent: 'Mozilla/5.0'
};
global.document = {
  cookie: 'sessionid=abc123'
};

3. 时间戳误差导致失败

有些接口会校验时间窗口,比如 ±5 秒或 ±30 秒。
你本地生成后如果请求发慢了,或者机器时间漂移,就会失败。

排查方式

  • 检查本机时间是否准确
  • 确认时间单位是秒还是毫秒
  • 观察服务端是否要求 UTC 格式或特定格式化字符串

4. 混淆代码里的“假入口”误导分析

有些站点故意在代码里放很多看起来像加密的函数:

  • 假 MD5
  • 假 base64
  • 垃圾字符串数组
  • 无意义控制流平坦化

如果你纯靠静态阅读,很容易被带偏。

排查方式

优先依赖运行时:

  • 发送前断点
  • hook 加密函数
  • 查看调用栈
  • 记录真实执行路径

我自己的经验是:
运行时证据 > 静态猜测


5. WebAssembly 或 Worker 中执行

如果你在主线程怎么都搜不到关键逻辑,可能它根本不在主线程。

现象

  • 页面 JS 很干净
  • 只看到很短的调用代码
  • 请求前有 wasm 初始化
  • 或者有 worker 脚本加载

排查方向

  • 关注 WebAssembly.instantiate
  • 关注 new Worker()
  • 抓取 worker 脚本
  • 分析 wasm 导出函数和输入输出

6. 请求成功一次,后续大量失败

这通常不是“算法偶发错误”,而是系统层约束:

  • token 过期
  • nonce 重放
  • IP/设备绑定
  • 风控阈值
  • cookie 状态失效

排查方式

分层验证:

  1. 参数是否一致
  2. cookie 是否更新
  3. token 是否轮换
  4. 请求频率是否过高
  5. 是否存在一次性挑战值

安全/性能最佳实践

即使是逆向分析脚本,也建议做工程化,不然后期维护会很痛苦。

1. 把“抓包验证”和“参数生成”解耦

推荐拆成两层:

  • 参数层:只负责产出 sign/data/timestamp
  • 请求层:只负责发 HTTP 请求

这样当接口字段变化时,你只需要调整一层。

classDiagram
    class ParamBuilder {
      +normalize(params)
      +buildSign(params, ts, token)
      +buildPayload(params, token)
    }

    class HttpClient {
      +setHeaders(headers)
      +post(url, payload)
    }

    class ReverseService {
      +query(params)
    }

    ReverseService --> ParamBuilder
    ReverseService --> HttpClient

2. 日志要记录“签名前原文”

生产环境里最难排查的问题,就是“怎么突然签名错了”。
解决办法很简单:保留这些日志:

  • 输入参数
  • 规范化后的字符串
  • 时间戳
  • token 摘要
  • 最终 sign

当然,敏感信息要脱敏,不要把完整凭证直接落盘。


3. 缓存可复用的上下文

如果某接口强依赖:

  • token
  • deviceId
  • session
  • 初始化指纹

那这些值可以做短期缓存,避免每次重新走完整流程。
这样既减轻页面负担,也减少环境初始化时间。


4. 评估容量与资源消耗

如果你用浏览器自动化批量生成参数,资源成本其实不低。

一个简单估算思路:

  • 单个浏览器实例占用内存:200MB ~ 500MB
  • 单台机器可稳定承载实例数:取决于 CPU/内存
  • 单次参数生成耗时:几十毫秒到数秒不等

所以:

  • 小规模验证:浏览器内复用最省事
  • 中等规模任务:Node 抽离更划算
  • 长期大规模:重写算法收益最高

不要一上来就“开 50 个无头浏览器”,很容易把自己机器先打趴。


5. 注意合法边界和敏感数据保护

这点必须明确:

  • 仅在授权测试、企业内审、教学研究、靶场练习中使用
  • 不要收集、传播、滥用他人账号凭证
  • 不要把真实 cookie、token、私钥写进代码仓库
  • 日志、样本、抓包文件要做脱敏

技术能力越强,越要有边界意识。


一个可落地的排查清单

如果你手头刚好有个还原不出来的接口,可以按这个顺序过一遍:

抓包层

  • 是否确认了真正发请求的接口
  • 是否识别出核心校验字段
  • 是否记录了 header/cookie/token

对照实验层

  • 是否只改一个参数做过对比
  • 是否观察过 sign 与哪些字段同步变化
  • 是否确认时间戳和 nonce 的作用

运行时层

  • 是否 hook 了 fetch/xhr
  • 是否尝试 hook 过 MD5/AES/RSA 等入口
  • 是否查看了请求发送前的调用栈

抽离层

  • 是否找到了签名前原文
  • 是否复现了参数排序/过滤/编码规则
  • 是否补齐了浏览器环境依赖

校验层

  • 本地输出是否与浏览器一致
  • 多组参数下是否都能成功
  • 是否排除 token 过期和风控因素

这份清单的价值在于:
你不会一直在“是不是算法错了”这个单点里打转,而是能系统排查整条链路。


总结

前端加密接口的逆向,本质上不是“猜一个密文怎么来的”,而是还原一个参数生产系统

中级阶段最值得建立的能力,是这条完整路径:

  1. 抓包定位关键接口与关键参数
  2. 通过运行时 hook 和断点找到真实生成点
  3. 分清算法、输入、环境三类依赖
  4. 抽离最小可运行逻辑进行本地复现
  5. 用多组样本验证稳定性,并逐步工程化

如果你只记住一个结论,我希望是这个:

参数还原成功的关键,不在于你认识多少加密算法,而在于你能否准确找到“签名前原文”和“真实依赖环境”。

最后给几个可执行建议:

  • 先抓发送层,再追生成层,不要一上来就埋头读混淆代码
  • 先验证可用,再考虑重写,别过早工程化
  • 遇到不一致时,先比签名前原文,不要只盯最终 sign
  • 把环境依赖当一等公民,很多失败根本不是算法问题
  • 长期任务优先抽象参数层,后面维护成本会低很多

如果你已经能独立做抓包和基础断点,那么把这套方法练熟之后,处理大多数前端签名接口,成功率会明显提升。真正难的站点当然还会有,但至少你不会再“看着一串 sign 发呆”,而是知道该从哪里下手。


分享到:

上一篇
《中级开发者如何用 RAG 构建企业知识库问答系统:从数据清洗、向量检索到效果评估》
下一篇
《Node.js 中基于 Worker Threads 与队列的 CPU 密集型任务处理实战》