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.”
Lab binary (two links)
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
readelf -d→BIND_NOW/FLAGS_1 NOWpresent or absent.readelf -l→GNU_RELROpresent or absent. Absent + writable GOT is “No RELRO.”readelf -r→ JUMP_SLOT list (imported names that survive strip).- One
/proc/pid/mapsline covering.got.plt:rw-p(Partial) orr--p(Full). - 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 ifchecksecsays 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.pltplus 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