考虑一个使用有错误的 OpenSSL 的应用程序。提供完整 SSL 会话的数据包捕获,以及应用程序和库的核心转储和调试符号。RSA 私钥也可用,但由于使用了 DHE 密码套件,因此无法使用 Wireshark 解密数据包捕获。
Thomas 在这篇文章中建议可以从 RAM 中提取密钥。如何为 OpenSSL 做到这一点?假设SSL
数据结构的地址已知并且正在使用 TLS 1.0。
考虑一个使用有错误的 OpenSSL 的应用程序。提供完整 SSL 会话的数据包捕获,以及应用程序和库的核心转储和调试符号。RSA 私钥也可用,但由于使用了 DHE 密码套件,因此无法使用 Wireshark 解密数据包捕获。
Thomas 在这篇文章中建议可以从 RAM 中提取密钥。如何为 OpenSSL 做到这一点?假设SSL
数据结构的地址已知并且正在使用 TLS 1.0。
注意:从 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 提取密钥的基本思想,然后给出了一个自动化脚本来执行捕获。
基于这个 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
等。
上面描述的基本思想适用于小型手动测试。对于密钥的批量提取(例如从 SSL 服务器),自动提取这些密钥会更好。
这是由 GDB 的 Python 脚本完成的:https ://git.lekensteyn.nl/peter/wireshark-notes/tree/src/sslkeylog.py (有关安装和使用说明,请参阅其标题)。它基本上是这样工作的:
SSLKEYLOGFILE
NSS 的格式)。与 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
SSL/TLS 只能在 SSL 握手步骤协商密钥。通过插入执行上述操作的 OpenSSL ( ) 的库接口,libssl.so
您将能够读取预主密钥。
对于客户端,您需要插入SSL_connect
. 对于服务器,您需要插入SSL_do_handshake
或SSL_accept
(取决于应用程序)。为了支持重新协商,您还必须拦截SSL_read
和SSL_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。