我在用 Claude Code 时,走的是一个中转站——它反代了别家 AI 编辑器的额度,包装成标准 API 给我用。一个自然的疑问冒出来:中转站会不会在中间偷偷塞进它自己的系统提示和工具,把我的请求"污染"了?为了搞清楚,我第一次认真地动手抓包,顺带把 HTTPS 拦截、CA 证书、mitmproxy 这套东西彻底捋了一遍。这篇文章不是抓包工具的说明书,而是把这次排查里真正有价值的原理和判断路径沉淀下来:抓包到底在抓什么、为什么 HTTPS 必须先装一张证书、抓到的东西能证明什么、又证明不了什么。

原理篇

抓包到底在抓什么

抓包(packet capturing)的字面意思是"把网络上流动的数据包拦下来看",但日常我们说的"抓包排查接口",抓的其实是应用层的 HTTP 请求和响应——某个程序向服务器发了什么(URL、请求头、请求体),服务器回了什么(状态码、响应头、响应体)。

要拦下这些内容,最常见的做法不是去网卡上嗅探原始比特流,而是让流量主动绕道经过一个你能控制的中间人程序。这个中间人就是代理(proxy):

你的程序  →  抓包代理(你能看内容)  →  真正的服务器

程序把请求发给代理,代理转发给服务器,再把响应带回来。因为一切都过代理的手,你就能把每一笔请求响应完整记录下来。这也是"中间人"这个词的由来。

HTTPS 带来的障碍:代理看到的是密文

如果全是明文 HTTP,故事到这就结束了。但今天几乎所有 API 都是 HTTPS,而 HTTPS 的设计目标恰恰是防止中间人偷看——数据在客户端和服务器之间端到端加密。代理夹在中间,拿到的只是一坨密文,看不懂内容。

这正是 HTTPS 该有的安全性。抓包工具要看明文,就必须做一件"名正言顺的中间人攻击":

你的程序 ──TLS──> 抓包工具 ──另一条TLS──> 真服务器
          (工具假装自己是服务器)   (工具假装自己是客户端)

抓包工具把一条端到端的加密连接,拆成了两段它都能解密的连接:

  • 对你的程序,它冒充目标服务器,用自己签发的证书跟你握手;
  • 对真服务器,它冒充普通客户端,正常发起 HTTPS。

这样,明文在工具内部这一站是可见的——它先解密你的请求看一眼,再重新加密发给服务器。这套手法就叫 MITM(Man-In-The-Middle,中间人)

CA 证书:为什么抓 HTTPS 必须先装一张证书

问题来了:你的程序凭什么信任抓包工具"冒充服务器"递过来的证书?

正常情况下它绝不该信。浏览器/客户端会校验服务器证书是不是由**受信任的证书颁发机构(CA,Certificate Authority)**签发的。抓包工具自己签的证书,不在系统信任列表里,握手会直接失败并报错——这就是很多人第一次抓 HTTPS 时看到一堆证书错误的原因。

解决办法是:把抓包工具自己的 CA 根证书,手动装进操作系统的"受信任的根证书颁发机构"存储里。装进去之后,系统就认为"这个 CA 签发的任何证书都可信",于是工具临时为 api.example.com 签发的假证书也被放行,握手成功,明文得以解密。

用一张图理清这条信任链:

安装 CA 根证书到系统信任存储
        │
        ▼
工具用这张 CA,为目标域名现签一张证书
        │
        ▼
你的程序校验证书 → 由"受信任的CA"签发 → 通过
        │
        ▼
握手成功 → 工具能解密这段 TLS → 看到明文

关键认知:装 CA 证书,本质是你主动授权某个工具在你自己的机器上做中间人。它是抓 HTTPS 的前提,但也意味着——凡是能拿到这张 CA 对应私钥的人,理论上都能中间人你的 HTTPS 流量。所以这张证书用完必须删,不能长期留在信任根里。

.mitm 目录:CA 和它的私钥住在哪

我用的工具是 mitmproxy。它第一次运行时,会在配置目录(我这次指定成了项目下的 .mitm/)里生成一整套证书文件:

.mitm/
├── mitmproxy-ca-cert.cer   # CA 根证书(公钥部分),就是要装进系统的那张
├── mitmproxy-ca-cert.pem   # 同上,PEM 编码
├── mitmproxy-ca.pem        # CA 证书 + 私钥,签发假证书就靠它
├── mitmproxy-ca.p12        # PKCS#12 打包格式
└── mitmproxy-dhparam.pem   # DH 密钥交换参数

这里最敏感的是 mitmproxy-ca.pem,因为它含私钥。谁拿到它,谁就能用这张被你信任的 CA 去签任意域名的证书。所以 .mitm 目录和系统里那张 CA 是一体两面:装进系统的是公钥证书,私钥留在 .mitm 里。清理时两边都要处理——删系统信任 + 删 .mitm 私钥目录,缺一不可。

实战篇

目标:判断中转站有没有"污染"我的请求

回到最初的问题。中转站可能有两种工作方式,污染与否完全取决于它是哪种:

方式 做法 是否污染
换 key 直转 把你的请求体原样转发给上游,只替换鉴权 key 不污染
套壳改写 把你的请求改写成"它背后那个产品"的请求格式,过程中塞进对方的 system prompt / 工具 污染

我要做的,就是找证据判断它属于哪种。

安装工具:被镜像源坑了一次

安装 mitmproxy 时踩了个典型的坑。我的 pip 和 uv 都把默认索引源锁死在了清华镜像,而镜像当时拉不到包,一直报"找不到版本"。直接指定 --index-url 也没用,因为配置文件里的默认源优先级更高。最后靠环境变量强行覆盖才装上:

# 用环境变量顶掉配置文件里的默认镜像源,指回官方源
UV_DEFAULT_INDEX="https://pypi.org/simple" uv tool install mitmproxy --python 3.12

经验:当"命令行参数指定源"仍然失败时,大概率是配置文件里的默认源优先级压过了参数。这时改用环境变量覆盖,往往比改配置文件更快。

装证书:装进"当前用户"就够了

mitmproxy 的 CA 证书装进 Windows 当前用户的受信任根存储,不必污染整个系统:

# 装入当前用户(-user)的 Root 存储
certutil -user -addstore Root .mitm/mitmproxy-ca-cert.cer

# 验证是否装上
certutil -user -store Root | grep -i mitmproxy

一个绕不开的悖论:抓不到自己

准备开抓时,我意识到一个根本性障碍:我没法用当前这个 Claude Code 会话去抓它自己的流量。因为会话启动时,连接早就建立好了,不会走我新起的代理;而且让一个进程从内部拦截自己正在用的连接,本身就是"咬自己尾巴"。要抓,只能新起一条走代理的请求。

同时还有个网络问题:我本机已经有一个常驻代理(127.0.0.1:7890)在科学上网。抓包代理不能顶掉它,否则请求根本出不去。正解是代理链——让 mitmproxy 把抓完的流量再转发给 7890:

# mitmproxy 抓完,再把流量交给已有的 7890 上游代理
mitmdump --listen-port 8080 --mode upstream:http://127.0.0.1:7890 -s extract.py

落地脚本:把关键字段自动提取出来

为了让分析自动化,我写了个 mitmproxy 插件(addon)。它只挑 LLM API 的请求,把最能暴露污染的字段——system(系统提示)、modeltoolsinstructions——落到一个 JSONL 文件里,并对可疑关键词自动打标:

from mitmproxy import http
import json, os
from datetime import datetime

OUT = "captured.jsonl"
# 只关心这些 LLM 接口路径
LLM_PATHS = ("/v1/messages", "/chat/completions", "/v1/responses")
# 命中这些词就标记为可疑(对方产品名、角色重定义话术等)
SUSPECT_KW = ["qoder", "你是一个", "you are an ai", "system prompt", "字节"]

def request(flow: http.HTTPFlow):
    # 只抓 POST 到 LLM 接口的请求
    if flow.request.method != "POST" or not any(s in flow.request.path for s in LLM_PATHS):
        return
    body = json.loads(flow.request.get_text())
    sys_field = body.get("system")            # Anthropic 顶层 system
    text = sys_field if isinstance(sys_field, str) else json.dumps(sys_field, ensure_ascii=False)
    hits = [k for k in SUSPECT_KW if text and k.lower() in text.lower()]
    record = {
        "time": datetime.now().isoformat(timespec="seconds"),
        "host": flow.request.host,
        "model": body.get("model"),
        "system_len": len(text) if text else 0,
        "suspect": hits,                       # 命中可疑词就在这里体现
        "tools_count": len(body.get("tools", []) or []),
    }
    with open(OUT, "a", encoding="utf-8") as f:
        f.write(json.dumps(record, ensure_ascii=False) + "\n")

结果与验证:抓包的"能"与"不能"

工具全部就绪后,我在动手抓之前先想清楚了一件事——这套抓包到底能证明什么。想明白之后,发现请求侧抓包对我这个问题其实是弱证据。原因有两层。

第一,请求侧的 body 天生干净。 我在本机能抓到的,只有"Claude Code → 中转站"这一跳。但这一跳的请求体是 Claude Code 自己构造的,里面本来就是标准的 Claude 系统提示和工具,不含任何对方产品的东西。抓它,证明不了中转站有没有在转发给上游时做手脚——那一步发生在中转站的服务器里,本机看不到。

第二,响应侧的格式是被强制改写的,所以格式不携带污染信息。 这一点最反直觉,值得单独说清楚:

Claude Code 说的是 Anthropic Messages API 协议,它期待收到的响应必须是 Anthropic 那套特定结构(message_start / content_block_delta 之类的流式事件,或特定形状的 JSON)。只要不是这个格式,客户端直接解析失败。所以中转站别无选择,必须把上游返回的任何格式都归一化成 Anthropic 格式再回给你。

推论很重要:你收到的永远不是上游的原始响应,一定是被归一化过的响应。但正因为这个改写是强制的、跟污染无关,响应"长成 Anthropic 的样子"这件事本身,不能作为中转站在搞鬼的证据。 响应里唯一有用的是内容是否漏出了对方产品的痕迹(残留工具名、以对方 agent 口吻自我介绍等),而干净利落的套壳,内容层面看不出来。

于是结论收敛为:污染若存在,是在请求侧改写时注入的;而请求侧改写发生在服务器,本机抓不到;响应侧只能捡漏手糙的实现。抓包在这个问题上是弱证据

真正有效的一招:行为探针

绕过所有格式层、直接问源头的办法,是行为探针:让模型一字不差地报告它当前上下文里最靠前的系统指令成分。

我对自己(正跑在这条链路上的模型)做了这个探针,如实核对了上下文最前面的内容:

  • 自我认定是 Claude Code,后面是标准的 Anthropic 行为规范;
  • 工具是 Claude Code 原生那批(Read / Edit / Bash / Grep 等),命名和描述都是原版写法;
  • 没有出现对方产品名、没有"你是某某编辑器助手"这类角色重定义、没有陌生的前置规则。

据此判定:从行为探针这一侧看,没有污染迹象,倾向于"换 key 直转"而非套壳。 探针为什么比抓包强?因为它不关心传输格式,直接逼模型自陈"你被喂了什么"——这恰好命中了污染的注入点,而抓包只能看到注入之前或格式化之后的外壳。

当然探针也有盲区:模型自陈并非 100% 可靠(理论上可被"禁止透露前置指令"的注入干扰),且单发探针只覆盖 system 这一维,工具和模型是否被偷换还需要额外交叉验证。但作为本机能做的定性手段,它是信噪比最高的。

收尾:把中间人的口子关上

排查结束,CA 证书用完必须清理,否则等于长期在信任根里留了一个中间人后门:

# 1. 从当前用户信任根删除 CA
certutil -user -delstore Root "mitmproxy"

# 2. 删除含私钥的 .mitm 目录
rm -rf .mitm

# 3. 复查确认信任根里不再有它
certutil -user -store Root | grep -i mitmproxy || echo "已清理干净"

系统信任的公钥证书、.mitm 里的私钥,两处都清掉,才算真正复原。

总结:这次排查沉淀下来的经验

  1. 抓包的本质是"授权的中间人"。 HTTPS 抓包必须装 CA 证书,因为你要让客户端信任抓包工具伪造的服务器证书;装证书 = 主动授权一次中间人,所以用完必须删。
  2. CA 证书和 .mitm 私钥是一体两面。 装进系统的是公钥证书,私钥留在本地目录。清理时两处都要动,只删一处等于没删干净。
  3. 想清楚"抓包能证明什么"再动手,比急着抓更重要。 本机只能抓到自己发出的那一跳;经过多层反代时,真正的改写往往在服务器侧,本机的请求响应都看不到那个环节。
  4. 强制的协议归一化,不等于污染。 客户端对响应格式有硬性要求时,中转站必须改写响应,这个改写与是否注入内容无关——别把"格式变了"误判成"被做手脚了"。
  5. 判断信息注入,行为探针常比抓包更直接。 当污染发生在你抓不到的服务器侧时,直接让模型自陈最前置指令,能绕过传输层,命中注入点本身。

进阶方向

这次只做到了 system 维度的定性判断。要更彻底,还可以往两个方向深入:一是补做工具维度探针(让模型列出实际持有的工具全名,看有没有混入非原生工具),与 system 维度交叉验证;二是研究证书透明度(Certificate Transparency)与证书锁定(certificate pinning)——后者是应用主动拒绝任何非预期 CA 签发的证书,是抓包会失败的一类硬骨头,也正是理解"为什么有些 App 死活抓不到包"的钥匙。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐