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
node src/extract.mjs /opt/homebrew/bin/qodercli extracted/

这一步解析 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
2
3
node src/resplit.mjs extracted/index.js resplit/
cp -r resplit/ decoded/
node src/deobfuscate.mjs --dir decoded/

14.2MB 的 index.js 被拆分为 3712 个独立模块(1750 CJS + 1962 ESM)。经指纹匹配,其中 746 个为第三方库代码(ajv、zod、ink、@anthropic-ai/sdk 等),剩余约 2600 个是应用自身逻辑。

去混淆流水线执行结构变换(!0truevoid 0undefined)、自动重命名、代码格式化等步骤,最终得到可读的 JS 源码。

BYOK 限制机制

BYOK 功能允许用户配置自定义 provider(如 DashScope、自定义 OpenAI 端点),但 qodercli 对其施加了两层限制:

  1. 组织邮箱检查:要求 allow_byok >= 2 或邮箱后缀为 @alibaba-inc.com,否则自定义模型在 TUI 中不可见
  2. 在线模型校验:对自定义模型调用 /algo/api/v2/byok/check 接口进行 HTTP 校验,失败则模型不可用

补丁设计

Bun 编译产物中的 JS 源码以纯文本形式嵌入,因此只需保证替换前后字节数严格相等,即可在不破坏 Mach-O 结构的前提下修改程序逻辑。

P1:绕过邮箱白名单

原代码中存在条件判断:

1
this.authManager.getUserInfo()?.email?.endsWith("@alibaba-inc.com")

补丁将域名装入注释块:

1
2
搜索:  endsWith("@alibaba-inc.com")      (28 bytes)
替换: endsWith(""/* */) (28 bytes)

替换后 "".endsWith("") 始终返回 true,BYOK 界面入口解锁。

P2:阻断在线校验请求

校验函数中,实际 HTTP 请求通过 e_() 发起,其 options 对象包含 path 属性:

1
2
3
4
5
e_({
operation: "checkBYOKModel",
path: "/algo/api/v2/byok/check",
...
})

path 的值替换为一个立即抛出的 IIFE:

1
2
搜索:  path:"/algo/api/v2/byok/check"     (30 bytes)
替换: path:(()=>{throw "......."})() (30 bytes)

对象构造阶段即抛出异常——HTTP 请求从未发起,因为 throw 发生在 e_() 被调用之前。异常被函数已有的 try-catch 捕获。

/algo/api/v2/byok/check 在二进制中出现两次,加上 path: 前缀后仅匹配 options 对象内的那一处,不会误伤 prepareRequest 的参数。

P3:伪造校验结果

P2 让异常被 catch 捕获,但 catch 块返回 !1(Bun minify 的 false),TUI 仍认为校验失败。需要将其改为 !0true):

1
2
3
锚点:  [byok] checkBYOKModel error input: provider=   (唯一出现)
搜索: 锚点后 300 字节内的第一个 !1 (2 bytes)
替换: !0 (2 bytes)

!1 在整个二进制中出现成千上万次,直接全局替换会导致灾难性后果。通过唯一的日志字符串锚点精确定位到 catch 块的返回值。

整体执行流程

1
2
3
4
5
6
7
8
9
10
11
用户输入 BYOK 模型 →
TUI 调用 checkBYOKModel() →
P1: endsWith("") → trueBYOK 界面解锁 ✓

进入校验函数 →
prepareRequest() 正常执行(无副作用)
P2: path IIFE 抛出异常 → HTTP 请求被跳过 ✓

catch 块捕获异常 →
P3: 返回 !0 (true) → TUI 认为校验通过 ✓
BYOK 自定义模型可用 ✓

签名与验证

二进制修改后需要重新签名,否则 macOS 拒绝运行:

1
2
xattr -d com.apple.quarantine qodercli   # 移除隔离标记
codesign --force --sign - qodercli # ad-hoc 重签名

补丁脚本支持幂等执行——检测到已替换的特征时自动跳过,备份仅在首次修改前创建。

意外发现:请求代理与隐私问题

最初我只应用了 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


Qoder CLI 逆向分析与 BYOK 解锁
https://neo.mufanc.xyz/posts/45450/
作者
Mufanc
发布于
2026年5月31日
许可协议