基于 GLM-5.2 的 X-Gnarly (332位) 纯算还原分析
目标接口:
/api/post/item_list
签名参数:X-Gnarly(332 字符)
SDK 版本:webmssdk 5.2.1
方法与工具:fast-spider skills 工作流 + GLM-5.2 辅助分析 + 语义级插桩
一、背景
在 TikTok Web 端中,访问/api/post/item_list等核心接口通常需要两个关键的签名参数:
X-Bogus:长度为 28 字符,一般非强制校验。X-Gnarly:长度为 332 字符,必须有效,服务端会对其进行多重深度校验。
X-Gnarly参数由webmssdk.js(byted_acrawler SDK)动态计算生成。由于该 SDK 采用了JSVMP(JavaScript VM Protection)混淆技术,其实际执行算法被编译为约 44KB 的字节码,源码中仅包含解释器内核,静态分析难以直观还原其明文逻辑。
本篇报告记录了如何利用 fast-spider skills 工作流,在 GLM-5.2 的辅助推理下,从 trace 日志中逐步还原出X-Gnarly的完整生成链路,并完成纯 JavaScript 算法复现的过程。
二、方法:fast-spider skills + 语义插桩
2.1 基础分析工作流
初始化环境 (init) → 捕获 SDK (capture) → 制定运行策略 (runbook) → 采集 trace → 逻辑分析 (analyze) → 制定方案 (plan) → 补全算法 (fill) → 对拍校验 (verify)2.2 针对 JSVMP 混淆的增强策略
传统的补环境方案(如 FakeEnv 或 Node vm 模拟)在面对该级别的混淆时,往往因为大量未知的浏览器特征检测而难以推进。因此,本方案采用语义级插桩思路:
- 动态插桩脚本(基于工作流中的
patch_sdk.js):通过源码级特征匹配,定位webmssdk.js中的核心执行位置,在每个.call()、.apply()以及关键二元/三元运算处注入__tk_log记录器,自动输出函数对象、调用参数、返回值及运算轨迹。 - Playwright 动态替换:使用拦截代理,将页面请求的线上 CDN 脚本无缝替换为本地生成的插桩版 SDK。
通过这一方法,单次流程可稳定产出约 28 万行语义日志,为后续算法推导提供了坚实的数据支撑。
三、算法全链路设计
X-Gnarly的生成可以划分为以下 4 个阶段:
(query, UA, ts) │ ▼ 阶段 1: TLV 序列化 200 字节明文数据 │ ▼ 阶段 2: ChaCha 变种流加密 200 字节密文数据 │ ▼ 阶段 3: W 字节拼装 (嵌入 48 字节 Key) 248 字节拼装数据 (W) │ ▼ 阶段 4: 自定义 Base64 编码 332 字符 X-Gnarly 签名四、阶段 1:TLV 序列化(→ 200 字节明文)
4.1 构建 17 元素原始数组
算法首先在内存中收集并构建一个包含 17 个元素的混合数组:
constarr=[0,// tag0: num0 (全局数字校验和,待后续计算)73,// tag1: 固定常量14,// tag2: 固定常量qsHash,// tag3: md5(queryString) 结果md5(''),// tag4: md5(空字符串) = 'd41d8cd98f00b204e9800998ecf8427e'md5(UA),// tag5: md5(User-Agent) 结果ts,// tag6: 秒级时间戳(10位整数)A,// tag7: 固定常量B,// tag8: 固定常量'5.2.1',// tag9: SDK 版本号'2.0.0.493',// tag10: webmssdk 内部版本号1,// tag11: 固定常量T12,// tag12: 固定常量T13,// tag13: 固定常量M1,// tag14: 固定常量M2,// tag15: 固定常量num15,// tag16: 内部校验和];4.2 校验和算法
在进行洗牌前,需要计算数组中的两个校验和:
num15(tag16):
num15=73⊕14⊕num3⊕num4⊕num5⊕ts⊕A⊕B⊕num9⊕num10⊕1⊕T12⊕T13⊕M1⊕M2\text{num15} = 73 \oplus 14 \oplus \text{num3} \oplus \text{num4} \oplus \text{num5} \oplus \text{ts} \oplus A \oplus B \oplus \text{num9} \oplus \text{num10} \oplus 1 \oplus T12 \oplus T13 \oplus M1 \oplus M2num15=73⊕14⊕num3⊕num4⊕num5⊕ts⊕A⊕B⊕num9⊕num10⊕1⊕T12⊕T13⊕M1⊕M2
注:其中num3, num4, num5, num9, num10\text{num3, num4, num5, num9, num10}num3, num4, num5, num9, num10是对应字符串字段前 4 字节转换而成的大端 uint32 整数。num0(tag0):
num0=∑⊕arr(仅对数组中数值类型的元素进行异或运算,非数值类型不参与)\text{num0} = \sum^{\oplus} \text{arr} \quad (\text{仅对数组中数值类型的元素进行异或运算,非数值类型不参与})num0=∑⊕arr(仅对数组中数值类型的元素进行异或运算,非数值类型不参与)
4.3 LCG 洗牌机制 (Fisher-Yates Shuffle)
基于线性同余生成器(LCG)生成的伪随机数对上述数组进行倒序洗牌:
letseed=B;// 初始种子来源于 tag8 对应的固定常量constmultiplier=1664525;constincrement=1013904223;constmodulus=Math.pow(2,32);for(leti=arr.length-1;i>0;i--){seed=(multiplier*seed+increment)%modulus;consttargetIndex=Math.floor((seed/modulus)*(i+1));// 交换位置consttemp=arr[i];arr[i]=arr[targetIndex];arr[targetIndex]=temp;}4.4 TLV 序列化打包
洗牌完成后的数组将被转化为 TLV(Type-Length-Value)二进制流:
- 结构布局:
[Tag (1B)] [Length_Hi (1B)] [Length_Lo (1B)] [Value (NB)] - 编码规则:对于整数类型,若≤0xFFFF\le 0xFFFF≤0xFFFF采用 2 字节大端表示,若>0xFFFF> 0xFFFF>0xFFFF则采用 4 字节大端表示;字符串统一采用 UTF-8 编码。
- 头部标识:整个二进制流的第 1 字节记录了字段总数(即固定的
0x11/ 17 个字段)。
最终输出:200 字节明文字节数组。
五、阶段 2:ChaCha 变种加密(→ 200 字节密文)
5.1 矩阵状态初始化 (16 Words / 64 Bytes)
加密状态矩阵的 16 个 32位 Word 布局如下:
[0..3] = OT = [1196819126, 600974999, 3863347763, 1451689750] // 4 个固定状态字 [4..15] = key48 的 12 个 Little-Endian Word // 48 字节固定 Key5.2 动态加密轮数计算
轮数由初始key48决定。由于本方案中key48采用提取自真机环境的静态常量,其推导轮数亦维持不变:
rounds=(∑i=011(keyWord[i] & 15))& 15+5\text{rounds} = \left( \sum_{i=0}^{11} (\text{keyWord}[i] \ \&\ 15) \right) \&\ 15 + 5rounds=(i=0∑11(keyWord[i]&15))&15+5
5.3 核心 Quarter Round (QR) 实现
QR 结构与标准 ChaCha 保持一致,保留了经典移位常数:
functionquarterRound(x,a,b,c,d){x[a]=(x[a]+x[b])|0;x[d]=rotl32(x[d]^x[a],16);x[c]=(x[c]+x[d])|0;x[b]=rotl32(x[b]^x[c],12);x[a]=(x[a]+x[b])|0;x[d]=rotl32(x[d]^x[a],8);x[c]=(x[c]+x[d])|0;x[b]=rotl32(x[b]^x[c],7);}5.4 魔改对角线变换 (Diagonal Round)
这是webmssdk对经典 ChaCha 算法做出的核心修改。对比标准对角线变换索引,本版本进行了如下替换:
| 变换组别 | 标准 ChaCha 索引 | webmssdk 5.2.1 变种索引 | 差异说明 |
|---|---|---|---|
| 第一组 | (0, 5, 10, 15) | (0, 5, 10, 15) | 无变化 |
| 第二组 | (1, 6, 11, 12) | (1, 6, 11, 12) | 无变化 |
| 第三组 | (2, 7, 8, 13) | (2, 7, x, 13) | 索引8被替换为x |
| 第四组 | (3, 4, 9, 14) | (3, 4, y, 14) | 索引9被替换为y |
该修改打破了标准 ChaCha 算法原有的对称扩散特性,需在重构时严格遵循变种索引。
5.5 块流处理与 Feed-Forward
分块对 200 字节明文执行流加密(每块 64 字节):
- 拷贝当前
state矩阵副本。 - 依次进行列变换与变种对角线变换,交替执行
rounds轮。 - 加密完毕后,执行Feed-Forward累加操作以生成最终密钥流块:
keystream[j]=(transformed[j]+initial[j])∣0\text{keystream}[j] = (\text{transformed}[j] + \text{initial}[j]) \mid 0keystream[j]=(transformed[j]+initial[j])∣0 - 初始状态矩阵中的
state[12](counter) 递增,用于下一块计算。 - 将得到的密钥流与明文块按字节进行异或。
最终输出:200 字节密文数组。
六、阶段 3:W 字节拼装(→ 248 字节)
6.1 分割点计算
分割索引依据密文及key48数据之和求模动态计算:
splitIndex=(∑i=0199ciphertext[i]+∑j=047key48[j]) mod 201\text{splitIndex} = \left( \sum_{i=0}^{199} \text{ciphertext}[i] + \sum_{j=0}^{47} \text{key48}[j] \right) \bmod 201splitIndex=(i=0∑199ciphertext[i]+j=0∑47key48[j])mod201
6.2 拼接融合
将 48 字节的key48插入到密文字节流的分割点处,构建合成数组W:
W=ciphertext[0…splitIndex]∥key48[0…47]∥ciphertext[splitIndex…199]\text{W} = \text{ciphertext}[0 \dots \text{splitIndex}] \mathbin{\Vert} \text{key48}[0 \dots 47] \mathbin{\Vert} \text{ciphertext}[\text{splitIndex} \dots 199]W=ciphertext[0…splitIndex]∥key48[0…47]∥ciphertext[splitIndex…199]
最终输出:248 字节的数据块。
七、阶段 4:自定义 Base64 编码(→ 332 字符)
7.1 添加协议前缀
在数据块头部附加固定标识0x4B(字符 ‘K’):
input=[0x4B]∥W(总长度为 249 字节)\text{input} = [0x4B] \mathbin{\Vert} \text{W} \quad (\text{总长度为 } 249 \text{ 字节})input=[0x4B]∥W(总长度为249字节)
7.2 变种 Base64 转换
使用指定的自定义字母表进行标准的 3 字节至 4 字符转换(转换过程中无填充字符=):
自定义字母表(64 字符):
u09tbS3UvgDEe6r-ZVMXzLpsAohTn7mdINQlW412GqBjfYiyk8JORCF5/xKHwacP=编码转换逻辑:
Group=(b0≪16)∣(b1≪8)∣b2\text{Group} = (b_0 \ll 16) \mid (b_1 \ll 8) \mid b_2Group=(b0≪16)∣(b1≪8)∣b2
char1=alphabet[(Group≫18) & 63],…char4=alphabet[Group & 63]\text{char}_1 = \text{alphabet}[(\text{Group} \gg 18) \ \&\ 63], \quad \dots \quad \text{char}_4 = \text{alphabet}[\text{Group} \ \&\ 63]char1=alphabet[(Group≫18)&63],…char4=alphabet[Group&63]
最终输出:332 字符长度的有效 X-Gnarly 签名。
八、常量与动态变量对照
为保持长期调用的稳定性,算法实现中明确区分了动态变量与静态常量:
| 参数类型 | 字段明细 | 处置策略 |
|---|---|---|
| 动态输入 | queryString(URL 查询参数),userAgent(浏览器 UA),ts(当前时间戳) | 随每次请求实时生成,用于计算数组中的 Tag 字段 |
| 固定参数 | 状态矩阵OT, 加密密钥key48,rounds, 变量A, B, M1, M2, T12, T13 | 采用从真机 trace 日志中分析固定的静态数值,降低运行时的环境依赖风险 |
九、对拍验证流程
9.1 校验路径
- Playwright 加载目标页面,利用代理机制将
webmssdk.js重定向至本地插桩版。 - 触发 API 请求,捕获签名过程中的 trace 日志。
- 从控制台捕获的最终
X-Gnarly反解出 249 字节,恢复出相应的密文与key48,直至还原明文 TLV。 - 运行纯 JS 复现程序,对比复现产物与浏览器端生成的结果是否完全对齐。
9.2 对拍结果
| 测试批次 | key48 摘要 | 加密轮数 | 变量 B | splitIndex | 复现结果一致性 |
|---|---|---|---|---|---|
| trace4 | e0671fe6... | 17 | 354945180 | 38 | ✅ 一致 |
| trace6 | 098bf137... | 11 | 1316161044 | 194 | ✅ 一致 |
十、复现实现与调用
10.1 本地调用示例 (Node.js)
const{generateXGnarly}=require('./registry/tiktok/xgnarly521.js');constquery='WebIdLastTime...msToken..';constua='Mozilla/5.0 (Windows NT 10.0; Win64; x64)...';consttimestamp=Math.floor(Date.now()/1000);constxGnarly=generateXGnarly(query,ua,timestamp);console.log('Generated X-Gnarly:',xGnarly);// 输出 332 字符的签名10.2 发包验证测试 (Python)
fromcurl_cffiimportrequests headers={'User-Agent':'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...',# ... 其他必要请求头}url=f'https://www.tiktok.com/api/post/item_list/?{query}&X-Bogus=&X-Gnarly={x_gnarly}'resp=requests.get(url,headers=headers,impersonate='chrome120')print("Status:",resp.status_code)# 数据成功解包说明签名通过深度校验十一、逆向方法论总结
11.1 突破 JSVMP 的静态阻碍
面对包含数百个虚拟机操作码分支及复杂分发循环的 JSVMP 保护,完全依赖静态反汇编分析极易陷入迷局。此时,借助 GLM-5.2 的代码分析与逻辑整合能力,配合同步插桩方案,是定位真实解密链路的高效途径。
11.2 语义日志的解构价值
通过自动向执行单元添加插桩,能够避开虚拟机内部繁琐的操作码转换,直接将寄存器数值与函数交互以自然语言级别记录下来。这不仅极大地缩短了逆向周期,而且保证了底层逻辑还原的准确性。