Most Linux reversing starts with two tables: names that still exist, and bytes the loader will patch. This lab is one 20-line PIE and a complete readelf -s / readelf -r session on it, then the same session on a stripped copy. I am not summarizing ELF. I am dumping the files.
Lab binary
1/* sym_lab.c — one local, one global, one libc import, one global object */
2#include <stdio.h>
3
4int g_counter = 7;
5
6static int hidden_add(int a, int b)
7{
8 return a + b + g_counter;
9}
10
11int main(void)
12{
13 int v = hidden_add(1, 2);
14 printf("v=%d\n", v);
15 return v;
16}
1cc -O0 -fPIE -pie -g -o sym_lab sym_lab.c
2cp -a sym_lab sym_lab.unstripped
3strip -o sym_lab.strip sym_lab
4file sym_lab.unstripped sym_lab.strip
5# both: ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked
6# unstripped: not stripped strip: stripped
readelf -h / -l / -S (where the tables live)
1$ readelf -h sym_lab.unstripped | egrep 'Class|Type|Machine|Entry|Magic'
2 Magic: 7f 45 4c 46 02 01 01 00 ...
3 Class: ELF64
4 Type: DYN (Shared object file)
5 Machine: AArch64
6 Entry point address: 0x6a0
7
8$ readelf -S sym_lab.unstripped | egrep 'symtab|dynsym|rela|plt|text|dynstr'
9 [ 6] .dynsym DYNSYM 00000000000003f0 000003f0
10 [ 7] .dynstr STRTAB 00000000000004c8 000004c8
11 [10] .rela.dyn RELA 00000000000005b0 000005b0
12 [11] .rela.plt RELA 00000000000006c8 000006c8
13 [12] .plt PROGBITS 00000000000006b0 000006b0
14 [14] .text PROGBITS 00000000000007a0 000007a0
15 [28] .symtab SYMTAB 0000000000000000 00001280
16 [29] .strtab STRTAB 0000000000000000 00001490
.symtab has no runtime VA (offset only). Strip deletes .symtab / .strtab. .dynsym stays; the dynamic linker needs it.
1$ readelf -S sym_lab.strip | egrep 'symtab|dynsym'
2 [ 6] .dynsym DYNSYM 00000000000003f0 000003f0
3# no .symtab line
readelf -s — full symbol dump, then the rows I keep
1$ readelf -s sym_lab.unstripped
2Symbol table '.dynsym' contains 8 entries:
3 Num: Value Size Type Bind Vis Ndx Name
4 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
5 1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@GLIBC_2.17 (2)
6 2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.17 (2)
7 3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
8 4: 00000000000007c8 72 FUNC GLOBAL DEFAULT 14 main
9 5: 0000000000001108 4 OBJECT GLOBAL DEFAULT 23 g_counter
10 6: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_deregisterT...
11 7: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _ITM_registerT...
12
13Symbol table '.symtab' contains 44 entries:
14 Num: Value Size Type Bind Vis Ndx Name
15 ...
16 18: 00000000000007a4 36 FUNC LOCAL DEFAULT 14 hidden_add
17 32: 00000000000007c8 72 FUNC GLOBAL DEFAULT 14 main
18 33: 0000000000001108 4 OBJECT GLOBAL DEFAULT 23 g_counter
19 40: 0000000000000000 0 FUNC GLOBAL DEFAULT UND printf@@GLIBC_2.17
Rows that matter:
| Name | Table | Bind | Ndx | Meaning |
|---|---|---|---|---|
printf | .dynsym | GLOBAL | UND | import; Value=0 until bind |
main | both | GLOBAL | 14 (.text) | exported from the PIE |
g_counter | both | GLOBAL | 23 (.data) | object, size 4 |
hidden_add | .symtab only | LOCAL | 14 | strip kills this name |
__gmon_start__ | .dynsym | WEAK UND | UND | optional; linker does not fail if missing |
1$ readelf -s sym_lab.strip
2Symbol table '.dynsym' contains 8 entries:
3 ... same UND printf, GLOBAL main, GLOBAL g_counter ...
4# no .symtab, no hidden_add
nm is the short form of the same tables:
1$ nm -C sym_lab.unstripped | egrep 'main|hidden|g_counter|printf'
20000000000001108 D g_counter
300000000000007a4 t hidden_add
400000000000007c8 T main
5 U printf
6
7$ nm -D sym_lab.strip | egrep 'main|hidden|g_counter|printf'
80000000000001108 D g_counter
900000000000007c8 T main
10 U printf
11# hidden_add gone from -D as well (it was never dynamic)
Local functions are the first thing strip steals. Imports and exported globals survive. That is why a stripped daemon still shows printf and not hidden_add.
readelf -r — full reloc dump
1$ readelf -r sym_lab.unstripped
2Relocation section '.rela.dyn' at offset 0x5b0 contains 6 entries:
3 Offset Info Type Sym. Value Sym. Name + Addend
40000000000000fd8 000000000403 R_AARCH64_RELATIVE 7c8
50000000000000fe0 000000000403 R_AARCH64_RELATIVE 7a0
60000000000001108 000000000403 R_AARCH64_RELATIVE 1108
70000000000000fa8 000300000401 R_AARCH64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
80000000000000fb0 000200000401 R_AARCH64_GLOB_DAT 0000000000000000 __libc_start_main + 0
90000000000000fb8 000100000401 R_AARCH64_GLOB_DAT 0000000000000000 printf + 0
10
11Relocation section '.rela.plt' at offset 0x6c8 contains 2 entries:
12 Offset Info Type Sym. Value Sym. Name + Addend
130000000000000ff8 000100000402 R_AARCH64_JUMP_SLOT 0000000000000000 printf@GLIBC_2.17 + 0
140000000000001000 000200000402 R_AARCH64_JUMP_SLOT 0000000000000000 __libc_start_main@GLIBC_2.17 + 0
How I read one row, every time:
1Offset = GOT / data slot the loader writes (file VA; add bias at runtime)
2Type = what kind of write
3Name = which symbol (empty for RELATIVE)
4Addend = for RELATIVE, the file VA being slid
Three types on this binary:
R_AARCH64_RELATIVE—*(bias+Offset) = bias + Addend. Used for internal pointers in a PIE (here Addend0x7c8ismain). No name needed. Strip does not remove these.R_AARCH64_GLOB_DAT— fill a GOT cell with the symbol’s resolved address (function or object). Eager even under lazy bind, typically inside the RELRO window.R_AARCH64_JUMP_SLOT— the PLT GOT cell. Lazy unlessBIND_NOW. This is theprintf@pltslot. Same dance as the GOT/PLT lab.
x86_64 names are R_X86_64_RELATIVE, R_X86_64_GLOB_DAT, R_X86_64_JUMP_SLOT. Same columns.
readelf -r on the stripped copy is byte-identical for these sections. Relocs are not in .symtab.
1$ readelf -r sym_lab.strip | md5sum
2$ readelf -r sym_lab.unstripped | md5sum
3# same digest on this toolchain
readelf -d — who consumes those tables
1$ readelf -d sym_lab.unstripped
2Dynamic section at offset 0xe28 contains 24 entries:
3 Tag Type Name/Value
4 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
5 0x000000000000000c (INIT) 0x6a8
6 0x000000000000000d (FINI) 0x8b8
7 0x0000000000000019 (INIT_ARRAY) 0xfd8
8 0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
9 0x0000000000000005 (STRTAB) 0x4c8
10 0x0000000000000006 (SYMTAB) 0x3f0 ; this is .dynsym, not .symtab
11 0x000000000000000a (STRSZ) 130 (bytes)
12 0x000000000000000b (SYMENT) 24 (bytes)
13 0x0000000000000015 (DEBUG) 0x0
14 0x0000000000000003 (PLTGOT) 0xfe8
15 0x0000000000000002 (PLTRELSZ) 48 (bytes)
16 0x0000000000000014 (PLTREL) RELA
17 0x0000000000000017 (JMPREL) 0x6c8 ; .rela.plt
18 0x0000000000000007 (RELA) 0x5b0 ; .rela.dyn
19 0x0000000000000008 (RELASZ) 144 (bytes)
20 0x0000000000000009 (RELAENT) 24 (bytes)
21 0x000000006ffffffb (FLAGS_1) Flags: PIE
22 0x000000006ffffffe (VERNEED) 0x590
23 0x000000006fffffff (VERNEEDNUM) 1
24 0x000000006ffffff0 (VERSYM) 0x560
SYMTAB in dynamic tags points at .dynsym. I have mixed that up with .symtab on stripped files and then wondered why readelf -s still printed printf.
No BIND_NOW here → lazy JUMP_SLOT. RELRO is Partial (see RELRO note).
objdump: printf is a reloc, not an immediate
1$ objdump -d sym_lab.unstripped | sed -n '/<main>:/,/ret/p'
200000000000007c8 <main>:
3 7c8: a9be7bfd stp x29, x30, [sp, #-32]!
4 7cc: 910003fd mov x29, sp
5 7d0: 52800020 mov w0, #1
6 7d4: 52800041 mov w1, #2
7 7d8: 97fffff3 bl 7a4 <hidden_add> ; direct, same module
8 7dc: 2a0003e1 mov w1, w0
9 7e0: 90000000 adrp x0, 0
10 7e4: 91208000 add x0, x0, #0x820 ; "v=%d\n"
11 7e8: 97ffffc2 bl 6d0 <printf@plt> ; JUMP_SLOT, not libc
12 7ec: 52800120 mov w0, #9 ; 1+2+7
13 7f0: a8c27bfd ldp x29, x30, [sp], #32
14 7f4: d65f03c0 ret
hidden_add is a bl with a relative immediate — no reloc. printf is a bl into .plt. After strip, objdump still labels printf@plt because .dynsym + .rela.plt survived. It labels hidden_add as <main-0x24> or a raw VA.
1$ objdump -d sym_lab.strip | sed -n '/<main>:/,+12p'
200000000000007c8 <main>:
3 7c8: ...
4 7d8: 97fffff3 bl 7a4 <main-0x24> ; was hidden_add
5 7e8: 97ffffc2 bl 6d0 <printf@plt> ; name kept
That is the practical difference between the two symbol tables, in one diff.
gdb: UND becomes a libc VA
1$ gdb -q ./sym_lab.unstripped
2(gdb) set disable-randomization on
3(gdb) break printf@plt
4(gdb) run
5Breakpoint 1, 0x0000aaaaaaab06d0 in printf@plt ()
6(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8 ; JUMP_SLOT Offset 0xff8 + bias
7$1 = 0x0000aaaaaaab06e0 ; still PLT tail
8(gdb) finish
9v=10
10(gdb) p/x *(unsigned long *)0xaaaaaaab0ff8
11$2 = 0x0000fffff7e9c4a0 ; libc printf [slide REDACTED]
12(gdb) info symbol 0xfffff7e9c4a0
13printf in section .text of /lib/aarch64-linux-gnu/libc.so.6
g_counter is a RELATIVE / data object. gdb sees the slid address:
1(gdb) p &g_counter
2$3 = (int *) 0xaaaaaaab1108
3(gdb) p g_counter
4$4 = 7
File VA 0x1108 + bias 0xaaaaaaab0000 = $3. RELATIVE reloc did that, not .symtab.
Sanitized reproduction (crash only)
A planted strcpy into a 8-byte global, so the overflow is in .data next to g_counter. Crash / ASan, not a reloc rewrite.
1/* -DPLANT_BUG */
2void load_tag(const char *s)
3{
4 char tag[8];
5 strcpy(tag, s);
6 g_counter = tag[0];
7}
1$ cc -O0 -fPIE -pie -fsanitize=address -g -o sym_asan sym_lab.c
2$ ./sym_asan $(python3 -c 'print("A"*32)')
3==412==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x...
4WRITE of size 33 at ... thread T0
5 #0 strcpy
6 #1 load_tag sym_lab.c:6
7 #2 main
8 This frame has 1 object(s):
9 [32, 40) 'tag' (line 5) <== Memory access at offset 40
Without ASan, 32 As smash the frame and pc becomes 0x4141… the same way as the other labs. I do not smash a JUMP_SLOT here; the RELRO note already did the str into .got.plt.
What I file after this lab
.dynsym:UND printf,T main,D g_counter; nohidden_add.symtab(unstripped only):t hidden_addat0x7a4.rela.plt: JUMP_SLOTprintfat Offset0xff8.rela.dyn: RELATIVE addend0x7c8(main), GLOB_DAT for the UND names- Strip deletes
.symtab, keeps relocs and.dynsym - Repro:
strcpy33 bytes intotag[8], ASan stack-buffer-overflow
Patch / detection
strip --strip-unneededis fine for shipping; do not expect local names in incident work.- Audit imports from
.dynsymUND / JUMP_SLOT, not from a wishfulnmwithout-D. - PIE internals: treat every code pointer in
.dataas RELATIVE untilreadelf -rsays otherwise. Screenshot VAs without bias are wrong.
Commands appendix
1readelf -h "$1" | egrep 'Class|Type|Machine|Entry'
2readelf -S "$1" | egrep 'dynsym|symtab|rela|plt'
3readelf -s "$1"
4readelf -r "$1"
5readelf -d "$1" | egrep 'NEEDED|SYMTAB|JMPREL|BIND_NOW|FLAGS_1'
6nm -D "$1"
7objdump -d "$1" | less +/<main>