Interactive course · about 6 hours

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.

How this works

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.

If you have not done the assembly course

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.

What this course expects you to know already

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

Step 1

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.

Lab 1 · A warm-up bomb
Try this firstType 0 into the answer box and press Defuse. It goes off, and the panel below says execution reached boom. That is what a wrong answer looks like, and now you have seen it before it costs you anything.
Read the instructions before you guess. This one is deliberately easy, and even so the answer is not written down as a number anywhere. Two instructions build it, one piece at a time, just before the comparison. Find those two, do the sum in your head, and type the result.
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.

Guessing does not scale

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.

Lab 2 · The three questions
Try this firstRead the round-one question at the top, then click the line of code you think answers it. Every line is a button. A wrong click tells you what that line actually does and why it is not the answer, so clicking to find out is allowed.
A method, not a trick. Every stage yields to the same three questions asked in the same order, and this deals you a stage you have not seen so you have to ask them rather than recognise the answer. Clear all three on two different stages. Then when a hard one arrives you have somewhere to start rather than a wall.
You are looking at a stage and cannot see how the answer is checked. What is the most useful next move?
Work backwards from the jump. There is exactly one place the program decides you were right, and everything that matters feeds into it. Reading forwards from the top means understanding instructions that may not matter at all. Find the decision, see what it compares, then find where that value came from. Three questions, usually.
Step 2

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.

Lab 3 · Stage one
Try this firstLook down the code for the one line starting 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.
Find the comparison, and read the number. There is one 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.
A stage contains cmp rax, 4095 followed by jne boom. What must rax hold to survive?
Exactly 4095. 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.
Step 3

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.

Lab 4 · One pass at a time
Try this firstPass 1 is already filled in. Work out what rbx and rdx will be after pass 2, type the two numbers, and press Check. The row fills in with what the machine really did, whether you were right or not, and the line underneath says what changed and why.
Two registers, six passes, one row at a time. rbx is the value being carried round; rdx is the running total of every rbx so far. Predicting the row before you see it is the part that teaches you the loop, so guess even when you are not sure. When the table is full, the last rdx is the number the stage below wants, and you will have worked it out rather than been given it.
Lab 5 · Stage two
Try this firstType the last rdx from the table above and press Defuse. If you skipped the table, press Open the debugger instead, then the break 15, run and print rdx buttons in that order, and the number it prints is the one to type.
Two ways in, both good. Either follow the loop and work out what it builds, or put a breakpoint on the line with the comparison, run it, and print the register it is about to test. Try the second even if you managed the first: it is the method that keeps working when the loop is a thousand instructions long.
Why can you read the expected answer out of a register at the moment of the comparison, even though it is nowhere in the code?
It has to have the value to compare against it. That is the weakness of every check of this kind, and no amount of hiding the number in the source removes it. At the instant of comparison, the thing you need is sitting in a register. Software that has to check a secret on the user's own machine always has this problem.
Step 4

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.

Lab 6 · Stage three
Try this firstType 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.
Write both conditions down before solving either. Two answers, separated by a space. The stage will tell you which of the two tests you failed, which is more help than a real bomb would give and still leaves the solving to you.
A stage checks a + b == 24 and then a - b == 4. What are a and b?
14 and 10. One way is to try. Pairs that add to 24 are 23 and 1, whose difference is 22; then 22 and 2, difference 20; then 21 and 3, difference 18. The difference drops by two each time, so keep walking down and 14 and 10 gives 4. The faster way is to add the two conditions together. The first says the two numbers make 24 when you add them. The second says they make 4 when you subtract the smaller from the larger. Add those two statements and the second number cancels out, because it is added once and taken away once, leaving two lots of the first number equal to 28. So the first number is 14, and the second is 24 take away 14, which is 10. Notice that the first option satisfies neither condition, and it is the one that looks right because both numbers from the question appear in it. Extracting the conditions from the instructions is the hard part; this is not.
Step 5

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.

Lab 7 · How big does it get
Try this firstLeave the slider at 4 and read the tree underneath it. Nine calls, each one indented under the call that made it. Then drag the slider up one notch at a time and watch the call count in the line below.
Every call in that tree really happened. The stage is run with the starting value you chose, and each 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.
Lab 8 · Stage four
Try this firstPress Open the debugger, then the 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.
This is where the tool beats the technique. Find the comparison at the end that decides your fate, break there, and print. If you want to understand it as well, look at the two recursive calls and what happens to their results: after the lab above, the shape should be familiar.
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.

Tracing this stage by hand would mean following how many calls?
Hundreds. Two calls per level means the number of calls doubles roughly every level, so starting at 12 works out at 465 calls in total. Writing all 465 down is possible and pointless. This is the moment where the tool beats the technique, and knowing when that moment has arrived is part of the skill.
Step 6

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.

Lab 9 · Stage five
Try this firstPress Dump the table. Sixteen lines appear, each one an index and the value stored at it, and they stay on screen while you try answers. Now find three of those values that add up to 33, and type their three indexes.
Read the table out first. There is a button to dump it as index and value, which is what you would do with 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.
Why does this stage accept more than one answer?
It checks the total. Anything that adds up passes, because adding up is all it looks at. This matters beyond the puzzle: a check that tests a property rather than an exact value has more answers than its author intended, and finding the unintended ones is a large part of what breaking software involves.
Step 7

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.

Lab 10 · Stage six
Try this firstPress Dump the table, then type 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.
Read the rule, then read the table. The code says what has to be true of the four numbers you give. The table says which numbers make it true. You need both, and neither on its own will get you there. One line is worth explaining before you start: 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.
You have read every instruction of a stage and still cannot work out the answer. What is most likely?
Look at the data. Code describes what happens to values; it very often does not contain them. When the instructions make sense and the answer still does not follow, the missing piece is almost always a table, a string or a structure somewhere in memory, and x is how you read it.
Step 8

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.

Lab 11 · The method, on a stage you have not seen
Try this firstAsk question one: which line decides. There are two jumps to 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.
One more, with no notes. Same method, unfamiliar code. Read it and you will finish with two conditions written down, and one of them has a multiplier in it this time. Two ways to finish from there. Try pairs: the second condition fixes the second number as three less than the first, so run the first number up from 1 and see what the total does, and the stage tells you the total it got each time, which means you are never guessing blind. Or do it in one move: put "three less than the first" in place of the second number in the first condition, and you are left with one unknown instead of two. If the three questions from Step 1 got you as far as those two conditions, they will get you through most things.
A program checks your licence key entirely on your own computer. How safe is the key itself?
Not safe, and no amount of hiding fixes it. The check must happen somewhere, and wherever it happens the compared value is present. Scrambling it only means a debugger sees it a moment later, once the program has unscrambled it for itself. This is why serious licensing asks a server, and why the same reasoning applies to anything a program tries to keep secret from the person running it.

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.
Step 9

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.

Lab 12 · Read a binary map
Try this firstSelect .bss. Compare its file size with its memory size, then switch formats and find the executable region.
Do this before opening the disassembly. Imports, strings, permissions and entry points tell you where to begin and which apparent byte ranges are data rather than code.
Why can a zero-filled .bss region occupy more memory than file bytes?
Repeated zero bytes do not need to be stored. The header records how much memory to reserve, and the loader supplies the initial zeros.
Step 10

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.

Lab 13 · Mark likely function entries
Try this firstSelect the direct call target. Compare its evidence with a familiar-looking prologue that no reachable code references.
Reachability is stronger than resemblance. A common prologue inside an unreachable data table is still data. An odd-looking address reached by a real call deserves analysis.
Why is scanning only for push rbp; mov rbp, rsp not enough to find functions?
A prologue is one clue. Call targets, entry points, relocations, unwind data and reachable control flow provide independent evidence.
Step 11

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.

Lab 14 · Follow both sides of a decision
Try this firstSet the input below 10 and run a path. Then raise it above 10 and compare the highlighted blocks and edges.
A trace is one path through the graph. The graph keeps alternatives that did not run, which is why static and dynamic views answer different questions.
What makes a sequence of instructions one basic block?
A basic block has straight-line control. Once the first instruction runs, every instruction through the final branch or return runs in order.
Step 12

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.

Lab 15 · Build a backward slice
Try this firstSelect the final comparison. Step backward through definitions and watch unrelated instructions drop out.
Keep the reason for every included line. “Looks relevant” is not enough. Each line should define a value used by a line already in the slice, or control whether that line runs.
Why does an uncertain memory alias usually make a static slice larger?
Uncertainty is kept, not wished away. Removing a possible writer could remove the actual cause, so a sound conservative slice includes it until stronger evidence separates the addresses.
Step 13

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.

Lab 16 · Pick a method and name its limit
Try this firstSelect the rare race. Choose a tool, then compare the result with the question and the tool's blind spot.
A tool recommendation is incomplete without its limit. One trace does not prove an unvisited path is safe, and one static warning does not prove the suspicious path is reachable.
Why can symbolic execution struggle with a program containing many input-dependent loops?
This is path explosion. Practical tools bound loops, merge states, prioritise paths or combine symbolic and concrete execution.
Step 14

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.

Lab 17 · Compare two recovered explanations
Try this firstChoose the signed comparison version. Run the boundary inputs and see which explanation matches every machine observation.
Prefer the explanation that predicts unseen cases. Both versions can fit easy positive inputs. A negative and a boundary value expose whether the original jump was signed or unsigned.
What does a variable name invented by a decompiler prove?
Names are annotations until debug information or other evidence supports them. The instructions prove operations and data movement; a helpful label is still a hypothesis.

The complete workflow

  1. Map the file, architecture, entry points, imports, permissions and load addresses.
  2. Find code connected to the behaviour through calls, strings, data references or a dynamic trace.
  3. Reduce it to a control-flow graph and a slice for the exact decision you care about.
  4. Choose static, dynamic, symbolic, fuzzing or emulation tools from the unanswered question.
  5. Treat recovered source and assistant explanations as hypotheses; test them at machine boundaries.
  6. Document the evidence, fix in source when possible, and add a regression test.