05:MITM 的五脏六腑——中间人的里里外外
大家好,我是毛衣哥。前四期铺垫了那么多,这一期终于到实战环节了。我们把中间人拆开,看看它肚子里到底塞了什么——堵路、分身、造假、偷看、传话,五步走完,你也能当个"白帽中间人"。
前四篇我们把 HTTPS 的加密和信任体系拆了个遍。这一期来点真格的——把中间人攻击拆成五个步骤,每一步都讲清楚中间人做了什么,以及每一步如果做砸了会发生什么。
看完这篇,你就知道装完那个证书之后,中间人到底在你的流量上干了哪些事。
我先把这五步列出来,然后一个一个拆:
第一步:堵路 —— 让流量走中间人这里过 第二步:分身 —— 同时扮演两个角色 第三步:造假 —— 即时签发假证书 第四步:偷看 —— 解密、记录、展示 第五步:传话 —— 透明转发,不让任何一端察觉第一步:堵路——怎么让流量走中间人这里过
中间人最大的一个问题:它得先让你把流量发给它,而不是直接发给服务器。
抓包工具怎么解决这个问题?有四种方式。每个的技术含量天差地别。
方式一:HTTP 代理配置(抓包工具最常用的方式)
你在系统偏好设置里配代理:HTTP 代理填127.0.0.1:8888。
然后你的浏览器发出的所有 HTTP/HTTPS 请求,都先发到了127.0.0.1:8888——那里正好跑着 Charles 或 Fiddler。
优点:不需要任何特殊权限,配置简单,修改方便。
缺点:应用如果不读取系统代理设置(很多移动 App 就这样),就无效。
方式二:DNS 劫持
这种方式更狠一点——不需要你配代理。
中间人控制了一个 DNS 服务器。当你查询www.example.com时,DNS 返回的不是真正的 IP,而是中间人自己的 IP。你的浏览器以为www.example.com就在那里,直接连过去了。
优点:客户端不需要任何配置——只需要使用中间人控制的 DNS 服务器。
缺点:如果你不用它的 DNS(自己配了 8.8.8.8),这一招就没用了。还有,HTTPS 的证书校验会失败——浏览器连上去之后,中间人给的证书不是 example.com 的。
方式三:ARP 欺骗(局域网内)
这是所有方式里最"黑客"的一种。
在同一个局域网里,你的电脑和路由器都有一张 ARP 表,记录了"这个 IP 对应的 MAC 地址是什么"。中间人伪造 ARP 包,告诉路由器:“我是 192.168.1.100(你的 IP)”。同时告诉你:“我是 192.168.1.1(网关)”。
结果:你要发给www.example.com的数据包先到了路由器——但你的电脑以为网关是中间人,所以数据先到了中间人那里。路由器觉得你是中间人,也把数据发给中间人。
优点:不需要修改任何系统设置。在同一个 WiFi 下的任何人都可以对你这么做。
缺点:要实现"双向冒充",需要在同一局域网内;HTTPS 还有证书问题,需要把证书也处理了。
方式四:BGP 路由劫持(国家级)
这是运营商级别的操作。通过 BGP 协议宣告一条更优的路由,让互联网上所有发给某个 IP 范围的流量都先经过你的路由器。
优点:一个机房、一个省、甚至一个国家的流量都能劫持。
缺点:需要操作骨干路由器。BGP 泄漏如果被监控发现,立刻会被全球通报。
但抓包工具用的是最简单的一种——方式一。你配个代理就行。
第二步:分身——同时扮演两个角色
流量到了中间人这里,中间人要做的第一件事:同时扮演两个角色。
视角一:跟服务器的通信 中间人(以客户端身份)→ 跟 example.com 完成真正的 TLS 握手 → 拿到会话密钥 A(中间人←→服务器) → 开始接收真正的响应数据 视角二:跟客户端的通信 中间人(以服务器身份)← 跟你的浏览器完成假的 TLS 握手 ← 用假证书欺骗你的浏览器 ← 拿到会话密钥 B(客户端←→中间人) ← 等待你发出 HTTP 请求关键是:这两个角色互不知道对方的存在。中间人同时维护着两条连接。一条是真正的连接——它跟 example.com 正常说话。一条是假的连接——它冒充 example.com 跟你说话。
在代码里,这种"双通道管理"的架构大概长这样:
// MITM 中"分身"阶段的核心逻辑 function handle_client_connection(client_conn, target_host, target_port): // 1. 跟真正的服务器建立连接 server_conn = tcp_connect(target_host, target_port) tls_handshake(server_conn, as_client=true) // 以客户端身份握手 // 2. 从 SNI 获取目标域名 target = client_conn.client_hello.sni // 3. 动态签发假证书 fake_cert = sign_cert(interceptor_ca, target) // 4. 跟客户端握手(用假证书) tls_handshake(client_conn, as_server=true, cert=fake_cert) // 5. 同时维护两条通道 while client_conn.is_connected() AND server_conn.is_connected(): // 客户端方向:解密 → 记录 → 加密转发 client_data = tls_decrypt(client_conn, key_B) record_to_storage(client_data) tls_encrypt(server_conn, key_A, client_data) // 服务器方向:解密 → 记录 → 加密转发 server_data = tls_decrypt(server_conn, key_A) record_to_storage(server_data) tls_encrypt(client_conn, key_B, server_data)如果中间人在分身这一步失败了会怎样?
假设中间人成功跟 example.com 建立了连接(拿到了真正的网页),但跟你的连接建立失败(比如你的浏览器不信任假证书)——那你的浏览器会直接显示一个"连接不安全"的红色警告页面,拒绝继续。
实际上,中间人是在两条连接都建立之后,才开始做第四步和第五步的。
第三步:造假——即时签发假证书
这是前面花了四期铺垫的核心。中间人已经截获了你的 ClientHello,从 SNI 里知道你要访问example.com。
它打开自己的 CA 证书(就是你装的那个),用它的私钥签发一张新的证书:
马上要签发的证书内容:
Version: 3 Serial Number: 随机生成一个 Issuer: CN=Charles Proxy CA(这是你自己装的那个 CA 的名字) Subject: CN=example.com(这是中间人替你填的,冒充的域名) Validity: Not Before: 现在 Not After : 现在 + 1 年 Subject Public Key Info: Public Key Algorithm: RSA Public-Key: (中间人自己生成的一对临时密钥对,把公钥放进来) X509v3 extensions: Subject Alternative Name: DNS:example.com DNS:www.example.com然后用自己的 CA 私钥签名,生成签名值。
整个过程耗时多少?如果证书已经缓存在内存里(Charles 会对常用的域名缓存证书),是微秒级的。如果需要重新生成,是毫秒级的。
如果造假失败了会怎样?
一种情况:中间人用了一个过期的 CA 证书来签。你装的那个抓包 CA 过期了(Charles 旧版本常见的问题)。这时候浏览器检查证书链会发现"这个 CA 已经过期了"。
另一种情况:中间人用的 CA 跟你的系统中安装的不匹配。比如你装的是 Charles 的 CA,但中间人用了 Fiddler 的 CA。你的浏览器不信任那个 CA,报错。
但最关键的造假条件只有一个:你必须在系统中安装过中间人 CA。
你永远不需要记住这个条件。因为它每次都会以"安装证书"的方式提醒你。
第四步:偷看——解密、记录、展示
两条连接都建立好了。假证书也发出去了。现在中间人手握着两把会话密钥:
钥匙串: 会话密钥 B(客户端 → 中间人):能解密你发过来的加密数据 会话密钥 A(中间人 → 服务器):能解密服务器发回来的加密数据解密过程(以你的浏览器发一个 POST 请求为例):
你的浏览器(加密)→ 中间人收到: [一堆密文] 中间人用会话密钥 B 解密: POST /login HTTP/1.1 Content-Type: application/x-www-form-urlencoded username=admin&password=123456 中间人记录到本地: timestamp: 2026-07-28 12:34:56 src_ip: 你的 IP dst_ip: example.com method: POST path: /login body: username=admin&password=123456看到了吗?你的登录密码,在中间人看来就是明文的。
中间人然后把这条记录的格式调整一下,显示在抓包工具的界面上——Charles 的"Structure"视图里就多了一条请求,你可以点开查看完整的请求和响应。
解密过程中的边界问题:
如果请求体很大(比如一个文件上传,几十 MB),中间人会怎么做?
- 它可以解密前几 KB 就展示给你看(流式显示)
- 也可以等全部解密完再展示(需要更多内存)
- 大多数抓包工具选择方案一——先把头部解密显示出来(HTTP 请求行 + 请求头),body 等下载完了再慢慢显示
第五步:传话——透明转发
解密并记录完之后,中间人需要做最后一步:把这次请求原样发给真正的服务器。
中间人从记录中取出原始请求(在解密的时候就已经保存了原始字节) → 用会话密钥 A(中间人 → 服务器)重新加密 → 发往真正的 example.com服务器收到加密数据,解密,响应。
响应的数据回到中间人,中间人再做一遍"解密记录"→"重新加密"的循环,然后发回给你。
整个过程,两端都没有任何感知:
- 服务器觉得:我是一个正常的客户端连上了我,在跟我正常通信
- 你觉得:我连的是 example.com,地址栏有小锁,没问题的
如果传话出了问题会怎样?
假设中间人在"解密 → 重新加密"的过程中把数据破坏了一点点——比如改了一个字节。那么服务器收到的请求就跟你发的不同了。
如果你发的是一次银行转账请求:金额从 100 变成了 1000——但中间人没有改,它原样转发了。但如果中间人故意改了,它完全可以改。
这就是 HTTPS 没解决的问题——它只能保证"传输过程中没人改",但不能保证"中间人端到端没有改"。因为你装了中间人的 CA,中间人在你的终端和服务器之间插了一整层。
不过,大多数抓包工具是诚实的,不会改你的数据。它们只管看和记。
如果五步中任何一步失败了
来看看各种失败场景:
| 步骤 | 失败了会怎样 | 用户看到什么 |
|---|---|---|
| 第一步:堵路 | 流量不走中间人,抓包工具什么都没抓到 | 一切正常,抓包工具界面空白 |
| 第二步:分身 | 两条连接有一条没建立成功 | 浏览器打不开网页 |
| 第三步:造假 | 假证书不被信任 | 浏览器红色警告"您的连接不是私密连接" |
| 第四步:偷看 | 解密失败,密文无法解析 | 抓包工具显示乱码 |
| 第五步:传话 | 数据破坏,服务器收到乱码 | 网页加载失败或显示错误内容 |
所以当你看到抓包工具界面有内容时——说明五步全部走通了。
一个有意思的问题:中间人能看到自己的流量吗?
答案是:不能。
因为中间人自己访问 HTTPS 网站的时候,它也只是一个普通客户端。如果它没有在系统里装自己的 CA(通常不装,因为不需要),那它的浏览器跟服务器之间的 TLS 握手就是正常的——中间人的抓包工具看不到自己浏览器的流量。
但如果在中间人的系统上装了它自己的 CA——那就形成了一个"自指"的循环:中间人抓中间人自己的包。这在调试抓包工具本身的 bug 时偶尔会用。
下期预告:装了一个证书,你的 HTTPS 就全裸了。我们只讲一件事:那个安装证书的弹窗,到底授予了抓包工具多么大的权限。
DumpAny 怎么做:五步中的每一步在 DumpAny 内部都是一个独立的模块。堵路(代理配置)、分身(双通道管理)、造假(动态证书签发)、偷看(解密记录)、传话(透明转发)——每个模块都可以独立升级和替换。这种架构的好处是:如果我们想支持新的协议(比如 MySQL 或 Redis),只需要在偷看这一步加一个新的协议解析器,前四步完全复用。
💬 聊几句:
- TLS 握手七步中,你觉得最关键的是哪一步?为什么?
- 你平时会去看 TLS 握手的细节吗?还是在用工具一键搞定?
- 如果有一天互联网的 CA 体系崩塌了,你觉得会发生什么?
- 你有没有遇到过证书相关的线上问题?怎么排查的?