Back to Technology

x86 Assembly Series Part 18: Memory Protection & Security

February 6, 2026 Wasil Zafar 35 min read

Understand memory vulnerabilities and protections: buffer overflows, stack canaries, ASLR, DEP/NX, return-oriented programming (ROP), and secure coding practices in assembly language.

Table of Contents

  1. Memory Vulnerabilities
  2. Protection Mechanisms
  3. Return-Oriented Programming
  4. Secure Assembly Practices

Memory Vulnerabilities

Buffer Overflow

Classic Vulnerability: Writing beyond buffer bounds can overwrite adjacent memory, including return addresses, allowing attackers to hijack control flow.
; Vulnerable code pattern
section .bss
    buffer resb 64          ; 64-byte buffer

section .text
vulnerable_func:
    ; No bounds checking!
    lea rdi, [buffer]
    call gets               ; NEVER use gets()
    ret                     ; May jump to attacker-controlled address

Stack Smashing Attack

A stack smashing attack overwrites the return address to hijack control flow. Let's visualize exactly what happens:

Stack Layout During Function Call:
                         High Memory
┌───────────────────┐
│ Return Address   │ ← Attacker's target! 
├───────────────────┤    Overwrite this to redirect execution
│ Saved RBP        │
├───────────────────┤
│ Local Buffer[63] │
│ ...              │  Buffer overflow direction ↑
│ Local Buffer[0]  │ ← Input starts here, overflows upward
└───────────────────┘
                         Low Memory (RSP)

Overflow Attack:
1. Attacker inputs 64+ bytes via gets()
2. Input overflows buffer, overwrites saved RBP
3. Keeps writing, overwrites return address
4. When function returns, RIP = attacker-controlled!
; Exploitable function
vulnerable:
    push rbp
    mov rbp, rsp
    sub rsp, 64             ; 64-byte buffer
    
    lea rdi, [rbp-64]       ; Buffer address
    call gets               ; Reads unlimited input!
    
    leave                   ; mov rsp, rbp; pop rbp
    ret                     ; pop rip - DANGER: may be overwritten!

; Attack payload (conceptual)
; [64 bytes padding][8 bytes fake RBP][8 bytes shellcode_addr]
Classic Attack Flow: Attacker sends 80 bytes: 64 bytes trash + 8 bytes fake RBP + 8 bytes pointing to shellcode. When ret executes, the CPU jumps to shellcode instead of the real caller.

Protection Mechanisms

Stack Canaries

; GCC -fstack-protector inserts canary
function_with_canary:
    push rbp
    mov rbp, rsp
    sub rsp, 80
    
    ; Canary placed between buffer and saved RBP
    mov rax, fs:[0x28]      ; Load canary from TLS
    mov [rbp-8], rax        ; Store on stack
    
    ; ... function body ...
    
    ; Check canary before return
    mov rax, [rbp-8]
    xor rax, fs:[0x28]
    jnz .stack_smash_detect ; Abort if modified
    
    leave
    ret
.stack_smash_detect:
    call __stack_chk_fail

Address Space Layout Randomization (ASLR)

ASLR randomizes where code and data are loaded in memory each time a program runs. This breaks exploits that hardcode addresses.

# Observe ASLR in action
$ cat /proc/sys/kernel/randomize_va_space
2   # 0=off, 1=stack/mmap, 2=full (heap too)

# Run program twice, check stack address
$ ./test_aslr
Stack address: 0x7fff2a3b4c80
$ ./test_aslr
Stack address: 0x7fffde123a50    # Different each time!

# Check library base addresses
$ ldd /bin/ls | head -2
    linux-vdso.so.1 => (0x00007ffd12345000)    # Randomized
    libc.so.6 => /lib/x86_64.../libc.so.6 (0x00007f8abc123000)
ASLR LevelRandomized RegionsEntropy (bits)
Level 1Stack, shared libraries, mmap~28 bits
Level 2+ Heap (brk)~28 bits
PIE Binary+ Main executable~28 bits
Bypassing ASLR: Attackers use information leaks (format string bugs, buffer over-reads) to discover actual addresses at runtime, then calculate offsets to known functions.

Data Execution Prevention (DEP/NX)

DEP (Windows) / NX bit (Linux) marks memory pages as non-executable. Even if an attacker gets shellcode into memory, the CPU refuses to execute it.

Page Table Entry NX Bit:
┌─────────────────────────────────────────────┐
Bit 63: NX (No eXecute)
  0 = Page is executable (code)
  1 = Page is NOT executable (data/stack)
└─────────────────────────────────────────────┘

W^X (Write XOR Execute) Policy:
  - A page can be Writable OR Executable, never both
  - Stack: RW- (read, write, no execute)
  - Code:  R-X (read, execute, no write)
  - Heap:  RW- (read, write, no execute)
# Check memory protections on a process
$ cat /proc/$(pidof victim)/maps
00400000-00401000 r-xp  /victim        # Code: executable
00600000-00601000 rw-p  /victim        # Data: writable, not exec
7ffffffde000-7ffffffff000 rw-p [stack] # Stack: not executable

# Attempt to execute shellcode on NX stack
$ ./shellcode_test
Segmentation fault (core dumped)       # CPU refused!
Legacy Note: The NX bit requires CPU support (most CPUs since ~2004) and 64-bit mode (or PAE in 32-bit). Check with grep nx /proc/cpuinfo.

Return-Oriented Programming (ROP)

ROP bypasses NX by reusing existing executable code snippets called "gadgets." Instead of injecting shellcode, attackers chain together instruction sequences that end in ret.

What is a Gadget?

A gadget is any sequence of instructions ending in RET:

; Example gadgets found in libc:
pop rdi; ret          ← Set RDI (first argument)
pop rsi; ret          ← Set RSI (second argument)
pop rdx; ret          ← Set RDX (third argument)
mov [rdi], rax; ret   ← Write memory
xchg eax, esp; ret    ← Stack pivot

Building a ROP Chain

Goal: Call execve("/bin/sh", NULL, NULL)

Stack Layout (attacker-controlled):
┌───────────────────────┐
│ pop rdi; ret         │  RSP → First gadget
├───────────────────────┤
│ addr of "/bin/sh"    │  ← Popped into RDI
├───────────────────────┤
│ pop rsi; ret         │  Next gadget
├───────────────────────┤
│ 0 (NULL)             │  ← Popped into RSI (argv)
├───────────────────────┤
│ pop rdx; ret         │  Next gadget
├───────────────────────┤
│ 0 (NULL)             │  ← Popped into RDX (envp)
├───────────────────────┤
│ syscall; ret         │  Execute syscall 59 (execve)
└───────────────────────┘

Execution Flow:
1. ret pops "pop rdi; ret" address into RIP
2. pop rdi loads "/bin/sh" address into RDI
3. ret pops next gadget address...
4. Eventually syscall executes execve("/bin/sh", NULL, NULL)
5. Attacker gets shell!

Finding Gadgets

# Use ROPgadget tool
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep "pop rdi"
0x000000000002155f : pop rdi ; ret
0x00000000000215a0 : pop rdi ; pop rbp ; ret

# Or use ropper
$ ropper -f /lib/x86_64-linux-gnu/libc.so.6 --search "pop rdi"
[INFO] Searching for gadgets: pop rdi
0x000000000002155f: pop rdi; ret;
Defense: Control Flow Integrity (CFI) and Shadow Stacks detect and prevent ROP by validating return addresses match legitimate call sites.

Secure Assembly Practices

Writing secure assembly requires constant vigilance. Here are essential practices to protect your code:

1. Always Bounds Check

; SAFE: Bounds-checked copy
safe_copy:
    ; RDI = dest, RSI = src, RDX = max_len, RCX = dest_size
    cmp rdx, rcx
    jbe .size_ok
    mov rdx, rcx            ; Clamp to buffer size
.size_ok:
    ; Now safe to copy RDX bytes
    rep movsb
    ret

2. Zero Sensitive Data

; Clear password buffer before freeing
zero_sensitive:
    mov rdi, password_buffer
    mov rcx, PASSWORD_LEN
    xor eax, eax
    rep stosb               ; Fill with zeros
    
    ; Use memory barrier to prevent optimization away
    mfence
    ret

3. Constant-Time Comparisons

; Timing-safe comparison (for passwords, tokens)
; Prevents timing attacks that measure comparison duration
constant_time_compare:
    ; RDI = buf1, RSI = buf2, RDX = length
    ; Returns: RAX = 0 if equal, non-zero if different
    xor eax, eax            ; Result accumulator
    test rdx, rdx
    jz .done
.loop:
    mov cl, [rdi]
    xor cl, [rsi]           ; XOR bytes (0 if equal)
    or al, cl               ; Accumulate differences
    inc rdi
    inc rsi
    dec rdx
    jnz .loop
.done:
    ret                     ; AL=0 only if all bytes matched

4. Stack Canary Manual Implementation

; Manual canary for functions with buffers
section .data
    canary dq 0xDEADBEEFCAFEBABE  ; Better: random at startup

section .text
protected_func:
    push rbp
    mov rbp, rsp
    sub rsp, 80
    
    ; Place canary
    mov rax, [rel canary]
    mov [rbp-8], rax
    
    ; ... function body with buffer at [rbp-72] ...
    
    ; Verify canary
    mov rax, [rbp-8]
    cmp rax, [rel canary]
    jne .abort              ; Corruption detected!
    
    leave
    ret
    
.abort:
    mov rdi, 1
    mov rax, 60             ; exit(1)
    syscall

Security Checklist

PracticePreventsImplementation
Bounds checkingBuffer overflowCompare index/length before access
Zero after useMemory disclosurerep stosb + memory barrier
Constant-time opsTiming attacksXOR + OR accumulator pattern
Validate pointersNULL deref, UAFCheck against NULL, valid ranges
Minimize privilegesEscalationDrop caps after initialization
Stack canariesStack smashingCompile with -fstack-protector-strong
Exercise

Security Audit Challenge

Find the vulnerabilities in this code:

get_user_input:
    sub rsp, 64
    mov rdi, rsp
    call gets           ; Vulnerability 1: ?
    mov rax, rsp
    add rsp, 64
    ret                 ; Vulnerability 2: ?

Answers: 1) Unbounded input via gets(), 2) Returns pointer to stack memory (use-after-free when caller accesses it)