ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

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_st、struct bio_st字段偏移和 TLS 版本常量来定位 SSL 会话状态进而从用户态进程中安全地提取 TLS 1.2 主密钥与 TLS 1.3 各阶段密钥。读完本文你能理解 ecapture 支持多个 OpenSSL/BoringSSL 小版本内核文件的根本原因、每个 BPF 探针读取哪些结构体字段以及捕获到的秘密如何按 NSS Key Log 格式输出供解密工具使用。为什么必须精确掌握 OpenSSL 内部结构体布局ecapture 的核心技术路线是通过 uprobe 挂在目标进程的SSL_write、SSL_read、SSL_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_VERSION、SSL_ST_SESSION、SSL_SESSION_ST_MASTER_KEY决定了从SSL*指针出发如何一步步走到密钥内存。kern/README.md 正是对这套结构体布局知识的存档它记录了 OpenSSL 1.0.x / 1.1.x 两个大版本下struct ssl_st与struct bio_st的布局差异以及 OpenSSL 内部 label 与 BoringSSL 内部结构字段在 master secret 语义上的对照表。这正是kern/目录下存在openssl_1_0_2a_kern.c、openssl_1_1_0a_kern.c、openssl_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 BIOs 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 0x0302、TLS1_2_VERSION 0x0303、TLS1_3_VERSION 0x0304呼应宏值说明SSL3_VERSION0x0300SSLv3TLS1_VERSION0x0301TLS 1.0TLS1_1_VERSION0x0302TLS 1.1TLS1_2_VERSION0x0303TLS 1.2TLS1_3_VERSION0x0304TLS 1.3DTLS1_VERSION0xFEFFDTLS 1.0DTLS1_2_VERSION0xFEFDDTLS 1.2DTLS1_BAD_VER0x0100早期 DTLS 协商值从这份布局可以读出 ecapture 用到的三类关键偏移ssl_st-versionint紧跟结构体开头探针用它区分 TLS 1.2 及以下与 TLS 1.3 的取密钥路径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中有完整实现ssl_st-session-master_keyTLS 1.2 主密钥所在需经过SSL_ST_SESSION再走SSL_SESSION_ST_MASTER_KEY两级偏移。bio_st的references在 1.0.x 是int在 1.1.x 变为CRYPTO_REF_COUNT并新增num_read/num_write/lock字段——这类看起来无害的结构体变化正是导致 BIO 相关偏移如BIO_ST_METHOD、BIO_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 BIOs 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-callback1.0.x 是函数指针long (*callback)(...)1.1.x 拆分为callbackcallback_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.h1.x 版、openssl_masterkey_3.0.h3.0/3.1 版与 openssl_masterkey_3.2.h3.2 版三个探针文件。以 openssl_masterkey.h 为例probe_ssl_master_key的取数路径是ssl_st_ptr SSL_ST_VERSION读出int version存入mastersecret-version读SSL_ST_S3得到ssl3_state_st*再偏移SSL3_STATE_ST_CLIENT_RANDOM读出 32 字节 client_random读SSL_ST_SESSION得到SSL_SESSION*若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的读取实现若为 TLS 1.3改走SSL_ST_EARLY_SECRET、SSL_ST_HANDSHAKE_SECRET、SSL_ST_HANDSHAKE_TRAFFIC_HASH、SSL_ST_CLIENT_APP_TRAFFIC_SECRET、SSL_ST_SERVER_APP_TRAFFIC_SECRET、SSL_ST_EXPORTER_MASTER_SECRET等偏移收集 TLS 1.3 的各阶段 secret。所有秘密字段都汇聚到共享结构体 mastersecret_tstruct 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 32、MASTER_SECRET_MAX_LEN 48、EVP_MAX_MD_SIZE 64三个尺寸常量在 tls_constants.h 中定义。由于 BPF 程序栈限制为 512 字节该结构体通过bpf_context_genPERCPU_ARRAY 充当BPF 堆分配并由make_event()借助bpf_contextLRU_HASH以 pid_tgid 为键返回指针最终经mastersecret_eventsPERF_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-secretEXPORTER_SECRETs-exporter_master_secretEXPORTER_SECRETssl-s3-exporter_secretEARLY_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_LABEL0-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_secrethandshake_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)其中length32 或 48与transcriptSHA256/SHA384由 cipher suite 决定对应笔记中的finsecretlen EVP_MD_size(ssl_handshake_md(s))。这也解释了为什么 eBPF 探针必须同时捕获handshake_secret和handshake_traffic_hash两个字段——缺了 hash 就无法派生握手密钥。从 eBPF 事件到 NSS Key Log完整闭环捕获的秘密最终由 KeylogHandler 统一格式化。它按事件接口区分两条路径keylog_handler.goOpenSSL 风格MasterSecretEvent带version字段version 0x0303走 TLS 1.2 路径输出CLIENT_RANDOM 64位hex client_random 96位hex master_secretversion 0x0304走 TLS 1.3 路径逐条输出CLIENT_TRAFFIC_SECRET_0、SERVER_TRAFFIC_SECRET_0、EXPORTER_SECRET、CLIENT_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_writeuretprobe/SSL_write以及 SSL_read 对应探针实现入口探针把SSL*、buf 指针、fd、BIO type、version 存入active_ssl_write_args_map返回探针再读返回值长度、拷贝data并经tls_events回传。小结从结构体笔记到可复现的捕获能力kern/README.md 篇幅不长但它承载了 ecapture 内核态代码的一组基础事实版本常量是 eBPF 程序内部分支TLS 1.2 vs 1.3 取密钥路径和用户态 KeylogHandler 分派的共同依据ssl_st/bio_st布局笔记解释了每个SSL_ST_*/BIO_ST_*偏移的来源以及为何 1.0.x/1.1.x/3.x 各自需要独立内核文件与偏移测量工具utils/下的 offset 脚本族master secret 对照表统一了 OpenSSL、BoringSSL 与 NSS Key Log 三种命名体系是跨库兼容含 Android 场景的语义基准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.h3.2 结构变化以及 internal/probe/base/handlers/keylog_handler.goNSS 格式输出与测试。【免费下载链接】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),仅供参考
返回列表