基于 GLM-5.2 的 X-Gnarly (332位) 纯算还原分析

基于 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 模拟)在面对该级别的混淆时,往往因为大量未知的浏览器特征检测而难以推进。因此,本方案采用语义级插桩思路:

  1. 动态插桩脚本(基于工作流中的patch_sdk.js):通过源码级特征匹配,定位webmssdk.js中的核心执行位置,在每个.call().apply()以及关键二元/三元运算处注入__tk_log记录器,自动输出函数对象、调用参数、返回值及运算轨迹。
  2. 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=7314num3num4num5tsABnum9num101T12T13M1M2
    注:其中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 0xFFFF0xFFFF采用 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 字节固定 Key

5.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=011(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 字节):

  1. 拷贝当前state矩阵副本。
  2. 依次进行列变换与变种对角线变换,交替执行rounds轮。
  3. 加密完毕后,执行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
  4. 初始状态矩阵中的state[12](counter) 递增,用于下一块计算。
  5. 将得到的密钥流与明文块按字节进行异或。

最终输出: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=0199ciphertext[i]+j=047key48[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[0splitIndex]key48[047]ciphertext[splitIndex199]

最终输出: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=(b016)(b18)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[(Group18)&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 校验路径

  1. Playwright 加载目标页面,利用代理机制将webmssdk.js重定向至本地插桩版。
  2. 触发 API 请求,捕获签名过程中的 trace 日志。
  3. 从控制台捕获的最终X-Gnarly反解出 249 字节,恢复出相应的密文与key48,直至还原明文 TLV。
  4. 运行纯 JS 复现程序,对比复现产物与浏览器端生成的结果是否完全对齐。

9.2 对拍结果

测试批次key48 摘要加密轮数变量 BsplitIndex复现结果一致性
trace4e0671fe6...1735494518038✅ 一致
trace6098bf137...111316161044194✅ 一致

十、复现实现与调用

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 语义日志的解构价值

通过自动向执行单元添加插桩,能够避开虚拟机内部繁琐的操作码转换,直接将寄存器数值与函数交互以自然语言级别记录下来。这不仅极大地缩短了逆向周期,而且保证了底层逻辑还原的准确性。