从 OpenSSL 应用程序中提取预主密钥

信息安全 tls openssl 线鲨 diffie-hellman
2021-08-28 04:38:39

考虑一个使用有错误的 OpenSSL 的应用程序。提供完整 SSL 会话的数据包捕获,以及应用程序和库的核心转储和调试符号。RSA 私钥也可用,但由于使用了 DHE 密码套件,因此无法使用 Wireshark 解密数据包捕获。

Thomas 在这篇文章中建议可以从 RAM 中提取密钥。如何为 OpenSSL 做到这一点?假设SSL数据结构的地址已知并且正在使用 TLS 1.0。

2个回答

注意:从 OpenSSL 1.1.1(未发布)开始,可以设置接收关键日志行的回调函数。有关详细信息,请参阅SSL_CTX_set_keylog_callback(3)手册。这可以像往常一样使用调试器或LD_PRELOAD钩子注入。如果您遇到较旧的 OpenSSL 版本,请继续阅读。

LD_PRELOAD有关Debian Stretch 上的 Apache 方法的演练,请参阅我从 apache2 中提取 openssl pre-master secret 的帖子。技术细节如下。


如果您只有 gdb 访问实时进程或核心转储,则可以从数据结构中读取数据。也可以使用插入库。

在下面的文本中,描述了使用 GDB 提取密钥的基本思想,然后给出了一个自动化脚本来执行捕获。

使用 GDB(基本思想)

基于这个 Stackoverflow 帖子,我能够构建一个函数,该函数可以打印适合 Wireshark 的关键日志文件的行。这在分析核心转储时特别有用。在 GDB 中,执行:

python
def read_as_hex(name, size):
    addr = gdb.parse_and_eval(name).address
    data = gdb.selected_inferior().read_memory(addr, size)
    return ''.join('%02X' % ord(x) for x in data)

def pm(ssl='s'):
    mk = read_as_hex('%s->session->master_key' % ssl, 48)
    cr = read_as_hex('%s->s3->client_random' % ssl, 32)
    print('CLIENT_RANDOM %s %s' % (cr, mk))
end

然后稍后,在您在堆栈中向上移动直到获得一个SSL结构后,调用该python pm()命令。例子:

(gdb) bt
#0  0x00007fba7d3623bd in read () at ../sysdeps/unix/syscall-template.S:81
#1  0x00007fba7b40572b in read (__nbytes=5, __buf=0x7fba5006cbc3, __fd=<optimized out>) at /usr/include/x86_64-linux-gnu/bits/unistd.h:44
#2  sock_read (b=0x7fba60191600, out=0x7fba5006cbc3 "\027\003\001\001\220T", outl=5) at bss_sock.c:142
#3  0x00007fba7b40374b in BIO_read (b=0x7fba60191600, out=0x7fba5006cbc3, outl=5) at bio_lib.c:212
#4  0x00007fba7b721a34 in ssl3_read_n (s=0x7fba60010a60, n=5, max=5, extend=<optimized out>) at s3_pkt.c:240
#5  0x00007fba7b722bf5 in ssl3_get_record (s=0x7fba60010a60) at s3_pkt.c:507
#6  ssl3_read_bytes (s=0x7fba60010a60, type=23, buf=0x7fba5c024e00 "Z", len=16384, peek=0) at s3_pkt.c:1011
#7  0x00007fba7b720054 in ssl3_read_internal (s=0x7fba60010a60, buf=0x7fba5c024e00, len=16384, peek=0) at s3_lib.c:4247
...
(gdb) frame
#4  0x00007fba7b721a34 in ssl3_read_n (s=0x7fba60010a60, n=5, max=5, extend=<optimized out>) at s3_pkt.c:240
240     in s3_pkt.c
(gdb) python pm()
CLIENT_RANDOM 9E7EFAC51DBFFF84FCB9...81796EBEA5B15E75FF71EBE 6ED2EA80181...

注意:不要忘记安装带有调试符号的 OpenSSL !在 Debian 衍生产品上,它会被命名为libssl1.0.0-dbg,Fedora/RHEL call itopenssl-debuginfo等。

使用 GDB(改进的自动化方法)

上面描述的基本思想适用于小型手动测试。对于密钥的批量提取(例如从 SSL 服务器),自动提取这些密钥会更好。

这是由 GDB 的 Python 脚本完成的:https ://git.lekensteyn.nl/peter/wireshark-notes/tree/src/sslkeylog.py (有关安装和使用说明,请参阅其标题)。它基本上是这样工作的:

  • 在可能出现新的预主密钥的几个函数上安装断点。
  • 等待函数完成并将这些密钥(如果以前不知道)写入文件(遵循SSLKEYLOGFILENSS 的格式)。

与 Wireshark 配对,您可以通过运行以下命令从远程服务器执行实时捕获:

# Start logging SSL keys to file premaster.txt. Be careful *not* to
# press Ctrl-C in gdb, these are passed to the application. Use
# kill -TERM $PID_OF_GDB (or -9 instead of -TERM if that did not work).
(server) SSLKEYLOGFILE=premaster.txt gdb -batch -ex skl-batch -p `pidof nginx`
# Read SSL keys from the remote server, flushing after each written line
(local) ssh user@host stdbuf -oL tailf premaster.txt > premaster.txt
# Capture from the remote side and immediately pass the pcap to Wireshark
(local) ssh user@host 'tcpdump -w - -U "tcp port 443"' |
    wireshark -k -i - -o ssl.keylog_file:premaster.txt

使用 LD_PRELOAD

SSL/TLS 只能在 SSL 握手步骤协商密钥。通过插入执行上述操作的 OpenSSL ( ) 的库接口,libssl.so您将能够读取预主密钥。

对于客户端,您需要插入SSL_connect. 对于服务器,您需要插入SSL_do_handshakeSSL_accept(取决于应用程序)。为了支持重新协商,您还必须拦截SSL_readSSL_write

一旦使用LD_PRELOAD库拦截了这些函数,您就可以使用dlsym(RTLD_NEXT, "SSL_...")从 SSL 库中查找“真实”符号。调用此函数,提取键并传递返回值。

此功能的实现可在https://git.lekensteyn.nl/peter/wireshark-notes/tree/src/sslkeylog.c获得。

请注意,不同的 OpenSSL 版本(1.0.2、1.1.0、1.1.1)彼此不兼容。如果您安装了多个 OpenSSL 版本并需要构建旧版本,则可能必须覆盖头文件和库路径:

make -B CFLAGS='-I/usr/include/openssl-1.0 -DOPENSSL_SONAME=\"libssl.so.1.0.0\"'

主要秘密在SSL->session->master_key.

或者,您可以按如下方式获取会话结构:

SSL_SESSION ss = SSL_get_session(SSL);

如上所述,结构中有一个master_key字段SSL_SESSION

或者,您可以使用SSL_SESSION_print()打印会话详细信息(包括 master_secret)SSL_SESSION_print_fp()

我认为一旦计算了 master_secret,就无法检索 pre_master_secret。