OpenSSL是一个安全套接字层密码库,囊括主要的密码算法、常用密钥、证书封装管理功能及实现ssl协议。OpenSSL整个软件包大概可以分成三个主要的功能部分:SSL协议库libssl、应用程序命令工具以及密码算法库libcrypto。:
-
SSL协议库libssl:这是OpenSSL的核心部分,提供了SSL和TLS协议的实现。它允许应用程序建立安全的网络连接,并进行加密、解密、身份验证和完整性保护等操作。libssl还包含SSL握手过程的实现,用于建立安全连接之前的协商和认证。
-
应用程序命令工具:OpenSSL还提供了一组命令行工具,用于执行各种与加密和证书相关的操作。这些工具包括openssl命令,可用于生成和管理数字证书、执行加密和解密操作、计算哈希值以及进行其他与安全通信相关的任务。这些工具在开发、测试和管理安全系统时非常有用。
-
密码算法库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明文的步骤为三:
而本文的重点便是如何在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,因此只要知道:
因此很容易就有下面一段代码,不过值得注意的是: 有些版本的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
,其值形如:
在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的?
我们只要知道rbio
和num
的偏移值,就能在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,rbio
和num
的偏移值是不一样的。因此,当我们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