OpenSSL 模块
目的与范围
OpenSSL 模块从使用 OpenSSL 库(版本 1.0.x、1.1.x 和 3.x)的应用程序中捕获 TLS/SSL 明文流量和主密钥。它使用 eBPF uprobe 钩住 SSL_read 和 SSL_write 函数,无需修改应用程序或安装根 CA 证书。该模块支持三种输出模式:text(明文显示)、pcap(网络数据包捕获)和 keylog(用于解密的主密钥提取)。
有关 BoringSSL 支持(Android 和非 Android 变体),请参阅BoringSSL 模块。有关 Go TLS 捕获,请参阅Go TLS 模块。有关所有库中主密钥提取的实现细节,请参阅主密钥提取。
来源: user/module/probe_openssl.go:15-106, cli/cmd/root.go:86-101
支持的 OpenSSL 版本
该模块通过智能字节码选择和偏移量分组支持广泛的 OpenSSL 版本:
| 版本范围 | 字节码文件 | 偏移量组 |
|---|---|---|
| 1.0.2a - 1.0.2u | openssl_1_0_2a_kern.o | 单一组(26 个版本) |
| 1.1.0a - 1.1.0l | openssl_1_1_0a_kern.o | 单一组(12 个版本) |
| 1.1.1a | openssl_1_1_1a_kern.o | 组 A |
| 1.1.1b - 1.1.1c | openssl_1_1_1b_kern.o | 组 B |
| 1.1.1d - 1.1.1i | openssl_1_1_1d_kern.o | 组 C |
| 1.1.1j - 1.1.1w | openssl_1_1_1j_kern.o | 组 D |
| 3.0.0 - 3.0.11, 3.0.13 - 3.0.17 | openssl_3_0_0_kern.o | 标准 3.0.x |
| 3.0.12 | openssl_3_0_12_kern.o | 特殊情况(唯一偏移量) |
| 3.1.0 - 3.1.8 | openssl_3_1_0_kern.o | 与 3.0.x 兼容 |
| 3.2.0 - 3.2.2 | openssl_3_2_0_kern.o | 3.2 基础组 |
| 3.2.3 | openssl_3_2_3_kern.o | 3.2 变体 A |
| 3.2.4 - 3.2.5 | openssl_3_2_4_kern.o | 3.2 变体 B |
| 3.3.0 - 3.3.1 | openssl_3_3_0_kern.o | 3.3 基础组 |
| 3.3.2 | openssl_3_3_2_kern.o | 3.3 变体 A |
| 3.3.3 - 3.3.4 | openssl_3_3_3_kern.o | 3.3 变体 B |
| 3.4.0 | openssl_3_4_0_kern.o | 3.4 基础 |
| 3.4.1 - 3.4.2 | openssl_3_4_1_kern.o | 3.4 变体 |
| 3.5.0 - 3.5.4 | openssl_3_5_0_kern.o | 3.5 基础组 |
注意: OpenSSL 3.0.12 是一个特殊情况,其内部结构偏移量与周围版本(3.0.11 和 3.0.13)不同,需要专用字节码。
来源: user/module/probe_openssl_lib.go:30-62, user/module/probe_openssl_lib.go:73-187
版本检测与字节码选择
检测流程
版本检测算法
detectOpenssl 函数对 OpenSSL 共享库执行二进制分析:
- ELF 解析:将共享库作为 ELF 文件打开并验证架构(x86_64 或 aarch64)
- 段扫描:定位包含只读数据的
.rodata段 - 版本字符串提取:以 1MB 块读取段,搜索正则表达式模式
(OpenSSL\s\d\.\d\.[0-9a-z]+) - 边界情况处理:使用重叠读取(每次迭代减去 30 字节)来处理跨缓冲区边界分割的版本字符串
来源: user/module/probe_openssl.go:178-278, user/module/probe_openssl_lib.go:189-282
降级策略
版本比较逻辑
isVersionLessOrEqual 函数通过以下方式比较版本字符串:
- 去除 "openssl " 前缀
- 按点分割(例如,
3.0.12→["3", "0", "12"]) - 将每个段解析为数字和字母部分(例如,
12a→(12, "a")) - 首先进行数字比较,然后对后缀进行字母比较
来源: user/module/probe_openssl_lib.go:341-422, user/module/probe_openssl_lib.go:284-317
eBPF 钩子架构
Uprobe 钩子点
Uprobe 实现细节
两阶段探针模式工作原理如下:
入口探针 (
probe_entry_SSL):- 捕获 SSL 上下文指针 (
PT_REGS_PARM1) 和缓冲区指针 (PT_REGS_PARM2) - 从
ssl->version偏移量读取 SSL 版本 - 调用
process_SSL_bio()从ssl->bio->num提取文件描述符和 BIO 类型 - 将上下文存储在以
pid_tgid为键的active_ssl_*_args_map中
- 捕获 SSL 上下文指针 (
返回探针 (
probe_ret_SSL):- 从
PT_REGS_RC检索返回值(读取/写入的字节数) - 从入口探针查找存储的上下文
- 调用
process_SSL_data()使用bpf_probe_read_user()读取实际缓冲区数据 - 通过性能事件数组向用户空间发送
ssl_data_event_t
- 从
来源: kern/openssl.h:268-323, kern/openssl.h:164-191, user/module/probe_openssl_text.go:46-151
连接跟踪集成
连接生命周期管理
该模块维护进程/文件描述符与网络连接之间的双向映射:
- pidConns:
map[pid]map[fd]{tuple, sock}- 通过进程和文件描述符查找连接信息 - sock2pidFd:
map[sock][pid, fd]- 用于在套接字销毁时清理的反向查找
当处理 SSLDataEvent 时,GetConn(pid, fd) 检索元组(源:端口-目标:端口)和套接字指针,用网络上下文丰富捕获的数据。
来源: user/module/probe_openssl.go:83-106, user/module/probe_openssl.go:406-488, kern/openssl.h:374-525
结构体偏移量计算
偏移量生成流程
偏移量提取机制
偏移量生成脚本自动化从 OpenSSL 源代码提取结构体成员偏移量的过程:
- 仓库设置:在
deps/openssl克隆或拉取 OpenSSL 仓库 - 版本迭代:遍历支持的版本(例如,3.0.0 到 3.0.17)
- 构建配置:运行
./config和make build_generated生成头文件 - 偏移量编译:编译使用
offsetof()宏计算结构体成员位置的offset.c - 头文件生成:执行编译后的二进制文件以输出 C 预处理器定义
偏移量输出示例(来自 offset.c):
printf("#define SSL_ST_VERSION %d\n", offsetof(struct ssl_st, version));
printf("#define SSL_ST_SESSION %d\n", offsetof(struct ssl_st, session));
printf("#define SSL_SESSION_ST_MASTER_KEY %d\n", offsetof(struct ssl_session_st, master_key));来源: utils/openssl_offset_3.0.sh:1-95, utils/openssl_offset_3.2.sh:1-82, utils/openssl_offset_3.3.sh:1-82
OpenSSL 3.x 结构变更
从 OpenSSL 3.2.0 开始,内部结构被重构:
| OpenSSL 版本 | 结构变更 | 偏移量映射 |
|---|---|---|
| 3.0.x, 3.1.x | 直接的 ssl_st 成员 | SSL_ST_VERSION, SSL_ST_WBIO, SSL_ST_RBIO |
| 3.2.x 及以后 | 通过 ssl_connection_st 间接访问 | SSL_CONNECTION_ST_VERSION, SSL_CONNECTION_ST_WBIO 等通过 #define SSL_ST_VERSION SSL_CONNECTION_ST_VERSION 映射回去 |
映射层允许 eBPF 代码使用一致的符号名称(SSL_ST_*),而实际偏移量值根据 OpenSSL 版本的内部结构布局而不同。
来源: utils/openssl_offset_3.2.sh:58-67, utils/openssl_offset_3.3.sh:58-67
主密钥提取
TLS 1.2 vs TLS 1.3 架构
主密钥结构
mastersecret_t 结构适用于 TLS 1.2 和 1.3:
struct mastersecret_t {
// 通用字段
s32 version; // TLS 版本
u8 client_random[SSL3_RANDOM_SIZE]; // 32 字节
// TLS 1.2 特定
u8 master_key[MASTER_SECRET_MAX_LEN]; // 48 字节
// TLS 1.3 特定
u32 cipher_id;
u8 early_secret[EVP_MAX_MD_SIZE]; // 64 字节
u8 handshake_secret[EVP_MAX_MD_SIZE]; // 64 字节
u8 handshake_traffic_hash[EVP_MAX_MD_SIZE]; // 64 字节
u8 client_app_traffic_secret[EVP_MAX_MD_SIZE]; // 64 字节
u8 server_app_traffic_secret[EVP_MAX_MD_SIZE]; // 64 字节
u8 exporter_master_secret[EVP_MAX_MD_SIZE]; // 64 字节
};来源: kern/openssl_masterkey.h:25-39, kern/openssl_masterkey.h:81-251, kern/openssl_masterkey_3.0.h:82-247
Keylog 文件生成 (TLS 1.3)
TLS 1.3 的 HKDF 密钥派生
TLS 1.3 使用 HKDF(基于 HMAC 的密钥派生函数)从握手密钥派生流量密钥。eBPF 程序捕获原始 handshake_secret 和 handshake_traffic_hash,用户空间执行:
clientHandshakeSecret = HKDF-Expand-Label(
handshake_secret,
"c hs traffic",
handshake_traffic_hash,
hash_length
)这与 TLS 1.3 RFC 8446 密钥调度匹配,生成 Wireshark 可用于解密的 SSLKEYLOGFILE 兼容输出。
来源: user/module/probe_openssl.go:490-583, user/module/probe_openssl.go:509-559
钩子函数选择
默认主密钥钩子函数(在模块级别定义):
var masterKeyHookFuncs = []string{
"SSL_do_handshake",
"SSL_connect",
"SSL_accept",
"SSL_in_before",
}对于 OpenSSL 1.0.x,SSL_in_before 被替换为 SSL_state,因为前者在旧版本中是宏而不是函数。
来源: user/module/probe_openssl.go:178-196, user/module/probe_openssl_keylog.go:32-94
MOpenSSLProbe 实现
核心结构
字段说明:
- bpfManager:管理 eBPF 程序生命周期(加载、附加、分离)
- eventFuncMaps:将 eBPF 映射映射到其相应的事件解码器
- pidConns:按 PID 和文件描述符进行连接跟踪
- sock2pidFd:从套接字指针到 [PID, FD] 的反向映射,用于清理
- keylogger:用于写入 SSLKEYLOGFILE 格式输出的文件句柄
- masterKeys:去重映射(client_random 十六进制 → bool)以避免重复密钥写入
- eBPFProgramType:确定要加载哪些 eBPF 程序(text/pcap/keylog)
- sslVersionBpfMap:版本字符串到字节码文件名的映射
- isBoringSSL:用于选择 BoringSSL 特定代码路径的标志
来源: user/module/probe_openssl.go:83-106, user/module/probe_openssl.go:109-176
初始化流程
来源: user/module/probe_openssl.go:109-176, user/module/probe_openssl.go:280-350, user/module/probe_openssl_text.go:18-188
捕获模式
模式比较
| 模式 | 使用的 eBPF 映射 | 输出格式 | 使用场景 |
|---|---|---|---|
| Text | tls_events, connect_events | 带元数据的明文 | 实时监控、调试 |
| Pcap | tls_events, connect_events, skb_events | 带 DSB 块的 Pcapng | Wireshark 分析、网络取证 |
| Keylog | mastersecret_events | SSLKEYLOGFILE 格式 | 预解密密钥提取 |
事件类型映射:
// 文本模式
m.eventFuncMaps[tls_events] = &event.SSLDataEvent{}
m.eventFuncMaps[connect_events] = &event.ConnDataEvent{}
// Keylog 模式
if m.isBoringSSL {
m.eventFuncMaps[mastersecret_events] = &event.MasterSecretBSSLEvent{}
} else {
m.eventFuncMaps[mastersecret_events] = &event.MasterSecretEvent{}
}
// Pcap 模式(包括文本模式的所有内容加上)
m.eventFuncMaps[skb_events] = &event.TcSkbEvent{}来源: user/module/probe_openssl_text.go:190-234, user/module/probe_openssl_keylog.go:97-118
文本模式架构
文本模式捕获 SSL 数据事件并用连接信息丰富它们:
来源: user/module/probe_openssl.go:741-783, user/module/probe_openssl_text.go:18-188
Keylog 模式架构
Keylog 模式专注于主密钥提取:
- 仅附加 uprobe 到主密钥钩子函数(不捕获数据)
- 以与 Wireshark 兼容的 NSS Key Log 格式写入密钥
- 通过 client_random 去重以避免重复密钥
- 对于 TLS 1.3,在用户空间执行 HKDF 密钥派生
Keylog 输出示例:
CLIENT_RANDOM 52d7... a8c9...
CLIENT_HANDSHAKE_TRAFFIC_SECRET 52d7... 9f3e...
SERVER_HANDSHAKE_TRAFFIC_SECRET 52d7... b2a1...
CLIENT_TRAFFIC_SECRET_0 52d7... 3c4d...
SERVER_TRAFFIC_SECRET_0 52d7... 7e8f...
EXPORTER_SECRET 52d7... 1a2b...来源: user/module/probe_openssl_keylog.go:32-118, user/module/probe_openssl.go:490-650
配置与过滤
OpensslConfig 结构
type OpensslConfig struct {
BaseConfig
Openssl string // libssl.so 路径
SslVersion string // 用户指定的版本
Model string // text/pcap/keylog
KeylogFile string // keylog 模式的输出路径
PcapFile string // pcap 模式的输出路径
PcapFilter string // BPF 过滤器表达式
ElfType uint8 // ElfTypeSo = 0
IsAndroid bool // Android 平台标志
AndroidVer string // Android 版本(例如,"13"、"14")
CGroupPath string // 用于过滤的 Cgroup 路径
}版本指定:
--ssl_version="openssl 3.0.12"- 精确版本--ssl_version="boringssl_a_14"- Android BoringSSL 变体- 如果未指定则自动检测
来源: user/module/probe_openssl.go:108-176, cli/cmd/root.go:156-175
用于过滤的常量编辑器
func (m *MOpenSSLProbe) constantEditor() []manager.ConstantEditor {
return []manager.ConstantEditor{
{
Name: "target_pid",
Value: uint64(m.conf.GetPid()),
},
{
Name: "target_uid",
Value: uint64(m.conf.GetUid()),
},
{
Name: "less52",
Value: kernelLess52, // 0 或 1
},
}
}这些常量在加载时在 eBPF 字节码中重写,允许高效过滤而无需映射查找。eBPF 中的 passes_filter() 函数在处理事件之前检查这些值。
来源: user/module/probe_openssl.go:361-395, kern/openssl.h:269-271
错误处理与边界情况
空密钥检测
模块在写入之前验证提取的密钥是否非空:
func (m *MOpenSSLProbe) mk13NullSecrets(hashLen int,
ClientHandshakeSecret, ClientTrafficSecret0,
ServerHandshakeSecret, ServerTrafficSecret0,
ExporterSecret [64]byte) bool {
isNullCount := 5
// 检查每个密钥;如果找到非零字节则递减计数器
for i := 0; i < hashLen; i++ {
if ClientHandshakeSecret[i] != 0 { isNullCount-- }
// ... 检查其他密钥
}
return isNullCount != 0 // 如果任何密钥全为零则返回 true
}这防止了将不完整或无效的密钥材料写入 keylog 文件。
来源: user/module/probe_openssl.go:697-739, user/module/probe_openssl.go:652-672
文件描述符回退
当 ssl->bio->num 为 0(未设置)时,模块检查由 SSL_set_fd 钩子填充的 ssl_st_fd 映射:
*fd = (u32)ssl_bio_num_addr;
if (*fd == 0) {
u64 ssl_addr = (u64)ssl;
u64 *fd_ptr = bpf_map_lookup_elem(&ssl_st_fd, &ssl_addr);
if (fd_ptr) {
*fd = (u32)*fd_ptr;
}
}这处理了显式调用 SSL_set_fd() 而不是使用 BIO 函数的应用程序。
来源: kern/openssl.h:225-267, kern/openssl.h:528-543
BIO 类型过滤
模块读取 BIO 方法类型以区分不同的 I/O 类型:
#define BIO_TYPE_SOURCE_SINK 0x0400
#define BIO_TYPE_DESCRIPTOR 0x0100具有 bio_type > BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR 的事件可能表示非套接字 BIO(例如,内存 BIO),并被记录为警告但不会被丢弃,因为 fd 仍可能从 ssl_st_fd 映射中有效。
来源: kern/openssl.h:193-223, user/module/probe_openssl.go:764-779
总结
OpenSSL 模块通过以下方式展示了复杂的二进制插桩:
- 版本无关设计:通过偏移量分组支持 100 多个 OpenSSL 版本(26 个 1.0.2.x 版本共享一个字节码文件)
- 双 TLS 协议支持:处理 TLS 1.2(单个 master_key)和 TLS 1.3(使用 HKDF 派生的多个流量密钥)
- 多模态输出:文本(实时)、pcap(Wireshark 兼容)和 keylog(预解密密钥)
- 智能回退:自动版本降级、libcrypto 回退和默认字节码选择
- 连接丰富:集成基于 kprobe 的 TCP 连接跟踪,将 SSL 事件映射到网络元组
该模块的架构清晰地分离了关注点:版本检测在初始化时进行,eBPF 程序处理内核空间数据捕获,用户空间使用协议特定的解析器和密钥派生逻辑处理事件。
来源: user/module/probe_openssl.go:1-795, user/module/probe_openssl_lib.go:1-449, kern/openssl.h:1-544