From af2d46a8462dacaa17a08f4fa8b95cc46c744844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=91=E5=BE=AE?= <1067852565@qq.com> Date: Mon, 21 Aug 2023 23:56:18 +0100 Subject: [PATCH] Add sslsniff documents (#67) * impl 25 signal * update more about openssl analysis * add skeleton for sslsniff * add code * fix summary link * add link for signal * add ci for 25 and 30 * fix docs --- .github/workflows/test-eunomia.yaml | 4 + .github/workflows/test-libbpf.yml | 4 + src/11-bootstrap/README.md | 4 +- src/25-signal/.gitignore | 2 + src/25-signal/README.md | 180 ++++++- src/25-signal/{signal.c => signal.bpf.c} | 2 +- src/30-openssl/README.md | 5 - src/30-openssl/README_en.md | 5 - src/30-openssl/common.h | 41 -- src/30-openssl/openssl.bpf.c | 248 ---------- src/30-openssl/openssl.bpf.h | 11 - src/30-openssl/openssl.c | 173 ------- src/{30-openssl => 30-sslsniff}/.gitignore | 0 src/{30-openssl => 30-sslsniff}/Makefile | 2 +- src/30-sslsniff/README.md | 538 +++++++++++++++++++++ src/30-sslsniff/README_en.md | 2 + src/30-sslsniff/sslsniff.bpf.c | 220 +++++++++ src/30-sslsniff/sslsniff.c | 452 +++++++++++++++++ src/30-sslsniff/sslsniff.h | 26 + src/SUMMARY.md | 1 + 20 files changed, 1426 insertions(+), 494 deletions(-) rename src/25-signal/{signal.c => signal.bpf.c} (98%) delete mode 100644 src/30-openssl/README.md delete mode 100644 src/30-openssl/README_en.md delete mode 100644 src/30-openssl/common.h delete mode 100644 src/30-openssl/openssl.bpf.c delete mode 100644 src/30-openssl/openssl.bpf.h delete mode 100644 src/30-openssl/openssl.c rename src/{30-openssl => 30-sslsniff}/.gitignore (100%) rename src/{30-openssl => 30-sslsniff}/Makefile (98%) create mode 100644 src/30-sslsniff/README.md create mode 100644 src/30-sslsniff/README_en.md create mode 100644 src/30-sslsniff/sslsniff.bpf.c create mode 100644 src/30-sslsniff/sslsniff.c create mode 100644 src/30-sslsniff/sslsniff.h diff --git a/.github/workflows/test-eunomia.yaml b/.github/workflows/test-eunomia.yaml index 7a38289..3290ae1 100644 --- a/.github/workflows/test-eunomia.yaml +++ b/.github/workflows/test-eunomia.yaml @@ -64,3 +64,7 @@ jobs: run: | ./ecc src/20-tc/tc.bpf.c sudo timeout -s 2 3 ./ecli run src/20-tc/package.json || if [ $? = 124 ]; then exit 0; else exit $?; fi + - name: test 25 signal + run: | + ./ecc src/25-signal/signal.bpf.c src/25-signal/signal.h + sudo timeout -s 2 3 ./ecli run src/25-signal/package.json || if [ $? = 124 ]; then exit 0; else exit $?; fi diff --git a/.github/workflows/test-libbpf.yml b/.github/workflows/test-libbpf.yml index 0fd5ffb..22c1db0 100644 --- a/.github/workflows/test-libbpf.yml +++ b/.github/workflows/test-libbpf.yml @@ -43,3 +43,7 @@ jobs: run: | make -C src/17-biopattern # sudo timeout -s 2 3 src/17-biopattern/biopattern || if [ $? = 124 ]; then exit 0; else exit $?; fi + - name: test 30 sslsniff + run: | + make -C src/30-sslsniff + # sudo timeout -s 2 3 src/30-sslsniff/sslsniff || if [ $? = 124 ]; then exit 0; else exit $?; fi diff --git a/src/11-bootstrap/README.md b/src/11-bootstrap/README.md index d76a373..48ea26f 100644 --- a/src/11-bootstrap/README.md +++ b/src/11-bootstrap/README.md @@ -625,6 +625,4 @@ TIME EVENT COMM PID PPID FILENAME/EXIT CODE 通过这个实例,我们了解了如何将 eBPF 程序与用户态程序结合使用。这种结合为开发者提供了一个强大的工具集,可以实现跨内核和用户空间的高效数据收集和处理。通过使用 eBPF 和 libbpf,您可以构建更高效、可扩展和安全的监控和性能分析工具。 -在接下来的教程中,我们将继续深入探讨 eBPF 的高级特性,分享更多关于 eBPF 开发实践的内容。通过不断学习和实践,您将更好地理解和掌握 eBPF 技术,并将其应用于解决实际问题。 - -如果您希望学习更多关于 eBPF 的知识和实践,请查阅 eunomia-bpf 的官方文档: 。您还可以访问我们的教程代码仓库 或网站 以获取更多示例和完整的教程。 +如果您希望学习更多关于 eBPF 的知识和实践,可以访问我们的教程代码仓库 或网站 以获取更多示例和完整的教程。 diff --git a/src/25-signal/.gitignore b/src/25-signal/.gitignore index e8a99c2..d3fb0ff 100644 --- a/src/25-signal/.gitignore +++ b/src/25-signal/.gitignore @@ -7,3 +7,5 @@ package.yaml ecli bootstrap bpfdos +ecc +ecli diff --git a/src/25-signal/README.md b/src/25-signal/README.md index 1fe9aaf..e0c9585 100644 --- a/src/25-signal/README.md +++ b/src/25-signal/README.md @@ -1,24 +1,192 @@ -# 用 bpf_send_signal 发送信号终止恶意进程 +# eBPF 入门实践教程:用 bpf_send_signal 发送信号终止恶意进程 + +eBPF (扩展的伯克利数据包过滤器) 是 Linux 内核的一种革命性技术,允许用户在内核空间执行自定义程序,而不需要修改内核源代码或加载任何内核模块。这使得开发人员可以非常灵活地对 Linux 系统进行观测、修改和控制。 + +本文介绍了如何使用 eBPF 的 bpf_send_signal 功能,向指定的进程发送信号进行干预。更多的教程文档,请参考 + +## 使用场景 + +**1. 性能分析:** +在现代软件生态系统中,优化应用程序的性能是开发人员和系统管理员的一个核心任务。当应用程序,如 hhvm,出现运行缓慢或资源利用率异常高时,它可能会对整个系统产生不利影响。因此,定位这些性能瓶颈并及时解决是至关重要的。 + +**2. 异常检测与响应:** +任何运行在生产环境中的系统都可能面临各种异常情况,从简单的资源泄露到复杂的恶意软件攻击。在这些情况下,系统需要能够迅速、准确地检测到这些异常,并采取适当的应对措施。 + +**3. 动态系统管理:** +随着云计算和微服务架构的普及,能够根据当前系统状态动态调整资源配置和应用行为已经成为了一个关键需求。例如,根据流量波动自动扩容或缩容,或者在检测到系统过热时降低 CPU 频率。 + +### 现有方案的不足 + +为了满足上述使用场景的需求,传统的技术方法如下: + +- 安装一个 bpf 程序,该程序会持续监视系统,同时对一个 map 进行轮询。 +- 当某个事件触发了 bpf 程序中定义的特定条件时,它会将相关数据写入此 map。 +- 接着,外部分析工具会从该 map 中读取数据,并根据读取到的信息向目标进程发送信号。 + +尽管这种方法在很多场景中都是可行的,但它存在一个主要的缺陷:从事件发生到外部工具响应的时间延迟可能相对较大。这种延迟可能会影响到事件的响应速度,从而使得性能分析的结果不准确或者在面对恶意活动时无法及时作出反应。 + +### 新方案的优势 + +为了克服传统方法的这些限制,Linux 内核提供了 `bpf_send_signal` 和 `bpf_send_signal_thread` 这两个 helper 函数。 + +这两个函数带来的主要优势包括: + +**1. 实时响应:** +通过直接从内核空间发送信号,避免了用户空间的额外开销,这确保了信号能够在事件发生后立即被发送,大大减少了延迟。 + +**2. 准确性:** +得益于减少的延迟,现在我们可以获得更准确的系统状态快照,这对于性能分析和异常检测尤其重要。 + +**3. 灵活性:** +这些新的 helper 函数为开发人员提供了更多的灵活性,他们可以根据不同的使用场景和需求来自定义信号的发送逻辑,从而更精确地控制和管理系统行为。 + +## 内核态代码分析 + +在现代操作系统中,一种常见的安全策略是监控和控制进程之间的交互。尤其在Linux系统中,`ptrace` 系统调用是一个强大的工具,它允许一个进程观察和控制另一个进程的执行,并修改其寄存器和内存。这使得它成为了调试和跟踪工具(如 `strace` 和 `gdb`)的主要机制。然而,恶意的 `ptrace` 使用也可能导致安全隐患。 + +这个程序的目标是在内核态监控 `ptrace` 的调用,当满足特定的条件时,它会发送一个 `SIGKILL` 信号终止调用进程。此外,为了调试或审计目的,该程序会记录这种干预并将相关信息发送到用户空间。 + +## 代码分析 + +### 1. 数据结构定义 (`signal.h`) + +signal.h + +```c +// Simple message structure to get events from eBPF Programs +// in the kernel to user spcae +#define TASK_COMM_LEN 16 +struct event { + int pid; + char comm[TASK_COMM_LEN]; + bool success; +}; +``` + +这部分定义了一个简单的消息结构,用于从内核的 eBPF 程序传递事件到用户空间。结构包括进程ID、命令名和一个标记是否成功发送信号的布尔值。 + +### 2. eBPF 程序 (`signal.bpf.c`) + +signal.bpf.c + +```c +// SPDX-License-Identifier: BSD-3-Clause +#include "vmlinux.h" +#include +#include +#include +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +// Ringbuffer Map to pass messages from kernel to user +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 256 * 1024); +} rb SEC(".maps"); + +// Optional Target Parent PID +const volatile int target_ppid = 0; + +SEC("tp/syscalls/sys_enter_ptrace") +int bpf_dos(struct trace_event_raw_sys_enter *ctx) +{ + long ret = 0; + size_t pid_tgid = bpf_get_current_pid_tgid(); + int pid = pid_tgid >> 32; + + // if target_ppid is 0 then we target all pids + if (target_ppid != 0) { + struct task_struct *task = (struct task_struct *)bpf_get_current_task(); + int ppid = BPF_CORE_READ(task, real_parent, tgid); + if (ppid != target_ppid) { + return 0; + } + } + + // Send signal. 9 == SIGKILL + ret = bpf_send_signal(9); + + // Log event + struct event *e; + e = bpf_ringbuf_reserve(&rb, sizeof(*e), 0); + if (e) { + e->success = (ret == 0); + e->pid = pid; + bpf_get_current_comm(&e->comm, sizeof(e->comm)); + bpf_ringbuf_submit(e, 0); + } + + return 0; +} +``` + +- **许可证声明** + + 声明了程序的许可证为 "Dual BSD/GPL",这是为了满足 Linux 内核对 eBPF 程序的许可要求。 + +- **Ringbuffer Map** + + 这是一个 ring buffer 类型的 map,允许 eBPF 程序在内核空间产生的消息被用户空间程序高效地读取。 + +- **目标父进程ID** + + `target_ppid` 是一个可选的父进程ID,用于限制哪些进程受到影响。如果它被设置为非零值,只有与其匹配的进程才会被目标。 + +- **主函数 `bpf_dos`** + + - **进程检查** + 程序首先获取当前进程的ID。如果设置了 `target_ppid`,它还会获取当前进程的父进程ID并进行比较。如果两者不匹配,则直接返回。 + + - **发送信号** + 使用 `bpf_send_signal(9)` 来发送 `SIGKILL` 信号。这将终止调用 `ptrace` 的进程。 + + - **记录事件** + 使用 ring buffer map 记录这个事件。这包括了是否成功发送信号、进程ID以及进程的命令名。 + +总结:这个 eBPF 程序提供了一个方法,允许系统管理员或安全团队在内核级别监控和干预 `ptrace` 调用,提供了一个对抗潜在恶意活动或误操作的额外层次。 + +## 编译运行 + +eunomia-bpf 是一个结合 Wasm 的开源 eBPF 动态加载运行时和开发工具链,它的目的是简化 eBPF 程序的开发、构建、分发、运行。可以参考 下载和安装 ecc 编译工具链和 ecli 运行时。我们使用 eunomia-bpf 编译运行这个例子。 编译: ```bash -make +./ecc signal.bpf.c signal.h ``` 使用方式: -```bash -sudo ./bpfdos +```console +sudo ./ecli package.json ``` 这个程序会对任何试图使用 `ptrace` 系统调用的程序,例如 `strace`,发出 `SIG_KILL` 信号。 -一旦 bpf-dos 开始运行,你可以通过运行以下命令进行测试: +一旦 eBPF 程序开始运行,你可以通过运行以下命令进行测试: ```bash -strace /bin/whoami +$ strace /bin/whoami +Killed ``` +原先的 console 中会输出: + +```txt +INFO [bpf_loader_lib::skeleton] Running ebpf program... +TIME PID COMM SUCCESS +13:54:45 8857 strace true +``` + +完整的源代码可以参考: + +## 总结 + +通过这个实例,我们深入了解了如何将 eBPF 程序与用户态程序相结合,实现对系统调用的监控和干预。eBPF 提供了一种在内核空间执行程序的机制,这种技术不仅限于监控,还可用于性能优化、安全防御、系统诊断等多种场景。对于开发者来说,这为Linux系统的性能调优和故障排查提供了一种强大且灵活的工具。 + +最后,如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 和教程网站 + ## 参考资料 - +- diff --git a/src/25-signal/signal.c b/src/25-signal/signal.bpf.c similarity index 98% rename from src/25-signal/signal.c rename to src/25-signal/signal.bpf.c index 4c83a41..42c1a8f 100644 --- a/src/25-signal/signal.c +++ b/src/25-signal/signal.bpf.c @@ -3,7 +3,7 @@ #include #include #include -#include "common.h" +#include "signal.h" char LICENSE[] SEC("license") = "Dual BSD/GPL"; diff --git a/src/30-openssl/README.md b/src/30-openssl/README.md deleted file mode 100644 index 61d5d1d..0000000 --- a/src/30-openssl/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# eBPF openssl - -TODO: make it work - -from https://github.com/kiosk404/openssl_tracer diff --git a/src/30-openssl/README_en.md b/src/30-openssl/README_en.md deleted file mode 100644 index 7f3f7ff..0000000 --- a/src/30-openssl/README_en.md +++ /dev/null @@ -1,5 +0,0 @@ -# eBPF openssl - -TODO: make it work - -from [https://github.com/kiosk404/openssl_tracer](https://github.com/kiosk404/openssl_tracer). \ No newline at end of file diff --git a/src/30-openssl/common.h b/src/30-openssl/common.h deleted file mode 100644 index 472a2c4..0000000 --- a/src/30-openssl/common.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef COMMON_H -#define COMMON_H - -#ifdef DEBUG_PRINT -#define debug_bpf_printk(fmt, ...) \ - do { \ - char s[] = fmt; \ - bpf_trace_printk(s, sizeof(s), ##__VA_ARGS__); \ - } while (0) -#else -#define debug_bpf_printk(fmt, ...) -#endif - -#define TASK_COMM_LEN 16 -#define MAX_DATA_SIZE_OPENSSL 1024 * 4 -#define MAX_DATA_SIZE_MYSQL 256 -#define MAX_DATA_SIZE_POSTGRES 256 -#define MAX_DATA_SIZE_BASH 256 - -// enum_server_command, via -// https://dev.mysql.com/doc/internals/en/com-query.html COM_QUERT command 03 -#define COM_QUERY 3 - -#define AF_INET 2 -#define AF_INET6 10 -#define SA_DATA_LEN 14 -#define BASH_ERRNO_DEFAULT 128 - -// Optional Target PID -// .rodata section bug via : https://github.com/ehids/ecapture/issues/39 -#ifndef KERNEL_LESS_5_2 -const volatile u64 target_pid = 0; -const volatile int target_errno = BASH_ERRNO_DEFAULT; -#else -// u64 target_pid = 0; -#endif - -char __license[] SEC("license") = "Dual MIT/GPL"; -__u32 _version SEC("version") = 0xFFFFFFFE; - -#endif \ No newline at end of file diff --git a/src/30-openssl/openssl.bpf.c b/src/30-openssl/openssl.bpf.c deleted file mode 100644 index 94cf442..0000000 --- a/src/30-openssl/openssl.bpf.c +++ /dev/null @@ -1,248 +0,0 @@ -#include "openssl_trace.bpf.h" - -enum ssl_data_event_type { kSSLRead, kSSLWrite }; -const u32 invalidFD = 0; - -struct ssl_data_event_t { - enum ssl_data_event_type type; - u64 timestamp_ns; - u32 pid; - u32 tid; - char data[MAX_DATA_SIZE_OPENSSL]; - s32 data_len; - char comm[TASK_COMM_LEN]; - u32 fd; -}; - -struct { - __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); -} tls_events SEC(".maps"); - -struct active_ssl_buf { - u32 fd; - const char* buf; -}; - -// BPF programs are limited to a 512-byte stack. We store this value per CPU -// and use it as a heap allocated value. -struct { - __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); - __type(key, u32); - __type(value, struct ssl_data_event_t); - __uint(max_entries, 1); -} data_buffer_heap SEC(".maps"); - -struct { - __uint(type, BPF_MAP_TYPE_HASH); - __type(key, u64); - __type(value, struct active_ssl_buf); - __uint(max_entries, 1024); -} active_ssl_read_args_map SEC(".maps"); - -struct { - __uint(type, BPF_MAP_TYPE_HASH); - __type(key, u64); - __type(value, struct active_ssl_buf); - __uint(max_entries, 1024); -} active_ssl_write_args_map SEC(".maps"); - -// OPENSSL struct to offset , via kern/README.md -typedef long (*unused_fn)(); - -struct unused {}; - -struct BIO { - const struct unused* method; - unused_fn callback; - unused_fn callback_ex; - char* cb_arg; /* first argument for the callback */ - int init; - int shutdown; - int flags; /* extra storage */ - int retry_reason; - int num; -}; - -struct ssl_st { - int version; - struct unused* method; - struct BIO* rbio; // used by SSL_read - struct BIO* wbio; // used by SSL_write -}; - -/*********************************************************** - * General helper functions - ***********************************************************/ -static __inline struct ssl_data_event_t* create_ssl_data_event( - u64 current_pid_tgid) { - u32 kZero = 0; - struct ssl_data_event_t* event = - bpf_map_lookup_elem(&data_buffer_heap, &kZero); - if (event == NULL) { - return NULL; - } - - const u32 kMask32b = 0xffffffff; - event->timestamp_ns = bpf_ktime_get_ns(); - event->pid = current_pid_tgid >> 32; - event->tid = current_pid_tgid & kMask32b; - event->fd = invalidFD; - - return event; -} - -/*********************************************************** - * BPF syscall processing functions - ***********************************************************/ -static int process_SSL_data(struct pt_regs* ctx, u64 id, - enum ssl_data_event_type type, const char* buf, - u32 fd) { - int len = (int)PT_REGS_RC(ctx); - if (len < 0) { - return 0; - } - - struct ssl_data_event_t* event = create_ssl_data_event(id); - if (event == NULL) { - return 0; - } - - event->type = type; - event->fd = fd; - // This is a max function, but it is written in such a way to keep older BPF - // verifiers happy. - event->data_len = - (len < MAX_DATA_SIZE_OPENSSL ? (len & (MAX_DATA_SIZE_OPENSSL - 1)) - : MAX_DATA_SIZE_OPENSSL); - bpf_probe_read(event->data, event->data_len, buf); - bpf_get_current_comm(&event->comm, sizeof(event->comm)); - bpf_perf_event_output(ctx, &tls_events, BPF_F_CURRENT_CPU, event, - sizeof(struct ssl_data_event_t)); - return 0; -} - -/*********************************************************** - * BPF probe function entry-points - ***********************************************************/ - -// Function signature being probed: -// int SSL_write(SSL *ssl, const void *buf, int num); -SEC("uprobe/SSL_write") -int probe_entry_SSL_write(struct pt_regs* ctx) { - u64 current_pid_tgid = bpf_get_current_pid_tgid(); - u32 pid = current_pid_tgid >> 32; - -#ifndef KERNEL_LESS_5_2 - // if target_ppid is 0 then we target all pids - if (target_pid != 0 && target_pid != pid) { - return 0; - } -#endif - // debug_bpf_printk("openssl uprobe/SSL_write pid :%d\n", pid); - - void* ssl = (void*)PT_REGS_PARM1(ctx); - // https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/crypto/bio/bio_local.h - struct ssl_st ssl_info; - bpf_probe_read_user(&ssl_info, sizeof(ssl_info), ssl); - - struct BIO bio_w; - bpf_probe_read_user(&bio_w, sizeof(bio_w), ssl_info.wbio); - - // get fd ssl->wbio->num - u32 fd = bio_w.num; - // debug_bpf_printk("openssl uprobe SSL_write FD:%d\n", fd); - - const char* buf = (const char*)PT_REGS_PARM2(ctx); - struct active_ssl_buf active_ssl_buf_t; - __builtin_memset(&active_ssl_buf_t, 0, sizeof(active_ssl_buf_t)); - active_ssl_buf_t.fd = fd; - active_ssl_buf_t.buf = buf; - bpf_map_update_elem(&active_ssl_write_args_map, ¤t_pid_tgid, - &active_ssl_buf_t, BPF_ANY); - - return 0; -} - -SEC("uretprobe/SSL_write") -int probe_ret_SSL_write(struct pt_regs* ctx) { - u64 current_pid_tgid = bpf_get_current_pid_tgid(); - u32 pid = current_pid_tgid >> 32; - -#ifndef KERNEL_LESS_5_2 - // if target_ppid is 0 then we target all pids - if (target_pid != 0 && target_pid != pid) { - return 0; - } -#endif - // debug_bpf_printk("openssl uretprobe/SSL_write pid :%d\n", pid); - struct active_ssl_buf* active_ssl_buf_t = - bpf_map_lookup_elem(&active_ssl_write_args_map, ¤t_pid_tgid); - if (active_ssl_buf_t != NULL) { - const char* buf; - u32 fd = active_ssl_buf_t->fd; - bpf_probe_read(&buf, sizeof(const char*), &active_ssl_buf_t->buf); - process_SSL_data(ctx, current_pid_tgid, kSSLWrite, buf, fd); - } - bpf_map_delete_elem(&active_ssl_write_args_map, ¤t_pid_tgid); - return 0; -} - -// Function signature being probed: -// int SSL_read(SSL *s, void *buf, int num) -SEC("uprobe/SSL_read") -int probe_entry_SSL_read(struct pt_regs* ctx) { - u64 current_pid_tgid = bpf_get_current_pid_tgid(); - u32 pid = current_pid_tgid >> 32; - -#ifndef KERNEL_LESS_5_2 - // if target_ppid is 0 then we target all pids - if (target_pid != 0 && target_pid != pid) { - return 0; - } -#endif - - void* ssl = (void*)PT_REGS_PARM1(ctx); - // https://github.com/openssl/openssl/blob/OpenSSL_1_1_1-stable/crypto/bio/bio_local.h - struct ssl_st ssl_info; - bpf_probe_read_user(&ssl_info, sizeof(ssl_info), ssl); - - struct BIO bio_r; - bpf_probe_read_user(&bio_r, sizeof(bio_r), ssl_info.rbio); - - // get fd ssl->rbio->num - u32 fd = bio_r.num; - debug_bpf_printk("openssl uprobe PID:%d, SSL_read FD:%d\n", pid, fd); - - const char* buf = (const char*)PT_REGS_PARM2(ctx); - struct active_ssl_buf active_ssl_buf_t; - __builtin_memset(&active_ssl_buf_t, 0, sizeof(active_ssl_buf_t)); - active_ssl_buf_t.fd = fd; - active_ssl_buf_t.buf = buf; - bpf_map_update_elem(&active_ssl_read_args_map, ¤t_pid_tgid, - &active_ssl_buf_t, BPF_ANY); - return 0; -} - -SEC("uretprobe/SSL_read") -int probe_ret_SSL_read(struct pt_regs* ctx) { - u64 current_pid_tgid = bpf_get_current_pid_tgid(); - u32 pid = current_pid_tgid >> 32; - -#ifndef KERNEL_LESS_5_2 - // if target_ppid is 0 then we target all pids - if (target_pid != 0 && target_pid != pid) { - return 0; - } -#endif - - struct active_ssl_buf* active_ssl_buf_t = - bpf_map_lookup_elem(&active_ssl_read_args_map, ¤t_pid_tgid); - if (active_ssl_buf_t != NULL) { - const char* buf; - u32 fd = active_ssl_buf_t->fd; - bpf_probe_read(&buf, sizeof(const char*), &active_ssl_buf_t->buf); - process_SSL_data(ctx, current_pid_tgid, kSSLRead, buf, fd); - } - bpf_map_delete_elem(&active_ssl_read_args_map, ¤t_pid_tgid); - return 0; -} \ No newline at end of file diff --git a/src/30-openssl/openssl.bpf.h b/src/30-openssl/openssl.bpf.h deleted file mode 100644 index c9e3a0a..0000000 --- a/src/30-openssl/openssl.bpf.h +++ /dev/null @@ -1,11 +0,0 @@ -#ifndef OPENSS_TRACE_BPF_H -#define OPENSS_TRACE_BPF_H - -//CO:RE is enabled -#include "headers/vmlinux.h" -#include -#include -#include -#include "common.h" - -#endif \ No newline at end of file diff --git a/src/30-openssl/openssl.c b/src/30-openssl/openssl.c deleted file mode 100644 index c93e4ed..0000000 --- a/src/30-openssl/openssl.c +++ /dev/null @@ -1,173 +0,0 @@ -// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) -/* Copyright (c) 2020 Facebook */ -#include -#include -#include -#include -#include -#include -#include "openssl.h" -#include "openssl.skel.h" - -static struct env { - bool verbose; - long min_duration_ms; -} env; - -const char *argp_program_version = "openssl 0.0"; -const char *argp_program_bug_address = ""; -const char argp_program_doc[] = -"BPF openssl demo application.\n" -"\n" -"It traces process start and exits and shows associated \n" -"information (filename, process duration, PID and PPID, etc).\n" -"\n" -"USAGE: ./openssl [-d ] [-v]\n"; - -static const struct argp_option opts[] = { - { "verbose", 'v', NULL, 0, "Verbose debug output" }, - { "duration", 'd', "DURATION-MS", 0, "Minimum process duration (ms) to report" }, - {}, -}; - -static error_t parse_arg(int key, char *arg, struct argp_state *state) -{ - switch (key) { - case 'v': - env.verbose = true; - break; - case 'd': - errno = 0; - env.min_duration_ms = strtol(arg, NULL, 10); - if (errno || env.min_duration_ms <= 0) { - fprintf(stderr, "Invalid duration: %s\n", arg); - argp_usage(state); - } - break; - case ARGP_KEY_ARG: - argp_usage(state); - break; - default: - return ARGP_ERR_UNKNOWN; - } - return 0; -} - -static const struct argp argp = { - .options = opts, - .parser = parse_arg, - .doc = argp_program_doc, -}; - -static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args) -{ - if (level == LIBBPF_DEBUG && !env.verbose) - return 0; - return vfprintf(stderr, format, args); -} - -static volatile bool exiting = false; - -static void sig_handler(int sig) -{ - exiting = true; -} - -static int handle_event(void *ctx, void *data, size_t data_sz) -{ - const struct event *e = data; - struct tm *tm; - char ts[32]; - time_t t; - - time(&t); - tm = localtime(&t); - strftime(ts, sizeof(ts), "%H:%M:%S", tm); - - if (e->exit_event) { - printf("%-8s %-5s %-16s %-7d %-7d [%u]", - ts, "EXIT", e->comm, e->pid, e->ppid, e->exit_code); - if (e->duration_ns) - printf(" (%llums)", e->duration_ns / 1000000); - printf("\n"); - } else { - printf("%-8s %-5s %-16s %-7d %-7d %s\n", - ts, "EXEC", e->comm, e->pid, e->ppid, e->filename); - } - - return 0; -} - -int main(int argc, char **argv) -{ - struct ring_buffer *rb = NULL; - struct openssl_bpf *skel; - int err; - - /* Parse command line arguments */ - err = argp_parse(&argp, argc, argv, 0, NULL, NULL); - if (err) - return err; - - /* Set up libbpf errors and debug info callback */ - libbpf_set_print(libbpf_print_fn); - - /* Cleaner handling of Ctrl-C */ - signal(SIGINT, sig_handler); - signal(SIGTERM, sig_handler); - - /* Load and verify BPF application */ - skel = openssl_bpf__open(); - if (!skel) { - fprintf(stderr, "Failed to open and load BPF skeleton\n"); - return 1; - } - - /* Parameterize BPF code with minimum duration parameter */ - skel->rodata->min_duration_ns = env.min_duration_ms * 1000000ULL; - - /* Load & verify BPF programs */ - err = openssl_bpf__load(skel); - if (err) { - fprintf(stderr, "Failed to load and verify BPF skeleton\n"); - goto cleanup; - } - - /* Attach tracepoints */ - err = openssl_bpf__attach(skel); - if (err) { - fprintf(stderr, "Failed to attach BPF skeleton\n"); - goto cleanup; - } - - /* Set up ring buffer polling */ - rb = ring_buffer__new(bpf_map__fd(skel->maps.rb), handle_event, NULL, NULL); - if (!rb) { - err = -1; - fprintf(stderr, "Failed to create ring buffer\n"); - goto cleanup; - } - - /* Process events */ - printf("%-8s %-5s %-16s %-7s %-7s %s\n", - "TIME", "EVENT", "COMM", "PID", "PPID", "FILENAME/EXIT CODE"); - while (!exiting) { - err = ring_buffer__poll(rb, 100 /* timeout, ms */); - /* Ctrl-C will cause -EINTR */ - if (err == -EINTR) { - err = 0; - break; - } - if (err < 0) { - printf("Error polling perf buffer: %d\n", err); - break; - } - } - -cleanup: - /* Clean up */ - ring_buffer__free(rb); - openssl_bpf__destroy(skel); - - return err < 0 ? -err : 0; -} diff --git a/src/30-openssl/.gitignore b/src/30-sslsniff/.gitignore similarity index 100% rename from src/30-openssl/.gitignore rename to src/30-sslsniff/.gitignore diff --git a/src/30-openssl/Makefile b/src/30-sslsniff/Makefile similarity index 98% rename from src/30-openssl/Makefile rename to src/30-sslsniff/Makefile index 7437c1a..f4d5ce3 100644 --- a/src/30-openssl/Makefile +++ b/src/30-sslsniff/Makefile @@ -24,7 +24,7 @@ INCLUDES := -I$(OUTPUT) -I../third_party/libbpf/include/uapi -I$(dir $(VMLINUX)) CFLAGS := -g -Wall ALL_LDFLAGS := $(LDFLAGS) $(EXTRA_LDFLAGS) -APPS = bootstrap # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall +APPS = sslsniff # minimal minimal_legacy uprobe kprobe fentry usdt sockfilter tc ksyscall CARGO ?= $(shell which cargo) ifeq ($(strip $(CARGO)),) diff --git a/src/30-sslsniff/README.md b/src/30-sslsniff/README.md new file mode 100644 index 0000000..9ad2498 --- /dev/null +++ b/src/30-sslsniff/README.md @@ -0,0 +1,538 @@ +# eBPF 实践教程:使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据 + +随着TLS在现代网络环境中的广泛应用,跟踪微服务RPC消息已经变得愈加棘手。传统的流量嗅探技术常常受限于只能获取到加密后的数据,导致无法真正观察到通信的原始内容。这种限制为系统的调试和分析带来了不小的障碍。 + +但现在,我们有了新的解决方案。 + +eBPF技术,通过其能力在用户空间进行探测,提供了一种方法重新获得明文数据,使得我们可以直观地查看加密前的通信内容。然而,每个应用可能使用不同的库,每个库都有多个版本,这种多样性给跟踪带来了复杂性。 + +在本教程中,我们将带您了解一种跨多种条件的技术,它不仅可以同时跟踪 GnuTLS 和 OpenSSL 等用户态库,而且相比以往,大大降低了对新版本库的维护工作。 + +## 背景知识 + +在深入本教程的主题之前,我们需要理解一些核心概念,这些概念将为我们后面的讨论提供基础。 + +### SSL 和 TLS + +SSL (Secure Sockets Layer): 由 Netscape 在 1990 年代早期开发,为网络上的两台机器之间提供数据加密传输。然而,由于某些已知的安全问题,SSL的使用已被其后继者TLS所替代。 + +TLS (Transport Layer Security): 是 SSL 的继任者,旨在提供更强大和更安全的数据加密方式。TLS 工作通过一个握手过程,在这个过程中,客户端和服务器之间会选择一个加密算法和相应的密钥。一旦握手完成,数据传输开始,所有数据都使用选择的算法和密钥加密。 + +### TLS 的工作原理 + +Transport Layer Security (TLS) 是一个密码学协议,旨在为计算机网络上的通信提供安全性。它主要目标是通过密码学,例如证书的使用,为两个或更多通信的计算机应用程序提供安全性,包括隐私(机密性)、完整性和真实性。TLS 由两个子层组成:TLS 记录协议和TLS 握手协议。 + +#### 握手过程 + +当客户端与启用了TLS的服务器连接并请求建立安全连接时,握手过程开始。握手允许客户端和服务器通过不对称密码来建立连接的安全性参数: + +1. **初始握手**:客户端连接到启用了TLS的服务器,请求安全连接,并提供它支持的密码套件列表(加密算法和哈希函数)。 +2. **选择密码套件**:从提供的列表中,服务器选择它也支持的密码套件和哈希函数,并通知客户端已做出的决定。 +3. **提供数字证书**:通常,服务器接下来会提供形式为数字证书的身份验证。此证书包含服务器名称、信任的证书授权机构(为证书的真实性提供担保)以及服务器的公共加密密钥。 +4. **验证证书**:客户端在继续之前确认证书的有效性。 +5. **生成会话密钥**:为了生成用于安全连接的会话密钥,客户端有以下两种方法: + - 使用服务器的公钥加密一个随机数(PreMasterSecret)并将结果发送到服务器(只有服务器才能使用其私钥解密);双方然后使用该随机数生成一个独特的会话密钥,用于会话期间的数据加密和解密。 + - 使用Diffie-Hellman 密钥交换(或其变体椭圆曲线DH)来安全地生成一个随机且独特的会话密钥,用于加密和解密,该密钥具有前向保密的额外属性:即使在未来公开了服务器的私钥,也不能用它来解密当前的会话,即使第三方拦截并记录了会话。 + +一旦上述步骤成功完成,握手过程便结束,加密的连接开始。此连接使用会话密钥进行加密和解密,直到连接关闭。如果上述任何步骤失败,则TLS握手失败,连接将不会建立。 + +#### OSI模型中的TLS + +TLS和SSL不完全适合OSI模型或TCP/IP模型的任何单一层次。TLS在“某些可靠的传输协议(例如,TCP)之上运行”,这意味着它位于传输层之上。它为更高的层提供加密,这通常是表示层的功能。但是,使用TLS的应用程序通常视其为传输层,即使使用TLS的应用程序必须积极控制启动TLS握手和交换的认证证书的处理。 + +### eBPF 和 uprobe + +eBPF (Extended Berkeley Packet Filter): 是一种内核技术,允许用户在内核空间中运行预定义的程序,不需要修改内核源代码或重新加载模块。它创建了一个桥梁,使得用户空间和内核空间可以交互,从而为系统监控、性能分析和网络流量分析等任务提供了无前例的能力。 + +uprobes: 是eBPF的一个重要特性,允许我们在用户空间应用程序中动态地插入探测点,特别适用于跟踪SSL/TLS库中的函数调用。 + +### 用户态库 + +SSL/TLS协议的实现主要依赖于用户态库。以下是一些常见的库: + +- OpenSSL: 一个开源的、功能齐全的加密库,广泛应用于许多开源和商业项目中。 +- BoringSSL: 是Google维护的OpenSSL的一个分支,重点是简化和优化,适用于Google的需求。 +- GnuTLS: 是GNU项目的一部分,提供了SSL,TLS和DTLS协议的实现。与OpenSSL和BoringSSL相比,GnuTLS在API设计、模块结构和许可证上有所不同。 + +## OpenSSL API 分析 + +OpenSSL 是一个广泛应用的开源库,提供了 SSL 和 TLS 协议的完整实现,并广泛用于各种应用程序中以确保数据传输的安全性。其中,SSL_read() 和 SSL_write() 是两个核心的 API 函数,用于从 TLS/SSL 连接中读取和写入数据。本章节,我们将深入这两个函数,帮助你理解其工作机制。 + +### 1. SSL_read 函数 + +当我们想从一个已建立的 SSL 连接中读取数据时,可以使用 `SSL_read` 或 `SSL_read_ex` 函数。 + +#### 函数原型 + +```c +int SSL_read_ex(SSL *ssl, void *buf, size_t num, size_t *readbytes); +int SSL_read(SSL *ssl, void *buf, int num); +``` + +`SSL_read` 和 `SSL_read_ex` 试图从指定的 `ssl` 中读取最多 `num` 字节的数据到缓冲区 `buf` 中。成功时,`SSL_read_ex` 会在 `*readbytes` 中存储实际读取到的字节数。 + +### 2. SSL_write 函数 + +当我们想往一个已建立的 SSL 连接中写入数据时,可以使用 `SSL_write` 或 `SSL_write_ex` 函数。 + +函数原型: + +```c +int SSL_write_ex(SSL *s, const void *buf, size_t num, size_t *written); +int SSL_write(SSL *ssl, const void *buf, int num); +``` + +`SSL_write` 和 `SSL_write_ex` 会从缓冲区 `buf` 中将最多 `num` 字节的数据写入到指定的 `ssl` 连接中。成功时,`SSL_write_ex` 会在 `*written` 中存储实际写入的字节数。 + +## eBPF 内核态代码编写 + +eBPF (扩展伯克利数据包过滤器) 是 Linux 内核中的一个功能强大的编程框架,它允许开发者在不修改内核源代码的情况下,为 Linux 内核动态插入自定义的程序。在我们的例子中,我们使用 eBPF 来 hook ssl_read 和 ssl_write 函数,从而在数据读取或写入 SSL 连接时执行自定义操作。 + +### 数据结构 + +首先,我们定义了一个数据结构 probe_SSL_data_t 用于在内核态和用户态之间传输数据: + +```c +#define MAX_BUF_SIZE 8192 +#define TASK_COMM_LEN 16 + +struct probe_SSL_data_t { + __u64 timestamp_ns; // 时间戳(纳秒) + __u64 delta_ns; // 函数执行时间 + __u32 pid; // 进程 ID + __u32 tid; // 线程 ID + __u32 uid; // 用户 ID + __u32 len; // 读/写数据的长度 + int buf_filled; // 缓冲区是否填充完整 + int rw; // 读或写(0为读,1为写) + char comm[TASK_COMM_LEN]; // 进程名 + __u8 buf[MAX_BUF_SIZE]; // 数据缓冲区 + int is_handshake; // 是否是握手数据 +}; +``` + +### Hook 函数 + +我们的目标是 hook 到 `SSL_read` 和 `SSL_write` 函数。我们定义了一个函数 `SSL_exit` 来处理这两个函数的返回值。该函数会根据当前进程和线程的 ID,确定是否需要追踪并收集数据。 + +```c +static int SSL_exit(struct pt_regs *ctx, int rw) { + int ret = 0; + u32 zero = 0; + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u32 uid = bpf_get_current_uid_gid(); + u64 ts = bpf_ktime_get_ns(); + + if (!trace_allowed(uid, pid)) { + return 0; + } + + /* store arg info for later lookup */ + u64 *bufp = bpf_map_lookup_elem(&bufs, &tid); + if (bufp == 0) + return 0; + + u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); + if (!tsp) + return 0; + u64 delta_ns = ts - *tsp; + + int len = PT_REGS_RC(ctx); + if (len <= 0) // no data + return 0; + + struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); + if (!data) + return 0; + + data->timestamp_ns = ts; + data->delta_ns = delta_ns; + data->pid = pid; + data->tid = tid; + data->uid = uid; + data->len = (u32)len; + data->buf_filled = 0; + data->rw = rw; + data->is_handshake = false; + u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len); + + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + + if (bufp != 0) + ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp); + + bpf_map_delete_elem(&bufs, &tid); + bpf_map_delete_elem(&start_ns, &tid); + + if (!ret) + data->buf_filled = 1; + else + buf_copy_size = 0; + + bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, + EVENT_SIZE(buf_copy_size)); + return 0; +} +``` + +这里的 `rw` 参数标识是读还是写。0 代表读,1 代表写。 + +#### 数据收集流程 + +1. 获取当前进程和线程的 ID,以及当前用户的 ID。 +2. 通过 `trace_allowed` 判断是否允许追踪该进程。 +3. 获取起始时间,以计算函数的执行时间。 +4. 尝试从 `bufs` 和 `start_ns` maps 中查找相关的数据。 +5. 如果成功读取了数据,则创建或查找 `probe_SSL_data_t` 结构来填充数据。 +6. 将数据从用户空间复制到缓冲区,并确保不超过预定的大小。 +7. 最后,将数据发送到用户空间。 + +注意:我们使用了两个用户返回探针 `uretprobe` 来分别 hook `SSL_read` 和 `SSL_write` 的返回: + +```c +SEC("uretprobe/SSL_read") +int BPF_URETPROBE(probe_SSL_read_exit) { + return (SSL_exit(ctx, 0)); // 0 表示读操作 +} + +SEC("uretprobe/SSL_write") +int BPF_URETPROBE(probe_SSL_write_exit) { + return (SSL_exit(ctx, 1)); // 1 表示写操作 +} +``` + +### Hook到握手过程 + +在 SSL/TLS 中,握手(handshake)是一个特殊的过程,用于在客户端和服务器之间建立安全的连接。为了分析此过程,我们 hook 到了 `do_handshake` 函数,以跟踪握手的开始和结束。 + +#### 进入握手 + +我们使用 `uprobe` 为 `do_handshake` 设置一个 probe: + +```c + +SEC("uprobe/do_handshake") +int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u64 ts = bpf_ktime_get_ns(); + u32 uid = bpf_get_current_uid_gid(); + + if (!trace_allowed(uid, pid)) { + return 0; + } + + /* store arg info for later lookup */ + bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); + return 0; +} +``` + +这段代码的主要功能如下: + +1. 获取当前的 `pid`, `tid`, `ts` 和 `uid`。 +2. 使用 `trace_allowed` 检查进程是否被允许追踪。 +3. 将当前时间戳存储在 `start_ns` 映射中,用于稍后计算握手过程的持续时间。 + +#### 退出握手 + +同样,我们为 `do_handshake` 的返回设置了一个 `uretprobe`: + +```c + +SEC("uretprobe/do_handshake") +int BPF_URETPROBE(probe_SSL_do_handshake_exit) { + u32 zero = 0; + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u32 uid = bpf_get_current_uid_gid(); + u64 ts = bpf_ktime_get_ns(); + int ret = 0; + + /* use kernel terminology here for tgid/pid: */ + u32 tgid = pid_tgid >> 32; + + /* store arg info for later lookup */ + if (!trace_allowed(tgid, pid)) { + return 0; + } + + u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); + if (tsp == 0) + return 0; + + ret = PT_REGS_RC(ctx); + if (ret <= 0) // handshake failed + return 0; + + struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); + if (!data) + return 0; + + data->timestamp_ns = ts; + data->delta_ns = ts - *tsp; + data->pid = pid; + data->tid = tid; + data->uid = uid; + data->len = ret; + data->buf_filled = 0; + data->rw = 2; + data->is_handshake = true; + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + bpf_map_delete_elem(&start_ns, &tid); + + bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, + EVENT_SIZE(0)); + return 0; +} +``` + +此函数的逻辑如下: + +1. 获取当前的 `pid`, `tid`, `ts` 和 `uid`。 +2. 使用 `trace_allowed` 再次检查是否允许追踪。 +3. 查找 `start_ns` 映射中的时间戳,用于计算握手的持续时间。 +4. 使用 `PT_REGS_RC(ctx)` 获取 `do_handshake` 的返回值,判断握手是否成功。 +5. 查找或初始化与当前线程关联的 `probe_SSL_data_t` 数据结构。 +6. 更新数据结构的字段,包括时间戳、持续时间、进程信息等。 +7. 通过 `bpf_perf_event_output` 将数据发送到用户态。 + +我们的 eBPF 代码不仅跟踪了 `ssl_read` 和 `ssl_write` 的数据传输,还特别关注了 SSL/TLS 的握手过程。这些信息对于深入了解和优化安全连接的性能至关重要。 + +通过这些 hook 函数,我们可以获得关于握手成功与否、握手所需的时间以及相关的进程信息的数据。这为我们提供了关于系统 SSL/TLS 行为的深入见解,可以帮助我们在需要时进行更深入的分析和优化。 + +## 用户态辅助代码分析 + +## 用户态辅助代码分析与解读 + +在 eBPF 的生态系统中,用户态和内核态代码经常协同工作。内核态代码负责数据的采集,而用户态代码则负责设置、管理和处理这些数据。在本节中,我们将解读上述用户态代码如何配合 eBPF 追踪 SSL/TLS 交互。 + +### 1. 支持的库挂载 + +上述代码片段中,根据环境变量 `env` 的设定,程序可以选择针对三种常见的加密库(OpenSSL、GnuTLS 和 NSS)进行挂载。这意味着我们可以在同一个工具中对多种库的调用进行追踪。 + +为了实现这一功能,首先利用 `find_library_path` 函数确定库的路径。然后,根据库的类型,调用对应的 `attach_` 函数来将 eBPF 程序挂载到库函数上。 + +```c + if (env.openssl) { + char *openssl_path = find_library_path("libssl.so"); + printf("OpenSSL path: %s\n", openssl_path); + attach_openssl(obj, "/lib/x86_64-linux-gnu/libssl.so.3"); + } + if (env.gnutls) { + char *gnutls_path = find_library_path("libgnutls.so"); + printf("GnuTLS path: %s\n", gnutls_path); + attach_gnutls(obj, gnutls_path); + } + if (env.nss) { + char *nss_path = find_library_path("libnspr4.so"); + printf("NSS path: %s\n", nss_path); + attach_nss(obj, nss_path); + } +``` + +这里主要包含 OpenSSL、GnuTLS 和 NSS 三个库的挂载逻辑。NSS 是为组织设计的一套安全库,支持创建安全的客户端和服务器应用程序。它们最初是由 Netscape 开发的,现在由 Mozilla 维护。其他两个库前面已经介绍过了,这里不再赘述。 + +### 2. 详细挂载逻辑 + +具体的 attach 函数如下: + +```c +#define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \ + do { \ + LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \ + .retprobe = is_retprobe); \ + skel->links.prog_name = bpf_program__attach_uprobe_opts( \ + skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \ + } while (false) + +int attach_openssl(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit); + + if (env.latency && env.handshake) { + ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake, + probe_SSL_do_handshake_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake, + probe_SSL_do_handshake_exit); + } + + return 0; +} + +int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit); + + return 0; +} + +int attach_nss(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit); + + return 0; +} +``` + +我们进一步观察 `attach_` 函数,可以看到它们都使用了 `ATTACH_UPROBE_CHECKED` 和 `ATTACH_URETPROBE_CHECKED` 宏来实现具体的挂载逻辑。这两个宏分别用于设置 uprobe(函数入口)和 uretprobe(函数返回)。 + +考虑到不同的库有不同的 API 函数名称(例如,OpenSSL 使用 `SSL_write`,而 GnuTLS 使用 `gnutls_record_send`),所以我们需要为每个库写一个独立的 `attach_` 函数。 + +例如,在 `attach_openssl` 函数中,我们为 `SSL_write` 和 `SSL_read` 设置了 probe。如果用户还希望追踪握手的延迟 (`env.latency`) 和握手过程 (`env.handshake`),那么我们还会为 `SSL_do_handshake` 设置 probe。 + +在eBPF生态系统中,perf_buffer是一个用于从内核态传输数据到用户态的高效机制。这对于内核态eBPF程序来说是十分有用的,因为它们不能直接与用户态进行交互。使用perf_buffer,我们可以在内核态eBPF程序中收集数据,然后在用户态异步地读取这些数据。我们使用 `perf_buffer__poll` 函数来读取内核态上报的数据,如下所示: + +```c + while (!exiting) { + err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); + if (err < 0 && err != -EINTR) { + warn("error polling perf buffer: %s\n", strerror(-err)); + goto cleanup; + } + err = 0; + } +``` + +最后,在 print_event 函数中,我们将数据打印到标准输出: + +```c +// Function to print the event from the perf buffer +void print_event(struct probe_SSL_data_t *event, const char *evt) { + ... + if (buf_size != 0) { + if (env.hexdump) { + // 2 characters for each byte + null terminator + char hex_data[MAX_BUF_SIZE * 2 + 1] = {0}; + buf_to_hex((uint8_t *)buf, buf_size, hex_data); + + printf("\n%s\n", s_mark); + for (size_t i = 0; i < strlen(hex_data); i += 32) { + printf("%.32s\n", hex_data + i); + } + printf("%s\n\n", e_mark); + } else { + printf("\n%s\n%s\n%s\n\n", s_mark, buf, e_mark); + } + } +} +``` + +完整的源代码可以在这里查看: + +## 编译与运行 + +要开始使用 `sslsniff`,首先要进行编译: + +```sh +make +``` + +完成后,请按照以下步骤操作: + +### **启动 sslsniff** + +在一个终端中,执行以下命令来启动 `sslsniff`: + +```sh +sudo ./sslsniff +``` + +### **执行 CURL 命令** + +在另一个终端中,执行: + +```console +curl https://example.com +``` + +正常情况下,你会看到类似以下的输出: + +```html + + + + Example Domain + ... + +
+ ... +
+ + +``` + +### **sslsniff 输出** + +当执行 `curl` 命令后,`sslsniff` 会显示以下内容: + +```txt + READ/RECV 0.132786160 curl 47458 1256 + ----- DATA ----- + + ... +
+

Example Domain

+ ... +
+ + + + ----- END DATA ----- +``` + +**注意**:显示的 HTML 内容可能会因 `example.com` 页面的不同而有所不同。 + +### 显示延迟和握手过程 + +要查看延迟和握手过程,请执行以下命令: + +```console +$ sudo ./sslsniff -l --handshake +OpenSSL path: /lib/x86_64-linux-gnu/libssl.so.3 +GnuTLS path: /lib/x86_64-linux-gnu/libgnutls.so.30 +NSS path: /lib/x86_64-linux-gnu/libnspr4.so +FUNC TIME(s) COMM PID LEN LAT(ms) +HANDSHAKE 0.000000000 curl 6460 1 1.384 WRITE/SEND 0.000115400 curl 6460 24 0.014 +``` + +### 16进制输出 + +要以16进制格式显示数据,请执行以下命令: + +```console +$ sudo ./sslsniff --hexdump +WRITE/SEND 0.000000000 curl 16104 24 +----- DATA ----- +505249202a20485454502f322e300d0a +0d0a534d0d0a0d0a +----- END DATA ----- + +... +``` + +## 总结 + +eBPF 是一个非常强大的技术,它可以帮助我们深入了解系统的工作原理。本教程是一个简单的示例,展示了如何使用 eBPF 来监控 SSL/TLS 通信。如果您对 eBPF 技术感兴趣,并希望进一步了解和实践,可以访问我们的教程代码仓库 和教程网站 + +参考资料: + +- +- +- +- +- diff --git a/src/30-sslsniff/README_en.md b/src/30-sslsniff/README_en.md new file mode 100644 index 0000000..0d7ae52 --- /dev/null +++ b/src/30-sslsniff/README_en.md @@ -0,0 +1,2 @@ +# eBPF openssl + diff --git a/src/30-sslsniff/sslsniff.bpf.c b/src/30-sslsniff/sslsniff.bpf.c new file mode 100644 index 0000000..4ed5173 --- /dev/null +++ b/src/30-sslsniff/sslsniff.bpf.c @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2023 Yusheng Zheng +// +// Based on sslsniff from BCC by Adrian Lopez & Mark Drayton. +// 15-Aug-2023 Yusheng Zheng Created this. +#include +#include +#include +#include +#include +#include "sslsniff.h" + +struct { + __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY); + __uint(key_size, sizeof(__u32)); + __uint(value_size, sizeof(__u32)); +} perf_SSL_events SEC(".maps"); + +#define BASE_EVENT_SIZE ((size_t)(&((struct probe_SSL_data_t *)0)->buf)) +#define EVENT_SIZE(X) (BASE_EVENT_SIZE + ((size_t)(X))) +#define MAX_ENTRIES 10240 + +#define min(x, y) \ + ({ \ + typeof(x) _min1 = (x); \ + typeof(y) _min2 = (y); \ + (void)(&_min1 == &_min2); \ + _min1 < _min2 ? _min1 : _min2; \ + }) + +struct { + __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY); + __uint(max_entries, 1); + __type(key, u32); + __type(value, struct probe_SSL_data_t); +} ssl_data SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_ENTRIES); + __type(key, __u32); + __type(value, __u64); +} start_ns SEC(".maps"); + +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, MAX_ENTRIES); + __type(key, __u32); + __type(value, __u64); +} bufs SEC(".maps"); + +const volatile pid_t targ_pid = 0; +const volatile uid_t targ_uid = -1; + +static __always_inline bool trace_allowed(u32 uid, u32 pid) +{ + /* filters */ + if (targ_pid && targ_pid != pid) + return false; + if (targ_uid != -1) { + if (targ_uid != uid) { + return false; + } + } + return true; +} + +SEC("uprobe/do_handshake") +int BPF_UPROBE(probe_SSL_rw_enter, void *ssl, void *buf, int num) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = pid_tgid; + u32 uid = bpf_get_current_uid_gid(); + u64 ts = bpf_ktime_get_ns(); + + if (!trace_allowed(uid, pid)) { + return 0; + } + + /* store arg info for later lookup */ + bpf_map_update_elem(&bufs, &tid, &buf, BPF_ANY); + bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); + return 0; +} + +static int SSL_exit(struct pt_regs *ctx, int rw) { + int ret = 0; + u32 zero = 0; + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u32 uid = bpf_get_current_uid_gid(); + u64 ts = bpf_ktime_get_ns(); + + if (!trace_allowed(uid, pid)) { + return 0; + } + + /* store arg info for later lookup */ + u64 *bufp = bpf_map_lookup_elem(&bufs, &tid); + if (bufp == 0) + return 0; + + u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); + if (!tsp) + return 0; + u64 delta_ns = ts - *tsp; + + int len = PT_REGS_RC(ctx); + if (len <= 0) // no data + return 0; + + struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); + if (!data) + return 0; + + data->timestamp_ns = ts; + data->delta_ns = delta_ns; + data->pid = pid; + data->tid = tid; + data->uid = uid; + data->len = (u32)len; + data->buf_filled = 0; + data->rw = rw; + data->is_handshake = false; + u32 buf_copy_size = min((size_t)MAX_BUF_SIZE, (size_t)len); + + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + + if (bufp != 0) + ret = bpf_probe_read_user(&data->buf, buf_copy_size, (char *)*bufp); + + bpf_map_delete_elem(&bufs, &tid); + bpf_map_delete_elem(&start_ns, &tid); + + if (!ret) + data->buf_filled = 1; + else + buf_copy_size = 0; + + bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, + EVENT_SIZE(buf_copy_size)); + return 0; +} + +SEC("uretprobe/SSL_read") +int BPF_URETPROBE(probe_SSL_read_exit) { + return (SSL_exit(ctx, 0)); +} + +SEC("uretprobe/SSL_write") +int BPF_URETPROBE(probe_SSL_write_exit) { + return (SSL_exit(ctx, 1)); +} + +SEC("uprobe/do_handshake") +int BPF_UPROBE(probe_SSL_do_handshake_enter, void *ssl) { + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u64 ts = bpf_ktime_get_ns(); + u32 uid = bpf_get_current_uid_gid(); + + if (!trace_allowed(uid, pid)) { + return 0; + } + + /* store arg info for later lookup */ + bpf_map_update_elem(&start_ns, &tid, &ts, BPF_ANY); + return 0; +} + +SEC("uretprobe/do_handshake") +int BPF_URETPROBE(probe_SSL_do_handshake_exit) { + u32 zero = 0; + u64 pid_tgid = bpf_get_current_pid_tgid(); + u32 pid = pid_tgid >> 32; + u32 tid = (u32)pid_tgid; + u32 uid = bpf_get_current_uid_gid(); + u64 ts = bpf_ktime_get_ns(); + int ret = 0; + + /* use kernel terminology here for tgid/pid: */ + u32 tgid = pid_tgid >> 32; + + /* store arg info for later lookup */ + if (!trace_allowed(tgid, pid)) { + return 0; + } + + u64 *tsp = bpf_map_lookup_elem(&start_ns, &tid); + if (tsp == 0) + return 0; + + ret = PT_REGS_RC(ctx); + if (ret <= 0) // handshake failed + return 0; + + struct probe_SSL_data_t *data = bpf_map_lookup_elem(&ssl_data, &zero); + if (!data) + return 0; + + data->timestamp_ns = ts; + data->delta_ns = ts - *tsp; + data->pid = pid; + data->tid = tid; + data->uid = uid; + data->len = ret; + data->buf_filled = 0; + data->rw = 2; + data->is_handshake = true; + bpf_get_current_comm(&data->comm, sizeof(data->comm)); + bpf_map_delete_elem(&start_ns, &tid); + + bpf_perf_event_output(ctx, &perf_SSL_events, BPF_F_CURRENT_CPU, data, + EVENT_SIZE(0)); + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/src/30-sslsniff/sslsniff.c b/src/30-sslsniff/sslsniff.c new file mode 100644 index 0000000..475ff15 --- /dev/null +++ b/src/30-sslsniff/sslsniff.c @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2023 Yusheng Zheng +// +// Based on sslsniff from BCC by Adrian Lopez & Mark Drayton. +// 15-Aug-2023 Yusheng Zheng Created this. +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sslsniff.skel.h" +#include "sslsniff.h" + +#define INVALID_UID -1 +#define INVALID_PID -1 +#define DEFAULT_BUFFER_SIZE 8192 + +#define __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe) \ + do { \ + LIBBPF_OPTS(bpf_uprobe_opts, uprobe_opts, .func_name = #sym_name, \ + .retprobe = is_retprobe); \ + skel->links.prog_name = bpf_program__attach_uprobe_opts( \ + skel->progs.prog_name, env.pid, binary_path, 0, &uprobe_opts); \ + } while (false) + +#define __CHECK_PROGRAM(skel, prog_name) \ + do { \ + if (!skel->links.prog_name) { \ + perror("no program attached for " #prog_name); \ + return -errno; \ + } \ + } while (false) + +#define __ATTACH_UPROBE_CHECKED(skel, binary_path, sym_name, prog_name, \ + is_retprobe) \ + do { \ + __ATTACH_UPROBE(skel, binary_path, sym_name, prog_name, is_retprobe); \ + __CHECK_PROGRAM(skel, prog_name); \ + } while (false) + +#define ATTACH_UPROBE_CHECKED(skel, binary_path, sym_name, prog_name) \ + __ATTACH_UPROBE_CHECKED(skel, binary_path, sym_name, prog_name, false) +#define ATTACH_URETPROBE_CHECKED(skel, binary_path, sym_name, prog_name) \ + __ATTACH_UPROBE_CHECKED(skel, binary_path, sym_name, prog_name, true) + +volatile sig_atomic_t exiting = 0; + +const char *argp_program_version = "sslsniff 0.1"; +const char *argp_program_bug_address = "https://github.com/iovisor/bcc/tree/master/libbpf-tools"; +const char argp_program_doc[] = + "Sniff SSL data.\n" + "\n" + "USAGE: sslsniff [OPTIONS]\n" + "\n" + "EXAMPLES:\n" + " ./sslsniff # sniff OpenSSL and GnuTLS functions\n" + " ./sslsniff -p 181 # sniff PID 181 only\n" + " ./sslsniff -u 1000 # sniff only UID 1000\n" + " ./sslsniff -c curl # sniff curl command only\n" + " ./sslsniff --no-openssl # don't show OpenSSL calls\n" + " ./sslsniff --no-gnutls # don't show GnuTLS calls\n" + " ./sslsniff --no-nss # don't show NSS calls\n" + " ./sslsniff --hexdump # show data as hex instead of trying to " + "decode it as UTF-8\n" + " ./sslsniff -x # show process UID and TID\n" + " ./sslsniff -l # show function latency\n" + " ./sslsniff -l --handshake # show SSL handshake latency\n" + " ./sslsniff --extra-lib openssl:/path/libssl.so.1.1 # sniff extra " + "library\n"; + +struct env { + pid_t pid; + int uid; + bool extra; + char *comm; + bool openssl; + bool gnutls; + bool nss; + bool hexdump; + bool latency; + bool handshake; + char *extra_lib; +} env = { + .uid = INVALID_UID, + .pid = INVALID_PID, + .openssl = true, + .gnutls = true, + .nss = true, + .comm = NULL, +}; + +#define HEXDUMP_KEY 1000 +#define HANDSHAKE_KEY 1002 +#define EXTRA_LIB_KEY 1003 + +static const struct argp_option opts[] = { + {"pid", 'p', "PID", 0, "Sniff this PID only."}, + {"uid", 'u', "UID", 0, "Sniff this UID only."}, + {"extra", 'x', NULL, 0, "Show extra fields (UID, TID)"}, + {"comm", 'c', "COMMAND", 0, "Sniff only commands matching string."}, + {"no-openssl", 'o', NULL, 0, "Do not show OpenSSL calls."}, + {"no-gnutls", 'g', NULL, 0, "Do not show GnuTLS calls."}, + {"no-nss", 'n', NULL, 0, "Do not show NSS calls."}, + {"hexdump", HEXDUMP_KEY, NULL, 0, + "Show data as hexdump instead of trying to decode it as UTF-8"}, + {"latency", 'l', NULL, 0, "Show function latency"}, + {"handshake", HANDSHAKE_KEY, NULL, 0, + "Show SSL handshake latency, enabled only if latency option is on."}, + {"verbose", 'v', NULL, 0, "Verbose debug output"}, + { NULL, 'h', NULL, OPTION_HIDDEN, "Show the full help" }, + {}, +}; + +static bool verbose = false; + +static error_t parse_arg(int key, char *arg, struct argp_state *state) { + switch (key) { + case 'p': + env.pid = atoi(arg); + break; + case 'u': + env.uid = atoi(arg); + break; + case 'x': + env.extra = true; + break; + case 'c': + env.comm = strdup(arg); + break; + case 'o': + env.openssl = false; + break; + case 'g': + env.gnutls = false; + break; + case 'n': + env.nss = false; + break; + case 'l': + env.latency = true; + break; + case 'v': + verbose = true; + break; + case HEXDUMP_KEY: + env.hexdump = true; + break; + case HANDSHAKE_KEY: + env.handshake = true; + break; + case 'h': + argp_state_help(state, stderr, ARGP_HELP_STD_HELP); + break; + default: + return ARGP_ERR_UNKNOWN; + } + return 0; +} + +#define PERF_BUFFER_PAGES 16 +#define PERF_POLL_TIMEOUT_MS 100 +#define warn(...) fprintf(stderr, __VA_ARGS__) + +static struct argp argp = { + opts, + parse_arg, + NULL, + argp_program_doc +}; + +static int libbpf_print_fn(enum libbpf_print_level level, const char *format, + va_list args) { + if (level == LIBBPF_DEBUG && !verbose) + return 0; + return vfprintf(stderr, format, args); +} + +static void handle_lost_events(void *ctx, int cpu, __u64 lost_cnt) { + warn("lost %llu events on CPU #%d\n", lost_cnt, cpu); +} + +static void sig_int(int signo) { + exiting = 1; +} + +int attach_openssl(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_write, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_read, probe_SSL_read_exit); + + if (env.latency && env.handshake) { + ATTACH_UPROBE_CHECKED(skel, lib, SSL_do_handshake, + probe_SSL_do_handshake_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, SSL_do_handshake, + probe_SSL_do_handshake_exit); + } + + return 0; +} + +int attach_gnutls(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_send, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, gnutls_record_recv, probe_SSL_read_exit); + + return 0; +} + +int attach_nss(struct sslsniff_bpf *skel, const char *lib) { + ATTACH_UPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Write, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Send, probe_SSL_write_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Read, probe_SSL_read_exit); + ATTACH_UPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_rw_enter); + ATTACH_URETPROBE_CHECKED(skel, lib, PR_Recv, probe_SSL_read_exit); + + return 0; +} + +/* + * Find the path of a library using ldconfig. + */ +char *find_library_path(const char *libname) { + char cmd[128]; + static char path[512]; + FILE *fp; + + // Construct the ldconfig command with grep + snprintf(cmd, sizeof(cmd), "ldconfig -p | grep %s", libname); + + // Execute the command and read the output + fp = popen(cmd, "r"); + if (fp == NULL) { + perror("Failed to run ldconfig"); + return NULL; + } + + // Read the first line of output which should have the library path + if (fgets(path, sizeof(path) - 1, fp) != NULL) { + // Extract the path from the ldconfig output + char *start = strrchr(path, '>'); + if (start && *(start + 1) == ' ') { + memmove(path, start + 2, strlen(start + 2) + 1); + char *end = strchr(path, '\n'); + if (end) { + *end = '\0'; // Null-terminate the path + } + pclose(fp); + return path; + } + } + + pclose(fp); + return NULL; +} + +void buf_to_hex(const uint8_t *buf, size_t len, char *hex_str) { + for (size_t i = 0; i < len; i++) { + sprintf(hex_str + 2 * i, "%02x", buf[i]); + } +} + +// Function to print the event from the perf buffer +void print_event(struct probe_SSL_data_t *event, const char *evt) { + static unsigned long long start = + 0; // Use static to retain value across function calls + char buf[MAX_BUF_SIZE + 1] = {0}; // +1 for null terminator + unsigned int buf_size; + + if (event->len <= MAX_BUF_SIZE) { + buf_size = event->len; + } else { + buf_size = MAX_BUF_SIZE; + } + + if (event->buf_filled == 1) { + memcpy(buf, event->buf, buf_size); + } else { + buf_size = 0; + } + + if (env.comm && strcmp(env.comm, event->comm) != 0) { + return; + } + + if (start == 0) { + start = event->timestamp_ns; + } + double time_s = (double)(event->timestamp_ns - start) / 1000000000; + + char lat_str[10]; + if (event->delta_ns) { + snprintf(lat_str, sizeof(lat_str), "%.3f", + (double)event->delta_ns / 1000000); + } else { + strncpy(lat_str, "N/A", sizeof(lat_str)); + } + + char s_mark[] = "----- DATA -----"; + char e_mark[64] = "----- END DATA -----"; + if (buf_size < event->len) { + snprintf(e_mark, sizeof(e_mark), + "----- END DATA (TRUNCATED, %d bytes lost) -----", + event->len - buf_size); + } + + char *rw_event[] = { + "READ/RECV", + "WRITE/SEND", + "HANDSHAKE" + }; + +#define BASE_FMT "%-12s %-18.9f %-16s %-7d %-6d" +#define EXTRA_FMT " %-7d %-7d" +#define LATENCY_FMT " %-7s" + + if (env.extra && env.latency) { + printf(BASE_FMT EXTRA_FMT LATENCY_FMT, rw_event[event->rw], + time_s, event->comm, event->pid, + event->len, event->uid, event->tid, lat_str); + } else if (env.extra) { + printf(BASE_FMT EXTRA_FMT, rw_event[event->rw], time_s, event->comm, event->pid, + event->len, event->uid, event->tid); + } else if (env.latency) { + printf(BASE_FMT LATENCY_FMT, rw_event[event->rw], time_s, event->comm, event->pid, + event->len, lat_str); + } else { + printf(BASE_FMT, rw_event[event->rw], time_s, event->comm, event->pid, + event->len); + } + + if (buf_size != 0) { + if (env.hexdump) { + // 2 characters for each byte + null terminator + char hex_data[MAX_BUF_SIZE * 2 + 1] = {0}; + buf_to_hex((uint8_t *)buf, buf_size, hex_data); + + printf("\n%s\n", s_mark); + for (size_t i = 0; i < strlen(hex_data); i += 32) { + printf("%.32s\n", hex_data + i); + } + printf("%s\n\n", e_mark); + } else { + printf("\n%s\n%s\n%s\n\n", s_mark, buf, e_mark); + } + } +} + +static void handle_event(void *ctx, int cpu, void *data, __u32 data_size) { + struct probe_SSL_data_t *e = data; + if (e->is_handshake) { + print_event(e, "perf_SSL_do_handshake"); + } else { + print_event(e, "perf_SSL_rw"); + } +} + +int main(int argc, char **argv) { + LIBBPF_OPTS(bpf_object_open_opts, open_opts); + struct sslsniff_bpf *obj = NULL; + struct perf_buffer *pb = NULL; + int err; + + err = argp_parse(&argp, argc, argv, 0, NULL, NULL); + if (err) + return err; + + libbpf_set_print(libbpf_print_fn); + + obj = sslsniff_bpf__open_opts(&open_opts); + if (!obj) { + warn("failed to open BPF object\n"); + goto cleanup; + } + + obj->rodata->targ_uid = env.uid; + obj->rodata->targ_pid = env.pid == INVALID_PID ? 0 : env.pid; + + err = sslsniff_bpf__load(obj); + if (err) { + warn("failed to load BPF object: %d\n", err); + goto cleanup; + } + + if (env.openssl) { + char *openssl_path = find_library_path("libssl.so"); + printf("OpenSSL path: %s\n", openssl_path); + attach_openssl(obj, "/lib/x86_64-linux-gnu/libssl.so.3"); + } + if (env.gnutls) { + char *gnutls_path = find_library_path("libgnutls.so"); + printf("GnuTLS path: %s\n", gnutls_path); + attach_gnutls(obj, gnutls_path); + } + if (env.nss) { + char *nss_path = find_library_path("libnspr4.so"); + printf("NSS path: %s\n", nss_path); + attach_nss(obj, nss_path); + } + + pb = perf_buffer__new(bpf_map__fd(obj->maps.perf_SSL_events), + PERF_BUFFER_PAGES, handle_event, handle_lost_events, + NULL, NULL); + if (!pb) { + err = -errno; + warn("failed to open perf buffer: %d\n", err); + goto cleanup; + } + + if (signal(SIGINT, sig_int) == SIG_ERR) { + warn("can't set signal handler: %s\n", strerror(errno)); + err = 1; + goto cleanup; + } + + // Print header + printf("%-12s %-18s %-16s %-7s %-7s", "FUNC", "TIME(s)", "COMM", "PID", + "LEN"); + if (env.extra) { + printf(" %-7s %-7s", "UID", "TID"); + } + if (env.latency) { + printf(" %-7s", "LAT(ms)"); + } + printf("\n"); + + while (!exiting) { + err = perf_buffer__poll(pb, PERF_POLL_TIMEOUT_MS); + if (err < 0 && err != -EINTR) { + warn("error polling perf buffer: %s\n", strerror(-err)); + goto cleanup; + } + err = 0; + } + +cleanup: + perf_buffer__free(pb); + sslsniff_bpf__destroy(obj); + return err != 0; +} diff --git a/src/30-sslsniff/sslsniff.h b/src/30-sslsniff/sslsniff.h new file mode 100644 index 0000000..3ef4c5e --- /dev/null +++ b/src/30-sslsniff/sslsniff.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) +// Copyright (c) 2023 Yusheng Zheng +// +// Based on sslsniff from BCC by Adrian Lopez & Mark Drayton. +// 15-Aug-2023 Yusheng Zheng Created this. +#ifndef __SSLSNIFF_H +#define __SSLSNIFF_H + +#define MAX_BUF_SIZE 8192 +#define TASK_COMM_LEN 16 + +struct probe_SSL_data_t { + __u64 timestamp_ns; + __u64 delta_ns; + __u32 pid; + __u32 tid; + __u32 uid; + __u32 len; + int buf_filled; + int rw; + char comm[TASK_COMM_LEN]; + __u8 buf[MAX_BUF_SIZE]; + int is_handshake; +}; + +#endif /* __SSLSNIFF_H */ diff --git a/src/SUMMARY.md b/src/SUMMARY.md index a8fe978..92e392b 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -33,6 +33,7 @@ - [使用 eBPF 添加 sudo 用户](26-sudo/README.md) - [使用 eBPF 替换任意程序读取或写入的文本](27-replace/README.md) - [BPF的生命周期:使用 Detached 模式在用户态应用退出后持续运行 eBPF 程序](28-detach/README.md) +- [使用 eBPF 用户态捕获多种库的 SSL/TLS 明文数据](30-sslsniff/README.md) # bcc tutorial