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 execstack for ret2shellcode labs, /GS-)

  • ASLR: Keep enabled system-wide; disable per-process (setarch -R) or in GDB (set disable-randomization on) for deterministic labs

  • Focus: Pure exploitation techniques without bypass complexity

Day 1: Environment Setup and Stack Overflow Fundamentals

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 strcpy without 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.sh passes and you recorded its output

  • Binary: vuln1 built and verified with checksec

  • Primitive proof: RIP control demonstrated (controlled crash address)

  • Exploit: exploit1.py (or equivalent) spawns a shell reliably

  • Notes: 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-venv for 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:

Feature
x86 (32-bit)
AMD64 (64-bit)

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):

  • syscall instruction triggers syscall (NOT int 0x80!)

  • rax = syscall number

  • rdi, 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 rsp or call rsp gadget provides a stable return target since RSP points to our controlled data after ret.

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 RBP

  • Return address at 0x7ffd11d25d18 contains our value 0xdeadbeefcafe

  • Offset confirmed: 72 bytes (64 buffer + 8 saved RBP) before return address

Method 2: Step-by-Step GDB Analysis (AMD64)

Common Debugging Scenarios:

Symptom
Likely Cause
Debug Command

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 nc or sockets are often fully buffered or line-buffered.

    • Always use p.recvuntil(b'prompt') before sending. Never rely on sleep() 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 \n and not just raw bytes.

  • Environment Variables

    • Remote servers have different env vars 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 -i or env={} 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:

  1. Compile Target (AMD64):

  2. Find Offset (AMD64 uses 8-byte patterns):

  3. Find Stack Address (or jmp rsp gadget):

  4. 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())

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

  1. Reproduce the crash reliably (>= 9/10) using the exact same input path and environment.

  2. Generate a core dump and confirm you control RIP.

  3. Replace your crashing bytes with a cyclic pattern and recover the exact offset.

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

  1. Build the target with AFL++ instrumentation.

  2. Run afl-fuzz until you get a crash.

  3. Minimize the crashing input with afl-tmin.

  4. Use the minimized crash to recover the offset and build a working exploit.

Success Criteria:

  • A fuzzer-generated input crashes the program

  • afl-tmin produces a smaller reproducer that still crashes

  • You 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

  1. Forgetting endianness: x86/x64 is little-endian. 0xdeadbeef becomes \xef\xbe\xad\xde

  2. Wrong architecture: AMD64 shellcode won't work in 32-bit process (and vice versa!)

  3. Using p32() on AMD64: Always use p64() for 64-bit binaries

  4. Bad characters: Null bytes (\x00) terminate strings in strcpy. Other common bad chars: \x0a (newline), \x0d (carriage return), \x20 (space)

  5. Stack alignment: AMD64 requires 16-byte alignment before call for some libc functions (add extra ret gadget if crashes in libc)

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

Instruction
Bytes
Problem
Solution

mov rax, 0

48 c7 c0 00 00 00 00

Immediate 0

xor eax, eax31 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:

Original
Null-Free Replacement
Notes

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 shellcraft with encoders for complex shellcode:

Debugging Tips:

Key Takeaways

  1. Stack overflows overwrite return address: Control RIP (AMD64) / EIP (x86)

  2. Finding offset is critical: Use cyclic patterns (8-byte on AMD64!)

  3. NOP sleds improve reliability: Don't need exact address

  4. Stack must be executable: -z execstack required for shellcode

  5. Per-process ASLR disable: Use setarch -R or GDB, NOT system-wide

  6. AMD64 uses 8-byte addresses: Always use p64() not p32()

Discussion Questions

  1. Why does a NOP sled improve exploit reliability?

  2. What happens if ASLR is enabled but other protections are disabled?

  3. How would you modify your exploit if the vulnerable function used read() instead of gets()?

  4. What are the limitations of this technique in real-world scenarios?

  5. Why is AMD64 stack alignment (16-byte) important for exploit reliability?

Day 2: Return-to-libc and Introduction to ROP

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() or execve() from libc to spawn a shell, just like we will do today.

Deliverables

  • Binary: vuln2 built with NX enabled and verified with checksec

  • Leak stage: Stage 1 leak works and returns to main

  • Libc base: libc.address correctly computed from the leak

  • Final 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

  • libc provides 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:

  1. Never use p.libs() in final exploits - it only works locally for debugging

  2. Always leak, then compute - this works with ASLR enabled

  3. Stack alignment - AMD64 requires 16-byte alignment before call; add ret gadget

  4. Return to main - allows second stage after leak

  5. Fix RBP for one_gadget - buffer overflows corrupt RBP; one_gadgets need rbp-0xXX writable

  6. Modern 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 movaps instruction), you have a stack alignment problem. The stack must be 16-byte aligned before any call instruction.

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 with checksec: if SHSTK: Enabled and IBT: 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 ret

  • Chain 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:

Gadget
Purpose
Usage

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; ret gadgets and has CET enabled. Traditional system("/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:

Gadget Type
Byte Sequence
Instruction

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; ret is rare in modern libc! You'll often find pop rdx; pop rbx; ret or similar multi-pop variants. This breaks simple execve(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:

  1. pop rdi; ret - for leak stage (from binary, not libc)

  2. ret - for stack alignment (from binary)

  3. pop rbp; ret - CRITICAL for one_gadget RBP fix (from libc)

  4. pop rax; ret - for one_gadget RAX=0 constraint (from libc)

  5. 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 require rbp-0xXX to 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:

Symptom
Cause
Fix

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:

  1. Use one_gadget with proper constraints (shown above)

  2. Syscall directly via execve syscall (bypasses libc CET checks)

  3. 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.address BEFORE building the ROP chain! Don't create ROP([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):

Symptom
Cause
Fix

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:

RELRO Level
GOT Writable?
PLT Behavior
Exploitation Impact

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_hook or __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

  1. Compile target with NX (AMD64):

  2. Find gadgets:

  3. Write leak exploit:

    • Stage 1: ROP to puts(puts@got), return to main

    • Parse leaked puts address

    • Compute libc.address = leak - libc.symbols['puts']

  4. Write final exploit:

    • Stage 2: pop rdi; ret + /bin/sh + system

    • Get shell

Task 2: Stack Alignment Practice

  1. Create exploit WITHOUT ret alignment gadget

  2. Observe crash in libc (movaps instruction)

  3. Add ret gadget and verify fix

Task 3: Gadget Hunting

  1. Find gadgets manually:

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

  1. Write exploit:

    • Build ret2libc payload

    • Call system("/bin/sh")

    • Get shell

Exercise: Function chaining

  1. Chain system() and exit():

    • Call system("whoami")

    • Then call exit(0)

    • Observe clean exit

  2. Read flag file:

    • Create flag.txt with secret

    • Chain to call system("cat flag.txt")

    • Display contents

Exercise: Simple ROP (AMD64 syscall)

  1. Find gadgets:

  2. 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 syscall instruction

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

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

  2. Compile both with identical flags:

  3. Patch diff:

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

  1. NX prevents shellcode execution: Need alternative techniques

  2. ret2libc reuses existing code: Call libc functions

  3. ROP chains gadgets: Build complex operations

  4. Stack layout is critical: Function arguments must be correct

  5. pwntools simplifies ROP: Automates gadget finding and chaining

  6. Modern libc has CET: system() ROP may fail, use one_gadget instead

  7. one_gadget needs RBP fix: Buffer overflows corrupt RBP, set it to .bss first

  8. pop rdx is rare: Modern libc lacks clean gadgets, use one_gadget

Discussion Questions

  1. Why is ret2libc effective even with NX enabled?

  2. What are the limitations of ret2libc vs ROP?

  3. How would ASLR complicate ret2libc exploitation?

  4. What types of gadgets are most useful for ROP chains?

Day 3: Heap Exploitation Fundamentals

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_heap built and verified with checksec

  • Primitive proof: function pointer overwrite demonstrated (redirect to admin_function)

  • Exploit: exploit_heap_fp.py (or equivalent) spawns a shell reliably

  • Notes: heap layout diagram + exact overwrite length and why read() enables null bytes in payloads

Heap vs Stack

Differences:

Feature
Stack
Heap

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 how2heaparrow-up-right 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:

  1. Overflow user1->name (32 bytes)

  2. Overwrite user1->print_func with address of admin_function

  3. When 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 corruption

  • ret2libc 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:

  1. Overflow chunk 'a' into chunk 'b'

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

  3. Free chunk 'b'

  4. Unlink writes: _(fd) = bk and _(bk) = fd

  5. Arbitrary write achieved!

Modern Protections:

  • Safe unlinking (glibc 2.3.4+)

  • Checks: fd->bk == chunk && bk->fd == chunk

  • Makes 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+:

  1. Safe-linking key: key = chunk_address >> 12 (simple right shift)

  2. Target alignment: Must be 0x10-aligned to avoid "unaligned tcache chunk detected"

  3. Corruption position: Target the SECOND tcache entry (B), not the first (A)

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

Version
Change
Impact
Ubuntu Version

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:

  1. No heap leak needed: The key is derived from the chunk you're corrupting

  2. Simple formula: Just chunk_addr >> 12, not complex heap base calculations

  3. Alignment critical: Target must be 0x10-aligned or glibc aborts

  4. Position matters: Corrupt the SECOND tcache entry, not the first

[!NOTE] Exception: tcache_perthread_struct counts are NOT protected by safe-linking! This enables advanced techniques like House of Water for leakless attacks.

Modern Techniques Still Working:

  1. Tcache Stash Unlink (TSU): Smallbin → tcache manipulation

  2. House of Lore variants: Smallbin bk pointer corruption

  3. Largebin attacks: Still viable for arbitrary write

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

  1. Compile vuln_heap.c

  2. Find admin_function address

  3. Build exploit to overwrite print_func

  4. Get shell

Exercise: Heap Spray

  1. Allocate many chunks

  2. Fill with shellcode

  3. Trigger vulnerability to jump into spray

  4. Execute shellcode

Exercise: Tcache Poisoning (Modern)

  1. Study how2heap/tcache_poisoning.c

  2. Understand tcache bin structure

  3. Corrupt fd pointer

  4. Allocate at arbitrary address

Success Criteria:

  • Function pointer overwrite works

  • Heap spray successful

  • Understand modern heap protections

  • Can explain tcache attack surface

Key Takeaways

  1. Heap is more complex than stack: Multiple allocator structures

  2. Metadata corruption is powerful: Enables write-what-where

  3. Modern heaps have protections: Safe unlinking, tcache checks, safe-linking (2.32+)

  4. Function pointers are targets: Easy to exploit if reachable - bypasses CET!

  5. Heap spray can bypass ASLR: Fill memory with shellcode

  6. CET doesn't block function pointer overwrites: Unlike ROP, direct calls work

  7. Modern libc removed __malloc_hook: Can't use hook overwrites anymore (2.34+)

Discussion Questions

  1. Why is heap exploitation more complex than stack overflow?

  2. How do safe unlinking checks prevent classic unlink attacks?

  3. What makes tcache a good target for exploitation?

  4. How would you detect heap corruption at runtime?

Day 4: Heap Exploitation Part 2 – Modern Techniques

Deliverables

  • Environment: glibc version recorded (ldd --version) and which how2heap example(s) you used

  • Reproduction: 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_func found at static address 0x4011bc (No PIE)

  • UAF exploit successfully overwrote callback pointer

  • Shell spawned with user privileges

  • Interactive shell obtained, confirming full control

Why This Works:

  1. UAF Pattern: obj pointer isn't NULLed after free(), creating a dangling pointer

  2. Heap Reuse: spray() allocates same-sized chunk (sizeof(Object)) that reclaims freed memory

  3. Controlled Overwrite: Spray payload overwrites callback pointer with admin_func address

  4. Trigger: use() calls obj->callback() which now points to attacker-controlled function

  5. No ASLR Bypass Needed: Binary has No PIE, so admin_func address 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 area

  • Stack memory successfully allocated via heap allocator

Why This Works:

  1. No Next Chunk Validation: Unlike fastbin, tcache free() doesn't validate the next chunk's size field

  2. Simple Fake Chunk: Only need size field (0x40) in fake_chunks[1], no complex metadata

  3. Pointer Arithmetic: a = &fake_chunks[2] points to "user data" area of fake chunk

  4. Alignment: __attribute__((aligned(0x10))) ensures 16-byte alignment for modern glibc

  5. Size 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:

Aspect
Original (Fastbin)
Tcache Version

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:

  1. Find/create writable region with controlled data

  2. Set up fake size field (0x20-0x410 range, bits 1-2 = 0)

  3. Ensure 16-byte alignment

  4. Overwrite pointer to point to fake chunk's data region

  5. free(corrupted_ptr) → fake chunk goes to tcache

  6. 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 0x28d522a0 used to locate metadata

  • Tcache 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] and entries[1] directly

  • No Safe-Linking: Metadata corruption bypasses pointer protection

  • Immediate Effect: Next malloc(0x20) returns controlled address

  • Powerful 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 0x7ffda554a440

  • Chunks a at 0x2b5992a0 and b at 0x2b599330 allocated

  • After double free: tcache contains b -> a -> NULL

  • Safe-linking bypass: target ^ (b >> 12) written to b[0]

  • Second malloc(128) returned our stack target!

Why This Works:

  • Safe-Linking Bypass: XOR with (chunk_addr >> 12) defeats pointer protection

  • Double 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: a at 0x22c663f0, b at 0x22c66420

  • After tcache fill and double free: fastbin contains cycle a -> b -> a

  • Allocations: c gets a, d gets b, e gets a again!

  • Double allocation achieved: c and e point 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: prev at 0xcd4aa10, victim at 0xcd4ab20

  • Overlapping chunk gives write access to victim's metadata

  • Offset 34 words to victim's tcache next pointer

  • Safe-linking bypass: target ^ (victim >> 12) written

  • Arbitrary write achieved: 0xcafebabe written 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 0x7ffc62e176c0 initially contains 0

  • Large chunks: p1 at 0x1153c290, p2 at 0x1153c6e0

  • After setup: p1 in largebin, p2 in unsorted bin

  • Corrupted p1->bk_nextsize to point at target-0x20

  • Arbitrary write achieved: target now contains heap pointer 0x1153c6e0

Why This Works:

  • Large Bin Insertion: When p2 inserted into large bin, glibc writes to bk_nextsize->fd_nextsize

  • Weak Validation: glibc doesn't validate bk_nextsize if new chunk is smallest

  • Arbitrary 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 at 0x7ffd808c11c0

  • Before: stack filled with 0xcdcdcdcdcdcdcdcd pattern

  • After trigger: heap pointers written to stack at 0x7ffd808c11c0 and 0x7ffd808c11c8

  • Arbitrary 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_ctfarrow-up-right.

[!IMPORTANT] Key insight: The tcache_perthread_struct metadata 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)

Technique
Target
glibc Version
Notes

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:

Era
glibc Versions
Key Features

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)

Protection
glibc Version
Mitigation
Bypass

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_alloc

  • Attacker-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+):

Target
RELRO Required
CET Impact
Notes

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:

  1. GOT Overwrite (Partial RELRO only):

  2. Function Pointer in Heap Object:

  3. 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_func located at static address 0x4011ac (No PIE)

  • UAF exploit successfully overwrote function pointer

  • Shell spawned: WIN! Spawning shell... message confirms success

  • Interactive 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):

  1. Corrupt a FILE struct: Overwrite stdout, stderr, or forge a fake _IO_FILE

  2. Set fake vtable: Point vtable to attacker-controlled memory

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

  • stdout at 0x776c6a6045c0, original vtable at 0x776c6a602030 (legitimate)

  • Fake vtable on stack at 0x7ffe77115fd0 successfully written

  • glibc aborts: _IO_vtable_check() detects invalid vtable outside legitimate range

  • Proof: 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):

Technique
glibc Range
CET Status
Notes

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:

Scenario
Recommended Technique

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:

  1. Write exploit:

based on what you've learned, write the proper exploit

Exercise: Tcache House of Spirit

  1. Compile how2heap tcache_house_of_spirit.c:

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

  3. Key observation: Unlike original House of Spirit, tcache doesn't check next chunk metadata!

Exercise: Tcache Poisoning

  1. Compile the tcache_poisoning.c example:

  2. Trace execution in GDB:

  3. Observe tcache state:

    • Before free: tcache empty

    • After free: chunk in tcache

    • After poisoning: tcache points to target

    • After second malloc: arbitrary address returned

  4. Modify to target a function pointer:

    • Add a function pointer variable

    • Poison tcache to point to it

    • Overwrite with system or win function

Exercise: House of Botcake

  1. Run how2heap example:

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

  3. Key insight: Bypasses tcache double-free detection via consolidation trick

Exercise: Safe-Linking Bypass

  1. Study the protection:

  2. Write a heap leak + tcache poison exploit:

    • First, leak a heap address (via UAF read or other bug)

    • Calculate the XOR key: heap_addr >> 12

    • Mangle your target address before writing to tcache

    • Verify with ~/tuts/how2heap/glibc_2.39/tcache_poisoning.c

Exercise: Advanced Techniques

  1. Large Bin Attack:

    • Run ~/tuts/how2heap/glibc_2.39/large_bin_attack

    • Understand bk_nextsize corruption for arbitrary write

  2. House of Water (Expert):

    • Study the technique on how2heap wiki

    • Requires understanding of tcache metadata structure

  3. House of Tangerine (Expert):

    • Run ~/tuts/how2heap/glibc_2.39/house_of_tangerine

    • Key: Exploits sysmalloc _int_free without needing free()

Success Criteria:

Task
Criterion

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

  1. Start with UAF patterns: Understand the core vulnerability before learning exploitation techniques

  2. Tcache House of Spirit is easiest: No next chunk validation makes it simpler than fastbin variant

  3. House of Botcake is most practical: Double-free bypass works on all modern glibc (2.29+)

  4. Safe-linking bypass uses chunk address: XOR key derived from corrupted chunk (chunk_addr >> 12) for glibc 2.32+

  5. Know your glibc version: Technique selection depends heavily on target version

  6. how2heap/glibc_2.41 is your reference: Practice techniques in order of difficulty

  7. CET doesn't block heap exploits: Function pointer/GOT overwrites bypass SHSTK/IBT

  8. Hooks are dead: __malloc_hook/__free_hook removed in glibc 2.34+, use GOT or FSOP instead

Discussion Questions

  1. Why is tcache House of Spirit easier than the original fastbin version?

  2. How does safe-linking protect against tcache poisoning, and why does it require a heap leak to bypass?

  3. What makes House of Botcake the most practical double-free technique for modern glibc?

  4. When would you use House of Tangerine over House of Botcake?

Day 5: Format String Vulnerabilities

Deliverables

  • Binary: vuln_fmt built and verified with checksec

  • Offset: your correct format string offset found (the %<n>$p where you see 0x4141414141414141)

  • Leak: at least one stable pointer leak (stack/libc) parsed in Python

  • Write: one working %n write (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:

Specifier
Description
Stack Effect (AMD64)

%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 %p for leaking! It prints the full 64-bit value in hex format (0x7fff...). Using %x only 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$p shows format string pointer 0xa70243625

  • %1$p shows stack address 0x7ffdca13b4b0

  • Key: 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: 0x4141414141414141 marker found

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

  1. Calculate Offset: Determine how many 8-byte blocks it takes to reach the end of your format string.

  2. Argument Ordering: Use %<offset>$n to tell printf to skip ahead to that end-block where the address lives.

Example:

Arbitrary Memory Write (AMD64)

The %n Specifier (64-bit considerations):

  • %n writes 4 bytes (int) - often enough!

  • %ln writes 8 bytes (long) - full 64-bit

  • %hn writes 2 bytes (short)

  • %hhn writes 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_payload

  • exit@GOT overwritten: Redirected to win() function

  • Shell 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:

  1. Find GOT entry for common function (printf, exit, etc.)

  2. Use format string to leak libc address (defeat ASLR)

  3. Use format string to overwrite GOT entry

  4. Point GOT entry to system or one_gadget

  5. Trigger function call → shell!

Example Program (fmt_got.c):

Compile (AMD64):

Exploit (AMD64):

Test Results:

Manual GOT Overwrite Success:

  • Addresses found: win() at 0x401186, exit@GOT at 0x404030

  • Offset scanning: Tested offsets 1-9, format string found at various positions

  • Payload generated: Complex multi-byte write using %lln, %hhn specifiers

  • GOT overwritten: exit@GOT redirected to win() function

  • Shell 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@GOT to win() function

  • Shell 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 redirects exit() to win(), but win() doesn't exit the program

  • Root cause: After GOT overwrite, program calls win() and waits for input, but recvall() expects program to exit

  • Solution: Use fmtstr_payload() directly instead of execute_writes() for interactive shells

Manual fmtstr_payload (More Control):

Test Results:

Manual fmtstr_payload Success:

  • Direct approach worked: Used fmtstr_payload() instead of execute_writes()

  • Shell achieved: "You won!" message and interactive shell

  • write_size='short': Used %hn (2-byte writes) for reliability

  • Interactive 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 → base 0x77d09f400000

  • One_gadget problem: 0xe3b01 offset 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.6 to find working offsets

Key Lessons:

  1. FmtStr.execute_writes(): Not suitable for interactive shells - use fmtstr_payload() directly

  2. One_gadget offsets: System-specific, must be recalculated for each libc version

  3. Manual approach: fmtstr_payload() with write_size='short' is most reliable

  4. ASLR 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-security

  • Use fortify source: -D_FORTIFY_SOURCE=2

  • Static analysis tools: scan for printf(user_controlled)

SROP (Sigreturn-Oriented Programming) Explained

What is SROP?:

  • Uses the sigreturn syscall to set all registers at once

  • sigreturn restores register state from a "signal frame" on stack

  • Attacker 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; ret chain. SROP only needs syscall; 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 rdx gadgets.

  • 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:

  1. Overflow buffer

  2. Set return address to sigreturn gadget

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

  4. sigreturn loads all registers from fake frame

  5. Execution continues at RIP (syscall)

  6. 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=none disables shadow stack/IBT protection

  • Executable stack: -z execstack allows shellcode if needed

  • Inline 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:

Scenario
Use SROP
Use 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:

  1. Program calls printf@plt

  2. PLT jumps to GOT entry

  3. First call: GOT points back to PLT

  4. PLT calls _dl_runtime_resolve(link_map, reloc_index)

  5. Resolver finds "printf" in libc, updates GOT

  6. Future calls go directly to libc printf

ret2dlresolve Attack:

  1. Craft fake Elf_Rel structure (relocation entry)

  2. Craft fake Elf_Sym structure (symbol entry)

  3. Craft fake string "system"

  4. Call _dl_runtime_resolve with fake reloc_index

  5. Resolver "resolves" our fake "system" symbol

  6. 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 → base 0x7d2ad6800000

  • Gadgets found: pop rdi; ret @ 0x40117e, ret @ 0x40101a (from inline assembly)

  • Targets calculated: system @ 0x7d2ad6858750, /bin/sh @ 0x7d2ad69cb42f

  • Shell 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 ret gadget for 16-byte alignment before pop rdi

  • Modern 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:

Problem
Cause
Solution

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

  1. Compile vuln_fmt.c

  2. Find format string offset

  3. Leak stack values

  4. Identify libc addresses on stack

  5. Calculate libc base (if ASLR enabled)

Task 2: Arbitrary Read

  1. Read memory at arbitrary address

  2. Leak binary strings

  3. Find interesting addresses (GOT entries)

  4. Document memory layout

Task 3: GOT Overwrite

  1. Compile fmt_got.c

  2. Find exit() GOT entry

  3. Find win() function address

  4. Overwrite GOT with format string

  5. Redirect exit() to win()

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

  1. Format strings are powerful: Read/write arbitrary memory

  2. %n is dangerous: Enables memory writes

  3. GOT is common target: Redirect execution flow - bypasses CET!

  4. pwntools simplifies exploitation: Automates offset finding

  5. Easy to prevent: Just use printf("%s", input)

  6. one_gadget works well with GOT overwrites: RBP usually valid, constraints easier

  7. SROP bypasses CET entirely: Direct syscalls don't use libc

Discussion Questions

  1. Why is %n particularly dangerous compared to other specifiers?

  2. How does Partial RELRO vs Full RELRO affect GOT overwrites?

  3. What makes format strings easier to exploit than buffer overflows?

  4. How can static analysis detect format string vulnerabilities?

Day 6: Logic Bugs and Modern Exploit Primitives

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:

  1. Time window: sleep(1) in vulnerable code creates race opportunity

  2. File swap: Rapid switching between legitimate file and malicious symlink

  3. Check vs use: Security checks performed on safe file, but opens malicious one

  4. Privilege escalation: SetUID binary runs with root privileges during file open

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

  1. Two separate reads: Length read twice with different values due to race

  2. Timing window: Small delay between check and use allows race condition

  3. Memory allocation: Based on safe length (32 bytes) but copy uses dangerous length (200)

  4. Heap corruption: Overflow could overwrite adjacent heap chunks or metadata

  5. 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/passwd instead

  • Privilege 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:

  1. Path validation: Checks performed on safe file in allowed directory

  2. File swap: Race condition replaces safe file with malicious symlink

  3. Open operation: Opens whatever file exists at path during actual read

  4. Security bypass: All validation passes, but reads different file entirely

  5. 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() function

  • Code execution: Virtual function call redirected to attacker-controlled address

Why This Works:

  1. Use-After-Free: Program continues using freed object pointer

  2. Vtable location: vptr at offset 0 points to function table

  3. Fake object: Attacker controls heap memory, creates fake vtable

  4. Pointer overwrite: vptr overwritten with win() function address

  5. Virtual 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() at 0x401256 used as fake virtual function

Advanced Technique: Heap Spray for Fake Vtable:

Why Vtable Attacks Matter:

Aspect
Function Pointer
Vtable

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:

  1. Use existing vtables: Point to legitimate vtable of wrong type (type confusion)

  2. Partial vtable corruption: Overwrite single entry if vtable is writable

  3. 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 0x1a1f56b0 as freed Note

  • Function pointer overwrite: Successfully overwrote print pointer from 0x401216 to 0x40124c (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:

  1. Dangling pointer: freed Note pointer still accessible in notes array

  2. Heap reuse: malloc reuses freed memory for profile allocation

  3. Precise overwrite: 32 bytes padding + 8 bytes function pointer = 40 bytes total

  4. Function hijack: notes[0]->print() calls win() instead of print_note()

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

  • Privilege escalation: Successfully gained admin access and shell

  • Data corruption: Changed is_admin from 0 to 1 without code pointer modification

  • Bypass 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_admin flag to bypass authentication

  • Real-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:

  1. OOB Read: Use negative/large index to read heap metadata or adjacent object pointers.

  2. Leak: Extract libc or heap addresses from the leaked data.

  3. OOB Write: Overwrite a function pointer or vtable in an adjacent object.

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

  1. Heap Layout: Create adjacent chunks to control what gets corrupted.

  2. Partial Overwrite: Use off-by-one to modify size field or least significant byte of pointer.

  3. Chunk Overlap: Corrupted size leads to overlapping chunks during free/malloc.

  4. 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, snprintf often 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_t instead of size_t for 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:

Target Type
Example
Impact

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

  1. Logic bugs bypass mitigations: DEP/ASLR/CFG don't protect against logic flaws

  2. Type confusion is powerful: Treating objects as wrong type leads to corruption

  3. UAF gives control: Dangling pointers let attackers control object contents

  4. Data-only attacks work: Corrupting non-pointer data achieves goals

  5. Race conditions exist everywhere: Check-use gaps are exploitable

Discussion Questions

  1. Why can't Control Flow Integrity (CFG/CET) stop data-only attacks?

  2. How does type confusion differ from a traditional buffer overflow?

  3. In the race condition example, why doesn't adding a mutex fully fix the bug?

  4. What makes UAF exploitation reliable compared to stack overflows?

Day 7: Integer Overflows and Putting It All Together

Deliverables

  • PoC input: a concrete (count, size, data) (or equivalent) that triggers the overflow, with the math shown

  • Primitive 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:

  1. Craft malicious WebP image with specific Huffman code lengths

  2. Trigger heap overflow when image is decoded

  3. Corrupt adjacent heap metadata or objects

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

  1. Integer overflow in size calculation (total_size = sizeof(User) + name_len) - present but not exploited

  2. Heap overflow via strcpy (no bounds checking on name) - present but not exploited

  3. Use-after-free (delete doesn't NULL the pointer in users array) - EXPLOITED

  4. Arbitrary write primitive (write_data function) - EXPLOITED

Exploitation Chain (Multi-stage tcache poisoning):

  1. Stage 1 - Heap Leak: Get addresses from program output (no ASLR)

  2. Stage 2 - Setup: Create victim and target users

  3. Stage 3 - UAF: Free victim, pointer remains in users array

  4. Stage 4 - Tcache Poisoning: Corrupt freed chunk's fd pointer using Safe-Linking

  5. Stage 5 - Arbitrary Write: Use write primitive to overwrite function pointer

  6. 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 write primitive corrupts the tcache structure

  • Multiple chunks in tcache can cause unexpected behavior

The exploit demonstrates both approaches:

  1. Tcache poisoning - The "proper" heap exploitation technique

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

  1. Tcache poisoning is powerful - Can turn UAF into arbitrary write

  2. Safe-Linking adds complexity - Need heap leak to calculate mangled pointers

  3. Heap exploitation is tricky - Small details matter (sizes, alignment, metadata)

  4. Multiple approaches exist - Use the simplest one that works

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

  1. auth <name>: Vulnerable to Stack Overflow → Requires ROP chain (NX enabled!)

  2. echo <msg>: Vulnerable to Format String → Provides libc leak

  3. note <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 overwritten

  • memcpy() 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:

Command
Vulnerability
Primitive
Exploitation Goal

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:

  1. Phase 1 - Stack Overflow (Direct Approach):

    • Calculate offset to return address (72 bytes)

    • Build payload with admin_shell address

    • Handle stack alignment (add ret gadget)

    • Send payload and get shell

  2. Phase 2 - Information Gathering (For Advanced Techniques):

    • Use echo %p.%p.%p.%p.%p.%p.%p.%p to leak stack addresses

    • Identify libc pointers (start with 0x7f on AMD64)

    • Calculate libc base address (must end in 000)

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

  1. Primary Goal: Exploit auth command to get shell via direct jump to admin_shell

  2. Secondary Goals (choose at least ONE):

    • Use echo format string to leak libc addresses

    • Exploit note UAF to redirect control flow

    • Exploit data integer overflow for heap corruption

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

  1. Input Validation Matters: Many functions stop at null bytes (strcpy, scanf, gets, string functions)

  2. Vulnerability Chaining: In real scenarios, you'd chain the format string or UAF vulnerabilities

  3. Alternative Input Methods: Look for binary protocols, file uploads, or other non-string inputs

  4. Partial Overwrites: On some architectures, you can overwrite just lower bytes (limited on x86-64)

Alternative Exploitation Paths (without modifying the server):

  1. Format String Arbitrary Write: Use echo command to write to GOT or function pointers

  2. Use-After-Free: Exploit note command to control function pointer (no null bytes needed)

  3. Integer Overflow: Use data command for heap corruption leading to arbitrary write

  4. Vulnerability Chaining: Combine multiple bugs for complete exploitation

Capstone Checklist

Key Takeaways

  1. Exploitation is Engineering: It requires precision, planning, and debugging. It's not just running a script.

  2. Primitives are Building Blocks: A "crash" is useless. A "write-what-where" is powerful.

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

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

  5. CET Changes Modern Exploitation: On glibc 2.34+, ROP to system() may fail; use one_gadget with RBP fix or function pointer overwrites instead.

  6. Know Your Attack Surface: Function pointers and GOT bypass CET; ROP chains don't.

  7. Input Validation Matters: Functions like strcpy() stop at null bytes, making some exploits impossible without modification or vulnerability chaining.

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

  1. How can integer overflows lead to exploitable conditions? Give examples of vulnerable size calculations.

  2. Why are integer overflows particularly dangerous in parsers (images, fonts, documents)?

  3. What's the difference between signed and unsigned integer overflow behavior in C?

  4. In the multi-stage exploit, how do you chain primitives from different vulnerability classes?

  5. Which vulnerability class did you find most difficult to exploit this week, and why?

  6. How would ASLR affect the exploits you built this week? What information would you need to leak to bypass it?

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

Linux Concept
Windows Equivalent
Key Difference

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 analysis

  • Understand 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