JS逆向案例:复杂Sign签名算法的分析与构造流程

作者:墨池时间:2026-10-30 18:35:16浏览:178

详细解析Web与H5接口中复杂Sign签名算法的逆向分析与本地构造全流程,涵盖抓包识别、断点精确定位、混淆逆向、Python纯算法复现与工程落地验证。

在现代Web平台及移动端H5接口的交互中,接口提供方普遍采用客户端签名(Sign)机制防止请求参数篡改和无授权重放。解决此类问题的关键,在于梳理签名涉及的数据构成、定位前端生成代码、还原处理逻辑并在脱离原浏览器上下文的环境中稳定复现。本文以典型生产系统中的复合签名方案为背景,完整拆解从接口抓包、代码定位、逻辑逆向到算法构造与验证的标准流程。

一、请求特征与复合签名模型剖析

在发起逆向分析前,首先需要通过抓包工具(如Chrome DevTools Network面板或Charles)捕获一次完整的业务API请求,提炼关键请求字段与参数规律。典型的复合签名请求通常具备如下几个特征:

  • 业务载荷参数:例如 userId=10086、page=1 或请求体JSON,携带实际业务数据。
  • 时间戳(timestamp):通常为10位或13位整数(如 1730000000),用于服务端校验请求时效(常见有效窗口为60秒至300秒)。
  • 随机字符串(nonce):通常为16至32位的随机字符或UUID,用于服务端防请求重放。
  • 签名结果(sign / signature):一段通常表现为32位Hex或44位Base64编码的不可逆摘要串。

复杂Sign算法的本质并非单一散列,而是一个多阶段的数据加工流水线。从原始参数规范化、加入隐蔽盐值(Salt)到多轮加密计算,整个逆向与构造的完整工程工作流如下图所示:

JS逆向案例:复杂Sign签名算法的分析与构造流程
图1:复杂Sign签名逆向与算法还原全景工作流

二、断点精确定位与调用栈回溯

在前端代码动辄数万行且经过Webpack打包、Terser压缩混淆的工程中,盲目全局搜索 sign 往往会得到成千上万个无关匹配。必须根据代码特征选择针对性的断点切入策略:

JS逆向案例:复杂Sign签名算法的分析与构造流程
图2:常见前端请求签名断点定位策略与适用场景对比

在实际操作中,推荐使用控制台或油猴脚本在页面加载初期注入属性拦截器代码。以拦截 headers["x-sign"] 为例,其核心注入实现如下:

// 在控制台运行或通过插件在页面最前置注入
(function() {
  const origSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
  XMLHttpRequest.prototype.setRequestHeader = function(header, value) {
    if (header.toLowerCase() === 'x-sign' || header.toLowerCase() === 'sign') {
      console.log(`[Sign Hook] 命中签名请求头: ${header} = ${value}`);
      debugger; // 在此处自动暂停,直接停留在设置请求头的上下文
    }
    return origSetRequestHeader.apply(this, arguments);
  };
})();

当请求被 debugger 挂起后,关注右侧的 Call Stack(调用栈)。从底层的 setRequestHeader 往上排查,通常前2~3层是 Axios 或封装的网络请求工具函数;再往上即可看到业务层的 interceptor 或 generateSignature(params) 函数,这便是签名算法的核心计算入口。

三、算法数据流拆解与混淆逆向

进入生成签名的函数体后,通常会观察到一系列经过混淆的变量与逻辑流。在生产项目中,为了兼顾性能与安全,复杂签名一般包含三个逻辑层次:参数清洗与排序、规范串拼装(Canonicalization)、哈希加盐与编码转换。

JS逆向案例:复杂Sign签名算法的分析与构造流程
图3:复杂Sign签名计算核心模型与数据流向

1. 混淆还原技巧与常量搜寻

在调试断点时,若遇到 _0x1a2b('0x12') 形式的字符串数组混淆或控制流平坦化(如 while(true) { switch(cases[i++]) {...} }),不要逐行分析每个分支,而应充分利用运行时控制台的计算能力:

  • 控制台直接求值:在当前断点作用域下,直接选中混淆表达式并在 Console 中执行,立刻得到脱敏字符串(如 _0x4b7c(0x3e) === "secretKey")。
  • 监控最终入参:加密函数在执行真正位运算前,一定会将整理好的字符串传入散列模块。在底层散列函数(如 update() 或 digest())的开头下断,直接查看待加密的规范化明文字符串。

四、算法本地构造与纯Python实现

经过逆向分析,确认本案例签名的计算逻辑如下:

  1. 提取所有请求参数(排除自身 sign 字段及空值)。
  2. 将 timestamp(13位毫秒时间戳)与 nonce(16位随机十六进制串)纳入参数字典。
  3. 所有键按 ASCII 升序排列,并按 key=value&key2=value2 的格式连接。
  4. 在连接字符串末尾追加固定硬编码盐值 &salt=ku_aifram_sign_sec_2026。
  5. 先计算该字符串的 SHA-256 摘要(小写Hex),再利用隐藏密钥对该摘要进行 HMAC-MD5 计算,得到最终32位小写签名。

针对这一逻辑,在脱离浏览器环境后,使用标准 Python 库即可实现高并发、毫秒级本地计算:

import hashlib
import hmac
import time
import uuid
from typing import Dict, Any

class SignGenerator:
    def __init__(self, salt: str, secret_key: str):
        self.salt = salt
        self.secret_key = secret_key.encode('utf-8')

    def generate_nonce(self) -> str:
        """生成16位随机Hex字符串"""
        return uuid.uuid4().hex[:16]

    def build_signature(self, params: Dict[str, Any], timestamp: int = None, nonce: str = None) -> Dict[str, Any]:
        """
        完整构造签名及配套请求头参数
        """
        if timestamp is None:
            timestamp = int(time.time() * 1000)
        if nonce is None:
            nonce = self.generate_nonce()

        # 1. 复制参数并合并签名辅助字段,剔除空值与已有sign
        sign_dict = {
            k: v for k, v in params.items()
            if v is not None and v != "" and k != "sign"
        }
        sign_dict["timestamp"] = timestamp
        sign_dict["nonce"] = nonce

        # 2. 键按 ASCII 字典序排序并拼接为 canonical 字符串
        sorted_keys = sorted(sign_dict.keys())
        canonical_str = "&".join([f"{k}={sign_dict[k]}" for k in sorted_keys])

        # 3. 追加尾部静态盐
        payload_with_salt = f"{canonical_str}&salt={self.salt}"

        # 4. 第一层:SHA-256 哈希
        sha256_digest = hashlib.sha256(payload_with_salt.encode('utf-8')).hexdigest().lower()

        # 5. 第二层:HMAC-MD5 加密
        hmac_obj = hmac.new(self.secret_key, sha256_digest.encode('utf-8'), hashlib.md5)
        final_sign = hmac_obj.hexdigest().lower()

        return {
            "sign": final_sign,
            "timestamp": timestamp,
            "nonce": nonce
        }

# 验证测试用例
if __name__ == "__main__":
    signer = SignGenerator(
        salt="ku_aifram_sign_sec_2026",
        secret_key="api_hmac_secret_key"
    )

    test_params = {
        "userId": "88481024",
        "action": "query_balance",
        "currency": "CNY",
        "version": "1.0.0"
    }

    # 模拟与浏览器捕获的同一组固定时间与随机数验证
    fixed_ts = 1730000000000
    fixed_nonce = "3a9f0b2c1d4e8f7a"
    res = signer.build_signature(test_params, timestamp=fixed_ts, nonce=fixed_nonce)

    print("计算所得签名结果:", res["sign"])
    print("配套生成参数:", res)

五、落地选型与常见陷阱排查

在工程化落地过程中,并不总是能够轻而易举地用纯 Python 或 Go 重写。针对不同复杂度的混淆代码,业界通常权衡采用以下三种落地路径:

JS逆向案例:复杂Sign签名算法的分析与构造流程
图4:签名还原工程化落地路径与避坑决策对比
排错核对法宝:在复现出现签名校验失败时,千万不要直接用动态数据去碰运气的网络请求。务必利用浏览器断点,截取一组原页面真实计算时的“入参字典、规范化连接串明文、中间摘要、最终签名”。将这一整组静态数据带入本地复现代码进行单测。如果规范化字符串完全相同,但摘要结果不同,说明散列算法存在特殊盐或魔改常数;如果字符串本身不同,说明字典序、URI转义或空值过滤规则存在细节差异。

六、总结

分析并构造复杂Sign签名算法是掌握Web安全攻防与接口自动化交互的核心基本功。其完整闭环包含三个核心阶段:通过属性拦截与断点在庞杂代码库中直击发包前夕;顺藤摸瓜拆解参数过滤、排序、盐值注入与级联摘要的数据流;最后在工程端依据业务吞吐量与算法复杂度,合理选用纯算法重写或Node补环境。掌握这一规范化的分析范式,能够有效应对绝大多数现代Web API的安全校验防护机制。

作者最新文章
相关文章
更多