CMU 15-213 Bomblab
Bomblab
This is the second lab of course CMU 15-213. I have to say that, unlike datalab, this lab is fairly ‘logical’ and fun to play with. The idea of looking into assembly code to get a fundamental understanding of how programs actually got compiled into machine level language is absolutely effective and genius in teaching.
Make sure you’ve solved the puzzles on yourself before checking my solutions!
If you find any mistakes in this blog, you’re most welcomed to leave a comment on it.
Records
- Reverse executable file into assembly code by:
1
objdump -D bomb > bomb.s
Phase 1
Obviously we gotta look into the assembly code of
phase_1:1
2
3
4
5
6
7
8
90000000000400ee0 <phase_1>:
400ee0:448 83 ec 08 sub $0x8,%rsp
400ee4:4be 00 24 40 00 mov $0x402400,%esi
400ee9:4e8 4a 04 00 00 call 401338 <strings_not_equal>
400eee:485 c0 test %eax,%eax
400ef0:474 05 je 400ef7 <phase_1+0x17>
400ef2:4e8 43 05 00 00 call 40143a <explode_bomb>
400ef7:448 83 c4 08 add $0x8,%rsp
400efb:4c3 retEven more obvious we should take a look into function
<strings_not_equal>:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
390000000000401338 <strings_not_equal>:
401338:441 54 push %r12
40133a:455 push %rbp
40133b:453 push %rbx
40133c:448 89 fb mov %rdi,%rbx
40133f:448 89 f5 mov %rsi,%rbp
401342:4e8 d4 ff ff ff call 40131b <string_length>
401347:441 89 c4 mov %eax,%r12d
40134a:448 89 ef mov %rbp,%rdi
40134d:4e8 c9 ff ff ff call 40131b <string_length>
401352:4ba 01 00 00 00 mov $0x1,%edx
401357:441 39 c4 cmp %eax,%r12d
40135a:475 3f jne 40139b <strings_not_equal+0x63>
40135c:40f b6 03 movzbl (%rbx),%eax
40135f:484 c0 test %al,%al
401361:474 25 je 401388 <strings_not_equal+0x50>
401363:43a 45 00 cmp 0x0(%rbp),%al
401366:474 0a je 401372 <strings_not_equal+0x3a>
401368:4eb 25 jmp 40138f <strings_not_equal+0x57>
40136a:43a 45 00 cmp 0x0(%rbp),%al
40136d:40f 1f 00 nopl (%rax)
401370:475 24 jne 401396 <strings_not_equal+0x5e>
401372:448 83 c3 01 add $0x1,%rbx
401376:448 83 c5 01 add $0x1,%rbp
40137a:40f b6 03 movzbl (%rbx),%eax
40137d:484 c0 test %al,%al
40137f:475 e9 jne 40136a <strings_not_equal+0x32>
401381:4ba 00 00 00 00 mov $0x0,%edx
401386:4eb 13 jmp 40139b <strings_not_equal+0x63>
401388:4ba 00 00 00 00 mov $0x0,%edx
40138d:4eb 0c jmp 40139b <strings_not_equal+0x63>
40138f:4ba 01 00 00 00 mov $0x1,%edx
401394:4eb 05 jmp 40139b <strings_not_equal+0x63>
401396:4ba 01 00 00 00 mov $0x1,%edx
40139b:489 d0 mov %edx,%eax
40139d:45b pop %rbx
40139e:45d pop %rbp
40139f:441 5c pop %r12
4013a1:4c3 retFrom the code we can see that the funcion first compare the length of the two strings and then compare each character.
If we type in
helloas a ‘test’ input for this phase, we can find out that it is the first call forstring_lengththat calculates the length of our input string, for%eaxis set to5(as the length ofhello) after it returns. (In the help of gdb)Then this
5is moved into%r12at:1
401347:441 89 c4 mov %eax,%r12d
Then compared with the second length calculated at:
1
2401357:441 39 c4 cmp %eax,%r12d
40135a:475 3f jne 40139b <strings_not_equal+0x63>
So the idea is simple: just step into the second
string_lengthand see what is at the memory address stored in the register corresponding to the first argument of a function call, which is%rdi:Step into the second <string_length>
1
2
3
4(gdb) print /x $rdi
$1 = 0x402400
(gdb) print (char *) 0x402400
$2 = 0x402400 "Border relations with Canada have never been better."And we’re done for phase 1.
Phase 2
As usual, assembly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
260000000000400efc <phase_2>:
400efc:455 push %rbp
400efd:453 push %rbx
400efe:448 83 ec 28 sub $0x28,%rsp
400f02:448 89 e6 mov %rsp,%rsi
400f05:4e8 52 05 00 00 call 40145c <read_six_numbers>
400f0a:483 3c 24 01 cmpl $0x1,(%rsp)
400f0e:474 20 je 400f30 <phase_2+0x34>
400f10:4e8 25 05 00 00 call 40143a <explode_bomb>
400f15:4eb 19 jmp 400f30 <phase_2+0x34>
400f17:48b 43 fc mov -0x4(%rbx),%eax
400f1a:401 c0 add %eax,%eax
400f1c:439 03 cmp %eax,(%rbx)
400f1e:474 05 je 400f25 <phase_2+0x29>
400f20:4e8 15 05 00 00 call 40143a <explode_bomb>
400f25:448 83 c3 04 add $0x4,%rbx
400f29:448 39 eb cmp %rbp,%rbx
400f2c:475 e9 jne 400f17 <phase_2+0x1b>
400f2e:4eb 0c jmp 400f3c <phase_2+0x40>
400f30:448 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35:448 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a:4eb db jmp 400f17 <phase_2+0x1b>
400f3c:448 83 c4 28 add $0x28,%rsp
400f40:45b pop %rbx
400f41:45d pop %rbp
400f42:4c3 retImmediately we see this
<read_six_numbers>, so1 2 3 4 5 6is an ideal test string we should put in here.Assembly of
read_six_numbers:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18000000000040145c <read_six_numbers>:
40145c:448 83 ec 18 sub $0x18,%rsp
401460:448 89 f2 mov %rsi,%rdx
401463:448 8d 4e 04 lea 0x4(%rsi),%rcx
401467:448 8d 46 14 lea 0x14(%rsi),%rax
40146b:448 89 44 24 08 mov %rax,0x8(%rsp)
401470:448 8d 46 10 lea 0x10(%rsi),%rax
401474:448 89 04 24 mov %rax,(%rsp)
401478:44c 8d 4e 0c lea 0xc(%rsi),%r9
40147c:44c 8d 46 08 lea 0x8(%rsi),%r8
401480:4be c3 25 40 00 mov $0x4025c3,%esi
401485:4b8 00 00 00 00 mov $0x0,%eax
40148a:4e8 61 f7 ff ff call 400bf0 <__isoc99_sscanf@plt>
40148f:483 f8 05 cmp $0x5,%eax
401492:47f 05 jg 401499 <read_six_numbers+0x3d>
401494:4e8 a1 ff ff ff call 40143a <explode_bomb>
401499:448 83 c4 18 add $0x18,%rsp
40149d:4c3 ret- We can see that the assembly code this function does not explicitly use anything stored in
%rdiwhile manipulating%rsia lot, which is pretty strange at first glance. - It turns out that the value stored inside
%rdiis implicitly passed tosscanfas the address to hold the user input. - We can trace back to
mainto see where%rdiis set afterread_line:1
2
3
4
5
6
7
8
9# ...Phase 1
400e3f:4e8 80 07 00 00 call 4015c4 <phase_defused>
400e44:4bf a8 23 40 00 mov $0x4023a8,%edi
400e49:4e8 c2 fc ff ff call 400b10 <puts@plt>
400e4e:4e8 4b 06 00 00 call 40149e <read_line>
400e53:448 89 c7 mov %rax,%rdi
400e56:4e8 a1 00 00 00 call 400efc <phase_2>
400e5b:4e8 64 07 00 00 call 4015c4 <phase_defused>
# Phase 3... - It is set as the pointer pointing towards the string returned by
read_line.
- We can see that the assembly code this function does not explicitly use anything stored in
There seems to be a lot going on before invoking
sscanf, but it’s mainly preparing space for user input on the stack.In short:
%rdipassed all along tosscanfas input string.%rsiholds the pointer pointing to the address of the buffer which is to store the six numbers parsed bysscanf.%rdx,%rcx,%r8,%r9holding the first four addresses of each of the target buffer block to hold the parsed numbers, while the rest 2 addresses are pushed on the stack.sscanfthen set the values stored in those addresses to the six input numbers.- After
sscanfreturns, compare the return value with0x5. If the return value, which represents the number of the values parsed, is less or equal to 5, then explode the bomb. - Free the stack frame and return.
1 | 401467:448 8d 46 14 lea 0x14(%rsi),%rax |
- This is where the last two addresses of the target buffer is pushed on the stack.
After reading six numbers from user input, first check whether the first number is
0x1:1
2400f0a:483 3c 24 01 cmpl $0x1,(%rsp)
400f0e:474 20 je 400f30 <phase_2+0x34>Then there is a loop determining whether the input numbers are of an expected pattern:
1
2
3
4
5
6
7
8
9
10
11
12400f17:48b 43 fc mov -0x4(%rbx),%eax
400f1a:401 c0 add %eax,%eax
400f1c:439 03 cmp %eax,(%rbx)
400f1e:474 05 je 400f25 <phase_2+0x29>
400f20:4e8 15 05 00 00 call 40143a <explode_bomb>
400f25:448 83 c3 04 add $0x4,%rbx
400f29:448 39 eb cmp %rbp,%rbx
400f2c:475 e9 jne 400f17 <phase_2+0x1b>
400f2e:4eb 0c jmp 400f3c <phase_2+0x40>
400f30:448 8d 5c 24 04 lea 0x4(%rsp),%rbx
400f35:448 8d 6c 24 18 lea 0x18(%rsp),%rbp
400f3a:4eb db jmp 400f17 <phase_2+0x1b>Can be written as pseudocode like:
1
2
3
4
5
6int rax;
for(int rbx = 0x7fffffffd3d4; rbx != 0x7fffffffd3e8; rbx += 4) {
rax = *(rbx - 4);
rax += rax;
if (rax != *rbx) explode_bomb();
}To this stage, the answer can not be more obvious.
Phase 3
Assembly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
370000000000400f43 <phase_3>:
400f43:448 83 ec 18 sub $0x18,%rsp
400f47:448 8d 4c 24 0c lea 0xc(%rsp),%rcx
400f4c:448 8d 54 24 08 lea 0x8(%rsp),%rdx
400f51:4be cf 25 40 00 mov $0x4025cf,%esi
400f56:4b8 00 00 00 00 mov $0x0,%eax
400f5b:4e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt>
400f60:483 f8 01 cmp $0x1,%eax
400f63:47f 05 jg 400f6a <phase_3+0x27>
400f65:4e8 d0 04 00 00 call 40143a <explode_bomb>
400f6a:483 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f:477 3c ja 400fad <phase_3+0x6a>
400f71:48b 44 24 08 mov 0x8(%rsp),%eax
400f75:4ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)
400f7c:4b8 cf 00 00 00 mov $0xcf,%eax
400f81:4eb 3b jmp 400fbe <phase_3+0x7b>
400f83:4b8 c3 02 00 00 mov $0x2c3,%eax
400f88:4eb 34 jmp 400fbe <phase_3+0x7b>
400f8a:4b8 00 01 00 00 mov $0x100,%eax
400f8f:4eb 2d jmp 400fbe <phase_3+0x7b>
400f91:4b8 85 01 00 00 mov $0x185,%eax
400f96:4eb 26 jmp 400fbe <phase_3+0x7b>
400f98:4b8 ce 00 00 00 mov $0xce,%eax
400f9d:4eb 1f jmp 400fbe <phase_3+0x7b>
400f9f:4b8 aa 02 00 00 mov $0x2aa,%eax
400fa4:4eb 18 jmp 400fbe <phase_3+0x7b>
400fa6:4b8 47 01 00 00 mov $0x147,%eax
400fab:4eb 11 jmp 400fbe <phase_3+0x7b>
400fad:4e8 88 04 00 00 call 40143a <explode_bomb>
400fb2:4b8 00 00 00 00 mov $0x0,%eax
400fb7:4eb 05 jmp 400fbe <phase_3+0x7b>
400fb9:4b8 37 01 00 00 mov $0x137,%eax
400fbe:43b 44 24 0c cmp 0xc(%rsp),%eax
400fc2:474 05 je 400fc9 <phase_3+0x86>
400fc4:4e8 71 04 00 00 call 40143a <explode_bomb>
400fc9:448 83 c4 18 add $0x18,%rsp
400fcd:4c3 retFrom the judgment towards the return value of
sscanf:1
2
3
4400f5b:4e8 90 fc ff ff call 400bf0 <__isoc99_sscanf@plt>
400f60:483 f8 01 cmp $0x1,%eax
400f63:47f 05 jg 400f6a <phase_3+0x27>
400f65:4e8 d0 04 00 00 call 40143a <explode_bomb>We can see that the number of the expected user input should be more than 1, also:
1
2
3
4400f6a:483 7c 24 08 07 cmpl $0x7,0x8(%rsp)
400f6f:477 3c ja 400fad <phase_3+0x6a>
# ...
400fad:4e8 88 04 00 00 call 40143a <explode_bomb>For function
sscanf,%rdiholds the address of the raw input,%esiholds theformatstring, thusrdxandrcxholds the two addresses where the program put the two parsed numbers to.
- The reason why I know the wanted inputs are numbers is that I’ve tried them in cli.
- So, the first input should be less or equal than
0x7(also non-negative forjareads unsigned values comparison result). As a result1 2should be an ideal test input.
- Like phase 2,
%rdiis passed frommainfunction, pointing to the address of user input string.
Then the ‘bomb’ put the second input into
%eaxand jump to somewhere in a jump table:1
2400f71:48b 44 24 08 mov 0x8(%rsp),%eax
400f75:4ff 24 c5 70 24 40 00 jmp *0x402470(,%rax,8)The syntax here in the second line of code means:
- Jump to address
0x402470 + 0 + (%rax) * 0x8
- Jump to address
So according to our test input, the value at address stored in
%raxshould be0x1, Thus the target address of thisjmpshould be located at0x402470 + 0 + 1 * 0x8 = 0x402478, which is:1
2(gdb) x/wx 0x402478
0x402478: 0x00400fb9Which is:
1
2
3
4
5
6400fb9:4b8 37 01 00 00 mov $0x137,%eax
400fbe:43b 44 24 0c cmp 0xc(%rsp),%eax
400fc2:474 05 je 400fc9 <phase_3+0x86>
400fc4:4e8 71 04 00 00 call 40143a <explode_bomb>
400fc9:448 83 c4 18 add $0x18,%rsp
400fcd:4c3 retWhich means the correct second input corresponding to
0x1as the first input is0x137 = 311.
We can therefore reverse the whole jump table:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20switch (first_input):
case 0:
if (second_input == 0xcf) break;
case 1:
if (second_input == 0x137) break;
case 2:
if (second_input == 0x2c3) break;
case 3:
if (second_input == 0x100) break;
case 4:
if (second_input == 0x185) break;
case 5:
if (second_input == 0xce) break;
case 6:
if (second_input == 0x2aa) break;
case 7:
if (second_input == 0x147) break;
default:
explode_bomb();
Phase 4
Assembly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23000000000040100c <phase_4>:
40100c:448 83 ec 18 sub $0x18,%rsp
401010:448 8d 4c 24 0c lea 0xc(%rsp),%rcx
401015:448 8d 54 24 08 lea 0x8(%rsp),%rdx
40101a:4be cf 25 40 00 mov $0x4025cf,%esi
40101f:4b8 00 00 00 00 mov $0x0,%eax
401024:4e8 c7 fb ff ff call 400bf0 <__isoc99_sscanf@plt>
401029:483 f8 02 cmp $0x2,%eax
40102c:475 07 jne 401035 <phase_4+0x29>
40102e:483 7c 24 08 0e cmpl $0xe,0x8(%rsp)
401033:476 05 jbe 40103a <phase_4+0x2e>
401035:4e8 00 04 00 00 call 40143a <explode_bomb>
40103a:4ba 0e 00 00 00 mov $0xe,%edx
40103f:4be 00 00 00 00 mov $0x0,%esi
401044:48b 7c 24 08 mov 0x8(%rsp),%edi
401048:4e8 81 ff ff ff call 400fce <func4>
40104d:485 c0 test %eax,%eax
40104f:475 07 jne 401058 <phase_4+0x4c>
401051:483 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056:474 05 je 40105d <phase_4+0x51>
401058:4e8 dd 03 00 00 call 40143a <explode_bomb>
40105d:448 83 c4 18 add $0x18,%rsp
401061:4c3 retEasy part:
sscanfuser input, write two numbers at address%rsp + 0x8and%rsp + 0xc.- The first input should be non-negative and less or equal to
0xe. - Set
%rdxto0xe. Set%rsito0x0. Set%rdito the first user input. - call
<func4>.
Let’s take a look into
func4:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
230000000000400fce <func4>:
400fce:448 83 ec 08 sub $0x8,%rsp
400fd2:489 d0 mov %edx,%eax
400fd4:429 f0 sub %esi,%eax
400fd6:489 c1 mov %eax,%ecx
400fd8:4c1 e9 1f shr $0x1f,%ecx
400fdb:401 c8 add %ecx,%eax
400fdd:4d1 f8 sar $1,%eax
400fdf:48d 0c 30 lea (%rax,%rsi,1),%ecx
400fe2:439 f9 cmp %edi,%ecx
400fe4:47e 0c jle 400ff2 <func4+0x24>
400fe6:48d 51 ff lea -0x1(%rcx),%edx
400fe9:4e8 e0 ff ff ff call 400fce <func4>
400fee:401 c0 add %eax,%eax
400ff0:4eb 15 jmp 401007 <func4+0x39>
400ff2:4b8 00 00 00 00 mov $0x0,%eax
400ff7:439 f9 cmp %edi,%ecx
400ff9:47d 0c jge 401007 <func4+0x39>
400ffb:48d 71 01 lea 0x1(%rcx),%esi
400ffe:4e8 cb ff ff ff call 400fce <func4>
401003:48d 44 00 01 lea 0x1(%rax,%rax,1),%eax
401007:448 83 c4 08 add $0x8,%rsp
40100b:4c3 retWe can see that the calculation for
%ecxis actually fixed, which means whatever the input numbers are,%ecxwill be0x7during the two comparisons:1
2
3
4
5
6
7
8
9
10
11# ...
400fe2:439 f9 cmp %edi,%ecx
400fe4:47e 0c jle 400ff2 <func4+0x24>
400fe6:48d 51 ff lea -0x1(%rcx),%edx
400fe9:4e8 e0 ff ff ff call 400fce <func4>
# ...
400ff7:439 f9 cmp %edi,%ecx
400ff9:47d 0c jge 401007 <func4+0x39>
400ffb:48d 71 01 lea 0x1(%rcx),%esi
400ffe:4e8 cb ff ff ff call 400fce <func4>
# ...Also we notice that each time the function returns from a recursion, it doubles the value in
%raxand increments it by 1.As the assembly code we can see after
func4returns:1
2
3
4
5
6# ...
40104d:485 c0 test %eax,%eax
40104f:475 07 jne 401058 <phase_4+0x4c>
# ...
401058:4e8 dd 03 00 00 call 40143a <explode_bomb>
# ...We definitely don’t want
%raxto be non-zero. As a result, we should pass a value tofunc4that bothjleandjgefires, which means%edishould equal to%ecxat the moment of comparison.Since we’ve already know that
%ecxwill be a fixed0x7, the value of%ediis now revealed.The second input is an easy zero:
1
2
3
4
5
6# ...
401051:483 7c 24 0c 00 cmpl $0x0,0xc(%rsp)
401056:474 05 je 40105d <phase_4+0x51>
401058:4e8 dd 03 00 00 call 40143a <explode_bomb>
40105d:448 83 c4 18 add $0x18,%rsp
401061:4c3 ret
To Be Continued…
- Title: CMU 15-213 Bomblab
- Author: Last
- Created at : 2026-07-21 09:17:10
- Link: https://blog.imlast.top/2026/07/21/cmu15213-bomblab/
- License: This work is licensed under CC BY-NC-SA 4.0.