Reverse Engineering

0x4142 0x4142 4 min read #reverse-engineering#gdb#x86-64

A series that starts from C you already know and ends with you reading, debugging and cracking real x86-64 binaries.

You’ve written C. You’ve compiled it, run it, maybe chased a segfault or two. But the file gcc hands back is a black box: you trust that it does what your source says, and you’ve never had a reason to look inside. This series opens the box. It starts with a five-line program you could write in your sleep and ends with you pulling a password out of a binary you’ve never seen the source for.

Who this is for

You can write and compile C. You know what a pointer is, what a function call is, and roughly what the stack is for. You have never read assembly and you’ve never used gdb for anything beyond bt after a crash. That’s the whole prerequisite list. Every instruction, register and tool gets introduced the first time it shows up, and not before.

What you’ll be able to do

The series is split into three arcs. Each one ends with a skill you can use on its own.

Arc 1: Reading the machine

Plain gdb and objdump, nothing fancy. You’ll learn to read the assembly your compiler produces for code you wrote yourself, so you always have the source to check your reading against. By the end of the arc you can look at a function’s disassembly and say what it does, including its ifs and loops.

  1. What your compiler actually made
  2. Registers and your first instructions
  3. Memory and the stack
  4. Functions and calling conventions
  5. Control flow: finding your if in assembly

Arc 2: Better tools, bigger programs

Once you can do it by hand, you get to stop doing it by hand. pwndbg and Ghidra take over the bookkeeping, and the programs get bigger: structs, strings, calls into libc, and finally binaries with no source and no symbols.

  1. Upgrading to pwndbg
  2. Data in memory
  3. Talking to libc
  4. Ghidra: letting the decompiler help
  5. Your first crackme
  6. Real-world binaries: stripped and optimized

Arc 3: Bridge to exploitation

Everything from the first two arcs, pointed at a bug. This arc reworks Smashing the Stack on top of the lab, with steppers showing the overflow one write at a time.

  1. Smashing the stack

Setting up the lab

Every binary and every terminal session in this series comes from one Docker image. Pull it and drop into a shell:

Terminal window
docker run --rm -it ghcr.io/oceanman42/reverse-engineering-lab

Or build it yourself from the lab repo:

Terminal window
git clone https://github.com/OceanMan42/reverse-engineering-lab
cd reverse-engineering-lab
make shell

Inside the container you land in /lab. Each part has its own folder: part-01/ holds the source and the compiled binary for part 1, and so on. The binaries are already built, so you can start poking at them right away.

The image pins its base system and toolchain (gcc, binutils, gdb) to exact versions. That matters more than it sounds: two gcc releases can emit different instructions for the same C, and addresses shift with them. With the pinned image, the output you get is the output in the posts, byte for byte.

How to read this series

Terminal blocks in these posts aren’t typed up by hand. They’re recorded from the lab image by a script, so they’re exactly what the tools print. A line starting with $ is the command, which is what you type; everything after it is output.

Some blocks have more than one frame. Those are steppers: the same terminal at several moments in time, one frame at a time. Here’s one that follows hello.c from source to finished binary:

Step 1 of 3: the source

From C to a binary
$ cat hello.c
#include <stdio.h>
int main(void) {
printf("Hello, world!\n");
return 0;
}

Step 2 of 3: the compiler's assembly output

From C to a binary
$ gcc -O0 -fno-pie -masm=intel -fno-asynchronous-unwind-tables -S -o - hello.c
.file "hello.c"
.intel_syntax noprefix
.text
.section .rodata
.LC0:
.string "Hello, world!"
.text
.globl main
.type main, @function
main:
endbr64
push rbp
mov rbp, rsp
mov edi, OFFSET FLAT:.LC0
call puts
mov eax, 0
pop rbp
ret
.size main, .-main
.ident "GCC: (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0"
.section .note.GNU-stack,"",@progbits
.section .note.gnu.property,"a"
.align 8
.long 1f - 0f
.long 4f - 1f
.long 5
0:
.string "GNU"
1:
.align 8
.long 0xc0000002
.long 3f - 2f
2:
.long 0x3
3:
.align 8
4:

Step 3 of 3: the finished binary

From C to a binary
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=c2389cb890c4ae64ef9b748ff4d79b84c6798306, for GNU/Linux 3.2.0, with debug_info, not stripped

Use Next and Prev to move between frames, or click the stepper and use the left and right arrow keys. When a code block inside the stepper has focus, the arrow keys scroll that block instead, so wide output stays readable. In later parts, steppers highlight the lines that changed since the previous frame, which is how you’ll watch a register or a stack slot change under a single instruction.

One convention to know up front: all assembly in this series uses Intel syntax (mov rbp, rsp, destination first), not the AT&T syntax that some tools default to (mov %rsp,%rbp). The lab’s objdump and gdb commands are set up for Intel everywhere.

Next

Part 1, What Your Compiler Actually Made, takes the smallest C program there is and pulls apart the file gcc turns it into.

Parts

  1. Part 1 What Your Compiler Actually Made

    Pulling apart the ELF file gcc builds from hello world: its header, sections, symbols, and a first look at main in assembly. 6 min read