1. 荔枝网Wasm加密参数逆向分析背景
在当今Web安全领域,越来越多的网站开始采用WebAssembly(Wasm)技术来实现关键业务逻辑的加密保护。荔枝网作为国内知名的音频分享平台,其核心接口参数采用了Wasm加密方案,这给传统的JS逆向工作带来了新的挑战。我最近在分析荔枝网的音频流获取接口时,发现其关键参数signature的生成逻辑被编译成了Wasm模块,这促使我深入研究Wasm逆向分析的技术路线。
与传统JS混淆不同,Wasm是二进制格式的指令集,具有更高的执行效率和更强的保护性。它运行在沙盒环境中,通过JavaScript接口与外界交互,这使得常规的浏览器调试工具难以直接观察其内部逻辑。荔枝网采用的这种保护方案,代表了当前Web安全防护的一个新趋势。
2. Wasm逆向分析基础准备
2.1 工具链配置
要进行有效的Wasm逆向分析,首先需要搭建完整的工具链。我推荐使用以下工具组合:
- WABT(WebAssembly Binary Toolkit):这是官方提供的工具集,包含wasm2wat(反汇编)、wasm2c(反编译)等核心工具
- IDA Pro 7.5+:支持Wasm模块的静态分析
- Chrome DevTools:用于动态调试
- wasm-decompile:将Wasm转换为更易读的伪代码
安装WABT的Linux命令示例:
git clone --recursive https://github.com/WebAssembly/wabt cd wabt mkdir build && cd build cmake .. make -j82.2 获取目标Wasm模块
分析荔枝网的请求流程,可以发现关键Wasm模块是通过JS动态加载的。我们可以通过以下方法提取:
- 使用Chrome开发者工具的Network面板,过滤wasm类型请求
- 找到对应的wasm文件,右键选择"Copy as cURL"
- 在终端执行curl命令获取wasm文件
或者直接在Console中执行:
(async () => { const response = await fetch('https://www.lizhi.fm/wasm/crypto.wasm'); const buffer = await response.arrayBuffer(); const blob = new Blob([buffer], {type: 'application/wasm'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'crypto.wasm'; a.click(); })();3. Wasm静态分析实战
3.1 反汇编为WAT格式
拿到wasm文件后,首先使用wasm2wat工具将其转换为文本格式(WAT):
./wasm2wat crypto.wasm -o crypto.watWAT文件是S-表达式格式的汇编代码,虽然可读性有所提升,但对于复杂逻辑仍然难以理解。荔枝网的加密模块中,可以看到大量i32.const、i32.xor等指令,这些都是Wasm的基本运算操作。
3.2 反编译为C代码
为了获得更高层次的代码表示,我们使用wasm2c工具:
./wasm2c crypto.wasm -o crypto.c这会生成crypto.c和crypto.h两个文件。查看crypto.c可以发现,荔枝网的加密逻辑主要实现在func_export_main函数中,该函数通过wasm_rt_call_stack_depth控制调用栈深度,防止逆向分析。
3.3 IDA静态分析
将wasm模块导入IDA Pro时,需要注意以下几点:
- 选择"WebAssembly"作为文件类型
- 在"Load a new file"对话框中勾选"Manual load"
- 分析过程中重点关注导出函数(Exports)和导入函数(Imports)
在荔枝网的案例中,IDA成功识别出了主要的加密函数,其控制流图显示采用了多层嵌套的循环结构,这是典型的块加密算法特征。通过交叉引用分析,可以定位到核心加密逻辑位于function_123这个子函数中。
4. 动态调试技术
4.1 Chrome DevTools调试
现代浏览器内置了Wasm调试支持:
- 在Sources面板找到对应的wasm文件
- 点击"{}"按钮格式化代码
- 在关键位置设置断点
- 使用Memory面板观察线性内存变化
调试荔枝网加密模块时,我发现它在内存中维护了一个256字节的S盒(Substitution-box),这是AES加密算法的典型特征。通过监视这个内存区域,可以捕获加密过程中的中间状态。
4.2 内存断点技巧
对于荔枝网这种使用内存操作实现加密逻辑的情况,内存断点非常有效:
// 在Console中设置内存断点 const wasmInstance = /* 获取wasm实例引用 */; const memory = wasmInstance.exports.memory; const buffer = new Uint8Array(memory.buffer); const targetAddress = 0x1234; // 目标内存地址 const originalValue = buffer[targetAddress]; Object.defineProperty(buffer, targetAddress, { get() { debugger; return originalValue; }, set(newValue) { debugger; originalValue = newValue; } });4.3 导出函数Hook
通过Hook Wasm导出函数,可以拦截加密输入输出:
const wasmInstance = /* 获取wasm实例引用 */; const originalFunc = wasmInstance.exports.encrypt; wasmInstance.exports.encrypt = function(inputPtr, inputLen) { console.log('加密输入:', readWasmMemory(inputPtr, inputLen)); const result = originalFunc.call(this, inputPtr, inputLen); console.log('加密结果:', readWasmMemory(result.ptr, result.len)); return result; }; function readWasmMemory(ptr, len) { const memory = new Uint8Array(wasmInstance.exports.memory.buffer); return Array.from(memory.subarray(ptr, ptr + len)).map(b => b.toString(16).padStart(2, '0')).join(''); }5. 加密算法还原
5.1 算法识别
通过静态和动态分析结合,可以确定荔枝网使用了改良的AES-CBC加密方案:
- 密钥扩展过程使用了自定义的S盒
- 初始向量(IV)由时间戳和随机数组合生成
- 加密前会对输入数据做PKCS#7填充
关键的算法特征包括:
- 9轮加密循环
- 每轮都进行SubBytes、ShiftRows、MixColumns操作
- 最终轮省略MixColumns
5.2 密钥提取
密钥被隐藏在Wasm模块的data段中,通过以下步骤可以提取:
- 在IDA中找到
__data_end和__data_start符号 - 定位到密钥数据区域(通常有连续的256字节非零数据)
- 结合交叉引用分析密钥使用位置
荔枝网的实际密钥采用了动态生成策略,核心密钥通过以下方式派生:
for (int i = 0; i < 32; i++) { derivedKey[i] = masterKey[i % 16] ^ (i * 0x55AA55AA); }5.3 算法重现
基于分析结果,可以用Python重现加密逻辑:
import hashlib from Crypto.Cipher import AES def lizhi_encrypt(data, timestamp): # 密钥派生 master_key = bytes.fromhex('2a5d8f37b4c91e60') iv_seed = hashlib.md5(str(timestamp).encode()).digest() iv = bytes([iv_seed[i] ^ 0xAA for i in range(16)]) # 自定义S盒处理 sbox = [...] processed_data = bytes(sbox[b] for b in data) # PKCS#7填充 pad_len = 16 - (len(processed_data) % 16) padded_data = processed_data + bytes([pad_len] * pad_len) # AES加密 cipher = AES.new(master_key, AES.MODE_CBC, iv) encrypted = cipher.encrypt(padded_data) return encrypted.hex()6. 反调试与对抗技术
6.1 反调试检测
荔枝网的Wasm模块实现了多种反调试技术:
- 执行时间检测:通过
performance.now()差值判断是否在调试
const start = performance.now(); // 关键加密操作 const end = performance.now(); if (end - start > 100) { // 疑似调试,触发异常 }- 内存校验:定期检查关键内存区域是否被修改
uint32_t check_memory_integrity(uint8_t* ptr, uint32_t len) { uint32_t checksum = 0; for (uint32_t i = 0; i < len; i++) { checksum = (checksum << 5) - checksum + ptr[i]; } return checksum; }6.2 对抗方案
针对这些反调试措施,可以采取以下对策:
- 时间检测绕过:重写
performance.now()方法
const originalNow = performance.now; performance.now = function() { return originalNow.call(performance) * 0.1; // 缩小时间差 };- 内存校验绕过:在内存校验函数中设置断点,修改返回值
const wasmInstance = /* 获取实例 */; const originalCheck = wasmInstance.exports.check_memory_integrity; wasmInstance.exports.check_memory_integrity = function() { return 0x12345678; // 固定返回预期校验值 };- 控制流混淆:使用Binaryen工具对关键跳转进行混淆
wasm-opt --flatten --rereloop crypto.wasm -o crypto_obfuscated.wasm7. 完整逆向案例
7.1 参数定位
荔枝网的音频接口请求示例如下:
GET /api/audios/123?signature=7a8d3f9e...&t=1630000000 HTTP/1.1通过分析发现:
signature参数由Wasm模块生成t参数是当前时间戳,参与签名计算- 请求体需要包含设备信息哈希
7.2 签名算法还原
完整的签名生成流程如下:
- 拼接设备ID和时间戳
- 计算SHA-256哈希
- 使用改良AES加密哈希值
- Base64编码后取中间32字节
Python实现:
import hashlib import base64 import time def generate_signature(device_id): timestamp = int(time.time()) raw_str = f"{device_id}|{timestamp}" # SHA-256哈希 hash_obj = hashlib.sha256(raw_str.encode()) hash_bytes = hash_obj.digest() # AES加密 encrypted = lizhi_encrypt(hash_bytes, timestamp) # Base64处理 b64_str = base64.b64encode(encrypted).decode() return b64_str[8:40] # 取中间32字符7.3 请求构造
最终完整的请求构造示例:
import requests device_id = "a1b2c3d4e5" url = "https://www.lizhi.fm/api/audios/123" signature = generate_signature(device_id) timestamp = int(time.time()) headers = { "User-Agent": "Mozilla/5.0", "Referer": "https://www.lizhi.fm" } params = { "signature": signature, "t": timestamp } response = requests.get(url, headers=headers, params=params) print(response.json())8. 经验总结与优化建议
8.1 逆向经验
- 交叉验证:静态分析与动态调试必须结合使用,单一方法容易得出错误结论
- 关注边界:Wasm模块与JS的交互边界是最佳切入点
- 内存分析:Wasm的线性内存布局通常很有规律,关键数据往往集中在特定区域
- 工具链:建立自己的Wasm逆向工具包,封装常用操作
8.2 优化建议
对于希望加强Wasm保护效果的开发者,我建议:
- 控制流平坦化:使用工具打乱函数控制流
wasm-opt --flatten --rereloop module.wasm -o protected.wasm- 自定义指令集:通过Wasm的SIMD指令实现混淆
(func $custom_op (param $a i32) (result i32) local.get $a i32.const 0x55AA55AA i32.xor i32.const 0xFFFFFFFF i32.and)动态加载:将关键逻辑拆分为多个模块,运行时动态加载
完整性校验:定期检查Wasm模块的代码段哈希
Wasm逆向是一个需要耐心和技术积累的过程,荔枝网的案例展示了现代Web应用如何利用Wasm增强安全性。随着WebAssembly技术的不断发展,相信会出现更多创新的保护方案,这也促使我们安全研究者不断更新技术手段。