Skip to content

OpenSSL 模块

目的与范围

OpenSSL 模块从使用 OpenSSL 库(版本 1.0.x、1.1.x 和 3.x)的应用程序中捕获 TLS/SSL 明文流量和主密钥。它使用 eBPF uprobe 钩住 SSL_readSSL_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.2uopenssl_1_0_2a_kern.o单一组(26 个版本)
1.1.0a - 1.1.0lopenssl_1_1_0a_kern.o单一组(12 个版本)
1.1.1aopenssl_1_1_1a_kern.o组 A
1.1.1b - 1.1.1copenssl_1_1_1b_kern.o组 B
1.1.1d - 1.1.1iopenssl_1_1_1d_kern.o组 C
1.1.1j - 1.1.1wopenssl_1_1_1j_kern.o组 D
3.0.0 - 3.0.11, 3.0.13 - 3.0.17openssl_3_0_0_kern.o标准 3.0.x
3.0.12openssl_3_0_12_kern.o特殊情况(唯一偏移量)
3.1.0 - 3.1.8openssl_3_1_0_kern.o与 3.0.x 兼容
3.2.0 - 3.2.2openssl_3_2_0_kern.o3.2 基础组
3.2.3openssl_3_2_3_kern.o3.2 变体 A
3.2.4 - 3.2.5openssl_3_2_4_kern.o3.2 变体 B
3.3.0 - 3.3.1openssl_3_3_0_kern.o3.3 基础组
3.3.2openssl_3_3_2_kern.o3.3 变体 A
3.3.3 - 3.3.4openssl_3_3_3_kern.o3.3 变体 B
3.4.0openssl_3_4_0_kern.o3.4 基础
3.4.1 - 3.4.2openssl_3_4_1_kern.o3.4 变体
3.5.0 - 3.5.4openssl_3_5_0_kern.o3.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 共享库执行二进制分析:

  1. ELF 解析:将共享库作为 ELF 文件打开并验证架构(x86_64 或 aarch64)
  2. 段扫描:定位包含只读数据的 .rodata
  3. 版本字符串提取:以 1MB 块读取段,搜索正则表达式模式 (OpenSSL\s\d\.\d\.[0-9a-z]+)
  4. 边界情况处理:使用重叠读取(每次迭代减去 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 实现细节

两阶段探针模式工作原理如下:

  1. 入口探针 (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
  2. 返回探针 (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

连接跟踪集成

连接生命周期管理

该模块维护进程/文件描述符与网络连接之间的双向映射:

  • pidConnsmap[pid]map[fd]{tuple, sock} - 通过进程和文件描述符查找连接信息
  • sock2pidFdmap[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 源代码提取结构体成员偏移量的过程:

  1. 仓库设置:在 deps/openssl 克隆或拉取 OpenSSL 仓库
  2. 版本迭代:遍历支持的版本(例如,3.0.0 到 3.0.17)
  3. 构建配置:运行 ./configmake build_generated 生成头文件
  4. 偏移量编译:编译使用 offsetof() 宏计算结构体成员位置的 offset.c
  5. 头文件生成:执行编译后的二进制文件以输出 C 预处理器定义

偏移量输出示例(来自 offset.c):

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:

c
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_secrethandshake_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

钩子函数选择

默认主密钥钩子函数(在模块级别定义):

go
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 映射输出格式使用场景
Texttls_events, connect_events带元数据的明文实时监控、调试
Pcaptls_events, connect_events, skb_events带 DSB 块的 PcapngWireshark 分析、网络取证
Keylogmastersecret_eventsSSLKEYLOGFILE 格式预解密密钥提取

事件类型映射

go
// 文本模式
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 结构

go
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

用于过滤的常量编辑器

go
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


错误处理与边界情况

空密钥检测

模块在写入之前验证提取的密钥是否非空:

go
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 映射:

c
*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 类型:

c
#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 模块通过以下方式展示了复杂的二进制插桩:

  1. 版本无关设计:通过偏移量分组支持 100 多个 OpenSSL 版本(26 个 1.0.2.x 版本共享一个字节码文件)
  2. 双 TLS 协议支持:处理 TLS 1.2(单个 master_key)和 TLS 1.3(使用 HKDF 派生的多个流量密钥)
  3. 多模态输出:文本(实时)、pcap(Wireshark 兼容)和 keylog(预解密密钥)
  4. 智能回退:自动版本降级、libcrypto 回退和默认字节码选择
  5. 连接丰富:集成基于 kprobe 的 TCP 连接跟踪,将 SSL 事件映射到网络元组

该模块的架构清晰地分离了关注点:版本检测在初始化时进行,eBPF 程序处理内核空间数据捕获,用户空间使用协议特定的解析器和密钥派生逻辑处理事件。

来源: user/module/probe_openssl.go:1-795, user/module/probe_openssl_lib.go:1-449, kern/openssl.h:1-544

OpenSSL 模块 has loaded