RELRO is a loader promise about which relocated bytes stay writable. I keep mixing it up with “the GOT exists,” so this note is a single binary compiled twice: lazy/Partial, then -z now/Full. Same C as the lazy-binding lab; here the question is not “when does puts resolve” but “can a write still hit the slot after _start.”

PLT stub through GOT to libc
Figure 1. Partial RELRO: GOT[puts] stays writable so the resolver can patch it. Full RELRO: the slot is already libc and the page is r--.
1/* relro_lab.c — toy, two puts so bind is visible */
2#include <stdio.h>
3
4int main(void)
5{
6    puts("relro-lab");
7    puts("relro-lab");
8    return 0;
9}
 1cc -O0 -fPIE -pie -Wl,-z,lazy -o relro_lazy relro_lab.c
 2cc -O0 -fPIE -pie -Wl,-z,relro,-z,now -o relro_now  relro_lab.c
 3
 4file relro_lazy relro_now
 5# both: ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked, not stripped
 6
 7checksec --file=relro_lazy
 8# RELRO           STACK CANARY      NX            PIE
 9# Partial RELRO   No canary found   NX enabled    PIE enabled
10
11checksec --file=relro_now
12# Full RELRO      No canary found   NX enabled    PIE enabled

checksec is a summary. The rest of the note is the readelf / gdb that checksec used.

readelf on the lazy (Partial) build

1$ readelf -d relro_lazy | egrep 'NEEDED|FLAGS|BIND_NOW|NOW|FLAGS_1'
2 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
3 0x000000006ffffffb (FLAGS_1)            Flags: PIE
4# no BIND_NOW, no NOW in FLAGS_1
5
6$ readelf -l relro_lazy | grep GNU_RELRO
7  GNU_RELRO      0x0000000000000d80 0x000000000000fd80 0x000000000000fd80
8                 0x0000000000000280 0x0000000000000280  R

GNU_RELRO without BIND_NOW is Partial: the loader remaps .init_array / .fini_array / .got (GLOB_DAT) read-only, and leaves .got.plt writable so lazy JUMP_SLOT updates work.

 1$ readelf -S relro_lazy | egrep 'got|plt|relro|data.rel'
 2  [10] .rela.plt         RELA            00000000000006c8  000006c8
 3  [12] .plt              PROGBITS        00000000000006b0  000006b0
 4  [21] .got              PROGBITS        0000000000000fd0  00000fd0
 5  [22] .got.plt          PROGBITS        0000000000000fe8  00000fe8
 6  [23] .data             PROGBITS        0000000000001008  00001008
 7
 8$ readelf -r relro_lazy
 9Relocation section '.rela.plt' at offset 0x6c8 contains 2 entries:
10  Offset          Info           Type            Sym. Name + Addend
1100000000000ff8  000200000402 R_AARCH64_JUMP_SLOT  puts@GLIBC_2.17 + 0
1200000000001000  000300000402 R_AARCH64_JUMP_SLOT  __libc_start_main@GLIBC_2.17 + 0

GOT slot for puts is file VA 0xff8. Runtime address = load bias + 0xff8.

PLT stub (same bytes on both binaries)

 1$ objdump -d relro_lazy | sed -n '/Disassembly of section .plt/,+20p'
 200000000000006b0 <.plt>:
 3    6b0:  a9bf7bf0   stp   x16, x30, [sp, #-16]!
 4    6b4:  90000090   adrp  x16, 1000
 5    6b8:  f9400e11   ldr   x17, [x16, #24]
 6    6bc:  91006210   add   x16, x16, #0x18
 7    6c0:  d61f0220   br    x17                 ; resolver on first lazy call
 8
 900000000000006d0 <puts@plt>:
10    6d0:  90000090   adrp  x16, 1000
11    6d4:  f9401211   ldr   x17, [x16, #32]     ; [GOT+0xff8]  (0x1000+0x20 wait: dump)
12    6d8:  91008210   add   x16, x16, #0x20
13    6dc:  d61f0220   br    x17

I confirm the GOT VA from the reloc, not from eyeballing the adrp addend. main only ever bl 6d0 <puts@plt>:

 100000000000007a4 <main>:
 2    7a4:  a9be7bfd   stp   x29, x30, [sp, #-32]!
 3    7a8:  910003fd   mov   x29, sp
 4    7ac:  90000000   adrp  x0, 0
 5    7b0:  91204000   add   x0, x0, #0x810      ; "relro-lab"
 6    7b4:  97ffffc7   bl    6d0 <puts@plt>
 7    7b8:  90000000   adrp  x0, 0
 8    7bc:  91204000   add   x0, x0, #0x810
 9    7c0:  97ffffc4   bl    6d0 <puts@plt>
10    7c4:  52800000   mov   w0, #0
11    7c8:  a8c27bfd   ldp   x29, x30, [sp], #32
12    7cc:  d65f03c0   ret

gdb: Partial — slot writable, patched on first call

 1$ gdb -q ./relro_lazy
 2(gdb) set disable-randomization on
 3(gdb) break puts@plt
 4(gdb) run
 5Breakpoint 1, 0x0000aaaaaaab06d0 in puts@plt ()
 6
 7(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
 8$1 = 0x0000aaaaaaab06e0          ; still inside PLT (lazy stub tail)
 9
10(gdb) info proc mappings
11      0xaaaaaaab0000     0xaaaaaaab1000     0x1000  r-xp  relro_lazy
12      0xaaaaaaab1f00     0xaaaaaaab2000     0x1000  rw-p  relro_lazy
13# .got.plt is in the rw-p LOAD. GNU_RELRO stopped earlier.
14
15(gdb) finish
16relro-lab
17(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
18$2 = 0x0000fffff7e8c4a0          ; libc puts   [slide REDACTED in field notes]
19(gdb) info symbol 0xfffff7e8c4a0
20puts in section .text of /lib/aarch64-linux-gnu/libc.so.6
21
22(gdb) # second hit of puts@plt: same word, already libc
23(gdb) continue
24Breakpoint 1, 0x0000aaaaaaab06d0 in puts@plt ()
25(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
26$3 = 0x0000fffff7e8c4a0

Writability check from the process, not from the ELF:

1$ cat /proc/4120/maps | grep relro_lazy
2aaaaaaab0000-aaaaaaab1000 r-xp 00000000 08:01 1234  /home/[REDACTED]/relro_lazy
3aaaaaaab1d80-aaaaaaab1f00 r--p 00000d80 08:01 1234  /home/[REDACTED]/relro_lazy
4aaaaaaab1f00-aaaaaaab2000 rw-p 00000f00 08:01 1234  /home/[REDACTED]/relro_lazy

r--p is the RELRO window (.init_array … GLOB_DAT). rw-p still contains .got.plt at 0xff8 (bias-adjusted). Partial RELRO in one maps dump.

readelf + gdb on the Full RELRO build

1$ readelf -d relro_now | egrep 'NEEDED|FLAGS|BIND_NOW|NOW|FLAGS_1'
2 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
3 0x000000000000001e (FLAGS)              BIND_NOW
4 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
5
6$ readelf -l relro_now | grep GNU_RELRO
7  GNU_RELRO      0x0000000000000d80 0x000000000000fd80 0x000000000000fd80
8                 0x0000000000000288 0x0000000000000288  R

BIND_NOW + GNU_RELRO = Full. Eager bind happens in the loader, then the RELRO window includes .got.plt.

 1$ gdb -q ./relro_now
 2(gdb) set disable-randomization on
 3(gdb) break *main          ; first instruction of main, after loader
 4(gdb) run
 5Breakpoint 1, 0x0000aaaaaaab07a4 in main ()
 6
 7(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
 8$1 = 0x0000fffff7e8c4a0    ; already libc, we never saw the PLT tail
 9(gdb) info symbol 0xfffff7e8c4a0
10puts in section .text of /lib/aarch64-linux-gnu/libc.so.6
11
12(gdb) shell cat /proc/$(pgrep relro_now)/maps | grep relro_now
13aaaaaaab0000-aaaaaaab1000 r-xp ... relro_now
14aaaaaaab1d80-aaaaaaab2000 r--p ... relro_now     ; RELRO ate .got.plt
15# no rw-p file-backed line for the GOT

A store through a wild pointer into 0xaaaaaaab0ff8 now SIGSEGVs:

1(gdb) set {unsigned long}0xaaaaaaab0ff8 = 0x4141414141414141
2Cannot access memory at address 0xaaaaaaab0ff8
3# or, from a lab poke function:
4Program received signal SIGSEGV, Segmentation fault.
50x0000aaaaaaab08c0 in poke ()
6(gdb) x/i $pc
7=> 0xaaaaaaab08c0:  str  x1, [x0]     ; x0 = GOT slot, x1 = junk

That is the Full RELRO punchline, measured: the slot is libc and the page is r– before main runs. I still need a different function-pointer class (C++ vptr, callback, heap) if I am triaging a write primitive. See the vtable lab for that class.

GLOB_DAT vs JUMP_SLOT (why Partial still freezes some GOT)

readelf -r on relro_lazy also has .rela.dyn. Those GLOB_DAT slots sit below .got.plt and are inside the RELRO window even on the lazy build.

 1$ readelf -r relro_lazy | grep GLOB_DAT
 20000000000000fd0  ... R_AARCH64_GLOB_DAT  __gmon_start__ + 0
 30000000000000fd8  ... R_AARCH64_GLOB_DAT  __libc_start_main + 0
 4
 5$ gdb -q ./relro_lazy
 6(gdb) set disable-randomization on
 7(gdb) start
 8(gdb) shell cat /proc/$(pgrep relro_lazy)/maps
 9aaaaaaab0000-aaaaaaab1000 r-xp  relro_lazy
10aaaaaaab1d80-aaaaaaab1f00 r--p  relro_lazy   ; GLOB_DAT lives here
11aaaaaaab1f00-aaaaaaab2000 rw-p  relro_lazy   ; JUMP_SLOT / .got.plt here
12(gdb) p/x 0xaaaaaaab0000+0xfd0
13$1 = 0xaaaaaaab0fd0
14(gdb) # 0xaaaaaaab0fd0 is inside r--p.  0xaaaaaaab0ff8 (puts JUMP_SLOT) is rw-p.

If a write-up says “Partial RELRO means the GOT is writable,” that is sloppy. Function PLT slots are writable; GLOB_DAT / RELATIVE in .got / .data.rel.ro are already frozen. I record both offsets so a later “we overwrote __gmon_start__” claim can be killed in one maps lookup.

x86_64 of the same two links, because most of the public RELRO screenshots are that ABI:

1$ cc -O0 -fPIE -pie -Wl,-z,lazy -o relro_lazy_x64 relro_lab.c     # x86_64 host
2$ objdump -d relro_lazy_x64 | sed -n '/<puts@plt>:/,+6p'
30000000000001060 <puts@plt>:
4    1060:  ff 25 92 2f 00 00   jmp    QWORD PTR [rip+0x2f92]  # 4020 <puts@GLIBC_2.2.5>
5    1066:  68 00 00 00 00      push   0x0
6    1068:  e9 e0 ff ff ff      jmp    1050 <.plt>
7$ readelf -r relro_lazy_x64 | grep puts
80000000000004020  ... R_X86_64_JUMP_SLOT  puts@GLIBC_2.2.5 + 0

[rip+disp] is the GOT slot. gdb p/x *(long*)0x555555558020 is the same experiment as 0xaaaaaaab0ff8 on aarch64. The RELRO story does not change with the mnemonic.

Sanitized reproduction (crash only)

A tiny helper writes 8 bytes through an attacker-chosen pointer. I aim it at the Partial GOT slot with a dummy value. This is a crash/observation lab, not a redirect-to-libc recipe.

1/* poke.c — lab only, linked into relro_lazy */
2void poke(unsigned long *slot, unsigned long v)
3{
4    *slot = v;
5}
 1$ gdb -q ./relro_lazy
 2(gdb) set disable-randomization on
 3(gdb) break main
 4(gdb) run
 5(gdb) call poke((unsigned long*)0xaaaaaaab0ff8, 0x4141414141414141)
 6(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
 7$1 = 0x4141414141414141
 8(gdb) continue
 9Program received signal SIGSEGV, Segmentation fault.
100x0000004141414141 in ?? ()
11(gdb) bt
12#0  0x0000004141414141 in ?? ()
13#1  0x0000aaaaaaab07b8 in main ()      ; return from first puts@plt

On relro_now the same call poke dies inside poke with str to an r– page. Two binaries, two fault sites, same 8-byte write. I do not then put a libc function in the slot.

ASan is the wrong tool for “wrote a GOT slot”; ASan tracks heap/stack redzones, not RELRO. The signal is SIGSEGV vs silent corruption.

What I actually record in an audit

  1. readelf -dBIND_NOW / FLAGS_1 NOW present or absent.
  2. readelf -lGNU_RELRO present or absent. Absent + writable GOT is “No RELRO.”
  3. readelf -r → JUMP_SLOT list (imported names that survive strip).
  4. One /proc/pid/maps line covering .got.plt: rw-p (Partial) or r--p (Full).
  5. Never copy a GOT VA from a screenshot into the next boot; PIE bias moves.

Static binaries have no PLT. I do not write a RELRO paragraph on those; I write “no dynamic GOT.”

Patch / detection

  • Link production daemons with -Wl,-z,relro,-z,now. Fail CI if checksec says Partial on an attack-facing target.
  • Lazy bind is a startup-time trade. If a binary must stay lazy, document it; do not leave it as the toolchain default.
  • Incident: writable got.plt plus a leftover JUMP_SLOT to a sensitive libc name is a hunting lead, not a conclusion. Confirm with the maps line.

Commands appendix

1readelf -d "$1" | egrep 'NEEDED|BIND_NOW|FLAGS_1|NOW'
2readelf -l "$1" | grep GNU_RELRO
3readelf -r "$1" | grep JUMP_SLOT
4objdump -d "$1" | less +/'.plt'
5checksec --file="$1"
6gdb -q "$1" -ex 'set disable-randomization on' -ex 'b puts@plt' -ex 'r'
7# at the bp: p/x *(unsigned long *)(bias+got_offset)
8# shell cat /proc/$(pgrep $name)/maps