Ai还原x-zse-96 vmp纯算

介绍
私信强校验
过程
总体情况
耗时
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 合计 | ~4.5 小时 |
成本
本次任务横跨两个会话(第一会话耗尽上下文,触发多次 Output token limit hit)。
|
|
|
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 总计(USD) | ~$3.30 |
| 总计(RMB,汇率 7.25) | ~¥24 |
❝
注:以上为 API 直接调用估算。Claude Code 订阅制下实际扣费方式不同,仅供参考。
工具与流程
工具
https://github.com/715494637/reverse-skill/
-
jsr-reverse:JS 逆向工作流主技能,负责阶段调度(intake → locate → recover → runtime → validation)
本次任务主要依赖 js-reverse MCP(浏览器自动化逆向工具集),核心工具:
|
|
|
|---|---|
list_network_requests |
|
get_request_initiator |
|
set_breakpoint_on_text |
|
evaluate_script |
|
get_script_source |
|
流程
输入:URL路径 + opts(d_c0 / authId / body 等)─── Step 1:构造 source 字符串 ───────────────────────────────source = "101_3_3.0" + "+" + urlPath [+ "+" + d_c0] [+ "+" + body] ...─── Step 2:MD5 ──────────────────────────────────────────────md5hex = MD5(source) → 32位小写十六进制字符串─── Step 3:构造 block(16字节)────────────────────────────────block[0] = randByte() ← Math.floor(random()*127) 映射到位变换置换表block[1] = 0x15block[2~15] = md5hex[0~13].charCode XOR K[0~13] K = [0x13,0x1a,0x1f,0x19,0x4c,0x1d,0x4e,0x1b,0x1f,0x4f,0x1a,0x1b,0x4e,0x1d]─── Step 4:SM4 加密 block → IV(16字节)───────────────────────IV = SM4_ENC(block) 使用经 JSVMP 变换后的 ZK(32个round key,非源码中的原始值)─── Step 5:构造明文 plaintext(32字节)────────────────────────plaintext = md5hex[14~31].charCode (18字节) + [0x0E] × 14 (PKCS7 padding,pad值=14)─── Step 6:SM4-CBC 加密 → cipher(32字节)─────────────────────cipher = SM4_CBC(plaintext, IV) C1 = SM4_ENC(plaintext[0~15] XOR IV) C2 = SM4_ENC(plaintext[16~31] XOR C1)─── Step 7:拼接 X(48字节)───────────────────────────────────X = reverse(cipher) ++ reverse(IV) = [cipher[31]..cipher[0], IV[15]..IV[0]]─── Step 8:位混洗 encode3(16组 × 3字节)──────────────────────对 X 每3字节 (b0,b1,b2) 执行: out[0] = ((b0&0x3F)<<2) | ((b1>>2)&0x3) out[1] = ((b1&0x3)<<6) | ((b0>>6)<<4) | ((b2&0x3)<<2) | (b1>>6) out[2] = ((b1&0x30)<<2) | ((b2>>2)&0x3F)─── Step 9:XOR 固定常量(48字节)─────────────────────────────CONST = [232,0,0,2,128,192,0,8,14,0,0,0] × 4out48[i] ^= CONST[i]─── Step 10:自定义 Base64 编码(48字节 → 64字符)──────────────ALPHA = "6fpLRqJO8M/c3jnYxFkUVC4ZIG12SiH=5v0mXDazWBTsuw7QetbKdoPyAl+hN9rgE"标准 base64 分组(每3字节→4字符),字符集替换为 ALPHA─── 输出 ─────────────────────────────────────────────────────x-zse-96 = "2.0_" + base64_with_ALPHA(out48) (总长 68 字符)
详细
定位入口:发现 JSVMP
过程:用 MCP 抓到请求,找到 sign() 函数。源码里看到熟悉的结构:
functionl() { ... }l.prototype.O = function(A,C,s){ for(...) switch(this.T) { case27: ... } }
这是标准 JSVMP(JS 虚拟机混淆),字节码驱动的解释器,直接阅读无意义。
第一个坑:用户拒绝了 JSVMP 方案。最初输出了一个”纯算法”实现,但实际上还是把 JSVMP 的字节码和调度器原封不动搬过去了——用户一眼看出 function l() 和 l.prototype.O 仍然存在,要求真正的纯算法。这逼迫转向完全逆向内部逻辑。
插桩策略:绕过 JSVMP 黑盒
修改 zhihu_sign.js 最后一行,将 __g 导出:
module.exports = { sign, _g: __g };
然后通过 patch __g.r(SM4 单块加密)和 __g.x(SM4 CBC)在 Node.js 本地捕获每次调用的输入输出,把 JSVMP 当黑盒驱动。这是核心策略。
第二个坑:以为输出是 IV || C1 || C2
最初假设签名的 48 字节就是 IV ++ C1 ++ C2 直接 base64,验证后不符。
排查过程:写 test_layout.js,把 cipher 置零,看哪些输出字节变化;再把 IV 置零看哪些字节变化。发现:
-
cipher 影响 out[0~31] -
IV 影响 out[31~47] -
当两者都置零时,out 仍有非零值(一个固定常量)
说明不是简单拼接,有额外变换。
逆向位混洗公式(encode3)
写 test_layout2.js,对 cipher 每个字节的每个 bit 分别置 1,记录哪个 out 字节发生 delta。
比如 cipher[31] bit0 → out[0] 变化 +4(即 bit2)。把 256 个 bit 的 delta 全部映射出来,拼出公式:
out[3g] = ((b0&0x3F)<<2) | ((b1>>2)&0x3)out[3g+1] = ((b1&0x3)<<6) | ((b0>>6)<<4) | ((b2&0x3)<<2) | (b1>>6)out[3g+2] = ((b1&0x30)<<2) | ((b2>>2)&0x3F)
4 组现场数据逐一验证,全部命中。
第三个坑:CONST 是固定的还是动态的
IV=0、cipher=0 时 out 仍非零,初始以为是 block 或 URL 相关的动态值。
写 test_third.js,用三个不同 URL 测,发现 baseline 完全一致:
[232,0,0,2,128,192,0,8,14,0,0,0] × 4
是硬编码常量,不是 URL 或 block 的函数。
rand_byte 公式推导
写 test_randbyte2.js,固定 Math.random() = i/256,遍历 i=0..255,捕获 block[0],建 LOOKUP 表。
分析发现:
-
127 个唯一值(0~126),每个出现 2 次(边界值 26 和 37 出现 3 次) -
规律: k = floor(random() * 127),然后对 k 的低 5 位做位变换: -
top 2 bits 取反 -
bit2 不变 -
低 2 bits XOR 2
公式:
const k = Math.floor(Math.random() * 127);const s = k & 0x1F;return (k & ~0x1F) + (((~s & 0x18) | (s & 0x04) | ((s^2) & 0x03)) & 0x1F);
ZK 被 JSVMP 运行时篡改
写好纯算实现,50 次测试全部失败。追查发现:zhihu_sign.js 源码中的 h.zk 是初始值,JSVMP 字节码运行时((new l).O(_BYTECODE, 0, _STRINGS))会原地修改 h.zk 为另一组值。
// 源码中(错误的):[1170614578, 1024848638, 1413669199, ...]// JSVMP 运行后(正确的):[1199388770, 946244156, 436498745, ...]
通过临时修改模块导出 _h: h 才拿到真实值。替换后 100/100 测试全部通过。
接口校验验证
实际测试发现知乎对签名的校验策略:
-
无 x-zse-96 头 → 403 -
有头但内容随机 → 200(不验证密码学内容) -
有头且内容正确 → 200
结论:签名头的存在性和格式是必须的,GET 接口不做内容校验;POST 写操作通常校验更严。
最终产出文件
|
|
|
|---|---|
zhihu_sign_pure.js |
|
zhihu_request.js |
|
zhihu_test.js |
|
test_layout2.js |
|
test_randbyte2.js |
|
test_third.js |
|
test_ivmap2.js |
|
夜雨聆风