本文的代码在 https://github.com/yfractal/sdb/tree/main/sdb-shim
在开发或者排查问题的时候,时常会需要查看请求内容。比如著名的 tcpdump 可以查看 http 内容。https 也有相应的工具,比如基于 eBPF 的 ecapture(eBPF 是为了区分早起的 BPF,但有的人建议还是用 bpf,另 tcpdump 是用 bpf 的,且作者都是 Van Jacobson)。
但这些工具都需要 root 权限,有些环境下,比如公司奇怪的安全要求或者在 docker 里,user 没有 root 权限。这个时候,想查看 https 的请求内容就非常困难。
本文介绍一种既不需要更改 Ruby 代码,也不需要 root 权限,查看 https 请求内容的方法。
我们可以让应用程序自己告诉我们,它的请求内容是什么。最直接的方法是,我们复写 Ruby http 请求方法,当请求反回后,打印解密后的内容。
但这样,就需要引入一个 Gem,如果是编译型语言,比如 Go,还需要重新编译。
eBPF 的 uprobe,会在方法的地址,插入一个 trap instruction 比如 int3,当执行到该地址,不会执行原有方法,而是跳转到指定地址。但 eBPF uprobe 需要 root 权限。
而我们既要避免侵入代码,又要在非 root 权限下运行,为了既要又要,在 Mac 系统下,我们可以使用 __interpose section
替换掉原有方法,然后用 DYLD_INSERT_LIBRARIES
链接入程序。达到类似 Ruby alias method 的目的。
首先我们要找到合适的方法。Ruby 使用 openssl 进行加密解密。但我对 Ruby 标准库和 openssl 并不熟悉,直接看代码会比较麻烦。
可以用 Ruby 构造请求,并用 stack profiling tool 查看请求了哪些方法,从而缩小范围。我用的是 https://github.com/yfractal/sdb,在这个并不好用的工具下,它帮我把位置定位到 read_nonblock openssl/buffering.rb:204。
之后通过 debug 和阅读代码,可以知道,Ruby 调用 SSL_read 进行解密。
找到对应的方法后,我们需要告诉 MacOS 做相应的替换。
#include "openssl/ssl.h"
struct __osx_interpose {
const void* new_func;
const void* orig_func;
};
static int Real__SSL_read (void *ssl, void *buf, int num) { return SSL_read (ssl, buf, num); }
extern int __interpose_SSL_read (void *ssl, void *buf, int num);
static const struct __osx_interpose __osx_interpose_SSL_read __attribute__((used, section("__DATA, __interpose"))) =
{ (const void*)((uintptr_t)(&(__interpose_SSL_read))),
(const void*)((uintptr_t)(&(SSL_read))) };
在 __interpose_SSL_read
里,我们调用 SSL_read
得到解密后的内容。由于 http body 是被压缩过的,我们需要先找到 body 的位置(同时解密了 headers 和 body),并进行解压,之后就可以拿到可读的 body。
代码在 https://github.com/yfractal/sdb/blob/main/sdb-shim/src/https_instrument.c 。
目前 https_instrument
还只是一个玩具,我只测试了一个最简单的例子。对我来说,写这样的工具是一件很有趣的事情。再者,公司的开发机,没有 root 权限,它毕竟也不是科技公司。。。
相比 eBPF,这种方法除了不用 root 权限外,开发起来也更容易,不需要额外的支持,还可以随便使用 library。
比如 opentelemetry-go-instrumentation 使用 eBPF 做 instrument,但 eBPF 是单独的内存空间,操作复杂的 Go 数据结构就极其困难,比如 hash map。
不过 linux 并不直接支持这种方法,但可以用 LD_PRELOAD
替换动态连结库的方法,相应代码。我之前的文章也有相关的介绍。
LD_PRELOAD
,虽然可以 instrument openssl,但没法改程序本身的代码。理论上,通过改 binary,比如在相应的地址插入 int3,生成新的 bianry,应该可以达到类似的效果,或者直接在编译的时候做相应操作,再或者改 ELF。
相比 eBPF,个人更喜欢 function Interposing 这种方法做 instrument。虽然需要应用配合,但比 eBPF 更可控。更重要的是,开发更简单也更灵活。
https://github.com/yfractal/sdb 目前来说,也是一个玩具,不但不好用,还会 segment fault。
https_instrument
应该还有很多问题,我会在个人使用过程中慢慢完善。如果真的有人需要的话,我再想办法让它用起来更简单。