news 2026/9/14 8:55:14

ecapture 内核态源码解读:基于 OpenSSL 内部结构体偏移的 TLS 明文与主密钥捕获原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ecapture 内核态源码解读:基于 OpenSSL 内部结构体偏移的 TLS 明文与主密钥捕获原理

ecapture 内核态源码解读:基于 OpenSSL 内部结构体偏移的 TLS 明文与主密钥捕获原理

【免费下载链接】ecaptureCapturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64.项目地址: https://gitcode.com/GitHub_Trending/ec/ecapture

本文以 kern/README.md 为核心,解读 ecapture 在内核态(eBPF)中如何依赖 OpenSSL 各版本的struct ssl_ststruct bio_st字段偏移和 TLS 版本常量来定位 SSL 会话状态,进而从用户态进程中安全地提取 TLS 1.2 主密钥与 TLS 1.3 各阶段密钥。读完本文,你能理解 ecapture 支持多个 OpenSSL/BoringSSL 小版本内核文件的根本原因、每个 BPF 探针读取哪些结构体字段,以及捕获到的秘密如何按 NSS Key Log 格式输出供解密工具使用。

为什么必须精确掌握 OpenSSL 内部结构体布局

ecapture 的核心技术路线是:通过 uprobe 挂在目标进程的SSL_writeSSL_readSSL_write_key等导出函数上,在 eBPF 程序里直接读取用户态 SSL 对象(SSL*)的内存。eBPF 无法调用任何用户态库函数,也不认识任何 C 结构体,它只能通过"结构体指针 + 字段偏移"逐个bpf_probe_read_user出需要的字节。这意味着:

  • 版本常量TLS1_3_VERSION = 0x0304等)用来判断当前连接跑的是 TLS 1.2 还是 TLS 1.3,从而决定读取master_key还是 TLS 1.3 的各阶段 secret;
  • 结构体字段偏移(如SSL_ST_VERSIONSSL_ST_SESSIONSSL_SESSION_ST_MASTER_KEY)决定了从SSL*指针出发如何一步步走到密钥内存。

kern/README.md 正是对这套"结构体布局知识"的存档:它记录了 OpenSSL 1.0.x / 1.1.x 两个大版本下struct ssl_ststruct bio_st的布局差异,以及 OpenSSL 内部 label 与 BoringSSL 内部结构字段在 master secret 语义上的对照表。这正是kern/目录下存在openssl_1_0_2a_kern.copenssl_1_1_0a_kern.copenssl_1_1_1d_kern.c直到openssl_3_5_0_kern.c十余个版本专属内核文件的根因——不同小版本的字段偏移、甚至字段组织方式(如 3.2 起SSL变为SSL_CONNECTION聚合体)都可能变化。

OpenSSL 1.1.x:版本常量与 ssl_st / bio_st 布局

kern/README.md 首先给出了 OpenSSL 1.1.* 使用的协议版本常量。这些常量与共享头文件 tls_constants.h 中的定义一致:

//https://github.com/openssl/openssl/blob/3e8f70c30d84861fcd257a6e280dc49e104eb145/ssl/ssl_local.h#L1068 struct ssl_st { /* * protocol version (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION, * DTLS1_VERSION) */ int version; /* SSLv3 */ const SSL_METHOD *method; /* * There are 2 BIO's even though they are normally both the same. This * is so data can be read and written to different handlers */ /* used by SSL_read */ BIO *rbio; /* used by SSL_write */ BIO *wbio; /* used during session-id reuse to concatenate messages */ BIO *bbio; // ... } //https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/crypto/bio/bio_local.h struct bio_st { const BIO_METHOD *method; /* bio, mode, argp, argi, argl, ret */ BIO_callback_fn callback; BIO_callback_fn_ex callback_ex; char *cb_arg; /* first argument for the callback */ int init; int shutdown; int flags; /* extra storage */ int retry_reason; int num; void *ptr; struct bio_st *next_bio; /* used by filter BIOs */ struct bio_st *prev_bio; /* used by filter BIOs */ CRYPTO_REF_COUNT references; uint64_t num_read; uint64_t num_write; CRYPTO_EX_DATA ex_data; CRYPTO_RWLOCK *lock; };

对应的版本常量(摘自 kern/README.md,与 tls_constants.h 中TLS1_1_VERSION 0x0302TLS1_2_VERSION 0x0303TLS1_3_VERSION 0x0304呼应):

说明
SSL3_VERSION0x0300SSLv3
TLS1_VERSION0x0301TLS 1.0
TLS1_1_VERSION0x0302TLS 1.1
TLS1_2_VERSION0x0303TLS 1.2
TLS1_3_VERSION0x0304TLS 1.3
DTLS1_VERSION0xFEFFDTLS 1.0
DTLS1_2_VERSION0xFEFDDTLS 1.2
DTLS1_BAD_VER0x0100早期 DTLS 协商值

从这份布局可以读出 ecapture 用到的三类关键偏移:

  1. ssl_st->versionint,紧跟结构体开头):探针用它区分 TLS 1.2 及以下与 TLS 1.3 的取密钥路径;
  2. ssl_st->rbio/wbioBIO*指针):明文捕获探针通过SSL_ST_RBIO/SSL_ST_WBIO偏移取到 BIO 指针,再读bio_st->method->type判断 BIO 类型,读bio_st->num得到 socket fd——这在 openssl.h 的process_SSL_bio()(约 L224-L265)中有完整实现;
  3. ssl_st->session->master_key:TLS 1.2 主密钥所在,需经过SSL_ST_SESSION再走SSL_SESSION_ST_MASTER_KEY两级偏移。

bio_streferences在 1.0.x 是int,在 1.1.x 变为CRYPTO_REF_COUNT并新增num_read/num_write/lock字段——这类"看起来无害"的结构体变化正是导致 BIO 相关偏移(如BIO_ST_METHODBIO_ST_NUM)必须按小版本校准的原因。

OpenSSL 1.0.x:与 1.1.x 的关键差异

kern/README.md 同时保留了 OpenSSL 1.0.*(引用自OpenSSL_1_0_0-stable分支)的布局:

struct ssl_st { /* * protocol version (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION, * DTLS1_VERSION) */ int version; /* SSL_ST_CONNECT or SSL_ST_ACCEPT */ int type; /* SSLv3 */ const SSL_METHOD *method; /* * There are 2 BIO's even though they are normally both the same. This * is so data can be read and written to different handlers */ # ifndef OPENSSL_NO_BIO /* used by SSL_read */ BIO *rbio; /* used by SSL_write */ BIO *wbio; /* used during session-id reuse to concatenate messages */ BIO *bbio; # else /* used by SSL_read */ char *rbio; /* used by SSL_write */ char *wbio; char *bbio; # endif /* *
struct bio_st { BIO_METHOD *method; /* bio, mode, argp, argi, argl, ret */ long (*callback) (struct bio_st *, int, const char *, int, long, long); char *cb_arg; /* first argument for the callback */ int init; int shutdown; int flags; /* extra storage */ int retry_reason; int num; void *ptr; struct bio_st *next_bio; /* used by filter BIOs */ struct bio_st *prev_bio; /* used by filter BIOs */ int references; unsigned long num_read; unsigned long num_write; CRYPTO_EX_DATA ex_data; };

对比 1.1.x,值得注意的差异:

  • bio_st->method类型:1.0.x 为BIO_METHOD *method(可变指针),1.1.x 为const BIO_METHOD *method。对 eBPF 而言取值方式相同,但这是偏移生成工具需要区分的签名差异;
  • bio_st->callback:1.0.x 是函数指针long (*callback)(...),1.1.x 拆分为callback+callback_ex两个字段,导致其后所有字段偏移整体漂移;
  • ssl_st无 BIO 时的降级形态:1.0.x 在OPENSSL_NO_BIO编译下rbio/wbio/bbio退化为char*,1.1.x 文档未保留该分支。

这些差异对应到仓库中 openssl_1_0_2a_kern.c 与 openssl_1_1_1a_kern.c 等文件各自携带的偏移宏(SSL_ST_*BIO_ST_*等),而 utils/openssl_offset_1.1.1.sh、utils/openssl_offset_1.0.2.sh 等脚本配合 utils/openssl_1_1_1_offset.c、utils/openssl_1_0_2_offset.c 正是用于在目标环境上实际测量这些偏移。

eBPF 侧的落地:masterkey 探针如何消费这些偏移

理解 kern/README.md 记录布局的最终目的,是看懂 openssl_masterkey.h(1.x 版)、openssl_masterkey_3.0.h(3.0/3.1 版)与 openssl_masterkey_3.2.h(3.2+ 版)三个探针文件。以 openssl_masterkey.h 为例,probe_ssl_master_key的取数路径是:

  1. ssl_st_ptr + SSL_ST_VERSION读出int version存入mastersecret->version
  2. SSL_ST_S3得到ssl3_state_st*,再偏移SSL3_STATE_ST_CLIENT_RANDOM读出 32 字节 client_random;
  3. SSL_ST_SESSION得到SSL_SESSION*
  4. version != TLS1_3_VERSION(即 TLS 1.2 及以下):直接从SSL_SESSION_ST_MASTER_KEY读 48 字节master_key,这正是 kern/README.md 对照表中MASTER_SECRET_LABEL → s->session->master_key的读取实现;
  5. 若为 TLS 1.3:改走SSL_ST_EARLY_SECRETSSL_ST_HANDSHAKE_SECRETSSL_ST_HANDSHAKE_TRAFFIC_HASHSSL_ST_CLIENT_APP_TRAFFIC_SECRETSSL_ST_SERVER_APP_TRAFFIC_SECRETSSL_ST_EXPORTER_MASTER_SECRET等偏移,收集 TLS 1.3 的各阶段 secret。

所有秘密字段都汇聚到共享结构体 mastersecret_t:

struct mastersecret_t { /* TLS 1.2 or older */ s32 version; u8 client_random[SSL3_RANDOM_SIZE]; u8 master_key[MASTER_SECRET_MAX_LEN]; /* TLS 1.3 */ u32 cipher_id; u8 early_secret[EVP_MAX_MD_SIZE]; u8 handshake_secret[EVP_MAX_MD_SIZE]; u8 handshake_traffic_hash[EVP_MAX_MD_SIZE]; u8 client_app_traffic_secret[EVP_MAX_MD_SIZE]; u8 server_app_traffic_secret[EVP_MAX_MD_SIZE]; u8 exporter_master_secret[EVP_MAX_MD_SIZE]; };

其中SSL3_RANDOM_SIZE 32MASTER_SECRET_MAX_LEN 48EVP_MAX_MD_SIZE 64三个尺寸常量在 tls_constants.h 中定义。由于 BPF 程序栈限制为 512 字节,该结构体通过bpf_context_gen(PERCPU_ARRAY 充当"BPF 堆")分配,并由make_event()借助bpf_context(LRU_HASH,以 pid_tgid 为键)返回指针,最终经mastersecret_events(PERF_EVENT_ARRAY)回传用户态。

3.2+ 版本的结构变化在 openssl_masterkey_3.2.h 中体现得最明显:SSL对象被拆分为SSL_CONNECTION等类型,探针改用SSL_CONNECTION_ST_*系列偏移,并对ssl->type == SSL_TYPE_QUIC_CONNECTION的场景做了显式拦截(打印 "coming soon"),说明 QUIC 连接的密钥捕获当时尚未支持。

master secrets 对照表:OpenSSL 与 BoringSSL 的秘密映射

kern/README.md 的另一核心资产是"OpenSSL label ↔ BoringSSL 结构字段"对照表,它把两个生态里语义相同、命名不同的 TLS 秘密对齐到同一组 NSS Key Log 标签上:

OpenSSL label 宏OpenSSL 结构字段NSS Key Log LabelBoringSSL 结构字段
MASTER_SECRET_LABELs->session->master_keyCLIENT_RANDOMsession->secret
EXPORTER_SECRETs->exporter_master_secretEXPORTER_SECRETssl->s3->exporter_secret
EARLY_EXPORTER_SECRET_LABELs->early_exporter_master_secretEARLY_EXPORTER_SECRET-
SERVER_APPLICATION_LABELs->server_app_traffic_secretSERVER_TRAFFIC_SECRET_0hs->server_traffic_secret_0()
CLIENT_APPLICATION_LABELs->client_app_traffic_secretCLIENT_TRAFFIC_SECRET_0hs->client_traffic_secret_0()
SERVER_HANDSHAKE_LABEL(由 handshake_secret 派生)SERVER_HANDSHAKE_TRAFFIC_SECREThs->server_handshake_secret()
CLIENT_HANDSHAKE_LABEL(由 handshake_secret 派生)CLIENT_HANDSHAKE_TRAFFIC_SECREThs->client_handshake_secret()
CLIENT_EARLY_LABEL(由 early_secret 派生)CLIENT_EARLY_TRAFFIC_SECREThs->early_traffic_secret()

这张表解释了为什么 ecapture 既需要openssl_masterkey*.h系列(挂 OpenSSL 的SSL_write_key符号),又需要 boringssl_masterkey.h 与 boringssl_na_kern.c 等 BoringSSL 探针:两边的SSL*对象布局完全不同,但捕获到的秘密最终都归一到 NSS 标签体系。这也解释了为什么 Android 平台(大量应用内嵌 BoringSSL)需要 utils/boringssl_offset.c 与 utils/boringssl_android_offset.sh 做独立的偏移测量。

TLS 1.3 密钥推导:HKDF-Expand-Label 的具体参数

kern/README.md 后半部分逐条列出了各 label 在 OpenSSL 源码中derive_secret_key_and_iv/tls13_derive时的参数(insecret/finsecret/label/labellen)。整理后如下:

SERVER_APPLICATION_LABEL(服务端应用数据密钥)

insecret = s->master_secret; label = server_application_traffic; labellen = sizeof(server_application_traffic) - 1; log_label = SERVER_APPLICATION_LABEL;

CLIENT_APPLICATION_LABEL(客户端应用数据密钥)

insecret = s->master_secret; label = client_application_traffic; labellen = sizeof(client_application_traffic) - 1; log_label = CLIENT_APPLICATION_LABEL;

SERVER_HANDSHAKE_LABEL(服务端握手流量密钥)

insecret = s->handshake_secret; finsecret = s->server_finished_secret; finsecretlen = EVP_MD_size(ssl_handshake_md(s)); label = server_handshake_traffic; labellen = sizeof(server_handshake_traffic) - 1; log_label = SERVER_HANDSHAKE_LABEL;

"再计算"步骤(即从握手 hash 派生实际密钥):

memcpy(s->handshake_traffic_hash, hashval, hashlen); derive_secret_key_and_iv(s, which & SSL3_CC_WRITE, md, cipher, insecret, hash, label, labellen, secret, iv, ciph_ctx)

CLIENT_HANDSHAKE_LABEL(客户端握手流量密钥)

insecret = s->handshake_secret; finsecret = s->client_finished_secret; finsecretlen = EVP_MD_size(ssl_handshake_md(s)); label = client_handshake_traffic; labellen = sizeof(client_handshake_traffic) - 1; log_label = CLIENT_HANDSHAKE_LABEL; hash = s->handshake_traffic_hash;

CLIENT_EARLY_LABEL(0-RTT 早期数据密钥)

insecret = s->early_secret; label = client_early_traffic; labellen = sizeof(client_early_traffic) - 1; log_label = CLIENT_EARLY_LABEL;

从源码结构看,这段笔记的要点是:TLS 1.3 的 handshake/early 密钥不是直接读出来的,而是用handshake_secret+handshake_traffic_hash作为 HKDF-Expand-Label 输入现算的。这与用户态实现完全吻合——keylog_handler.go 在 TLS 1.3 分支中正是这样做的:

secrets[hkdf.KeyLogLabelClientHandshake] = hkdf.ExpandLabel(event.GetHandshakeSecret()[:length], hkdf.ClientHandshakeTrafficLabel, event.GetHandshakeTrafficHash()[:length], length, transcript) secrets[hkdf.KeyLogLabelServerHandshake] = hkdf.ExpandLabel(event.GetHandshakeSecret()[:length], hkdf.ServerHandshakeTrafficLabel, event.GetHandshakeTrafficHash()[:length], length, transcript)

其中length(32 或 48)与transcript(SHA256/SHA384)由 cipher suite 决定,对应笔记中的finsecretlen = EVP_MD_size(ssl_handshake_md(s))。这也解释了为什么 eBPF 探针必须同时捕获handshake_secrethandshake_traffic_hash两个字段——缺了 hash 就无法派生握手密钥。

从 eBPF 事件到 NSS Key Log:完整闭环

捕获的秘密最终由 KeylogHandler 统一格式化。它按事件接口区分两条路径(keylog_handler.go):

  • OpenSSL 风格MasterSecretEvent,带version字段):version <= 0x0303走 TLS 1.2 路径输出CLIENT_RANDOM <64位hex client_random> <96位hex master_secret>version == 0x0304走 TLS 1.3 路径,逐条输出CLIENT_TRAFFIC_SECRET_0SERVER_TRAFFIC_SECRET_0EXPORTER_SECRETCLIENT_EARLY_TRAFFIC_SECRET,并按上文 HKDF 规则现算握手密钥(keylog_handler.go)。其中0x0303判界正源自本文开头的版本常量表。
  • GoTLS 风格GoTLSMasterSecretEvent,带label字段):直接按LABEL <client_random> <secret>输出,用于 gotls_kern.c 捕获的 Go 程序 TLS 事件。

几个工程细节值得注意:

  • 零值跳过isZeroBytes()检查会静默丢弃全零密钥,因为 uprobe 可能早于握手完成就触发,此时字段尚未填充(keylog_handler.go);
  • 去重:以label + client_random为键的seenKeysmap 保证同一连接同一 secret 类型只输出一次,相关行为有 keylog_handler_test.go 与 keylog_handler_dedup_test.go 覆盖;
  • 明文捕获(keylog 之外的另一条数据通路)由 openssl.h 中成对的uprobe/SSL_write+uretprobe/SSL_write(以及 SSL_read 对应探针)实现:入口探针把SSL*、buf 指针、fd、BIO type、version 存入active_ssl_write_args_map,返回探针再读返回值长度、拷贝data并经tls_events回传。

小结:从结构体笔记到可复现的捕获能力

kern/README.md 篇幅不长,但它承载了 ecapture 内核态代码的一组基础事实:

  1. 版本常量是 eBPF 程序内部分支(TLS 1.2 vs 1.3 取密钥路径)和用户态 KeylogHandler 分派的共同依据;
  2. ssl_st/bio_st布局笔记解释了每个SSL_ST_*/BIO_ST_*偏移的来源,以及为何 1.0.x/1.1.x/3.x 各自需要独立内核文件与偏移测量工具(utils/下的 offset 脚本族);
  3. master secret 对照表统一了 OpenSSL、BoringSSL 与 NSS Key Log 三种命名体系,是跨库兼容(含 Android 场景)的语义基准;
  4. HKDF-Expand-Label 参数笔记直接对应用户态 keylog_handler.go 的 TLS 1.3 密钥派生实现。

继续深入的阅读路径建议:kern/openssl.h(明文捕获主流程)、kern/include/openssl_masterkey_common.h(事件结构与 BPF map 定义)、kern/openssl_masterkey_3.2.h(3.2+ 结构变化)以及 internal/probe/base/handlers/keylog_handler.go(NSS 格式输出与测试)。

【免费下载链接】ecaptureCapturing SSL/TLS plaintext without a CA certificate using eBPF. Supported on Linux/Android kernels for amd64/arm64.项目地址: https://gitcode.com/GitHub_Trending/ec/ecapture

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/14 8:53:03

Dozzle:面向 Docker、Swarm 与 K8s 的实时容器日志查看器

Dozzle&#xff1a;面向 Docker、Swarm 与 K8s 的实时容器日志查看器 【免费下载链接】dozzle Realtime log viewer for containers. Supports Docker, Swarm and K8s. 项目地址: https://gitcode.com/GitHub_Trending/do/dozzle 本文围绕 Dozzle 项目的 README 展开&a…

作者头像 李华
网站建设 2026/9/14 8:50:34

MCP协议中context-mode与SQLite FTS5上下文检索实战

1. “context-mode”不是功能开关&#xff0c;而是MCP协议中上下文感知能力的底层抽象最近在多个AI工程实践场景里反复看到“context-mode”这个短语——它既不出现在任何主流框架的官方文档首页&#xff0c;也不作为独立CLI参数被显式声明&#xff0c;却频繁出现在Figma插件日…

作者头像 李华
网站建设 2026/9/14 8:50:30

Agent-as-a-Judge:大模型智能体自动化评估框架解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/14 8:48:51

context-mode不是开关,而是SQLite语义协同的启动协议

1. “context-mode”不是功能开关&#xff0c;而是智能体系统里的一次范式迁移 你第一次在某个开源项目文档里看到 context-mode: true 这行配置时&#xff0c;大概率会下意识把它当成一个“开启上下文记忆”的普通开关——就像 debug: true 或 verbose: true 那样。我当…

作者头像 李华
网站建设 2026/9/14 8:48:00

基于JSP与SQLServer的高校科研项目管理系统设计与QR码实现

简介&#xff1a;这是一套面向高校计算机专业毕业设计的Java/JSP高校科研项目管理系统源码包&#xff0c;后端使用SQL Server数据库&#xff0c;JDK1.8环境&#xff0c;适用于Eclipse、MyEclipse、STS、IDEA等常见开发工具。系统围绕教师科研与论文信息交流场景&#xff0c;实现…

作者头像 李华