Claude Code 二进制里的 Unicode 隐写:用 XOR 还原检测代码的完整实验

发布时间:2026/10/12 21:53:36
Claude Code 二进制里的 Unicode 隐写:用 XOR 还原检测代码的完整实验
1. 从 claude.exe 里挖出 Unicode 隐写一次本地可复现的 XOR 还原实验Claude Code 二进制里藏检测代码这件事真正值得动手复现的不是有没有这个结论而是怎么从 225 MB 的 PE 可执行文件里把 JavaScript 源码捞出来、怎么认出 Unicode 隐写、怎么用 XOR 把黑名单解出来。这套方法本身是通用的Bun SEASingle Executable Application把 JS 源码以字符串形式嵌进原生二进制不做反编译也能直接搜。你只要会fs.readFileSync加indexOf就能在本地把整条链路走一遍。这篇写给三类人一是想验证自己装的 Claude Code 版本里到底有没有这段逻辑的开发者二是对二进制字符串提取、XOR 解码、Unicode 隐写这类逆向基础手法感兴趣的安全学习者三是想搞清楚隐写标记到底怎么被服务端读出来的技术好奇者。全程在本地跑不需要联网抓包也不需要任何特殊网络环境一个 Node.js 环境加一个claude.exe就够了。我会按定位文件 → 提取字符串 → 识别 XOR 线索 → 试探密钥 → 还原黑名单 → 比对隐写字符的顺序走每一步都给可复制的脚本和实际输出。踩过的坑也会标出来比如为什么直接toString(utf8)会丢字符、为什么 XOR 密钥要从91这个常量反推而不是暴力枚举。2. TaoToken 前置把还原出来的检测逻辑接到可控的模型端点上还原出检测逻辑之后下一步自然是那我怎么在一个干净、可控的环境里继续用 Claude 系模型做编码。这里就涉及接入层的事。TaoToken 提供的是兼容 Anthropic 与 OpenAI 风格的统一 API 入口你可以把它理解成一个模型网关本地工具Claude Code、Cline、Codex 等把请求发到统一 Base URL由网关路由到具体模型。对做这类逆向实验的人来说它的价值在于请求端点、模型 ID、密钥三者都是你自己配的链路透明方便对照观察 System Prompt 里到底带了什么。先把几个关键地址记下来后面配置要用官网入口https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentAPI 根地址https://taotoken.net/api模型对话验证模型是否通https://taotoken.net/api/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewriteCoding Plan长期编码/Agent 场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 接入说明https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite为什么做这个实验要先把接入层理清因为原版检测逻辑里有个关键分支NN1()会读process.env.ANTHROPIC_BASE_URL取出 hostname 去匹配那 147 条黑名单。也就是说你用什么 Base URL直接决定了known这个标记位是 true 还是 false。如果你把 Base URL 指向一个自建或可信的网关known分支就不会命中黑名单隐写字符会退化成正常撇号。这本身就是验证隐写是否生效的一个对照实验同一份二进制换 Base URL观察 System Prompt 里撇号码点的变化。拿 Key 的流程不复杂进控制台 → API Keys → 新建 → 复制。但注意Key 只在创建时完整显示一次关掉页面就只剩掩码所以建完立刻存到本地环境变量或密码管理器。我一般这么放# Linux / macOS写进 ~/.zshrc 或 ~/.bashrc export TAOTOKEN_API_KEYsk-你的key export ANTHROPIC_BASE_URLhttps://taotoken.net/api export ANTHROPIC_AUTH_TOKEN$TAOTOKEN_API_KEY# Windows PowerShell写进 $PROFILE $env:TAOTOKEN_API_KEY sk-你的key $env:ANTHROPIC_BASE_URL https://taotoken.net/api $env:ANTHROPIC_AUTH_TOKEN $env:TAOTOKEN_API_KEY这里有个容易混的点Claude Code 认的是ANTHROPIC_BASE_URLANTHROPIC_AUTH_TOKEN或ANTHROPIC_API_KEY而很多第三方工具认的是OPENAI_BASE_URLOPENAI_API_KEY。做实验时建议两套都显式设好避免工具读错变量导致 401。设完之后用echo $ANTHROPIC_BASE_URL确认一下别让旧的代理配置残留——这一点在排障章节还会展开。3. 可复制配置定位二进制、提取字符串、还原 XOR 黑名单这一节是全文的技术核心全部可复制。先明确目标文件路径。Claude Code 用 Bun SEA 打包Windows 下典型位置是D:\nodejs\node_global\node_modules\anthropic-ai\claude-code\bin\claude.exemacOS / Linux 下通常是/usr/local/bin/claude ~/.npm/_npx/node_modules/anthropic-ai/claude-code/bin/claude注意bin/claude有时是个几十 KB 的启动器launcher真正的 225 MB 二进制在node_modules里。所以脚本里要先判断文件大小小于 10 MB 就顺着目录往上找真正的二进制。下面这个find_code.cjs就是干这个的同时扫描关键字符串// find_code.cjs —— 定位并扫描 claude 二进制中的检测代码关键词 const fs require(fs); const path require(path); const { execSync } require(child_process); function resolveRealBinary(p) { try { if (fs.statSync(p).size 10 * 1024 * 1024) return p; } catch {} const dir path.dirname(p); const exe process.platform win32 ? .exe : ; for (const rel of [ node_modules/anthropic-ai/claude-code/bin/claude exe, ../node_modules/anthropic-ai/claude-code/bin/claude exe, ]) { const full path.resolve(dir, rel); try { if (fs.statSync(full).size 10 * 1024 * 1024) return full; } catch {} } return null; } function scanFile(exePath) { if (!fs.existsSync(exePath)) { console.error(文件不存在:, exePath); process.exit(1); } const data fs.readFileSync(exePath); const text data.toString(utf8); const stats fs.statSync(exePath); console.log(文件:, exePath); console.log(大小:, (stats.size / 1024 / 1024).toFixed(1), MB); console.log(---\n); const keywords [ Asia/Shanghai, Asia/Urumqi, function EN1(), function hN1(H,q), function ff7(H), function _f7(H), function NN1(), cnTZ, labKw, DO NOT mention this, ]; let found 0; keywords.forEach(k { const idx text.indexOf(k); if (idx 0) { found; const start Math.max(0, idx - 20); const end Math.min(text.length, idx k.length 80); let ctx ; for (let i start; i end; i) { const c text.charCodeAt(i); ctx (c 0x20 c 0x7E) ? text[i] : .; } console.log(✓ ${k}\n ${ctx}\n); } else { console.log(✗ ${k} → 未找到\n); } }); console.log(---\n结论: found / keywords.length 命中); } const arg process.argv[2]; if (arg) { scanFile(resolveRealBinary(arg) || arg); } else { // 简化版直接尝试 npm root -g try { const root execSync(npm root -g, { encoding: utf8 }).trim(); const exe process.platform win32 ? .exe : ; scanFile(root /anthropic-ai/claude-code/bin/claude exe); } catch (e) { console.error(未找到请手动指定路径: node find_code.cjs path); } }跑起来node find_code.cjs # 或指定路径 node find_code.cjs D:\nodejs\node_global\node_modules\anthropic-ai\claude-code\bin\claude.exe命中Asia/Shanghai、cnTZ、labKw、function EN1()这些关键词就说明这段逻辑在。接下来是还原 XOR 黑名单。原逻辑里有个常量var GN1 91;这是 XOR 密钥配合 Base64 使用// decode_blacklist.cjs —— 还原 Base64 XOR(91) 编码的域名黑名单 // 密文需要从二进制里提取这里用占位符演示流程 const GN1 91; // XOR 密钥来自二进制中的常量 function _f7(H) { const q Buffer.from(H, base64); let $ ; for (const K of q) $ String.fromCharCode(K ^ GN1); return $.split(,); } // 从 claude.exe 中搜索形如 aHR0c... 的长 Base64 串替换到下面 const cipher 把从二进制里提取到的Base64密文粘到这里; const list _f7(cipher); console.log(解密后条目数:, list.length); console.log(list.slice(0, 20).join(\n));怎么找密文在二进制里搜_f7(或GN1附近的字符串通常能看到一个很长的、只含A-Za-z0-9/的串。把它整段复制出来。如果密钥不是 91可以写个小循环试探// 密钥试探对同一段密文尝试 0-255看哪个密钥解出可读域名 const cipher 你的Base64密文; const buf Buffer.from(cipher, base64); for (let key 0; key 256; key) { let s ; for (const b of buf) s String.fromCharCode(b ^ key); // 可读域名通常含 .com / .cn / .ai 且大部分是可打印字符 const printable [...s].filter(c c.charCodeAt(0) 0x20 c.charCodeAt(0) 0x7E).length; if (printable / s.length 0.95 /\.(com|cn|ai|net|org)/.test(s)) { console.log(候选密钥:, key, →, s.slice(0, 120)); } }实测下来密钥 91 解出来的结果里域名占比很高且逗号分隔规整基本一眼能确认。这一步的关键是先看可打印字符比例再看是否含域名后缀两个条件同时满足才值得人工核对否则 256 个候选里会有一堆噪声。4. 验证请求与成功结果对照隐写字符的码点变化还原出黑名单只是第一步真正要验证的是隐写标记到底怎么被写进 System Prompt。原逻辑里hN1(H, q)根据known和labKw两个布尔值返回四个不同的撇号字符knownlabKw返回字符Unicode 码点视觉表现falsefalseU0027正常撇号truefalse’U2019右单引号falsetrueʼU02BC修饰字母撇号truetrueʹU02B9修饰字母上撇号这四个字符在终端里几乎看不出区别但码点完全不同。验证方法就是构造一个最小请求把 System Prompt 打出来逐字符看码点。下面这段脚本模拟ff7的拼接逻辑并打印每个字符的码点// verify_stego.cjs —— 复现日期字符串拼接并检查撇号码点 function hN1(known, labKw) { if (!known !labKw) return ; // U0027 if (known !labKw) return \u2019; // U2019 if (!known labKw) return \u02BC; // U02BC return \u02B9; // U02B9 } function ff7(dateStr, known, labKw, cnTZ) { const $ hN1(known, labKw); const K cnTZ ? dateStr.replaceAll(-, /) : dateStr; return Today${$}s date is ${K}.; } // 场景 A非中国时区无代理 console.log(A:, ff7(2026-06-30, false, false, false)); // 场景 B中国时区命中黑名单 const b ff7(2026-06-30, true, false, true); console.log(B:, b); console.log(B 的码点:, [...b].map(c U c.codePointAt(0).toString(16).toUpperCase().padStart(4, 0)).join( ));跑出来的结果场景 A 是Todays date is 2026-06-30.撇号是 U0027日期用横杠场景 B 是Today’s date is 2026/06/30.撇号变成 U2019日期分隔符变成斜杠。服务端只要检查这两个特征就能还原出known和cnTZ两个状态位——这就是隐写的全部机制没有独立上报通道标记搭在本来就存在的 System Prompt 里。要验证真实请求里带的是什么可以在接入层加一个本地日志代理或者直接用 TaoToken 的模型对话页面发一条请求观察返回是否正常。如果只是想确认模型端点通不通用 curl 打一发curl https://taotoken.net/api/v1/messages \ -H x-api-key: $TAOTOKEN_API_KEY \ -H anthropic-version: 2023-06-01 \ -H content-type: application/json \ -d { model: claude-sonnet-4-5, max_tokens: 64, messages: [{role: user, content: reply with OK only}] }返回里带content数组且stop_reason正常说明 Base URL、Key、Model ID 三件套配对了。这一步的意义在于把接入层固定下来之后你才能确定 System Prompt 里的隐写字符是本地二进制注入的而不是网关改的。对照实验的变量控制就在这里。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth做这类实验最容易卡在环境配置上而不是逆向本身。下面几个报错我都遇到过按现象对号入座。401 Unauthorized / invalid x-api-key。九成是 Key 没设对或变量名写错。Claude Code 读ANTHROPIC_AUTH_TOKEN有些工具读ANTHROPIC_API_KEY还有的读OPENAI_API_KEY。先echo一遍确认非空再确认没有多余空格或引号。如果 Key 是从网页复制的注意别把首尾的换行带进去。另外Key 只在创建时完整显示如果你存的是掩码版本必然 401回控制台重新建一个。local proxy failed / connection refused。这个报错通常出现在你设了ANTHROPIC_BASE_URL指向本地某个端口但那个本地服务没起来。做隐写实验时如果你用本地日志代理抓 System Prompt代理挂了就会报这个。排查顺序先curl一下那个本地地址看通不通再确认代理进程还在。如果不用本地代理了记得unset ANTHROPIC_BASE_URL否则 Claude Code 会一直往一个死端口发请求。reading choices of undefined。这是 OpenAI 风格响应解析失败的典型报错多出现在用 OpenAI 兼容工具打 Anthropic 风格端点或反过来的时候。choices是 OpenAI 的字段Anthropic 返回的是content。检查你的工具期望哪种格式Base URL 是否带了正确的路径后缀有的要/v1有的不要。TaoToken 的接入文档里对两种风格都有说明对照着改。OAuth / authentication_error。Claude Code 某些版本会走 OAuth 流程如果你同时设了ANTHROPIC_AUTH_TOKEN和 OAuth 凭据可能冲突。做实验时建议清掉旧的 OAuth 缓存通常在用户目录的配置文件夹里只保留环境变量这一条路径减少变量。Codex auth.json / Cline MCP 配置。如果你用 Codex 或 Cline 做对照实验注意它们的配置文件位置不同。Codex 读~/.codex/auth.jsonCline 的 MCP 配置在 VS Code 的设置里。无论哪个Base URL、Key、Model ID 三件套必须同时写全缺一个都会报错。以 Codex 为例{ base_url: https://taotoken.net/api, api_key: sk-你的key, model: claude-sonnet-4-5 }Cline 的 MCP 配置则是 JSON 里嵌mcpServers每个 server 单独给command、args、env。env 里把ANTHROPIC_BASE_URL和ANTHROPIC_AUTH_TOKEN显式传进去别指望它继承 shell 环境。最后一个坑二进制里搜不到关键词不代表没有检测代码。Bun SEA 打包时可能对字符串做了压缩或分片function EN1()这种带空格的写法可能变成function EN1(){或干脆被 minify 掉。所以扫描时除了精确匹配也试试EN1、cnTZ这种短标识符。如果短标识符也搜不到再考虑版本差异——v2.1.91 之前和 v2.1.197 之后本来就没有这段逻辑。6. 继续用 Claude 系模型做编码把接入层固定成可复现的基线实验做完你手里应该有三样东西一份从二进制提取的关键词命中报告、一份 XOR 还原出的域名列表、一份隐写字符码点的对照输出。这三样构成了一个可复现的基线。之后无论你升级 Claude Code、换接入端点还是换工具都可以拿这套脚本重新跑一遍看基线有没有变化。如果你打算长期用 Claude 系模型做编码或 Agent 任务建议把接入层固定下来别每次手动改环境变量。TaoToken 的 Coding Plan 就是为这种长期场景准备的配好之后 Base URL 和 Key 一次设定Claude Code、Cline、Codex 都能复用同一套凭据。具体接入步骤在 Claude Code 接入说明里有模型 ID 和路径后缀的对应关系在接入文档里查。验证模型是否可用最直接的是模型对话页面发一条短请求看返回。需要管理多个 Key 或看用量去控制台。新建或轮换 Key 在 API Keys 页面。这几个入口分工明确做实验时按需取用就行。最后留一个实用习惯每次升级 Claude Code 之后先跑一遍find_code.cjs把输出存成带日期的文件比如scan_20260703.txt。这样你就有了一条时间线哪个版本引入了什么、哪个版本移除了什么一目了然。逆向这件事单次结论价值有限可复现的对照记录才是真正有用的东西。