Qoder CLI 逆向分析与 BYOK 解锁
Qoder CLI 逆向分析与 BYOK 解锁
Qoder 的命令行客户端 qodercli 通过 Homebrew 分发,二进制体积约 95MB。对它进行逆向分析后发现,它的 BYOK(Bring Your Own Key)功能存在两层人为限制——组织邮箱白名单校验和在线模型 HTTP 校验。由于 Bun 编译产物中嵌入了完整的 JS 源码,可以通过字符串级的等长替换来绕过这些限制。
确认目标:Bun 编译的二进制
并非所有叫 qodercli 的二进制都是 Bun 编译的。Qoder.app 内嵌的版本(约 38MB,位于 Qoder.app/Contents/Resources/app/resources/bin/aarch64_darwin/qodercli)实际上是 Go 编译的原生程序——file 显示为标准 Mach-O arm64,strings 可以看到大量 Go 运行时特征。
而通过 Homebrew 安装的版本(/opt/homebrew/bin/qodercli,约 95MB)才是 Bun 编译的。它的 Mach-O flags 中有一个关键标志 HAS_TLV_DESCRIPTORS,这是 Bun 二进制的特征。
反编译:使用 bun-demincer
bun-demincer 是一个开源工具,用于反编译 Bun 编译的独立二进制文件。完整流水线包含提取、拆分、匹配第三方库、去混淆四个自动化步骤。
提取嵌入文件
1 | |
这一步解析 Bun 的二进制格式,从 __BUN Mach-O section 中提取所有嵌入文件。结果包含 39 个文件:
| 文件 | 大小 | 说明 |
|---|---|---|
index.js |
14.2 MB | JS 业务代码的打包 bundle(ESM) |
libvips-cpp.8.17.3.dylib |
15.3 MB | sharp 依赖的图像处理动态库 |
rg |
4.3 MB | 内嵌的 ripgrep 二进制 |
sharp-darwin-arm64.node |
256 KB | sharp native addon |
pty.node |
83 KB | node-pty native addon |
6 个 .sb 文件 |
~19 KB | macOS sandbox 配置 |
4 个 SKILL.md |
~44 KB | 内置 AI 技能定义 |
chat.proto |
8 KB | Protobuf 协议定义 |
所有模块的 bytecode size 均为 0,sourcemap size 也为 0——说明编译时既没有启用字节码选项,也没有将 sourcemap 打包进来。
拆分与去混淆
1 | |
14.2MB 的 index.js 被拆分为 3712 个独立模块(1750 CJS + 1962 ESM)。经指纹匹配,其中 746 个为第三方库代码(ajv、zod、ink、@anthropic-ai/sdk 等),剩余约 2600 个是应用自身逻辑。
去混淆流水线执行结构变换(!0 → true、void 0 → undefined)、自动重命名、代码格式化等步骤,最终得到可读的 JS 源码。
BYOK 限制机制
BYOK 功能允许用户配置自定义 provider(如 DashScope、自定义 OpenAI 端点),但 qodercli 对其施加了两层限制:
- 组织邮箱检查:要求
allow_byok >= 2或邮箱后缀为@alibaba-inc.com,否则自定义模型在 TUI 中不可见 - 在线模型校验:对自定义模型调用
/algo/api/v2/byok/check接口进行 HTTP 校验,失败则模型不可用
补丁设计
Bun 编译产物中的 JS 源码以纯文本形式嵌入,因此只需保证替换前后字节数严格相等,即可在不破坏 Mach-O 结构的前提下修改程序逻辑。
P1:绕过邮箱白名单
原代码中存在条件判断:
1 | |
补丁将域名装入注释块:
1 | |
替换后 "".endsWith("") 始终返回 true,BYOK 界面入口解锁。
P2:阻断在线校验请求
校验函数中,实际 HTTP 请求通过 e_() 发起,其 options 对象包含 path 属性:
1 | |
将 path 的值替换为一个立即抛出的 IIFE:
1 | |
对象构造阶段即抛出异常——HTTP 请求从未发起,因为 throw 发生在 e_() 被调用之前。异常被函数已有的 try-catch 捕获。
/algo/api/v2/byok/check 在二进制中出现两次,加上 path: 前缀后仅匹配 options 对象内的那一处,不会误伤 prepareRequest 的参数。
P3:伪造校验结果
P2 让异常被 catch 捕获,但 catch 块返回 !1(Bun minify 的 false),TUI 仍认为校验失败。需要将其改为 !0(true):
1 | |
!1 在整个二进制中出现成千上万次,直接全局替换会导致灾难性后果。通过唯一的日志字符串锚点精确定位到 catch 块的返回值。
整体执行流程
1 | |
签名与验证
二进制修改后需要重新签名,否则 macOS 拒绝运行:
1 | |
补丁脚本支持幂等执行——检测到已替换的特征时自动跳过,备份仅在首次修改前创建。
意外发现:请求代理与隐私问题
最初我只应用了 P1,绕过邮箱白名单后就能在 TUI 中配置自定义模型了。填入其他云服务商的 URL 和 API Key 时,一切正常。
但当我尝试填写本地 CLIProxyAPI 的地址(http://127.0.0.1:8317)时,始终返回校验失败——而我确认服务已正常启动。于是添加了 P2 和 P3,尝试绕过在线校验环节。模型确实可以选择了,但发送消息时仍然报错:

报错信息中有几个关键细节:
Failed to forward request——“转发请求失败”,说明请求并非由客户端直接发往我配置的端点,而是经过了一个中间层转发dial tcp 127.0.0.1:8317: connect: connection refused——连接 localhost 失败。但这个连接失败显然不是发生在我本地(服务确实在跑),而是发生在 Qoder 的服务端——它拿着我配置的127.0.0.1:8317在服务器上尝试连接,自然连不上
结合这些现象,结论很明确:Qoder 的 BYOK 并非客户端直连模式,而是将用户配置的 API Key 和端点地址上传至 Qoder 服务端,由服务端代理发起实际的推理请求。这意味着用户的 API Key 会完整地经过 Qoder 的服务器——无论你配置的是 OpenAI、Anthropic 还是其他任何服务商的密钥。
更进一步,既然所有请求都经由 Qoder 服务端代理,那么用户与模型之间的完整对话内容也对 Qoder 完全透明。考虑到 Qoder 背后是阿里巴巴,而阿里同时在训练自家的 Qwen 系列模型,有充分理由怀疑这些对话数据会被用于模型训练。
这是一个严重的隐私问题。建议不要再使用 Qoder。