ASLR is not a boolean. It is how many bits of each mapping still surprise the next run, minus whatever the process prints. This lab is a PIE toy that prints one pointer (a format-string style %p of a function address). I measure load bias in gdb, dump /proc/self/maps twice, and show the leak collapsing the text-segment entropy to zero. I do not then pivot into libc.
Lab binary
1/* aslr_lab.c — toy, prints one pointer on purpose */
2#include <stdio.h>
3#include <stdint.h>
4#include <unistd.h>
5
6static int marker(void)
7{
8 return 0x4d; /* 'M' */
9}
10
11int main(int argc, char **argv)
12{
13 if (argc > 1 && argv[1][0] == 'p') {
14 /* lab leak: text pointer as hex. production must not do this. */
15 printf("marker=%p\n", (void *)(uintptr_t)marker);
16 printf("main=%p\n", (void *)(uintptr_t)main);
17 }
18 /* always dump our own maps path so the note has a file */
19 printf("pid=%d\n", getpid());
20 return marker();
21}
1cc -O0 -fPIE -pie -g -o aslr_lab aslr_lab.c
2file aslr_lab
3# aslr_lab: ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked, not stripped
4
5readelf -h aslr_lab | egrep 'Type|Entry'
6 Type: DYN (Shared object file)
7 Entry point address: 0x6a0
8
9readelf -l aslr_lab | grep -A1 'LOAD'
10 LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
11 0x0000000000000c28 0x0000000000000c28 R E
Type: DYN plus an executable ELF is PIE. File VAs start at 0. Runtime VAs = bias + file VA. That subtraction is the whole lab.
/proc/self/maps — two runs, two biases
I exec a tiny helper so maps is the lab binary’s, not the shell’s.
1/* maps_dump.c — write /proc/self/maps to stdout */
2#include <stdio.h>
3int main(void) {
4 FILE *f = fopen("/proc/self/maps", "r");
5 char buf[256];
6 while (fgets(buf, sizeof buf, f))
7 fputs(buf, stdout);
8 fclose(f);
9 return 0;
10}
Easier: from the lab itself, or cat /proc/$(./aslr_lab &)/maps races. I use gdb’s info proc mappings plus a shell loop on the binary’s maps after pausing.
1$ ./aslr_lab p
2marker=0xaaaaaaab07a4
3main=0xaaaaaaab07c0
4pid=4120
5
6$ cat /proc/4120/maps
7aaaaaaab0000-aaaaaaab1000 r-xp 00000000 08:01 777 /home/[REDACTED]/aslr_lab
8aaaaaaab1d80-aaaaaaab1f00 r--p 00000d80 08:01 777 /home/[REDACTED]/aslr_lab
9aaaaaaab1f00-aaaaaaab2000 rw-p 00000f00 08:01 777 /home/[REDACTED]/aslr_lab
10fffff7d00000-fffff7ec0000 r-xp 00000000 08:01 12 /lib/aarch64-linux-gnu/libc-2.31.so
11fffff7fc0000-fffff7fd0000 r-xp 00000000 08:01 80 /lib/aarch64-linux-gnu/ld-2.31.so
12fffffffff000-ffffffffff000 rw-p 00000000 00:00 0 [stack]
13# vDSO / heap lines omitted
Second run, without disable-randomization:
1$ ./aslr_lab p
2marker=0xaaab0c3d07a4
3main=0xaaab0c3d07c0
4pid=4121
5
6$ # maps (trimmed)
7aaab0c3d0000-aaab0c3d1000 r-xp ... /home/[REDACTED]/aslr_lab
8fffff7900000-fffff7ac0000 r-xp ... libc-2.31.so
Subtract file VA of marker (nm says 00000000000007a4):
1run1 bias = 0xaaaaaaab07a4 - 0x7a4 = 0xaaaaaaab0000 ; matches maps r-xp start
2run2 bias = 0xaaab0c3d07a4 - 0x7a4 = 0xaaab0c3d0000
Two independent facts:
- Main image slide changed. PIE is doing work.
- libc slide also changed, and it is not
bias + libc_file_va. Leakingmarkerdoes not giveputs. I need a second pointer (GOT slot, libc return address on stack,/procin a local-attacker model) for the libc base.
x86_64 numbers look like 0x555555554000 vs 0x7ffff7a00000. Same subtraction.
gdb: measure bias with randomization on and off
1$ gdb -q ./aslr_lab
2(gdb) set disable-randomization off ; ask gdb not to flatten ASLR
3(gdb) break marker
4(gdb) run p
5Breakpoint 1, marker () at aslr_lab.c:7
6(gdb) p/x $pc
7$1 = 0x0000aaab11c007a4 ; [REDACTED-style: this is run-N]
8(gdb) info proc mappings
9 Start Addr End Addr Size Offset objfile
10 0xaaab11c00000 0xaaab11c01000 0x1000 0x0 /home/[REDACTED]/aslr_lab
11(gdb) p/x 0xaaab11c007a4 - 0x7a4
12$2 = 0x0000aaab11c00000 ; bias == r-xp start
Now flatten, so the rest of the notebook is stable:
1(gdb) set disable-randomization on
2(gdb) run p
3Breakpoint 1, marker ()
4(gdb) p/x $pc
5$3 = 0x0000aaaaaaab07a4
6(gdb) info proc mappings
7 0xaaaaaaab0000 0xaaaaaaab1000 0x1000 0x0 aslr_lab
8 0xfffff7d00000 0xfffff7ec0000 0x1c0000 0x0 libc-2.31.so
disable-randomization is a lab-only gdb knob (personality ADDR_NO_RANDOMIZE). Production crash dumps do not have it. I never paste flattened addresses into an advisory.
Kernel still-ASLR check, from the host:
1$ cat /proc/sys/kernel/randomize_va_space
22
3# 0 = off, 1 = conservative, 2 = full (including data segments)
A box with 0 makes every maps dump in this note a lie. I record the sysctl next to the maps.
Entropy, as a table, not a slogan
On this aarch64 Linux 5.x userland, what actually moved between the two runs:
| Region | Run 1 start | Run 2 start | Bits that changed (this sample) |
|---|---|---|---|
PIE text (aslr_lab) | 0xaaaaaaab0000 | 0xaaab0c3d0000 | bits 12–32 of the user VA, page aligned |
| libc | 0xfffff7d00000 | 0xfffff7900000 | independent of PIE |
| stack | 0xffffffffe000 | 0xfffffff4e000 | separate slide |
[heap] | (lazy) | (lazy) | not in the first maps if unused |
I do not publish a fake “entropy = 28 bits” as gospel; the bit count is kernel, arch, and 32-vs-64 specific. 32-bit userland is the one that still brute-forces. 64-bit remote, no leak, one-shot: ASLR is doing its job. 64-bit plus a text pointer in the banner: the first row of the table is gone.
Forked workers that inherit the parent’s maps make the leak sticky across children. I check fork vs exec in the service model before I say “each connection is a new slide.”
32-bit contrast, same C, because that is the layout people still brute-force:
1$ cc -m32 -O0 -fPIE -pie -o aslr_lab32 aslr_lab.c # if a 32-bit libc exists
2$ ./aslr_lab32 p
3marker=0x56556251
4$ ./aslr_lab32 p
5marker=0x56557251
6# file VA of marker ~ 0x1251; bias 0x56555000 vs 0x56556000
7# page-aligned, ~8–16 bits of text slide on this kernel — not 28.
I do not treat a 32-bit banner leak as “ASLR bypassed”; there was almost nothing to bypass. I do treat a 64-bit marker=%p as a full text-base disclosure.
vDSO / [vdso] is a third slide. I dump it so I do not confuse a leaked gettimeofday trampoline with libc:
1$ cat /proc/4120/maps | grep vdso
2ffffc77ff000-ffffc7800000 r-xp 00000000 00:00 0 [vdso]
3(gdb) info auxv
4AT_SYSINFO_EHDR 0xffffc77ff000 ; [REDACTED] per run
The leak, matched to maps
1$ ./aslr_lab p
2marker=0xaaaaaaab07a4
3main=0xaaaaaaab07c0
4pid=4120
1$ nm aslr_lab | egrep 'marker|main'
200000000000007a4 t marker
300000000000007c0 T main
0xaaaaaaab07a4 - 0x7a4 = 0xaaaaaaab0000 = r-xp line. The format sink leaked the bias. Every other file VA in this ELF is now a runtime VA: puts@plt file 0x6d0 → 0xaaaaaaab06d0. That is why pointer prints are first-class bugs next to overflows — see the format-string lab for a %p chain that also walks a canary.
A GOT slot is a libc leak, not a PIE leak:
1(gdb) set disable-randomization on
2(gdb) break *main
3(gdb) run
4(gdb) x/gx 0xaaaaaaab0fd8 ; puts JUMP_SLOT after bind, from readelf -r
50xaaaaaaab0fd8: 0x0000fffff7e8c4a0
6(gdb) info symbol 0xfffff7e8c4a0
7puts in section .text of /lib/aarch64-linux-gnu/libc.so.6
8(gdb) p/x 0xfffff7e8c4a0 - &_IO_puts
9# libc bias = maps r-xp of libc
Two leaks, two bases. Advisories that say “ASLR bypassed” after one %p of a stack slot are over-claiming unless that slot actually pointed at the module they needed.
Sanitized reproduction (crash + leak, no chain)
I add a 16-byte stack buffer and a memcpy of argv[1] with no cap, so a long input both crashes and, on a shorter %p-shaped input, leaks. Two invocations, not one exploit.
1/* extra, -DPLANT_BUG */
2static void echo(const char *s)
3{
4 char buf[16];
5 memcpy(buf, s, strlen(s) + 1); /* lab bug */
6 puts(buf);
7}
Short leak (no smash):
1$ ./aslr_lab p
2marker=0xaaaaaaab07a4
Smash:
1$ ./aslr_lab $(python3 -c 'print("A"*40)')
2Segmentation fault
3
4$ gdb -q ./aslr_lab core
5(gdb) info registers pc x30
6pc 0x4141414141414141 [smashed]
7x30 0x4141414141414141
ASan:
1$ cc -O0 -fPIE -pie -fsanitize=address -g -o aslr_asan aslr_lab.c
2$ ./aslr_asan $(python3 -c 'print("A"*40)')
3==412==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x...
4WRITE of size 41 at ... thread T0
5 #0 memcpy
6 #1 echo aslr_lab.c:12
7 #2 main
8 This frame has 1 object(s):
9 [32, 48) 'buf' (line 11) <== Memory access at offset 48
I do not combine the leaked marker address with the smash. The file has both artifacts so the audit trail is complete: disclosure of PIE bias, and a separate stack overflow crash.
What still leaks in products (classes, not recipes)
- Direct pointer prints —
printf("%p", obj), debug banners, exception pages. - Out-of-bounds reads — heap/stack slides that include a saved
x30or a vptr (vtable lab). - procfs / cores —
maps,coredump_filter, world-readable crash directories. Local attacker model. - Hashing pointers into identifiers that invert or narrow.
Partial overwrites: if a bug only hits the low 16 bits of a pointer, the high bits (the slide) stay. ASLR of bits the bug cannot touch is still doing work. I record which bits the write hits next to the maps dump.
Patch / detection
- Build with PIE (
-fPIE -pie). Confirmreadelf -hTypeDYN. - Leave
randomize_va_space=2. Do not start daemons withsetarch -Rin production. - Ban
%pof function or object pointers in any protocol that crosses a trust boundary. Log hashes if you must correlate. - CI: run the binary twice,
nm-subtractmain, fail the test if biases are equal andrandomize_va_spaceis 2 (catchADDR_NO_RANDOMIZEinherited from a parent). - Treat leaks as equal priority to writes when NX+ASLR are the backbone.
Commands appendix
1cc -O0 -fPIE -pie -g -o aslr_lab aslr_lab.c
2readelf -h aslr_lab | egrep 'Type|Entry'
3nm aslr_lab | egrep 'marker|main'
4./aslr_lab p
5cat /proc/sys/kernel/randomize_va_space
6gdb -q ./aslr_lab \
7 -ex 'set disable-randomization off' \
8 -ex 'b marker' -ex 'run p' \
9 -ex 'p/x $pc' -ex 'info proc mappings'