Skip to content

OpenSSL Module

Relevant source files

The following files were used as context for generating this wiki page:

Purpose and Scope

The OpenSSL Module captures TLS/SSL plaintext traffic and master secrets from applications using the OpenSSL library (versions 1.0.x, 1.1.x, and 3.x). It uses eBPF uprobes to hook into SSL_read and SSL_write functions without requiring application modification or root CA certificates. The module supports three output modes: text (plaintext display), pcap (network packet capture), and keylog (master secret extraction for decryption).

For BoringSSL support (Android and non-Android variants), see BoringSSL Module. For Go TLS capture, see Go TLS Module. For master secret extraction implementation details across all libraries, see Master Secret Extraction.

Sources: user/module/probe_openssl.go:15-106, cli/cmd/root.go:86-101


Supported OpenSSL Versions

The module supports a wide range of OpenSSL versions through intelligent bytecode selection and offset grouping:

Version RangeBytecode FileOffset Groups
1.0.2a - 1.0.2uopenssl_1_0_2a_kern.oSingle group (26 versions)
1.1.0a - 1.1.0lopenssl_1_1_0a_kern.oSingle group (12 versions)
1.1.1aopenssl_1_1_1a_kern.oGroup A
1.1.1b - 1.1.1copenssl_1_1_1b_kern.oGroup B
1.1.1d - 1.1.1iopenssl_1_1_1d_kern.oGroup C
1.1.1j - 1.1.1wopenssl_1_1_1j_kern.oGroup D
3.0.0 - 3.0.11, 3.0.13 - 3.0.17openssl_3_0_0_kern.oStandard 3.0.x
3.0.12openssl_3_0_12_kern.oSpecial case (unique offsets)
3.1.0 - 3.1.8openssl_3_1_0_kern.oCompatible with 3.0.x
3.2.0 - 3.2.2openssl_3_2_0_kern.o3.2 base group
3.2.3openssl_3_2_3_kern.o3.2 variant A
3.2.4 - 3.2.5openssl_3_2_4_kern.o3.2 variant B
3.3.0 - 3.3.1openssl_3_3_0_kern.o3.3 base group
3.3.2openssl_3_3_2_kern.o3.3 variant A
3.3.3 - 3.3.4openssl_3_3_3_kern.o3.3 variant B
3.4.0openssl_3_4_0_kern.o3.4 base
3.4.1 - 3.4.2openssl_3_4_1_kern.o3.4 variant
3.5.0 - 3.5.4openssl_3_5_0_kern.o3.5 base group

Note: OpenSSL 3.0.12 is a special case where internal structure offsets differ from surrounding versions (3.0.11 and 3.0.13), requiring dedicated bytecode.

Sources: user/module/probe_openssl_lib.go:30-62, user/module/probe_openssl_lib.go:73-187


Version Detection and Bytecode Selection

Detection Process

Version Detection Algorithm

The detectOpenssl function performs binary analysis on the OpenSSL shared library:

  1. ELF Parsing: Opens the shared library as an ELF file and validates architecture (x86_64 or aarch64)
  2. Section Scanning: Locates the .rodata section containing read-only data
  3. Version String Extraction: Reads the section in 1MB chunks, searching for the regex pattern (OpenSSL\s\d\.\d\.[0-9a-z]+)
  4. Edge Case Handling: Uses overlapping reads (subtracting 30 bytes per iteration) to handle version strings split across buffer boundaries

Sources: user/module/probe_openssl.go:178-278, user/module/probe_openssl_lib.go:189-282

Downgrade Strategy

Version Comparison Logic

The isVersionLessOrEqual function compares version strings by:

  • Stripping the "openssl " prefix
  • Splitting by dots (e.g., 3.0.12["3", "0", "12"])
  • Parsing each segment into numeric and alphabetic parts (e.g., 12a(12, "a"))
  • Comparing numerically first, then alphabetically for suffixes

Sources: user/module/probe_openssl_lib.go:341-422, user/module/probe_openssl_lib.go:284-317


eBPF Hook Architecture

Uprobe Hook Points

Uprobe Implementation Details

The two-stage probe pattern works as follows:

  1. Entry Probe (probe_entry_SSL):

    • Captures SSL context pointer (PT_REGS_PARM1) and buffer pointer (PT_REGS_PARM2)
    • Reads SSL version from ssl->version offset
    • Calls process_SSL_bio() to extract file descriptor and BIO type from ssl->bio->num
    • Stores context in active_ssl_*_args_map keyed by pid_tgid
  2. Return Probe (probe_ret_SSL):

    • Retrieves return value (bytes read/written) from PT_REGS_RC
    • Looks up stored context from entry probe
    • Calls process_SSL_data() to read actual buffer data using bpf_probe_read_user()
    • Sends ssl_data_event_t to userspace via perf event array

Sources: kern/openssl.h:268-323, kern/openssl.h:164-191, user/module/probe_openssl_text.go:46-151

Connection Tracking Integration

Connection Lifecycle Management

The module maintains bidirectional mappings between process/file descriptors and network connections:

  • pidConns: map[pid]map[fd]{tuple, sock} - Lookup connection info by process and file descriptor
  • sock2pidFd: map[sock][pid, fd] - Reverse lookup for cleanup on socket destruction

When SSLDataEvent is processed, GetConn(pid, fd) retrieves the tuple (source:port-dest:port) and socket pointer, enriching the captured data with network context.

Sources: user/module/probe_openssl.go:83-106, user/module/probe_openssl.go:406-488, kern/openssl.h:374-525


Structure Offset Calculation

Offset Generation Process

Offset Extraction Mechanism

The offset generation scripts automate the process of extracting structure member offsets from OpenSSL source code:

  1. Repository Setup: Clone or fetch OpenSSL repository at deps/openssl
  2. Version Iteration: Loop through supported versions (e.g., 3.0.0 to 3.0.17)
  3. Build Configuration: Run ./config and make build_generated to generate headers
  4. Offset Compilation: Compile offset.c which uses offsetof() macro to calculate structure member positions
  5. Header Generation: Execute the compiled binary to output C preprocessor defines

Example Offset Output (from 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));

Sources: 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 Structural Changes

Starting with OpenSSL 3.2.0, internal structures were refactored:

OpenSSL VersionStructure ChangesOffset Mapping
3.0.x, 3.1.xDirect ssl_st membersSSL_ST_VERSION, SSL_ST_WBIO, SSL_ST_RBIO
3.2.x onwardsIndirection via ssl_connection_stSSL_CONNECTION_ST_VERSION, SSL_CONNECTION_ST_WBIO, etc.
Mapped back via #define SSL_ST_VERSION SSL_CONNECTION_ST_VERSION

The mapping layer allows the eBPF code to use consistent symbolic names (SSL_ST_*) while the actual offset values differ based on the OpenSSL version's internal structure layout.

Sources: utils/openssl_offset_3.2.sh:58-67, utils/openssl_offset_3.3.sh:58-67


Master Secret Extraction

TLS 1.2 vs TLS 1.3 Architecture

Master Secret Structure

The mastersecret_t structure accommodates both TLS 1.2 and 1.3:

c
struct mastersecret_t {
    // Common fields
    s32 version;                                    // TLS version
    u8 client_random[SSL3_RANDOM_SIZE];            // 32 bytes
    
    // TLS 1.2 specific
    u8 master_key[MASTER_SECRET_MAX_LEN];          // 48 bytes
    
    // TLS 1.3 specific
    u32 cipher_id;
    u8 early_secret[EVP_MAX_MD_SIZE];              // 64 bytes
    u8 handshake_secret[EVP_MAX_MD_SIZE];          // 64 bytes
    u8 handshake_traffic_hash[EVP_MAX_MD_SIZE];    // 64 bytes
    u8 client_app_traffic_secret[EVP_MAX_MD_SIZE]; // 64 bytes
    u8 server_app_traffic_secret[EVP_MAX_MD_SIZE]; // 64 bytes
    u8 exporter_master_secret[EVP_MAX_MD_SIZE];    // 64 bytes
};

Sources: kern/openssl_masterkey.h:25-39, kern/openssl_masterkey.h:81-251, kern/openssl_masterkey_3.0.h:82-247

Keylog File Generation (TLS 1.3)

HKDF Key Derivation for TLS 1.3

TLS 1.3 uses HKDF (HMAC-based Key Derivation Function) to derive traffic secrets from handshake secrets. The eBPF program captures the raw handshake_secret and handshake_traffic_hash, and userspace performs:

clientHandshakeSecret = HKDF-Expand-Label(
    handshake_secret, 
    "c hs traffic", 
    handshake_traffic_hash, 
    hash_length
)

This matches the TLS 1.3 RFC 8446 key schedule, producing SSLKEYLOGFILE-compatible output that Wireshark can use for decryption.

Sources: user/module/probe_openssl.go:490-583, user/module/probe_openssl.go:509-559

Hook Function Selection

Default Master Key Hook Functions (defined at module level):

go
var masterKeyHookFuncs = []string{
    "SSL_do_handshake",
    "SSL_connect",
    "SSL_accept",
    "SSL_in_before",
}

For OpenSSL 1.0.x, SSL_in_before is replaced with SSL_state because the former is a macro rather than a function in older versions.

Sources: user/module/probe_openssl.go:178-196, user/module/probe_openssl_keylog.go:32-94


MOpenSSLProbe Implementation

Core Structure

Field Descriptions:

  • bpfManager: Manages eBPF program lifecycle (loading, attaching, detaching)
  • eventFuncMaps: Maps eBPF maps to their corresponding event decoders
  • pidConns: Connection tracking by PID and file descriptor
  • sock2pidFd: Reverse mapping from socket pointer to [PID, FD] for cleanup
  • keylogger: File handle for writing SSLKEYLOGFILE format output
  • masterKeys: Deduplication map (client_random hex → bool) to avoid duplicate key writes
  • eBPFProgramType: Determines which eBPF programs to load (text/pcap/keylog)
  • sslVersionBpfMap: Version string to bytecode filename mapping
  • isBoringSSL: Flag to select BoringSSL-specific code paths

Sources: user/module/probe_openssl.go:83-106, user/module/probe_openssl.go:109-176

Initialization Flow

Sources: user/module/probe_openssl.go:109-176, user/module/probe_openssl.go:280-350, user/module/probe_openssl_text.go:18-188


Capture Modes

Mode Comparison

ModeeBPF Maps UsedOutput FormatUse Case
Texttls_events, connect_eventsPlaintext with metadataReal-time monitoring, debugging
Pcaptls_events, connect_events, skb_eventsPcapng with DSB blocksWireshark analysis, network forensics
Keylogmastersecret_eventsSSLKEYLOGFILE formatPre-decryption key extraction

Event Type Mapping:

go
// Text Mode
m.eventFuncMaps[tls_events] = &event.SSLDataEvent{}
m.eventFuncMaps[connect_events] = &event.ConnDataEvent{}

// Keylog Mode
if m.isBoringSSL {
    m.eventFuncMaps[mastersecret_events] = &event.MasterSecretBSSLEvent{}
} else {
    m.eventFuncMaps[mastersecret_events] = &event.MasterSecretEvent{}
}

// Pcap Mode (includes all of Text Mode plus)
m.eventFuncMaps[skb_events] = &event.TcSkbEvent{}

Sources: user/module/probe_openssl_text.go:190-234, user/module/probe_openssl_keylog.go:97-118

Text Mode Architecture

Text mode captures SSL data events and enriches them with connection information:

Sources: user/module/probe_openssl.go:741-783, user/module/probe_openssl_text.go:18-188

Keylog Mode Architecture

Keylog mode focuses exclusively on master secret extraction:

  • Only attaches uprobes to master key hook functions (no data capture)
  • Writes secrets in NSS Key Log Format compatible with Wireshark
  • Deduplicates by client_random to avoid repeated keys
  • For TLS 1.3, performs HKDF key derivation in userspace

Example Keylog Output:

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...

Sources: user/module/probe_openssl_keylog.go:32-118, user/module/probe_openssl.go:490-650


Configuration and Filtering

OpensslConfig Structure

go
type OpensslConfig struct {
    BaseConfig
    Openssl      string // Path to libssl.so
    SslVersion   string // User-specified version
    Model        string // text/pcap/keylog
    KeylogFile   string // Output path for keylog mode
    PcapFile     string // Output path for pcap mode
    PcapFilter   string // BPF filter expression
    ElfType      uint8  // ElfTypeSo = 0
    IsAndroid    bool   // Android platform flag
    AndroidVer   string // Android version (e.g., "13", "14")
    CGroupPath   string // Cgroup path for filtering
}

Version Specification:

  • --ssl_version="openssl 3.0.12" - Exact version
  • --ssl_version="boringssl_a_14" - Android BoringSSL variant
  • Auto-detect if not specified

Sources: user/module/probe_openssl.go:108-176, cli/cmd/root.go:156-175

Constant Editors for Filtering

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 or 1
        },
    }
}

These constants are rewritten in the eBPF bytecode at load time, allowing efficient filtering without map lookups. The passes_filter() function in eBPF checks these values before processing events.

Sources: user/module/probe_openssl.go:361-395, kern/openssl.h:269-271


Error Handling and Edge Cases

Null Secret Detection

The module validates that extracted secrets are non-null before writing them:

go
func (m *MOpenSSLProbe) mk13NullSecrets(hashLen int, 
    ClientHandshakeSecret, ClientTrafficSecret0, 
    ServerHandshakeSecret, ServerTrafficSecret0, 
    ExporterSecret [64]byte) bool {
    
    isNullCount := 5
    // Check each secret; decrement counter if non-zero byte found
    for i := 0; i < hashLen; i++ {
        if ClientHandshakeSecret[i] != 0 { isNullCount-- }
        // ... check other secrets
    }
    return isNullCount != 0 // true if any secret is all zeros
}

This prevents writing incomplete or invalid key material to the keylog file.

Sources: user/module/probe_openssl.go:697-739, user/module/probe_openssl.go:652-672

File Descriptor Fallback

When ssl->bio->num is 0 (not set), the module checks the ssl_st_fd map populated by SSL_set_fd hooks:

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;
    }
}

This handles applications that explicitly call SSL_set_fd() instead of using BIO functions.

Sources: kern/openssl.h:225-267, kern/openssl.h:528-543

BIO Type Filtering

The module reads the BIO method type to distinguish between different I/O types:

c
#define BIO_TYPE_SOURCE_SINK 0x0400
#define BIO_TYPE_DESCRIPTOR  0x0100

Events with bio_type > BIO_TYPE_SOURCE_SINK | BIO_TYPE_DESCRIPTOR may indicate non-socket BIOs (e.g., memory BIOs) and are logged as warnings but not discarded, as the fd may still be valid from ssl_st_fd map.

Sources: kern/openssl.h:193-223, user/module/probe_openssl.go:764-779


Summary

The OpenSSL Module demonstrates sophisticated binary instrumentation through:

  1. Version Agnostic Design: 100+ OpenSSL versions supported through offset grouping (26 versions of 1.0.2.x share one bytecode file)
  2. Dual TLS Protocol Support: Handles both TLS 1.2 (single master_key) and TLS 1.3 (multiple traffic secrets with HKDF derivation)
  3. Multi-Modal Output: Text (real-time), pcap (Wireshark-compatible), and keylog (pre-decryption keys)
  4. Intelligent Fallback: Automatic version downgrade, libcrypto fallback, and default bytecode selection
  5. Connection Enrichment: Integrates kprobe-based TCP connection tracking to map SSL events to network tuples

The module's architecture separates concerns cleanly: version detection occurs at initialization, eBPF programs handle kernel-space data capture, and userspace processes events with protocol-specific parsers and key derivation logic.

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

OpenSSL Module has loaded