ELF Anatomy

ELF is short for Executable and Linkable Format. It's a format used for storing binaries, libraries, and core dumps on disks in Linux and Unix-based systems.

The ELF specification used on Linux for the kernel itself and Linux kernel modules.

ELF Structure

The ELF file is divided into two parts.

1. ELF header (metadata about the file)

$ readelf --help
#   -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
#   -h --file-header       Display the ELF file header
#   -l --program-headers   Display the program headers
#      --segments          An alias for --program-headers
#   -S --section-headers   Display the sections' header
#      --sections          An alias for --section-headers
#   -g --section-groups    Display the section groups
#   -t --section-details   Display the section details
#   -e --headers           Equivalent to: -h -l -S
#   -s --syms              Display the symbol table
#      --symbols           An alias for --syms
#      --dyn-syms          Display the dynamic symbol table
#      --lto-syms          Display LTO symbol tables
#      --sym-base=[0|8|10|16]

$ readelf -h /bin/ls

# ELF Header:
#   Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00                                <--- file signature
#   Class:                             ELF64                                                <--- architecture (32-bit, 64-bit)
#   Data:                              2's complement, little endian                        <--- data encoding
#   Version:                           1 (current)                                          
#   OS/ABI:                            UNIX - System V                                      <--- ABI (Application Binary Interface)
#   ABI Version:                       0
#   Type:                              DYN (Position-Independent Executable file)           <--- Executable / Shared Obj / Core File
#   Machine:                           Advanced Micro Devices X86-64                        <--- architecture needed for the file.
#   Version:                           0x1
#   Entry point address:               0x6180                                               <--- EP 
#   Start of program headers:          64 (bytes into file)
#   Start of section headers:          145256 (bytes into file)
#   Flags:                             0x0
#   Size of this header:               64 (bytes)                                           <--- See Hexdump below
#   Size of program headers:           56 (bytes)
#   Number of program headers:         11
#   Size of section headers:           64 (bytes)
#   Number of section headers:         30
#   Section header string table index: 29

The full ELF header and its values

$ hexdump -C -n 64 /bin/ls

00000000  7f 45 4c 46 02 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  03 00 3e 00 01 00 00 00  80 61 00 00 00 00 00 00  |..>......a......|
00000020  40 00 00 00 00 00 00 00  68 37 02 00 00 00 00 00  |@.......h7......|
00000030  00 00 00 00 40 00 38 00  0b 00 40 00 1e 00 1d 00  |....@.8...@.....|

0x3e ==> 62 –> AMDs x86-64 architecture

to view all machine types : elf.h

$ dumpelf /bin/ls

# include <elf.h>
# ...
# ...
# ... alot of stuff here ^^

2. File data

2.1 Program headers Table (Segments)

Program headers table –> stores information about segments –> Each segment is made up of one or more sections.

The kernel uses this information at run time, It tells the kernel how to create the process and map the segments into memory.

To run a program

  • the kernel loads the ELF header and the program header table into memory.
  • loads the contents that are specified in LOAD in the program header table into memory.
  • the control is given to the executable itself or the interpreter if it's available
$ readelf -l /bin/ls

# Program Headers / Segments :
#   Type           Offset             VirtAddr           PhysAddr           FileSiz            MemSiz              Flags  Align
#   PHDR           0x0000000000000040 0x0000000000000040 0x0000000000000040 0x0000000000000268 0x0000000000000268  R      0x8
#   INTERP         0x00000000000002a8 0x00000000000002a8 0x00000000000002a8 0x000000000000001c 0x000000000000001c  R      0x1
#       [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

#   LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000003538 0x0000000000003538  R      0x1000
#   LOAD           0x0000000000004000 0x0000000000004000 0x0000000000004000 0x00000000000143c9 0x00000000000143c9  R E    0x1000
#   LOAD           0x0000000000019000 0x0000000000019000 0x0000000000019000 0x0000000000008ab8 0x0000000000008ab8  R      0x1000
#   LOAD           0x0000000000022350 0x0000000000023350 0x0000000000023350 0x0000000000001278 0x0000000000002568  RW     0x1000
#   DYNAMIC        0x0000000000022dd8 0x0000000000023dd8 0x0000000000023dd8 0x00000000000001f0 0x00000000000001f0  RW     0x8
#   NOTE           0x00000000000002c4 0x00000000000002c4 0x00000000000002c4 0x0000000000000044 0x0000000000000044  R      0x4
#   GNU_EH_FRAME   0x000000000001df0c 0x000000000001df0c 0x000000000001df0c 0x0000000000000944 0x0000000000000944  R      0x4
#   GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000  RW     0x10
#   GNU_RELRO      0x0000000000022350 0x0000000000023350 0x0000000000023350 0x0000000000000cb0 0x0000000000000cb0  R      0x1
Note

Program headers are essential when running the executable because they tell the operating system all it needs to know to put the executable into memory and run it.



2.2 Section headers Table (Sections)

Section headers Table –> stores information about sections –> used during dynamic link time, just before the program is executed.

Note

A linker links the binary file with shared libraries that it needs by loading them into memory. The linker’s implementation is specific to the operating system.

$ readelf -S /bin/ls

# Section Headers / Sections :
#   [Nr] Name              Type             Address           Offset   Size              EntSize           Flags   Link  Info  Align
#   [ 0]                   NULL             0000000000000000  00000000 0000000000000000  0000000000000000           0     0     0
#   [ 1] .interp           PROGBITS         00000000000002a8  000002a8 000000000000001c  0000000000000000   A       0     0     1
#   [ 2] .note.gnu.bu[...] NOTE             00000000000002c4  000002c4 0000000000000024  0000000000000000   A       0     0     4
#   [ 3] .note.ABI-tag     NOTE             00000000000002e8  000002e8 0000000000000020  0000000000000000   A       0     0     4
#   [ 4] .gnu.hash         GNU_HASH         0000000000000308  00000308 00000000000000ac  0000000000000000   A       5     0     8
#   [ 5] .dynsym           DYNSYM           00000000000003b8  000003b8 0000000000000c00  0000000000000018   A       6     1     8
#   [ 6] .dynstr           STRTAB           0000000000000fb8  00000fb8 00000000000005c5  0000000000000000   A       0     0     1
#   [ 7] .gnu.version      VERSYM           000000000000157e  0000157e 0000000000000100  0000000000000002   A       5     0     2
#   [ 8] .gnu.version_r    VERNEED          0000000000001680  00001680 00000000000000a0  0000000000000000   A       6     2     8
#   [ 9] .rela.dyn         RELA             0000000000001720  00001720 0000000000001440  0000000000000018   A       5     0     8
#   [10] .rela.plt         RELA             0000000000002b60  00002b60 00000000000009d8  0000000000000018  AI       5    24     8
#   [11] .init             PROGBITS         0000000000004000  00004000 0000000000000017  0000000000000000  AX       0     0     4
#   [12] .plt              PROGBITS         0000000000004020  00004020 00000000000006a0  0000000000000010  AX       0     0     16
#   [13] .plt.got          PROGBITS         00000000000046c0  000046c0 0000000000000018  0000000000000008  AX       0     0     8
#   [14] .text             PROGBITS         00000000000046e0  000046e0 0000000000013cde  0000000000000000  AX       0     0     16
#   [15] .fini             PROGBITS         00000000000183c0  000183c0 0000000000000009  0000000000000000  AX       0     0     4
#   [16] .rodata           PROGBITS         0000000000019000  00019000 0000000000004f09  0000000000000000   A       0     0     32
#   [17] .eh_frame_hdr     PROGBITS         000000000001df0c  0001df0c 0000000000000944  0000000000000000   A       0     0     4
#   [18] .eh_frame         PROGBITS         000000000001e850  0001e850 0000000000003268  0000000000000000   A       0     0     8
#   [19] .init_array       INIT_ARRAY       0000000000023350  00022350 0000000000000008  0000000000000008  WA       0     0     8
#   [20] .fini_array       FINI_ARRAY       0000000000023358  00022358 0000000000000008  0000000000000008  WA       0     0     8
#   [21] .data.rel.ro      PROGBITS         0000000000023360  00022360 0000000000000a78  0000000000000000  WA       0     0     32
#   [22] .dynamic          DYNAMIC          0000000000023dd8  00022dd8 00000000000001f0  0000000000000010  WA       6     0     8
#   [23] .got              PROGBITS         0000000000023fc8  00022fc8 0000000000000038  0000000000000008  WA       0     0     8
#   [24] .got.plt          PROGBITS         0000000000024000  00023000 0000000000000360  0000000000000008  WA       0     0     8
#   [25] .data             PROGBITS         0000000000024360  00023360 0000000000000268  0000000000000000  WA       0     0     32
#   [26] .bss              NOBITS           00000000000245e0  000235c8 00000000000012d8  0000000000000000  WA       0     0     32
#   [27] .gnu_debugaltlink PROGBITS         0000000000000000  000235c8 0000000000000049  0000000000000000           0     0     1
#   [28] .gnu_debuglink    PROGBITS         0000000000000000  00023614 0000000000000034  0000000000000000           0     0     4
#   [29] .shstrtab         STRTAB           0000000000000000  00023648 000000000000011c  0000000000000000           0     0     1

W (write), A (alloc), X (execute), I (info)

.init Section that contains code that is executed when a program is loaded into memory. Specifically, it contains the initialization code that sets up the runtime environment for the program.

.note Section contains note information.

.text Section contains executable text/instructions/functions. (AX)

global variables and the rodata section that usually contains constant strings.

.bss Section contains uninitialized read-write data/global varaibles. (WA)

.data Section contains initialized read-write data/global varaibles. (WA)

.rodata Section contains initialized read-only data (A)

these sections (.data & .rodata) contain the actual, initialized data, which the program will need in memory. The memory reserves more space for the data segment than specified in the ELF file to make room for uninitialized variables.



Ex

// sections.c
// gcc sections.c -o sections
#include<stdio.h>

int GLOBAL = 0xdeadbeef;            // Initialized     	--> .data
int GLOBAL2;                        // UnInitialized   	--> .bss

char* string;                       // UnInitialized   	--> .bss
char* string2 = "HIIOOO";           // Constant String 	--> .rodata
                                    // string2       	--> .data
int main(){
	char *string3 = "Hello World!!";	// Constant String 	--> .rodata
                                        // string3          --> stack  
	return 0;
}
pwndbg> b *main
# Breakpoint 1 at 0x1129

pwndbg> r
# Starting program: /home/o54ma/exploit-exercises-protostar-2/ELF/sections
# [Thread debugging using libthread_db enabled]
# Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1"

# Breakpoint 1, 0x0000555555555129 in main ()

pwndbg> vmmap

pwndbg> info file

vmmap

pwndbg> info files
Symbols from "/home/o54ma/exploit-exercises-protostar-2/ELF/sections".
Native process:
        Using the running image of child Thread 0x7ffff7db7740 (LWP 34936).
        While running this, GDB does not access memory from...
Local exec file:
        `/home/o54ma/exploit-exercises-protostar-2/ELF/sections', file type elf64-x86-64.                               
        Entry point: 0x555555555040
        0x0000555555554318 - 0x0000555555554334 is .interp
        0x0000555555554338 - 0x0000555555554358 is .note.gnu.property
        0x0000555555554358 - 0x000055555555437c is .note.gnu.build-id
        0x000055555555437c - 0x000055555555439c is .note.ABI-tag
        0x00005555555543a0 - 0x00005555555543c4 is .gnu.hash
        0x00005555555543c8 - 0x0000555555554458 is .dynsym
        0x0000555555554458 - 0x00005555555544e0 is .dynstr
        0x00005555555544e0 - 0x00005555555544ec is .gnu.version
        0x00005555555544f0 - 0x0000555555554520 is .gnu.version_r
        0x0000555555554520 - 0x00005555555545f8 is .rela.dyn
        0x0000555555555000 - 0x0000555555555017 is .init
        0x0000555555555020 - 0x0000555555555030 is .plt
        0x0000555555555030 - 0x0000555555555038 is .plt.got 
        0x0000555555555040 - 0x000055555555513f is .text
        0x0000555555555140 - 0x0000555555555149 is .fini
        0x0000555555556000 - 0x0000555555556019 is .rodata
        0x000055555555601c - 0x0000555555556048 is .eh_frame_hdr
        0x0000555555556048 - 0x00005555555560f4 is .eh_frame
        0x0000555555557e00 - 0x0000555555557e08 is .init_array
        0x0000555555557e08 - 0x0000555555557e10 is .fini_array
        0x0000555555557e10 - 0x0000555555557fc0 is .dynamic 
        0x0000555555557fc0 - 0x0000555555557fe8 is .got
        0x0000555555557fe8 - 0x0000555555558000 is .got.plt 
        0x0000555555558000 - 0x0000555555558020 is .data
        0x0000555555558020 - 0x0000555555558038 is .bss

.init

pwndbg> disass 0x0000555555555000

   0x555555555000 <_init>                    sub    rsp, 8
   0x555555555004 <_init+4>                  mov    rax, qword ptr [rip + 0x2fc5]
   0x55555555500b <_init+11>                 test   rax, rax
   0x55555555500e <_init+14>                 je     _init+18                             <_init+18>
    
   0x555555555012 <_init+18>                 add    rsp, 8
   0x555555555016 <_init+22>                 ret    
    
   0x7ffff7de126c <__libc_start_main+172>    mov    rdi, qword ptr [r14 + 0x108]
   0x7ffff7de1273 <__libc_start_main+179>    test   rdi, rdi
   0x7ffff7de1276 <__libc_start_main+182>    je     __libc_start_main+102                <__libc_start_main+102>
    ...
    ...
    ...
  0x7ffff7de1240 <__libc_start_main+128>    call   __libc_start_call_main               <__libc_start_call_main>
    // rdi: 0x555555555129 (main) ◂— push   rbp
    // rsi: 0x1
    // rdx: 0x7fffffffdfa8 —▸ 0x7fffffffe2f0 ◂— '/home/o54ma/exploit-exercises-protostar-2/ELF/sections'
    
   0x555555555129 <main>                          push   rbp
   0x55555555512a <main+1>                        mov    rbp, rsp
   0x55555555512d <main+4>                        lea    rax, [rip + 0xed7]
   0x555555555134 <main+11>                       mov    qword ptr [rbp - 8], rax
   0x555555555138 <main+15>                       mov    eax, 0
   0x55555555513d <main+20>                       pop    rbp
   0x55555555513e <main+21>                       ret

.rodata

pwndbg> x/5s 0x0000555555556000
0x555555556000 <_IO_stdin_used>:        "\001"
0x555555556002 <_IO_stdin_used+2>:      "\002"
0x555555556004: "HIIOOO"
0x55555555600b: "Hello World!!"
0x555555556019: ""

.data

pwndbg> x/100xb 0x0000555555558000
0x555555558000: 0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x555555558008: 0x08    0x80    0x55    0x55    0x55    0x55    0x00    0x00
0x555555558010 <GLOBAL>:        0xef    0xbe    0xad    0xde    0x00    0x00    0x00    0x00
0x555555558018 <string2>:       0x04    0x60    0x55    0x55    0x55    0x55    0x00    0x00

string2 pointing at 0x555555556004 in .rodata

.bss

pwndbg> x/100x 0x0000555555558020
0x555555558020 <completed.0>:   0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x555555558028 <GLOBAL2>:       0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0x555555558030 <string>:        0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00



Static versus Dynamic binaries

static binaries include all the libraries needed to run the program, while dynamic binaries use shared libraries that are loaded at runtime. The choice between static and dynamic linking depends on factors such as portability, size of the resulting binary, and ease of updating the libraries.

$ file /bin/ls  
# /bin/ls: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=6e3da6f0bc36b6398b8651bbc2e08831a21a90da, for GNU/Linux 3.2.0, stripped

$ gcc -o myprogram myprogram.c            # compiled dynamically by default

$ gcc -static -o myprogram myprogram.c      # compiled statically 

to list the external libraries used by the binary.

$ ldd /bin/ls
   #   linux-vdso.so.1 (0x00007fff6ed3e000)
   #   libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007f82f75c9000)
   #   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f82f73e8000)
   #   libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007f82f734e000)
   #   /lib64/ld-linux-x86-64.so.2 (0x00007f82f7647000)



PLT and GOT - the key to code sharing and dynamic libraries

they are a data structures used for dynamic linking of shared libraries.

PLT (Procedure Linkage Table) : used to call external procedures/functions whose address isn't known in the time of linking, and is left to be resolved by the dynamic linker at run time.

GOT (Global Offset Table) : holds addresses of functions that are dynamically linked.

relocations are entries in binaries that are left to be filled in later –> at link time by the toolchain linker or at runtime by the dynamic linker

In Linux, dynamic linking is performed by a dynamic linker program, which is responsible for locating and loading the shared libraries into memory. When an executable program is loaded, the dynamic linker reads a special section in the executable file called the dynamic section, which contains information about the shared libraries that the program depends on.

such as :

  • .dynamic: This section contains information about the dynamic linker, such as its name and version, and the libraries that the program depends on.

  • .got: This section contains the global offset table, which is used by the dynamic linker to resolve symbols at runtime.

  • .plt: This section contains the procedure linkage table, which is used by the dynamic linker to jump to shared library functions.

  • .rel.dyn: This section contains relocation information for symbols that are defined in shared libraries.

  • .rel.plt: This section contains relocation information for symbols that are used by the program but defined in shared libraries.

The dynamic linker then searches for the required shared libraries and loads them into memory. Once loaded, the shared libraries can be used by the program. If a shared library is not found, the dynamic linker will report an error and the program will not be executed.

Note

the .got and .plt sections are the standard data structures used in dynamic linking. The .got.plt and .plt.got sections are variants that are used in some systems to optimize dynamic linking. The difference between them is in the way they combine entries from the GOT and PLT to optimize the dynamic linking process.

How puts function called dynamically

#                                ┌──────┐
#                                │ puts │
#                                └─▲──▲─┘
#                                  │  │
#           the rest of the time   │  │
#      ────────────────────────────┘  │
#                                     │
# ┌──────────┐   .plt      ┌──────────┴────────┐   .plt      ┌─────────────────────────────────┐
# │ puts@plt ├────────────►│jmp [puts@got.plt] ├────────────►│jmp  <_dl_runtime_resolve_xsave> │
# └──────────┘             └──────────▲────────┘             └────────────────┬────────────────┘
#                                     │                                       │
#                                     │                                       │
#                                     │                                       │
#                                     │         just the first time           │
#                                     └───────────────────────────────────────┘
#                                              the GOT table updated


Ex

#include<stdio.h>

int main(){
	puts("Hello World!!");
	puts("Bye!!");

	return 0;
}
pwndbg> b *main
pwndbg> r
pwndbg> disass main
Dump of assembler code for function main:
=> 0x0000555555555139 <+0>:     push   rbp
   0x000055555555513a <+1>:     mov    rbp,rsp
   0x000055555555513d <+4>:     lea    rax,[rip+0xec0]        # 0x555555556004
   0x0000555555555144 <+11>:    mov    rdi,rax
   0x0000555555555147 <+14>:    call   0x555555555030 <puts@plt>
   0x000055555555514c <+19>:    lea    rax,[rip+0xebf]        # 0x555555556012
   0x0000555555555153 <+26>:    mov    rdi,rax
   0x0000555555555156 <+29>:    call   0x555555555030 <puts@plt>
   0x000055555555515b <+34>:    mov    eax,0x0
   0x0000555555555160 <+39>:    pop    rbp
   0x0000555555555161 <+40>:    ret    
End of assembler dump.

//---------------------------------------------------------------------------------

pwndbg> info files 
        Entry point: 0x555555555050
        0x0000555555554318 - 0x0000555555554334 is .interp
        ...
        ...
        0x0000555555555020 - 0x0000555555555040 is .plt
        0x0000555555555040 - 0x0000555555555048 is .plt.got
        0x0000555555555050 - 0x0000555555555162 is .text
        0x0000555555555164 - 0x000055555555516d is .fini
        0x0000555555556000 - 0x0000555555556018 is .rodata
        ....
        ....
        0x0000555555557fc0 - 0x0000555555557fe8 is .got
        0x0000555555557fe8 - 0x0000555555558008 is .got.plt
        0x0000555555558008 - 0x0000555555558018 is .data
        0x0000555555558018 - 0x0000555555558020 is .bss
pwndbg> ni 4

   0x555555555139 <main>       push   rbp
   0x55555555513a <main+1>     mov    rbp, rsp
   0x55555555513d <main+4>     lea    rax, [rip + 0xec0]
   0x555555555144 <main+11>    mov    rdi, rax
  0x555555555147 <main+14>    call   puts@plt <puts@plt>   // Next instruction to be executed !!
        // s: 0x555555556004 ◂— 'Hello World!!'

//---------------------------------------------------------------------------------
pwndbg> si

  0x555555555030 <puts@plt>                         jmp    qword ptr [rip + 0x2fca]      <puts@got[plt]>
    
   0x555555555036 <puts@plt+6>                       push   0
   0x55555555503b <puts@plt+11>                      jmp    0x555555555020                <0x555555555020>
    
   0x555555555020                                    push   qword ptr [rip + 0x2fca]      <_GLOBAL_OFFSET_TABLE_+8>
   0x555555555026                                    jmp    qword ptr [rip + 0x2fcc]      <_dl_runtime_resolve_xsave>
    
   0x7ffff7fdd150 <_dl_runtime_resolve_xsave>        push   rbx


// rip + 0x2fca ===> puts@got.plt  ===>  puts@plt+6  (in the first time) 
// keep stepping into, and you will end up calling the linker !! 

// now the seccond puts call in main 

   0x555555555139 <main>       push   rbp
   0x55555555513a <main+1>     mov    rbp, rsp
   0x55555555513d <main+4>     lea    rax, [rip + 0xec0]
   0x555555555144 <main+11>    mov    rdi, rax
   0x555555555147 <main+14>    call   puts@plt                <puts@plt>

   0x55555555514c <main+19>    lea    rax, [rip + 0xebf]    
   0x555555555153 <main+26>    mov    rdi, rax
  0x555555555156 <main+29>    call   puts@plt                <puts@plt>
 
   0x55555555515b <main+34>    mov    eax, 0
   0x555555555160 <main+39>    pop    rbp
   0x555555555161 <main+40>    ret

//---------------------------------------------------------------------------------

pwndbg> si

  0x555555555030 <puts@plt>    jmp    qword ptr [rip + 0x2fca]      <puts>
    
   0x7ffff7e31820 <puts>        push   r14
   0x7ffff7e31822 <puts+2>      push   r13
   0x7ffff7e31824 <puts+4>      push   r12
   0x7ffff7e31826 <puts+6>      mov    r12, rdi

// rip + 0x2fca ===>  puts@got.plt  ===>  puts

ELF Security

1. Stripping

the process of reducing the size of a binary by removing some of the debugging and symbol information that is typically included in the binary during the linking process.

it can also have security implications, since it can make it more difficult to analyze or debug the binary.

The symbol table

The symbol table contains information about the symbols defined in the binary, including function and variable names, their memory addresses, and other attributes.

#include<stdio.h>

int fun1(){
   return 0;
}

void fun2(){
   puts("Hi");
}

int main(){
   return 0;
}
$ gcc symbols.c -o symbols

$ file symbols            
# symbols: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f35a2f312de31cd75205536eb0d353b0d07db98e, for GNU/Linux 3.2.0, not stripped
$ nm symbols | grep " T " 
   # 0000000000001168 T _fini
   # 0000000000001139 T fun1
   # 0000000000001144 T fun2
   # 0000000000001000 T _init
   # 000000000000115a T main
   # 0000000000001050 T _start

$ nm symbols | grep " U " 
   # U __libc_start_main@GLIBC_2.34
   # U puts@GLIBC_2.2.5


# T : The symbol is in the text (code) section.
# U : The symbol is undefined.
$ strip symbols
# OR
$ gcc symbols.c -o symbols -s

$ file symbols 
# symbols: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=f35a2f312de31cd75205536eb0d353b0d07db98e, for GNU/Linux 3.2.0, stripped
$ nm symbols              
# nm: symbols: no symbols

How to find the main function address on gdb ?? (stripped binary)

pwndbg> b *main
// No symbol table is loaded.  Use the "file" command.
pwndbg> b _start
// Function "_start" not defined.
pwndbg> b __libc_start_main
// Function "__libc_start_main" not defined.

// -------------------------------------------------------

// Just Try to run it normally in the first time 
pwndbg> r
// [Inferior 1 (process 13270) exited normally]

pwndbg> b __libc_start_main                     // __libc_start_main --> will be defined after that by the debugger

pwndbg> r 
// Breakpoint 1, __libc_start_main_impl(main=0x555555400920, argc=1, argv=0x7fffffffdef8, init=0x555555400ba0, fini=0x555555400c10, rtld_fini=0x7ffff7fceaa0 <_dl_fini>,stack_end=0x7fffffffdee8) at ../csu/libc-start.c:340

// -------------------------------------------------------

main=0x555555400920     is the addr of the main ^^

2. FULL RELRO vs. Partial RELRO

RELRO is a generic exploit mitigation technique to harden the data sections of an ELF binary or process.

From an attackers point-of-view, partial RELRO makes almost no difference, other than it forces the GOT to come before the BSS in memory,

eliminating the risk of a buffer overflows on a global variable overwriting GOT.


Full RELRO makes the entire GOT read-only which removes the ability to perform a "GOT overwrite" attack

// main.c
#include<stdio.h>

int main(){
    printf("Hello World\n");

return 0;
}
$ gcc main.c -o main      // Partial RELRO 
$ r2 main

    [0x00001050]> iS
    [Sections]

    nth paddr        size vaddr       vsize perm name
    ―――――――――――――――――――――――――――――――――――――――――――――――――
    0   0x00000000    0x0 0x00000000    0x0 ---- 
         ...
         ...
    13  0x00001020   0x20 0x00001020   0x20 -r-x .plt                   // .plt
    14  0x00001040    0x8 0x00001040    0x8 -r-x .plt.got               // .plt.got
    15  0x00001050  0x103 0x00001050  0x103 -r-x .text                  // .text
         ...
         ...
    23  0x00002fc0   0x28 0x00003fc0   0x28 -rw- .got                   // .got          it's writable
    24  0x00002fe8   0x20 0x00003fe8   0x20 -rw- .got.plt               // .got.plt      it's writable
    25  0x00003008   0x10 0x00004008   0x10 -rw- .data                  // .data         it's writable
    26  0x00003018    0x0 0x00004018    0x8 -rw- .bss                   // .bss          it's writable
         ...
         ...
$ gcc main.c -o main -Wl,-z,relro,-z,now      // FULL RELRO 
$ r2 main

    [0x00001050]> iS
    [Sections]

    nth paddr        size vaddr       vsize perm name
    ―――――――――――――――――――――――――――――――――――――――――――――――――
    0   0x00000000    0x0 0x00000000    0x0 ---- 
         ...
         ...
    13  0x00001020   0x20 0x00001020   0x20 -r-x .plt                   // .plt
    14  0x00001040    0x8 0x00001040    0x8 -r-x .plt.got               // .plt.got
    15  0x00001050  0x103 0x00001050  0x103 -r-x .text                  // .text
         ...
         ...
    23  0x00002fc0   0x28 0x00003fc0   0x28 -rw- .got                   // .got          it's writable
    24  0x00002fe8   0x20 0x00003fe8   0x20 -rw- .got.plt               // .got.plt      it's writable
    25  0x00003008   0x10 0x00004008   0x10 -rw- .data                  // .data         it's writable
    26  0x00003018    0x0 0x00004018    0x8 -rw- .bss                   // .bss          it's writable
         ...
         ...

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, int *argv[])
{

// puts("adding the puts function in GOT table");

// printf("Address of GOT: %p\n", &puts);
// size_t *p = (size_t *) strtol(argv[1], NULL, 10);
// int (*p2)(const char *s) = puts;

// p[0] = p2;

// p2("HII");
// printf("RELRO: %p\n", p);

void *main_addr = (void *)(&main - 0x149);
printf("The address of _init is: %p\n", main_addr);

void *puts_got_addr = (void *)(main_addr + 0x3000);
printf("The address of puts@got is: %p\n", puts_got_addr);

puts("HI");

// memcpy(puts_got_addr, &puts, 8);


return 0;
}

Find glibc version

The GNU C Library project provides the core libraries for the GNU system and GNU/Linux systems, as well as many other systems that use Linux as the kernel.

So if you managed to know the ubuntu version the binary is running on. you could find the libc versoin that the binary uses, which could help in revealing some exploits

glibc/versions

Tip

some ctf challenges provide a Dockerfile, which could tell the version of ubuntu used.

# if you were givin the shared libc file that the binary use. you can find it's version like this. 
$ strings ./libc.so.6 | grep ubuntu
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1.2) stable release version 2.27.
<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
# To find glibc version on your system
$ ldd --version

Patch the binary !!

patching ELF binaries to use specific versions of glibc with pwninit :

$ tree 
.
└── Folder
    ├── binary
    └── libc.so.6

$ cd Folder
$ pwninit

$ tree 
.
└── Folder
    ├── binary
    ├── binary_patched
    ├── ld-2.27.so
    └── libc.so.6

what pwninit did ??

  • Downloaded a linker (ld-linux.so.*) that can segfaultlessly load the provided libc
  • Downloaded debug symbols and unstrip the libc
  • Patched the binary with patchelf to use the correct RPATH and interpreter for the provided libc
# after pwninit, you can run patchelf on the non-patched binary to patch it ^^ 
$ patchelf  --set-interpreter ./ld-2.27.so ./binary

CheckSec

$ checksec binary
[*] '/tmp/a.out'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

PIE vs. ASLR

Address Space Layout Randomisation (ASLR), every time you run a binary, libc (and other libraries) get loaded into a different memory address.

simply it randomizes everything in the proccess except hte code section

Position Independent Executable (PIE), randomizes the code & data sections at runtime, like main address and other functions addresses.

a table comparing the ELF sections that are affected by PIE and ASLR mitigation techniques: (NOT SURE !!)

ELF Section	   PIE	   ASLR
.text          Yes	   Yes
.data          Yes	   Yes
.bss           Yes	   Yes
.got           Yes	   No
.dynamic       Yes	   No
.plt           Yes	   No
.rel           Yes	   No
.symtab        Yes	   No
.rodata        No      Yes
Note

The main difference is that PIE can be compiled into the binary while the presence of ASLR is completely dependant on the environment running the binary.

But anyway, ASLR is enabled by PIE, but PIE is a thing even with ASLR disabled. :triumph:

to Disable ASLR on your system : (Do it everytime the system restarted) ```bash $ sudo bash -c "echo 0 > /proc/sys/kernel/randomize_va_space"

$ # sudo bash -c "echo 2 > /proc/sys/fs/suid_dumpable" ?? ```


NX / DEP

Data Execution Prevention (DEP) or No-Execute (NX), When this option is enabled, it works with the processor to help prevent buffer overflow attacks by blocking code execution from memory that is marked as non-executable.

Stack Canaries

One way to prevent the stack-based buffer overflow, it's a secret value placed on the stack which changes every time the program is started. Prior to a function return,

the stack canary is checked and if it appears to be modified, the program exits immeadiately.

GOT & PLT

Global Offset Table (GOT)

Phy vs. Virt Addresses

$ pmap

Ghidra

References

  • https://www.baeldung.com/linux/executable-and-linkable-format-file
  • https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
  • https://www.technovelty.org/linux/plt-and-got-the-key-to-code-sharing-and-dynamic-libraries.html