During several previous CTF competitions, I could do a bit of every category except pwn. So this winter holiday I decided to properly learn it. Turns out it’s really fun when you actually manage to pwn something.
This blog post will keep updating (hopefully).
What “pwn” means
Pwn basically means own—it started as a typo because p is next to o on the keyboard. In CTF context, “pwn” usually means gaining unauthorized control of a program by exploiting vulnerabilities (often memory corruption).
If you want to learn pwn, some assembly + basic systems knowledge helps a lot. If you don’t have those yet, you can learn them along the way.
Anyway, let’s start the journey of learning PWN by solving challenges (the most fun way, in my opinion).
Challenge: test_your_nc
File: https://files.buuoj.cn/files/643dec2806122d3fac330c9792d43b5d/test
To access remote services you typically use netcat. This challenge is very simple: after throwing it into IDA, it’s basically a program that spawns /bin/sh.
So you can just:
nc ip port- then
cat /flag
Challenge: rip
“RIP” = rest in peace… but in x86_64 it’s also the register that points to the next instruction.
File: https://files.buuoj.cn/files/96928d9cad0663625615b96e2970a30f/pwn1
Recon
In IDA, the program flow is essentially:
putsgetsputs(echo)puts- return
And there is an unused function fun() that calls system("/bin/sh").
So the goal is straightforward: overwrite the return address so that execution returns into fun().
Disassembly (key part)
I dumped assembly with:
1 | objdump -d pwn1 | vim - |
And the crucial part looks like:
1 | 0000000000401142 <main>: |
Why overflow works (offset calculation)
The call to gets() is the vulnerability: it reads until newline without bounds checking.
Stack layout (as suggested by the disassembly):
- buffer starts at
rbp - 0xF(i.e. 15 bytes belowrbp) - saved
rbpis at[rbp] - return address is at
[rbp + 0x8]
To reach the return address:
- 15 bytes to fill up to saved
rbp - then 8 bytes to overwrite saved
rbp
So the offset to the return address is:
15 + 8 = 23bytes- the 24th byte starts overwriting the return address
First exploit attempt (local)
1 | from pwn import * |
This crashed with:
SIGSEGV (Segmentation Fault)
Fix: stack alignment (x86_64)
On x86_64 SysV ABI, the stack pointer (RSP) should be 16-byte aligned at certain call boundaries. system() (and glibc internals) may use instructions like movaps that assume proper alignment. If you “return into” a function without the stack being aligned as expected, it can crash inside libc.
A common fix: add a ret gadget before the function address. That shifts rsp by 8 bytes and can restore alignment.
Working exploit (local + remote)
1 | from pwn import * |
Challenge: warmup_csaw_2016
File: https://files.buuoj.cn/files/dcd3c0cc561089a3969fba10d626ccf6/warmup_csaw_2016
Decompile main
In IDA, main() looks like:
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
And the target function is:
1 | int sub_40060D() |
Key point: the program prints the address of sub_40060D. That’s extremely helpful because with ASLR/PIE you often can’t hardcode function addresses.
Assembly near gets
1 | 400692: 48 8d 45 c0 lea -0x40(%rbp),%rax |
So the overflow distance is:
- 64 bytes buffer
- 8 bytes saved
rbp - => offset =
72
Then place:
- optional
retgadget for alignment - then the leaked function address
Exploit script
1 | from pwn import * |
Why also include ret_gadget here?
- You’re not reaching
system()via a normalcallchain. - Returning directly into the target can lead to an 8-byte stack misalignment.
- The extra
retoften fixes it by restoring the expected 16-byte alignment before libc code runs.