Claude Code 2.1.88 逆向笔记:fingerprint 和 cch 是怎么回事
Claude Code 2.1.88 逆向笔记:fingerprint 和 cch
首先,Anthropic 是 ** 🤣
26年开年先给Opencode ban了,然后开始逐渐加紧各种审查
开始逐渐封禁使用第三方反代,中转(如opencode-claude-auth, sub2api)等服务的账号
- 开号?杀!
- 充值?杀!
- 正常使用?杀!
及时是一名个人用户也要小心再小心,各种遥测,数据发送 A\比任何人都知道你的环境
作为用户还是开发者,却不让自定义自己的开发环境,被迫使用claudecode,还要忍受claude code的 遥测。
我相信没有一名开发者愿意接受。
通过mitm抓包官方claudecode得到了以下遥测内容
时区,语言,系统,并会计算每一个设备的uuid
社区上有一个一直已来的未解之谜和缓存问题,就是cch字段是干什么的?有哪些因素会影响缓存命中率?
并且社区的各种开源 中转/反代都没有做以下字段的处理 这很有可能是导致 被封号和缓存异常的 主要原因!
本次逆向感谢codex的帮助👍
逆向anthropic的时候没道德负罪感了() OAI:什么道德审查,干死友商是第一目的(误)
本文逆向 Claude Code 2.1.88 的客户端,主要想搞清楚:
cc_version=2.1.88.xxx后面那三位 hex 到底怎么算的?x-anthropic-billing-header里那个cch五位 hex 又是什么?
这俩值有什么区别?
通过抓包表面上看都是短 hex 串,但性质不一样:
- fingerprint 跟在版本号后面,同样的 prompt 跑多次基本不变。更像是一种请求归因标记。
- cch 在 billing header 里,每次运行都可能不同。明显是运行时动态算出来的。
用了什么材料
感谢 anthropic 运维事故送上 cc 源码 🙏
四类东西:
npm 包和 sourcemap — 官方 @anthropic-ai/claude-code@2.1.88 自带 cli.js.map,能还原出不少 TypeScript 源码片段。fingerprint 的逻辑基本全在这一层。
本地装的官方二进制 — 装完以后拿到的是个 ELF,没 strip 干净,带个大 .bun 段。
动态调试 — gdb 断点、watchpoint、LD_PRELOAD tracer。
本地验证 — 把恢复出来的算法跑一遍,和实际抓到的值对比。
fingerprint:纯 JS 层,比较简单
这部分完全能从公开的 sourcemap 里恢复。
大致逻辑是:从第一条用户消息里取几个固定位置的字符,拼上一个内置盐值和版本号,做一次常规哈希,截取前几位。
因为输入基本固定(同一条消息、同一个版本),所以算出来的值也很稳定。没什么花活。
cch:坑在 JS 层和 native 层的分界线上
这个才是真正费时间的部分。
一开始被 sourcemap 误导了
从 cli.js.map 里恢复出来的代码显示,JS 层构造 attribution header 时,cch 的值就是写死的 00000。
如果只看这一层,很容易得出”cch 就是个固定占位符”的结论。但实际抓包看到的 cch 明显不是零——每次都是不同的非零 hex。
这说明 JS 层只负责占位,真正的计算发生在更底层。
试过一堆错误方向
在搞清楚真正算法之前,我试过很多常见思路:
- 拿 request-id、session-id 去算各种常见哈希(SHA-256、MD5、CRC32、Adler32、BLAKE2s……)
- 拿 body 去算零种子的哈希
- 试各种截取方式(高位、低位、中间位)
全都对不上。
转折点:盯住那块 buffer
后来换了思路,不猜算法了,直接去追”到底是哪一步把 00000 改成了非零值”。
用 gdb 和 LD_PRELOAD tracer 盯住包含 cch=00000 的那块请求 body 缓冲区。终于在某一次命中里看到了:
- 写入前,buffer 里是
cch=00000 - 写入后,同一块 buffer,同一个位置,变成了一个非零的五位 hex
关键是——同一块 buffer,原地覆盖,长度不变。
这就解释了为什么 JS 层要预留 00000:五个零和五个 hex 字符长度一样,不用改 Content-Length,不用重新分配内存。设计上很干净。
缩到具体写入指令
继续往里追,把写入点定位到了具体的地址。写法不是 memcpy 一个现成字符串进去,而是先得到一个 64 位数值结果,再按 nibble 拆开转成 hex 字符逐个写入。
确认输入
在核心哈希路径被调用之前,抓到了传入的参数:一个指向完整请求 body 的指针和它的长度。此时 body 里的 cch 仍然是 00000。
所以输入就是:完整请求 body 的原始字节(cch 槽位保持为零占位符的状态)。
确认算法
静态分析 native 代码,看到了一组特征非常明确的状态初始化常量和 update/finalize 流程——是一种带种子的非密码学哈希。
闭环验证
最终在同一次运行里完成了闭环:
- 写入前,gdb 抓到计算结果寄存器的值
- 写入后,body 里 cch 变成了该值低位对应的 hex
- 用同一次运行 dump 出的 prewrite body,本地跑同样的算法,结果和寄存器值完全一致
真正难的地方
现在回头看,算法本身其实不复杂。真正卡人的是两件事:
分层问题。JS 层和 native 运行时层各干各的。如果不意识到这一点,就会一直困在”sourcemap 里只有 00000 但实际不是零”这个矛盾里出不来。
字节级一致性。就算算法完全正确,只要最终 body 在字节级上有任何差异——字段顺序不同、转义方式不同、空白字符不同、数值序列化方式不同——算出来的值就会不一样。所以 cch 本质上是对最终发送 body 的完整校验,不是对某几个业务字段的摘要。
结语
Claude 是好模型,ClaudeCode 是好工具
但是沾上 Anthropic 😢
如果后续有继续更新对抗和检测指纹,本文将持续跟踪