微软Windows KDC代理远程代码执行漏洞(CVE-2024-43639)技术分析

本文详细分析了Microsoft Windows KDC代理服务中存在的整数溢出漏洞(CVE-2024-43639),包括Kerberos协议工作机制、ASN.1编码细节、漏洞触发原理及内存破坏过程,并提供了检测指导和补丁信息。

CVE-2024-43639: Microsoft Windows KDC代理远程代码执行漏洞

2025年3月4日 | Trend Micro研究团队

本文节选自趋势科技漏洞研究服务的漏洞报告,由趋势科技研究团队的Simon Humbert和Guy Lederfein详细分析了Microsoft Windows密钥分发中心(KDC)代理中一个已修补的代码执行漏洞。该漏洞最初由Kunlun Lab的k0shl和Wei与Cyber KunLun共同发现。成功利用可能导致在目标服务的安全上下文中执行任意代码。以下是他们关于CVE-2024-43639的报告部分,略有修改。

漏洞概述

Microsoft Windows KDC代理存在整数溢出漏洞。该漏洞是由于缺少对Kerberos响应长度的检查。远程未经身份验证的攻击者可引导KDC代理将Kerberos请求转发到其控制的服务器,服务器随后发回特制的Kerberos响应。成功利用可能导致在目标服务的安全上下文中执行任意代码。

技术背景

Windows认证协议与Kerberos

Microsoft Windows操作系统实现了一套默认认证协议,包括Kerberos、NTLM、传输层安全/安全套接字层(TLS/SSL)和摘要协议,作为可扩展架构的一部分。对于Active Directory域内的认证,Windows使用Kerberos。

Kerberos是一种计算机网络认证协议,基于票据机制,允许节点在非安全网络上以安全方式相互证明身份。Kerberos基于对称密钥加密,需要可信第三方——密钥分发中心(KDC),与认证领域中的所有其他方共享密钥。客户端和服务与KDC交换Kerberos消息。Kerberos消息可通过UDP或TCP端口88传输。当通过TCP发送时,每个请求和响应前都有4个八位组网络字节序的消息长度。

KDC代理工作机制

Microsoft Windows Server操作系统实现Kerberos版本5认证协议。每个Active Directory域控制器运行Kerberos KDC实例,使用域的目录服务数据库作为安全账户数据库。要认证,客户端必须与域控制器有网络连接。虽然这对于组织网络内的机器通常成立,但对于使用远程连接的客户端可能不成立。为支持远程工作负载(特别是RDP网关和DirectAccess等服务),可通过KDC代理使用HTTPS代理Kerberos流量。

KDC代理是基于HTTP的服务器,实现Kerberos KDC代理协议(KKDCP)。客户端将Kerberos请求包装在KDC代理消息中,并将其放在HTTPS POST请求的正文中发送,其中Request-URI设置为/KdcProxy。KDC代理消息使用抽象语法记法一(ASN.1)定义。ASN.1是定义数据结构的标准接口描述语言(IDL),可跨平台序列化和反序列化。ASN.1的完整规范包括词法单元、分隔符、递归定义、本地数据类型、空白、产生式规则等可在此处找到。

KDC代理消息的结构如下:

1
2
3
4
5
KDC-PROXY-MESSAGE::= SEQUENCE {
    kerb-message [0] OCTET STRING,
    target-domain [1] KERB-REALM OPTIONAL,
    dclocator-hint [2] INTEGER OPTIONAL
}

其中:

  • kerb-message是Kerberos消息,包括4八位组消息长度前缀。
  • target-domain是DNS或NetBIOS域名,表示Kerberos消息必须发送到的领域(KDC代理请求需要target-domain,但KDC代理响应不使用)。
  • dclocator-hint是可选字段,包含用于查找域控制器的附加数据。

KDC代理消息使用可辨别编码规则(DER)编码。DER是一种类型-长度-值编码系统,每个DER编码字段具有以下结构:

1
2
3
4
5
6
名称                 描述
------------------- --------------------
标识符八位组        结构类型
长度八位组          内容长度
内容八位组          数据
内容结束八位组      (可选)

标识符八位组编码内容八位组的类型。通常,它由单个八位组组成,结构如下:

1
2
3
4
位         8    7    6    5    4    3    2    1
      +-----------+-----+--------------------------+
      |   类      | P/C |       标签号            |
      +-----------+-----+--------------------------+

类字段可以是通用(位:00)、应用特定(位:01)、上下文特定(位:10)或私有(位:11)。P/C字段指定字段是原始数据类型(位:0)(如INTEGER)还是构造数据类型(位:1)(即内容八位组包含其他原始或构造数据类型)。如果类字段是通用,则规范定义了几个标准标签号,如BOOLEAN(\x1)、INTEGER(\x2)、OCTET STRING(\x4)、UTF8STRING(\x0C)、SEQUENCE(\x10)、IA5STRING(\x16)、GeneralString(\x1B)等。对于非通用类,有编码大于30的标签号的规则。

在DER中,有两种编码长度八位组的方法。在短形式中,使用单个长度八位组,最高位设置为0,剩余7位表示内容八位组的数量。在长形式中,第一个长度八位组的最高位设置为1,剩余7位编码后续长度八位组的数量,这些八位组本身包含内容八位组的数量。长形式通常仅在必要时使用。所有多字节整数都是大端格式。

例如,编码后的KDC代理请求如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
30 82 01 07 # SEQUENCE标签和长度
   a0 81 f4 # EXPLICIT标签0和长度
       04 81 f1 00 00 00 ed 6a 81 ea 30 81 e7 a1 03 02 01 05 a2 03 02 01 0a a3 15
       30 13 30 11 a1 04 02 02 00 80 a2 09 04 07 30 05 a0 03 01 01 ff a4 81 c3 30
       81 c0 a0 07 03 05 00 40 81 00 10 a1 1d 30 1b a0 03 02 01 01 a1 14 30 12 1b
       10 44 45 53 4b 54 4f 50 2d 51 55 38 39 50 51 51 24 a2 0e 1b 0c 4b 44 43 50
       52 4f 58 59 2e 43 4f 4d a3 21 30 1f a0 03 02 01 02 a1 18 30 16 1b 06 6b 72
       62 74 67 74 1b 0c 4b 44 43 50 52 4f 58 59 2e 43 4f 4d a5 11 18 0f 32 30 33
       37 30 39 31 33 30 32 34 38 30 35 5a a6 11 18 0f 32 30 33 37 30 39 31 33 30
       32 34 38 30 35 5a a7 06 02 04 10 d3 7a 0f a8 16 30 14 02 01 12 02 01 17 02
       02 ff 7b 02 01 80 02 01 18 02 02 ff 79 a9 1d 30 1b 30 19 a0 03 02 01 14 a1
       12 04 10 44 45 53 4b 54 4f 50 2d 51 55 38 39 50 51 51 20 # OCTET STRING标签、长度和值(kerb-message)
   a1 0e # EXPLICIT标签1和长度
       1b 0c 4b 44 43 50 52 4f 58 59 2e 43 4f 4d # GeneralString标签、长度和值(target-domain)

缩进显示了构造数据类型和原始数据类型之间的关系。请注意,SEQUENCE的标签号是\x10,但SEQUENCE字段是构造数据类型,P/C字段设置为1。因此,SEQUENCE的标识符八位组是0x30。SEQUENCE项分配了从0到2的显式标签,编码时封装在EXPLICIT标签数据类型中。在EXPLICIT标签的标识符八位组中,类字段设置为上下文特定(位:10),P/C字段设置为1。最后,Kerberos领域编码为KerberosString,它是GeneralString的别名。

接收KDC代理请求后,KDC代理提取target-domain并定位该领域的域控制器。首先,KDC代理查询DNS SRV记录,名称为_kerberos._tcp.Default-First-Site-Name._sites.dc._msdcs.<target_domain>,并在需要时解析匹配的A记录。然后,KDC代理向结果IP地址集发送LDAP ping。LDAP ping是无连接LDAP(CLDAP)rootDSE搜索,用于Netlogon属性,验证域控制器的活跃性,并检查是否满足特定要求。域控制器返回小端字节字符串,编码NETLOGON_SAM_LOGON_RESPONSE_EX结构。

最后,KDC代理从KDC代理请求中提取kerb-message并将其转发到域控制器。请注意,KDC代理仅通过TCP转发Kerberos请求。另请注意,虽然只能在域加入的机器上运行,但KDC代理会代理任意域的Kerberos请求。当KDC代理从域控制器接收Kerberos响应时,将其包装在KDC代理消息中(仅包含kerb-message字段),并在HTTPS 200 OK响应的正文中返回给客户端。

漏洞详情

漏洞根源

Microsoft Windows KDC代理报告了整数溢出漏洞。该漏洞是由于缺少对Kerberos响应长度的检查。

将Kerberos请求发送到域控制器后,KDC代理从网络套接字读取4字节以获取Kerberos响应长度。然后,尝试读取所需字节以获取完整响应。多个函数参与读取Kerberos响应,所有函数都传递指向_KPS_IO结构的指针作为参数。_KPS_IO结构的大小为0x120字节,部分定义如下(本节所有结构定义通过逆向工程确定;大多数结构和字段名称由我们选择):

1
2
3
4
5
6
7
偏移量(字节)  长度(字节)  名称        描述
-------        -------      ----------- -------------
[... 截断字段 ...]
0x100          0x8          recvbuf     指向保存从套接字读取字节的缓冲区的指针
0x108          0x4          bytesread   到目前为止读取的字节数
0x10C          0x4          bytestoread 要从套接字读取的字节数
[... 截断字段 ...]

每当从套接字读取后续字节时,调用DLL文件kpssvc.dll中的函数KpsSocketRecvDataIoCompletion()。它检查是否已读取足够字节以获取完整响应,如果是,则调用函数KpsPackProxyResponse(),传递指向_KPS_IO结构的指针作为参数。

KpsPackProxyResponse()首先调用函数KpsCheckKerbResponse()验证Kerberos响应。值得注意的是,如果紧接消息长度前缀后的字节设置为0x7E0x6B,KpsCheckKerbResponse()验证响应是否正确构造的Kerberos消息。如果不是这种情况,它不执行任何验证并无错误返回。

KpsPackProxyResponse()局部变量包括类型为ASN1_KDC_PROXY_MSG的结构。ASN1_KDC_PROXY_MSG结构的大小为0x28字节,部分定义如下:

1
2
3
4
5
6
7
偏移量(字节)  长度(字节)  名称        描述
-------        -------      ------------ -------------
[... 截断字段 ...]
0x8            0x4          len         Kerberos响应长度
[... 截断字段 ...]
0x10           0x8          buf         指向保存Kerberos响应的缓冲区的指针
[... 截断字段 ...]

调用KpsCheckKerbResponse()后,KpsPackProxyResponse()初始化结构如下:ASN1_KDC_PROXY_MSG.buf设置为_KPS_IO.recvbuf,ASN1_KDC_PROXY_MSG.len设置为_KPS_IO.bytesread。然后,为将Kerberos响应包装在KDC代理响应中,它调用函数KpsDerPack(),传递ASN1_KDC_PROXY_MSG结构的地址作为参数。

从此开始,代码流在实现KDC代理服务器的DLL文件kpssvc.dll中的函数和Microsoft ASN.1库msasn1.dll中的函数之间交替。后者随后称为"MSASN.1"函数。

KpsDerPack()调用MSASN.1函数ASN1_CreateEncoder(),分配类型为ASN1_encoder的结构。ASN1_encoder结构的大小为0x50字节,部分定义如下:

1
2
3
4
5
6
7
8
偏移量(字节)  长度(字节)  名称        描述
-------        -------      ------------ -------------
[... 截断字段 ...]
0x10           0x8          buf         指向保存DER编码数据的缓冲区的指针
0x18           0x4          len         DER编码数据的长度
[... 截断字段 ...]
0x28           0x8          current     指向buf中当前写入位置的指针
[... 截断字段 ...]

KpsDerPack()然后调用MSASN.1函数ASN1_Encode(),传递指向ASN1_encoder和ASN1_KDC_PROXY_MSG结构的指针作为参数。ASN1_Encode()调用函数ASN1Enc_KDC_PROXY_MESSAGE()。ASN1Enc_KDC_PROXY_MESSAGE()调用MSASN.1函数ASN1BEREncExplicitTag(),传递指向ASN1_encoder结构的指针作为参数。ASN1BEREncExplicitTag()被调用两次,以编码SEQUENCE和EXPLICIT字段。

编码数据附加到ASN1_encoder.buf,缓冲区在编码字段时分配然后重新分配。为此,MSASN.1函数调用ASN1EncCheck(),传递所需大小作为参数。对于初始分配,ASN1EncCheck()通过调用Windows API函数LocalAlloc()在堆中分配空间。初始分配的大小至少为1,024字节。在后续调用期间,如果缓冲区无法容纳所需大小,ASN1EncCheck()重新分配缓冲区。在这种情况下,它将缓冲区的当前大小和所需大小相加,然后将结果作为参数传递给Windows API函数LocalReAlloc()。

ASN1BEREncExplicitTag()调用MSASN.1函数ASN1BEREncTag()。ASN1BEREncTag()编码标识符八位组,首先调用ASN1EncCheck()确保ASN1_encoder.buf有足够空间,然后在地址ASN1_encoder.current写入标识符八位组,最后递增ASN1_encoder.current。在此阶段,构造字段的长度未知,因为它取决于尚未编码的其他构造和原始字段的长度。因此,ASN1BEREncExplicitTag()通过调用ASN1EncCheck()并传递大小1,在ASN1_encoder.buf中为长度八位组保留一个字节,并将ASN1_encoder.current递增1。

ASN1Enc_KDC_PROXY_MESSAGE()然后调用MSASN.1函数ASN1DEREncOctetString()编码kerb-message OCTET STRING字段,传递指向ASN1_encoder结构的指针以及ASN1_KDC_PROXY_MSG.buf和ASN1_KDC_PROXY_MSG.len作为参数。ASN1DEREncOctetString()是函数ASN1BEREncCharString()的别名。ASN1BEREncCharString()首先调用ASN1BEREncTag()编码标识符八位组,然后调用ASN1BEREncLength(),传递ASN1_KDC_PROXY_MSG.len作为参数。

ASN1BEREncLength()首先计算编码长度八位组所需的字节数,添加ASN1_KDC_PROXY_MSG.len,然后将结果值作为参数传递给ASN1EncCheck()。这确保ASN1_encoder.buf有足够空间用于长度八位组和内容八位组。ASN1BEREncLength()然后在地址ASN1_encoder.current写入长度八位组,最后将ASN1_encoder.current递增长度八位组的大小。最后,ASN1BEREncCharString()调用Windows API函数memcpy()将ASN1_KDC_PROXY_MSG.len从地址ASN1_KDC_PROXY_MSG.buf复制到地址ASN1_encoder.current。

漏洞触发机制

然而,MSASN.1函数并不总是正确处理意外输入,特别是在处理大长度值时不会检查可能的整数溢出。此外,KpsSocketRecvDataIoCompletion()在调用KpsPackProxyResponse()之前不检查Kerberos响应的长度。最后,通过将紧接消息长度前缀后的字节设置为0x7E或0x6B以外的任何值,可以绕过KpsCheckKerbResponse()中的Kerberos响应验证。因此,恶意域控制器可能发送大的Kerberos响应,导致内存破坏错误。

整数溢出和内存破坏错误在编码kerb-message OCTET STRING字段时发生。此时,SEQUENCE和EXPLICIT字段均已编码,ASN1_encoder.buf指向大小为1,024的缓冲区,ASN1_encoder.current指向地址ASN1_encoder.buf + 4。KDC代理接受的Kerberos响应的最大大小为4,294,967,295。

如果发送长度从4,294,967,291到4,294,967,295(含)的Kerberos响应,ASN1BEREncLength()将发现需要5字节编码长度八位组,然后添加Kerberos响应的长度。但是,加法结果存储在4字节无符号变量中,该变量溢出。因此,作为参数传递给ASN1EncCheck()的大小非常小。ASN1EncCheck()不重新分配ASN1_encoder.buf缓冲区,随后当ASN1BEREncCharString()调用memcpy()时,发生堆缓冲区溢出。

或者,当发送长度从4,294,966,267到4,294,967,290(含)的Kerberos响应时,ASN1BEREncLength()调用ASN1EncCheck()。由于当前ASN1_encoder.buf缓冲区太小,ASN1EncCheck()继续重新分配它。它将缓冲区的当前大小(1,024)与Kerberos响应的长度相加。但是,加法结果存储在4字节无符号变量中,该变量溢出。因此,LocalReAlloc()实际上减小了缓冲区的大小。随后当ASN1BEREncCharString()调用memcpy()时,发生越界写入或堆缓冲区溢出。

作为一个有趣的边缘情况,可能将0作为新大小传递给LocalReAlloc()。LocalReAlloc()返回内存地址而不是错误,但是内存实际上未分配,尝试写入该地址时发生访问冲突。

远程未经身份验证的攻击者可引导KDC代理将Kerberos请求转发到其控制的服务器,服务器随后发回特制的Kerberos响应。成功利用可能导致在目标服务的安全上下文中执行任意代码。

注意:要达到易受攻击的代码,仅发送短Kerberos响应且前四个字节中具有大消息长度前缀值是不够的。Kerberos响应长度必须实际匹配前缀值。

检测指导

要检测利用此漏洞的攻击,检测设备必须监视和解析UDP端口389和TCP端口88上的流量。Kerberos消息可通过UDP或TCP端口88传输。但是,当通过TCP发送时,每个请求和响应前都有4个八位组网络字节序的消息长度。

检测设备必须检查Kerberos响应。请注意,KDC代理仅使用TCP端口88进行Kerberos流量(非UDP)。因此,设备不需要完全解析Kerberos响应。它只需要解析4字节消息长度前缀并能够隔离TCP流中的响应。如果Kerberos响应为0x80000000(2,147,483,648)字节或更长,流量应视为可疑,可能正在进行利用此漏洞的攻击。

注意:上述检测指导基于Kerberos V5 RFC的7.2.2节。它提到,在4八位组消息长度前缀中,高位必须设置为0。因此,根据RFC,通过TCP传输的Kerberos消息的最大长度为0x7FFFFFFF。

补丁相关问题

我们的研究表明,漏洞存在于ASN.1库中,但是Microsoft公告提到KDC代理服务器。此外,通过

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计