Basic Exploitation (Linux with Mitigations Disabled)
Basic Exploitation (Linux with Mitigations Disabled)
Learning Environment:
CPU arch (default): amd64 (x86-64)
OS: Ubuntu 24.04 LTS (Linux)
Compiler Flags: Disable protections (
-fno-stack-protector,-no-pie,-z execstackfor ret2shellcode labs,/GS-)ASLR: Keep enabled system-wide; disable per-process (
setarch -R) or in GDB (set disable-randomization on) for deterministic labsFocus: Pure exploitation techniques without bypass complexity
Day 1: Environment Setup and Stack Overflow Fundamentals
Goal: Set up exploitation lab and understand stack buffer overflow mechanics.
Activities:
Reading:
"Hacking: The Art of Exploitation" 2nd edition, by Jon Erickson - Chapter 0x300: "EXPLOITATION"
Smashing The Stack For Fun And Profit - Classic paper
Online Resources:
Tool Setup:
Ubuntu VM with protections disabled
pwntools, pwndbg, ROPgadget
Exercise:
Compile and exploit first vulnerable program
Overwrite return address to execute shellcode
Context: QNAP Stack Overflow (CVE-2024-27130)
Recall the QNAP QTS Stack Overflow from Week 1? That was a classic stack buffer overflow caused by
strcpywithout bounds checking—exactly what we'll be exploiting today.While modern systems have mitigations (which we'll disable for now), the underlying mechanic remains the same: overwriting the return address to hijack control flow.
Deliverables
Environment:
~/check_env.shpasses and you recorded its outputBinary:
vuln1built and verified withchecksecPrimitive proof: RIP control demonstrated (controlled crash address)
Exploit:
exploit1.py(or equivalent) spawns a shell reliablyNotes: brief writeup covering offset, return target, and payload layout
Setting Up the Lab Environment
Ubuntu VM Configuration:
[!IMPORTANT] ASLR Policy: Keep ASLR enabled system-wide for security. Disable only per-process for labs. Never disable ASLR globally on a machine connected to the internet.
[!NOTE] Ubuntu 24.04:
Uses glibc 2.39 with full safe-linking and removed hooks
Requires
python3-venvfor pip package installation (PEP 668)For classic heap techniques, consider using Docker with older Ubuntu
GDB Enhancement Options
Verify Setup:
pwntools Essentials
Before diving into exploitation, master these pwntools fundamentals. The ELF() class is your primary interface for analyzing binaries—use it throughout this course.
ELF() Basics:
Context Configuration (set BEFORE any pwntools operations):
Understanding the Stack (AMD64)
Stack Layout (x86-64 / AMD64):
AMD64 vs x86 Key Differences:
Register prefix
E (EAX, EBP, ESP)
R (RAX, RBP, RSP)
Instruction pointer
EIP
RIP
Address size
4 bytes
8 bytes
Arguments
All on stack
RDI, RSI, RDX, RCX, R8, R9
Return value
EAX
RAX
Syscall instruction
int 0x80
syscall
Stack alignment
4-byte
16-byte before call
System V AMD64 ABI Calling Convention:
Function Call Mechanics (AMD64):
Buffer Overflow Visualization (AMD64):
First Vulnerable Program
vuln1.c:
Compile without protections (AMD64):
Finding the Offset
Step 1: Cause a Crash:
Step 2: Find Exact Offset (using pattern):
In GDB with pwndbg (AMD64):
Verify Offset (AMD64):
In GDB (AMD64):
Working Exploit for vuln1 (stdin-based)
Writing Simple Shellcode
Linux AMD64 Shellcode Basics:
Syscall Convention (AMD64):
syscallinstruction triggers syscall (NOTint 0x80!)rax= syscall numberrdi, rsi, rdx, r10, r8, r9= arguments (note: r10 instead of rcx)Return value in
rax
execve("/bin/sh", NULL, NULL) Shellcode (AMD64):
Assemble and Extract Bytes (AMD64):
Result (23 bytes AMD64 shellcode):
Test Shellcode Standalone (AMD64):
Complete Exploit
exploit1.py (AMD64):
Better Approach: Using jmp rsp Gadget (AMD64) (More Reliable):
[!TIP] Hardcoding stack addresses is fragile—addresses vary between GDB and normal execution, different terminals, environment sizes, etc. A
jmp rsporcall rspgadget provides a stable return target since RSP points to our controlled data afterret.
Debugging Your Exploit
When your exploit doesn't work (it won't on the first try!), use these systematic debugging techniques.
Method 1: GDB Attach with pwntools
Usage:
Example Debug Session Output:
After hitting the breakpoint at ret, you'll see something like:
Interpreting the output:
Buffer address:
0x7ffd11d25cd0(first A's at offset 0x8 from 0x7ffd11d25cc8)Our A's (
0x4141414141414141) fill 64 bytes of buffer + 8 bytes of saved RBPReturn address at
0x7ffd11d25d18contains our value0xdeadbeefcafeOffset confirmed: 72 bytes (64 buffer + 8 saved RBP) before return address
Method 2: Step-by-Step GDB Analysis (AMD64)
Common Debugging Scenarios:
Crash at wrong address
Offset incorrect
cyclic -l <crash_addr>
Crash at correct addr but no shell
Shellcode bad or wrong location
x/20i <shellcode_addr>
"Illegal instruction"
Bad shellcode or architecture mismatch
Check context.binary
Segfault in libc
Stack alignment (AMD64!)
Add extra ret gadget
Works in GDB, fails outside
Environment variable difference
setarch -R ./vuln
The GDB vs Real Execution Problem:
The stack layout differs between GDB and normal execution due to environment variables:
Essential pwndbg Commands for Exploit Development (AMD64):
Debugging Checklist (Use Before Asking for Help!):
Environment Hygiene (Critical for Exploit Development)
Stack addresses differ between environments due to variables like LINES, COLUMNS, PWD, TERM, and program name length. This is the #1 cause of "works in GDB, fails outside" issues.
The Problem:
Solution: Force Consistent Environment:
The "It Works on My Machine" Checklist
Buffering Hell
Local
process()typically uses PTY (unbuffered).Remote
ncor sockets are often fully buffered or line-buffered.Always use
p.recvuntil(b'prompt')before sending. Never rely onsleep()unless absolutely necessary.
IO Handling
p.recv()is dangerous—it returns some data, not all data.p.clean()removes unread data (useful before sending payload).p.sendline()adds\n. Ensure target expects\nand not just raw bytes.
Environment Variables
Remote servers have different
envvars than your GDB session.This shifts stack addresses by +/- 0x100 bytes.
Never rely on exact stack addresses (hardcoded
0x7ffffff...).Always use leaks (libc/stack) and relative offsets, or NOP sleds.
GDB Environment Matching:
pwntools Best Practice for Learning:
Verification:
[!WARNING] Always use
env -iorenv={}when developing exploits with hardcoded addresses! Once your exploit works, convert to using leaks for portability.
Practical Exercise
Exercise: Exploit vuln1 to get a shell
Steps:
Compile Target (AMD64):
Find Offset (AMD64 uses 8-byte patterns):
Find Stack Address (or jmp rsp gadget):
Build Exploit (AMD64):
NOP sled (50 bytes)
AMD64 shellcode (use
asm(shellcraft.amd64.linux.sh()))Padding to offset (72 bytes typical)
Return address (8 bytes - use
p64())
Test Exploit:
Success Criteria:
Successfully overflow return address
Shellcode executes
Shell obtained
Can run commands (id, whoami, ls)
Week 4 Deliverable Exercise: From Minimized Crash to Exploit
Use one of your Week 4 deliverables (reproduction fidelity + minimized crash) and turn it into a working Day 1 exploit.
Inputs from Week 4:
A minimized crash input (file or stdin blob)
An exact reproduction command (argv + input path)
Your reproduction notes (OS/libc, environment variables, ASLR settings)
Task:
Reproduce the crash reliably (>= 9/10) using the exact same input path and environment.
Generate a core dump and confirm you control RIP.
Replace your crashing bytes with a cyclic pattern and recover the exact offset.
Build an exploit that spawns a shell (ret2shellcode for Day 1).
Success Criteria:
Offset derived from the crash (not guessed)
Exploit works multiple times in a row
Week 2 Integration Exercise: AFL++ Crash -> Minimize -> Exploit
Reuse the Week 2 AFL++ workflow, but target a Week 5 binary.
Goal: produce a fuzzer-found crashing input for a Day 1 style target, minimize it, then turn it into a working exploit.
Task:
Build the target with AFL++ instrumentation.
Run
afl-fuzzuntil you get a crash.Minimize the crashing input with
afl-tmin.Use the minimized crash to recover the offset and build a working exploit.
Success Criteria:
A fuzzer-generated input crashes the program
afl-tminproduces a smaller reproducer that still crashesYou can transform the minimized input into a working exploit
Common Issues and Solutions
Issue 1: Segfault at wrong address
Issue 2: Shellcode not executing
Issue 3: Stack address wrong
Common Mistakes to Avoid
Forgetting endianness: x86/x64 is little-endian.
0xdeadbeefbecomes\xef\xbe\xad\xdeWrong architecture: AMD64 shellcode won't work in 32-bit process (and vice versa!)
Using p32() on AMD64: Always use
p64()for 64-bit binariesBad characters: Null bytes (
\x00) terminate strings instrcpy. Other common bad chars:\x0a(newline),\x0d(carriage return),\x20(space)Stack alignment: AMD64 requires 16-byte alignment before
callfor some libc functions (add extraretgadget if crashes in libc)Environment differences: Stack addresses differ between GDB and normal execution (due to environment variables)
Exercise: Removing Null Bytes from Shellcode
Why This Matters: String functions like strcpy(), gets(), and scanf("%s") stop at null bytes. If your shellcode contains \x00, it gets truncated.
Common Null Byte Sources:
mov rax, 0
48 c7 c0 00 00 00 00
Immediate 0
xor eax, eax → 31 c0
mov rdi, 0x68732f6e69622f
Contains nulls
String padding
Use push/mov sequences
mov al, 59
b0 3b
No nulls!
OK as-is
syscall
0f 05
No nulls
OK as-is
Task: Convert this null-containing shellcode to null-free:
Solution: Null-Free Version:
pwntools Verification:
Null-Byte Elimination Techniques:
mov rax, 0
xor eax, eax
Zero-extends to 64-bit
mov rdi, 0
xor edi, edi
Zero-extends to 64-bit
mov rax, small_num
xor eax, eax; mov al, num
For values < 256
mov rax, imm64
push imm32; pop rax
If value fits in 32-bit
String in .data
push string onto stack
Build string at runtime
jmp label with null offset
Use short jumps or restructure
Relative offset issue
Identifying Bad Characters:
[!TIP] Use pwntools
shellcraftwith encoders for complex shellcode:
Debugging Tips:
Key Takeaways
Stack overflows overwrite return address: Control RIP (AMD64) / EIP (x86)
Finding offset is critical: Use cyclic patterns (8-byte on AMD64!)
NOP sleds improve reliability: Don't need exact address
Stack must be executable:
-z execstackrequired for shellcodePer-process ASLR disable: Use
setarch -Ror GDB, NOT system-wideAMD64 uses 8-byte addresses: Always use
p64()notp32()
Discussion Questions
Why does a NOP sled improve exploit reliability?
What happens if ASLR is enabled but other protections are disabled?
How would you modify your exploit if the vulnerable function used
read()instead ofgets()?What are the limitations of this technique in real-world scenarios?
Why is AMD64 stack alignment (16-byte) important for exploit reliability?
Day 2: Return-to-libc and Introduction to ROP
Goal: Learn code-reuse exploitation when stack is not executable.
Activities:
Reading:
"The Shellcoder's Handbook" 2nd edition - Chapter 2: "Stack Overflows"
The Geometry of Innocent Flesh on the Bone - Original ROP paper (Shacham, 2007)
Online Resources:
ROP Emporium - Practice challenges (start with ret2win)
Tool Setup:
Same VM as Day 1
Enable NX bit (disable execstack)
Exercise:
Exploit with ret2libc technique
Find gadgets manually before using ROPgadget
Build and debug a ROP chain
Context: Router Exploitation (MIPS/ARM)
Return-to-libc is a staple in embedded device exploitation (routers, IoT).
Many of these devices run on MIPS or ARM architectures where stack execution is often disabled or cache coherency issues make shellcode unreliable.
Attackers frequently use
system()orexecve()from libc to spawn a shell, just like we will do today.
Deliverables
Binary:
vuln2built with NX enabled and verified withchecksecLeak stage: Stage 1 leak works and returns to
mainLibc base:
libc.addresscorrectly computed from the leakFinal stage: Stage 2 gains code execution (shell)
Notes: gadgets + alignment rationale, plus the parsed leak value
Non-Executable Stack (NX/DEP)
What is NX?:
NX (No eXecute) bit marks stack as non-executable
Also called DEP (Data Execution Prevention) on Windows
Shellcode on stack cannot execute
Need alternative exploitation strategy
Enable NX for Practice (AMD64):
Return-to-libc Technique
Concept:
Instead of executing shellcode, call existing functions
libcprovides useful functions (system, execve, etc.)Chain function calls to achieve goal
No shellcode needed!
[!IMPORTANT] AMD64 Calling Convention: Unlike x86 where arguments go on the stack, AMD64 passes the first 6 arguments in registers: RDI, RSI, RDX, RCX, R8, R9. This means we need gadgets to load registers before calling functions!
The Canonical Exploit Pattern: Leak → Compute → Exploit
[!CAUTION] Never hardcode libc addresses! Even with ASLR disabled for testing, addresses change between libc versions and systems. Always use the leak → compute base → build ROP pattern.
The Real-World Pattern:
Why This Matters:
Works even with ASLR enabled (after one leak)
Portable across different libc versions (with correct libc file)
This is how real exploits work—not "paste address from GDB"
Required Lab: Libc Leak via ROP (AMD64)
This is the most important skill in basic exploitation. Even with ASLR "disabled" in labs, always practice the leak pattern.
vuln2.c (Vulnerable program for leak practice):
Compile (AMD64, NX enabled):
Complete Leak-Based Exploit (AMD64):
Key Points:
Never use
p.libs()in final exploits - it only works locally for debuggingAlways leak, then compute - this works with ASLR enabled
Stack alignment - AMD64 requires 16-byte alignment before
call; addretgadgetReturn to main - allows second stage after leak
Fix RBP for one_gadget - buffer overflows corrupt RBP; one_gadgets need
rbp-0xXXwritableModern libc has CET - SHSTK/IBT enabled;
system()may fail, use one_gadget instead
AMD64 Stack Alignment
[!CAUTION] AMD64 Failure Mode: If your exploit crashes with SIGSEGV inside libc (e.g., in
movapsinstruction), you have a stack alignment problem. The stack must be 16-byte aligned before anycallinstruction.
The Problem:
System V AMD64 ABI requires:
Stack must be 16-byte aligned BEFORE the 'call' instruction
'call' pushes 8-byte return address → stack becomes misaligned
Function prologue (push rbp) realigns it
When ROP chains skip prologues, alignment breaks!
The Fix - Always Include ret Gadget:
[!WARNING] Modern libc (glibc 2.34+) has Intel CET enabled! Even with correct alignment,
system()may still crash due to Shadow Stack (SHSTK) and Indirect Branch Tracking (IBT). Check withchecksec: ifSHSTK: EnabledandIBT: Enabled, use one_gadget instead.
When Alignment Isn't Enough (CET):
Debugging Alignment Issues:
Automated Address Finding (Local Debugging Only)
[!WARNING]
p.libs()only works for local debugging. Never use it in exploits targeting remote systems! Always use the leak pattern.
Finding one_gadget Offsets:
Identifying Your Libc Version:
Introduction to ROP
What is ROP?:
Technique to chain existing code "gadgets"
Gadget = short instruction sequence ending in
retChain gadgets to build arbitrary operations
Bypasses NX/DEP without shellcode
AMD64 ROP Basics:
Unlike x86 where you push arguments to the stack, AMD64 passes arguments in registers. This means you need gadgets like pop rdi; ret to load arguments!
Essential AMD64 Gadgets:
pop rdi; ret
Load 1st argument
Almost always needed!
pop rsi; ret
Load 2nd argument
For two-arg functions
pop rdx; ret
Load 3rd argument
Rare in modern libc! Use one_gadget
pop rbp; ret
Fix RBP for one_gadget
Critical for one_gadget!
pop rax; ret
Set RAX (syscall #)
For one_gadget constraints
ret
Stack alignment / pivot
Fix 16-byte alignment
[!NOTE] Modern libc (glibc 2.34+) lacks clean
pop rdx; retgadgets and has CET enabled. Traditionalsystem("/bin/sh")ROP often fails. Use one_gadget instead!
Simple AMD64 ROP Example (Traditional - may fail on modern libc):
Modern AMD64 ROP Example (one_gadget - works on glibc 2.34+):
Finding ROP Gadgets
Master manual gadget hunting before relying on tools—it builds intuition for what's possible.
Manual Gadget Finding (Do This First!)
Common AMD64 Gadget Byte Patterns:
pop rdi; ret
5f c3
Load RDI (arg 1)
pop rsi; ret
5e c3
Load RSI (arg 2)
pop rdx; ret
5a c3
Load RDX (arg 3) - rare!
pop rcx; ret
59 c3
Load RCX (arg 4)
pop rax; ret
58 c3
Load RAX (for one_gadget)
pop rbp; ret
5d c3
Fix RBP for one_gadget!
ret
c3
Stack alignment
syscall
0f 05
Syscall (AMD64)
pop rsi; pop r15; ret
5e 41 5f c3
Common in __libc_csu_init
[!WARNING]
pop rdx; retis rare in modern libc! You'll often findpop rdx; pop rbx; retor similar multi-pop variants. This breaks simpleexecve(path, NULL, NULL)chains. Use one_gadget instead of manually building execve calls.
Using GDB/pwndbg for Gadget Search:
Automated Gadget Finding (Use After Understanding Manual)
Gadget Priority for Modern Libc Exploitation:
pop rdi; ret- for leak stage (from binary, not libc)ret- for stack alignment (from binary)pop rbp; ret- CRITICAL for one_gadget RBP fix (from libc)pop rax; ret- for one_gadget RAX=0 constraint (from libc)pop rbx; ret/pop r12; ret- for other one_gadget constraints (from libc)
One_Gadget Constraints
[!CAUTION] Modern glibc one_gadgets have strict constraints! Buffer overflows corrupt RBP with your padding bytes (
0x4141414141414141), but one_gadgets often requirerbp-0xXXto be a writable address. This causes SIGBUS/SIGSEGV crashes.
Common one_gadget constraints:
The Problem: After buffer overflow, RBP = 0x4141414141414141 (A's). So rbp-0x50 = invalid address → SIGBUS when one_gadget tries to access it!
The Solution: Set RBP to a writable address before calling one_gadget:
One_Gadget Troubleshooting:
SIGBUS at one_gadget
RBP points to invalid memory
Set RBP to .bss or stack before calling
SIGSEGV in one_gadget
Register constraints not met
Try different one_gadget, set rax/rbx/r12=0
one_gadget exists but no shell
Wrong libc version
Verify libc, recalculate offsets
All one_gadgets fail
Constraints too strict
Fall back to ROP execve syscall
Why system() Fails on Modern Libc:
Modern glibc (2.34+) enables Intel CET (Control-flow Enforcement Technology):
SHSTK (Shadow Stack): Hardware-backed return address protection
IBT (Indirect Branch Tracking): Validates indirect jumps
checksec shows: SHSTK: Enabled, IBT: Enabled
This makes traditional system("/bin/sh") ROP chains crash. Solutions:
Use one_gadget with proper constraints (shown above)
Syscall directly via
execvesyscall (bypasses libc CET checks)Disable CET when compiling test binaries:
gcc -fcf-protection=none
Gadget Quality Checklist:
Using pwntools ROP Correctly
[!IMPORTANT] ROP Chain Timing: You must set
libc.addressBEFORE building the ROP chain! Don't createROP([elf, libc])until you've computed the libc base from a leak.
Correct ROP Workflow (Modern Libc with one_gadget):
Traditional Workflow (Older libc without CET):
Common Mistakes:
Quick Checklist for Modern Libc ROP:
Debugging ROP Chains
ROP exploits often fail silently. Here's how to systematically debug them.
Step 1: Print the Chain (Verify BEFORE Sending)
Step 2: Visualize Stack Layout (AMD64)
Step 3: Debug in GDB (AMD64)
In a second terminal, attach GDB:
Step 4: Trace Each Gadget (AMD64)
Common ROP Debugging Issues (AMD64):
Crash before first gadget
Wrong offset
Re-verify with cyclic pattern (8-byte!)
First gadget runs, then crash
Bad second address
Check stack alignment, verify addr
"Illegal instruction"
Jumped to data, not code
Verify gadget address is correct
Crash in system() (movaps)
AMD64 stack alignment!
Add ret gadget before call
system() crashes (CET)
Modern libc has SHSTK/IBT
Use one_gadget instead of system()
SIGBUS in one_gadget
RBP corrupted by overflow
Set RBP to .bss before one_gadget
system() runs but no shell
/bin/sh addr wrong
Re-find string after setting libc.address
Works locally, fails remote
Different libc version
Use libc database, leak to confirm
Stack Alignment Fix (AMD64):
RELRO (Relocation Read-Only) Explained
RELRO affects GOT overwrite attacks:
No RELRO
Yes (always)
Lazy binding
GOT overwrite works
Partial RELRO
Yes (GOT)
Lazy binding
GOT overwrite works
Full RELRO
No
Immediate binding
GOT is read-only!
Checking RELRO:
Compiling for Different RELRO Levels:
Full RELRO Bypass Options:
Overwrite
__malloc_hookor__free_hook(removed in glibc 2.34+)Overwrite return addresses (stack)
Overwrite function pointers in .data/.bss
Use FSOP (File Stream Oriented Programming)
Practical Exercise
Exercise: Libc Leak + ret2libc
Compile target with NX (AMD64):
Find gadgets:
Write leak exploit:
Stage 1: ROP to
puts(puts@got), return tomainParse leaked puts address
Compute
libc.address = leak - libc.symbols['puts']
Write final exploit:
Stage 2:
pop rdi; ret+/bin/sh+systemGet shell
Task 2: Stack Alignment Practice
Create exploit WITHOUT ret alignment gadget
Observe crash in libc (movaps instruction)
Add ret gadget and verify fix
Task 3: Gadget Hunting
Find gadgets manually:
Find in libc:
Success Criteria:
Libc leak working and parsed correctly
Libc base calculated correctly (ends in 000)
Stack alignment understood and applied
Shell obtained via ret2libc
Can explain each step of the exploit
Write exploit:
Build ret2libc payload
Call system("/bin/sh")
Get shell
Exercise: Function chaining
Chain system() and exit():
Call
system("whoami")Then call
exit(0)Observe clean exit
Read flag file:
Create
flag.txtwith secretChain to call
system("cat flag.txt")Display contents
Exercise: Simple ROP (AMD64 syscall)
Find gadgets:
Build ROP chain manually:
Set RAX to 59 (execve on AMD64)
Set RDI to address of "/bin/sh"
Set RSI and RDX to 0
Execute
syscallinstruction
Test ROP exploit:
Should get shell without any shellcode
Success Criteria:
ret2libc exploit works
Function chaining successful
ROP chain executes
Shell obtained in all three tasks
Week 3 Integration Exercise: Patch Diff -> Find Bug -> Exploit Old Build
Reuse the Week 3 patch-diffing workflow on a controlled Day 2-style target.
Goal: build a vulnerable and a patched version of the same program, diff them, then exploit only the vulnerable build.
Make two versions of the source:
vuln2_vuln.c: contains the bug (e.g., unbounded read / missing length check)vuln2_patched.c: fix the bug (e.g., bounded read or explicit length validation)
Compile both with identical flags:
Patch diff:
Validation:
Your Day 2 exploit should work on
vuln2_vuln.It should fail (or at least not gain control) on
vuln2_patched.
Success Criteria:
You can point to the exact function/basic-block changed by the patch
You can explain why the patch removes the exploit primitive
Key Takeaways
NX prevents shellcode execution: Need alternative techniques
ret2libc reuses existing code: Call libc functions
ROP chains gadgets: Build complex operations
Stack layout is critical: Function arguments must be correct
pwntools simplifies ROP: Automates gadget finding and chaining
Modern libc has CET:
system()ROP may fail, use one_gadget insteadone_gadget needs RBP fix: Buffer overflows corrupt RBP, set it to .bss first
pop rdxis rare: Modern libc lacks clean gadgets, use one_gadget
Discussion Questions
Why is ret2libc effective even with NX enabled?
What are the limitations of ret2libc vs ROP?
How would ASLR complicate ret2libc exploitation?
What types of gadgets are most useful for ROP chains?
Day 3: Heap Exploitation Fundamentals
Goal: Understand heap memory management and exploit heap overflows.
Activities:
Online Resources:
Tool Setup:
how2heap repository
Heap visualization tools
Exercise:
Exploit heap overflow to corrupt metadata
Achieve arbitrary write primitive
Context: libWebP Heap Overflow (CVE-2023-4863)
In Week 1, we discussed the libWebP Heap Buffer Overflow that affected billions of devices.
That vulnerability involved writing past the end of a heap buffer, corrupting adjacent metadata.
Today, we'll learn how to intentionally trigger and exploit such conditions to gain code execution.
Deliverables
Binary:
vuln_heapbuilt and verified withchecksecPrimitive proof: function pointer overwrite demonstrated (redirect to
admin_function)Exploit:
exploit_heap_fp.py(or equivalent) spawns a shell reliablyNotes: heap layout diagram + exact overwrite length and why
read()enables null bytes in payloads
Heap vs Stack
Differences:
Allocation
Automatic (local variables)
Manual (malloc/new)
Lifetime
Function scope
Explicit free
Size
Fixed per thread (~8MB)
Dynamic, grows as needed
Speed
Very fast
Slower (allocator overhead)
Layout
LIFO (Last In First Out)
Complex (bins, chunks)
Overflow Impact
Overwrites return address
Overwrites metadata
Heap Allocator Basics (glibc malloc)
This section provides a detailed walkthrough of how glibc's malloc works. Understanding these internals is essential for heap exploitation—don't skip it.
[!WARNING] Which glibc version? Run
ldd --version. This course uses glibc 2.31-2.35 examples. Many classic techniques (unlink, fastbin dup) are mitigated in 2.35+. Check how2heap for version-specific techniques.
Chunk Structure Deep Dive
Chunk Structure:
Size Field Flags (critical for exploitation):
Visual Representation:
Understanding malloc() Step by Step
Understanding free() Step by Step
Bin Organization (Visual)
Debugging Heap with pwndbg/GEF
Essential Commands (Use these constantly!):
Example Debugging Session:
Key Insight for Exploitation:
Heap Overflow Vulnerability
Vulnerable Program (vuln_heap.c):
Compile (AMD64):
Vulnerability Analysis:
Exploiting Function Pointer Overwrite
Exploit Strategy:
Overflow
user1->name(32 bytes)Overwrite
user1->print_funcwith address ofadmin_functionWhen
user1->print_func()is called, get shell
Find admin_function address:
Exploit (AMD64):
Test:
[!NOTE] Why does
system()work here but not in ROP chains?Intel CET (SHSTK/IBT) blocks indirect jumps/calls via corrupted return addresses (ROP). But function pointer overwrites are direct calls - the program legitimately calls through a pointer, which CET allows. This is why heap exploits targeting function pointers still work on modern libc, while stack-based ROP to
system()fails.When CET blocks you:
ROP chains returning to
system()via stack corruptionret2libc attacks using gadgets
When CET does NOT block you:
Function pointer overwrites (heap, GOT if writable)
Direct control flow hijack to existing functions
One_gadget (uses internal code paths that satisfy CET)
Heap Metadata Corruption
Unlink Exploit (Classic technique):
Concept:
Corrupt free chunk metadata (fd/bk pointers)
When chunk is unlinked from bin, write arbitrary address
Achieve write-what-where primitive
Vulnerable Code (unlink_vuln.c):
Exploit Technique:
Overflow chunk 'a' into chunk 'b'
Fake chunk 'b' metadata:
Set prev_size to overflow into 'a'
Set size with prev_inuse=0 (fake free)
Set fd/bk to target addresses
Free chunk 'b'
Unlink writes: _(fd) = bk and _(bk) = fd
Arbitrary write achieved!
Modern Protections:
Safe unlinking (glibc 2.3.4+)
Checks:
fd->bk == chunk && bk->fd == chunkMakes classic unlink harder
Legacy Tcache Poisoning (glibc 2.27-2.31, no safe-linking):
This is the foundational technique to learn before tackling modern bypasses:
For testing on Ubuntu 24.04 (glibc 2.39 with safe-linking):
Why This Works (glibc 2.39 safe-linking bypass):
Critical Requirements for glibc 2.39+:
Safe-linking key:
key = chunk_address >> 12(simple right shift)Target alignment: Must be 0x10-aligned to avoid "unaligned tcache chunk detected"
Corruption position: Target the SECOND tcache entry (B), not the first (A)
Proper chaining: Free A then B, corrupt B's fd, drain A, get target
Real-World Impact:
Bypasses modern glibc protections: Works on Ubuntu 24.04 (glibc 2.39)
Arbitrary write: Achieves write-what-where primitive
ASLR bypass: No need for leaks if you have a known target
Reliable: High success rate when conditions are met
glibc 2.35+ / Ubuntu 24.04
Key Changes in Modern glibc:
2.32+
Tcache pointer XOR (safe-linking)
XOR key from chunk addr (chunk_addr >> 12)
22.04+
2.34+
__malloc_hook removed
Hook overwrite attacks dead
22.04+
2.35+
Enhanced tcache key checks
Double-free detection improved
23.04+
2.37+
global_max_fast type change
Fastbin size attacks limited
23.10+
2.38+
_IO_list_all checks tightened
FSOP attacks significantly harder
24.04+
2.39+
Additional largebin checks
Largebin attack constraints
24.04
Safe-Linking Explained (glibc 2.32+):
Safe-linking protects singly-linked list pointers (tcache and fastbin) using XOR mangling:
Bypassing Safe-Linking (working method for glibc 2.39):
Key Insights from Working Implementation:
No heap leak needed: The key is derived from the chunk you're corrupting
Simple formula: Just
chunk_addr >> 12, not complex heap base calculationsAlignment critical: Target must be 0x10-aligned or glibc aborts
Position matters: Corrupt the SECOND tcache entry, not the first
[!NOTE] Exception:
tcache_perthread_structcounts are NOT protected by safe-linking! This enables advanced techniques like House of Water for leakless attacks.
Modern Techniques Still Working:
Tcache Stash Unlink (TSU): Smallbin → tcache manipulation
House of Lore variants: Smallbin bk pointer corruption
Largebin attacks: Still viable for arbitrary write
Tcache struct hijack: Control allocation via
tcache_perthread_struct
Practicing Classic Heap Techniques (Docker Setup):
For learning classic heap exploitation without modern hardening:
Patchelf for Specific glibc Versions:
Practical Exercise
Exercise: Exploit heap overflow vulnerabilities
Setup:
Exercise: Function Pointer Overwrite
Compile vuln_heap.c
Find admin_function address
Build exploit to overwrite print_func
Get shell
Exercise: Heap Spray
Allocate many chunks
Fill with shellcode
Trigger vulnerability to jump into spray
Execute shellcode
Exercise: Tcache Poisoning (Modern)
Study how2heap/tcache_poisoning.c
Understand tcache bin structure
Corrupt fd pointer
Allocate at arbitrary address
Success Criteria:
Function pointer overwrite works
Heap spray successful
Understand modern heap protections
Can explain tcache attack surface
Key Takeaways
Heap is more complex than stack: Multiple allocator structures
Metadata corruption is powerful: Enables write-what-where
Modern heaps have protections: Safe unlinking, tcache checks, safe-linking (2.32+)
Function pointers are targets: Easy to exploit if reachable - bypasses CET!
Heap spray can bypass ASLR: Fill memory with shellcode
CET doesn't block function pointer overwrites: Unlike ROP, direct calls work
Modern libc removed
__malloc_hook: Can't use hook overwrites anymore (2.34+)
Discussion Questions
Why is heap exploitation more complex than stack overflow?
How do safe unlinking checks prevent classic unlink attacks?
What makes tcache a good target for exploitation?
How would you detect heap corruption at runtime?
Day 4: Heap Exploitation Part 2 – Modern Techniques
Goal: Master modern heap exploitation: tcache poisoning with safe-linking bypass, House of Botcake, House of Water, House of Tangerine.
Activities:
Reading:
Foundation:
Tcache House of Spirit - simplest, no next chunk validation
Tcache Metadata Poisoning - easy if metadata accessible
Core Techniques:
Tcache Poisoning - with safe-linking bypass
Fastbin Dup - classic double-free
House of Botcake - double-free bypass (most practical)
Overlapping Chunks - chunk overlap via size corruption
Advanced:
Large Bin Attack - arbitrary write (heap ptr)
Fastbin Reverse Into Tcache - write heap ptr to stack
Unsafe Unlink - classic unlink with modern constraints
Expert:
House of Water - UAF to tcache metadata control
House of Tangerine - no free() needed!
House of Einherjar - backward consolidation
Safe-Linking Double Protect Bypass - blind safe-linking bypass
Online Resources:
how2heap Repository (glibc_2.41) - Practice modern techniques
Tool Setup:
Compile with
-no-pie -Wl,-z,norelrofor easier practiceUse pwndbg
heapandbinscommands constantly
Exercise (follow order for progressive learning):
Start with Use-After-Free patterns
Exploit tcache House of Spirit (easiest tcache attack)
Exploit tcache poisoning with safe-linking bypass
Exploit House of Botcake for double-free scenarios
Attempt House of Water for advanced UAF exploitation
Deliverables
Environment: glibc version recorded (
ldd --version) and which how2heap example(s) you usedReproduction: at least one modern technique reproduced end-to-end (e.g., tcache poisoning with safe-linking)
Primitive proof: demonstrated controlled allocation to a chosen address (and explained the safe-linking XOR key)
Notes: minimal writeup showing the exact leak used (heap/libc) and how it enables the technique
Use-After-Free Exploitation (Foundation)
UAF is a type of vulnerability; tcache/fastbin poisoning is the technique to exploit it.
Classic UAF Pattern:
UAF Heap Feng Shui:
The key to reliable UAF exploitation is controlling what gets allocated in the freed memory.
Interactive UAF Target (vuln_uaf.c):
Compile:
Complete UAF Exploit Example:
Test:
Test Results:
Exploit Success:
admin_funcfound at static address0x4011bc(No PIE)UAF exploit successfully overwrote callback pointer
Shell spawned with user privileges
Interactive shell obtained, confirming full control
Why This Works:
UAF Pattern:
objpointer isn't NULLed afterfree(), creating a dangling pointerHeap Reuse:
spray()allocates same-sized chunk (sizeof(Object)) that reclaims freed memoryControlled Overwrite: Spray payload overwrites
callbackpointer withadmin_funcaddressTrigger:
use()callsobj->callback()which now points to attacker-controlled functionNo ASLR Bypass Needed: Binary has
No PIE, soadmin_funcaddress is static
Key Insight: UAF exploitation is about controlling what gets allocated in freed memory and then using the dangling pointer to access attacker-controlled data.
Tcache House of Spirit (glibc 2.41)
Key insight from malloc.c: tcache_put() is called without checking if next chunk's size and prev_inuse are sane (search for "invalid next size" and "double free or corruption" - those checks are bypassed).
Build and Run:
Test Results:
Attack Success:
Fake chunk created on stack at
0x7ffea7295d98(size field)Data area at
0x7ffea7295da0(8 bytes later due to chunk header)malloc(0x30)returns our fake chunk data areaStack memory successfully allocated via heap allocator
Why This Works:
No Next Chunk Validation: Unlike fastbin, tcache
free()doesn't validate the next chunk's size fieldSimple Fake Chunk: Only need size field (0x40) in
fake_chunks[1], no complex metadataPointer Arithmetic:
a = &fake_chunks[2]points to "user data" area of fake chunkAlignment:
__attribute__((aligned(0x10)))ensures 16-byte alignment for modern glibcSize Range: 0x40 is valid tcache size (requests 0x30-0x38 round to 0x40)
Warning Explained: The compiler warning is expected - we're intentionally freeing stack memory as a fake chunk, which is the whole point of the attack!
Why Tcache House of Spirit is Easier:
Next chunk validation
Required
Not needed
Size constraints
Fastbin range only
Up to 0x410
Complexity
Must craft 2 fake chunks
Only 1 fake chunk
glibc version
Works on older
Works on 2.41
Attack Pattern:
Find/create writable region with controlled data
Set up fake size field (0x20-0x410 range, bits 1-2 = 0)
Ensure 16-byte alignment
Overwrite pointer to point to fake chunk's data region
free(corrupted_ptr) → fake chunk goes to tcache
malloc(matching_size) → returns your controlled region!
Tcache Metadata Poisoning (Direct Metadata Control)
Build and Run:
Test Results:
Attack Success:
Stack target at
0x7ffd375a38c0(16-byte aligned)Victim chunk at
0x28d522a0used to locate metadataTcache metadata found at
0x28d52010(start of heap page)Direct metadata corruption inserted target into bin 1
malloc(0x20)returned stack address - arbitrary allocation achieved!
Why This Works:
Direct Metadata Control: Overwrites
counts[1]andentries[1]directlyNo Safe-Linking: Metadata corruption bypasses pointer protection
Immediate Effect: Next
malloc(0x20)returns controlled addressPowerful Primitive: Gives arbitrary allocation capability
Tcache Poisoning with Safe-Linking Bypass (Working glibc 2.39)
Build and Run:
Test Results:
Attack Success:
Found 16-byte aligned stack target at
0x7ffda554a440Chunks
aat0x2b5992a0andbat0x2b599330allocatedAfter double free: tcache contains
b -> a -> NULLSafe-linking bypass:
target ^ (b >> 12)written tob[0]Second
malloc(128)returned our stack target!
Why This Works:
Safe-Linking Bypass: XOR with
(chunk_addr >> 12)defeats pointer protectionDouble Free: Creates tcache list we can corrupt
Pointer Corruption: Overwrites next pointer with encoded target
Arbitrary Allocation: Next malloc returns controlled address
Fastbin Dup (Modern - glibc 2.41)
Modern Double Free via Fastbin:
Build and Run:
Test Results:
Attack Success:
Chunks allocated:
aat0x22c663f0,bat0x22c66420After tcache fill and double free: fastbin contains cycle
a -> b -> aAllocations:
cgetsa,dgetsb,egetsaagain!Double allocation achieved:
candepoint to same memory
Why This Works:
Tcache Fill: 7 chunks fill tcache, forcing frees to fastbin
Double Free: Creates cycle in fastbin list
No Safe-Linking: Fastbin doesn't use safe-linking protection
Double Allocation: Same chunk returned twice
House of Botcake (glibc 2.29+ Double-Free Bypass)
Build and Run:
Test Results:
Attack Success:
Stack target at
0x7ffe38860b00(16-byte aligned)Chunks:
prevat0xcd4aa10,victimat0xcd4ab20Overlapping chunk gives write access to victim's metadata
Offset 34 words to victim's tcache next pointer
Safe-linking bypass:
target ^ (victim >> 12)writtenArbitrary write achieved:
0xcafebabewritten to stack!
Why This Works:
Consolidation Trick: Chunk consolidation creates overlapping memory
Tcache Double Placement: Same chunk in both unsorted bin and tcache
Metadata Corruption: Overlapping chunk corrupts tcache next pointer
Safe-Linking Bypass: XOR with chunk address defeats protection
Arbitrary Write: Next malloc returns controlled address
Large Bin Attack (glibc 2.30+ Variant)
Build and Run:
Test Results:
Attack Success:
Target at
0x7ffc62e176c0initially contains0Large chunks:
p1at0x1153c290,p2at0x1153c6e0After setup:
p1in largebin,p2in unsorted binCorrupted
p1->bk_nextsizeto point attarget-0x20Arbitrary write achieved: target now contains heap pointer
0x1153c6e0
Why This Works:
Large Bin Insertion: When
p2inserted into large bin, glibc writes tobk_nextsize->fd_nextsizeWeak Validation: glibc doesn't validate
bk_nextsizeif new chunk is smallestArbitrary Write: Corrupted pointer causes write to any address
Heap Pointer: Writes heap address (useful for bypassing ASLR)
Fastbin Reverse Into Tcache (glibc 2.41)
Similar to unsorted_bin_attack but works with small allocations. When tcache is empty and fastbin has entries, malloc refills tcache from fastbin in reverse order, writing heap pointers to stack.
Build and Run:
Test Results:
Attack Success:
Victim chunk at
0x29d2a4d0, stack target at0x7ffd808c11c0Before: stack filled with
0xcdcdcdcdcdcdcdcdpatternAfter trigger: heap pointers written to stack at
0x7ffd808c11c0and0x7ffd808c11c8Arbitrary allocation achieved:
malloc(allocsize)returned stack address!
Why This Works:
Fastbin→Tcache Refill: When tcache empty, malloc refills from fastbin in reverse
Reverse Order: Fastbin entries processed backwards, writing to stack
Heap Pointer Write: Victim chunk address written to stack during refill
Arbitrary Allocation: Stack address now in tcache, next malloc returns it
House of Water (glibc 2.32+)
Leakless heap exploitation technique by @udp_ctf.
[!IMPORTANT] Key insight: The
tcache_perthread_structmetadata on the heap is NOT protected by safe-linking! This allows manipulation without needing a heap leak first.
Why tcache metadata is vulnerable:
House of Tangerine (glibc 2.32+)
Modern House of Orange that doesn't need free()!
Safe-Linking Double-Protect Bypass (Blind - Hard)
Bypass safe-linking without a heap leak (4-bit bruteforce):
House of XXX Techniques (Overview)
Tcache House of Spirit
Fake chunk in tcache
2.27-2.41
No next chunk validation!
Tcache Metadata Poison
Direct tcache metadata
2.27-2.41
Metadata NOT safe-link protected!
House of Spirit (Fastbin)
Fake chunk in fastbin
2.23-2.41
Need heap leak for 2.32+
House of Lore
Small bin corruption
2.23-2.41
Still works
House of Botcake
Tcache + unsorted bin
2.29-2.41
Most practical double-free
House of Tangerine
sysmalloc _int_free
2.27-2.41
No free() needed!
House of Einherjar
Backward consolidation
2.23-2.41
Needs null byte write
House of Water
UAF → tcache metadata ctrl
2.32-2.41
Leakless! 1/256 bruteforce
House of Gods
Arena hijacking
2.23-2.26
Pre-tcache arena corruption
House of Mind (Fastbin)
Arena corruption
2.23-2.41
Complex arena manipulation
House of Force
Top chunk size overwrite
2.23-2.28
Patched in 2.29
House of Orange
Unsorted bin + FSOP
2.23-2.26
Patched in 2.27
glibc Version Eras:
Pre-Tcache
2.23-2.25
Classic heap, hooks available, no tcache
Tcache Era
2.26-2.31
Tcache introduced, hooks still work
Safe-Linking Era
2.32-2.33
Pointer XOR mangling, alignment checks
Post-Hooks Era
2.34+
__malloc_hook/__free_hook REMOVED
Modern Era
2.38+
FSOP hardened, enhanced checks
Learning Path (Recommended Order):
Modern Techniques (glibc 2.32+):
When to Use Which:
Modern Heap Protections (glibc 2.41)
Tcache double-free key
2.29+
Key in freed chunk
House of Botcake, leak key
Safe-linking
2.32+
XOR pointer mangling
Heap leak, or double-protect
Pointer alignment check
2.32+
16-byte alignment required
Craft aligned fake chunk
Fastbin fd validation
2.32+
Check fd points to valid
Need heap leak
Top chunk size check
2.29+
Validate top chunk size
House of Tangerine
Unsorted bin checks
2.29+
bk->fd == victim check
House of Botcake
fd pointer validation
2.32+
Check fd in expected range
Target must be in heap range
Recent Vulnerability: Integer overflow in memalign family (glibc 2.30-2.42):
Affects
memalign,posix_memalign,aligned_allocAttacker-controlled size + alignment → heap corruption
Patched in glibc 2.39-286, 2.40-216, 2.41-121, 2.42-49
Checking Protections:
From Arbitrary Write to Code Execution
Once you have an arbitrary write/allocation primitive from heap exploitation, here's how to achieve code execution on modern systems:
Target Selection (Modern glibc 2.34+):
GOT entry (e.g., exit)
Partial
Bypasses!
Best target if available
Function pointer in struct
Any
Bypasses!
Common in heap exploits
__free_hook
Any
N/A
REMOVED in glibc 2.34+
__malloc_hook
Any
N/A
REMOVED in glibc 2.34+
_IO_list_all (FSOP)
Any
Complex
Hardened in glibc 2.38+
Return address on stack
Any
Blocked
CET shadow stack prevents
Best Targets on Modern Systems:
GOT Overwrite (Partial RELRO only):
Function Pointer in Heap Object:
Stack Pivot + ROP (if GOT/funptr unavailable):
Why CET Doesn't Block Heap Exploits:
Complete Heap-to-Shell Example (Modern glibc):
This complete example demonstrates UAF exploitation with function pointer overwrite - the most reliable technique on modern systems with CET.
Target Program (heap_shell_target.c):
Compile:
Exploit (exploit_heap_shell.py):
Run:
Test Results:
Exploit Success:
win_funclocated at static address0x4011ac(No PIE)UAF exploit successfully overwrote function pointer
Shell spawned:
WIN! Spawning shell...message confirms successInteractive shell obtained with user privileges
CET Bypassed: Function pointer call works even with modern CET protection
Why This Works on Modern Systems:
Generic Heap-to-Shell Pattern (when you have arbitrary write):
Introduction to FSOP (File Stream Oriented Programming)
With the removal of __malloc_hook and __free_hook in glibc 2.34+, FSOP is the primary method for turning heap primitives into code execution when GOT is not writable (Full RELRO).
The Concept:
glibc uses _IO_FILE structures (like stdin, stdout, stderr) to manage streams. These structures contain a vtable pointer (_IO_file_jumps) that points to a table of function pointers for I/O operations.
Classic FSOP Attack (glibc < 2.24):
Corrupt a FILE struct: Overwrite
stdout,stderr, or forge a fake_IO_FILESet fake vtable: Point vtable to attacker-controlled memory
Trigger: Call
exit()(flushes all streams) or any stdio function
Modern FSOP (glibc 2.24+):
glibc 2.24 added _IO_vtable_check() which validates that vtable pointers fall within the legitimate vtable section. Direct fake vtable attacks no longer work.
Bypass via _IO_str_jumps / _IO_wfile_jumps:
The trick is to use legitimate vtables but manipulate FILE struct fields to control what gets called:
Why Classic FSOP Fails on Modern glibc (fsop_demo.c):
This demonstrates that direct vtable overwrite FAILS on glibc 2.24+ due to _IO_vtable_check():
Compile and Run:
Test Results:
Classic FSOP Failure:
stdoutat0x776c6a6045c0, original vtable at0x776c6a602030(legitimate)Fake vtable on stack at
0x7ffe77115fd0successfully writtenglibc aborts:
_IO_vtable_check()detects invalid vtable outside legitimate rangeProof: Modern glibc (2.24+) blocks classic FSOP with fake vtables
Why It Fails:
Exploit for fsop_target (Function Pointer Overwrite):
Since CET blocks GOT overwrite to system(), we use function pointer overwrite which calls legitimate functions.
Updated target with function pointer (fsop_target.c):
Compile:
Working Exploit (exploit_fsop.py):
Compile and Run:
Test Results:
Exploit Success - CET Bypassed!:
Modern Protections Active: SHSTK and IBT enabled (CET protection)
win_func at static address
0x401230(No PIE)UAF Success: Function pointer overwritten with controlled address
CET Bypassed: Function pointer call is legitimate indirect call, not blocked by IBT
Shell Achieved:
WIN!message and interactive shell with user privileges
Why This Works While Classic FSOP Fails:
Key Insight: On modern systems with CET, function pointer overwrite is superior to FSOP because it uses legitimate indirect calls that CET is designed to allow, while FSOP requires bypassing vtable validation.
Modern Exploitation Decision Tree (glibc 2.39+):
Technique Compatibility (Updated for CET):
Direct vtable overwrite
< 2.24
N/A
No vtable check
_IO_str_jumps abuse
2.24-2.37
N/A
Patched in 2.38
House of Apple/Emma
2.34-2.38
Blocked
CET blocks gadget calls
Function pointer overwrite
All
WORKS
Calls real functions
House of Water
2.32+
WORKS
Gets tcache control
House of Tangerine
2.27+
WORKS
No free() needed
Recommended Approach for Modern Systems:
CET enabled (2.39+)
Function pointer overwrite via UAF/heap corrupt
Full RELRO + CET
House of Water → func ptr overwrite
Partial RELRO no CET
GOT overwrite (simpler)
Need tcache control
House of Water or House of Tangerine
Practical Exercise
Exercise: Use-After-Free Exploitation
Create the target (uaf_challenge.c):
Compile:
Find admin_greet address:
Write exploit:
based on what you've learned, write the proper exploit
Exercise: Tcache House of Spirit
Compile how2heap tcache_house_of_spirit.c:
Trace in GDB to understand the simple requirements:
Only need valid size field (no next chunk validation!)
Region must be 16-byte aligned
Size must be in tcache range (0x20-0x410)
Key observation: Unlike original House of Spirit, tcache doesn't check next chunk metadata!
Exercise: Tcache Poisoning
Compile the tcache_poisoning.c example:
Trace execution in GDB:
Observe tcache state:
Before free: tcache empty
After free: chunk in tcache
After poisoning: tcache points to target
After second malloc: arbitrary address returned
Modify to target a function pointer:
Add a function pointer variable
Poison tcache to point to it
Overwrite with
systemor win function
Exercise: House of Botcake
Run how2heap example:
Understand the technique:
Fill tcache → chunk goes to unsorted bin
Consolidation creates overlapping chunk
Free victim to tcache → exists in both bins!
Allocate from unsorted → control tcache entry
Key insight: Bypasses tcache double-free detection via consolidation trick
Exercise: Safe-Linking Bypass
Study the protection:
Write a heap leak + tcache poison exploit:
First, leak a heap address (via UAF read or other bug)
Calculate the XOR key:
heap_addr >> 12Mangle your target address before writing to tcache
Verify with ~/tuts/how2heap/glibc_2.39/tcache_poisoning.c
Exercise: Advanced Techniques
Large Bin Attack:
Run
~/tuts/how2heap/glibc_2.39/large_bin_attackUnderstand bk_nextsize corruption for arbitrary write
House of Water (Expert):
Study the technique on how2heap wiki
Requires understanding of tcache metadata structure
House of Tangerine (Expert):
Run
~/tuts/how2heap/glibc_2.39/house_of_tangerineKey: Exploits sysmalloc _int_free without needing free()
Success Criteria:
Task 1
UAF exploit hijacks function pointer
Task 2
Understand tcache House of Spirit simplicity
Task 3
Tcache poisoning achieves arbitrary write
Task 4
Can explain House of Botcake consolidation trick
Task 5
Safe-linking bypass works with heap leak
Task 6
At least one advanced technique understood
Minimum requirement: Complete Tasks 1-4 with full understanding
Key Takeaways
Start with UAF patterns: Understand the core vulnerability before learning exploitation techniques
Tcache House of Spirit is easiest: No next chunk validation makes it simpler than fastbin variant
House of Botcake is most practical: Double-free bypass works on all modern glibc (2.29+)
Safe-linking bypass uses chunk address: XOR key derived from corrupted chunk (chunk_addr >> 12) for glibc 2.32+
Know your glibc version: Technique selection depends heavily on target version
how2heap/glibc_2.41 is your reference: Practice techniques in order of difficulty
CET doesn't block heap exploits: Function pointer/GOT overwrites bypass SHSTK/IBT
Hooks are dead:
__malloc_hook/__free_hookremoved in glibc 2.34+, use GOT or FSOP instead
Discussion Questions
Why is tcache House of Spirit easier than the original fastbin version?
How does safe-linking protect against tcache poisoning, and why does it require a heap leak to bypass?
What makes House of Botcake the most practical double-free technique for modern glibc?
When would you use House of Tangerine over House of Botcake?
Day 5: Format String Vulnerabilities
Goal: Master format string exploitation techniques.
Activities:
Reading:
"The Shellcoder's Handbook" - Chapter 4
Online Resources:
Tool Setup:
pwntools with FmtStr module
GDB with format string helpers
Exercise:
Read arbitrary memory with %x
Write arbitrary memory with %n
Overwrite GOT entry for exploitation
Deliverables
Binary:
vuln_fmtbuilt and verified withchecksecOffset: your correct format string offset found (the
%<n>$pwhere you see0x4141414141414141)Leak: at least one stable pointer leak (stack/libc) parsed in Python
Write: one working
%nwrite (flip a variable or overwrite a GOT entry)Exploit: a pwntools script that reaches code execution (shell or
win())
Understanding Format Strings
What is a Format String Bug?:
User input passed directly to printf-like function
Attacker controls format specifiers
Can read/write arbitrary memory
Vulnerable Code:
Format String Basics
Common Format Specifiers:
%d
Print int
Reads from register/stack
%x
Print hex (32-bit)
Reads 4 bytes, zero-extended
%lx
Print hex (64-bit)
Reads full 8 bytes
%s
Print string
Reads pointer, dereferences
%n
Write byte count
Writes to pointer
%p
Print pointer (BEST!)
Shows full 64-bit pointer as hex
%<number>$
Direct parameter access
Access specific position
[!IMPORTANT] On AMD64, always use
%pfor leaking! It prints the full 64-bit value in hex format (0x7fff...). Using%xonly shows 32 bits and can confuse beginners.
AMD64 Format String Parameter Passing:
On AMD64, the first 6 printf arguments after the format string come from registers:
Position 1-6: RDI (fmt), RSI, RDX, RCX, R8, R9
Position 7+: Stack
This means your buffer typically appears at offset 6 or higher on AMD64!
Reading Stack:
Stack Reading Success:
Input buffer found at offset 6 (confirmed with
0x4141414141414141)%6$pshows format string pointer0xa70243625%1$pshows stack address0x7ffdca13b4b0Key: On AMD64, input typically appears at offset 6+ due to register passing
Information Disclosure
Leaking Stack Values (AMD64):
Run:
Test Results:
Stack Scanning Success:
Input confirmed at offset 6:
0x4141414141414141marker foundVarious stack values leaked: Stack addresses, libc pointers, NULL values
Consistent results: Each offset returns predictable values
Process management: Each scan spawns new process to avoid state corruption
Finding Your Input Offset (Quick Method):
Result: On this system, input buffer is at offset 6.
Arbitrary Memory Read (AMD64)
Reading Memory at Address:
On AMD64, addresses are 8 bytes and may contain null bytes (0x00004...). Null bytes terminate strings, so we put the address AFTER the format specifier!
The 64-bit Null Byte Problem (Manual Payload Construction)
fmtstr_payload is magic, but you must understand why 64-bit writes are painful.
The Issue: 64-bit addresses (e.g., 0x00007fffffffe000) contain null bytes at the start (little endian: 00 e0 ff ...). If you put the address at the start of your payload (like in 32-bit exploits), printf reads the null bytes and stops processing the rest of the string immediately.
The Solution: Place the target address at the very end of the payload.
Calculate Offset: Determine how many 8-byte blocks it takes to reach the end of your format string.
Argument Ordering: Use
%<offset>$nto tell printf to skip ahead to that end-block where the address lives.
Example:
Arbitrary Memory Write (AMD64)
The %n Specifier (64-bit considerations):
%nwrites 4 bytes (int) - often enough!%lnwrites 8 bytes (long) - full 64-bit%hnwrites 2 bytes (short)%hhnwrites 1 byte (char) - most precise
For AMD64 addresses (0x7fff...), use %hhn to write byte-by-byte, or %hn to write 2 bytes at a time. Full 8-byte writes are rarely practical.
Writing with pwntools (Recommended):
Test Results:
GOT Overwrite Success:
Payload generated: 64 bytes using
fmtstr_payloadexit@GOT overwritten: Redirected to
win()functionShell achieved: Interactive shell with user privileges
pwntools magic: Automatic handling of 64-bit address alignment and null bytes
GOT Overwrite Attack (AMD64)
Global Offset Table (GOT):
Stores addresses of dynamically linked functions
Writable by default (Partial RELRO)
Overwriting GOT entry redirects function calls
Exploit Strategy:
Find GOT entry for common function (printf, exit, etc.)
Use format string to leak libc address (defeat ASLR)
Use format string to overwrite GOT entry
Point GOT entry to
systemorone_gadgetTrigger function call → shell!
Example Program (fmt_got.c):
Compile (AMD64):
Exploit (AMD64):
Test Results:
Manual GOT Overwrite Success:
Addresses found:
win()at0x401186,exit@GOTat0x404030Offset scanning: Tested offsets 1-9, format string found at various positions
Payload generated: Complex multi-byte write using
%lln,%hhnspecifiersGOT overwritten:
exit@GOTredirected towin()functionShell achieved: "You win!" message and interactive shell
Using pwntools FmtStr Class (Automated):
Test Results:
Automated FmtStr Success:
Auto-detection: FmtStr automatically found format string offset 6
Multiple processes: Used oracle function to test different payloads
Payload generation: Automatic creation of format string payload
GOT overwrite: Successfully redirected
exit@GOTtowin()functionShell achieved: "You win!" message and interactive shell
pwntools power: Automated offset finding and payload generation
Automated Exploitation with pwntools (AMD64)
Using FmtStr Module:
Test Results:
FmtStr.execute_writes() Issue:
Offset detection worked: Found format string at offset 6
Problem:
execute_writes()hangs because successful GOT overwrite redirectsexit()towin(), butwin()doesn't exit the programRoot cause: After GOT overwrite, program calls
win()and waits for input, butrecvall()expects program to exitSolution: Use
fmtstr_payload()directly instead ofexecute_writes()for interactive shells
Manual fmtstr_payload (More Control):
Test Results:
Manual fmtstr_payload Success:
Direct approach worked: Used
fmtstr_payload()instead ofexecute_writes()Shell achieved: "You won!" message and interactive shell
write_size='short': Used
%hn(2-byte writes) for reliabilityInteractive mode: Properly handled program that doesn't exit after exploitation
Format String + Libc Leak Pattern (ASLR Bypass):
Test Results:
ASLR Bypass + One_Gadget Issues:
Libc leak successful:
0x77d09f42a1ca→ base0x77d09f400000One_gadget problem:
0xe3b01offset caused crash (SIGSEGV)Root cause: Wrong one_gadget offset for this libc version/system
Solution: Run
one_gadget /lib/x86_64-linux-gnu/libc.so.6to find working offsets
Key Lessons:
FmtStr.execute_writes(): Not suitable for interactive shells - use
fmtstr_payload()directlyOne_gadget offsets: System-specific, must be recalculated for each libc version
Manual approach:
fmtstr_payload()withwrite_size='short'is most reliableASLR bypass: Libc leak + base calculation works perfectly
Format String Protection Mechanisms
Fortify Source (-D_FORTIFY_SOURCE=2):
Checks format string at compile time
Warns on non-literal format strings
Adds runtime checks
Mitigations:
Always use literal format strings:
printf("%s", input)Enable compiler warnings:
-Wformat -Wformat-securityUse fortify source:
-D_FORTIFY_SOURCE=2Static analysis tools: scan for printf(user_controlled)
SROP (Sigreturn-Oriented Programming) Explained
What is SROP?:
Uses the
sigreturnsyscall to set all registers at oncesigreturnrestores register state from a "signal frame" on stackAttacker provides fake signal frame with controlled register values
Single syscall sets RAX, RDI, RSI, RDX, RIP, RSP, etc.
Why Use SROP?:
Tiny Binaries: In statically linked binaries or small containers, you might not have enough gadgets for a full
pop rdi; retchain. SROP only needssyscall; ret.Gadget Scarcity: If CET blocks complex ROP chains, SROP (which does context switching in kernel space) can sometimes simplify the userspace requirements.
One-Shot Setup: It sets RDI, RSI, RDX, and RAX simultaneously. No need to hunt for elusive
pop rdxgadgets.Sets ALL registers in one operation (including RBP for one_gadget!)
Useful when gadgets are scarce
Bypasses CET! Syscall-based approach doesn't use libc functions
[!NOTE] SROP vs one_gadget on modern libc: SROP uses direct syscalls, bypassing libc entirely. This avoids CET issues that plague libc function calls. If one_gadget fails due to CET constraints, SROP is an excellent alternative.
Signal Frame Structure (simplified x64):
SROP Exploit Flow:
Overflow buffer
Set return address to sigreturn gadget
Place fake signal frame on stack with:
RAX = 59 (execve syscall number)RDI = address of "/bin/sh"RSI = 0 (NULL)RDX = 0 (NULL)RIP = syscall gadget
sigreturn loads all registers from fake frame
Execution continues at RIP (syscall)
execve("/bin/sh", NULL, NULL) executed
SROP with pwntools:
Target Source (vuln_srop.c):
Exploit Script:
Test Results:
SROP Exploit Success:
Compilation: Built with
-fcf-protection=none -z execstack(CET disabled, executable stack)Gadgets found:
pop rax @ 0x40114a,syscall @ 0x40114c(from inline assembly)String located:
/bin/sh @ 0x404028(global variable in .data)Shell achieved: Interactive shell with full user privileges
Clean exit: Process exits normally after shell session
Key Technical Details:
CET bypass:
-fcf-protection=nonedisables shadow stack/IBT protectionExecutable stack:
-z execstackallows shellcode if neededInline gadgets: Assembly provides reliable gadget addresses
Direct syscall: Bypasses libc entirely, avoiding CET restrictions
Signal frame: 248-byte structure sets all registers simultaneously
When to Use SROP vs ROP:
Few gadgets available
x
Need to set many registers
x
Simple function call
x
Binary has sigreturn gadget
x
Need fine-grained control
x
ret2dlresolve Explained
What is ret2dlresolve?:
Abuses the dynamic linker's lazy binding mechanism
Forces linker to resolve ANY function, even if not imported
Works even with Full RELRO (in some cases)
No libc address leak required!
How Lazy Binding Works:
Program calls printf@plt
PLT jumps to GOT entry
First call: GOT points back to PLT
PLT calls
_dl_runtime_resolve(link_map, reloc_index)Resolver finds "printf" in libc, updates GOT
Future calls go directly to libc printf
ret2dlresolve Attack:
Craft fake Elf_Rel structure (relocation entry)
Craft fake Elf_Sym structure (symbol entry)
Craft fake string "system"
Call
_dl_runtime_resolvewith fakereloc_indexResolver "resolves" our fake "system" symbol
system("/bin/sh") gets called!
ret2libc with Leak:
Instead of ret2dlresolve, most real exploits use a two-stage approach:
Test Results:
ret2libc with Leak Success:
Binary analysis: Partial RELRO, no PIE, no stack canary (vulnerable)
Libc detection: Found system libc with Full RELRO and CET protections
Leak successful:
puts@GOT: 0x7d2ad6887be0→ base0x7d2ad6800000Gadgets found:
pop rdi; ret @ 0x40117e,ret @ 0x40101a(from inline assembly)Targets calculated:
system @ 0x7d2ad6858750,/bin/sh @ 0x7d2ad69cb42fShell achieved: Interactive shell with full user privileges
Payload efficiency: 104 bytes total (72 offset + 32 ROP chain)
Key Technical Insights:
CET compatibility: Despite SHSTK/IBT being enabled, ret2libc works because we use legitimate gadgets
Automatic discovery: pwntools dynamically finds gadgets and calculates addresses
Two-stage approach: Leak → Calculate → Exploit pattern works reliably
Stack alignment: Added
retgadget for 16-byte alignment beforepop rdiModern mitigations: NX enabled prevents shellcode, but ROP bypasses this restriction
ret2dlresolve Requirements:
Partial RELRO (lazy binding enabled)
Ability to write to known address (for fake structures)
Sufficient gadgets (pop rdi, pop rsi, pop rdx minimum)
Compatible libc/dynamic linker version
Reality Check:
In practice, ret2libc with leak is used in 90%+ of real exploits because:
More reliable across libc versions
Simpler to implement and debug
Works with modern mitigations
Most vulnerabilities provide some leak (format string, heap, etc.)
ret2dlresolve is mainly useful for:
CTF challenges that specifically block leaks
Research and educational purposes
Very constrained scenarios with no leak primitive
one_gadget - Quick Shell Gadgets
What is one_gadget?:
Finds "magic gadgets" in libc that spawn shells with single jump
Much faster than building full ROP chains
Constraints must be satisfied (register/stack state)
Usage:
Practical Exercise
Exercise: The Challenge (fmt_challenge.c)**
The goal is to modify the secret_code variable to 0x1337 to unlock the shell. This program runs in a loop, allowing you to test multiple format strings in one session.
Compile:
Testing & Offset Finding
Exploitation (Python)
write the python exploit using pwn tools
Exercise: Bypassing libc with execve Syscall (SROP)
Why This Matters: On modern systems with CET (glibc 2.34+), system() via ROP crashes due to IBT/Shadow Stack checks on libc functions. This exercise shows how to bypass libc entirely using a direct execve syscall via SROP.
Vulnerable Program:
Finding Required Gadgets:
Complete SROP Exploit (No libc Required):
How It Works:
Why This Bypasses CET (Partially):
Troubleshooting:
Crash before sigreturn
Wrong gadget addresses
Use objdump -d to verify gadget locations
Payload too large
Signal frame is 248 bytes
Ensure read() size ≥ 344 bytes (72+24+248)
SIGSEGV after sigreturn
Invalid RSP in frame
Set RSP to valid stack address (use buf_addr)
execve returns EFAULT
Bad /bin/sh address
Verify string address with readelf -x .data
No gadgets found
Binary too small
Add inline asm gadgets or use libc
Shell doesn't spawn
Wrong syscall number
AMD64 execve = 59, verify with SYS_execve
system() crashes (SIGSEGV)
Stack misalignment
Add ret gadget before call for 16-byte align
Process exits immediately
Shell has no stdin
Ensure stdin is connected to process
CET blocks ROP chain
Hardware shadow stack
Use ENDBR64 gadgets or disable CET for demo
Exercise: vuln_fmt
Task 1: Information Disclosure
Compile vuln_fmt.c
Find format string offset
Leak stack values
Identify libc addresses on stack
Calculate libc base (if ASLR enabled)
Task 2: Arbitrary Read
Read memory at arbitrary address
Leak binary strings
Find interesting addresses (GOT entries)
Document memory layout
Task 3: GOT Overwrite
Compile fmt_got.c
Find exit() GOT entry
Find win() function address
Overwrite GOT with format string
Redirect exit() to win()
Get shell
Success Criteria:
Can read arbitrary memory
Can write arbitrary values
GOT overwrite successful
Shell obtained via format string
Exercise: format string over network
Exploit a format string over a network:
Key Takeaways
Format strings are powerful: Read/write arbitrary memory
%n is dangerous: Enables memory writes
GOT is common target: Redirect execution flow - bypasses CET!
pwntools simplifies exploitation: Automates offset finding
Easy to prevent: Just use printf("%s", input)
one_gadget works well with GOT overwrites: RBP usually valid, constraints easier
SROP bypasses CET entirely: Direct syscalls don't use libc
Discussion Questions
Why is %n particularly dangerous compared to other specifiers?
How does Partial RELRO vs Full RELRO affect GOT overwrites?
What makes format strings easier to exploit than buffer overflows?
How can static analysis detect format string vulnerabilities?
Day 6: Logic Bugs and Modern Exploit Primitives
Goal: Understand non-memory-corruption exploitation and modern primitives that bypass traditional mitigations.
Activities:
Online Resources:
Exercise:
Exploit a race condition vulnerability
Trigger a type confusion bug
Understand when logic bugs are more practical than memory corruption
Deliverables
Race PoC: a script that wins the race at least once and demonstrates the impact (e.g., reads forbidden data)
Type confusion PoC: input + steps that trigger the bug and show corrupted behavior or a privileged action
Notes: explain the broken invariant and why mitigations (NX/ASLR/CET) don't stop it
Why Logic Bugs Matter
Why Logic Bugs Win:
Mitigations don't apply: DEP, ASLR, CFG, CET protect memory—not logic
Often simpler: No shellcode, no ROP chains, no heap feng shui
Higher reliability: Deterministic vs probabilistic exploitation
Stealthier: Less anomalous behavior for detection
Race Condition Exploitation
TOCTTOU (Time-of-Check to Time-of-Use):
Exploitation:
Test Results:
TOCTTOU Race Success:
Setup complete: SetUID binary created with root ownership and permissions
Attack automation: Background script continuously swaps files between safe and malicious
Race won multiple times: Successfully read root-owned secret file 3 times
Privilege escalation: Bypassed file ownership and permission checks
Protected symlinks: Required sudo for symlink creation due to modern Linux protections
Why This Works:
Time window:
sleep(1)in vulnerable code creates race opportunityFile swap: Rapid switching between legitimate file and malicious symlink
Check vs use: Security checks performed on safe file, but opens malicious one
Privilege escalation: SetUID binary runs with root privileges during file open
Modern mitigations: Protected symlinks require root ownership, but race still works
User-Space Double-Fetch Simulation:
Compile and Run:
Test Results:
Double-Fetch Race Success:
Race efficiency: Won race in 863 attempts (relatively quick for multi-threaded race)
Vulnerability demonstrated: Length checked as 32 bytes, would copy 200 bytes
Heap overflow potential: 168-byte overflow could corrupt heap metadata
Threading model: Attacker thread flips between safe/dangerous values, victim thread processes
Real-world impact: Could lead to arbitrary code execution via heap corruption
Why This Works:
Two separate reads: Length read twice with different values due to race
Timing window: Small delay between check and use allows race condition
Memory allocation: Based on safe length (32 bytes) but copy uses dangerous length (200)
Heap corruption: Overflow could overwrite adjacent heap chunks or metadata
Multi-threading: Concurrent access to shared memory creates race condition
Complete TOCTTOU Practical Exercise:
Here's a self-contained TOCTTOU lab with attack automation:
TOCTTOU Exploitation Script:
Running the TOCTTOU Lab:
Test Results:
TOCTTOU Lab Success:
Instant race win: Won on first attempt (0 attempts) due to effective race timing
Security bypass: All checks passed for safe file, but read
/etc/passwdinsteadPrivilege escalation: Successfully read system file despite security restrictions
Path validation: Initial checks passed for
/tmp/tocttou_safe/attack(safe location)File swap: Race condition swapped safe file with symlink to
/etc/passwd
Why This Works:
Path validation: Checks performed on safe file in allowed directory
File swap: Race condition replaces safe file with malicious symlink
Open operation: Opens whatever file exists at path during actual read
Security bypass: All validation passes, but reads different file entirely
System access: Gains read access to sensitive system files
Type Confusion Exploitation
What is Type Confusion?:
Type confusion occurs when code treats an object as a different type than it actually is. Unlike memory corruption, the memory itself is valid—the interpretation is wrong.
Exploitation:
Expected Output:
C++ Virtual Function Exploitation (Vtable Smashing)
In C++, dynamic polymorphism is implemented using Virtual Method Tables (vtables). This is the most common target in modern browser and game exploitation.
Memory Layout:
An object with virtual functions contains a hidden pointer (vptr) at the very beginning (offset 0) pointing to a table of function pointers (vtable).
The Vulnerability:
If you can overwrite the vptr (via UAF or Overflow), you can point it to a fake vtable you created in memory. When the program calls object->virtualFunction(), it fetches the pointer from your fake table and executes it.
Vulnerable Example:
Exploitation Strategy:
Vtable Smashing Success:
Binary analysis: C++ program with Partial RELRO, no PIE, CET enabled
Object size: 40 bytes (8-byte vptr + 32 bytes data)
UAF exploitation: Used freed object to write fake vtable
Vtable hijacking: Successfully overwrote vptr to point to
win()functionCode execution: Virtual function call redirected to attacker-controlled address
Why This Works:
Use-After-Free: Program continues using freed object pointer
Vtable location: vptr at offset 0 points to function table
Fake object: Attacker controls heap memory, creates fake vtable
Pointer overwrite: vptr overwritten with
win()function addressVirtual dispatch:
speak()virtual call jumps to attacker-controlled function
Key Technical Details:
CET bypass: Vtable smashing bypasses CET because it uses legitimate virtual dispatch
Heap control: UAF provides write-what-where primitive on heap
Object layout: 8-byte vptr followed by data members
Function pointer:
win()at0x401256used as fake virtual function
Advanced Technique: Heap Spray for Fake Vtable:
Why Vtable Attacks Matter:
Location
Explicit in struct
Hidden at object start
Detection
Easier to spot in code review
Implicit, harder to audit
Prevalence
C code, callbacks
All C++ polymorphic classes
Real-world targets
Legacy C apps
Browsers, games, office apps
Mitigation: VTable Integrity checks (CFI, VTV)
Clang CFI: Validates vtable pointers at virtual calls
GCC VTV: Verifies vtable via separate validation tables
MSVC CFG: Control Flow Guard for indirect calls
Bypassing VTable Protections:
Use existing vtables: Point to legitimate vtable of wrong type (type confusion)
Partial vtable corruption: Overwrite single entry if vtable is writable
COOP attacks: Chain existing virtual functions
Use-After-Free Again
Some UAF fundamentals:
Exploitation Strategy:
Test Results:
UAF Exploitation Success:
Binary analysis: No PIE (fixed addresses), Partial RELRO, CET enabled but bypassed
Memory reuse: Profile allocated at same address
0x1a1f56b0as freed NoteFunction pointer overwrite: Successfully overwrote print pointer from
0x401216to0x40124c(win())Heap layout: 40-byte structure with 32-byte name + 8-byte function pointer
Code execution: UAF triggered, called attacker-controlled function pointer
Why This Works:
Dangling pointer: freed Note pointer still accessible in notes array
Heap reuse: malloc reuses freed memory for profile allocation
Precise overwrite: 32 bytes padding + 8 bytes function pointer = 40 bytes total
Function hijack:
notes[0]->print()callswin()instead ofprint_note()CET bypass: Legitimate function pointer call bypasses CET restrictions
Data-Only Attacks
Concept: Corrupt data, not code pointers. Bypasses CFG, CET, and most CFI.
Exploit:
Test Results:
Data-Only Attack Success:
Compilation warnings:
gets()deprecated but still works for demonstrationPrivilege escalation: Successfully gained admin access and shell
Data corruption: Changed
is_adminfrom 0 to 1 without code pointer modificationBypass all mitigations: CFG, CET, DEP all ineffective against data-only attacks
Shell access: Achieved interactive shell with current user privileges
Why This Matters:
No code pointer corrupted: CFG won't help - no indirect calls to validate
No return address modified: CET won't help - no control flow changes
No shellcode executed: DEP won't help - no executable memory needed
Just changed a data value: Modified
is_adminflag to bypass authenticationReal-world impact: Many vulnerabilities are data corruption, not code execution
Out-of-Bounds Read/Write (Infoleak & Primitive)
Why it matters: OOB reads are common in parsers and image/video codecs. They often leak sensitive memory (infoleak) or, when combined with integer overflows, turn into OOB writes that corrupt adjacent objects.
Vulnerable Pattern:
Exploitation Flow:
OOB Read: Use negative/large index to read heap metadata or adjacent object pointers.
Leak: Extract libc or heap addresses from the leaked data.
OOB Write: Overwrite a function pointer or vtable in an adjacent object.
Trigger: Call the overwritten pointer to gain code execution.
pwntools Exploit Example:
Key Insights:
OOB reads are powerful infoleaks: They bypass ASLR by leaking heap/libc addresses.
OOB writes enable corruption: Can overwrite function pointers, vtables, or heap metadata.
Combination is deadly: Leak first, then write with precise targeting.
Common in parsers: Image/video codecs often have array bounds issues.
Detection & Mitigation:
Off-by-One / Partial Overwrite
Why it matters: Off-by-one errors are subtle but extremely common. They can corrupt heap metadata (size fields) or stack canaries, leading to powerful exploits.
Vulnerable Pattern:
Exploitation Strategy:
Heap Layout: Create adjacent chunks to control what gets corrupted.
Partial Overwrite: Use off-by-one to modify size field or least significant byte of pointer.
Chunk Overlap: Corrupted size leads to overlapping chunks during free/malloc.
Arbitrary Write: Use overlapping chunks to write to arbitrary addresses.
pwntools Exploit Example:
Key Insights:
Single byte matters: Off-by-one can corrupt critical metadata (size fields, pointers).
Heap metadata targeting: Size field corruption enables out-of-bounds writes beyond allocated buffer.
Partial pointer overwrite: Can bypass ASLR by corrupting only LSB of pointers.
Common in string operations:
strncpy,snprintfoften have off-by-one issues when boundary is miscalculated.Heap layout exploitation: Corrupted size field allows writing to adjacent heap chunks.
Backward exploitation: Negative offsets enable writing to lower memory addresses when target is allocated before source.
Critical Success Factors:
Heap allocation order: Target must be allocated before source buffer for backward offset exploitation
Signed arithmetic: Use
ssize_tinstead ofsize_tfor proper negative offset handling (unsigned comparison breaks)Size field corruption: Off-by-one on size metadata creates exploitable out-of-bounds condition
Offset calculation: Calculate signed distance from corrupted buffer to target function pointer
Direct memory write: Use corrupted buffer bounds to write directly to target address via pointer arithmetic
Practical Exercise
Exercise: Data-Only Attack
Context: CFG (Windows) and CET (Intel) are becoming ubiquitous. Control-flow hijacking is increasingly blocked. Data-only attacks are the future.
Challenge: Achieve privilege escalation WITHOUT corrupting any code pointers.
Why Data-Only Attacks Are the Future:
Real-World Data-Only Targets:
Permission flags
is_admin, user_role
Privilege escalation
Authentication state
is_authenticated
Auth bypass
Pointer indices
array_index
Arbitrary read/write
Object references
file_descriptor
File access
Crypto keys
session_key
Decryption
Network config
allowed_hosts
Access control bypass
Defense: These attacks require data-flow integrity (DFI), not just control-flow integrity. DFI is still largely a research topic.
Exercise: Logic Bug Exploitation
Challenge: Exploit without memory corruption
Key Takeaways
Logic bugs bypass mitigations: DEP/ASLR/CFG don't protect against logic flaws
Type confusion is powerful: Treating objects as wrong type leads to corruption
UAF gives control: Dangling pointers let attackers control object contents
Data-only attacks work: Corrupting non-pointer data achieves goals
Race conditions exist everywhere: Check-use gaps are exploitable
Discussion Questions
Why can't Control Flow Integrity (CFG/CET) stop data-only attacks?
How does type confusion differ from a traditional buffer overflow?
In the race condition example, why doesn't adding a mutex fully fix the bug?
What makes UAF exploitation reliable compared to stack overflows?
Day 7: Integer Overflows and Putting It All Together
Goal: Understand integer overflow exploitation and complete a multi-stage exploit.
Activities:
Online Resources:
Tool Setup:
UBSan (Undefined Behavior Sanitizer)
Static analysis tools
Exercise:
Exploit integer overflow leading to buffer overflow
Build complete multi-stage exploit chain
Deliverables
PoC input: a concrete
(count, size, data)(or equivalent) that triggers the overflow, with the math shownPrimitive proof: demonstrated out-of-bounds write / heap overflow caused by the overflow
Exploit: a pwntools script that completes the multi-stage chain (reaches code execution)
Notes: root cause + the minimal safe fix (bounds/overflow checks)
Understanding Integer Overflows
What is Integer Overflow?:
Arithmetic result exceeds type's maximum value
Wraps around to minimum (or vice versa)
Can lead to unexpected behavior
Examples:
Vulnerable Pattern: Size Calculation
Common Vulnerability:
Exploitable Example (int_overflow.c):
Exploitation:
Real-World Example: CVE-2023-4863 (libwebp)
This critical vulnerability (CVSS 8.8) affected Chrome, Firefox, and billions of devices. It was a heap buffer overflow in the WebP lossless compression (VP8L) decoder, caused by improper handling of Huffman table sizes.
Simplified Vulnerability Concept:
Exploitation Flow:
Craft malicious WebP image with specific Huffman code lengths
Trigger heap overflow when image is decoded
Corrupt adjacent heap metadata or objects
Achieve code execution in browser renderer process
Key Lesson: Integer-related bugs in size calculations are extremely common in parsers (images, fonts, documents) and lead to heap overflows. Always validate calculated sizes before use.
Detecting Integer Overflows
Using UBSan:
Multi-Stage Exploit Challenge
Final Challenge: Combine multiple techniques
Vulnerable Application (challenge.c):
Vulnerabilities Present:
Integer overflow in size calculation (
total_size = sizeof(User) + name_len) - present but not exploitedHeap overflow via strcpy (no bounds checking on name) - present but not exploited
Use-after-free (delete doesn't NULL the pointer in users array) - EXPLOITED
Arbitrary write primitive (write_data function) - EXPLOITED
Exploitation Chain (Multi-stage tcache poisoning):
Stage 1 - Heap Leak: Get addresses from program output (no ASLR)
Stage 2 - Setup: Create victim and target users
Stage 3 - UAF: Free victim, pointer remains in users array
Stage 4 - Tcache Poisoning: Corrupt freed chunk's fd pointer using Safe-Linking
Stage 5 - Arbitrary Write: Use write primitive to overwrite function pointer
Stage 6 - Trigger: Call print to execute admin_print() and get shell
The Technique: Modern tcache poisoning with Safe-Linking bypass!
Multi-Stage Exploit (AMD64) - Tcache Poisoning:
try to write it yourself, then look at it
Expected Output:
[!TIP] Why Tcache Poisoning Failed Here:
The tcache poisoning technique is correct, but in this specific challenge:
Variable-size allocations make tcache behavior unpredictable
The
writeprimitive corrupts the tcache structureMultiple chunks in tcache can cause unexpected behavior
The exploit demonstrates both approaches:
Tcache poisoning - The "proper" heap exploitation technique
Direct overwrite - Simpler when you have arbitrary write
In real-world scenarios without arbitrary write, you'd need to:
Carefully control allocation sizes
Ensure tcache has only one entry
Avoid corrupting tcache metadata
Key Takeaways:
Tcache poisoning is powerful - Can turn UAF into arbitrary write
Safe-Linking adds complexity - Need heap leak to calculate mangled pointers
Heap exploitation is tricky - Small details matter (sizes, alignment, metadata)
Multiple approaches exist - Use the simplest one that works
Modern glibc is harder - More protections than older versions
Compilation and Testing:
Capstone Project - The Exploitation Gauntlet
Goal: Apply all techniques to exploit a custom vulnerable server with multiple bugs.
Activities:
Analyze: Review source code for
vuln_server.Plan: Identify Stack Overflow, UAF, and Format String bugs.
Exploit: Write reliable Python exploits for each.
Chain: Combine leaks and overwrites for a full RCE chain.
Deliverables
Recon: map all bugs in the server (stack, heap, format string, logic, integer)
Exploit chain: a single pwntools script that chains at least two primitives (e.g., leak → heap corrupt → shell)
Reliability: exploit works >90% of the time
Writeup: brief explanation of which primitives you used and why
The Challenge: VulnServer v1.0 (AMD64)
You are provided with a binary vuln_server running on port 1337. It has the following commands:
auth <name>: Vulnerable to Stack Overflow → Requires ROP chain (NX enabled!)echo <msg>: Vulnerable to Format String → Provides libc leaknote <id> <text>: Vulnerable to UAF (delete/use).
VulnServer Source Code (vuln_server.c):
Compile VulnServer (AMD64 - NX ENABLED!):
Critical Note on Exploitation:
The handle_auth() function in the provided source uses memcpy(username, data, 200) instead of the traditional strcpy(). This is intentional for the training exercise.
Why this matters:
strcpy()stops copying at the first null byte (\x00)x86-64 addresses always contain null bytes (e.g.,
0x0000000000401000)With
strcpy(), the return address would never be fully overwrittenmemcpy()with a fixed length allows null bytes, making the exploit work
In real-world scenarios with strcpy():
Direct stack overflow exploitation would fail
You would need to chain vulnerabilities (format string + UAF)
Or find alternative input methods (binary protocols, file uploads)
Or use partial overwrites (limited effectiveness on x86-64)
This is an important lesson: not all vulnerabilities are directly exploitable due to input validation constraints!
Vulnerability Summary:
auth <name>
Stack Buffer Overflow (memcpy)
Control RIP
Direct jump to admin_shell
echo <msg>
Format String (snprintf)
Leak + Write
Leak libc addresses, overwrite GOT
note create/delete/show/edit
Use-After-Free
Control function pointer
Redirect to admin_shell
data alloc <size>
Integer Overflow
Heap overflow
Corrupt heap metadata
Note: The auth command uses memcpy() instead of strcpy() to allow null bytes in the payload. With strcpy(), this vulnerability would require chaining with other bugs (format string or UAF) for exploitation.
Exploitation Strategy:
Phase 1 - Stack Overflow (Direct Approach):
Calculate offset to return address (72 bytes)
Build payload with admin_shell address
Handle stack alignment (add
retgadget)Send payload and get shell
Phase 2 - Information Gathering (For Advanced Techniques):
Use
echo %p.%p.%p.%p.%p.%p.%p.%pto leak stack addressesIdentify libc pointers (start with 0x7f on AMD64)
Calculate libc base address (must end in 000)
Phase 3 - Alternative Attack Vectors (Optional):
Option A: Format string arbitrary write → overwrite GOT entry
Option B: Stack overflow with ROP → ret2libc (requires libc leak)
Option C: UAF → overwrite function pointer with admin_shell
Option D: Integer overflow → heap corruption → control flow hijack
Task:
Primary Goal: Exploit
authcommand to get shell via direct jump to admin_shellSecondary Goals (choose at least ONE):
Use
echoformat string to leak libc addressesExploit
noteUAF to redirect control flowExploit
datainteger overflow for heap corruption
Advanced Goal: Chain multiple vulnerabilities for a complete exploit
Practical Exercise (AMD64)
The Capstone Challenge: Build working exploits for VulnServer
Task 1: Stack Overflow (auth command) - START HERE
Task 2: Format String Leak (echo command)
Task 3: UAF Exploit (note command)
Task 4: Integer Overflow (data command)
Task 5: Full Chain Exploit (combine multiple techniques)
Task 1
The working exploit requires modifying handle_auth() to use memcpy() instead of strcpy() because strcpy() stops at null bytes, and x86-64 addresses always contain null bytes. write it yourself, look at this in case you got stuck
Real-World Implications:
Input Validation Matters: Many functions stop at null bytes (strcpy, scanf, gets, string functions)
Vulnerability Chaining: In real scenarios, you'd chain the format string or UAF vulnerabilities
Alternative Input Methods: Look for binary protocols, file uploads, or other non-string inputs
Partial Overwrites: On some architectures, you can overwrite just lower bytes (limited on x86-64)
Alternative Exploitation Paths (without modifying the server):
Format String Arbitrary Write: Use
echocommand to write to GOT or function pointersUse-After-Free: Exploit
notecommand to control function pointer (no null bytes needed)Integer Overflow: Use
datacommand for heap corruption leading to arbitrary writeVulnerability Chaining: Combine multiple bugs for complete exploitation
Capstone Checklist
Task 1: Stack Overflow (Primary Goal) [x] Working exploit provided
Buffer overflow offset found (72 bytes)
admin_shell address located
ret gadget found for stack alignment
Payload built correctly (padding + ret + admin_shell)
Shell obtained reliably (>90% success)
Understand why memcpy() is used instead of strcpy()
Task 2: Format String Leak
Format string vulnerability confirmed via
echocommandStack layout mapped (positions 1-20)
Libc addresses identified (0x7f...)
Libc base calculated correctly (ends in 000)
Leak is reliable (100% success rate)
Note: Exploit code provided in earlier sections (find_libc_offset.py)
Task 3: Use-After-Free
UAF vulnerability confirmed via
notecommandNote struct layout understood (64 bytes content + 8 bytes function pointer)
admin_shell address located
Limitation identified: strcpy prevents null bytes in function pointer
Alternative: Use format string to write function pointer
Task 4: Integer Overflow
Integer overflow identified in
data alloccommandVulnerability:
size + 1can wrap to 0 if size = 0xffffffffHeap overflow potential confirmed
Note: Exploitation requires heap feng shui techniques
Documentation
Working exploit (pwn_vuln_server.py) tested and understood
Understand the strcpy() vs memcpy() lesson
Know why direct exploitation of strcpy() buffer overflow fails
Understand alternative exploitation paths (format string, UAF)
Document lessons learned about input validation constraints
Key Takeaways
Exploitation is Engineering: It requires precision, planning, and debugging. It's not just running a script.
Primitives are Building Blocks: A "crash" is useless. A "write-what-where" is powerful.
Reliability separates Pros from Script Kiddies: An exploit that works 100% of the time is infinitely better than one that works 10% of the time.
Mitigations Change the Game: Everything you learned this week assumes no mitigations. Next week, you'll see how ASLR and DEP break these techniques (and how to fix them).
CET Changes Modern Exploitation: On glibc 2.34+, ROP to
system()may fail; use one_gadget with RBP fix or function pointer overwrites instead.Know Your Attack Surface: Function pointers and GOT bypass CET; ROP chains don't.
Input Validation Matters: Functions like
strcpy()stop at null bytes, making some exploits impossible without modification or vulnerability chaining.Real-World Constraints: The strcpy() limitation in this exercise teaches an important lesson - not all vulnerabilities are directly exploitable due to input validation.
Discussion Questions
How can integer overflows lead to exploitable conditions? Give examples of vulnerable size calculations.
Why are integer overflows particularly dangerous in parsers (images, fonts, documents)?
What's the difference between signed and unsigned integer overflow behavior in C?
In the multi-stage exploit, how do you chain primitives from different vulnerability classes?
Which vulnerability class did you find most difficult to exploit this week, and why?
How would ASLR affect the exploits you built this week? What information would you need to leak to bypass it?
What makes data-only attacks valuable in modern exploitation scenarios where CFI/CET is enabled?
Bridging to Windows (Week 6 Preparation)
The techniques you learned this week apply to Windows with some modifications:
execve("/bin/sh")
WinExec("cmd.exe")
Different API, same goal
GOT/PLT
IAT (Import Address Table)
Similar lazy binding concept
Stack canary
/GS cookie
XOR'd with stack frame pointer on Windows
NX bit
DEP
Same hardware feature
ASLR
ASLR + High Entropy VA
More entropy on 64-bit Windows
Signal handlers
SEH (Structured Exception Handling)
Different exploitation approach (chain overwrites)
glibc heap
NT Heap / Segment Heap
Different allocator internals and metadata
Format strings
Same vulnerability
Different format specifiers (%p, %n work)
ROP gadgets
Same technique
Different calling convention (stack-based args)
one_gadget
Magic gadgets in system DLLs
Similar concept, different tools
Techniques Covered This Week
Day 1: Stack buffer overflow, shellcode execution, NOP sleds, offset finding Day 2: ret2libc, ROP chains, libc leaks, one_gadget, stack alignment Day 3: Heap fundamentals, heap overflow, fastbin/tcache poisoning Day 4: Modern heap techniques (House of Botcake, House of Water, House of Tangerine), safe-linking bypass Day 5: Format string exploitation, arbitrary read/write, GOT overwrites, FSOP Day 6: Logic bugs, data-only attacks, UAF exploitation, race conditions Day 7: Integer overflows, multi-stage exploits, combining primitives
Looking Ahead to Week 6
Next week introduces modern exploit mitigations (DEP, ASLR, stack canaries, CFI/CET) and how they prevent the techniques you learned this week. You'll learn to:
Identify active mitigations using
checksec,vmmap, and runtime analysisUnderstand protection mechanisms (how they work internally)
Recognize when mitigations are improperly configured or bypassable
Prepare for Week 7's mitigation bypass techniques (info leaks, partial overwrites, heap spraying, etc.)
The goal is to understand what each mitigation protects against and why it works before learning how to defeat it.
Last updated