Bomb Lab · Defusing the Bomb¶
Verified locally
./bomb psol.txt defuses all 6 phases plus the secret stage — ending on "Congratulations! You've defused the bomb!"
A binary that reads a line, checks it against something hidden, and calls explode_bomb if you're wrong. Six phases, plus a seventh you have to discover. The only tool that matters is GDB, and the only skill is reading intent out of assembly.
The method
The bomb ships with no source for the phases — only the executable. Every phase follows the same anatomy, and so does the way in:
break phase_n,run, thendisasto see the phase's code.- Find the conditional branch that guards the call to
explode_bomb. The condition needed to skip that call is your constraint. - Read the operands being compared — dump memory with
x/s,x/d, print registers — and work backward to an input that satisfies them.
A useful reflex: every je/jne/jle/ja/... that jumps toward explode_bomb is a wrong turn, so its negation points straight at the solution.
Phase 1 · a plain string compare¶
The gentlest phase. It hands your input and a fixed pointer to strings_not_equal (which, like strcmp, returns 0 when the two strings are equal) and explodes unless the result is zero:
mov $0x402400,%esi ; arg2 = a fixed string in .rodata
callq strings_not_equal ; %eax = 0 iff input == that string
test %eax,%eax ; set flags from %eax
je 400ef7 ; %eax == 0 -> jump past the bomb
callq explode_bomb ; ...otherwise, boom
So survival means strings_not_equal returns 0, i.e. your line equals the string at 0x402400. There's nothing to compute — just read that address in GDB:
Border relations with Canada have never been better.
Phase 2 · a loop over six numbers¶
read_six_numbers parses six integers onto the stack. Two checks follow. First, the very first number is pinned to 1:
callq read_six_numbers
cmpl $0x1,(%rsp) ; numbers[0] must be 1
je 400f30 ; ok -> enter the loop setup
callq explode_bomb
Then a loop walks the array with %rbx (a moving pointer) up to %rbp (one past the end), checking that each element is twice its predecessor — add %eax,%eax is just %eax * 2:
mov -0x4(%rbx),%eax ; eax = previous element
add %eax,%eax ; eax = previous * 2
cmp %eax,(%rbx) ; current == previous * 2 ?
jne ... explode
add $0x4,%rbx ; advance to the next int
cmp %rbp,%rbx ; reached the end?
jne ... (loop)
Starting from 1 and doubling five times gives a geometric sequence:
1 2 4 8 16 32
Phase 3 · a switch / jump table¶
Here sscanf reads two integers (its format string lives at 0x4025cf — inspect it and you'll see "%d %d"). The return value, the count of items parsed, must be at least 2:
lea 0x8(%rsp),%rdx ; &first
lea 0xc(%rsp),%rcx ; &second
mov $0x4025cf,%esi ; "%d %d"
callq sscanf
cmp $0x1,%eax
jg 400f6a ; parsed > 1 value -> continue
callq explode_bomb
The first number selects a branch through a jump table — the shape a switch takes once compiled. It is bounded to 0–7, then used to index a table of code addresses at 0x402470:
cmpl $0x7,0x8(%rsp) ; first must be <= 7 (unsigned compare)
ja ... explode
mov 0x8(%rsp),%eax
jmpq *0x402470(,%rax,8) ; goto table[first]
Each case loads a constant into %eax, and the final check demands the second number equal it. (Dump the table itself with x/8a 0x402470 to read every case's target address.) Following the entry for first = 1 lands on:
Any of the eight cases yields a valid answer; taking case 1:
1 311
Phase 4 · a recursive function¶
sscanf again reads two integers; this time exactly two, and the first is capped at 14:
cmp $0x2,%eax ; exactly two values parsed
jne ... explode
cmpl $0xe,0x8(%rsp) ; first <= 14
jbe ... continue
The first number is passed to func4(first, 0, 14), whose result must be 0, and the second number must also be 0:
mov $0xe,%edx ; hi = 14
mov $0x0,%esi ; lo = 0
mov 0x8(%rsp),%edi ; x = first
callq func4
test %eax,%eax ; func4 must return 0
jne ... explode
cmpl $0x0,0xc(%rsp) ; second == 0
je ... defused
func4 is a binary search over [lo, hi]. It computes the midpoint (the shr/sar pair is just a signed divide-by-two that rounds toward zero), then recurses into one half — doubling the running result on the way back:
mov %edx,%eax
sub %esi,%eax ; hi - lo
...
sar %eax ; (hi-lo)/2
lea (%rax,%rsi,1),%ecx ; mid = lo + (hi-lo)/2
cmp %edi,%ecx
jle 400ff2 ; mid <= x ? ...
lea -0x1(%rcx),%edx ; x < mid -> recurse on [lo, mid-1], result = 2*r
...
lea 0x1(%rcx),%esi ; x > mid -> recurse on [mid+1, hi], result = 2*r+1
The result stays 0 only along a path that never takes the 2*r + 1 (go-right) branch. So func4 returns 0 exactly for the midpoints reached by going left from [0, 14]:
func4(x, 0, 14), mid = (lo + hi) / 2 at each step:
x == mid -> return 0
x < mid -> go left, result = 2*r (0 stays 0)
x > mid -> go right, result = 2*r + 1 (never 0 again)
Follow the all-left spine; the matching x at each level returns 0:
[0,14] mid = 7 -- x = 7 --> 0
|
+-- [0,6] mid = 3 -- x = 3 --> 0
|
+-- [0,2] mid = 1 -- x = 1 --> 0
|
+-- [0,0] mid = 0 -- x = 0 --> 0
=> func4 returns 0 for x in {0, 1, 3, 7}
Take 1, and pair it with the required second 0:
1 0— and note what appendingDrEvilhere unlocks in the secret phase.
Phase 5 · a table-lookup cipher¶
The input must be 6 characters long. Then each character is transformed and the six results must spell a target word. The transform: mask a character down to its low 4 bits, and use that as an index into a 16-byte table at 0x4024b0 — the string "maduiersnfotvbyl" (peek with x/s 0x4024b0):
movzbl (%rbx,%rax,1),%ecx ; ecx = input[i]
and $0xf,%edx ; keep the low nibble
movzbl 0x4024b0(%rdx),%edx ; edx = table[nibble]
mov %dl,0x10(%rsp,%rax,1) ; append to the result buffer
...
mov $0x40245e,%esi ; target = "flyers"
callq strings_not_equal
So the puzzle is: pick six bytes whose low nibbles index the letters of "flyers". Reading the table, those letters sit at indices 9, 15, 14, 5, 6, 7. Any characters with those low nibbles work — a printable set is one per column:
| target letter | f |
l |
y |
e |
r |
s |
|---|---|---|---|---|---|---|
| table index (= low nibble needed) | 9 | 15 | 14 | 5 | 6 | 7 |
| a printable char with that low nibble | ) 0x29 |
/ 0x2f |
. 0x2e |
% 0x25 |
& 0x26 |
' 0x27 |
)/.%&'
Phase 6 · reordering a linked list¶
The hardest of the six, and worth taking slowly. It reads six numbers and enforces two properties: each is in 1–6, and all are distinct (a nested loop compares every pair):
mov 0x0(%r13),%eax
sub $0x1,%eax
cmp $0x5,%eax ; (value - 1) <= 5 -> value in 1..6
jbe ...
...
cmp %eax,0x0(%rbp) ; compare against every other value
jne ... ; must differ -> distinct
Next it maps every value x to 7 - x, and uses those to index into a six-node linked list in .data (each node is { int value; int pad; node *next; }). It threads the nodes into the order your numbers specify, then verifies that order is descending by node value:
mov $0x7,%ecx
sub (%rax),%edx ; 7 - each input
...
mov 0x8(%rdx),%rdx ; follow ->next to the chosen node
...
mov 0x8(%rbx),%rax
mov (%rax),%eax
cmp %eax,(%rbx)
jge ... ; node.value >= next.value -> descending
So the recipe is: read the node values, sort them descending, and emit 7 - node for each. Dumping the list (x/12xw 0x6032d0 shows each node's value and next):
node 1 2 3 4 5 6
value 0x14c 0xa8 0x39c 0x2b3 0x1dd 0x1bb
sort the values high -> low; that fixes the node order,
and the input is 7 - node:
value 0x39c 0x2b3 0x1dd 0x1bb 0x14c 0xa8
node 3 4 5 6 1 2
input 4 3 2 1 6 5
4 3 2 1 6 5
The secret phase¶
There's a seventh phase, and finding it is half the puzzle. After all six are defused, phase_defused quietly re-parses the line you gave phase 4 — this time as "%d %d %s" — and checks whether the trailing word is "DrEvil":
mov $0x402622,%esi ; "DrEvil"
lea 0x10(%rsp),%rdi ; the %s captured from phase 4's line
callq strings_not_equal ; match -> call secret_phase
So phase 4's answer grows a third token: 1 0 DrEvil. The secret phase then reads one integer (1 <= n <= 1001, from a (n-1) <= 0x3e8 check) and calls fun7 on a binary search tree rooted at 0x6030f0; the return value must be exactly 2:
mov (%rdi),%edx ; node value
cmp %esi,%edx ; compare node value with input n
jle ... ; node <= n -> handle match or descend right
; fall through when n < node -> left child (0x8)
; n > node -> right child (0x10); n == node -> return 0
fun7 encodes the path it walks into its return value: each step left multiplies by 2, each step right does 2·r + 1, and a match returns 0. Read backward, a result of 2 factors uniquely as 2 = 2·(2·0 + 1) — that is, left, then right, then a match:
fun7 walks a BST; each step folds into the return value:
n < node -> go left, return 2*r
n > node -> go right, return 2*r + 1
n == node -> match, return 0
Want return = 2. Factor it to read the path back off:
2 = 2 * ( 2*0 + 1 )
| | `-- match -> 0
| `--------- step RIGHT -> 2*0 + 1
`------------------ step LEFT -> 2*1
So the path is left, then right, then match:
root (0x6030f0)
/
node (n < root -> left)
\
[ 22 ] (n > node -> right, and n == 22 -> match)
=> n = the value at root -> left -> right = 22
So n must equal the value stored at root -> left -> right, which is 22 — the one number that threads exactly the left, right, match path:
22