ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

WebAssembly逆向实战:荔枝网Wasm加密参数分析

WebAssembly逆向实战:荔枝网Wasm加密参数分析

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 -j8

2.2 获取目标Wasm模块

分析荔枝网的请求流程,可以发现关键Wasm模块是通过JS动态加载的。我们可以通过以下方法提取:

  1. 使用Chrome开发者工具的Network面板,过滤wasm类型请求
  2. 找到对应的wasm文件,右键选择"Copy as cURL"
  3. 在终端执行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.wat

WAT文件是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时,需要注意以下几点:

  1. 选择"WebAssembly"作为文件类型
  2. 在"Load a new file"对话框中勾选"Manual load"
  3. 分析过程中重点关注导出函数(Exports)和导入函数(Imports)

在荔枝网的案例中,IDA成功识别出了主要的加密函数,其控制流图显示采用了多层嵌套的循环结构,这是典型的块加密算法特征。通过交叉引用分析,可以定位到核心加密逻辑位于function_123这个子函数中。

4. 动态调试技术

4.1 Chrome DevTools调试

现代浏览器内置了Wasm调试支持:

  1. 在Sources面板找到对应的wasm文件
  2. 点击"{}"按钮格式化代码
  3. 在关键位置设置断点
  4. 使用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加密方案:

  1. 密钥扩展过程使用了自定义的S盒
  2. 初始向量(IV)由时间戳和随机数组合生成
  3. 加密前会对输入数据做PKCS#7填充

关键的算法特征包括:

  • 9轮加密循环
  • 每轮都进行SubBytes、ShiftRows、MixColumns操作
  • 最终轮省略MixColumns

5.2 密钥提取

密钥被隐藏在Wasm模块的data段中,通过以下步骤可以提取:

  1. 在IDA中找到__data_end和__data_start符号
  2. 定位到密钥数据区域(通常有连续的256字节非零数据)
  3. 结合交叉引用分析密钥使用位置

荔枝网的实际密钥采用了动态生成策略,核心密钥通过以下方式派生:

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模块实现了多种反调试技术:

  1. 执行时间检测:通过performance.now()差值判断是否在调试
const start = performance.now(); // 关键加密操作 const end = performance.now(); if (end - start > 100) { // 疑似调试,触发异常 }
  1. 内存校验:定期检查关键内存区域是否被修改
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 对抗方案

针对这些反调试措施,可以采取以下对策:

  1. 时间检测绕过:重写performance.now()方法
const originalNow = performance.now; performance.now = function() { return originalNow.call(performance) * 0.1; // 缩小时间差 };
  1. 内存校验绕过:在内存校验函数中设置断点,修改返回值
const wasmInstance = /* 获取实例 */; const originalCheck = wasmInstance.exports.check_memory_integrity; wasmInstance.exports.check_memory_integrity = function() { return 0x12345678; // 固定返回预期校验值 };
  1. 控制流混淆:使用Binaryen工具对关键跳转进行混淆
wasm-opt --flatten --rereloop crypto.wasm -o crypto_obfuscated.wasm

7. 完整逆向案例

7.1 参数定位

荔枝网的音频接口请求示例如下:

GET /api/audios/123?signature=7a8d3f9e...&t=1630000000 HTTP/1.1

通过分析发现:

  • signature参数由Wasm模块生成
  • t参数是当前时间戳,参与签名计算
  • 请求体需要包含设备信息哈希

7.2 签名算法还原

完整的签名生成流程如下:

  1. 拼接设备ID和时间戳
  2. 计算SHA-256哈希
  3. 使用改良AES加密哈希值
  4. 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 逆向经验

  1. 交叉验证:静态分析与动态调试必须结合使用,单一方法容易得出错误结论
  2. 关注边界:Wasm模块与JS的交互边界是最佳切入点
  3. 内存分析:Wasm的线性内存布局通常很有规律,关键数据往往集中在特定区域
  4. 工具链:建立自己的Wasm逆向工具包,封装常用操作

8.2 优化建议

对于希望加强Wasm保护效果的开发者,我建议:

  1. 控制流平坦化:使用工具打乱函数控制流
wasm-opt --flatten --rereloop module.wasm -o protected.wasm
  1. 自定义指令集:通过Wasm的SIMD指令实现混淆
(func $custom_op (param $a i32) (result i32) local.get $a i32.const 0x55AA55AA i32.xor i32.const 0xFFFFFFFF i32.and)
  1. 动态加载:将关键逻辑拆分为多个模块,运行时动态加载

  2. 完整性校验:定期检查Wasm模块的代码段哈希

Wasm逆向是一个需要耐心和技术积累的过程,荔枝网的案例展示了现代Web应用如何利用Wasm增强安全性。随着WebAssembly技术的不断发展,相信会出现更多创新的保护方案,这也促使我们安全研究者不断更新技术手段。

返回列表