fuqiuluo’s blog

记录美好生活

心情随笔

逆向 Claude Code cch 生成算法

#安全#逆向#Claude Code
type
Post
status
Published
date
May 28, 2026
slug
fuck_claude_code
summary
就是非常非常简单的展示一下,没有别的意思 !
tags
安全
逆向
Claude Code
category
心情随笔
icon
password

环境准备

启动目标claude进程, 然后执行:
最开始是直接用ida的, 但是老是卡死故回归原始人的操作方式!
启动gdb:
进 GDB 后:
然后:
如果端口正常,会看到类似:

插话

这个时候可能会出现:
这是 GDB 在问:要不要启用 debuginfod,自动从 Fedora 的调试符号服务器下载 debuginfo / source / symbols。
也就是它发现系统支持:
然后问:
默认是 n

分析

错误的尝试1

连接后执行:
你会看到类似:
看目标模块 /app/targetOffset 为 0x0 的那一行
所以:
notion image
找这个是因为我在ida看到了这个, 如然后我就兴高采烈想写个读断点看看什么情况, 地址算出来是:
在 GDB 里可以直接这样写,不用手算:
应该输出:
只看读取 (断点):
非常可惜, 屋漏偏逢连夜雨, qswl:
执行一下handle SIGPWR nostop noprint pass 就可以解决问题, 但是0读取, 我尝试
这个结果说明 0x6e88b00 不是 IDA 里那条 .bun:0000000006C88B00 aCch00000 db ' cch=00000;',0 的运行时地址
重新阅读 maps 判断:刚才把 IDA 地址当成 file offset 算了,所以算到了错误地址 0x6e88b00
IDA 里看到的是:
这个 0x6C88B00 更像是 运行时虚拟地址 / VA,不是 file offset。
因为 maps 里这段正好覆盖它:
0x6c88b00 落在这个范围内:
所以先直接在 GDB 里试:
对应函数反编译得到:
很显然它不是 CCH 专用逻辑,只是这次刚好读到了 " cch=00000;"。这个函数本质是:读取 4 个单字节字符,然后把它们扩展成 4 个 UTF-16LE code unit,打包进一个 uint64_t
PS: 还有一个变体, 0x41DF660 这条链路也不是 CCH 算法本体,而是 Bun/JSC/WTF 这类 runtime 的通用字符串 hash / intern / atom hash。

正确的尝试

根据抓包,我们得到:
0x6c88b00 那里是静态占位模板,不会变成 cch=1c9df。真正发出去的 JSON 里,已经在后面的序列化/发送路径里把占位符替换成了最终值。
重新拉取内存maps:
"system":[ 的 ASCII bytes 是:
也就是:
扫这些:
然后扫这一组中小段:
现在找到了两个 ASCII "system":[ 命中点:
清掉旧 watchpoint,否则硬件槽位不够:
忽略干扰信号:
确认内容:
差分/反复调试:
我第一次发hello他是d1178, 后面我发hello2变成了b78be, 来个写断点! 这个地址已经是最终 JSON body buffer,而且同一个位置从 hello 变成 hello2,说明它很可能被复用/重写。现在要抓的是 谁写入 cch=d1178/b78be 那几个字节
先用当前 body 附近找 cch= 的精确地址。
下个断点:
从以上断点信息我们可以分析出来 0x4bb900f 附近是在重建/重置请求 body,把旧的真实 cch 清回 00000 占位符。第二次命中:HTTP Client 写入真实 cch
接下来确认 0x2e0687e 这条线是不是新版 cch 逻辑。可能的歧义是地址口径;同时看一 0x2e0687e 和它所在函数,结论为 0x2e0687e 是cch新版本的完算法完整串起来。它在 sub_2E05B10 里,不是 hash 核心入口,但就是把最后一个 hex 字符写回 cch=00000 的位置。前面的同一函数里做了这些事:
  • 先确认请求路径是 "/v1/messages"
  • 再找 "system"
  • 再在后面 300 字节内找 "cch=00000"
  • 对整份 placeholder body 做 hash
  • 取低 20 bit,转成小写 hex
  • 回填到 cch= 后面的 5 个字符里
关键点是:新版其实没换掉 hash 算法,还是标准 XXH64,只是把 seed 换了。
新 seed 是:0x4D659218E32A3268
这是从 sub_2E05B10 里直接还原出来的:
  • sub_2AD99C0 是标准 XXH64 的 32-byte stripe update
  • 里面直接用了 PRIME64_1 = 0x9E3779B185EBCA87
  • 以及 PRIME64_2 = 0xC2B2AE3D27D4EB4F(反编译里表现成减 0x3D4D51C2D82B14B1,本质一样)
  • 初始化四个 accumulator 正好满足标准 XXH64(seed):
    • acc1 = seed + P1 + P2 = 0xAE4FBA0790EAE83E
    • acc2 = seed + P2 = 0x101840560AFF1DB7
    • acc3 = seed = 0x4D659218E32A3268
    • acc4 = seed - P1 = 0xAF2E18675D3E67E1
  • sub_2917FE0 是 finalize
  • 0x2e06878 写前 4 个 hex 字符,0x2e0687e 写最后 1 个
Loading...