Skip to content

billion-context ACP kernel 改写 body 导致 ZCode (zcode.z.ai) 405/3012 #661

Description

@Juna9969

Issue 报告:billion-context ACP kernel 改写 body 导致 ZCode 405/3012

仓库:https://github.com/ranxianglei/billion-context
版本:billion-context 0.1.96
日期:2026-09-09
上游:https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages(Coding Plan 周末额度)


一、问题描述

billion-context 作为 MITM 代理时,对 Anthropic 协议请求执行 ACP kernel(anthropicToCore → processTurn → coreToAnthropic)重建 messages body 后,ZCode 上游返回 405 反作弊拦截:

HTTP 405
{"code":3012,"msg":"request has been blocked due to unusual activity."}

即使 compress.injectTool=false / compress.injectNudge=false,只要 kernel 跑过(processTurn 被调用),messages 仍会被 round-trip 重建(JSON 字段顺序 / 空白 / prompt_cache_key 变化),触发上游风控。


二、405 现场

时间:2026-09-06T04:35:48Z
路径POST https://zcode.z.ai/api/v1/zcode-plan/anthropic/v1/messages
模型GLM-5.3-Flash
响应405 / 3012(135ms 内返回,request-id 202609060435486b04dbd894d282b1e9a9

billion-context 日志(bili.log)关键证据:

L9291: tunnel cloudauth-device-dualstack.cn-shanghai.aliyuncs.com:443 established (blind TCP, not decrypted)
L9292: tunnel no8xfe-verify.captcha-open.aliyuncs.com:443 established (blind TCP, not decrypted)
L9297: [eea280ab-...] processTurn: 7 msgs, renderTags=text-only, 6 text tagged, 0 tool tagged
L9299: forward POST → https://<private-host>/api/v1/zcode-plan/anthropic/v1/messages
L9300: ← upstream 405 request-id=202609060435486b04dbd894d282b1e9a9: {"code":3012,...}

关键观察:

  1. bili 对 messages body 跑了 processTurn(L9297,7 条消息被 kernel 处理后重建)
  2. 同一秒前后,bili 对 Aliyun captcha 域名做了 blind TCP 隧道(未解密,非 MITM)——这是 ZCode 客户端自身的反作弊心跳
  3. 同一 session 中,/api/v1/agent/configs(GET)和 /api/v1/event/report(POST)都成功返回,只有被 bili 改写 body 的 messages 接口被 405
  4. 请求已到达 zcode.z.ai 服务端(135ms 有响应),不是网络连接问题

三、排查过程

3.1 确认 405 来源是上游而非代理本身

通过 bili.log 确认:

  • 请求确实被 bili 转发到 https://<private-host>/api/v1/zcode-plan/anthropic/v1/messages(L9299)
  • 135ms 后收到上游 405(L9300)
  • 同一 session 中其他路径正常(/agent/configs/event/report/billing/balance
  • → 排除网络/代理配置问题,确认是上游反作弊拦截

3.2 确认 bili kernel 确实改写了 body

通过 bili.log L9297 确认:

  • processTurn: 7 msgs 说明 kernel 被调用并处理了 messages
  • 即使 injectTool=false / injectNudge=falseanthropicToCore → processTurn → coreToAnthropic 的 round-trip 仍会改变 JSON 字段顺序、空白,并删除 prompt_cache_key
  • 对比:未走 bili 的 ZCode 直连请求不会触发 405

3.3 分析 ZCode V4 签名协议,排除签名篡改假说

从 ZCode 客户端二进制(Electron app.asar)中逆向提取了 V4 签名协议完整实现(详见第四节)。关键发现:

  • V4 Ed25519 签名 payload 不包含 body hash(签名内容是 {apiKeyId}.{ts}.{clientVersion}.{sessionId}.{nonce}
  • 因此服务端无法通过 V4 签名本身检测 body 是否被篡改
  • 405/3012 的触发条件更可能是 body fingerprint(H1)、TLS/JA3 指纹(H2)、或两者叠加(H3)

3.4 确认 Aliyun captcha 与 405 无关

  • Aliyun captcha header(X-Aliyun-Captcha-Verify-Param)只挂在 /api/v1/zcode-plan/billing/claim 等计费接口
  • messages 接口不需要 captcha token
  • bili 对 Aliyun captcha 域名的 blind tunnel 是客户端自身心跳,与 405 无关

四、ZCode V4 签名协议(详细)

以下是从 ZCode 客户端(Electron app.asar,版本 3.11.2)逆向提取的完整签名流程。

4.1 密钥体系

ZCode 的 API Key 格式为 {apiKeyId}.{apiKeySecret}。签名体系由两部分密钥组成,均从 apiKeySecret 通过 HKDF-SHA256 派生:

KDF_SALT = "WD_CLIENT_SIGN_KDF_SALT"

# HMAC key(用于 handshake 签名)
HKDF(ikm=apiKeySecret, salt=KDF_SALT, info="getSignKey_hmac", len=32) → HMAC-SHA256 key

# Ed25519 私钥加密 key(用于解密 handshake 返回的 privateCipher)
HKDF(ikm=apiKeySecret, salt=KDF_SALT, info="ed25519_priv", len=32) → AES-256-GCM key

4.2 握手(Handshake)

端点POST {origin}/api/paas/c1f3a7e2/v2/client
认证Authorization: {apiKeyFull}

请求体

{
  "apiKey": "{apiKeyId}.{apiKeySecret}",
  "ts": "{unix_millis}",
  "nonce": "{32_hex_random}",
  "sig": "{base64_hmac}"
}

handshake 签名

key = HKDF(apiKeySecret, salt=KDF_SALT, info="getSignKey_hmac")
msg = f"get_sign_key.{apiKeyId}.{ts}.{nonce}".encode()
sig = base64(hmac_sha256(key, msg))

响应体(HTTP 200):

{
  "code": 200,
  "data": {
    "privateCipher": "{base64_aes256gcm_encrypted_pkcs8_ed25519_private_key}"
  }
}

privateCipher 解密

key = HKDF(apiKeySecret, salt=KDF_SALT, info="ed25519_priv")
raw = base64_decode(privateCipher)          # raw = IV(12B) + ciphertext + tag(16B)
plaintext = AES256GCM_decrypt(key, iv=raw[:12], ciphertext=raw[12:-16], aad=apiKeyId)
ed25519_private_key = DER_decode_PKCS8(base64_decode(plaintext))

handshake 成功后,客户端持有从服务端派发的 Ed25519 私钥,用于后续业务请求签名。每个客户端会话生成一个 sessionId(UUID v4)。

4.3 业务请求签名

需要签名的路径

/api/v1/zcode-plan/anthropic/v1/messages
/api/v1/zcode-plan/chat/completions
/api/v1/off-peak/anthropic/v1/messages

签名 payload(Ed25519):

{apiKeyId}.{ts}.{clientVersion}.{sessionId}.{nonce}
  • ts:Unix 毫秒时间戳
  • clientVersion"3.11.2"(从 asar 偏移 235178431 处提取的常量 pn
  • sessionId:handshake 时生成的 UUID v4
  • nonce:每次请求新生成的 32 hex random

签名结果base64(ed25519_sign(privateKey, payload))

4.4 Proof of Work(PoW)

每次业务请求还需附带 PoW header。算法:

seed = sha256(f"{apiKeyId}.{appId}.{sessionId}.{ts}").hexdigest()[:32]
nonce = random_hex(6)  # 6 bytes = 12 hex chars
# 找 counter 使 sha256(f"{seed}.{nonce}{counter:08x}") 的整数表示 < 2^(256-8)
# 即前 8 bits 为 0
for counter in range(0x100000000):
    candidate = f"{nonce}{counter:08x}"
    digest = int(sha256(f"{seed}.{candidate}").digest(), "big")
    if digest < 1 << 248:  # threshold
        return candidate  # 20 hex chars = 12 hex nonce + 8 hex counter

参数:appId = "zcode"POW_BITS = 8,最终 PoW 值为 20 hex 字符。

4.5 签名 Header 集合

业务请求需附带以下自定义 header:

Header 说明
X-Client-Ts {ts} Unix 毫秒
X-Client-Version 3.11.2 客户端版本常量
X-Client-Sig {base64_ed25519_sig} 签名 payload = {apiKeyId}.{ts}.{version}.{sessionId}.{nonce}
X-Session-Id {uuid_v4} handshake 时生成
X-Client-Nonce {32_hex_random} 每次请求重新生成
X-App-Id zcode 固定值
X-Client-Pow {20_hex} PoW 结果

4.6 关键限制

签名 payload 不包含 body hash。这意味着:

  1. V4 签名无法保护 request body 的完整性
  2. 服务端无法通过签名本身检测 body 是否被篡改
  3. 405/3012 反作弊的触发条件是 body 结构/内容特征(或 TLS 指纹),而非签名校验失败

五、根因

billion-context 的 prepareAnthropic() 对所有 Anthropic 协议请求执行:

  1. anthropicToCore() — 解析 Anthropic body 为内部格式
  2. core.processTurn() — ACP kernel 处理(压缩/absorb/nudge)
  3. coreToAnthropic() — 重建 Anthropic messages
  4. delete rebuilt.prompt_cache_key — 无条件删除
  5. JSON.stringify(rebuilt) — 整包序列化替换原始 body

即使 compress.injectTool=false / injectNudge=false 且没有活跃压缩 block,步骤 1→3 的 round-trip 仍会:

  • 改变 JSON 字段顺序(JavaScript 对象属性遍历顺序)
  • 改变空白/换行
  • 删除 prompt_cache_key(如果客户端发送了)
  • 可能添加或移除空数组/空对象

ZCode 服务端检测到 messages body 的结构特征与 ZCode 客户端预期不符,触发 405/3012 反作弊。

复现步骤

  1. ZCode 客户端代理指向 billion-context(HTTP proxy http://127.0.0.1:8787,信任 bili CA)
  2. ZCode 使用 Coding Plan(builtin:bigmodel-coding-plan)发任何对话
  3. bili 日志出现 processTurn: N msgs → 后续 forward POST → zcode-plan/anthropic/v1/messagesupstream 405 / 3012
  4. 同一 session 中,不经 bili 的路径(/agent/configs GET、/event/report POST)正常返回 200

六、修复建议

6.1 建议 A(推荐):内置 per-host 旁路配置

billion-context.json 中增加 per-host / per-origin 的 ACP kernel 旁路配置:

{
  "compress": {
    "bypassHosts": ["zcode.z.ai", "open.bigmodel.cn"]
  }
}

在 kernel 入口条件(dist/index.js:58985)中检查:

if (!opts.passthrough && hopMarker === void 0 && protocol && parsed && typeof parsed === "object"
    && !opts.compress.bypassHosts?.includes(new URL(upstreamOrigin).hostname)) {

这样用户通过 config 声明哪些 upstream 不需要压缩,无需修改 dist 代码。

6.2 建议 B:Anthropic 协议 passthrough 优化

compress.injectTool=false && compress.injectNudge=false && !activeBlocks(session.state) 时,跳过 processTurn 并原样转发 body:

if (!opts.compress.injectTool && !opts.compress.injectNudge
    && session.state.blocks.filter(b => b.active).length === 0) {
  return { body: JSON.stringify(parsed), ... };  // 不走 processTurn
}

这不会影响压缩功能(因为没有活跃 block),但避免了不必要的 body 重建。

6.3 建议 C:blind tunnel 模式

提供 isMitmHost() 可配置排除名单,让用户声明哪些域名不做 MITM(只做 blind TCP tunnel,不解密 TLS、不改 body):

BILI_BLIND_TUNNEL_HOSTS=zcode.z.ai,open.bigmodel.cn

这样 TLS 指纹也是客户端原样的,405 概率最低。


七、总结

根因:billion-context 的 ACP kernel 对 Anthropic 协议请求无条件执行 anthropicToCore → processTurn → coreToAnthropic 重建 messages body,即使 injectTool=false / injectNudge=false。ZCode 服务端检测到 body 结构与客户端预期不符,返回 405/3012。

签名协议限制:V4 Ed25519 签名 payload 不包含 body hash,服务端无法通过签名检测 body 篡改。405 的触发条件是 body 结构 fingerprint(或 TLS 指纹)。

复现:ZCode → bili MITM → Coding Plan messages 接口 → 405/3012。同一 session 中不经 bili 改写的路径正常。

建议:在官方版本中增加 per-host bypassHosts 配置(建议 A),使包更新后配置持续生效。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions