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 );
Copy 参考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 ;
};
Copy 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 ;
}
Copy 以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
Copy 因此,我们很容易就很得到3.0.2版本的偏移值.
1
2
#define kOpenSSL_3_0_2_RBIO_offset 0x10
#define kOpenSSL_3_0_2_RBIO_num_offset 0x38
Copy 获取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 ();
Copy 在更早的版本(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 );
Copy 值得一提的是这两个函数接口是在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
Copy 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
Copy 而在1.1.x和1.0.x的版本中, OPENSSL_VERSION_NUMBER 的形式为:
1
MNNFFPPS: major minor fix patch status
Copy 因此,在代码中可以这样实现:
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 ;
};
Copy 一段示例代码为:
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 ;
}
Copy 在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 ;
Copy 然而,对于不同版本的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