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)) 。这个公式看起来有点吓人,我们把它拆解成一步步可操作的过程:
-
密钥处理 :首先处理密钥
K。- 如果密钥长度比哈希函数要求的块长度(Block Size,例如SHA-256是64字节)长,则先对密钥
K进行一次哈希运算H(K),将其结果作为新的密钥。 - 如果密钥长度比块长度短,则在末尾补零(0x00)直到达到块长度。处理后的密钥我们记为
K'。
- 如果密钥长度比哈希函数要求的块长度(Block Size,例如SHA-256是64字节)长,则先对密钥
-
内外填充 :定义两个固定的填充常量:
-
ipad(inner pad):将字节0x36重复块长度次。 -
opad(outer pad):将字节0x5C重复块长度次。 - 然后,分别将
K'与ipad和opad进行按位异或(XOR)操作,得到K' ⊕ ipad和K' ⊕ opad。
-
-
两次哈希计算 :
- 内哈希 :计算
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)、加密或拆分成多个部分,散布在代码的不同位置,需要拼接或解密后才能使用。
- 固定密钥 :最简单的情况。密钥是一个硬编码在客户端(如JS、APP)里的字符串。通过搜索关键词(如
-
消息(Message) :
- 消息的拼接规则 :服务器和客户端必须按照完全相同的顺序和格式拼接数据,才能得到相同的HMAC。常见的拼接方式有:
- 按参数名ASCII排序 :将所有待签名的参数(
key=value形式)按参数名的字典序排序,然后用&或空字符串连接。例如a=1&b=2&c=3。 - 固定模板 :使用类似
{timestamp}{path}{body}的模板进行拼接。 - JSON字符串 :将参数对象序列化为紧凑(无空格)的JSON字符串作为消息。
- 包含特定分隔符 :如
|、#、\n等。
- 按参数名ASCII排序 :将所有待签名的参数(
- 哪些参数参与签名? 并非所有请求参数都会参与HMAC计算。通常,
sign或signature参数本身不参与,而timestamp、nonce(随机数)、path(API路径)、请求体(body)等核心参数会参与。需要通过测试(增删改参数观察sign是否变化)或静态分析来确定。
- 消息的拼接规则 :服务器和客户端必须按照完全相同的顺序和格式拼接数据,才能得到相同的HMAC。常见的拼接方式有:
逆向的核心工作,就是精确地复现出 “用什么样的密钥,对什么样的消息,使用哪种哈希算法,最终生成了那个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签名。
第一步:测试参数敏感性。
- 修改
keyword为“电脑”,重新发送请求(使用工具如Postman或Copy as cURL),观察x-sign是否变化。如果变化,说明keyword参与了签名。 - 修改
timestamp的值,x-sign几乎一定会变。 - 尝试删除
nonce参数,看服务器是否返回“签名错误”。如果是,则nonce参与签名。 - 尝试在请求中添加一个无关参数
test: 1,如果签名依然有效,说明签名时可能只选取了特定参数;如果失效,说明所有参数(或除x-sign外的所有参数)都参与了签名。
通过这一步,我们可以初步确定参与签名的参数范围。
3.2 静态代码分析与关键点搜索
接下来,在开发者工具的 Sources 或 网络请求的Initiator调用栈 中,寻找生成这个请求的JavaScript代码。
搜索关键词 :
- 直接搜索签名名 :在JS文件(.js)或HTML内联脚本中,全局搜索
x-sign、sign、signature。 - 搜索HMAC相关函数 :搜索
Hmac、createHmac、CryptoJS.Hmac、sha256、md5、encrypt、hash。 - 搜索可能的密钥名 :搜索
secret、key、appKey、appSecret、salt、token(有时token用作密钥的一部分)。 - 搜索参数拼接逻辑 :搜索
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 动态调试与逻辑验证
找到代码不等于万事大吉,我们需要验证其正确性,并获取运行时真实的密钥值。
- 下断点 :在
generateSign函数入口或var secretKey = ...这一行设置断点。 - 触发请求 :在网页上进行操作,触发那个需要签名的API请求。
- 观察变量 :当断点触发时,在调试器的
Scope或Console中,查看secretKey、params、message的值。- 记录下
secretKey的真实值。 - 观察
params对象是否与我们抓包看到的请求参数一致。 - 查看拼接后的
message字符串,确认其格式是否符合预期。
- 记录下
- 计算验证 :在调试器的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。可以Hookjavax.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进行调试,HookCCHmac等函数。
- 砸壳与反编译 :对IPA包进行砸壳后,使用
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 签名总是无效?排查清单
当你按照分析出的逻辑复现签名,但服务器始终返回无效时,请按以下顺序排查:
- 编码问题(最常见) :
- 消息字符串编码 :密钥和消息在计算前,是作为字符串还是字节数组?在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。必须完全匹配。
- 消息字符串编码 :密钥和消息在计算前,是作为字符串还是字节数组?在Python中,
- 参数顺序与空格 :
- 确认参数排序规则是否正确。是升序还是降序?排序是基于参数名(key)还是
key=value整个字符串? - 确认拼接时是否有空格、换行符。
a=1&b=2和a=1&b=2(末尾有空格)的哈希值不同。 - JSON消息体是否进行了“紧凑化”(去除空格和换行)?有些库
JSON.stringify(obj)默认会带格式,而签名计算可能需要JSON.stringify(obj, null, 0)或手动去除空格。
- 确认参数排序规则是否正确。是升序还是降序?排序是基于参数名(key)还是
- 密钥错误 :
- 你找到的密钥真的是用来计算这个签名的吗?可能有多个密钥用于不同接口。
- 密钥是否经过了预处理?比如先进行了一次Base64解码,或MD5哈希后才作为HMAC的密钥。
- 密钥是否是动态的?可能依赖于
sessionId、某个一次性令牌(token)或时间戳。
- 算法错误 :
- 你确定是SHA256吗?会不会是SHA1甚至MD5?通过输出长度和代码调用双重确认。
- 是否存在自定义的哈希算法或魔改的标准算法?(较少见,但存在)
5.2 进阶技巧与对抗策略
- 环境依赖检测的绕过 :有些前端代码会检测
window、document或浏览器特有对象,如果不在浏览器环境(比如直接在Node.js里跑)就不执行或返回假数据。在Node.js中模拟这些全局变量,或使用无头浏览器(Puppeteer)来执行签名函数。 - “盐值”(Salt)的混淆 :密钥可能不是直接给出的,而是
固定盐值 + 动态变量组合后再哈希得到。这个动态变量可能藏在Cookie、LocalStorage或某个早期接口的返回里。需要梳理完整的数据流。 - WebAssembly(Wasm)加密 :为了提升安全性和性能,越来越多的关键加密逻辑被编译成Wasm模块。这增加了静态分析的难度。应对策略:
- 在开发者工具的 Sources -> WebAssembly 调试器中下断点。
- 使用工具如
wasm2c或wasm-decompile尝试反编译,但可读性较差。 - 重点放在输入输出 :动态Hook调用Wasm函数的JavaScript接口,记录传入的参数和返回的结果。只要黑盒测试能复现,不一定需要完全理解内部逻辑。
- 请求链路追踪 :签名所需的密钥或参数,可能是在登录成功后,由服务器通过另一个接口下发的。你需要完整地模拟用户登录流程,保存登录后的
session、cookies以及服务器返回的token或secret,用于后续请求的签名计算。
5.3 一个综合案例:带时间戳和随机数的签名
假设你逆向出的签名逻辑如下:
- 密钥:
appSecret = "my_fixed_secret_123" - 参与签名的参数:除
sign外的所有请求参数,按参数名升序排列,key=value用&连接。 - 消息末尾额外拼接
&key=appSecret。 - 对最终消息字符串进行 HMAC-SHA256 ,输出Hex。
那么对于一个请求 ?name=alice&age=20×tamp=123456&nonce=abc ,签名过程是:
- 排序参数:
age=20&name=alice&nonce=abc×tamp=123456 - 拼接密钥:
age=20&name=alice&nonce=abc×tamp=123456&key=my_fixed_secret_123 - 计算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技术、编程语言、密码学基础以及调试工具都有所了解。最大的成就感,莫过于看到自己逆向出来的代码成功生成一个被服务器认可的签名,那一刻,所有的抠代码、下断点、查文档的辛苦都值了。记住,耐心和细致的观察力,是爬虫逆向工程师最重要的品质。

164

被折叠的 条评论
为什么被折叠?



