Claude Code 有个 /buddy 命令,会根据你的账户 ID 孵化一只陪你写代码的小动物。稀有度五档,legendary shiny 的概率是万分之一。
我的默认宠物是一只 common axolotl——最低档,无闪光。我想要传说闪光卡皮巴拉。
于是我把这个系统的漏洞原理告诉了 Claude,让它去 190MB 的 Claude Code 二进制里找算法。它自己定位到了打包进去的 JS bundle,读懂了压缩混淆的代码,写了枚举脚本,还在第一版算法出错后自己设计实验修正。最终 14400 次枚举,0.04 秒,传说闪光卡皮巴拉出来了。
漏洞在哪里
/buddy 宠物由种子哈希决定——同一个种子永远孵化同一只动物。种子的来源有优先级:
- 优先用
accountUuid——Anthropic 服务器下发的账户唯一标识,绑定账户,无法伪造 - 没有
accountUuid 的话,fallback 到 ~/.claude.json 里的 userID 字段 - 连
userID 都没有,用 "anon"
关键在第二条:**userID 是本地文件里的字段,你可以随便改。**
什么情况下没有 accountUuid?用第三方 API(自定义 API key 或 proxy)的用户,配置文件里天然没有这个字段。订阅用户(Claude Max)正常登录会写入,但用 CLAUDE_CODE_OAUTH_TOKEN 环境变量方式启动时不会写——这是另一个大 V 发现的绕过路径。
这只涉及本地配置文件里的一个字段,不涉及任何服务器端数据。
操作步骤
如果你正在用 Claude Code,最省事的做法是直接把这篇文章发给它,告诉它你想要什么宠物,让它来判断你的情况、跑脚本、改配置文件。这篇文章里的卡皮巴拉,就是这么来的。
下面是手动步骤。
第一步:确认你的情况
1 2 3 4 5 6
| python3 -c " import json, os d = json.load(open(os.path.expanduser('~/.claude.json'))) uuid = d.get('oauthAccount', {}).get('accountUuid') print('订阅用户,需要先绕过 accountUuid(见文末)' if uuid else '第三方 API 用户,直接操作') "
|
第二步:找到目标 userID
需要安装 Bun(Claude Code 本身依赖 Bun,一般已经装了)。
把下面的脚本保存为 buddy-reroll.js,把 TARGET 改成你想要的宠物,运行 bun buddy-reroll.js:
buddy-reroll.js(核心脚本,点击展开)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| const SALT = "friend-2026-401"; const SPECIES = ["duck","goose","blob","cat","dragon","octopus","owl","penguin", "turtle","snail","ghost","axolotl","capybara","cactus","robot", "rabbit","mushroom","chonk"]; const RARITIES = ["common","uncommon","rare","epic","legendary"]; const RARITY_WEIGHTS = { common: 60, uncommon: 25, rare: 10, epic: 4, legendary: 1 }; const STATS = ["DEBUGGING","PATIENCE","CHAOS","WISDOM","SNARK"]; const STAT_BASE = { common:5, uncommon:15, rare:25, epic:35, legendary:50 };
function hash(str) { return Number(BigInt(Bun.hash(str)) & 0xffffffffn); } function makeRng(seed) { let s = seed >>> 0; return function() { s |= 0; s = s + 1831565813 | 0; let q = Math.imul(s ^ s >>> 15, 1 | s); q = q + Math.imul(q ^ q >>> 7, 61 | q) ^ q; return ((q ^ q >>> 14) >>> 0) / 4294967296; }; } function R0H(rng, arr) { return arr[Math.floor(rng() * arr.length)]; } function pickRarity(rng) { let r = rng() * 100; for (const rarity of RARITIES) { r -= RARITY_WEIGHTS[rarity]; if (r < 0) return rarity; } return "common"; } function rollStats(rng, rarity) { const base = STAT_BASE[rarity]; let K = R0H(rng, STATS), O = R0H(rng, STATS); while (O === K) O = R0H(rng, STATS); const result = {}; for (const T of STATS) { if (T === K) result[T] = Math.min(100, base + 50 + Math.floor(rng() * 30)); else if (T === O) result[T] = Math.max(1, base - 10 + Math.floor(rng() * 15)); else result[T] = base + Math.floor(rng() * 40); } return result; } function roll(userID) { const rng = makeRng(hash(userID + SALT)); const rarity = pickRarity(rng); const species = R0H(rng, SPECIES); rng(); if (rarity !== "common") rng(); const shiny = rng() < 0.01; rollStats(rng, rarity); return { rarity, species, shiny }; } function randomHex64() { let s = ""; for (let i = 0; i < 64; i++) s += Math.floor(Math.random() * 16).toString(16); return s; }
const TARGET = { species: "capybara", rarity: "legendary", shiny: true };
let attempts = 0; const start = Date.now(); while (true) { const userID = randomHex64(); const result = roll(userID); attempts++; if (attempts % 500000 === 0) process.stdout.write(`\r${(attempts/1e6).toFixed(1)}M attempts...`); if (result.species === TARGET.species && result.rarity === TARGET.rarity && result.shiny === TARGET.shiny) { console.log(`\nFound after ${attempts} attempts (${((Date.now()-start)/1000).toFixed(2)}s)`); console.log(`userID: ${userID}`); process.exit(0); } }
|
物种可选:duck goose blob cat dragon octopus owl penguin turtle snail ghost axolotl capybara cactus robot rabbit mushroom chonk
稀有度可选:common uncommon rare epic legendary
0.04 秒内输出:
1 2
| Found after 14400 attempts (0.04s) userID: b625d73f2b07bbb7b108571274a4ac64bf65031967e99c773288f1b8452d0948
|
第三步:写入配置文件
1
| cp ~/.claude.json ~/.claude.json.bak
|
1 2 3 4 5 6 7 8 9 10 11
| import json, os
path = os.path.expanduser('~/.claude.json') with open(path, 'r') as f: d = json.load(f)
d['userID'] = '这里填脚本输出的 userID' d.pop('companion', None)
with open(path, 'w') as f: json.dump(d, f, indent=2)
|
companion 字段是已孵化宠物的缓存,不删的话 Claude Code 直接读旧数据,不会重新算。
第四步:重开会话,输入 /buddy
订阅用户的额外步骤
正常登录后 accountUuid 会写入配置文件,宠物种子被锁定,userID 字段会被忽略。绕过方式是用 CLAUDE_CODE_OAUTH_TOKEN 环境变量启动——这种方式不会写入 accountUuid:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| claude setup-token
cp ~/.claude.json ~/.claude.json.bak python3 -c " import json, os path = os.path.expanduser('~/.claude.json') d = json.load(open(path)) d.pop('oauthAccount', None) d.pop('companion', None) json.dump(d, open(path, 'w'), indent=2) "
CLAUDE_CODE_OAUTH_TOKEN=<你的token> claude
|
然后再执行上面的第二、三、四步。
Claude 逆向自己的过程
这件事的起因是我看到有人发现了这个漏洞——原理我懂了,但具体的哈希算法不知道,没法直接枚举。于是我把原理告诉了 Claude,问它能不能帮我搞定。
接下来的事情是我在旁边看着发生的。
在 190MB 二进制里找代码
Claude Code 的可执行文件是 190MB 的 Mach-O 二进制,没有公开源码。Claude 先用 grep -oba 在二进制里搜索 buddy 关键词的字节偏移,定位到几个集中出现的区域,再用 dd 把那段内容提取出来,用 strings 过滤可读文本。
在压缩混淆的 JS bundle 里,它找到了物种列表(每个物种名用 String.fromCharCode 编码成数字数组)、稀有度权重、salt 常量、哈希函数、PRNG,以及完整的宠物生成逻辑。
第一版脚本出错,自己修正
Claude 没有猜测算法,而是设计了一个校准实验:把 userID 改成 64 个 a,让我去 Claude Code 里跑 /buddy 看实际结果,再用脚本算同一个值比对——如果两边完全一致,说明算法还原正确。
第一版用了 FNV-1a 哈希——源码里确实有这个实现,是 Node.js 环境的 fallback。结果:脚本算出来是 uncommon capybara,实际显示是 uncommon robot,不匹配。rarity 对了,species 错了,说明哈希值本身就不对,后续所有随机数都跑偏了。
原因:Claude Code 实际运行在 Bun 上,走的是 Bun.hash(Wyhash 算法),不是 FNV-1a。换掉之后,rarity、species、全部五项 stats 数值完全一致。
14400 次,0.04 秒
算法校准后,枚举就是纯体力活了。legendary 概率 1/100,shiny 概率 1/100,同时出现是万分之一。14400 次命中,和理论期望吻合。
算法细节
种子选取逻辑
1 2 3 4 5 6 7 8 9 10
| function oS6() { let H = z_(); return H.oauthAccount?.accountUuid ?? H.userID ?? "anon"; }
function rS6(userID) { return yI4(GI4(ZI4(userID + hI4))); }
|
哈希:Bun.hash(Wyhash)
1 2 3 4 5 6 7 8 9
| function ZI4(H) { if (typeof Bun !== "undefined") return Number(BigInt(Bun.hash(H)) & 0xffffffffn); let _ = 2166136261; for (let q = 0; q < H.length; q++) _ ^= H.charCodeAt(q), _ = Math.imul(_, 16777619); return _ >>> 0; }
|
PRNG:Mulberry32 变体
1 2 3 4 5 6 7 8 9
| function GI4(H) { let _ = H >>> 0; return function() { _ |= 0; _ = _ + 1831565813 | 0; let q = Math.imul(_ ^ _ >>> 15, 1 | _); q = q + Math.imul(q ^ q >>> 7, 61 | q) ^ q; return ((q ^ q >>> 14) >>> 0) / 4294967296; }; }
|
生成顺序
1 2 3 4 5 6 7 8 9
| function yI4(rng) { const rarity = pickRarity(rng); const species = R0H(rng, Dsq); const eye = R0H(rng, Msq); const hat = rarity === "common" ? "none" : R0H(rng, Psq); const shiny = rng() < 0.01; const stats = vI4(rng, rarity); return { rarity, species, eye, hat, shiny, stats }; }
|
枚举脚本必须完整模拟这个序列(包括 stats),否则 shiny 的 rng 调用位置对不上。
物种列表
物种名在二进制里用字符码编码:
1
| lk_ = String.fromCharCode(99,97,112,121,98,97,114,97)
|
18 种:duck, goose, blob, cat, dragon, octopus, owl, penguin, turtle, snail, ghost, axolotl, capybara, cactus, robot, rabbit, mushroom, chonk