Featured image of post eBPF:如何获取OpenSSL的ssl结构体中的fd?

eBPF:如何获取OpenSSL的ssl结构体中的fd?

OpenSSL是一个安全套接字层密码库,囊括主要的密码算法、常用密钥、证书封装管理功能及实现ssl协议。OpenSSL整个软件包大概可以分成三个主要的功能部分:SSL协议库libssl、应用程序命令工具以及密码算法库libcrypto。:

  1. SSL协议库libssl:这是OpenSSL的核心部分,提供了SSL和TLS协议的实现。它允许应用程序建立安全的网络连接,并进行加密、解密、身份验证和完整性保护等操作。libssl还包含SSL握手过程的实现,用于建立安全连接之前的协商和认证。

  2. 应用程序命令工具:OpenSSL还提供了一组命令行工具,用于执行各种与加密和证书相关的操作。这些工具包括openssl命令,可用于生成和管理数字证书、执行加密和解密操作、计算哈希值以及进行其他与安全通信相关的任务。这些工具在开发、测试和管理安全系统时非常有用。

  3. 密码算法库libcrypto:libcrypto是OpenSSL中涵盖了许多主要的密码算法的部分。它包括对称加密算法(如AES、DES)、非对称加密算法(如RSA、DSA、ECC)以及哈希函数(如SHA-1、SHA-256)等。libcrypto还提供了各种密码学功能,如生成随机数、进行数字签名和验证、实现密钥派生函数等。

对于HTTPS,一种简单的理解可以为 HTTPS = HTTP + SSL/TLS。在C/C++的网络编程环境中OpenSSL常被用作为HTTPS的SSL/TLS部分的实现。

当我们在使用ebpf捕获HTTPS的明文时,一种方式就是对OpenSSL的libssl.so的SSL_read/SSL_write进行挂钩,获取其入参即原始明文。一次HTTPS请求的过程会调用若干次SSL_read/SSL_write,而为了还原同一应用在同一时间多个HTTPS请求的报文,势必需要找到一个索引将SSL_read/SSL_write调用关联起来,以便组织和拼接。

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

参考pixie,{pid+fd}作为那个索引最合适不过了。因此使用bpf捕获使用OpenSSL的应用的HTTPS明文的步骤为三:

  • 使用bpf挂钩SSL_read/SSL_write 函数,获取明文信息

  • 在上述两个函数调用过程中,找到pid和fd

  • 根据{pid+fd}组织还原出HTTP报文

而本文的重点便是如何在SSL_read/SSL_write调用过程中找到pid和fd.

ssl_st & bio_st

在ecapture和pixie代码注释和笔记中可知,sockfd其实就是ssl->rbio->num.

https://github.com/pixie-io/pixie/blob/main/README.md

https://github.com/pixie-io/pixie/src/stirling/source_connectors/socket_tracer/uprobe_symaddrs.cc

pid的获取自然简单,在bpf中很容易就很通过bpf_get_current_pid_tgid()获取得到当前的进程id和线程id,进程id即高32位,线程id即低32位,问题的重点在于如何获取fd。好在ecapture和pixie直接告诉我们ssl->rbio->num就是那个fd。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// openssl-3.0.3/ssl/ssl_local.h
struct ssl_st {
    /*
     * protocol version (one of SSL2_VERSION, SSL3_VERSION, TLS1_VERSION,
     * DTLS1_VERSION)
     */
    int version;
    /* SSLv3 */
    const SSL_METHOD *method;
    /*
     * There are 2 BIO's even though they are normally both the same.  This
     * is so data can be read and written to different handlers
     */
    /* used by SSL_read */
    BIO *rbio;
    /* used by SSL_write */
    BIO *wbio;
    /* used during session-id reuse to concatenate messages */
    BIO *bbio;
    // ... 省略一些内容 ...
};


// openssl-3.0.3/crypto/bio/bio_local.h
struct bio_st {
    OSSL_LIB_CTX *libctx;
    const BIO_METHOD *method;
    /* bio, mode, argp, argi, argl, ret */
#ifndef OPENSSL_NO_DEPRECATED_3_0
    BIO_callback_fn callback;
#endif
    BIO_callback_fn_ex callback_ex;
    char *cb_arg;               /* first argument for the callback */
    int init;
    int shutdown;
    int flags;                  /* extra storage */
    int retry_reason;
    int num;
    void *ptr;
    struct bio_st *next_bio;    /* used by filter BIOs */
    struct bio_st *prev_bio;    /* used by filter BIOs */
    CRYPTO_REF_COUNT references;
    uint64_t num_read;
    uint64_t num_write;
    CRYPTO_EX_DATA ex_data;
    CRYPTO_RWLOCK *lock;
};

Get sockfd

现在我们知道ssl->rbio->num就是sockfd,因此只要知道:

  • rbio在ssl_st中的偏移就能得到rbio(bio_st)

  • num在bio_st中的偏移就能得到sockfd

因此很容易就有下面一段代码,不过值得注意的是: 有些版本的OpenSSL定义ssl_st和bio_st的头文件有所不同,如遇报错还需根据实际情况调整。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stddef.h>
// #include <bio/bio_local.h>
// #include <ssl/ssl_lcl.h>

// #include <bio/bio_locl.h>     // version < 1.1.1
// #include <ssl/ssl_local.h>   // version < 1.1.1

int main(int argc, char **argv)
{
  if(argc != 2) {
    printf("usage: ./open_offset ${version} \n \
    ./open_offset 1.1.1k \n");
    return -1;
  }

  printf("\n#define kOpenSSL_%s_RBIO_offset 0x%lx \n", argv[1], offsetof(struct ssl_st, rbio));
  printf("#define kOpenSSL_%s_RBIO_num_offset  0x%lx \n\n", argv[1], offsetof(struct bio_st, num));

  return 0;
} 

以3.0.2版本为例,需要包含以下目录:

见ecapture的脚本:

https://github.com/gojue/ecapture/blob/master/utils/openssl_offset_3.0.sh

1
gcc -I include/ -I . -I crypto/ -I crypto/include offset.c -o out

因此,我们很容易就很得到3.0.2版本的偏移值.

1
2
#define kOpenSSL_3_0_2_RBIO_offset 0x10 
#define kOpenSSL_3_0_2_RBIO_num_offset  0x38

获取OpenSSL的版本号

依据pixie和ecapture的总结和实际验证可知,不同版本的OpenSSL的上述两个偏移值是不同的,所以我们需要知道OpenSSL的版本号才能确定偏移值。

查阅OpenSSL的3.0手册可发现,其提供了一个宏OPENSSL_VERSION_NUMBER用于获取对应的版本号,以及对应的函数接口OpenSSL_version_num().

1
2
3
4
5
/* from openssl/opensslv.h */
#define OPENSSL_VERSION_NUMBER 0xnnnnnnnnL

/* from openssl/crypto.h */
unsigned long OpenSSL_version_num();

在更早的版本(1.0)中,对应的函数原型为:

1
2
3
4
5
6
7
 #include <openssl/opensslv.h>
 #define OPENSSL_VERSION_NUMBER 0xnnnnnnnnnL
 #define OPENSSL_VERSION_TEXT "OpenSSL x.y.z xx XXX xxxx"

 #include <openssl/crypto.h>
 long SSLeay(void);
 const char *SSLeay_version(int t);

值得一提的是这两个函数接口是在libcrypto.so而非libssl.so。

1
2
3
4
5
6
7
8
 ldd /usr/bin/openssl
        linux-vdso.so.1 (0x00007fff49ce6000)
        libssl.so.3 => /lib/x86_64-linux-gnu/libssl.so.3 (0x00007f67d8a91000)
        libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007f67d8600000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f67d8200000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f67d8c3f000)
nm -D /lib/x86_64-linux-gnu/libcrypto.so.3|grep OpenSSL_version_num
00000000001b8070 T OpenSSL_version_num@@OPENSSL_3.0.0 

OpenSSL_version_num()函数返回unsigned long,其值形如:

1
2
805306400
0x30000020

在OpenSSL的文档中是这样描述的:OPENSSL_VERSION_NUMBER is a combination of the major, minor and patch version into a single integer 0xMNN00PP0L.

1
2
3
4
0x30000020
  MNN00PP0
  major.minor.patch
  3.0.2      

而在1.1.x和1.0.x的版本中, OPENSSL_VERSION_NUMBER的形式为:

1
 MNNFFPPS: major minor fix patch status

因此,在代码中可以这样实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 3.0.x
union open_ssl_3_0_x_version_num_t {
  struct __attribute__((packed)) {
  uint32_t unused1 : 4;
  uint32_t patch : 8;
  uint32_t unused2 : 8;
  uint32_t minor : 8;
  uint32_t major : 8;
  uint32_t unused : 64 - (4 + 4 * 8);
  };  // NOLINT(readability/braces) False claim that ';' is unnecessary.
  uint64_t packed;
};

// 1.1.x 1.0.x
union open_ssl_1_x_version_num_t {
  struct __attribute__((packed)) {
  uint32_t status : 4;
  uint32_t patch : 8;
  uint32_t fix : 8;
  uint32_t minor : 8;
  uint32_t major : 8;
  uint32_t unused : 64 - (4 + 4 * 8);
  };  // NOLINT(readability/braces) False claim that ';' is unnecessary.
  uint64_t packed;
};

一段示例代码为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// g++ version3_0.cpp -o openssl_offset3
// ./openssl_offset3 libssl3_3.0.2/usr/lib/x86_64-linux-gnu/libcrypto.so.3

// version_3_0.cpp
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>

typedef unsigned long uint64_t;
typedef unsigned int uint32_t;
union open_ssl_version_num_t {
  struct __attribute__((packed)) {
  uint32_t unused1 : 4;
  uint32_t patch : 8;
  uint32_t unused2 : 8;
  uint32_t minor : 8;
  uint32_t major : 8;
  uint32_t unused : 64 - (4 + 4 * 8);
  };  // NOLINT(readability/braces) False claim that ';' is unnecessary.
  uint64_t packed;
};

template <class T>
T* DLSymbolToFptr(void* handle, const char * symbol_name) {
  T* fptr = reinterpret_cast<T*>(dlsym(handle, symbol_name));

  const char* dlsym_error = dlerror();
  if (dlsym_error) {
    return nullptr;
  }

  return fptr;
}

// 0xMNN00PP0L
void get_openssl_version(const char* libpath) {

    void *handle = dlopen(libpath, RTLD_LAZY);
    if(handle == nullptr) {
        printf("open %s fail \n", libpath);
        return;
    }

    const char* version_fn_symbol = "OpenSSL_version_num";
    auto version_fn = DLSymbolToFptr<unsigned long()>(handle, version_fn_symbol);

    if(version_fn) {
      const uint64_t version = version_fn();
      open_ssl_version_num_t v;
      v.packed = version;
      printf("openssl version_num:%ld\n", version);
      printf("openssl version_num:0x%lx\n", version);
      printf("openssl version(major.minor.patch):%d.%d.%d\n", v.major, v.minor, v.patch);
    }
    dlclose(handle);
}

int main(int argc, char **argv)
{
  if(argc !=2) {
    printf("usage: openssl_version \"/lib/x86_64-linux-gnu/libcrypto.so.3\"\n");
    return -1;
  }  

  const char* libpath = argv[1];
  get_openssl_version(libpath);

  return 0;
}

在bpf中获取fd

回到最初的问题:我们在eBPF内核态hookSSL_read/SSL_write函数时是如何获取fd的?

我们只要知道rbionum的偏移值,就能在ebpf内核态中像如下操作获取fd:

1
2
3
4
5
6
7
 // pixie/src/stirling/source_connectors/socket_tracer/bcc_bpf/openssl_trace.c

 // Extract FD via ssl->rbio->num.
 const void** rbio_ptr_addr = ssl + symaddrs->SSL_rbio_offset;
 const void* rbio_ptr = *rbio_ptr_addr;
 const int* rbio_num_addr = rbio_ptr + symaddrs->RBIO_num_offset;
 const int rbio_num = *rbio_num_addr;

然而,对于不同版本的OpenSSL,rbionum的偏移值是不一样的。因此,当我们hook不同版本的OpenSSL时可能会因为偏移值不一致而获取不到正确的fd。

在pixie中可以找到解决这一问题的方法,那就是使用bpf_map做一个映射。很容易想到映射的key应该是libssl.so的路径或者OpenSSL的版本,value自然就是上面的两个偏移值。

但是,这是理论上的做法,现实是我们在bpf内核态中既取不到libssl.so的路径也取不到OpenSSL的版本。而pixie的做法是:

  • 在bpf内核态我们能够知道SSL_read/SSS_write被调用时的pid.

  • 在用户态,我们能够根据pid从/proc/{pid}/maps中得出libssl.so/libcrypto.so的路径.

  • 依据libcrypto.so我们便能够知道对应的版本号,从而知道对应的偏移值.

因此,bpf_map的key即pid,value为偏移值。用户态负责将pid依赖的libssl.so对应的偏移值更新至bpf_map中,内核态依据key=pid取出,便可计算得到sockfd.

链接

OpenSSL Manpages List

man3.1/man3/OPENSSL_VERSION_NUMBER.html

哦吼是一首歌。
Built with Hugo
Theme Stack designed by Jimmy