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.

PIE text and libc are independent slides
Figure 1. PIE randomizes the main image. libc is a second slide. One leaked text pointer does not by itself give 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:

  1. Main image slide changed. PIE is doing work.
  2. libc slide also changed, and it is not bias + libc_file_va. Leaking marker does not give puts. I need a second pointer (GOT slot, libc return address on stack, /proc in 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:

RegionRun 1 startRun 2 startBits that changed (this sample)
PIE text (aslr_lab)0xaaaaaaab00000xaaab0c3d0000bits 12–32 of the user VA, page aligned
libc0xfffff7d000000xfffff7900000independent of PIE
stack0xffffffffe0000xfffffff4e000separate 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 0x6d00xaaaaaaab06d0. 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)

  1. Direct pointer printsprintf("%p", obj), debug banners, exception pages.
  2. Out-of-bounds reads — heap/stack slides that include a saved x30 or a vptr (vtable lab).
  3. procfs / coresmaps, coredump_filter, world-readable crash directories. Local attacker model.
  4. 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). Confirm readelf -h Type DYN.
  • Leave randomize_va_space=2. Do not start daemons with setarch -R in production.
  • Ban %p of function or object pointers in any protocol that crosses a trust boundary. Log hashes if you must correlate.
  • CI: run the binary twice, nm-subtract main, fail the test if biases are equal and randomize_va_space is 2 (catch ADDR_NO_RANDOMIZE inherited 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'