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

admin 2026-08-04 08:06:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细记录了利用GLM-5.2辅助分析和语义级插桩技术,逆向还原TikTokWeb端X-Gnarly签名算法(332位)的完整过程。算法分为TLV序列化、ChaCha变种加密、W字节拼装和自定义Base64编码四个阶段。通过动态插桩突破JSVMP混淆,成功复现了纯JavaScript算法,并验证了其与浏览器端生成结果的一致性。该方法为分析类似混淆保护提供了高效路径。 综合评分: 92 文章分类: 逆向分析,安全工具,实战经验,WEB安全


cover_image

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

红皮西瓜 红皮西瓜

看雪学苑

2026年7月28日 17:59 上海

在小说阅读器读本章

去阅读

目标接口:/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 个元素的混合数组:

const arr = [
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 校验和算法

在进行洗牌前,需要计算数组中的两个校验和:

4.3 LCG 洗牌机制 (Fisher-Yates Shuffle)

基于线性同余生成器(LCG)生成的伪随机数对上述数组进行倒序洗牌:

let seed = B; // 初始种子来源于 tag8 对应的固定常量
const multiplier = 1664525;
const increment = 1013904223;
const modulus = Math.pow(2, 32);

for (let i = arr.length - 1; i > 0; i--) {
    seed = (multiplier * seed + increment) % modulus;
const targetIndex = Math.floor((seed / modulus) * (i + 1));
// 交换位置
const temp = 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 字节固定 Key

5.2 动态加密轮数计算

轮数由初始 key48 决定。由于本方案中 key48 采用提取自真机环境的静态常量,其推导轮数亦维持不变:

5.3 核心 Quarter Round (QR) 实现

QR 结构与标准 ChaCha 保持一致,保留了经典移位常数:

function quarterRound(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 算法原有的对称扩散特性,需在重构时严格遵循变种索引。

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 数据之和求模动态计算:

6.2 拼接融合

将 48 字节的 key48 插入到密文字节流的分割点处,构建合成数组 W

最终输出:248 字节的数据块。

#

七、阶段 4:自定义 Base64 编码(→ 332 字符)

7.1 添加协议前缀

在数据块头部附加固定标识 0x4B(字符 ‘K’):

7.2 变种 Base64 转换

使用指定的自定义字母表进行标准的 3 字节至 4 字符转换(转换过程中无填充字符 =):

  • 自定义字母表(64 字符):
  u09tbS3UvgDEe6r-ZVMXzLpsAohTn7mdINQlW412GqBjfYiyk8JORCF5/xKHwacP=
  • 编码转换逻辑

最终输出:332 字符长度的有效 X-Gnarly 签名。

#

八、常量与动态变量对照

为保持长期调用的稳定性,算法实现中明确区分了动态变量与静态常量:

| 参数类型 | 字段明细 | 处置策略 | | — | — | — | | 动态输入 | queryString (URL 查询参数), userAgent (浏览器 UA), ts (当前时间戳) | 随每次请求实时生成,用于计算数组中的 Tag 字段 | | 固定参数 | 状态矩阵 OT, 加密密钥 key48rounds, 变量 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');

const query = 'WebIdLastTime...msToken..';
const ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)...';
const timestamp = Math.floor(Date.now() / 1000);

const xGnarly = generateXGnarly(query, ua, timestamp);
console.log('Generated X-Gnarly:', xGnarly); // 输出 332 字符的签名

10.2 发包验证测试 (Python)

from curl_cffi import requests

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

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

看雪ID:红皮西瓜

https://bbs.kanxue.com/user-home-1046416.htm

*本文为看雪论坛优秀文章,由 红皮西瓜 原创,转载请注明来自看雪社区

第十届安全开发者峰会【议题征集】-欢迎投稿

往期推荐

ISCC 2026 题目 [house of apple 2] 利用方法

Android 内核无痕 Hook 框架设计思路和避坑经验

从零手写 ARM64 自定义 Linker

从2026CCB决赛堆题CreditMarket学习Glibc2.39后ptmalloc的机制更新

2019-SUCTF-SUDriver 学习笔记

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 红皮西瓜 红皮西瓜《基于 GLM-5.2 的 X-Gnarly (332位) 纯算还原分析》

评论:0   参与:  0