bpf filter_使用bpf拦截缩放的加密数据

bpf filter

I originally wrote an earlier version of this post at the end of March, when I was working on adding uprobes support to redbpf. I wanted to blog about the work I was doing and needed an application to instrument for the purpose of this post. At that time Zoom’s popularity was rising quickly, and I happened to read somewhere that it supported this creepy attention tracking feature that allowed meeting hosts to monitor if attendees were paying attention. I figured I could try to use uprobes to snoop into the data Zoom was sending to their servers and see how the tracking worked.

我最初是在三月底写这篇文章的早期版本的,当时我正在为redbpf添加uprobes支持。 我想写博客介绍我正在做的工作,因此需要一个应用程序作为本文的工具。 那时,Zoom的受欢迎程度Swift提高,我碰巧在某处读到它支持这种令人毛骨悚然的注意力跟踪功能,该功能使会议主持人可以监视与会者是否在关注。 我想我可以尝试使用uprobs窥探Zoom正在发送到其服务器的数据,并查看跟踪的工作原理。

But then Zoom quickly started getting under a lot of fire. Zoombombing became a thing, several security issues were discovered and pretty much everyone started piling on the company. Considering all that, I was advised and ultimately decided not to publish the post.

但是随后ZoomSwift受到了很多攻击。 Zoombombing成为一件事,发现了几个安全问题,几乎每个人都开始向该公司求助。 考虑到所有这些,我被告知并最终决定不发布该帖子。

Now things seem to have settled, Zoom improved their security and by popular demand got rid of attention tracking. So I think I can finally publish this! I edited out the part about attention tracking (which no longer exists) and a couple of other things that could potentially get me in trouble.

现在事情似乎已经解决,Zoom提高了其安全性,并且由于受欢迎的需求而摆脱了关注跟踪。 所以我想我终于可以发表这个! 我编辑了有关注意力跟踪的部分(不再存在)和其他一些可能会给我带来麻烦的事情。

TLDR: I wrote a command line tool that uses BPF uprobes to intercept the TLS encrypted data that zoom sends over the network, and here I’m going to show the process I went through to write it. After I wrote this post I made the tool generic so that it can now instrument any program that uses OpenSSL. I published the code at https://github.com/alessandrod/snuffy.

TLDR:我编写了一个命令行工具,该工具使用BPF uprobes截取zoom通过网络发送的TLS加密数据,在这里,我将展示编写过程。 在写完这篇文章之后,我使该工具变得通用,以便它现在可以检测使用OpenSSL的任何程序。 我在https://github.com/alessandrod/snuffy上发布了代码。

仪表应用程序 (Instrumenting applications with uprobes)

Uprobes let you instrument user space applications by attaching custom code to arbitrary locations inside a target process. It’s a bit like running an application in a debugger, setting breakpoints and fiddling around, but programmatically and without the overhead of a debugger.

Uprobes使您可以通过将自定义代码附加到目标进程内的任意位置来检测用户空间应用程序。 这有点像在调试器中运行应用程序,设置断点并摆弄,但以编程方式且没有调试器的开销。

An uprobe must be compiled and loaded like any other BPF program, then it can be attached with the following API:

必须像其他任何BPF程序一样编译加载Uprobe,然后可以将其附加以下API:

pub fn attach_uprobe(
    &mut self,
    fn_name: Option<&str>,
    offset: u64,
    target: &str,
    pid: Option<pid_t>,
) -> Result<()>;

attach_uprobe() parses the target ELF binary or shared library, looks up the function fn_name, and once the target is running it injects the probe code at the resolved address. If offset is non-zero, its value is added to the address of fn_name. If fn_name is None, then offset is interpreted as starting from the beginning of the target's .text section. Finally if a pid is given, the probe will only be attached to the process with the given id.

attach_uprobe()解析target ELF二进制或共享库,查找函数fn_name ,一旦目标运行,它将在解析的地址注入探测代码。 如果offset不为零, fn_name其值添加到fn_name的地址中。 如果fn_nameNone ,则将offset解释为从目标的.text节的开头开始。 最后,如果给出了pid ,则探针将仅以给定的ID附加到进程。

In the rest of the post I’m going to show some examples of uprobes, focusing on the code that gets compiled to BPF bytecode, loaded in the kernel and then injected in the target process (in our case zoom). I’m not going to show much of the user-space code that loads the probes. That part is pretty standard rust code that does some setup, then prints out the data coming from the probes as it receives it. If you’re interested you can still find all the user-space code at https://github.com/alessandrod/snuffy/blob/master/src/.

在本文的其余部分中,我将展示一些uprobes的示例,重点介绍被编译为BPF字节码,加载到内核然后注入目标进程(在我们的情况下为zoom)的代码。 我不会显示很多加载探针的用户空间代码。 该部分是相当标准的生锈代码,它进行了一些设置,然后在接收到时打印出来自探针的数据。 如果您有兴趣,仍然可以在https://github.com/alessandrod/snuffy/blob/master/src/中找到所有用户空间代码。

放大 (Poking into Zoom)

We’re going to use uprobes to inspect the network traffic between the zoom client and the company’s servers. Zoom uses Transport Layer Security to encrypt the data. In order to intercept the unencrypted data, we need to find out which TLS library is used by the client, then attach uprobes to strategic places inside it.

我们将使用uprob来检查zoom客户端和公司服务器之间的网络流量。 Zoom使用传输层安全性来加密数据。 为了拦截未加密的数据,我们需要找出客户端使用哪个TLS库,然后将uprobs附加到其中的重要位置。

Let’s start with searching for common TLS symbols using objdump:

让我们开始使用objdump搜索常见的TLS符号:

$ objdump -CT /opt/zoom/zoom | grep -iE "ssl|gnutls"
000000000080d5b0 g DF .text 0000000000000013 Base PreMeetingUIMgr::sig_blockUnknownSSLCertChanged()
000000000080d590 g DF .text 0000000000000013 Base PreMeetingUIMgr::sig_sslCertWarningChanged()

Those look like callbacks that get invoked when a certificate is invalid, and Zoom does indeed show a warning if you try to intercept its traffic with a tool like mitmproxy. The callbacks deal with certificates, not unencrypted buffers, so they are not useful to us.

这些看起来像是在证书无效时调用的回调,如果尝试使用mitmproxy类的工具拦截其流量,Zoom确实会显示警告。 回调处理的是证书,而不是未加密的缓冲区,因此它们对我们没有用。

Looking at the output of ldd we can see that Zoom links to Qt Network, which includes some potentially relevant APIs:

查看ldd的输出,我们可以看到Zoom链接到Qt Network ,其中包括一些可能相关的API:

$ objdump -CT /opt/zoom/zoom | grep -iE "QNetworkReq"
0000000000000000 DF *UND* 0000000000000000 Qt_5 QNetworkRequest::QNetworkRequest(QUrl const&)
0000000000000000 DF *UND* 0000000000000000 Qt_5 QNetworkRequest::~QNetworkRequest()
0000000000000000 DF *UND* 0000000000000000 Qt_5 QNetworkAccessManager::get(QNetworkRequest const&)

QNetworkRequest(QUrl const&) looks like something that could be used to communicate with the backend and does support TLS. I tried attaching to it and other functions exported by the framework but none of them turned out to be invoked. Zoom supports a number of platforms and devices, it's possible that they use Qt just for the UI on linux, and then have some lower level networking code that can be shared with their other clients.

QNetworkRequest(QUrl const&)看起来可以用于与后端通信,并且确实支持TLS 。 我尝试将其附加到框架以及框架导出的其他函数,但结果都没有一个被调用。 Zoom支持多种平台和设备,它们可能仅将Qt用于Linux上的UI,然后具有一些可与其他客户端共享的较低级的网络代码。

At this point it is pretty likely that zoom is linking statically to the TLS library. Let’s see if in the .rodata section of the binary there's anything that could point us in the right direction:

此时,缩放很有可能已静态链接到TLS库。 让我们看看二进制文件的.rodata部分中是否有什么可以指出正确的方向:

$ readelf -p .rodata /opt/zoom/zoom | grep -i ssl | wc -l
739
$ # 😏
$ readelf -p .rodata /opt/zoom/zoom | grep -i 'openssl 1'
[4a1b66] OpenSSL 1.1.1g 21 Apr 2020
[58cd50] OpenSSL 1.1.1g 21 Apr 2020

Aha! The client is using OpenSSL version 1.1.1g (knowing this will turn out to be very useful), and the library is statically linked.

啊哈! 客户端使用的是OpenSSL 1.1.1g版(知道这将非常有用),并且库是静态链接的。

检测OpenSSL (Instrumenting OpenSSL)

OpenSSL exports two functions named SSL_read and SSL_write, which have the following signature:

OpenSSL导出名为SSL_readSSL_write的两个函数,它们具有以下签名:

int SSL_read(SSL *ssl, void *buf, int num);
int SSL_write(SSL *ssl, const void *buf, int num);

SSL_read reads encrypted data sent by a remote peer, decrypts it and stores the decrypted data in the provided buffer. SSL_write encrypts the given buffer and sends it to a remote peer. Attaching an uprobe where SSL_read returns, and one at the entry of SSL_write, we can therefore access unencrypted memory.

SSL_read读取远程对等方发送的加密数据,将其解密并将解密的数据存储在提供的缓冲区中。 SSL_write加密给定的缓冲区,并将其发送到远程对等方。 附加的uprobe其中SSL_read入境回报,一个SSL_write ,因此,我们可以访问加密存储。

Here’s the uprobes that do just that:

这就是这样做的滋味:

use redbpf_probes::uprobe::prelude::*;


struct SSLArgs {
    ssl: usize,
    buf: usize,
}


// temporary storage map
static mut ssl_args: HashMap<u64, SSLArgs> = HashMap::with_max_entries(1024);


fn output_buf(regs: Registers, mode: AccessMode, buf_addr: usize, len: usize) {
  // Ignore how this is implemented for now. Assume it reads `len` bytes from `buf_addr`
  // and sends them to our user-space process where they are hex-dumped.
  ...
}


#[uprobe]
fn SSL_write_entry(regs: Registers) {
    let ssl = regs.parm1() as usize;
    let buf = regs.parm2() as usize;
    let num = regs.parm3() as i32;
    if num <= 0 {
        return;
    }


    // This is where SSL_write begins, the buffer isn't encrypted yet
    // so we send it to user-space
    output_buf(regs, AccessMode::Write, buf, num as usize);
}


#[uprobe]
fn SSL_read_entry(regs: Registers) {
    let ssl = regs.parm1() as usize;
    let buf = regs.parm2() as usize;


    // store the function arguments so we can retrieve them once the
    // function returns
    unsafe {
        ssl_args.set(&bpf_get_current_pid_tgid(), &SSLArgs { ssl, buf });
    }
}


#[uretprobe]
fn SSL_read_ret(regs: Registers) {
    // the return value of SSL_read contains the length of the buffer
    let num = regs.rc() as i32;
    if num < 0 {
        return;
    }
    // This is where SSL_read returns, the buffer is now decrypted
    // so we send it to user-space
    let tgid = bpf_get_current_pid_tgid();
    let args = unsafe { ssl_args.get(&tgid) };
    if let Some(SSLArgs { ssl, buf }) = args {
        output_buf(regs, AccessMode::Read, *buf, num as usize);
        unsafe { ssl_args.delete(&tgid) };
    }
}

Uprobes are annotated with the #[uprobe] attribute. Once they are triggered, they get passed a Registers argument through which they can access memory.

上衣使用#[uprobe]属性进行注释。 一旦被触发,它们就会传递一个Registers参数,通过它们可以访问内存。

The SSL_write_entry probe is the simplest. It reads the registers containing the values of the buf and num arguments passed to SSL_write, and sends a copy of the buffer to user-space before it gets encrypted.

SSL_write_entry探针是最简单的。 它读取包含传递给SSL_writebufnum参数值的寄存器,并在缓冲区被加密之前将缓冲区的副本发送到用户空间。

The SSL_read_entry probe is similar in that it reads the content of the ssl, buf and num arguments passed to SSL_read. It doesn't send the buffer to user-space though. Remember the data is decrypted after SSL_read returns, so we need a second uprobe that we attach to the return address of the function. That's what SSL_read_ret is for. It's similar to the other two probes, but is annotated with #[uretprobe], which means that it will trigger once the function it's attached to returns.

SSL_read_entry探针的相似之处在于,它读取传递给SSL_readsslbufnum参数的SSL_read 。 它不会将缓冲区发送到用户空间。 请记住,数据SSL_read返回之后被解密,因此我们需要附加到函数的返回地址的第二项袍子。 这就是SSL_read_ret目的。 它类似于其他两个探针,但注有#[uretprobe] ,这意味着它会触发一旦它连接到的函数返回

But why do we need two probes for SSL_read, why not just have SSL_read_ret? The answer is that when SSL_read returns, it's likely that the registers that used to contain the function arguments were modified, so we need to read their values at the start of the function and store them so we can retrieve them later. This is a very common pattern when writing BPF code.

但是,为什么我们需要两个SSL_read探针,为什么不仅仅拥有SSL_read_ret ? 答案是,当SSL_read返回时,很可能已经修改了用于包含函数参数的寄存器,因此我们需要在函数开始时读取它们的值并存储它们,以便以后可以检索它们。 在编写BPF代码时,这是一种非常常见的模式。

Finally if zoom linked to OpenSSL dynamically or if debugging symbols were present, the user-space code to attach the probes would be as simple as:

最后,如果缩放是动态链接到OpenSSL的,或者存在调试符号,则附加探针的用户空间代码将非常简单:

use redbpf::load::Loader;


let mut loader = Loader::load_file(COMPILED_BPF_BINARY)?;
let pid = None;
for uprobe in loader.uprobes_mut() {
    // Attach to SSL_read and SSL_write inside libssl.
    // Let redbpf resolve the symbol addresses.
    match uprobe.name().as_str() {
        "SSL_read_entry" | "SSL_read_ret" => {
            uprobe.attach_uprobe(Some("SSL_read"), 0, "libssl", pid)?;
        }
        "SSL_write_entry" => {
            uprobe.attach_uprobe(Some("SSL_write"), 0, "libssl", pid)?;
        }
        _ => continue,
    }
}

Unfortunately since OpenSSL is statically linked and the symbols have been stripped, redbpf can’t automatically resolve the addresses of SSL_read and SSL_write, instead we have to explicitly provide the offsets we want to attach to:

不幸的是,由于OpenSSL是静态链接的,并且符号已被剥离,因此redbpf无法自动解析SSL_readSSL_write的地址,相反,我们必须显式提供要附加到的偏移量:

use redbpf::load::Loader;


let mut loader = Loader::load_file(COMPILED_BPF_BINARY)?;
let pid = None;
for uprobe in loader.uprobes_mut() {
    let zoom_binary = "/opt/zoom/zoom";
    // the offset of SSL_read in zoom's .text section
    let ssl_read_offset = ???;
    // and the offset of SSL_write
    let ssl_write_offset = ???;


    match uprobe.name().as_str() {
        "SSL_read_entry" | "SSL_read_ret" => {
            uprobe.attach_uprobe(None, ssl_read_offset, zoom_binary, pid)?;
        }
        "SSL_write_entry" => {
            uprobe.attach_uprobe(None, ssl_write_offset, zoom_binary, pid)?;
        }
        _ => continue,
    }
}

But how do we find the offsets? What values do we give to ssl_read_offset and ssl_write_offset?

但是,我们如何找到偏移量呢? 我们给ssl_read_offsetssl_write_offset什么值?

[已删除] ([ REDACTED ])

I had a nice little section on how to find the offsets here. When I first wrote it I was convinced that publishing two addresses couldn’t possibly get me sued for reverse engineering. Some of the people who read the draft of this post changed my mind about it though, and it is 2020 after all so it is not a good time to be optimistic.

我有一个很好的小节,关于如何在这里找到偏移量。 当我第一次写它时,我坚信发布两个地址不可能使我被要求进行逆向工程。 虽然有些人读了这篇文章的草稿后改变了主意,但毕竟是2020年,所以现在不是一个乐观的好时机。

Hypothetically I suppose one could find the offsets by disassembling zoom with objdump, then disassembling OpenSSL 1.1.1g and comparing the two. I guess the code wouldn't match exactly, but the function prologues and the relative addressing around the SSL * context used by SSL_read and SSL_write could make for good enough patterns. With a few carefully crafted ripgrep -U (multiline) searches on the disassembled code, I bet it wouldn't take that long to find the functions.

假设我假设可以通过用objdump分解zoom,然后分解OpenSSL 1.1.1g并比较两者来找到偏移量。 我想代码可能不完全匹配,但是函数序言和SSL_readSSL_write使用的SSL *上下文周围的相对地址可以构成足够好的模式。 通过对反汇编的代码进行一些精心设计的ripgrep -U (多行)搜索,我敢肯定,找到这些功能不会花那么长时间。

The rest of the post assumes that we did find the offsets, and that we put them in a file named zoom-offsets.toml with the following format:

文章的其余部分假定我们确实找到了偏移量,并将它们放置在名为zoom-offsets.toml的文件中,格式如下:

# the values below are just examples, not the real ones
ssl_read = 0xBAAAAAAD
ssl_write = 0xDECAFBAD

so that the values passed to attach_uprobe() can be loaded from the file.

这样就可以从文件中加载传递给attach_uprobe()的值。

最后一些数据 (Finally some data)

Having found the offsets of SSL_read and SSL_write, if we load the uprobes we wrote above and then start zoom, we'll get output that looks like this:

找到SSL_readSSL_write的偏移量后,如果加载上面编写的uprobes然后开始缩放,则将获得如下所示的输出:

$ sudo target/debug/snuffy --hex-dump --offsets zoom-offsets.toml
Write 575 bytes
|504f5354 202f7265 6c656173 656e6f74| POST /releasenot 00000000
|65732048 5454502f 312e310d 0a486f73| es HTTP/1.1..Hos 00000010
|743a2075 73303477 65622e7a 6f6f6d2e| t: us04web.zoom. 00000020
|75730d0a 55736572 2d416765 6e743a20| us..User-Agent: 00000030
|4d6f7a69 6c6c612f 352e3020 285a4f4f| Mozilla/5.0 (ZOO 00000040
|4d2e4c69 6e757820 5562756e 74752031| M.Linux Ubuntu 1 00000050
...

Read 3088 bytes
|48545450 2f312e31 20323030 200d0a44| HTTP/1.1 200 ..D 00000000
|6174653a 20467269 2c203034 20536570| ate: Fri, 04 Sep 00000010
|20323032 30203035 3a30343a 31352047| 2020 05:04:15 G 00000020
|4d540d0a 436f6e74 656e742d 54797065| MT..Content-Type 00000030
|3a206170 706c6963 6174696f 6e2f782d| : application/x- 00000040
|70726f74 6f627566 3b636861 72736574| protobuf;charset 00000050
...

When zoom starts it checks for updates with that HTTP POST request. The uprobes get triggered, send the unencrypted data to the snuffy process and there the data gets hex-dumped. Success!

缩放开始时,它将检查该HTTP POST请求的更新。 触发扰动,将未加密的数据发送到snuffy进程,然后将数据进行十六进制转储。 成功!

跟踪网络连接 (Tracing network connections)

Turns out that Zoom uses many TLS connections simultaneously, so the output from snuffy quickly becomes an unreadable mess of intermingled data belonging to different connections.

事实证明Zoom会同时使用许多TLS连接,因此snuffy的输出很快就变成了属于不同连接的混合数据的不可读混乱。

To improve readability, we’re going to try to associate reads and writes to ip addresses by digging into OpenSSL a bit more.

为了提高可读性,我们将通过进一步研究OpenSSL来尝试将读取和写入与ip地址相关联。

提取套接字描述符 (Extracting socket descriptors)

OpenSSL provides the BIO API to implement IO. Looking at the relevant header files we can see:

OpenSSL提供了BIO API来实现IO。 查看相关的头文件,我们可以看到:

// a pointer of this type gets passed to SSL_read and SSL_write
typedef struct ssl_st SSL;
struct ssl_st {
    int version;
    const SSL_METHOD *method;
    /* used by SSL_read */
    BIO *rbio;
    /* used by SSL_write */
    BIO *wbio;
    ...
};


typedef struct bio_st BIO;
struct bio_st {
    const BIO_METHOD *method;
    BIO_callback_fn callback;
    BIO_callback_fn_ex callback_ex;
    char *cb_arg;
    int init;
    int shutdown;
    int flags;
    int retry_reason;
    int num; // <- This is the socket descriptor
    ...
};

Given a SSL * pointer - which is the first argument passed to SSL_read and SSL_write - we can retrieve the associated BIO values. Inside those BIO values, the num field holds the underlying socket descriptor. Here's some hacky BPF code to get the descriptor given a SSL *:

给定SSL *指针-这是传递给SSL_readSSL_write的第一个参数-我们可以检索关联的BIO值。 在这些BIO值内, num字段保存基础套接字描述符。 这是一些骇人的BPF代码,用于在给定SSL *获取描述符:

// this is equivalent to ssl->rbio
fn ssl_rbio(ssl: usize) -> Result<*const c_void, i32> {
    unsafe { bpf_probe_read((ssl + 16) as *const *const c_void) }
}


// this is equivalent to ssl->wbio
fn ssl_wbio(ssl: usize) -> Result<*const c_void, i32> {
    unsafe { bpf_probe_read((ssl + 24) as *const *const c_void) }
}


// this is equivalent to bio->num, which happens to be the socket descriptor
fn bio_fd(bio: *const c_void) -> Result<i32, i32> {
   unsafe { bpf_probe_read((bio as usize + 48) as *const i32) }
}

Note: For brevity here I computed the offsets of rbio, wbio and num manually looking at the headers, but I could have used cargo bpf bindgen to generate Rust bindings for struct SSL.

注意:为简便起见,我在头文件中手动计算了rbiowbionum的偏移量,但是我本可以使用cargo bpf bindgen生成struct SSL Rust绑定。

Let’s update the uprobes to send the file descriptors along with the data. Here’s the relevant changes:

让我们更新uprobes,以将文件描述符与数据一起发送。 这是相关的更改:

#[uprobe]
fn SSL_write_entry(regs: Registers) {
    ...


    // send the fd along with the buffer
    let fd = ssl_wbio(ssl).and_then(bio_fd).ok();
    output_buf(regs, fd, AccessMode::Write, buf, num as usize);
}


#[uretprobe]
fn SSL_read_ret(regs: Registers) {
    ...
        // send the fd along with the buffer
        let fd = ssl_rbio(*ssl).and_then(bio_fd).ok();
        output_buf(regs, fd, AccessMode::Read, *buf, num as usize);
    ...
}

Pretty much same as before, except now with every intercepted buffer we also send its corresponding socket descriptor.

与以前几乎相同,除了现在,每个侦听缓冲区都将发送其对应的套接字描述符。

将套接字描述符映射到地址 (Mapping socket descriptors to addresses)

Now we have socket descriptors, but how do we get IP addresses from them? Let’s take a look at the signature of connect():

现在我们有了套接字描述符,但是如何从它们获取IP地址呢? 让我们看一下connect()的签名:

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

The connect function is used to establish a connection from the given socket descriptor sockfd to the network address addr. Let's write a new uprobe that sends all the (sockfd, addr) pairs to user-space:

connect函数用于建立从给定套接字描述符sockfd到网络地址addr 。 让我们写一个新的长袍,将所有(sockfd, addr)对发送到用户空间:

use redbpf_probes::uprobe::prelude::*;


#[derive(Clone)]
pub struct Connection {
    pub fd: u64,
    pub addr: u32,
    pub port: u32,
}


// user-space will receive all the values we insert in this BPF map
#[map("connection")]
static mut connection_events: PerfMap<Connection> = PerfMap::with_max_entries(1024);


#[uprobe]
fn connect(regs: Registers) {
    let _ = do_connect(regs);
}


fn do_connect(regs: Registers) -> Option<()> {
    let fd = regs.parm1() as i32;
    let addr = regs.parm2() as *const sockaddr;


    // only record ipv4 connections
    if unsafe { &*addr }.sa_family()? as u32 != AF_INET {
        return None;
    }


    // and only for the zoom command
    if !comm_is_zoom() {
        return None;
    }


    let addr = unsafe { &*(addr as *const sockaddr_in) };
    let conn = Connection {
        fd: fd as u64,
        addr: addr.sin_addr()?.s_addr()?,
        port: u16::from_be(addr.sin_port()?) as u32,
    };


    unsafe {
        // send the value to user-space
        connection_events.insert(regs.ctx, &conn);
    }


    None
}


fn comm_is_zoom() -> bool {
    let comm = bpf_get_current_comm();
    let cmd = unsafe { core::slice::from_raw_parts(comm.as_ptr() as *const u8, 16) };
    return &cmd[..4] == b"zoom";
}

When zoom initiates a connection do_connect gets called, creates a Connection value holding socket descriptor and address of the connection, and sends it to the snuffy user-space process. There we store all the Connection values in a hash map keyed by socket descriptors. Then whenever we receive the data and socket descriptor of an intercepted SSL_read or SSL_write, we can lookup the IP address by indexing the connections hash map with the descriptor.

当zoom启动连接do_connect将调用do_connect ,创建一个包含套接字描述符和连接地址的Connection值,并将其发送到用户空间狭窄的进程。 在那里,我们将所有Connection存储在以套接字描述符为键的哈希映射中。 然后,每当收到截取的SSL_readSSL_write的数据和套接字描述符时,我们都可以通过使用描述符对连接哈希映射建立索引来查找IP地址

Since connect() is linked in dynamically via libpthread (part of glibc), to attach we can simply call:

由于connect()是通过libpthread(glibc的一部分)动态链接的,因此,我们可以简单地调用:

uprobe.attach_uprobe(Some("connect"), 0, "libpthread", pid)

With connection tracing in place the output now looks like this:

有了连接跟踪之后,输出现在如下所示:

$ sudo target/debug/snuffy --hex-dump --trace-connections --offsets zoom-offsets.toml
Write 577 bytes to 3.235.82.213:443
|504f5354 202f7265 6c656173 656e6f74| POST /releasenot 00000000
|65732048 5454502f 312e310d 0a486f73| es HTTP/1.1..Hos 00000010
|743a2075 73303477 65622e7a 6f6f6d2e| t: us04web.zoom. 00000020
|75730d0a 55736572 2d416765 6e743a20| us..User-Agent: 00000030
|4d6f7a69 6c6c612f 352e3020 285a4f4f| Mozilla/5.0 (ZOO 00000040
|4d2e4c69 6e757820 5562756e 74752031| M.Linux Ubuntu 1 00000050
...

Read 3088 bytes from 3.235.82.213:443
|48545450 2f312e31 20323030 200d0a44| HTTP/1.1 200 ..D 00000000
|6174653a 20467269 2c203034 20536570| ate: Fri, 04 Sep 00000010
|20323032 30203035 3a30383a 31322047| 2020 05:08:12 G 00000020
|4d540d0a 436f6e74 656e742d 54797065| MT..Content-Type 00000030
|3a206170 706c6963 6174696f 6e2f782d| : application/x- 00000040
|70726f74 6f627566 3b636861 72736574| protobuf;charset 00000050
...

As a final touch in order to make it even easier to read the output, we can see what domain names those IPs correspond to by instrumenting getaddrinfo(), the function that zoom uses to resolve domains to addresses:

最后,为了使读取输出变得更加容易,我们可以通过使用getaddrinfo()来查看这些IP对应的域名, getaddrinfo()是zoom用来将域解析为地址的函数:

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints, struct addrinfo **res);

getaddrinfo resolves the node domain name and populates the res out argument with the corresponding IP addresses. So we're going to create a new #[uretprobe] that once getaddrinfo() returns, builds a hash map from each IP in res to the domain node. Since conceptually this is exactly what we just did for connect(), i'm not going to show the code again. You can see it here.

getaddrinfo解析node域名,并使用相应的IP地址填充res out参数。 因此,我们将创建一个新的#[uretprobe] ,一旦getaddrinfo()返回,便会构建从res中的每个IP到域node的哈希映射。 因为从概念上讲这正是我们对connect()所做的,所以我不再显示代码。 你可以在这里看到它。

全部放在一起 (Putting it all together)

We’ve written uprobes for SSL_read, SSL_write, connect and getaddrinfo. With them we can see what DNS queries the zoom client does, what addresses it connects to and what encrypted data it sends and receives.

我们已经为SSL_readSSL_writeconnectgetaddrinfo编写了SSL_read 。 通过它们,我们可以看到缩放客户端执行的DNS查询,其连接的地址以及发送和接收的加密数据。

The final output looks like this:

最终输出如下所示:

$ sudo target/debug/snuffy --hex-dump --trace-connections --command /opt/zoom/zoom --offsets zoom-offsets.toml
Connected to 127.0.0.53:53
Resolved us04web.zoom.us to 3.235.69.6
Connected to us04web.zoom.us:443 (3.235.69.6:443)
Write 571 bytes to us04web.zoom.us:443 (3.235.69.6:443)
|504f5354 202f7265 6c656173 656e6f74| POST /releasenot 00000000
|65732048 5454502f 312e310d 0a486f73| es HTTP/1.1..Hos 00000010
|743a2075 73303477 65622e7a 6f6f6d2e| t: us04web.zoom. 00000020
|75730d0a 55736572 2d416765 6e743a20| us..User-Agent: 00000030
|4d6f7a69 6c6c612f 352e3020 285a4f4f| Mozilla/5.0 (ZOO 00000040
|4d2e4c69 6e757820 5562756e 74752031| M.Linux Ubuntu 1 00000050
...Read 3088 bytes from us04web.zoom.us:443 (3.235.69.6:443)
|48545450 2f312e31 20323030 200d0a44| HTTP/1.1 200 ..D 00000000
|6174653a 20467269 2c203034 20536570| ate: Fri, 04 Sep 00000010
|20323032 30203035 3a31313a 30352047| 2020 05:11:05 G 00000020
|4d540d0a 436f6e74 656e742d 54797065| MT..Content-Type 00000030
|3a206170 706c6963 6174696f 6e2f782d| : application/x- 00000040
|70726f74 6f627566 3b636861 72736574| protobuf;charset 00000050
...

There’s a lot interesting stuff that zoom does over the network (like XMPP 🤓), but analyzing that is left as an exercise to the reader.

缩放在网络上可以做很多有趣的事情(例如XMPP,),但是将其分析留给读者练习。

The final version of the uprobes is at https://github.com/alessandrod/snuffy/blob/master/snuffy-probes/src/snuffy/ and the user-space code that loads them is at https://github.com/alessandrod/snuffy/blob/master/src/. The code is slightly different from what I inlined above as after I first wrote the post, I made snuffy generic so it can now be used to instrument any program that uses OpenSSL. See the README for more info.

升级的最终版本位于https://github.com/alessandrod/snuffy/blob/master/snuffy-probes/src/snuffy/ ,加载它们的用户空间代码位于https://github.com / alessandrod / snuffy / blob / master / src / 。 该代码与我上面写的内容略有不同,因为我写了这篇文章后,将其变得通用,因此现在可以将其用于检测使用OpenSSL的任何程序。 有关更多信息,请参见自述文件。

I had a lot of fun writing this! I’m going to keep working on snuffy and add support for more libs in addition to OpenSSL. I’ve tested it with a few programs and if you find that it doesn’t work with any please do let me know. I’m not working full-time on redbpf anymore but I’m still contributing to the project, so if you find bugs writing your own uprobes please open an issue on github and I’ll take a look. And finally if your team is looking for Rust developers, please do get in touch!

我写这篇文章很有趣! 我将继续努力工作,并增加对OpenSSL之外的更多库的支持。 我已经通过一些程序对其进行了测试,如果您发现它不适用于任何程序,请告诉我。 我不再全职从事redbpf的工作,但我仍在为该项目做贡献,因此,如果您发现编写自己的uprobes的错误,请在github上打开一个问题,我来看一下。 最后,如果您的团队正在寻找Rust开发人员,请与我们取得联系!

Originally published at https://confused.ai.

最初发布于https://confused.ai

翻译自: https://medium.com/swlh/intercepting-zooms-encrypted-data-with-bpf-586e05bd0fc9

bpf filter

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值