Lesson 9 of 9 · 26m
From Instructions to Intent: Building the Model
The skill isn't reading assembly faster — it's building a theory of the program, knowing when it's wrong, and proving it by reimplementation.
What separates working reversers from tutorial readers isn't reading assembly faster — it's building a theory of the program and knowing when the theory is wrong. Nobody holds a megabyte of machine code in their head. You hold a model; the binary is evidence for or against it.
The loop
- Anchor — a string, an import, a packet format, a file magic. Something legible.
- Hypothesize — "this function parses the config file." Name it accordingly.
- Predict — if the model is right, something observable follows: a breakpoint here fires when I feed it this input; that buffer holds the magic bytes after the call.
- Verify — run it, watchpoint it, feed it. The binary is the judge.
- Revise — when the prediction fails, the model is wrong, not the program. Rename, retype, update the notes.
The renaming itself is the notebook: parse_config, check_signature, dispatch_cmd — when you can no longer defend a name, you've learned something concrete.
Reconstruct the data first
Code is ambiguous; data is honest. When [rcx+0x20] is written in three functions and read in seven, +0x20 is a field that matters. Build structures before functions — offsets, sizes, pointer-versus-inline — then let Ghidra's struct application ripple meaning through every function that touches the type. One recovered key structure explains more code than an hour of instruction-level reading.
Prove it by reimplementing
The acid test of "I understand this function" is writing it in a language you control and matching outputs:
// The disassembly showed: mul by 0x01000193, then ror eax,7, then xor 0xA5A5A5A5.
// 0x01000193 is the FNV prime — the tail is the program's own twist.
function mystery(bytes) {
let h = 0x811c9dc5; // FNV offset basis
for (const b of bytes) h = Math.imul(h ^ b, 0x01000193) >>> 0;
h = ((h >>> 7) | (h << 25)) >>> 0; // ror eax, 7
return (h ^ 0xA5A5A5A5) >>> 0;
}
const input = [...new TextEncoder().encode("hello")];
console.log(mystery(input).toString(16)); // compare against the binary's outputThe moment you spotted 0x01000193 you knew the skeleton was FNV-1a — constants again. Your reimplementation becomes a test oracle: feed both sides the same inputs, and any divergence is a flaw in your model, found cheaply without another hour in the listing.
Know when you're done
You are never "done" understanding a binary — there is no bottom. You're done when you can answer the question you came with: reproduce the file format, bypass the check, locate the vulnerability, write the interoperable client. State the question before you start, or the work expands to fill your lifetime.
Field notes
- Write hypotheses down, including the dead ones. "Thought RC4, was actually XOR" is progress — on paper, where the next session can find it.
- When evidence and model collide, trust the debugger first, the listing second, the decompiler third, and your assumptions last.
- Time-box unknown functions: if twenty minutes doesn't crack it, rename it
TODO_and move on — context from elsewhere usually resolves it later. - Hand off like a professional: names, comments, struct definitions, and a notes file. The next reverser — often future you — inherits either a map or a maze.