漏洞简介
2021年5月,奇安信 CERT 监测到了一枚影响广泛的高危漏洞(CVE-2021-31166),此漏洞被微软官方标记为 Wormable 和 Exploitation More Likely,这意味着漏洞利用可能性很大且有可能被恶意攻击者制作成可自我复制的蠕虫病毒进行大规模攻击。漏洞存在于 Windows 10 和 Windows Server 中的 HTTP 协议栈驱动程序 (http.sys)中,未授权的攻击者可构造带有 Accept-Encoding 的恶意请求包来攻击目标服务器。成功利用此漏洞的攻击者可在目标服务器上执行任意代码。
HTTP 协议栈常见于应用之间或设备之间通信,以及Internet Information Services (IIS)中,在我们的验证中,漏洞只影响 2020 年以后发布的并且启用了 IIS 服务器的 Windows 或 Windows Server 主机。因而,Windows Server 2019 以及以前的服务器版本不受此漏洞影响。漏洞影响版本如下:
Windows Server, version 20H2 (Server Core Installation)
Windows Server, version 2004 (Server Core installation)
Windows 10 Version 20H2 for x64-based Systems
Windows 10 Version 20H2 for ARM64-based Systems
Windows 10 Version 20H2 for 32-bit Systems
Windows 10 Version 2004 for x64-based Systems
Windows 10 Version 2004 for ARM64-based Systems
Windows 10 Version 2004 for 32-bit Systems
5月16日,该漏洞 POC 已于互联网上公开,经测试,该脚本可稳定触发 BSoD。复现截图如下:
漏洞分析
以下为崩溃现场,事故出现在 HTTP!UlFreeUnknownCodingList 函数中,程序自己执行了 int 29h。INT 29H 是微软从 Windows8 开始引入的一个新的中断,用来快速抛出异常,如果中断发生在 Ring0 中,操作系统会抛出一个KERNEL_SECURITY_CHECK_FAILURE (0x139) 的蓝屏,嗯,正好和上图对应。
0: kd> .trap 0xffffb30079e14480
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=0000000000000001 rbx=0000000000000000 rcx=0000000000000003
rdx=ffff9683fd276e20 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8041a12f677 rsp=ffffb30079e14610 rbp=ffffb30079e146c9
r8=ffff9683fd27518c r9=0000000000000004 r10=00000000ffffffff
r11=0000000000000092 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
HTTP!UlFreeUnknownCodingList+0x63:
fffff804`1a12f677 cd29 int 29h
0: kd> kb
*** Stack trace for last set context - .thread/.cxr resets it
# RetAddr : Args to Child : Call Site
00 fffff804`1a0e6c05 : ffffca8f`882efe26 ffffb300`00000001 ffffb300`79e14694 00000000`00000000 : HTTP!UlFreeUnknownCodingList+0x63
01 fffff804`1a0bd201 : ffffa20e`c94c5eb7 ffffb300`79e14819 00000000`00000010 fffff804`1a0bd1b0 : HTTP!UlpParseAcceptEncoding+0x299c5
02 fffff804`1a0993d8 : fffff804`1a0646e0 ffffb300`79e14819 ffffca8f`8831a010 00000000`00000000 : HTTP!UlAcceptEncodingHeaderHandler+0x51
03 fffff804`1a098ab7 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : HTTP!UlParseHeader+0x218
04 fffff804`19ff4c5f : ffffca8f`8748b968 ffffca8f`8748b750 ffffb300`79e14a79 00000000`00000000 : HTTP!UlParseHttp+0xac7
05 fffff804`19ff490a : fffff804`19ff4760 ffffca8f`882efd50 00000000`00000000 00000000`00000001 : HTTP!UlpParseNextRequest+0x1ff
06 fffff804`1a0948c2 : fffff804`19ff4760 fffff804`19ff4760 00000000`00000001 00000000`00000000 : HTTP!UlpHandleRequest+0x1aa
07 fffff804`14d20e85 : ffffca8f`8748b7d0 fffff804`1a065f80 00000000`000006f8 00000000`00000000 : HTTP!UlpThreadPoolWorker+0x112
08 fffff804`14e06498 : fffff804`13425180 ffffca8f`86438040 fffff804`14d20e30 00000000`00000000 : nt!PspSystemThreadStartup+0x55
09 00000000`00000000 : ffffb300`79e15000 ffffb300`79e0f000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x28
以下为漏洞触发前处理的相关伪代码(UlFreeUnknownCodingList),大致可以得出结论:在依次释放链表中节点的过程中,会进行一些校验,如果通过检验,就将其从链表中摘除并释放;如果校验不通过就调用 __fastfail(3u) 抛出异常:
//HTTP!UlFreeUnknownCodingList
while ( 1 )
{
UnknownCodingListHead_Flink = *UnknownCodingListHead;
if ( *UnknownCodingListHead == UnknownCodingListHead )// 如果 UnknownCodingList 不为空
break;
v3 = *UnknownCodingListHead_Flink; // v3 = UnknownCodingListHead->Flink->Flink
if ( *(_QWORD **)(*UnknownCodingListHead_Flink + 8i64) != UnknownCodingListHead_Flink// UnknownCodingListHead->Flink->Flink->Blink != UnknownCodingListHead->Flink
|| (v4 = (_QWORD *)UnknownCodingListHead_Flink[1], (_QWORD *)*v4 != UnknownCodingListHead_Flink) )// UnknownCodingListHead->Flink->Blink->Flink != UnknownCodingListHead->Flink
{
__fastfail(3u);
}
*v4 = v3; // UnknownCodingListHead->Flink->Blink->Flink = UnknownCodingListHead->Flink->Flink
*(_QWORD *)(v3 + 8) = v4; // UnknownCodingListHead->Flink->Flink->Blink = UnknownCodingListHead->Flink->Blink
ExFreePoolWithTag(UnknownCodingListHead_Flink - 2, 0);// 释放 UnknownCodingListHead->Flink 节点所在堆
}
由于调用 UlFreeUnknownCodingList 的是 UlpParseAcceptEncoding 函数,所以来分析一下这个函数。它会循环解析 Accept-Encoding 中的编码方式,首先通过 UlpParseContentCoding 函数依次获取每个编码信息,如果是支持的编码方式,则设置相应位。如果是没有被识别出的其他编码方式,则调用 ExAllocatePoolWithTagPriority 函数申请 0x20 大小的堆来存放这些信息,然后将它们链到 UnknownCodingList 中,其结构如下:
*(_QWORD *)new_node = pUnknownCodingStr; //指向当前的 Unknown 编码名称
*((_WORD *)new_node + 4) = len; //当前编码名称长度
*((_WORD *)new_node + 5) = v26; //置位相关
p_new_node = new_node + 0x10; //LIST_ENTRY 结构
//以 POC 中的数据为例
0: kd> dq ffffb300`79e146a0 l2 //UnknownCodingListHead
ffffb300`79e146a0 ffff9683`fd276a00 ffff9683`fd276af0
0: kd> db ffff9683`fd276a00-10 l20 //node1
ffff9683`fd2769f0 14 fe 2e 88 8f ca ff ff-06 00 e8 03 8e 32 a2 32 .............2.2
ffff9683`fd276a00 30 6a 27 fd 83 96 ff ff-a0 46 e1 79 00 b3 ff ff 0j'......F.y....
0: kd> db poi(ffff9683`fd276a00-10) l6
ffffca8f`882efe14 64 6f 61 72 2d 65 doar-e
0: kd> db ffff9683`fd276a30-10 l20 //node2
ffff9683`fd276a20 1c fe 2e 88 8f ca ff ff-03 00 e8 03 00 b3 ff ff ................
ffff9683`fd276a30 f0 6a 27 fd 83 96 ff ff-00 6a 27 fd 83 96 ff ff .j'......j'.....
0: kd> db poi(ffff9683`fd276a30-10) l3
ffffca8f`882efe1c 66 74 77 ftw
0: kd> db ffff9683`fd276af0-10 l20 //node3
ffff9683`fd276ae0 21 fe 2e 88 8f ca ff ff-03 00 e8 03 83 96 ff ff !...............
ffff9683`fd276af0 a0 46 e1 79 00 b3 ff ff-30 6a 27 fd 83 96 ff ff .F.y....0j'.....
0: kd> db poi(ffff9683`fd276af0-10) l3
ffffca8f`882efe21 69 6d 6f imo
获取完所有编码后,如果 UnknownCodingList 不为空且满足一些判断条件,会进行以下操作:将 UnknownCodingListHead 取下,然后把 RequestUnknownCodingListHead 链进去。但没有 UnknownCodingListHead 将清空,通过 UnknownCodingListHead 还是可以访问 UnknownCodingListHead->Flink 和 UnknownCodingListHead->Blink。
//*************如果满足以下任意条件,则会调用 __fastfail(3u)*************
//校验 UnknownCodingListHead 前后关系
UnknownCodingListHead.Flink->Blink != &UnknownCodingListHead
UnknownCodingListHead.Blink->Flink != &UnknownCodingListHead
//将 UnknownCodingListHead 取下
UnknownCodingListHead.Blink->Flink = UnknownCodingListHead.Flink
UnknownCodingListHead.Flink->Blink = UnknownCodingListHead.Blink
//判断 RequestUnknownCodingListHead 是否为空
Request->UnknownCodingListHead.Flink->Blink != &Request->UnknownCodingListHead
Request->UnknownCodingListHead.Blink->Flink != &Request->UnknownCodingListHead
//校验 UnknownCodingListHead->Flink 前后关系
UnknownCodingListHead->Flink->Flink->Blink != UnknownCodingListHead->Flink
UnknownCodingListHead->Blink->Flink != UnknownCodingListHead->Flink
//***********判断条件通过之后,将 RequestUnknownCodingListHead 链进去***********
Request->UnknownCodingListHead.Blink->Flink = UnknownCodingListHead->Flink
Request->UnknownCodingListHead.Blink = UnknownCodingListHead.Flink->Blink
UnknownCodingListHead.Flink->Blink->Flink = &Request->UnknownCodingListHead
UnknownCodingListHead.Flink->Blink = Request->UnknownCodingListHead.Blink
上述流程的相关代码如下,在这之后,会判断 v5 是否小于 0,如果是的话就跳到 LABEL_30,LABEL_30 处会判断 UnknownCodingList 是否为空,如果不为空就调用 UlFreeUnknownCodingList 函数,且参数为 &UnknownCodingListHead。由于 UnknownCodingListHead 已经不在循环双链表中,按照这个流程对 UnknownCodingList 进行释放会出现问题。下面先分析这种流程如何触发,后面再进行验证。
// UlpParseAcceptEncoding
if ( UnknownCodingListHead.Flink != &UnknownCodingListHead )// 如果这个链表不是空的,就进入以下循环
{
UnknownCodingListHead_Blink = UnknownCodingListHead.Blink;
if ( UnknownCodingListHead.Flink->Blink != &UnknownCodingListHead
|| UnknownCodingListHead.Blink->Flink != &UnknownCodingListHead
|| (UnknownCodingListHead.Blink->Flink = UnknownCodingListHead.Flink,
RequestUnknownCodingListHeadAddr = (__int64)&Request->UnknownCodingListHead,
UnknownCodingListHead_Flink->Blink = UnknownCodingListHead_Blink,// UnknownCodingListHead->Flink->Blink = UnknownCodingListHead->Blink
RequestUnknownCodingListHead_Blink = (#289 **)Request->UnknownCodingListHead.Blink,
Request->UnknownCodingListHead.Flink->Blink != &Request->UnknownCodingListHead)
|| *RequestUnknownCodingListHead_Blink != (#289 *)RequestUnknownCodingListHeadAddr// RequestUnknownCodingListHead->Blink->Flink != RequestUnknownCodingListHeadAddr
|| UnknownCodingListHead_Flink->Flink->Blink != UnknownCodingListHead_Flink// UnknownCodingListHead->Flink->Flink->Blink != UnknownCodingListHead->Flink
|| UnknownCodingListHead_Blink->Flink != UnknownCodingListHead_Flink )// UnknownCodingListHead->Blink->Flink != UnknownCodingListHead->Flink
{
LABEL_44:
__fastfail(3u);
}
*RequestUnknownCodingListHead_Blink = (#289 *)UnknownCodingListHead_Flink;// RequestUnknownCodingListHead->Blink->Flink = UnknownCodingListHead->Flink
Request->UnknownCodingListHead.Blink = UnknownCodingListHead_Flink->Blink;// RequestUnknownCodingListHead->Blink = UnknownCodingListHead->Flink->Blink
UnknownCodingListHead_Flink->Blink->Flink = (_LIST_ENTRY *)RequestUnknownCodingListHeadAddr;// UnknownCodingListHead->Flink->Blink->Flink = RequestUnknownCodingListHeadAddr
UnknownCodingNums = v28; // 3
UnknownCodingListHead_Flink->Blink = (_LIST_ENTRY *)RequestUnknownCodingListHead_Blink;// UnknownCodingListHead->Flink->Blink = RequestUnknownCodingListHead->Blink
Request->UnknownCodingNums = UnknownCodingNums;
LABEL_43:
UnknownCodingListHead_Flink = UnknownCodingListHead.Flink;
}
if ( v5 < 0 )
goto LABEL_30;
}
else
{
LABEL_22:
Request->field_97A = 1;
}
LABEL_30:
if ( UnknownCodingListHead_Flink != &UnknownCodingListHead )// 如果 UnknownCodingList 不为空,就 Free
UlFreeUnknownCodingList((__int64)&UnknownCodingListHead);
v5 为 UlpParseContentCoding 函数的返回值, UlpParseContentCoding 函数执行完成后,会对其返回值进行判断。如下所示,如果 v5 小于 0 且不等于 0xC0000225,就跳到 LABEL_43(如上所示),这样会跳过将 RequestUnknownCodingListHead 链进链表的过程,在 UlFreeUnknownCodingList 函数中释放 UnknownCodingList 也不会出现问题。如果进了下面第二个条件判断,就意味着 UnknownCodingList 是空的 (v9 为 0),这样也不会去调用 UlFreeUnknownCodingList 函数。而当 v5 为 0xC0000225 时,会进行上面的链表操作,并且在最后判断 v5 < 0 后,跳到 LABEL_30 去执行 UlFreeUnknownCodingList 函数。
// UlpParseAcceptEncoding
while ( 1 )
{
v26 = 0x3E8;
v5 = UlpParseContentCoding(
p_start_addr,
temp_len, // 剩余长度
(int *)&unknown,
(unsigned __int8 **)&buffer_str,
&len, // 当前编码长度
(__int64)&v26, // 0x3E8
(__int64 *)&temp_addr);
if ( v5 < 0 )
{
if ( v5 != 0xC0000225 )
goto LABEL_43; //跳过链入 RequestCodingListHead
if ( temp_addr == end_addr && !v9 ) // 如果有编码被处理过,v9为1
{
v5 = 0;
goto LABEL_22; //跳过链入 RequestCodingListHead
}
}
UlpParseContentCoding 函数会先将数据中的空格跳过去,因而后面出现的 buffer_str 指向第一个不是空格的字符。
//UlpParseContentCoding
v9 = a2;
for ( i = v9; v9; v50 = v9 )
{
if ( !(HttpChars[*buffer_str] & 0x20) ) // (HttpChars+*buffer_str*4)&0x20
break;
++buffer_str;
i = --v9;
}
在 UlpParseContentCoding 函数中存在以下两个流程,可以将返回值设置为 0xC0000225:
公开的 POC 中使用的上面的第二种路径,如果第一个不是空格的字符就是 0x2c(,) 的话,就返回 0xC0000225。
POC 中将 Accept-Encoding 设置为 'doar-e, ftw, imo, ,'
分析过漏洞触发条件后,它也可能是'doar-e, ftw, imo,,' 或 'doar-e, ftw, imo, ,'等等( Accept-Encoding 中需要至少1个 Unknown Coding)
并且由于还有另一种路径,也还有会其他可能
下面是验证,在 HTTP!UlFreeUnknownCodingList 函数处下断点重新运行,查看此时的 UnknownCodingList,如下,此时的 UnknownCodingListHead 为 0xffffb30079e146a0,其 Flink 指向 0xffff9683fd276dc0,Blink 指向 0xffff9683fd276a90,而 0xffff9683fd276dc0 的 Blink 指向 0xffffca8f8831a9a0,0xffff9683fd276a90 的 Flink 也指向 0xffffca8f8831a9a0,这个 0xffffca8f8831a9a0 就是前面分析的 RequestUnknownCodingListHead 的地址,目前来看,它和 UnknownCodingListHead 中的内容一样,但显然 Unknown 编号列表们是和 RequestUnknownCodingListHead 组成了循环双链表:
0: kd> g
Breakpoint 0 hit
HTTP!UlFreeUnknownCodingList:
fffff804`1a12f614 4053 push rbx
0: kd> dq rcx l2
ffffb300`79e146a0 ffff9683`fd276dc0 ffff9683`fd276a90
0: kd> dq ffff9683`fd276dc0 l2
ffff9683`fd276dc0 ffff9683`fd276a60 ffffca8f`8831a9a0
0: kd> dq ffff9683`fd276a60 l2
ffff9683`fd276a60 ffff9683`fd276a90 ffff9683`fd276dc0
0: kd> dq ffff9683`fd276a90 l2
ffff9683`fd276a90 ffffca8f`8831a9a0 ffff9683`fd276a60
0: kd> dq ffffca8f`8831a9a0 l2
ffffca8f`8831a9a0 ffff9683`fd276dc0 ffff9683`fd276a90
以下为循环中的大致操作:如果 UnknownCodingList 不为空,则判断 UnknownCodingList->Flink->Flink->Blink 是否等于 UnknownCodingList->Flink,UnknownCodingList->Flink->Blink->Flink 是否等于 UnknownCodingList->Flink。如果相等,则将 UnknownCodingList->Flink 摘除下来并释放。此例中释放了 0xffff9683fd276db0,即 0xffff9683fd276dc0(UnknownCodingList->Flink)- 0x10,释放该节点后的循环链表如下。
1: kd> dq ffffca8f`8831a9a0 l2
ffffca8f`8831a9a0 ffff9683`fd276a60 ffff9683`fd276a90
1: kd> dq ffff9683`fd276a60 l2
ffff9683`fd276a60 ffff9683`fd276a90 ffffca8f`8831a9a0
1: kd> dq ffff9683`fd276a90 l2
ffff9683`fd276a90 ffffca8f`8831a9a0 ffff9683`fd276a60
但 UnknownCodingList 中还是引用了 0xffff9683fd276dc0。在下一轮中,还是会判断 UnknownCodingList->Flink->Flink->Blink 是否等于 UnknownCodingList->Flink。UnknownCodingList->Flink 为 0xffff9683fd276dc0,0xffff9683fd276dc0 在上一轮已经被释放了,因而在它的 “Flink” 0xffff9683fd276a60 中已经修改了 Blink 指针为 0xffffca8f8831a9a0,不等于 0xffff9683fd276dc0,因而校验失败,程序主动调用 __fastfail(3u) 触发 BSoD。
1: kd> dq rbx l2 //UnknownCodingList
ffffb300`79e146a0 ffff9683`fd276dc0 ffff9683`fd276a90
1: kd> dq ffff9683`fd276dc0 l2
ffff9683`fd276dc0 ffff9683`fd276a60 ffffca8f`8831a9a0
1: kd> p
HTTP!UlFreeUnknownCodingList+0x34:
fffff804`1a12f648 48394a08 cmp qword ptr [rdx+8],rcx
1: kd> dq ffff9683`fd276a60 l2
ffff9683`fd276a60 ffff9683`fd276a90 ffffca8f`8831a9a0
1: kd> p
HTTP!UlFreeUnknownCodingList+0x38:
fffff804`1a12f64c 7524 jne HTTP!UlFreeUnknownCodingList+0x5e (fffff804`1a12f672)
1: kd>
HTTP!UlFreeUnknownCodingList+0x5e:
fffff804`1a12f672 b903000000 mov ecx,3
1: kd>
HTTP!UlFreeUnknownCodingList+0x63:
fffff804`1a12f677 cd29 int 29h
补丁分析
补丁后的程序添加了如下代码,将取下的 UnknownCodingList 链表清空,避免了错误引用的问题。
漏洞总结
HTTP协议栈(http.sys)在处理 Accept-Encoding 时存在远程代码执行漏洞,此漏洞 CVSSv3 评分为9.8 分,微软指出该漏洞可蠕虫式传播。未授权的攻击者可构造带有 Accept-Encoding 的恶意请求包来攻击目标服务器。成功利用此漏洞的攻击者可在目标服务器上执行任意代码。目前,网络上已公开此漏洞相关的 POC,经测试,该 POC 可稳定触发 BSoD。UlpParseAcceptEncoding 函数以及 UlFreeUnknownCodingList 函数在处理 Accept-Encoding 字段的过程中可触发释放后重用。经分析,目标缓冲区释放后就会被重用,难以控制释放后的空间,并且后面存在校验,校验不通过则主动执行 int 29h 触发蓝屏崩溃,因而该漏洞利用难度较大。
参考链接
https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-31166
https://github.com/0vercl0k/CVE-2021-31166
https://mp.weixin.qq.com/s/V2dbHDIPGCHpWo-m--s_tQ
https://mp.weixin.qq.com/s/i4CacMrLCi4FRCKPYY65wg
本文始发于微信公众号(奇安信 CERT):POC已公开,CVE-2021-31166 HTTP 协议栈远程代码执行漏洞研究
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论