截获TLS密钥——Windows Schannel - FreeBuf网络安全行业门户
简介
这篇文章是研究在终端上劫持进程来截获TLS密钥以用于解密的方式,主要是使用SChannel组件的Windows应用的TLS流量,如IIS,RDP,IE以及旧版的Edge,Outlook,Powershell及其他,不包括使用OpenSSL或NNS(基本上除了IE和旧版Edge的所有浏览器都在使用)。
SChannel
SChannel也被称为Secure Channel 23,是一个windows子系统,当windows应用程序想要做任何与TLS相关的事情时,比如与远程服务器建立一个加密会话,或接受来自客户端的TLS连接,就会使用Schannel,
从体系来看,SChannel实现了Security Support Provider Interface (SSPI)接口,是微软提供的SSP包之一。SSP包还包括CredSSP、Negotiate、NTLM、Kerberos和Digest24等等。
SChannel使用示例:
HTTPS连接
由IE,Edge,powershell的发起的
由IIS web server收到的
RDP连接
客户端的mstsc.exe
服务器上的终端服务(svchost.exe中的termsrv.dll)
到LDAP服务器的动态目录的LDAPS连接
当服务器HTTPS lisener开启时的部分WinRM(PS remoting)连接,PS remoting还支持使用TLS客户端证书的SSL authentication,启用时也通过schannel实现。
之前提到过其他浏览器,如Firefox和谷歌Chrome使用其他库来处理TLS,即NSS和OpenSSL,因此它们的流量超出了本文的范围。但是NSS和OpenSSL都是开源的,都有文档记录了导出secrets的方法;对于Firefox和Chrome,密钥导出是内置的,可以通过使用环境变量激活。
TLS1.2流量解密及临时密钥
这项研究并不依赖协议的漏洞或脆弱性,而是基于完全控制建立或接受连接的应用程序或操作系统的情况下,我们能够检索出所使用的任何密钥和secrets,来获得解密TLS流量所需的信息。
关于TLS的内部工作原理1的第2.2节已经总结的很好了。因此本文不会非常详细地进行介绍。快速过一下TLS1.2连接及加密中的关键概念:
临时密钥
每当创建一个TLS会话时,都会有许多密钥与此连接相关联。一些密钥可能用于加密,另一些用于消息验证。对于不同的方向(客户端到服务器和服务器到客户端)有不同的密钥。与服务器TLS证书密钥等长期密钥相对,这些密钥称为临时密钥以强调它们是短期的。
完全正向保密
所有密码套件可以根据是否支持完全正向保密(Perfect Forward Secrecy,PFS)25来进行分类。当使用非PFS密码套件时,任何加密连接都可以使用其捕获流量和服务器TLS私钥对进行解密。相反,对于PFS密码套件,你需要相关会话的临时密钥才能解密它。
主密钥 master key
形成临时密钥的过程有多个步骤。在TLS1.2中,开始时服务器和客户端一起生产一些关键素材,称之为Pre-Master Secret,再扩充到Master Secret,然后依次生成一组用于加密和认证的密钥和IV——write keys和MAC keys。MAC密钥只使用或非AEAD密码。
TLS session tickets
多个独立的TLS连接可以属于同一个TLS会话,因此无需每次都计算密钥。以前的方法是使用session ID,服务器向客户机发送session ID,然后由客户机在后续连接上使用。服务器应该存储与会话关联的密钥,因此此方法需要占用服务器上大量内存。因此后来提出了TLS session tickets(rfc5077),服务器向客户端发送一个加密的会话状态,使用只有服务器知道的密钥进行加密,然后客户端在下一个连接上发送回来,并由服务器解密。所有这意味着,尽管临时密钥本应在连接后销毁,但实际上它们可能在服务器和客户端的内存中都持久存在。有关TLS session tickets的安全性影响,请参见12
SSL keylog文件
为了解密一个TLS流量dump,需要一种方法来
对于TLS 1.2,提供此信息的标准方法是通过由OpenSSL和NSS23共同支持的ssl keylog文件,keylog文件的每一行由常量标签字符串、标识TLS会话的值和secrets的值组成。作为参考,可以在4中找到Wireshark的keylog解析例程
获取每个会话使用的secrets
将这些密钥和会话关联起来
client random 和 session id
TLS1.2的keylog文件支持会话的pre-master或master secrets。会话可以通过client random(在TLS握手期间由客户端发送的随机非加密值)或session id(由服务器发送的非加密值)来标识。TLS1.2的keylog文件的示例如下:
TLS1.3流量解密
上面提到的关于TLS1.2的许多内容也适用于TLS1.3。然而,secrets产生的方式有许多变化。
对于TLS1.2,我们有以下密钥生成方案:
在TLS1.2中,keylog文件格式要求你提供步骤(1)或步骤(2)的secrets。
对于TLS1.3,该方案已发展成以下版本(参见RFC844634的第93页):
TLS1.3 keylog文件还要求你提供步骤(2)的secrets。与TLS 1.2不同,每个TLS会话需要多个行,每一行提供一个特定的secrets,并通过client random将其绑定到一个TLS会话。你至少需要四个secrets:
* 客户端和服务器握手secret
* 客户端和服务器通信secret
一个keylog文件示例:
密钥隔离
Windows schannel API具有密钥隔离的概念(参见5),通过将各种机密数据存储在一个集中隔离的地方,从而使其更难以泄露。假设我们有一个进程(例如,terminal services client, mstsc)希望建立一个TLS连接,然而实际的TLS握手将在另一个进程中执行(即lsass.exe),握手期间生成的secrets(即TLS1.2的pre-master和master keys)永远不会离开lsass.exe的内存,也永远不会接触mstsc.exe。所有这些对应用程序来说都是透明的,它只使用来自schannel.dll的函数。
应用程序端的schannel .dll在幕后(参见1中的图2.6和2.7)使用ALPC连接到lsass端的schannel .dll。ALPC调用由加载到lsass.exe中的schannel .dll副本处理,然后使用一组加密API (CNG,6,主要在和中实现)来执行各种密钥相关的任务。
这种操作模式不是SChannel特有的,而是适用于所有实施SSPI的安全提供程序。当你调用37时,这个调用在LSA端的回调中处理,然后结果被传递到它在应用端上的。因此不需要在内存中保存凭证,windows应用程序也能够使用NTLM或Kerberos身份验证。
对我们来说,这意味着lsass .exe是个好地方,在这里可以提取任何启用SChannel的应用程序所使用的所有临时TLS密钥。我们需要hook密钥创建/操作路径,或者找到一种方法能够可靠地在内存中找到它们。我们还需要以某种方式将它们绑定到一个TLS会话来利用获得的密钥,最好采用Wireshark支持的方式(即session id或client random)。
目标
我们的目标是在完全控制连接的客户端或服务端上的应用程序和/或操作系统时,使用Wireshark解密SChannel TLS流量。与Jacob Kambic的论文1的问题陈述(从内存dump中提取密钥)相比,这种方法更为灵活,因为我们不仅可以使用内存扫描,还可以使用dbg和函数hook。其他关键要求如下:
不依赖会话恢复和其他机制,来防止密钥在连接关闭时被清除出内存;
尽可能不依赖于硬编码的偏移量,或其他特定于Windows和/或库的特定版本的东西;
可以从连接的客户端和服务端两端提取密钥;
在不需要管理员权限的情况下提取密钥的区域,即不触及lsass.exe的内存。类似于10中提出的方法。
获取TLS1.2密钥
对于TLS1.2,获取client random和密钥的配对,生成一个keylog行,就可以放进wireshark解密。
正常(非恢复)会话
SslGenerateMasterKey
在正常的(非恢复的)TLS1.2会话中调用的函数
参考SslGenerateMasterKey13官方文档:
如上所示,第四个参数被注释为_Out_(一种“header注释”类型14),意味着这个指针在函数调用结束后将被密钥地址填充。
获取master key
跟入这个*phMasterkey指针指向的地址会进入结构
在这个结构偏移0x04处包含一个非常重要的magic value(参见1第77页),在偏移0x10(x64情况,在x86情况下为0x0C)处包含另一个magic value为结构的指针(参见1第64-68页),实验环境内存(x64)如下
跟入pNcryptSslKey指针指向的地址000002a8`bcd81e70,进入结构,下称结构
参见1的第68页,master key本身位于SSL5结构偏移0x1C(x64,x86为0x14),大小为48 (0x30)字节:
获取client random
再回到13,可以看到关于pParameterList参数的注释:
客户端和服务端的random正是我们需要绑定密钥和会话的东西。和结构被记录在.NET框架的MS参考源文档,参阅17
跟入pParamterList指针导,在偏移0x04获得的数量,在偏移量0x08获得指向NCryptBuffer结构数组的指针:
跟入pBuffers指针
在pBuffers结构数组偏移0x04处是该缓冲区的数据类型:
BufferType=0x14=20时,为NCRYPTBUFFER_SSL_CLIENT_RANDOM
BufferType=0x15=21时,为NCRYPTBUFFER_SSL_SERVER_RANDOM
当BufferType为0x14时,在偏移0x08处是指向client random数据的指针
使用RSA握手的正常(非恢复)会话
SslImportMasterKey
上面的方式适用于使用PFS密码(即Diffie-Hellman密钥交换)进行密钥交换的场景,也适用于使用非PFS加密套件(RSA)的Windows客户端。但是当windows服务器接受非PFS加密套件的连接时并不会调用函数,用于9中的也不例外。这是因为基于RSA密钥交换的master key并不是在Diffie - Hellman交换期间计算,而是由客户机生成并发送到服务器,由服务器的公钥来加密(这也是为什么它不是前向安全性——只要有服务器私钥任何时候都能解密)。
在这种情况下,另一个函数26包含了我们要找的东西。给定一个私钥,由客户端发送master key(通过服务器的公钥加密),master key将被解密并将其写入:
获取client random
与一样,第四个参数包含了指向client random的指针,此处不再赘述
获取 master key
第三个参数包含了指master key的指针
使用TLS Session Hash的会话
当试图从的args获得client_random时,有时会发现它并没有没有在中传递。
官方文档26说
列表至少将包含包含客户端和服务端提供的random的缓冲区,但在某些情况下,它只包含类型为22和25的缓冲区。
类型22是NCRYPTBUFFER_SSL_HIGHEST_VERSION,没啥用。
类型25是NCRYPTBUFFER_SSL_SESSION_HASH。即使用session hash的情况。
Session Hash
在派生master key的过程中使用 client/server random会引发一些特定类型的滥用,因此发展出了一个名为TLS Session Hash和Extended Master Secret的TLS扩展(RFC 762727)。当启用这个扩展时,计算master secret将包含握手消息内容的hash(ClientHello, ServerHello),而不只是client/server random。不过Wireshark不支持使用Session Hash将密钥绑定到会话。
当然,当我们试图从服务器连接中获取密钥时,我们会得到Session Hash而不是client random。如果远程服务器支持并愿意使用,这也可用于客户端连接。因此我们需要寻找一种新的方式,在不基于现有的或TLS session id的方式来提取client random。
SslHashHandshake
当我们深入挖掘28的文档时我们会发现:
ncrypt.dll/SslHashHandshake函数是生成SSL握手hash的三个函数之一,三个函数包括:
1. 函数被调用时获得一个hash句柄
2. 函数可以被hash句柄调用任意次数,以向hash中添加数据
3. SslComputeFinishedHash函数被hash句柄调用时获得散列数据摘要
在使用RFC 7627 session hash方式时,TLS1.3和TLS1.2会调用这个函数。
第一次调用是在client hello,用msg_type==1和version==0x0303表示
需要注意的是,TLS1.2和TLS1.3的版本都是0x0303,这是TLS 1.3中的向后兼容性
重点关注第三个和第四个参数
pbInput [out]
包含需要被hash的数据的缓冲区地址
cbInput [in]
pbInput 缓冲区大小(bytes)
获取client random
首先跟踪ncrypt.dll/SslHashHandshake函数,跟进第3个参数pbInput指针获取待哈希数据的缓冲区地址(以下称buffer),跟进第4个参数cbInput获取缓冲区长度
跟入buffer地址,先读入1字节格式的msg_type从第4字节开始读入2字节的version,若msg_type为1并且version为0x0303时,从第6字节开始读入32字节的client random
获取TLS1.3密钥
根据RFC 844634第92-94页,在正常(非恢复)握手期间将生成以下内容:
两个 handshake traffic secrets
两个 application traffic secrets
一个 exporter master secret
一个 resumption master secret
每个traffic secrets用于生成一个write key和IV。一个SslExpandTrafficKeys调用后,会调用两次SslExpandWriteKey,分别计算客户端和服务端的secrets。这个调用会发生两次,一次用于 handshake traffic secrets,另一次用于 application traffic secrets。
从ghidra可以发现,和两个地方都调用了。
使用ghidra反编译,该函数包含一个调用,接着调用两次:
将生成的两个密钥放到了第4个和第5个参数中。之前提到过,该函数会调用两次,第一次生成handshake traffic secrets,第二次生成application traiffic secrets。
SslExpandTrafficKeys
如图,hook函数,跟入第4、第5个参数,两个都会进入一个结构(BDDD结构),与TLS1.2中一样,包含了指向密钥结构体地址的指针
跟入偏移0x10(x64,x86中为0x0C),进入了SSL3结构体,而不是TLS1.2中的TLS5结构体。见1第73页。
SSL3结构指向RUUU结构,而RUUU结构又指向MSKY结构,而MSKY结构最终指向我们要找的secrets。
偏移0x20(x64,x86下为0x1C)包含指向RUUU Bcrypt Key结构体的指针
阅读mimikatz源代码42可以找到RUUU结构
在偏移0x10(x64,x86为0x0C)出跟入指针进入到MKSY结构
在偏移0x10处包含密钥的长度,会根据密钥算法的不同而变化,在偏移0x18处包含我们需要的密钥。
第一调用产生的是HANDSHAKE_TRAFFIC_SECRET,第二次调用产生的是TRAFFIC_SECRET_0
SslExpandExporterMasterKey
对于TLS1.3,还要额外hook。虽然目前不确定wireshark目前是否需要使用它,但是openssl的keylog函数确实把它打印到keylog中了。
跟入第4、第5个参数进入BDDD结构,剩下的步骤和一样。
到最后获得EXPORT_SECRET
参考
[1] ?Jacob M. Kambic. Cunning With CNG: Soliciting Secrets from Schannel - Whitepaper from DEFCON 24, Slides from BlackHat USA 2016, “Extracting CNG TLS/SSL artifacts from LSASS memory” by Jacob M. Kambic
[2] ?MDN: NSS Key Log Format
[3] ?OpenSSL man page: SSL_CTX_set_keylog_callback
[4] ?Wireshark source code: SSLKEYLOG parsing, wireshark/packet-tls-utils.c
[5] ?Microsoft Docs: Key Storage and Retrieval
[6] ?Microsoft Docs: Cryptography API: Next Generation
[7] ?StackExchange: Decryping TLS packets between Windows 8 apps and Azure
[8] ?StackExchange: Is it possible to decrypt an SSL connection (short of bruteforcing)?
[9] ?Choi, H., & Lee, H. (2016) Extraction of TLS master secret key in windows. 2016 International Conference on Information and Communication Technology Convergence (ICTC). The paper is available on sci-hub if you search for its DOI.
[10] ?Microsoft TechNet Forums: Obtaining SSLKEYLOGFILE-like data from Edge et al (Schannel clients)
[11] ?GitHub - NytroRST/NetRipper: Smart traffic sniffing for penetration testers
[12] ?Filippo Valsorda: We need to talk about Session Tickets
[13] ?Microsoft Docs: SslGenerateMasterKey function (Sslprovider.h)
[14] ?Microsoft Docs: Header Annotations
[15] ?GitHub - droe/sslsplit: Transparent SSL/TLS interception
[16] ?Microsoft Docs: x64 software conventions
[17] ?MS .NET Reference Source: NCryptBuffer structure
[18] ?Windows SDK: NCRYPTBUFFER_SSL_* constans in ncrypt.h
[19] ?The blog of a gypsy engineer: How does TLS 1.3 protect against downgrade attacks?
[20] ?RFC 5246: The Transport Layer Security (TLS) Protocol Version 1.2
[21] ?Frida: A world-class dynamic instrumentation framework
[22] ?Microsoft Docs: x64 stack usage
[23] ?Microsoft Docs: Secure Channel
[24] ?Microsoft Docs: SSP Packages Provided by Microsoft
[25] ?Wikipedi@: Forward Secrecy (https://en.wikipedi@.org/wiki/Forward_secrecy把@改成a)
[26] ?Microsoft Docs: SslImportMasterKey function (Sslprovider.h)
[27] ?RFC 7627: Transport Layer Security (TLS) Session Hash and Extended Master Secret Extension
[28] ?Microsoft Docs: SslHashHandshake function (Sslprovider.h)
[30] ?Microsoft: News on TLS1.3 experimental support in Windows 10
[31] ?GitHub - microsoft/msquic: Testing instructions
[32] ?IETF draft: Using TLS to Secure QUIC
[33] ?GitHub - microsoft/msquic: SCHANNEL TLS Implementation for QUIC
[34] ?RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
[35] ?GitHub - dotnet/runtime: TLS1.3 does not work on Windows · Issue #1720
[36] ?Naughter blog: SSLWrappers + TLS v1.3 support
[37] ?Microsoft Docs: InitializeSecurityContextW function (sspi.h)
[38] ?Windows SDK: SEC_TRAFFIC_SECRETS definition in ntifs.h
[39] ?Windows SDK: SECBUFFER_TRAFFIC_SECRETS definition in sspi.h
[40] ?RFC 5705: Keying Material Exporters for Transport Layer Security (TLS)
[41] ?Peter Wu: sslkeylog.c for keylogging apps that use OpenSSL
[42] ?GitHub - gentilkiwi/mimikatz: kuhl_m_crypto_extractor.c - a TODO line which mentions MSKY magic tag)