爬虫逆向实战:HMAC签名原理、逆向分析与破解策略

1. 项目概述:为什么爬虫工程师必须啃下HMAC这块硬骨头?

如果你在爬虫逆向的战场上摸爬滚打过一阵子,肯定遇到过这种场景:目标网站的请求参数里,总有几个长得像乱码一样的字符串,比如 sign token x-signature 。你尝试直接复制使用,却发现它有时效性,过几分钟就失效了;你试图用常见的MD5或SHA1去撞,结果也对不上。这时候,你很可能就撞上了HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)。这玩意儿在当今的Web API、APP接口以及各类数据反爬策略中,出场率极高,几乎成了中高级爬虫工程师的“必修课”。它不像简单的Base64编码一眼就能看穿,也不像对称加密(如AES)那样有固定的密钥可以逆向推导,HMAC更像一个带着“动态口令”的哈希锁,让简单的重放攻击和参数篡改无所遁形。

简单来说,HMAC不是一种独立的哈希算法,而是一种利用哈希函数(如MD5、SHA256、SM3等)来构造消息认证码的技术。它的核心价值在于 验证数据的完整性和真实性 。服务器和客户端共享一个密钥,发送方用这个密钥和待发送的消息一起计算出一个HMAC值,接收方用同样的密钥和收到的消息再计算一次,如果两个HMAC值一致,就证明消息在传输过程中没被篡改,且确实来自合法的发送方。对于爬虫而言,我们的目标就是扮演那个“合法的客户端”,在不知道服务器密钥的前提下,逆向出生成这个HMAC值的逻辑,或者找到绕过验证的方法。

为什么它这么棘手?因为它结合了哈希算法的单向性和密钥的私密性。你无法从HMAC值反推出原始消息或密钥(哈希的特性),同时,没有密钥,你也无法为伪造的消息生成一个合法的HMAC值。在爬虫逆向中,我们面对HMAC加密的参数,核心任务往往不是破解密钥(这在密码学上是困难的),而是定位到前端JavaScript或客户端代码中生成这个HMAC的算法、密钥(或密钥的生成逻辑)以及消息的拼接规则。这就像在迷宫里寻找拼图,需要耐心、技巧和对前端执行逻辑的深刻理解。

2. HMAC加密的核心原理与算法拆解

要逆向,必须先理解正向流程。HMAC的生成过程是一个标准化的“配方”,理解了它,你才知道该从哪里下手去逆向分析。

2.1 HMAC的标准计算流程

HMAC的计算公式可以简洁地表示为: HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m)) 。这个公式看起来有点吓人,我们把它拆解成一步步可操作的过程:

  1. 密钥处理 :首先处理密钥 K

    • 如果密钥长度比哈希函数要求的块长度(Block Size,例如SHA-256是64字节)长,则先对密钥 K 进行一次哈希运算 H(K) ,将其结果作为新的密钥。
    • 如果密钥长度比块长度短,则在末尾补零(0x00)直到达到块长度。处理后的密钥我们记为 K'
  2. 内外填充 :定义两个固定的填充常量:

    • ipad (inner pad):将字节 0x36 重复块长度次。
    • opad (outer pad):将字节 0x5C 重复块长度次。
    • 然后,分别将 K' ipad opad 进行按位异或(XOR)操作,得到 K' ⊕ ipad K' ⊕ opad
  3. 两次哈希计算

    • 内哈希 :计算 H((K' ⊕ ipad) || m) 。这里 || 表示拼接。先将 K' ⊕ ipad 与原始消息 m 拼接起来,然后对整个拼接后的数据进行一次哈希运算。
    • 外哈希 :计算 H((K' ⊕ opad) || 内哈希结果) 。将 K' ⊕ opad 与上一步得到的内哈希结果(二进制格式)拼接,再进行一次哈希运算。最终的结果就是HMAC值。

注意 :在实际的JavaScript或Python库(如CryptoJS、hashlib)调用中,这些底层步骤都被封装好了。你只需要关心三个输入: 密钥 消息 哈希算法 。但理解这个过程,能帮助你在逆向时,识别出代码中是否进行了自定义的、非标准的HMAC变种,这是关键。

2.2 常见哈希算法在HMAC中的应用

HMAC本身是一个框架,它可以搭载不同的哈希函数。爬虫逆向中常见的搭配有:

  • HMAC-MD5 :较旧,强度较低,但仍有部分老系统使用。输出128位(16字节)十六进制字符串。
  • HMAC-SHA1 :曾经广泛应用,目前安全性已不被推荐用于新系统,但存量系统很多。输出160位(20字节)。
  • HMAC-SHA256 当前最主流、最推荐的选择 。输出256位(32字节),在安全性和性能上有很好的平衡。你遇到的大多数 sign 参数很可能就是HMAC-SHA256。
  • HMAC-SHA384/SHA512 :强度更高,用于对安全性要求极高的场景。
  • HMAC-SM3 :中国国家密码管理局发布的商用密码哈希算法,在国密标准要求的场景中(如一些国内金融、政务应用)必须使用。输出长度256位。

在逆向时,你需要通过观察HMAC输出值的长度(通常是十六进制或Base64编码后的长度),或直接追踪代码中调用的API(如 CryptoJS.HmacSHA256 crypto.createHmac('sha256', key) )来判断具体使用的算法。

2.3 密钥(Key)与消息(Message)的奥秘

这是逆向工程中最具挑战性的部分,也是决定成败的关键。

  • 密钥(Key)

    • 固定密钥 :最简单的情况。密钥是一个硬编码在客户端(如JS、APP)里的字符串。通过搜索关键词(如 secret key salt )、分析网络请求初始化过程或反编译APP,有可能直接找到。
    • 动态密钥 :密钥不是固定的,可能由其他参数计算得出。例如,密钥 = MD5(时间戳 + 固定盐值) ,或者密钥是从服务器下发的某个 token 衍生而来。这就需要你逆向出密钥本身的生成逻辑。
    • 密钥混淆 :密钥可能被编码(如Base64)、加密或拆分成多个部分,散布在代码的不同位置,需要拼接或解密后才能使用。
  • 消息(Message)

    • 消息的拼接规则 :服务器和客户端必须按照完全相同的顺序和格式拼接数据,才能得到相同的HMAC。常见的拼接方式有:
      • 按参数名ASCII排序 :将所有待签名的参数( key=value 形式)按参数名的字典序排序,然后用 & 或空字符串连接。例如 a=1&b=2&c=3
      • 固定模板 :使用类似 {timestamp}{path}{body} 的模板进行拼接。
      • JSON字符串 :将参数对象序列化为紧凑(无空格)的JSON字符串作为消息。
      • 包含特定分隔符 :如 | # \n 等。
    • 哪些参数参与签名? 并非所有请求参数都会参与HMAC计算。通常, sign signature 参数本身不参与,而 timestamp nonce (随机数)、 path (API路径)、请求体(body)等核心参数会参与。需要通过测试(增删改参数观察 sign 是否变化)或静态分析来确定。

逆向的核心工作,就是精确地复现出 “用什么样的密钥,对什么样的消息,使用哪种哈希算法,最终生成了那个HMAC签名” 这一完整链条。

3. 逆向实战:定位与解析HMAC生成逻辑

理论说再多,不如动手干。下面我们模拟一个典型的爬虫逆向HMAC的场景,假设目标网站对某个查询API的请求使用了 x-sign 参数进行保护。

3.1 初步侦察与信息收集

首先,用浏览器开发者工具(F12)抓取目标请求。假设我们抓到一个POST请求:

POST /api/v1/search HTTP/1.1
Host: target.com
Content-Type: application/json

{
    "keyword": "手机",
    "page": 1,
    "timestamp": 1687854321000,
    "nonce": "a1b2c3d4",
    "x-sign": "f7a3e2c1b8d94a5f0123e456789abcde"
}

观察发现,除了业务参数 keyword page ,还有 timestamp nonce x-sign x-sign 很可能就是HMAC签名。

第一步:测试参数敏感性。

  1. 修改 keyword 为“电脑”,重新发送请求(使用工具如Postman或Copy as cURL),观察 x-sign 是否变化。如果变化,说明 keyword 参与了签名。
  2. 修改 timestamp 的值, x-sign 几乎一定会变。
  3. 尝试删除 nonce 参数,看服务器是否返回“签名错误”。如果是,则 nonce 参与签名。
  4. 尝试在请求中添加一个无关参数 test: 1 ,如果签名依然有效,说明签名时可能只选取了特定参数;如果失效,说明所有参数(或除 x-sign 外的所有参数)都参与了签名。

通过这一步,我们可以初步确定参与签名的参数范围。

3.2 静态代码分析与关键点搜索

接下来,在开发者工具的 Sources 网络请求的Initiator调用栈 中,寻找生成这个请求的JavaScript代码。

搜索关键词

  1. 直接搜索签名名 :在JS文件(.js)或HTML内联脚本中,全局搜索 x-sign sign signature
  2. 搜索HMAC相关函数 :搜索 Hmac createHmac CryptoJS.Hmac sha256 md5 encrypt hash
  3. 搜索可能的密钥名 :搜索 secret key appKey appSecret salt token (有时token用作密钥的一部分)。
  4. 搜索参数拼接逻辑 :搜索 sort join & JSON.stringify Object.keys 等可能与消息拼接相关的代码。

假设我们搜索 HmacSHA256 ,找到了一段疑似代码:

function generateSign(params) {
    var secretKey = window._globalConfig.apiSecret; // 密钥可能在这里
    // 确定密钥来源
    var sortedKeys = Object.keys(params).sort();
    var messageParts = [];
    for (var i = 0; i < sortedKeys.length; i++) {
        var key = sortedKeys[i];
        if (key !== 'x-sign') { // 排除签名自身
            messageParts.push(key + '=' + params[key]);
        }
    }
    var message = messageParts.join('&');
    // 使用CryptoJS库
    var hash = CryptoJS.HmacSHA256(message, secretKey);
    return CryptoJS.enc.Hex.stringify(hash); // 转换为十六进制字符串
}

这是一个非常清晰的例子。它告诉我们:

  • 密钥 :来自 window._globalConfig.apiSecret 。我们需要在代码其他地方找到这个 _globalConfig 是如何被赋值的。可能是在另一个JS文件,也可能是在HTML页面初始化时由服务器注入。
  • 消息拼接规则 :将所有参数(除了 x-sign )按参数名排序,转换成 key=value 形式,再用 & 连接。
  • 算法 :HMAC-SHA256。
  • 输出格式 :十六进制字符串。

3.3 动态调试与逻辑验证

找到代码不等于万事大吉,我们需要验证其正确性,并获取运行时真实的密钥值。

  1. 下断点 :在 generateSign 函数入口或 var secretKey = ... 这一行设置断点。
  2. 触发请求 :在网页上进行操作,触发那个需要签名的API请求。
  3. 观察变量 :当断点触发时,在调试器的 Scope Console 中,查看 secretKey params message 的值。
    • 记录下 secretKey 的真实值。
    • 观察 params 对象是否与我们抓包看到的请求参数一致。
    • 查看拼接后的 message 字符串,确认其格式是否符合预期。
  4. 计算验证 :在调试器的Console中,我们可以手动调用 CryptoJS.HmacSHA256(message, secretKey) 并转换为十六进制,将结果与请求头中的 x-sign 对比。如果一致,恭喜你,逆向成功!

实操心得 :很多时候,代码会被混淆或压缩。函数名可能变成 a0b1c2 ,变量名也是单字母。这时候,关键词搜索可能失效。你需要:

  • 关注网络请求发起前的调用栈 ,从入口点一步步跟进去。
  • 留意字符串常量 ,即使变量名被混淆,像 & = x-sign sha256 这样的字符串常量通常不会被混淆,它们是重要的路标。
  • 使用“Hook”技术 :在控制台重写关键函数,如 CryptoJS.HmacSHA256 ,让其打印出输入参数和输出结果。这是一种非常高效的动态追踪手段。
    var originalHmac = CryptoJS.HmacSHA256;
    CryptoJS.HmacSHA256 = function(message, key) {
        console.log('HMAC Message:', message);
        console.log('HMAC Key:', key);
        var result = originalHmac(message, key);
        console.log('HMAC Result:', CryptoJS.enc.Hex.stringify(result));
        return result;
    };
    

4. 不同场景下的HMAC逆向策略与工具

不同的客户端环境,逆向策略有所不同。

4.1 Web端(JavaScript)逆向

这是最常见的情况。除了上述的浏览器开发者工具调试法,还有以下高级手段:

  • AST(抽象语法树)反混淆 :对于严重混淆的代码,可以使用像 ast-explorer jsnice 这样的在线工具,或本地库如 babel esprima 进行解析、反混淆和格式化,让代码恢复一定的可读性。
  • 全局变量监控 :如果密钥保存在全局变量(如 window.appSecret ),可以在页面加载早期执行脚本监控其变化。
  • 请求拦截与重放 :使用爬虫框架(如Puppeteer、Playwright)自动化浏览器,在页面上下文中直接执行JavaScript代码来获取密钥或计算签名,完全模拟浏览器行为。

4.2 移动端(Android/iOS APP)逆向

APP中的HMAC逻辑通常更隐蔽,但思路相通。

  • Android
    • 反编译 :使用 jadx-gui apktool 反编译APK,得到Smali或Java代码。
    • 搜索关键词 :在Java代码中搜索 Hmac Mac.getInstance SecretKeySpec sign 等。
    • 动态调试 :使用 Frida 框架进行Hook。可以Hook javax.crypto.Mac 类的 doFinal 方法,直接打印出密钥、消息和结果。这是目前最强大的逆向手段之一。
    // Frida脚本示例:Hook Android的Mac.doFinal
    Java.perform(function() {
        var Mac = Java.use('javax.crypto.Mac');
        Mac.doFinal.overload('[B').implementation = function(data) {
            console.log('Mac.doFinal called');
            console.log('Algorithm:', this.getAlgorithm());
            // 需要先Hook update方法或getInstance来获取密钥,这里简化
            var result = this.doFinal(data);
            console.log('Input data (hex):', bytesToHex(data));
            console.log('Output (hex):', bytesToHex(result));
            return result;
        };
    });
    
  • iOS
    • 砸壳与反编译 :对IPA包进行砸壳后,使用 Hopper Disassembler IDA Pro 进行静态分析。
    • 搜索字符串 :在二进制文件中搜索 HMAC CCHmac CommonCrypto 等字符串。
    • 动态调试 :同样可以使用 Frida lldb 进行调试,Hook CCHmac 等函数。

4.3 常用工具链总结

场景 主要工具 用途
Web分析 Chrome/Firefox DevTools 网络抓包、JS调试、断点、调用栈分析
JS调试 Chrome DevTools, Fiddler/Charles 高级断点、条件断点、变量监控
JS Hook 浏览器Console, Tampermonkey 重写函数,打印关键参数
反混淆 ast-explorer.net, jsnice.org 还原混淆的JS代码结构
APP逆向(Android) jadx-gui, apktool, Frida 反编译APK,静态分析,动态Hook
APP逆向(iOS) Hopper/IDA, Frida, lldb 静态反汇编,动态调试
密码学验证 Python hashlib/hmac库, Node.js crypto 验证逆向出的算法和密钥是否正确

5. 常见问题、坑点与实战技巧实录

在实际逆向过程中,你会遇到各种各样的问题。下面是我踩过的一些坑和总结的技巧。

5.1 签名总是无效?排查清单

当你按照分析出的逻辑复现签名,但服务器始终返回无效时,请按以下顺序排查:

  1. 编码问题(最常见)
    • 消息字符串编码 :密钥和消息在计算前,是作为字符串还是字节数组?在Python中, hmac.new(key.encode('utf-8'), msg.encode('utf-8'), 'sha256') hmac.new(key, msg, 'sha256') (假设key和msg是bytes)结果天差地别。确保与目标环境(通常是JS)的编码一致。JS中字符串是UTF-16,但在与CryptoJS这样的库交互时,它通常将字符串当作UTF-8的字节序列来处理。 最稳妥的方式是,在调试器中直接查看参与计算的原始消息和密钥的字节表示。
    • 输出格式 :HMAC计算结果是二进制数据。对方使用的是十六进制(Hex)还是Base64?JS中 CryptoJS.enc.Hex.stringify(hash) 是Hex, CryptoJS.enc.Base64.stringify(hash) 是Base64。必须完全匹配。
  2. 参数顺序与空格
    • 确认参数排序规则是否正确。是升序还是降序?排序是基于参数名(key)还是 key=value 整个字符串?
    • 确认拼接时是否有空格、换行符。 a=1&b=2 a=1&b=2 (末尾有空格)的哈希值不同。
    • JSON消息体是否进行了“紧凑化”(去除空格和换行)?有些库 JSON.stringify(obj) 默认会带格式,而签名计算可能需要 JSON.stringify(obj, null, 0) 或手动去除空格。
  3. 密钥错误
    • 你找到的密钥真的是用来计算这个签名的吗?可能有多个密钥用于不同接口。
    • 密钥是否经过了预处理?比如先进行了一次Base64解码,或MD5哈希后才作为HMAC的密钥。
    • 密钥是否是动态的?可能依赖于 sessionId 、某个一次性令牌(token)或时间戳。
  4. 算法错误
    • 你确定是SHA256吗?会不会是SHA1甚至MD5?通过输出长度和代码调用双重确认。
    • 是否存在自定义的哈希算法或魔改的标准算法?(较少见,但存在)

5.2 进阶技巧与对抗策略

  1. 环境依赖检测的绕过 :有些前端代码会检测 window document 或浏览器特有对象,如果不在浏览器环境(比如直接在Node.js里跑)就不执行或返回假数据。在Node.js中模拟这些全局变量,或使用无头浏览器(Puppeteer)来执行签名函数。
  2. “盐值”(Salt)的混淆 :密钥可能不是直接给出的,而是 固定盐值 + 动态变量 组合后再哈希得到。这个动态变量可能藏在Cookie、LocalStorage或某个早期接口的返回里。需要梳理完整的数据流。
  3. WebAssembly(Wasm)加密 :为了提升安全性和性能,越来越多的关键加密逻辑被编译成Wasm模块。这增加了静态分析的难度。应对策略:
    • 在开发者工具的 Sources -> WebAssembly 调试器中下断点。
    • 使用工具如 wasm2c wasm-decompile 尝试反编译,但可读性较差。
    • 重点放在输入输出 :动态Hook调用Wasm函数的JavaScript接口,记录传入的参数和返回的结果。只要黑盒测试能复现,不一定需要完全理解内部逻辑。
  4. 请求链路追踪 :签名所需的密钥或参数,可能是在登录成功后,由服务器通过另一个接口下发的。你需要完整地模拟用户登录流程,保存登录后的 session cookies 以及服务器返回的 token secret ,用于后续请求的签名计算。

5.3 一个综合案例:带时间戳和随机数的签名

假设你逆向出的签名逻辑如下:

  1. 密钥: appSecret = "my_fixed_secret_123"
  2. 参与签名的参数:除 sign 外的所有请求参数,按参数名升序排列, key=value & 连接。
  3. 消息末尾额外拼接 &key=appSecret
  4. 对最终消息字符串进行 HMAC-SHA256 ,输出Hex。

那么对于一个请求 ?name=alice&age=20&timestamp=123456&nonce=abc ,签名过程是:

  1. 排序参数: age=20&name=alice&nonce=abc&timestamp=123456
  2. 拼接密钥: age=20&name=alice&nonce=abc&timestamp=123456&key=my_fixed_secret_123
  3. 计算HMAC-SHA256。

在Python中复现:

import hmac
import hashlib

def generate_sign(params, app_secret):
    # 1. 排序并拼接参数
    sorted_params = '&'.join([f'{k}={v}' for k, v in sorted(params.items())])
    # 2. 拼接密钥
    message = f'{sorted_params}&key={app_secret}'
    # 3. 计算HMAC-SHA256 (注意编码)
    key = app_secret.encode('utf-8')
    msg = message.encode('utf-8')
    hmac_obj = hmac.new(key, msg, hashlib.sha256)
    return hmac_obj.hexdigest()

# 测试
params = {'name': 'alice', 'age': '20', 'timestamp': '123456', 'nonce': 'abc'}
app_secret = 'my_fixed_secret_123'
sign = generate_sign(params, app_secret)
print(f'Generated Sign: {sign}')
# 将生成的sign与抓包得到的sign对比

逆向HMAC的过程,是一个典型的“侦察-分析-验证-复现”的工程循环。它没有一成不变的银弹,需要你对Web技术、编程语言、密码学基础以及调试工具都有所了解。最大的成就感,莫过于看到自己逆向出来的代码成功生成一个被服务器认可的签名,那一刻,所有的抠代码、下断点、查文档的辛苦都值了。记住,耐心和细致的观察力,是爬虫逆向工程师最重要的品质。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值