DEFINITIONS FROM MACHINE
------------------------

Before we begin, every tool and term must be defined.
All definitions come from man pages and headers installed on this machine.


MACHINE CONFIGURATION
---------------------

All output in this document was captured on:

    OS:       Ubuntu 24.04.3 LTS (Noble Numbat)
    Kernel:   6.8.0-x86_64
    Compiler: GCC 13.3.0
    libc:     GNU libc 2.39

Compile command used:
    gcc minimal_open.c -o minimal_open
    (No special flags. System defaults apply.)

Ubuntu 24.04 GCC defaults (even without flags):
    -fcf-protection=full   Adds endbr64 to mark valid jump targets.
                           On CPUs without CET, this executes as NOP.
                           On Intel 11th gen+ or AMD Zen 3+, CPU enforces it.

    -fno-omit-frame-pointer Keeps: push %rbp / mov %rsp,%rbp.
                            This is Ubuntu 24.04 policy for better debugging.
                            Standard GCC -O2 omits frame pointer.

If your output differs:
    1. endbr64 missing?
       Your distro does not enable -fcf-protection by default.
       Fix: gcc -fcf-protection=full minimal_open.c -o minimal_open

    2. No push %rbp / mov %rsp,%rbp?
       Your GCC uses -fomit-frame-pointer (standard upstream).
       Fix: gcc -fno-omit-frame-pointer minimal_open.c -o minimal_open

    3. No stack canary (%fs:0x28)?
       Canary is in LIBC's open(), not in your main().
       The worksheet shows libc internals via: gdb -ex "x/15i 0x7ffff7d1b150"
       Your main() has no buffer, so no canary in YOUR code. That is correct.

Verify your GCC defaults:
    $ gcc -Q --help=target 2>/dev/null | grep cf-protection
    $ gcc -Q -O0 --help=optimizers 2>/dev/null | grep frame-pointer

$ man objdump | head -5
  NAME
    objdump - display information from object files

objdump reads binary files and shows their contents.
The -d flag disassembles machine code to assembly.

$ man readelf | head -5
  NAME
    readelf - display information about ELF files

readelf reads ELF files and shows their structure.
Unlike objdump, it does not disassemble code.

What is ELF?

$ man elf | head -10
  NAME
    elf - format of Executable and Linking Format (ELF) files
  DESCRIPTION
    The header file <elf.h> defines the format of ELF executable
    binary files. Amongst these files are normal executable files,
    relocatable object files, core files, and shared objects.

ELF = file format for executables and object files on Linux.
.o files are ELF.
Executable files are ELF.
Shared libraries (.so) are ELF.

$ man ld | head -15
  NAME
    ld - The GNU linker
  DESCRIPTION
    ld combines a number of object and archive files, relocates
    their data and ties up symbol references. Usually the last
    step in compiling a program is to run ld.

Linker = program that combines .o files into executable.
It fills in addresses that the compiler left blank.

$ man nm | head -5
  NAME
    nm - list symbols from object files

nm shows symbol names in object files.
Symbols = function names, variable names.

$ man gdb | grep -A3 "\-batch"
  --batch
    Run in batch mode. Exit with status 0 after processing
    all the command files specified with -x.

-batch = run gdb without interactive mode, exit when done.

$ man gdb | grep -A2 "\-ex"
  -ex command
    Execute given GDB command.

-ex = run a single gdb command.

Example: gdb -batch -ex "x/gx 0x3fd0" ./minimal_open
This runs gdb, executes "x/gx 0x3fd0", then exits.
x/gx = examine memory as giant (8-byte) hex.


WHAT IS LITTLE-ENDIAN?
----------------------

Question: How are multi-byte values stored in memory?

Intel CPUs use little-endian byte order.
Little = least significant byte is stored at lowest address.

Example:
    Value: 0x12345678 (4 bytes).
    Bytes: 78 56 34 12 (stored in memory).
    Address 0: 78 (least significant).
    Address 1: 56.
    Address 2: 34.
    Address 3: 12 (most significant).

When reading raw memory, you must reverse the byte order.


WHAT IS RIP?
------------

RIP = Instruction Pointer register (64-bit).
It holds the address of the NEXT instruction to execute.
When the CPU fetches an instruction, RIP advances past it.

Example:
    Instruction at 0x1169 is 5 bytes long.
    CPU fetches it.
    RIP becomes 0x1169 + 5 = 0x116e.

PC-relative addressing uses RIP to calculate targets.
Target = RIP + Displacement.


WHAT IS BASE AND OFFSET?
------------------------

Base = starting address of a memory region.
Offset = distance from the base.
Final Address = Base + Offset.

Example:
    libc base = 0x7ffff7c00000.
    open offset = 0x11b150.
    open address = 0x7ffff7c00000 + 0x11b150.


WHAT IS RUNTIME?
----------------

Runtime = when the program is actually running.
Contrast with:
    Compile time = when the compiler runs.
    Link time = when the linker runs.
    Load time = when the kernel loads the program.

Some addresses are not known until runtime.
ASLR randomizes base addresses at load time.


STAGE 0: CREATING THE OBJECT FILE
----------------------------------

$ cat minimal_open.c
  #include <fcntl.h>
  int main() {
      int fd = open("somefile", O_RDWR);
      return fd;
  }

Question: How do we create minimal_open.o?

$ man gcc | grep -A2 "^\s*-c"
  -c  Compile or assemble the source files, but do not link.
      The output is an object file for each source file.

$ gcc -c minimal_open.c -o minimal_open.o

-c = compile only, do not link.
Output: minimal_open.o (ELF object file).

$ file minimal_open.o
  minimal_open.o: ELF 64-bit LSB relocatable, x86-64

Relocatable = contains placeholders for addresses.
The linker will fill them in.


STAGE 1: SOURCE CODE
--------------------

$ cat minimal_open.c | grep open
  int fd = open("somefile", O_RDWR);

Question: What is O_RDWR?

$ grep -r "define O_RDWR" /usr/include | grep 0
  /usr/include/asm-generic/fcntl.h:#define O_RDWR 00000002

O_RDWR = 00000002 (octal notation).

Octal 02:
    0*8 + 2*1 = 2 decimal.

Preprocessor:
    Replaces O_RDWR with 2.

$ gcc -E minimal_open.c | tail -1
  int fd = open("somefile", 2);

At this stage: "open" is text. No address.


STAGE 2: COMPILATION
--------------------

The compiler translates C to machine code.

$ objdump -d minimal_open.o

Question: What is objdump showing?

objdump -d = disassemble.
It converts machine code bytes to assembly mnemonics.

$ objdump -d minimal_open.o | grep "call"
  20:   e8 00 00 00 00    call   25 <main+0x25>

Breakdown:
    20 = offset in the file (hex).
    e8 = opcode.
    00 00 00 00 = operand (4 bytes).

Question: What is opcode e8?

From Intel manual: e8 = CALL rel32.
rel32 = relative 32-bit displacement.
The CPU will jump to: RIP + displacement.

Question: Why is the operand all zeros?

The compiler does not know where open() is.
open() is in libc.
libc is a separate file.
The compiler has never seen libc.
It writes zeros as a placeholder.

Question: How does the linker know to fill this?

$ readelf -r minimal_open.o | grep 21
  000000000021  000500000004 R_X86_64_PLT32    ... open - 4

This is a RELOCATION RECORD.

Breakdown:
    000000000021 = offset where the blank is.
    000500000004 = info field (contains symbol index and type).
    R_X86_64_PLT32 = relocation type.

Question: What is R_X86_64_PLT32?

$ cat /usr/include/elf.h | grep "R_X86_64_PLT32"
  #define R_X86_64_PLT32    4    /* 32 bit PLT address */

PLT = Procedure Linkage Table.
32 = write a 32-bit value.
The linker will compute: Target - RIP.

Question: What is -4 addend?

The relocation says: open - 4.
This is the addend.

RIP points to the NEXT instruction after the call.
The blank starts at 0x21.
The blank is 4 bytes.
Next instruction is at 0x21 + 4 = 0x25.
RIP = 0x25 when evaluating the displacement.

Linker formula:
    Displacement = Target - RIP.
    Displacement = Target - (Blank + 4).

The -4 is baked into the relocation.


STAGE 3: LINKING
----------------

$ man ld | grep -A2 "DESCRIPTION"
  ld combines a number of object and archive files, relocates
  their data and ties up symbol references.

We run:
$ gcc minimal_open.o -o minimal_open

This invokes the linker (ld).

Question: What sections does the linker create?

$ readelf -S minimal_open | grep -E "(plt|text|got)"
  .plt      0x1020
  .plt.got  0x1040
  .plt.sec  0x1050
  .text     0x1060
  .got      0x3fb8

Definitions:

.text = your code.
    This is where main() lives.

.plt = Procedure Linkage Table.
    Small code stubs for calling external functions.
    External = in another file (like libc).

.plt.sec = PLT secure entries.
    Direct jump stubs with security markers.

.got = Global Offset Table.
    Data section.
    Stores addresses of external functions.
    The dynamic linker writes here at runtime.

Why separate .plt and .got?

.plt contains CODE (read + execute).
.got contains DATA (read + write).
Separation enables memory protection.
Writable pages cannot be executed.
Executable pages cannot be written.


STAGE 3: GOT STRUCTURE
----------------------

Question: What is in the GOT?

$ readelf -x .got minimal_open
  0x3fb8 c83d0000 00000000 00000000 00000000
  0x3fc8 00000000 00000000 30100000 00000000

Breakdown (each slot is 8 bytes):
    0x3fb8: c8 3d 00 00 ... = GOT[0].
    0x3fc0: 00 00 00 00 ... = GOT[1].
    0x3fc8: 00 00 00 00 ... = GOT[2].
    0x3fd0: 30 10 00 00 ... = GOT[3].

GOT[3] at 0x3fd0:
    Bytes: 30 10 00 00 00 00 00 00.
    Little-endian: reverse = 00 00 00 00 00 00 10 30.
    Value: 0x1030.

Question: Why are GOT[0], GOT[1], GOT[2] reserved?

ELF ABI specification:
    GOT[0] = _DYNAMIC section address (linker metadata).
    GOT[1] = link_map structure (list of loaded libraries).
    GOT[2] = resolver function address.

These are used by the dynamic linker at runtime.
User functions start at GOT[3].


STAGE 3: PLT STRUCTURE
----------------------

Question: What is at address 0x1030?

$ objdump -d minimal_open | grep -A3 "1030:"
  1030:   f3 0f 1e fa          endbr64
  1034:   68 00 00 00 00       push   $0x0
  1039:   e9 e2 ff ff ff       jmp    1020 <_init+0x20>

This is the PLT stub for open.

Instruction breakdown:

1030: endbr64.
    f3 0f 1e fa = opcode.
    endbr64 = End Branch 64-bit.
    Marks a valid indirect jump target.
    CPU checks: did we land on endbr64?
    If not, CPU raises exception.
    Protection against ROP attacks.

1034: push $0x0.
    68 00 00 00 00 = opcode + operand.
    Push value 0 onto the stack.
    0 = index of open in the relocation table.

1039: jmp 1020.
    e9 e2 ff ff ff = opcode + displacement.
    Jump to address 0x1020.
    0x1020 is the PLT header.

Question: What is at 0x1020?

$ objdump -d minimal_open | grep -A3 "<.plt>:"
  1020:   ff 35 9a 2f 00 00    push   0x2f9a(%rip)
  1026:   ff 25 9c 2f 00 00    jmp    *0x2f9c(%rip)

1020: push 0x2f9a(%rip).
    Push value at address (RIP + 0x2f9a).
    RIP = 0x1026 (after this instruction).
    0x1026 + 0x2f9a = 0x3fc0.
    0x3fc0 = GOT[1] = link_map pointer.

1026: jmp *0x2f9c(%rip).
    Indirect jump.
    * means: read address from memory, then jump there.
    RIP = 0x102c.
    0x102c + 0x2f9c = 0x3fc8.
    0x3fc8 = GOT[2] = resolver function.

The PLT header calls the resolver with:
    Argument 1: link_map (from stack).
    Argument 2: index 0 (pushed by stub).


STAGE 3: PLT.SEC ENTRY
----------------------

Question: Where does main() call?

$ objdump -d minimal_open | grep "call.*open"
  1169:   e8 e2 fe ff ff       call   1050 <open@plt>

The call goes to 0x1050 (in .plt.sec).

Question: What is at 0x1050?

$ objdump -d minimal_open | grep -A2 "open@plt"
  1050:   f3 0f 1e fa          endbr64
  1054:   ff 25 76 2f 00 00    jmp    *0x2f76(%rip)

1050: endbr64.
    Valid branch target marker.

1054: jmp *0x2f76(%rip).
    Indirect jump.
    Read address from memory and jump there.

Question: What address does it read?

Calculate:
    Instruction at 0x1054.
    Instruction length = 6 bytes.
    Next RIP = 0x1054 + 6 = 0x105a.
    Displacement = 0x2f76.
    Target = 0x105a + 0x2f76.

    0x105a = 4186.
    0x2f76 = 12150.
    4186 + 12150 = 16336 = 0x3fd0.

Result: reads from 0x3fd0 = GOT[3].
GOT[3] contains 0x1030 (the stub).

First call: jumps to stub (slow path).
After resolution: GOT[3] contains libc address (fast path).


STAGE 3: CALL INSTRUCTION PATCH
-------------------------------

Before linking (in .o file):
    20:   e8 00 00 00 00

After linking (in executable):
    1169: e8 e2 fe ff ff

Question: How did 00 00 00 00 become e2 fe ff ff?

The linker applied the relocation formula.

Calculate:
    Call at 0x1169.
    Length = 5 bytes.
    Next RIP = 0x116e.
    Target = 0x1050.
    Displacement = 0x1050 - 0x116e.

    0x1050 = 4176.
    0x116e = 4462.
    4176 - 4462 = -286.

Question: How is -286 encoded?

Two's complement (32-bit):
    286 = 0x0000011e.
    Invert bits: 0xfffffee1.
    Add 1: 0xfffffee2.

Little-endian:
    Value: ff ff fe e2.
    Stored: e2 fe ff ff.

Verified: e2 fe ff ff matches.


STAGE 4: RUNTIME
----------------

What is runtime?
    When the program actually executes.
    The kernel has loaded it into memory.

What happens before main()?
    1. Kernel loads ELF into memory.
    2. Kernel invokes dynamic linker (ld-linux.so).
    3. Dynamic linker loads shared libraries (libc).
    4. Dynamic linker fills GOT[1] and GOT[2].
    5. Control passes to _start.
    6. _start calls __libc_start_main.
    7. __libc_start_main calls main().

Question: What is in GOT[3] before first call?

$ gdb -batch -ex "break main" -ex "run" -ex "x/gx 0x3fd0" ./minimal_open
  0x3fd0: 0x0000555555555030

The value is 0x555555555030.
Low bits: 0x1030 (our stub).
High bits: 0x555555554000 (ASLR base).

Question: What is ASLR?

ASLR = Address Space Layout Randomization.
The kernel loads the executable at a random base address.
Each run = different base.
Prevents attackers from knowing where code is.

Calculation:
    Base (random): 0x555555554000.
    Stub offset: 0x1030.
    Runtime address: 0x555555554000 + 0x1030 = 0x555555555030.


STAGE 4: RESOLUTION
-------------------

We established:
    Executable base (from gdb): 0x555555554000.
    PLT.SEC offset (from readelf): 0x1050.
    GOT[3] offset (from readelf): 0x3fd0.
    Stub offset (from objdump): 0x1030.

Derive runtime addresses:
    PLT.SEC runtime = 0x555555554000 + 0x1050 = 0x555555555050.
    GOT[3] runtime  = 0x555555554000 + 0x3fd0 = 0x555555557fd0.
    Stub runtime    = 0x555555554000 + 0x1030 = 0x555555555030.

First call: call 0x1050.

Step 1:
    The call instruction is at runtime address (base + 0x1169).
    Call target = base + 0x1050 = 0x555555555050.
    CPU jumps to 0x555555555050.

Step 2:
    At 0x555555555050 is: jmp *0x2f76(%rip).
    This reads from GOT[3].
    GOT[3] runtime address = 0x555555557fd0.
    Value at GOT[3] = 0x555555555030 (stub).

Step 3:
    CPU jumps to 0x555555555030 (the stub).

Step 4:
    Stub at 0x555555555030 does:
        push $0x0    (index 0)
        jmp 0x555555555020   (PLT header = base + 0x1020)

Step 5:
    PLT header at 0x555555555020 does:
        push GOT[1]  (link_map)
        jmp *GOT[2]  (resolver)

    GOT[2] = base + 0x3fc8 = 0x555555557fc8.
    Resolver address is stored there.

Step 6:
    Resolver receives: (link_map, 0).
    Resolver searches libc symbol table for index 0.
    Index 0 = "open".

Libc addresses:

$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep " open@"
  000000000011b150  ... open@@GLIBC_2.2.5

Offset in libc: 0x11b150.

libc base (from /proc/self/maps, example run): 0x7ffff7c00000.

Derive open() address:
    libc base: 0x7ffff7c00000.
    open offset: 0x11b150.
    open runtime = 0x7ffff7c00000 + 0x11b150.

    Calculate:
        0x7ffff7c00000 = 140737349853184.
        0x11b150 = 1159504.
        Sum = 140737351012688 = 0x7ffff7d1b150.

Step 7:
    Resolver writes 0x7ffff7d1b150 into GOT[3].
    GOT[3] at 0x555555557fd0 now contains 0x7ffff7d1b150.

Step 8:
    Resolver jumps to 0x7ffff7d1b150.

Question: What happens on second call?

$ gdb -batch -ex "break *main+50" -ex "run" -ex "x/gx 0x3fd0" ./minimal_open
  0x3fd0: 0x00007ffff7d1b150

GOT[3] = 0x7ffff7d1b150.
Second call: reads GOT[3], jumps directly to libc.
No resolver. Fast.

This is LAZY BINDING.
Resolve functions only when first called.
Saves startup time.


STAGE 5: INSIDE LIBC - WEAK SYMBOLS
-----------------------------------

$ readelf -s /lib/x86_64-linux-gnu/libc.so.6 | grep 11b150
  000000000011b150  ... __open_nocancel
  000000000011b150  ... open@@GLIBC_2.2.5
  000000000011b150  ... __libc_open64

Three names, same address.
They are aliases.

$ nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " open@"
  000000000011b150 W open@@GLIBC_2.2.5

W = Weak symbol.

Question: What is a weak symbol?

A weak symbol can be overridden.
If you define your own open(), linker uses yours.
Libc's open is a fallback.

Why weak?
    Allows LD_PRELOAD tricks.
    Allows custom implementations.


STAGE 5: INSIDE LIBC - SECURITY
-------------------------------

$ gdb -batch -ex "x/10i 0x7ffff7d1b150" ./minimal_open
  0x7ffff7d1b150: endbr64
  0x7ffff7d1b154: push   %rbp
  0x7ffff7d1b155: mov    %rsp,%rbp
  0x7ffff7d1b158: push   %r12
  0x7ffff7d1b15a: push   %rbx
  0x7ffff7d1b15b: sub    $0x60,%rsp

First instruction: endbr64.
We defined this earlier.
Valid indirect branch target marker.

sub $0x60,%rsp:
    0x60 hex = 96 decimal.
    Reserve 96 bytes on stack.

Question: What is the stack canary?

$ gdb -batch -ex "x/15i 0x7ffff7d1b150" ./minimal_open | grep fs
  mov    %fs:0x28,%rax
  mov    %rax,-0x48(%rbp)

%fs = segment register.
%fs points to Thread Local Storage (TLS).
TLS = per-thread data.

%fs:0x28 = offset 40 bytes into TLS.
This contains a random value (the canary).

The code:
    1. Reads random value from TLS.
    2. Stores it on stack at rbp-0x48.
    3. Before returning, checks: did it change?
    4. If changed: buffer overflow detected, crash.


STAGE 5: INSIDE LIBC - ARGUMENT SHUFFLE
---------------------------------------

You called: open("somefile", O_RDWR).

Registers at entry:
    RDI = pointer to "somefile".
    RSI = 2 (O_RDWR).

Kernel expects: openat(dirfd, pathname, flags, mode).

$ gdb -batch -ex "x/5i 0x7ffff7d1b199" ./minimal_open
  mov    $0xffffff9c,%edi
  mov    $0x101,%eax
  syscall

EDI = 0xffffff9c.

Question: What is 0xffffff9c?

Two's complement of -100.
    100 = 0x64.
    Invert: 0xffffff9b.
    Add 1: 0xffffff9c.

$ grep AT_FDCWD /usr/include/linux/fcntl.h
  #define AT_FDCWD -100

AT_FDCWD = "use current working directory".

EAX = 0x101 = 257.

$ grep 257 /usr/include/asm/unistd_64.h
  #define __NR_openat 257

257 = syscall number for openat.

Transformation:
    open(file, flags) becomes openat(-100, file, flags, 0).

Why openat?
    Kernel deprecated sys_open.
    openat is unified.
    AT_FDCWD makes it behave like old open.


STAGE 6: SYSCALL
----------------

Registers before syscall:
    RAX = 257 (syscall number).
    RDI = -100 (AT_FDCWD).
    RSI = pointer to "somefile".
    RDX = 2 (O_RDWR).
    R10 = 0 (mode).

syscall instruction:
    Transfers control to kernel.
    User-space journey complete.


FAILURE MODES
-------------

$ grep -E "ENOENT|EACCES|EMFILE" /usr/include/asm-generic/errno-base.h
  #define ENOENT  2
  #define EACCES 13
  #define EMFILE 24

+--------+-------+----------------------------+
| errno  | Value | Condition                  |
+--------+-------+----------------------------+
| ENOENT |   2   | File does not exist        |
| EACCES |  13   | Permission denied          |
| EMFILE |  24   | Too many open files        |
+--------+-------+----------------------------+

$ strace ./minimal_open 2>&1 | grep openat
  openat(AT_FDCWD, "somefile", O_RDWR) = -1 ENOENT


COMPLETE CHAIN
--------------

SOURCE: open("somefile", O_RDWR)
    |
    v
O_RDWR = 2 (from /usr/include/asm-generic/fcntl.h).
    |
    v
gcc -c: creates minimal_open.o.
Compiler writes: e8 00 00 00 00.
Relocation: R_X86_64_PLT32, offset 0x21, addend -4.
    |
    v
gcc (linker): creates executable.
Creates: .plt.sec (0x1050), .got (0x3fd0).
Patches: e8 00 00 00 00 -> e8 e2 fe ff ff.
GOT[3] = 0x1030 (stub).
    |
    v
Runtime: kernel loads, dynamic linker runs.
libc loads at random base (ASLR).
    |
    v
First call: call 1050 -> jmp *GOT[3] -> stub.
Stub: push 0, jmp resolver.
Resolver: open = base + 0x11b150.
GOT[3] = 0x7ffff7d1b150.
    |
    v
Libc: endbr64, canary, checks.
Shuffle: RDI=-100, RAX=257.
    |
    v
syscall -> Kernel.
sys_openat(-100, "somefile", 2, 0).
    |
    v
Return: file descriptor or -1 with errno.

Every definition from man pages.
Every value from system headers.
Every address from tool output.