Skip to content

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

  1. Anchor — a string, an import, a packet format, a file magic. Something legible.
  2. Hypothesize — "this function parses the config file." Name it accordingly.
  3. 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.
  4. Verify — run it, watchpoint it, feed it. The binary is the judge.
  5. 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.

The hypothesis loop

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 output

The 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.