Defuse the Bomb
Here is a program. It asks you a question, and if you get it wrong it stops and tells you the bomb went off. There are six stages. You do not get the source code, and nobody is going to tell you the answers. What you get is the instructions the machine will run and a debugger, which turns out to be enough, because the program has to contain the answer in order to check yours.
Every stage is a real program on the machine from the assembly course. When you type an answer it is really loaded into memory and the stage really runs, and it either reaches the instruction that means defused or the one that means it went off. Nothing is pretending. There is a debugger on every stage, and reading the instructions is the intended way through, not a shortcut.
You can still start here. It goes quicker if you can already read cmp, jmp
and the conditional jumps, follow a loop, and use a debugger to stop somewhere and print a register.
Where a stage here uses something that course did not cover, there is a note beside it that teaches it
from the beginning rather than just reminding you.
This course carries on from Assembly & Debugging, and it
leans on five things taught there. What a register is. What the sixteen of them are called
(its Step 2). What assembly is, and why a
processor runs instructions rather than lines (Step 1).
Labels and the jumps that aim at them
(Step 4). The stack, which push
and pop work on (Step 7). And a
debugger you can stop, step and print with
(Step 10).
If you arrived here from a search and have none of that, carry on anyway. Every one of those five words is explained again in Step 1 below, in two or three sentences, at the point where this course first needs it. That is enough to get you through the six stages. The longer version is the other course, and it is worth the two hours.
The steps
What you have to work with
The situation is the one a security researcher is in every day, and it is less hopeless than it sounds. You have a program and no source. You want to know what input it accepts. What you can read instead of source is the program's instructions, written one per line. Each one is a single small step the processor carries out. They happen in the order they are written unless an instruction says otherwise. Instructions written out as text like that are called assembly and one line of assembly is exactly one instruction, no more and no less. A debugger is a second program that runs this one for you. It can pause the program part way through, so you can look at what it is holding at that moment. That is why the debugger here can stop at a line number and mean one step of the machine. What makes it possible at all is that the program has to be able to tell a right answer from a wrong one, so the right answer, or a way of producing it, has to be in there somewhere.
Your job is to find it before you type anything.
Where does the number I type go?
Whatever you type into the answer box is written straight into the machine's memory at the label
answer, before the first instruction runs. Nothing else is done to it: it is not checked,
cleaned up or converted. mov rax, [answer] then reads it back out, exactly like any other
value sitting in memory.
That is what makes the whole thing readable. Your number is not tucked away somewhere private; it
sits in a known place with a name on it, and every stage has to fetch it from there before it can test
it. If you want to see that for yourself, open the debugger and type x/1 answer, which
prints what is in that slot right now. x is the debugger's examine command. It reads
memory rather than registers. The number after the slash is how many values you want and the name after
it is where to start. So x/1 answer asks for one value at answer, and
x/16 table asks for sixteen in a row starting at table.
Why this is a real skill and not a puzzle game
Working out what a program does without its source is called reverse engineering, and people do it for a living: to find out what malware is doing, to check whether a device is spying on its owner, to make an old file format readable again, to find security holes before someone else does.
The exercise here is a version of a famous university assignment that has been set for over twenty years, because it teaches the one habit that matters: read what the machine will actually do, rather than what you assume it does.
One convention runs through every stage. One word first, because it is about to turn up on almost every
line. A register is one of sixteen named slots inside the processor, and each one holds a single
whole number. The ones you will see here are rax, rbx, rcx,
rdx, rsi, rdi, and r8 up to r15.
Instructions do their arithmetic in registers rather than in memory. So most of what you read is a number
being moved into a register, changed there, or compared with another. The verdict lives in the register
r15. The first
instruction of each stage sets it to 0, and the only route to 1 is the instruction sitting under the label
defused. A label among the instructions is a name with a colon after it, on a line of
its own: main:, defused: or boom:. It is not an instruction. Nothing happens when the machine reaches one. Its only job is to give a jump or a call
somewhere to aim at. When a stage tells you it went off, that means execution reached boom
instead and r15 never changed. So a stage is a maze with two exits, and your answer chooses which one it
takes.
What are the lines with dots in front of them?
The lines starting with a dot are not instructions. They are directions to the assembler, the
program that turns this text into the numbers a machine can run. .data means "what follows
is values, not instructions", and .text means "back to instructions again". The machine
never executes either line; by the time it starts running, both have already done their work and
disappeared.
answer: .quad 0 sets aside one slot of memory, puts the name answer on it,
and starts it holding zero. Quad is short for quad word, which is four 16-bit words, which is 64 bits,
which is eight bytes, which is exactly the size of a register like rax. table: .quad 2, 10, 6
would set aside three such slots in a row. Anywhere you see a name with a colon after it in the
.data part, that is a place in memory with a label on it, and the instructions below reach
it by that name.
Is there a list of what each instruction does?
Every bomb in this course is built from a small set, so here they all are in one place to come back
to. mov a, b puts b into a. add and sub add to and take away from
the first thing named. inc adds one and dec takes one away. and
is a bit operation, explained in Step 6 where it is used. push puts a value on the
stack, an area of memory kept for values a program wants back shortly, and pop takes
the top one back off. Values come back in the opposite order to the one they went on in.
call runs the code at a label and comes
back afterwards, and ret is the coming back.
The jumps all read the notes a cmp left behind about how its two values differed.
jmp goes without asking. jne jumps when the two were not equal.
jl, jle, jg and jge jump when the first was less
than, less than or equal to, greater than, or greater than or equal to the second. jnz
jumps when the last result was not zero, which after a comparison is the same test as
jne. Every one of them either jumps or does nothing at all, and which of those two happens
is the whole of what a stage decides.
boom. That is what a
wrong answer looks like, and now you have seen it before it costs you anything.How do I drive this debugger?
The numbers down the left of the code are line numbers, and that is what break takes.
break 9 stops the machine just before line 9 runs, without running it. Then run
starts the stage and stops at your breakpoint, print rax shows what is in a register at
that exact moment, step runs one single instruction, and continue carries on
to the next breakpoint or to the end. One more is worth having. x examines memory rather
than registers, so x/4 answer prints four values in a row starting at answer.
That is how you read a table the code is using.
Type a command into the box and press Enter, or press one of the buttons under it, which type the
command for you. help lists everything it takes and reset puts the stage back
to the start. While it is stopped somewhere, the line it is stopped on and any breakpoints you set are
marked in the code above, so you can always see where the machine is.
That warm-up had one right answer out of every number there is, and you found it by reading rather than by trying. Guessing was never going to work: you could sit there for the rest of your life. Every stage from here is beyond guessing on purpose, so that the only way through is the way that works.
Stage one: read the comparison
The first real stage. Your answer is loaded into memory before the stage runs, and the stage decides
whether to reach defused or boom.
Why is the number written inside the instruction?
A comparison can take its second value from a register, from memory, or from the instruction itself.
When the number is written into the instruction, as in cmp rax, 1729, it is called an
immediate: it travels with the instruction and is not stored anywhere else. That is why nothing needs
to fetch it first, and why it is sitting there in plain text for you to read.
It also changes what the debugger can do for you here. On this stage there is no register holding the wanted number, so printing registers only ever shows you your own guess coming back. Later stages work the number out into a register before comparing, and those are the ones where a breakpoint hands you the answer.
cmp. There is exactly one, and the number on it is the only number in this
stage that matters. Type that number in and press Defuse.cmp and
one conditional jump. Everything else is noise put there to make you look. If you would rather watch it
happen, break on the comparison and step: the debugger prints the instruction it stopped
on, and the number it wants is written into that instruction itself.cmp rax, 4095 followed by jne boom. What must
rax hold to survive?jne jumps when the two were not equal, and the jump
goes to boom. So surviving means not taking the jump, which means being equal. Reading a
jump-to-failure backwards like this is most of the work in every stage that follows.Stage two: run the loop in your head
This time the number you have to match is not written anywhere. The stage works it out with a loop, and then compares. So you have to work out the same thing the loop does, either by reading it or by letting the machine do it and looking.
I have not seen dec or jnz before
dec rcx is decrement. It takes one away from rcx, exactly as sub rcx, 1
would, and like every other piece of arithmetic it writes down facts about the answer in the flags. The
flag that matters here is the zero flag, which is set when the answer came out exactly zero and clear
otherwise.
jnz is jump if not zero, and it jumps when that flag is clear. So dec rcx
followed by jnz top reads as: take one off the counter, and go round again unless the
counter has just hit zero. That pair is the standard countdown loop. You will meet it constantly,
because it does the counting and the testing in two instructions instead of three. It is the same jump
as jne under a second name: both mean "the zero flag is clear" and an assembler accepts
either spelling.
Letting the program calculate the answer for you
You do not have to be cleverer than the loop. The value it builds sits in a register just before the
comparison, so a breakpoint on the comparison and one print hands you the answer without
you working anything out.
This is not cheating, it is the technique. A real program's check might involve thousands of instructions, and nobody traces those by hand. You find the moment of decision, look at what is in front of it, and read the value off.
Stage three: two conditions at once
Now two numbers, and two separate tests. Passing one is not enough, and neither test alone tells you either answer. You need both conditions and then a little arithmetic of your own.
Reading a chain of "fail if" jumps
Real checks are rarely one comparison. They are a run of them, each jumping to failure, with the success case simply being what happens if you fall off the end without jumping.
So read them as a list of conditions that all have to hold at once. Write each one down as you find it, in ordinary arithmetic, and only then start solving. For this stage that comes out as: the two numbers add up to 20, and the first minus the second is 6. Two conditions on two numbers can always be cracked by trying, and here there are only ten pairs that add to 20, so start at 19 and 1 and walk down until the difference is right. There is a faster way that does not need any trying and the question below shows it, but the trying works and it never lets you down on a stage this size. Extracting the two conditions from the instructions is the part that is actually hard. You have already done it.
19 1 and press
Defuse. It goes off, and the line underneath tells you which of the two tests failed and by how
much. That message is the thing to work with: walk the pair down, 18 and 2, then 17 and 3, and watch
the numbers it reports move.a + b == 24 and then a - b == 4. What are a
and b?Stage four: it calls itself
This stage calls a function, and that function calls itself. Tracing recursion by hand is unpleasant and error-prone, which is exactly why this stage is here: it is the first one where the debugger is clearly faster than being clever.
Recognising a recursive function in instructions
Look for a call whose target is the label the function itself starts at. Above it there
will be a comparison and a jump: that is the case where it stops rather than calling again, and without
one the program would never finish.
When a function calls itself twice with two different arguments and adds the two answers together, it is almost always building the Fibonacci numbers: 0, 1, 1, 2, 3, 5, 8, 13, 21, and so on, where each number is the two before it added up. That is exactly what the instructions say here, once you read them: the value it stops at is the argument itself, and everything above is two smaller calls with their results added. Spotting the shape is quicker than tracing it and a breakpoint will confirm your guess in one step, so guess first and check second.
call the machine executes is recorded along with the number it
was handed. At 4 you can follow the whole thing. Somewhere around 7 it stops fitting on a screen. The
stage below starts at 12, and now you know what that costs, which is the point at which reaching for
the debugger is a decision rather than an instruction.break 10, run and print rbx buttons in that order. The last
one prints the number the stage is about to compare your answer with. Type that in and press
Defuse.What is the stack, and what are push and pop doing here?
The stack is an area of memory set aside for values a program wants back shortly, with one
register, rsp, holding the address of its top. push rdi writes a copy of rdi
there and moves that top mark on by one slot of eight bytes. pop rdi takes the top value off
again and puts it back into rdi. Values come off in the opposite order to the one they went on in, which is the whole
idea of it.
This stage needs it because there is only one rdi, and the function is about to call
itself with a smaller number in it. So the outer call pushes its own copy of rdi, makes the inner call,
and pops the copy back afterwards to carry on where it was. call uses the stack for the same
reason: it pushes the address of the instruction to return to, and ret pops that address and
goes there.
Stage five: through a table
Your answer is not compared with anything directly. It is used to look things up in a table, and the results of those lookups are what gets checked. So you have to work backwards through the table: not "what does this input give" but "what input would give the thing it wants".
What does and rax, 15 do?
and is a bit operation. Every number in the machine is really a row of 64 bits, each
one a 0 or a 1, and and lines two rows up and compares them place by place: the answer has
a 1 only where both rows had a 1, and a 0 everywhere else. The number 15 written out as bits is a row
of zeros with 1111 on the end, four ones in the bottom four places. Anding your number with 15
therefore keeps its bottom four bits exactly as they were and wipes everything above them to zero.
Keeping the bottom four bits is the same thing as taking the remainder after dividing by 16, so whatever you type, the index that actually reaches the table is a number from 0 to 15. Type 100 and it looks up index 4, because 100 divided by 16 leaves 4. That instruction is not decoration: it is the program making certain that a number you chose can never send it reading memory past the end of a sixteen-entry table. Programs mask like this constantly, and now you can read it when they do.
Why does table have no brackets when answer has them?
A label in the .data section is a name for a place in memory. Written on its own, as in
mov rsi, table, it means the address of that place, so rsi ends up holding where the table
begins and nothing that is inside it. Written inside square brackets, as in
mov rax, [answer], it means the contents of that place, so rax ends up holding the value
that is stored there. Same name, two entirely different things, and the brackets are the only
signal.
This stage needs both. It wants the table's starting address in a register so it can do the index
arithmetic [rsi+rax*8], which reads as "begin at rsi, go rax items along, each item eight
bytes wide". And it wants the contents of your answer slots, so those are fetched with brackets. Mixing
the two up is the most common mistake there is in reading assembly. It is also, one layer up, exactly
what a pointer is: a number that says where something lives rather than what it is.
Working a lookup backwards
Forwards is easy: take the input, use it as an index, read the table, add it up. Backwards means finding which indexes produce the total that is wanted, and there may be several answers or none.
The way through is to write the table out as a plain list of index to value, then look for the combination you need. Sixteen entries is small enough to inspect by eye, which is why the stage uses sixteen. A real one might use two hundred and fifty-six, and the method would be identical.
x/16 in the debugger. Then find three indexes whose values
add up to the target. There is more than one answer, and the stage accepts any of them, because it
checks the total rather than your route to it.Stage six: put them in order
The last stage. Four numbers, and the stage checks a relationship between them rather than their values. Reading it tells you the rule; working out which four numbers satisfy the rule is then up to you. It needs the data as well as the code.
What is jle, and what does strictly increasing mean?
jle is jump if less than or equal. This stage does cmp rdx, rbx and then
jle boom, where rdx is the weight it has just looked up and rbx is the weight from the
number before it. So it goes off if the new weight is smaller than the previous one, and it also goes
off if the two are exactly the same. Surviving means every weight is greater than the one before it,
with no ties allowed.
That is what strictly increasing means: each value larger than the last, and equal does not count.
Had the author written jl instead of jle, equal values would have been fine
and the rule would have been the looser "never goes down". One letter, two different puzzles, and no
comment in the code to tell you which you are looking at. This is why the instruction has to be read
rather than skimmed, and it is the kind of one-letter difference that real security holes are made
of.
When the code alone is not enough
Everything so far has been answerable from the instructions. This one is not: the rule is in the instructions, and what satisfies the rule depends on a table of values sitting in the program's data. Change the data and the answer changes while the code stays identical.
This is worth meeting, because it is the normal case. Programs are code plus data, and a great deal
of real reverse engineering is reading data structures rather than instructions. The debugger's
x command is how you look at them.
0 1 2 3 and press Defuse. It goes off, and the line underneath names the four weights
those indexes looked up and says which position it fell over at. Reorder the four indexes from there.mov rbx, -1 runs once,
before the loop. rbx holds the previous weight, and on the first time round there is no previous
weight, so it is set to a value no weight could possibly fail to beat. Every entry in the table is
positive, so anything at all is greater than minus one and your first number is never rejected for this
reason. A starting value picked so the first comparison cannot fail is a trick you will see in a great
deal of real code.x is how you
read it.What you just learned to do
Six stages, no source code. What you actually practised was one habit repeated: find the moment the program decides, then work backwards from what that decision depends on.
Why real programs are harder, and in what ways
Bigger, mostly. A real program is hundreds of thousands of instructions and the interesting check is four of them. Finding those four is the work, and it is done with searches, breakpoints on library functions, and watching which code runs when you change your input.
Some programs also fight back: they detect a debugger, scramble their own code until it runs, or check a secret on a server where you cannot reach it. That last one is the only defence in the list that actually works, and the reason is Step 3: if the check happens on your machine, the thing being checked against is on your machine.
boom in this stage, so there are two conditions, and both have to
hold. Write both down in ordinary arithmetic before you type anything into the box.What you can do now
- Find where a program decides, and work backwards from it.
- Read a value out of a register at the moment it matters, rather than deriving it.
- Turn a chain of jump-to-failure tests into a list of conditions.
- Recognise recursion, and know when to stop tracing and use a tool instead.
- Work a table lookup backwards.
- Read a program's data as well as its code.
Where this goes
- How Memory Breaks. The other side: not reading a program but making it do something it was never meant to, starting with writing past the end of an array, and every defence invented since.
- Operating System Engineering. What a program is allowed to do, who decides, and what happens when it asks the operating system for something.
Map the file before reading code
A real executable is a container, not one uninterrupted list of instructions. ELF is common on Linux, PE on Windows, and Mach-O on macOS. Each begins with headers that identify the architecture, entry point, regions to map, permissions and tables the loader or linker needs. The names differ, but the questions are the same: what can execute, what can change, what is imported, and where does control enter?
Sections organise the file for linking and analysis. Segments describe what the loader maps into memory.
They are related but not identical. .text usually contains code, .rodata
read-only constants, .data initialised writable values, and .bss zero-filled
writable space that occupies memory without storing all those zero bytes in the file.
File offsets are not virtual addresses
A file offset counts bytes from the start of the file. A virtual address names where the loader intends a byte to appear in a process. Alignment, unmapped gaps and zero-filled regions mean the two numbers need not match. Tools normally show both. Use the address column when following a running process and the offset column when inspecting or patching bytes on disk.
PIE and ASLR add a load base at runtime, so an instruction's absolute address can change between runs. Its offset within the module stays useful. The linking and loader stages are introduced in Assembly & Debugging, Step 14.
.bss. Compare its
file size with its memory size, then switch formats and find the executable region..bss region occupy more memory than file bytes?Recover functions and references
Symbols and debug information make function boundaries easy. Stripped binaries may remove both. An analyser then follows direct call targets, known entry points and relocation targets, decoding reachable instructions as it goes. Cross-references connect a call or data use back to every place that mentions it. Strings and imported functions often provide the fastest route from a visible behaviour to code.
Function prologues are clues, not laws. Optimised functions may omit push rbp, tail-call
another function instead of returning, share an exit block, or be inlined until no separate function
remains. Linear sweep, which decodes every byte in order, can mistake embedded data for code. Recursive
traversal can miss an indirect call or jump-table target. Good tools combine methods and record
uncertainty.
Imports, exports, relocations and cross-references
An import says this file expects another module to provide a symbol. An export offers one. Relocation
records identify instruction or data fields that need an address fixed later. Even when function
names are stripped, an imported strcmp, malloc or connect
still tells you what kind of work happens nearby.
Indirect calls need more care. The target may come from a virtual-method table, callback field, dynamic import table or computed jump table. List the values that can reach the target register rather than assigning one guessed destination.
push rbp; mov rbp, rsp not enough to find functions?Draw the control-flow graph
A basic block is a run of instructions with one entry and no branch in the middle. Its final instruction determines the outgoing edges: fall through, jump, return, or several possible indirect targets. The control-flow graph, or CFG, keeps those blocks and edges while hiding instruction details you do not need yet.
Loops appear as edges that lead back to an earlier controlling block. A block dominates another when every path to the second must pass through the first. Dominators help identify loop headers and checks that cannot be bypassed. They also reveal dead blocks that no entry path can reach.
Indirect edges make a graph provisional
A computed jump or return does not spell its target in the instruction. Static analysis estimates possible targets from tables and data flow. Dynamic tracing shows the edge taken in one run, not every edge that could be taken. Mark an unresolved indirect edge explicitly instead of silently deleting it.
Exceptions, signals and long jumps add non-local edges that ordinary source-shaped graphs can omit. The smallest graph that answers your question is usually better than a large graph pretending to be complete.
Trace only the values that matter
The first eight steps taught this by hand: start at the deciding comparison and work backward. A backward slice gives the method a name. It contains the instructions that can affect a chosen value at a chosen point. Definitions write a register or memory location; uses read it. Following those chains removes unrelated logging, setup and error handling from the immediate problem.
Registers are easy because their names are explicit. Memory is harder: two different address expressions may refer to the same byte. This is alias analysis. A conservative analyser keeps a write when it cannot prove the addresses differ. Compilers often convert register-like values to SSA form, where each version is assigned once, because the definition-use chain then becomes direct.
A slice answers one question, not the whole program
A slice for “why did this comparison fail?” can omit code responsible for allocation, timing or thread ordering even when that code explains why the compared value became corrupt. Start narrow, then expand through memory writers, callers and concurrent events when the first slice reaches an unknown input.
SSA and compiler data-flow analysis continue in Language Engineering. Invalid memory and aliasing failures continue in Memory Exploits.
Choose the tool from the question
Static analysis can cover code you have not run. Dynamic analysis gives exact state for a particular run. Symbolic execution treats an input as an unknown expression and solves path conditions, but branches multiply until the search becomes too large. Fuzzing runs many concrete inputs and is good at finding crashes or violated invariants. Emulation gives control over a foreign or isolated machine model.
No tool wins every case. If you need the value at one instruction, use a debugger or trace. If you need all callers, use cross-references. If a compact parser accepts hostile input, fuzz it with sanitizers. If one shallow branch condition hides a stage answer, symbolic execution may solve it directly. State the question first, then select the cheapest evidence that can answer it.
Scope and permission come before technique
Analyse software you own, are authorised to test, or are studying in a legal challenge environment. Reverse engineering rules vary by licence and jurisdiction, and bypassing access controls can carry additional restrictions. Keep written scope, protect captured data and report defects through the agreed channel.
This course uses deliberately small local programs. The method transfers; permission does not.
Check recovered source against the machine
A decompiler turns control flow and data flow into source-like text. It can recover a useful loop or condition, but stripped code has usually lost original variable names, comments, typedefs and many type distinctions. Several different source programs can compile to the same instructions. The result is a readable model, not the file the programmer wrote.
Current assistants can explain disassembly, rename variables, propose structures and draft scripts. That is useful for navigation. It also makes a confident wrong story cheap to produce. Check every important claim against instruction bytes, cross-references, live register and memory values, multiple inputs, and a regression test. Keep uncertain names marked as guesses until evidence earns them.
Patching proves less than a regression test
Changing a branch byte can demonstrate that you found a decision. It does not prove you understand all callers, error paths or integrity checks. Signed binaries may refuse modified files, and an update will replace the patch. In maintained software, fix the source, rebuild with matching symbols and test both the original failure and nearby behaviour.
Use Assembly & Debugging, Step 18 for the full reproduce-reduce-observe-test loop, and Systems That Fail Well for production evidence and recovery.
The complete workflow
- Map the file, architecture, entry points, imports, permissions and load addresses.
- Find code connected to the behaviour through calls, strings, data references or a dynamic trace.
- Reduce it to a control-flow graph and a slice for the exact decision you care about.
- Choose static, dynamic, symbolic, fuzzing or emulation tools from the unanswered question.
- Treat recovered source and assistant explanations as hypotheses; test them at machine boundaries.
- Document the evidence, fix in source when possible, and add a regression test.