用 eBPF 观测 SSL/TLS 明文:原理、钩点与边界
HTTPS 在网络上看到的是密文;排障、审计、零信任探针有时却需要在 进程内加解密边界 看到明文。eBPF 通过 uprobe 挂到 OpenSSL 等库的 SSL_read / SSL_write,可以在不改应用代码的前提下截取缓冲区内容。本文说明 TLS 握手在观测中的位置、OpenSSL API 要点、典型 sslsniff 思路,以及 合规与局限------独立成篇,可直接当实践备忘。
重要前提:这不是「破解 TLS」或「用私钥解密抓包」,而是读取 库函数已经处理过的用户态缓冲区。
目录
- 问题:密文链路上的盲区
- [TLS 握手在观测里意味着什么](#TLS 握手在观测里意味着什么)
- [为何钩 SSL_read / SSL_write](#为何钩 SSL_read / SSL_write)
- [OpenSSL 读写 API 要点](#OpenSSL 读写 API 要点)
- [eBPF 实现思路(sslsniff 类)](#eBPF 实现思路(sslsniff 类))
- 工程坑位
- 合规、隐私与安全边界
- 替代与互补方案
- 免责声明
问题:密文链路上的盲区
| 观测位置 | 能看到 | 看不到 |
|---|---|---|
| 网卡 / tcpdump | IP/TCP、TLS record 密文 | HTTP 正文、密钥材料(正常情况) |
| 内核 socket | 仍多是密文(TLS 在用户态库) | 应用层明文 |
| OpenSSL 解密之后 | 明文缓冲区 | --- |
结论:对 用户态 TLS (OpenSSL、BoringSSL、部分 Go crypto 等),明文出现在 库函数读写缓冲区 的瞬间。eBPF uprobe 正是打在这一层。
TLS 握手在观测里意味着什么
握手成功前没有稳定的应用数据通道;钩 SSL_read/SSL_write 主要关注 握手完成后的应用数据。握手流程(简化):
- ClientHello:客户端给出支持的密码套件等
- ServerHello:服务端选定套件
- 证书:服务端出示证书(含公钥等)
- 客户端验证书
- 派生会话密钥
- 经典:客户端用服务端公钥保护 PreMasterSecret,双方派生会话密钥
- 或 ECDHE 等:提供前向保密;即使日后私钥泄露,也难解历史会话
之后双方用会话密钥加解密。握手任一步失败则连接不会进入应用数据阶段。
对观测的含义:
- 只钩读写 API,不会自动得到私钥或破解 TLS
- 看到的是 本进程用户态已经解密/尚未加密 的字节
- 换库、换静态链接、换内核 TLS(kTLS)路径,钩点可能失效
Server Client Server Client #mermaid-svg-fZfu4I12gUBxcCIp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fZfu4I12gUBxcCIp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fZfu4I12gUBxcCIp .error-icon{fill:#552222;}#mermaid-svg-fZfu4I12gUBxcCIp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fZfu4I12gUBxcCIp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fZfu4I12gUBxcCIp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fZfu4I12gUBxcCIp .marker.cross{stroke:#333333;}#mermaid-svg-fZfu4I12gUBxcCIp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fZfu4I12gUBxcCIp p{margin:0;}#mermaid-svg-fZfu4I12gUBxcCIp .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fZfu4I12gUBxcCIp text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-fZfu4I12gUBxcCIp .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fZfu4I12gUBxcCIp .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-fZfu4I12gUBxcCIp .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-fZfu4I12gUBxcCIp .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-fZfu4I12gUBxcCIp #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-fZfu4I12gUBxcCIp .sequenceNumber{fill:white;}#mermaid-svg-fZfu4I12gUBxcCIp #sequencenumber{fill:#333;}#mermaid-svg-fZfu4I12gUBxcCIp #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-fZfu4I12gUBxcCIp .messageText{fill:#333;stroke:none;}#mermaid-svg-fZfu4I12gUBxcCIp .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fZfu4I12gUBxcCIp .labelText,#mermaid-svg-fZfu4I12gUBxcCIp .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-fZfu4I12gUBxcCIp .loopText,#mermaid-svg-fZfu4I12gUBxcCIp .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-fZfu4I12gUBxcCIp .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-fZfu4I12gUBxcCIp .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-fZfu4I12gUBxcCIp .noteText,#mermaid-svg-fZfu4I12gUBxcCIp .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-fZfu4I12gUBxcCIp .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fZfu4I12gUBxcCIp .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fZfu4I12gUBxcCIp .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-fZfu4I12gUBxcCIp .actorPopupMenu{position:absolute;}#mermaid-svg-fZfu4I12gUBxcCIp .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-fZfu4I12gUBxcCIp .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-fZfu4I12gUBxcCIp .actor-man circle,#mermaid-svg-fZfu4I12gUBxcCIp line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-fZfu4I12gUBxcCIp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 握手(证书与密钥协商) 应用数据(密文在网上) SSL_write 入参仍是明文 SSL_read 出参已是明文 ClientHello ServerHello + Certificate TLS records
为何钩 SSL_read / SSL_write
在 OpenSSL 中:
SSL_write:应用把 明文 交给库,由库加密后经 socket 发出SSL_read:库从 socket 读密文,解密后把 明文 拷到应用缓冲区
因此:
| 函数 | 钩入口时缓冲区含义 |
|---|---|
SSL_write |
即将加密的明文 |
SSL_read |
返回前/返回后可拿到已解密明文(具体挂 entry 还是 return 要按实现选) |
这比在 send/recv 上钩更贴近「应用层内容」,也避开了仍是密文的 socket I/O。
返回探针与非阻塞重试 :SSL_read 更常挂在 uretprobe(返回路径) ------只有成功返回后,输出缓冲区内容才可靠。非阻塞 Socket 下,读操作可能因 I/O 未就绪返回错误(如经 SSL_get_error 得到 SSL_ERROR_WANT_READ),此时缓冲区 无效或未填充 ,探针不得当成功明文上报。即便单次返回成功,一次调用也往往只是应用层消息的一个片段(短读);再叠加 SSL_pending 等缓冲语义,用户态 Agent 做短读短写重组是生产级实现的必要工作,而不是可选优化。
OpenSSL 读写 API 要点
SSL_read / SSL_read_ex
用于从已建立的 SSL 连接读数据。
| 参数 | 含义 |
|---|---|
ssl |
已建立的 SSL * 连接 |
buf |
输出缓冲区 |
num |
最大读取字节数 |
readbytes(_ex) |
实际读取长度输出 |
SSL_read 用返回值表示读到的字节数;SSL_read_ex 用额外参数返回更明确的长度信息。嗅探时需同时覆盖 _ex 变体(视目标二进制导出符号而定)。
SSL_write / SSL_write_ex
| 参数 | 含义 |
|---|---|
ssl |
目标 SSL 连接 |
buf |
待写明文 |
num |
长度 |
written(_ex) |
实际写入长度 |
实现嗅探时通常在 uprobe 里读取 buf 与长度,写入 eBPF Map 或 ringbuf,再由用户态打印/归档。
eBPF 实现思路(sslsniff 类)
典型 sslsniff 思路:
- 定位进程加载的
libssl.so(或等价)路径与符号 - 对
SSL_read/SSL_write(及_ex变体,视目标而定)挂 uprobe/uretprobe - 在探针中用辅助函数安全读取用户缓冲区(见下)
- (进阶) 经
SSL *上下文读取底层 BIO(如rbio/wbio)关联的 Socket FD ,再在用户态用getsockname/getpeername(或内核侧 sock 信息)补齐 五元组,便于全链路关联 - 携带 PID、TID、时间戳、方向(读/写)、截断后的 payload(及可选 FD)送到用户态
- 用户态做协议识别(HTTP/1、HTTP/2 前言等)、短读重组与展示
#mermaid-svg-H4UU89sd01sukzHj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-H4UU89sd01sukzHj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-H4UU89sd01sukzHj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-H4UU89sd01sukzHj .error-icon{fill:#552222;}#mermaid-svg-H4UU89sd01sukzHj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-H4UU89sd01sukzHj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-H4UU89sd01sukzHj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-H4UU89sd01sukzHj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-H4UU89sd01sukzHj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-H4UU89sd01sukzHj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-H4UU89sd01sukzHj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-H4UU89sd01sukzHj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-H4UU89sd01sukzHj .marker.cross{stroke:#333333;}#mermaid-svg-H4UU89sd01sukzHj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-H4UU89sd01sukzHj p{margin:0;}#mermaid-svg-H4UU89sd01sukzHj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-H4UU89sd01sukzHj .cluster-label text{fill:#333;}#mermaid-svg-H4UU89sd01sukzHj .cluster-label span{color:#333;}#mermaid-svg-H4UU89sd01sukzHj .cluster-label span p{background-color:transparent;}#mermaid-svg-H4UU89sd01sukzHj .label text,#mermaid-svg-H4UU89sd01sukzHj span{fill:#333;color:#333;}#mermaid-svg-H4UU89sd01sukzHj .node rect,#mermaid-svg-H4UU89sd01sukzHj .node circle,#mermaid-svg-H4UU89sd01sukzHj .node ellipse,#mermaid-svg-H4UU89sd01sukzHj .node polygon,#mermaid-svg-H4UU89sd01sukzHj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-H4UU89sd01sukzHj .rough-node .label text,#mermaid-svg-H4UU89sd01sukzHj .node .label text,#mermaid-svg-H4UU89sd01sukzHj .image-shape .label,#mermaid-svg-H4UU89sd01sukzHj .icon-shape .label{text-anchor:middle;}#mermaid-svg-H4UU89sd01sukzHj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-H4UU89sd01sukzHj .rough-node .label,#mermaid-svg-H4UU89sd01sukzHj .node .label,#mermaid-svg-H4UU89sd01sukzHj .image-shape .label,#mermaid-svg-H4UU89sd01sukzHj .icon-shape .label{text-align:center;}#mermaid-svg-H4UU89sd01sukzHj .node.clickable{cursor:pointer;}#mermaid-svg-H4UU89sd01sukzHj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-H4UU89sd01sukzHj .arrowheadPath{fill:#333333;}#mermaid-svg-H4UU89sd01sukzHj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-H4UU89sd01sukzHj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-H4UU89sd01sukzHj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-H4UU89sd01sukzHj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-H4UU89sd01sukzHj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-H4UU89sd01sukzHj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-H4UU89sd01sukzHj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-H4UU89sd01sukzHj .cluster text{fill:#333;}#mermaid-svg-H4UU89sd01sukzHj .cluster span{color:#333;}#mermaid-svg-H4UU89sd01sukzHj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-H4UU89sd01sukzHj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-H4UU89sd01sukzHj rect.text{fill:none;stroke-width:0;}#mermaid-svg-H4UU89sd01sukzHj .icon-shape,#mermaid-svg-H4UU89sd01sukzHj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-H4UU89sd01sukzHj .icon-shape p,#mermaid-svg-H4UU89sd01sukzHj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-H4UU89sd01sukzHj .icon-shape .label rect,#mermaid-svg-H4UU89sd01sukzHj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-H4UU89sd01sukzHj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-H4UU89sd01sukzHj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-H4UU89sd01sukzHj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 钩 read/write
应用
OpenSSL
TCP socket
eBPF uprobe
ringbuf/Map
用户态 Agent
读取用户态缓冲区 :现代内核中必须使用 bpf_probe_read_user / bpf_probe_read_user_str 等辅助函数。在探针里 直接解引用 (void *)arg_buf 会被 Verifier 拒绝。较旧内核上常见的是更笼统的 bpf_probe_read;跨内核版本时要按目标能力选择 helper,并做好 CO-RE/兼容分支。
伪逻辑(概念级,非可编译代码):
text
on SSL_write(ctx):
len = min(arg_num, MAX)
bpf_probe_read_user(buf, len, arg_buf)
submit(pid, "write", buf, len)
on SSL_read return(ctx):
if ret > 0: # 忽略 WANT_READ 等失败/重试
bpf_probe_read_user(buf, min(ret, MAX), arg_buf)
submit(pid, "read", buf, len)
返回探针常更适合 SSL_read:只有成功返回后缓冲区内容才可靠。
工程坑位
| 坑 | 说明 |
|---|---|
| 符号找不到 | 静态链接、符号剥离、用的是 BoringSSL/自研栈;OpenSSL 大版本符号/布局差异 |
| 多版本共存 | 容器内库路径与宿主机不同;要以目标进程 maps 为准 |
| 短读短写 / 重试 | 一次调用不是完整消息;WANT_READ/WANT_WRITE 时勿采空缓冲;需用户态重组 |
| HTTP/2 / gRPC | 明文是二进制帧,不能当纯文本日志 |
| 性能 | 大包全量拷贝开销大;应截断、采样、按 PID 过滤 |
| 异步 / BIO | 复杂 BIO 或内存 BIO 路径可能绕开你钩的符号 |
| 协程调度 | Go goroutine / C++ 协程等可能导致 SSL* 跨线程、栈迁移;函数钩得到,但 TID/连接关联变复杂 |
| kTLS | 部分加解密下沉内核时,用户态钩可能变空 |
合规、隐私与安全边界
技术上「能钩到」≠「可以随便钩」。
| 原则 | 说明 |
|---|---|
| 授权 | 仅在自有系统、明确授权的排障/安全审计范围内使用 |
| 最小化 | 过滤 PID/容器、截断 payload、避免落盘全流量 |
| 隔离 | 探针权限极高,Agent 本身需防篡改与防滥用 |
| 告知 | 涉及个人数据时遵守隐私与等保/行业要求 |
威胁模型:uprobe ≠ 中间人(MitM)
| 能力 | eBPF uprobe | 中间人代理(MitM) |
|---|---|---|
| 需 Root / 高权限跟踪 | 通常是 | 否(常只需控制代理与证书信任) |
| 需私钥 / 导入根证书 | 否 | 是 |
| 影响范围 | 单主机上的目标进程/容器 | 经代理的客户端或网段流量 |
| 对终端「证书告警」 | 无(不改证书链) | 常见(未信任根时) |
| 本质 | 在 已授权主机 上看库内缓冲区 | 在路径上 终止并重加密 TLS |
二者都可能触及隐私,但 权限形态与部署面完全不同,不可互相替代,也不可混为一谈。
替代与互补方案
| 方案 | 特点 |
|---|---|
| 应用内日志 / OpenTelemetry | 最干净,需改代码或插桩 SDK |
| 反向代理终止 TLS | 明文在代理上,架构上可控 |
| 内核 TLS / 服务网格策略 | 观测点随架构变化 |
| 仅元数据(SNI、连接五元组) | 隐私更友好,信息更少 |
收束 :eBPF 观测 TLS 明文,本质是 在用户态密码学库的边界做动态跟踪 ;价值在排障与受控审计,难度在符号、FD 关联与协议重组,风险在权限与合规。先定义谁允许看明文,再写探针。拿到的明文若要落到「哪条连接、哪段协议栈」,往往还要回到网络路径与 sk_buff 一侧的可观测设计。
免责声明
本文所述技术仅限在 合法授权 环境下用于故障排查、性能分析与安全审计。严禁用于未经授权窥探通信内容或侵犯他人隐私。读者须自行确保符合所在司法辖区与组织的法律法规及合规要求。
整理自《深入理解 eBPF 与可观测性》SSL/TLS 观测相关章节,并补充工程、威胁模型与合规说明。