首页
社区
课程
招聘
[原创]GameGuard NP CS_Auth3协议
发表于: 1天前 259

[原创]GameGuard NP CS_Auth3协议

1天前
259

GameGuard CSAuth3 心跳协议逆向分析

虽然GameGuard 或者说棒子那边的反外挂已经基本上距离ACE/BE水平还差5个EAC?
但是做为技术分析还是有点用?
毕竟换汤不换药


一、协议是什么

CSAuth3 是 GameMon(客户端反作弊)和对端之间的心跳。问答循环:对端发挑战包,客户端回心跳包,断了就判掉线。

客户端                          对端
  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)

头部明文(KDF解密后):

struct CsAuth3Header {
    uint16_t magic;      // [0-1]  0x001E
    uint16_t seq;        // [2-3]  序号, ≤5
    uint16_t msg_count;  // [4-5]  期望消息数
    uint16_t flags;      // [6-7]  bit0=错误码映射 bit2=首包
    uint8_t  protocol;   // [8]    协议号 (首包=0)
    uint8_t  pad[7];     // [9-15]
};

四、完整往返示例(首包, counter=0)

【对端发来】 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(首包空载荷,下面讲)。

两端头相同(同 K=0x91284712, counter=0)。客户端首包载荷 8 字节全 0,空载荷不 scramble。


五、头部 KDF + XOR

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];
}

首包 K=0x91284712,密钥流 = 12 47 28 91 24 8E 50 24 36 D5 78 B3 48 1C A1 44
首包 seed=0(包计数器清零后),后续包 seed=counter。


六、CRC1(固定多项式)

uint16_t crc1(const uint8_t *data, int len) {
    uint16_t crc = 0;
    for (int i = 0; i < len; i++) {
        uint16_t e = data[i] << 8;
        for (int b = 0; b < 8; b++) {
            if ((e ^ crc) & 0x8000)
                crc = (crc ^ 0x0FAE) << 1;
            else
                crc <<= 1;
            e <<= 1;
        }
    }
    return ~crc;
}
// 范围 [0 .. size-3],存 [size-2],小端

76B 首包: crc1(pkt[0:74]) = 0x518B = 存储值 8B 51


七、CRC2(keyed)

uint16_t crc2(const uint8_t *data, int len, uint16_t key16) {
    uint16_t crc = 0;
    for (int i = 0; i < len; i++) {
        uint16_t br = data[i] << 8;
        for (int b = 0; b < 8; b++) {
            uint16_t t = br ^ crc;
            crc <<= 1;                       // shift 先
            if (t & 0x8000) crc ^= key16;     // xor 后
            br <<= 1;
        }
    }
    return ~crc;
}
// 覆盖 = 解密头(16) + descramble后载荷, 范围 [0 .. size-5],存 [size-4],小端

key 派生:

uint16_t derive_crc2_key(uint16_t counter, uint32_t prev_size) {
    uint32_t key;
    if (counter == 0)
        key = 0x245D96E8;
    else
        key = ((prev_size * 0x4912 - 0x918) * prev_size) * 0x9184;
    return key & 0xFFFF;   // 只用低16位 = 0x96E8 (counter=0)
}

76B 首包: crc2(解密头+descramble后载荷, 0x96E8) = 0x5B67 = 存储值。注意 CRC2 算的是 descramble 之后的载荷,所以构造包时必须先 scramble 再算 CRC2。


八、载荷 scramble(Blowfish)

载荷用 Blowfish 变换

8.1 常量

// 模块内置 Blowfish π 常量 (初始化用, 标准 Blowfish P-array/S-box)
// P_init[18] 以 0x243F6A88 开头; S_init[4][256] 标准 Blowfish S-box
// (这两个表是公开的 Blowfish 标准, 任何 Blowfish 实现都有)

// 硬编码密钥 (8 字节, 内存序):
static const uint8_t SCRAMBLE_KEY[8] = {
    0x9F, 0x5A, 0x86, 0x03, 0x7E, 0x72, 0xA3, 0x30
};
// 按 big-endian 取两个 32 位: K32[0]=0x9F5A8603, K32[1]=0x7E72A330

8.2 密钥编排(一次性,生成会话 P/S)

uint32_t P[18];        // 会话 P-array
uint32_t S[4][256];    // 会话 S-box

uint32_t F(uint32_t x) {
    uint32_t t = (S[1][(x >> 16) & 0xFF] + S[0][(x >> 24) & 0xFF]) & 0xFFFFFFFF;
    t ^= S[2][(x >> 8) & 0xFF];
    return (t + S[3][x & 0xFF]) & 0xFFFFFFFF;
}

// 加密方向块变换 (密钥编排内部用, P 正序)
void bf_enc(uint32_t *l, uint32_t *r) {
    uint32_t L = *l, R = *r;
    for (int i = 0; i < 16; i++) { L ^= P[i]; R ^= F(L); uint32_t t=L; L=R; R=t; }
    uint32_t t = L; L = R; R = t;   // 撤销最后一次交换
    R ^= P[16]; L ^= P[17];
    *l = L; *r = R;
}

void scramble_key_schedule(void) {
    memcpy(P, P_init, sizeof(P));
    memcpy(S, S_init, sizeof(S));
    // ① 密钥异或: 偶数 P ^= K32[0], 奇数 P ^= K32[1]
    uint32_t K32[2] = { 0x9F5A8603, 0x7E72A330 };
    for (int i = 0; i < 18; i++) P[i] ^= K32[i & 1];
    // ② 用加密方向更新 P 和 S (标准 Blowfish 密钥扩展)
    uint32_t L = 0, R = 0;
    for (int k = 0; k < 18; k += 2) { bf_enc(&L, &R); P[k] = L; P[k+1] = R; }
    for (int t = 0; t < 4; t++)
        for (int k = 0; k < 256; k += 2) { bf_enc(&L, &R); S[t][k] = L; S[t][k+1] = R; }
}

8.3 载荷变换(scramble / descramble)

scramble 用解密方向(P 逆序),逐 8 字节块就地变换:

// 解密方向块变换 (载荷 scramble 用, P 逆序)
void bf_dec(uint32_t *l, uint32_t *r) {
    uint32_t L = *l, R = *r;
    for (int i = 17; i >= 2; i--) { L ^= P[i]; R ^= F(L); uint32_t t=L; L=R; R=t; }
    uint32_t t = L; L = R; R = t;   // 撤销最后一次交换
    R ^= P[1]; L ^= P[0];
    *l = L; *r = R;
}

// 对载荷 [data+16 .. size-4) 逐 8 字节块就地 scramble
void scramble_payload(uint8_t *data, int size) {
    for (int off = 16; off + 8 <= size - 4; off += 8) {
        uint32_t l, r;
        memcpy(&l, data + off, 4);
        memcpy(&r, data + off + 4, 4);
        bf_dec(&l, &r);              // scramble = 解密方向
        memcpy(data + off, &l, 4);
        memcpy(data + off + 4, &r, 4);
    }
}

收包 descramble 与发包 scramble 用同一个 bf_dec(解密方向),因为 Blowfish 解密是加密的逆;发送方把明文 TLV 跑一遍 bf_dec 得到线上密文,接收方再跑一遍 bf_dec 还原——所以"scramble"和"descramble"是同一函数,靠 Blowfish 的加解密对称性工作。

8.4 拿首包跑一遍

首包 76B 载荷 (线上 scramble 后, 56B):
  C5 83 18 C9 C8 F8 43 40 06 70 09 20 B8 79 4C A9
  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

scramble 后 (bf_dec 跑一遍, 56B):
  F3 F5 08 00 F3 F5 DE DE DD 40 14 00 19 00 00 00
  2A 24 8A CE 6E 24 BD B9 0E F9 99 33 17 E8 16 00
  0E 00 00 00 0C 07 0D 09 0E 0F 0F 09 0F 0D 0E 0C
  0D 0C 00 00 00 00 00 00

拿这 56 字节加上解密头一起算 CRC2(key=0x96E8),得 0x5B67,跟包里存的 67 5B 一致。

密钥写死、常量是公开的 Blowfish π,P/S 编排结果确定,所以 scramble 能离线复现,不依赖运行时状态。


九、错误码

发包后对端返回的错误码(抓包/自己黑盒测试不一定对,我自己的理解):

含义
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 停发

十、TLV 消息(载荷)

struct TlvMsg {
    uint16_t type;
    uint16_t length;   // 含头
    uint32_t value;
    // 后续字段按类型定
};
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

msg_count(头 word2)= 期望消息数。


十一、State 结构(1456 = 0x5B0 字节)

会话状态结构(双端各自维护一份):

struct CsAuth3State {        // offset
    uint32_t magic;          // +0x04  0x11223344
    uint32_t flags;          // +0x08  bit0/2/3
    uint16_t word1;          // +0x5A  消息1同步
    uint32_t prev_size;      // +0x5C  CRC2 key源
    uint16_t first_pkt;      // +0x60
    char    *user_id;        // +0x64
    char    *ip_addr;        // +0x68
    uint16_t counter;        // +0x6C  KDF种子
    char    *exinfo;         // +0x70
    uint8_t  list[100*10];   // +0x47C 消息12
    uint16_t list_count;     // +0x4E0
    uint32_t ext_info[6];    // +0x4E8 消息13
    uint16_t status[4];      // +0x502 消息20-24
    uint8_t  ready;          // +0x510
    uint32_t error_flag;     // +0x5A8 ≥0xBB8置位
};

十二、首包特征

首包(counter=0):

  • K = 0x91284712(固定)
  • CRC2 key = 0x245D96E8(低16=0x96E8,固定)
  • flags bit2 = 1(首包标志)
  • 客户端首包载荷 = 8字节全0,空载荷不 scramble
  • 客户端首包 CRC2 = 0x0000

后续包(counter≠0):

  • K = derive_K(counter),counter 经 KDF 迭代
  • CRC2 key = ((prev_size × 0x4912 − 0x918) × prev_size) × 0x9184 的低16位
  • prev_size 由消息类型1 同步
  • 载荷有 TLV 内容时需 scramble

十三、识别(C 伪代码)

// 用 K 解密前 16 字节,检查 magic 判定是否 CSAuth3 包
int is_csauth3(const uint8_t *pkt, int len, uint16_t counter) {
    if (len < 16) return 0;
    uint32_t K = derive_K(counter);
    uint8_t head[16];
    encrypt_header(pkt, K, head);   // encrypt = decrypt (XOR 自逆)
    return head[0] == 0x1E && head[1] == 0x00;
}

// 完整解析,字段填入 out
int parse_csauth3(const uint8_t *pkt, int len, uint16_t counter, CsAuth3Header *out) {
    if (!is_csauth3(pkt, len, counter)) return 0;
    uint32_t K = derive_K(counter);
    encrypt_header(pkt, K, (uint8_t*)out);
    out->size      = len;
    out->body      = (uint8_t*)(pkt + 16);
    out->body_len  = len - 16 - 4;
    out->crc2      = *(uint16_t*)(pkt + len - 4);
    out->crc1      = *(uint16_t*)(pkt + len - 2);
    out->crc1_ok   = (crc1(pkt, len - 2) == *(uint16_t*)(pkt + len - 2));
    return 1;
}

拿首包(counter=0)跑一遍识别:

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 算出来一致

十四、伪造客户端包

void build_client_pkt(uint8_t *out, uint16_t counter, uint32_t prev_size,
                      uint8_t *tlv_payload, int payload_len) {
    // 1. 头
    uint32_t K = derive_K(counter);
    CsAuth3Header hdr = { 0x001E, 1, msg_count, flags, 0, {0} };
    memcpy(out, &hdr, 16);

    // 2. 载荷补齐 8 字节
    int body_len = payload_len;
    memcpy(out + 16, tlv_payload, body_len);
    while (body_len % 8) out[16 + body_len++] = 0;
    int size = 16 + body_len + 4;

    // 3. scramble 载荷 (空载荷跳过)
    if (body_len > 0) scramble_payload(out, size);

    // 4. CRC2 (覆盖 解密头 + scramble后载荷)
    uint16_t k2 = derive_crc2_key(counter, prev_size);
    *(uint16_t*)(out + size - 4) = crc2(out, size - 4, k2);

    // 5. CRC1 (覆盖整包到 size-2)
    *(uint16_t*)(out + size - 2) = crc1(out, size - 2);

    // 6. 加密头 (就地)
    encrypt_header(out, K, out);
}

载荷需含至少消息类型1(同步包大小),否则后续 CRC2 key 源对不上。


十五、抓包要点

  • 包无明文特征,前2字节密文随 K 变化(首包固定 0C 47
  • 识别靠:用 K=0x91284712 解密前16字节,检查 1E 00
  • 包长 76(首包)或 8 的倍数+20(头+CRC)
  • 包尾 4 字节 = CRC2(2) + CRC1(2),客户端/对端包同样

总结

CSAuth3 心跳是一套自包含的对称协议,全部密码学材料都是硬编码常量:

环节 算法 密钥来源
头加密 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 派生

唯一随会话变化的是 counter(驱动 KDF 和 CRC2 key)与 prev_size(由消息1同步)。其余全是固定常量。因此只要拿到首包挑战、复现密钥编排,就能纯脱机心跳
State 1456 会根据版本协议 老版本应该有0x61x系列,auth2 应该是16byte
但是gameguard应该是已经摆烂了,这个分析是基于24年 但是一直更新只是驱动稳定性问题

最后嫌长不看版

客户端有个callback 里面根据参数 然后收到挑战包 然后客户端自己走自己发送发过去
那么你注入进去hook到这个callback handler 就知道发什么了,转发一下也可以


冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。

最后于 1天前 被MaMy编辑 ,原因:
收藏
免费 3
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回