EternalChampion漏洞利用分析 | MSRC博客
注意: 此博客文章发布时间已超过一年,下文提供的信息可能已过时。
最近,一个名为ShadowBrokers的组织发布了多个针对旧版Windows各种协议的远程服务器漏洞利用程序。本文我们将详细研究EternalChampion漏洞利用,了解它利用了哪些漏洞、如何利用它们,以及Windows 10中的最新缓解措施如何阻断该漏洞利用。
未来几周,您将看到对其他多个“Eternal”漏洞利用的类似分析。
本文讨论的漏洞已在MS17-010中修复;已安装所有补丁的客户受到保护。
概述
EternalChampion是一个针对Windows XP至Windows 8的身份验证后SMB v1漏洞利用。正如我们将展示的,该漏洞利用依赖于自Windows 8以来已被缓解的技术,并在Windows 10中进一步缓解。
本分析将检查针对Windows 7 x64 RTM的漏洞利用。
漏洞
被利用的问题是SMBv1处理事务时的竞态条件。事务是一种可能跨越多个数据包的请求类型。
例如,如果请求太大无法放入单个服务器消息块(SMB),可以创建适当大小的事务,该事务将存储从多个SMB接收的数据。注意,多个SMB可以包含在单个数据包中,也可以分散在多个数据包中。
事务首先使用主请求创建。请求的数据被复制到事务中,事务被添加到连接的事务列表中。如果接收到所有预期数据,事务立即执行。可以发出辅助请求以向事务添加更多数据,直到事务接收到所有预期数据。
当主事务包含所有预期数据时存在问题:事务未标记为“正在执行”,但被放置在事务列表中,并发送到“ExecuteTransaction”函数。这意味着可能发生竞态:可以在ExecuteTransaction执行事务的同时(在单独线程上处理)发出修改事务的辅助请求。
ExecuteTransaction不认为这可能发生,并且在运行时修改事务数据,结合宽松的参数验证,可能导致访问违规。
漏洞利用
此漏洞以两种方式被利用:首先用于信息泄露,其次用于远程代码执行。
信息泄露
该漏洞首先被利用通过越界读取泄露池信息。为此,向服务器发送包含多个SMB的单个数据包。
在深入事件链之前,以下是与此漏洞利用相关的TRANSACTION数据结构:
1
2
3
4
5
6
7
8
9
10
11
|
srv!TRANSACTION
…
+0x010 Connection : Ptr64 _CONNECTION
…
+0x080 InData : Ptr64 Char // 接收的数据
+0x088 OutData : Ptr64 Char // 要发送的数据(与InData相同的缓冲区)
…
+0x0a4 DataCount : Uint4B // 到目前为止接收的数据
+0x0a8 TotalDataCount : Uint4B // 预期的总数据(InData的大小)
…
+0x0e3 Executing : UChar
|
发送到服务器的数据包包含三个主要部分:
- 用于函数NT RENAME的NT主事务请求。此请求包含所有预期数据并将立即执行。
- 旨在触发主请求竞态条件的NT辅助事务请求。
- 一系列旨在用srv!TRANSACTION结构喷洒池的主事务请求。目标是让其中一个结构直接分配在跟踪第一个NT主事务请求的srv!TRANSACTION结构之后。
创建的初始事务是针对NT RENAME请求的。此函数实际上是一个无操作,将请求读入其InData缓冲区,将DataCount设置为复制的字节数,随后将DataCount字节从其OutBuffer(指向InBuffer)返回给客户端(简单地回显客户端发送的内容)。
注意,因为主请求包含所有预期数据,DataCount == TotalDataCount。
辅助事务请求利用了先前描述的竞态条件和一些其他问题。
处理辅助事务时,参数验证过于宽松。该函数验证客户端发送的数据是否适合InData缓冲区,如果适合,则复制数据。注意,客户端告诉服务器要写入数据的InData中的偏移量以及要写入的字节数。这允许合法客户端使用多个SMB填充事务的所有部分。此验证是正常的。
事务还有一个DataCount字段,用于跟踪接收的字节数,以及TotalDataCount,即InData缓冲区的大小。当辅助事务请求处理程序将数据写入InData时,DataCount按写入的字节数递增。虽然有代码验证InData不会溢出,但没有检查确保DataCount不超过TotalDataCount。
通常这不是太大问题,因为辅助事务请求处理程序不会执行事务,除非DataCount == TotalDataCount。不幸的是,由于被攻击的竞态条件,另一个线程已经在此事务上执行ExecuteTransaction。
进行中的ExecuteTransaction最终从InData复制回DataCount字节,但DataCount已被辅助事务请求线程递增到大于InData的大小,导致越界数据被发送回客户端。此越界数据包含喷洒到池的TRANSACTION结构之一的内容。漏洞利用的目标是泄露TRANSACTION结构中包含的“Connection”指针。
远程代码执行
该漏洞以类似方式被利用用于远程代码执行,这次使用Transaction2/Transaction2Secondary请求而不是NT事务。
首先,创建一个包含shellcode的事务。此事务不用于触发漏洞;它只包含第二阶段有效负载。
接下来,发送包含多个SMB的数据包。数据包包含:
- 执行QueryPathInfo的Trans2主SMB
- 几个触发竞态条件的Trans2辅助SMB
Trans2主SMB再次包含所有预期事务数据并立即开始执行。最终函数SrvSmbQueryPathInformation执行。此函数有一行对攻击者特别有用的代码:
1
|
transaction->InData = (PVOID)&objectName;
|
这行代码使事务的InData指针指向堆栈变量。
由于先前描述的竞态,当这种情况发生时,其他线程同时处理Trans2辅助SMB。如前所述,辅助事务处理程序确保辅助事务请求的数据可以放入InData缓冲区,如果可以,则复制它。但由于竞态,InData指针现在指向主事务请求处理程序线程的堆栈(而不是预期的池缓冲区)。这允许攻击者将其数据直接写入另一个线程的堆栈。
1
2
3
4
5
6
7
|
if ( dataCount != 0 ) {
RtlMoveMemory(
transaction->InData + dataDisplacement,//位移攻击者控制
(PCHAR)header + dataOffset, // 请求中的攻击者数据
dataCount //dataCount攻击者控制
);
}
|
攻击者控制从InData复制数据的位移以及要复制的数据量。这允许他们精确覆盖存储在主事务请求处理程序线程堆栈上的返回地址。
有效负载
在此阶段,攻击者可以精确覆盖另一个线程上的返回地址(其他数据不会被篡改)。因为被覆盖的保存返回地址在另一个线程中,不能保证寄存器包含用于构建堆栈枢轴并最终执行ROP的有用信息。
在信息泄露阶段,攻击者泄露了CONNECTION对象的地址。此对象中的一个字段是名为ClientOSType的UNICODE_STRING,其中包含在初始连接握手期间设置的攻击者控制的UNICODE_STRING。攻击者还可以根据其初始握手控制UNICODE_STRING中Length和MaximumLength的值。
1
2
3
4
|
ntdll!UNICODE_STRING
+0x000 Length : Uint2B
+0x002 MaximumLength : Uint2B
+0x008 Buffer : Ptr64 Wchar
|
UNICODE_STRING在MaximumLength和Buffer字段之间有4个填充字节(在这种情况下设置为0),Buffer指向攻击者控制的数据。
有了这些知识,攻击者强制MaximumLength为0x15ff。攻击者覆盖返回地址为CONNECTION->ClientOSType.MaximumLength的地址。当这(与4个填充字节结合)被解释为指令时,我们得到:
1
|
fffffa83`0398e3ea ff1500000000 call qword ptr [fffffa83`0398e3f0]
|
这调用此指令后立即存储的地址。此指令后的地址是“Buffer”指针,因此这调用到攻击者控制的ClientOSType数据缓冲区。注意,因为这是调用指令,推送到堆栈的返回地址是“Buffer”字段的地址。
有关执行的shellcode的详细分析,请参见下文。简要说明:
阶段1(位置无关shellcode):
- 从堆栈弹出返回地址以获取CONNECTION->ClientOSType.Buffer的地址
- 使用此知识循环遍历存储在CONNECTION->TransactionList的TRANSACTION链表
- 查找以特殊标识符开头的事务,其中包含阶段2 shellcode
- 将阶段2 shellcode从事务复制到阶段1之后的数据缓冲区
- 执行它
阶段2:
- 执行额外的健全性检查
- 执行用户提供的(通过fuzzbunch)shellcode
缓解措施对漏洞利用的影响
如上所述,Windows 8 x64(及更新)平台不受此漏洞利用影响。主要原因是自Windows 8开始,大量内核虚拟地址空间(包括分页和非分页池)被设置为不可执行。因为此漏洞利用的每个阶段都依赖于执行池中的内存,所以编写的漏洞利用对Windows 8 x64及更高版本无效。有关Windows 8的NX更改的更多信息,请参见:http://media.blackhat.com/bh-us-12/Briefings/M_Miller/BH_US_12_Miller_Exploit_Mitigation_Slides.pdf。
出于应用程序兼容性原因,32位Windows仍然具有可执行分页池(以及其他区域),因此漏洞利用确实成功针对32位Windows 8。
除了Windows 8,Microsoft在Windows 10中改进了内核安全性,使此漏洞的利用更加困难:
- 虚拟机监控程序强制代码完整性(HVCI)防止未签名的内核页面被执行,进一步阻止了利用途径。
- Windows 10 1607和1703引入了完整的64位内核ASLR(1607中除HAL堆外的所有区域随机化,1703中HAL堆随机化),这将使将此漏洞适应更现代平台更加困难(需要更复杂的漏洞利用来绕过NX内存缓解措施)。
最后的话
我要感谢Swamy Shivaganga Nagaraju和Nicolas Joly(MSRC漏洞和缓解团队)以及Viktor Brange(WDG OSR团队)提供的笔记和帮助,以识别这些竞态条件。
Shellcode
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
;;;;;;;;;;;;;;;;;;;;
; 阶段0
;;;;;;;;;;;;;;;;;;;;
;
; 阶段0非常简单。它是存储在CONNECTION->ClientOSType的UNICODE_STRING结构。
; 当攻击者首次连接到SMB服务器时初始化此UNICODE_STRING。其Length、
; MaximumLength和Buffer内容受攻击者控制或影响。
; 攻击者使UNICODE_STRING的MaximumLength为0x15ff。
; MaximumLength和Buffer之间的4个填充字节为0。
; 当MaximumLength作为指令执行时,这4个字节导致相对调用到指令后立即存储的地址。
; 这导致Buffer指向的缓冲区的第一个字节作为阶段1 shellcode接下来执行。
;
fffffa83`0398e3ea ff1500000000 call qword ptr [fffffa83`0398e3f0]
;;;;;;;;;;;;;;;;;;;;
; 阶段0结束
;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;
; 阶段1
;;;;;;;;;;;;;;;;;;;;
;
; 阶段1 shellcode位于CONNECTION->ClientOSType.Buffer缓冲区中。
; 此shellcode的目的是从TRANSACTION复制自定义shellcode并执行它。
;
stage1_shellcode_entry:
;
; 通过弹出r8确定此SMB连接的连接结构位置。
; 这弹出堆栈的返回地址,该地址恰好是CONNECTION->Buffer的地址
; (因为这是它被调用的地方)。
; 知道CONNECTION结构的位置,攻击者可以搜索
; PAGED_CONNECTION->TransactionList查找具有魔术标识符的事务。
;
; 第一条指令跳转到嵌入在shellcode中的一些数据之后。
; 它们需要以跳转开始(而不是首先嵌入数据并调用指令开始的位置),
; 因为它们只能直接调用到此缓冲区,而不是其中的偏移量。
fffffa83`02dbb010 eb08 jmp fffffa83`02dbb01a
<< some_embedded_data >>
; 从堆栈弹出返回指令。
; 返回指令是:&(CONNECTION->ClientOSType.Buffer)
fffffa83`02dbb01a 4158 pop r8
fffffa83`02dbb01c 50 push rax
fffffa83`02dbb01d 50 push rax
; lea部分some_embedded_data
fffffa83`02dbb01e 488d0df4ffffff lea rcx,[fffffa83`02dbb019]
; 在我的测试中从1开始。似乎是测试是否需要从另一个缓冲区复制自定义shellcode
; 或者是否应使用默认shellcode。这可能是某种多线程保护。
fffffa83`02dbb025 8a01 mov al,byte ptr [rcx]
fffffa83`02dbb027 84c0 test al,al
; 跳转到stage2_shellcode_entry
fffffa83`02dbb029 745b je fffffa83`02dbb086
; *rcx最初为1,现在设置为0
fffffa83`02dbb02b c60100 mov byte ptr [rcx],0
fffffa83`02dbb02e 31c0 xor eax,eax
; 检索some_embedded_data的偏移量(在我的测试中为0x228)
fffffa83`02dbb030 668b05dbffffff mov ax,word ptr [fffffa83`02dbb012]
; 从r8指针(指向&(CONNECTION->ClientOSType.Buffer))减去0x228。
; r8现在是&(CONNECTION->PagedConnection)
fffffa83`02dbb037 4929c0 sub r8,rax
; rax = 对应于CONNECTION的PAGED_CONNECTION
fffffa83`02dbb03a 498b00 mov rax,qword ptr [r8]
; rax = 来自PAGED_CONNECTION->TransactionList.Flink的链表中的第一个TRANSACTION
fffffa83`02dbb03d 488b4010 mov rax,qword ptr [rax+10h]
fffffa83`02dbb041 4831d2 xor rdx,rdx
; 从some_embedded_data读取TRANSACTION->ConnectionListEntry和TRANSACTION->InData之间的偏移量。
; 在我的测试中这是0x58。
fffffa83`02dbb044 8a15ceffffff mov dl,byte ptr [fffffa83`02dbb018]
; 从some_embedded_data读取我将称为“AttackerTransactionMarker”的数据
fffffa83`02dbb04a 448b15c3ffffff mov r10d,dword ptr [fffffa83`02dbb014]
; 保存事务列表头,以便如果未找到匹配的TRANSACTION不会无限循环
fffffa83`02dbb051 4989c3 mov r11,rax
; 加载srv!TRANSACTION->InData的第一个QWORD
fffffa83`02dbb054 488b0c10 mov rcx,qword ptr [rax+rdx]
; 检查srv!TRANSACTION->InData的第一个DWORD == AttackerTransactionMarker
fffffa83`02dbb058 443b11 cmp r10d,dword ptr [rcx]
; 如果找到匹配,跳转到matching_transaction_found
fffffa83`02dbb05b 740a je fffffa83`02dbb067
; 否则,查看链表中的下一个TRANSACTION
fffffa83`02dbb05d 488b00 mov rax,qword ptr [rax]
; 将当前元素与列表头比较
fffffa83`02dbb060 4c39d8 cmp rax,r11
; 如果未找到匹配的TRANSACTION(列表结束),跳转到无限循环的指令(我猜是为了防止崩溃)
fffffa83`02dbb063 741f je fffffa83`02dbb084
; 循环回搜索下一个链接结构
fffffa83`02dbb065 ebed jmp fffffa83`02dbb054
matching_transaction_found:
;
; 如果找到匹配的事务,将事务中包含的有效负载复制到此缓冲区(阶段1有效负载之后)。执行它。
;
fffffa83`02dbb067 57 push rdi
fffffa83`02dbb068 56 push rsi
; rdi = 阶段1起始地址
fffffa83`02dbb069 488d3da0ffffff lea rdi,[fffffa83`02dbb010]
fffffa83`02dbb070 31c0 xor eax,eax
fffffa83`02dbb072 b076 mov al,76h
; dest = stage2_shellcode_entry
fffffa83`02dbb074 4801c7 add rdi,rax
; source == TRANSACTION->InData+0x8(需要跳过标记)
fffffa83`02dbb077 488d7108 lea rsi,[rcx+8]
; rep计数 == InData缓冲区的第二个DWORD(第一个DWORD是要查找的AttackerTransactionMarker)
fffffa83`02dbb07b 8b4904 mov ecx,dword ptr [rcx+4]
; 将第二阶段shellcode从TRANSACTION->InData+8复制到stage2_shellcode_entry
fffffa83`02dbb07e f3a4 rep movs byte ptr [rdi],byte ptr [rsi]
fffffa83`02dbb080 5e pop rsi
fffffa83`02dbb081 5f pop rdi
; 跳转到stage2_shellcode_entry
fffffa83`02dbb082 eb02 j
|