虽然GameGuard 或者说棒子那边的反外挂已经基本上距离ACE/BE水平还差5个EAC?
但是做为技术分析还是有点用?
毕竟换汤不换药
CSAuth3 是 GameMon(客户端反作弊)和对端之间的心跳。问答循环:对端发挑战包,客户端回心跳包,断了就判掉线。
头部明文(KDF解密后):
两端头相同(同 K=0x91284712, counter=0)。客户端首包载荷 8 字节全 0,空载荷不 scramble。
首包 K=0x91284712,密钥流 = 12 47 28 91 24 8E 50 24 36 D5 78 B3 48 1C A1 44。
首包 seed=0(包计数器清零后),后续包 seed=counter。
76B 首包: crc1(pkt[0:74]) = 0x518B = 存储值 8B 51
key 派生:
76B 首包: crc2(解密头+descramble后载荷, 0x96E8) = 0x5B67 = 存储值。注意 CRC2 算的是 descramble 之后的载荷,所以构造包时必须先 scramble 再算 CRC2。
载荷用 Blowfish 变换
scramble 用解密方向(P 逆序),逐 8 字节块就地变换:
收包 descramble 与发包 scramble 用同一个 bf_dec(解密方向),因为 Blowfish 解密是加密的逆;发送方把明文 TLV 跑一遍 bf_dec 得到线上密文,接收方再跑一遍 bf_dec 还原——所以"scramble"和"descramble"是同一函数,靠 Blowfish 的加解密对称性工作。
密钥写死、常量是公开的 Blowfish π,P/S 编排结果确定,所以 scramble 能离线复现,不依赖运行时状态。
发包后对端返回的错误码(抓包/自己黑盒测试不一定对,我自己的理解):
msg_count(头 word2)= 期望消息数。
会话状态结构(双端各自维护一份):
首包(counter=0):
后续包(counter≠0):
拿首包(counter=0)跑一遍识别:
载荷需含至少消息类型1(同步包大小),否则后续 CRC2 key 源对不上。
CSAuth3 心跳是一套自包含的对称协议,全部密码学材料都是硬编码常量:
唯一随会话变化的是 counter(驱动 KDF 和 CRC2 key)与 prev_size(由消息1同步)。其余全是固定常量。因此只要拿到首包挑战、复现密钥编排,就能纯脱机心跳
State 1456 会根据版本协议 老版本应该有0x61x系列,auth2 应该是16byte
但是gameguard应该是已经摆烂了,这个分析是基于24年 但是一直更新只是驱动稳定性问题
客户端有个callback 里面根据参数 然后收到挑战包 然后客户端自己走自己发送发过去
那么你注入进去hook到这个callback handler 就知道发什么了,转发一下也可以
| 码 |
含义 |
| 0xBAE |
首包标志(正常) |
| 0xBB8 |
CRC2 不匹配(映射模式基值) |
| 0xCE5 |
seq > 5 |
| 0xCE8 |
CRC2 不匹配(严格) |
| 0xCE9 |
CRC1 不匹配 |
| 0xCEA |
magic ≠ 0x1E |
| 0xCEC |
size < 16 |
| 0xCEE |
size > 0x1000 |
| 0xCEF |
size < 16(body) |
| 0xD4A |
body 未 8 对齐 |
| 0xD52 |
消息数不符 |
| 0xD67 |
消息类型2 失败 |
| 0xD7A |
消息类型11 内容不符 |
| 0xD913 |
消息类型1 哈希失败 |
| ≥0xBB8 |
置错误标志 → 后续 Check 假通过、Get 停发 |
| type |
含义 |
关键操作 |
| 1 |
同步 |
hash = value² × msg.word2 × 0x622B60B;同步下个包大小(CRC2 key源) |
| 2 |
认证 |
设 ID/IP;查表校验 |
| 3 |
日志 |
— |
| 10 |
配置 |
— |
| 11 |
查表 |
索引 ≤ 50 |
| 12 |
列表 |
≤10 项 × 10 字节 |
| 13 |
扩展 |
按模式分支 |
| 20-24 |
状态 |
写状态字段,含 magic 0x9128/0x8757/0x4813 |
| 包 |
size |
magic |
seq |
msg_count |
flags |
protocol |
crc1 |
crc1_ok |
| 对端 76B |
76 |
0x001E |
1 |
3 |
0x0002 |
0 |
0x518B |
算出来一致 |
| 客户端 28B |
28 |
0x001E |
1 |
3 |
0x0002 |
0 |
0x0BEB |
算出来一致 |
| 环节 |
算法 |
密钥来源 |
| 头加密 |
XOR 流密码, 密钥流 {K,2K,3K,4K} |
K = KDF(counter), counter=0 时 K=0x91284712 |
| 载荷 scramble |
Blowfish (解密方向) |
固定密钥 0x9F5A8603/0x7E72A330 + 标准 π 常量 |
| CRC1 |
CRC-16, poly 0x0FAE, xor-then-shift |
无密钥 |
| CRC2 |
CRC-16, shift-then-xor, keyed |
counter=0 时 0x96E8; 否则 prev_size 派生 |
客户端 对端
GameMon CSAuth3 实例
│ ◄──── 挑战包 ──────────────│ Get
│ ────── 心跳包 ───────────►│ Check
└──────── 循环 ──────────────┘
客户端 GameMon 对端 CSAuth3
│ counter=0, prev_size 初始 │
│ │
│ ◄──── 挑战包 (头16+载荷56+CRC2+CRC1) │
│ │
│ ① 解密头: KDF XOR, K=derive(counter)│
│ ② 校验 magic / seq / CRC1 │
│ ③ descramble 载荷 (Blowfish 解密方向)│
│ ④ 解析 TLV 消息, 更新 state │
│ ⑤ 取消息1的 prev_size (下轮 CRC2 key)│
│ ⑥ 构造心跳包载荷 (8 对齐) │
│ ⑦ scramble 载荷 (空载荷跳过) │
│ ⑧ 算 CRC2 / CRC1 │
│ ⑨ 加密头: KDF XOR, K=derive(counter)│
│ │
│ ──── 心跳包 (头16+载荷+CRC2+CRC1) ──► │
│ │
│ counter++ │
│ │
└──── 循环, counter 递增驱动 KDF 与 CRC2 key
客户端包:
┌────────┬──────────────┬───────┬───────┐
│ 头 16B │ 载荷 (8对齐) │ CRC2 │ CRC1 │
│ KDF加密│ scramble+TLV │ 2B │ 2B │
└────────┴──────────────┴───────┴───────┘
↑size-4 ↑size-2
对端包: 同结构 (头16 + 载荷56 + CRC2 + CRC1)
struct CsAuth3Header {
uint16_t magic;
uint16_t seq;
uint16_t msg_count;
uint16_t flags;
uint8_t protocol;
uint8_t pad[7];
};
【对端发来】 76B
0C 47 29 91 27 8E 52 22 36 D5 78 B3 48 1C A1 44 <- 头(密文)
C5 83 18 C9 C8 F8 43 40 06 70 09 20 B8 79 4C A9 <- 载荷(scramble后)
28 4D 4D 3B FA CB E7 9D 10 96 F2 AC E1 23 08 CB
CB E4 91 8F 92 37 C6 C1 9B 78 E2 B9 47 F9 98 F9
39 A0 BB 9F 36 4C CE 6F <- 载荷尾
67 5B <- CRC2
8B 51 <- CRC1
解密头 (XOR K=0x91284712):
1E 00 01 00 03 00 02 00 00 00 00 00 00 00 00 00
magic=0x001E seq=1 msg_count=3 flags=0x0002(首包) protocol=0
CRC1 算出来 0x518B,包里存 8B 51,对得上
descramble 载荷 (Blowfish) → 明文 TLV
CRC2 算出来 0x5B67,覆盖「解密头 + descramble 后载荷」
【客户端回发】 28B
0C 47 29 91 27 8E 52 22 36 D5 78 B3 48 1C A1 44 <- 头(同K, 同counter)
00 00 00 00 00 00 00 00 <- 载荷(8字节全0, 空载荷不scramble)
00 00 <- CRC2 = 0
EB 0B <- CRC1
解密头: 同对端包头
CRC1 算出来 0x0BEB,包里存 EB 0B,对得上。
CRC2 = 0x0000(首包空载荷,下面讲)。
uint32_t derive_K(uint32_t seed) {
uint32_t K = (seed == 0) ? 0x91284712 : seed;
if (K >= 100) K %= 100;
if (K % 100 != 0) {
for (int i = 0; i < K % 100; i++) {
K = K * 0x27D2 - ((uint64_t)K * 0x76C4E92B >> 16) * 0x55D49D5A;
if (K == 0) K = 0x7FFFFFFF;
}
}
return K;
}
void encrypt_header(uint8_t plain[16], uint32_t K, uint8_t out[16]) {
uint32_t key[4] = { K, 2*K, 3*K, 4*K };
for (int i = 0; i < 16; i++)
out[i] = plain[i] ^ ((uint8_t*)key)[i];
}
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
最后于 2026-8-10 07:00
被MaMy编辑
,原因: