Breaking CMU's Bomblab with Angr for Fun and Profit - Part 3
We’re back now with Part 3 of this series on cracking CMU’s Bomblab using Angr! If you are new, I would recommend starting with Part 1 here.
Phase 3
If you went through what we did for phase 2, the idea behind phase 3 is also super similar.
Start again by creating a function stub for phase 3:
1
2
3
def phase_3(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
Let’s see the disassembly for Phase 3:
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
39
gef➤ disas phase_3
Dump of assembler code for function phase_3:
0x0000000000400f43 <+0>: sub rsp,0x18
0x0000000000400f47 <+4>: lea rcx,[rsp+0xc]
0x0000000000400f4c <+9>: lea rdx,[rsp+0x8]
0x0000000000400f51 <+14>: mov esi,0x4025cf
0x0000000000400f56 <+19>: mov eax,0x0
0x0000000000400f5b <+24>: call 0x400bf0 <__isoc99_sscanf@plt>
0x0000000000400f60 <+29>: cmp eax,0x1
0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39>
0x0000000000400f65 <+34>: call 0x40143a <explode_bomb>
0x0000000000400f6a <+39>: cmp DWORD PTR [rsp+0x8],0x7
0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106>
0x0000000000400f71 <+46>: mov eax,DWORD PTR [rsp+0x8]
0x0000000000400f75 <+50>: jmp QWORD PTR [rax*8+0x402470]
0x0000000000400f7c <+57>: mov eax,0xcf
0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123>
0x0000000000400f83 <+64>: mov eax,0x2c3
0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123>
0x0000000000400f8a <+71>: mov eax,0x100
0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123>
0x0000000000400f91 <+78>: mov eax,0x185
0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123>
0x0000000000400f98 <+85>: mov eax,0xce
0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123>
0x0000000000400f9f <+92>: mov eax,0x2aa
0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123>
0x0000000000400fa6 <+99>: mov eax,0x147
0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123>
0x0000000000400fad <+106>: call 0x40143a <explode_bomb>
0x0000000000400fb2 <+111>: mov eax,0x0
0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123>
0x0000000000400fb9 <+118>: mov eax,0x137
0x0000000000400fbe <+123>: cmp eax,DWORD PTR [rsp+0xc]
0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134>
0x0000000000400fc4 <+129>: call 0x40143a <explode_bomb>
0x0000000000400fc9 <+134>: add rsp,0x18
0x0000000000400fcd <+138>: ret
End of assembler dump.
We see a sscanf
call, which reads formatted input from a string. This is probably what builds the arguments, based on the previous read_line
call, since rdi
is not modified before sscanf
is called. Let’s check out the format string that was passed in:
1
2
gef➤ x/s 0x4025cf
0x4025cf: "%d %d"
Cool, so two integers. We can follow more or less the same format as in Phase 2 by pushing these two values onto the stack. We define the start address to be the instruction after the sscanf
call:
1
2
3
4
5
6
start_addr = 0x00400f60
initial_state = project.factory.blank_state(addr=start_addr)
num_12 = claripy.BVS('num_12', 64)
initial_state.stack_push(num_12)
However, there is one major difference. In Phase 2, our arguments were at the bottom of the stack, and so we could just push them onto the stack. However, in this case, we see that we have a stack size of 0x18 from line 3, but the addresses of the two arguments that were populated by sscanf
are rsp + 0xc
and rsp + 0x8
respectively. This means that we need an additional 8 bytes of padding afterwards:
1
2
padding_length_in_bytes = 8
initial_state.regs.rsp -= padding_length_in_bytes
Let’s set our find condition to be right before the ret
, and avoid to be explode_bomb
as usual, and then copy and paste what we had for Phase 2 to extract the solution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
simulation = project.factory.simgr(initial_state)
success_addr = 0x00400fcd # right before ret
explode_addr = 0x0040143a # explode_bomb
simulation.explore(find=success_addr, avoid=explode_addr)
# Check that we have found a solution
if simulation.found:
solution_state = simulation.found[0]
num_12_sol = solution_state.se.eval(num_12, cast_to=int)
def unpack_ints(n):
lower_32_mask = (1 << 32) - 1
return (n & lower_32_mask, (n >> 32) & lower_32_mask)
num_1_sol, num_2_sol = unpack_ints(num_12_sol)
print(f"{num_1_sol} {num_2_sol}")
else:
raise Exception('Could not find the solution')
Full Solution Script
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
39
40
41
42
43
44
45
import angr
import claripy
import sys
def phase_3(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
# Tell Angr where to start executing
start_addr = 0x00400f60
initial_state = project.factory.blank_state(addr=start_addr)
num_12 = claripy.BVS('num_12', 64)
initial_state.stack_push(num_12)
padding_length_in_bytes = 8
initial_state.regs.rsp -= padding_length_in_bytes
# Create a simulation manager initialized with the starting state
simulation = project.factory.simgr(initial_state)
success_addr = 0x00400fcd # right before ret
explode_addr = 0x0040143a # explode_bomb
simulation.explore(find=success_addr, avoid=explode_addr)
# Check that we have found a solution
if simulation.found:
solution_state = simulation.found[0]
num_12_sol = solution_state.se.eval(num_12, cast_to=int)
def unpack_ints(n):
lower_32_mask = (1 << 32) - 1
return (n & lower_32_mask, (n >> 32) & lower_32_mask)
num_1_sol, num_2_sol = unpack_ints(num_12_sol)
print(f"{num_1_sol} {num_2_sol}")
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
phase_3(sys.argv)
Let’s try running it:
1
2
3
4
5
6
7
8
9
$ solve.py bomb
WARNING | 2020-08-02 19:42:16,368 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-08-02 19:42:16,368 | angr.state_plugins.symbolic_memory | angr will cope with this by generating an unconstrained symbolic variable and continuing. You can resolve this by:
WARNING | 2020-08-02 19:42:16,368 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-08-02 19:42:16,369 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-08-02 19:42:16,369 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-08-02 19:42:16,369 | angr.state_plugins.symbolic_memory | Filling register rax with 8 unconstrained bytes referenced from 0x400f60 (phase_3+0x1d in bomb (0x400f60))
CRITICAL | 2020-08-02 19:42:17,120 | angr.sim_state | The name state.se is deprecated; please use state.solver.
1 311
Now using the solution on the actual bomb:
1
2
3
4
5
6
7
8
9
$ ./bomb
Welcome to my fiendish little bomb. You have 6 phases with
which to blow yourself up. Have a nice day!
Border relations with Canada have never been better.
Phase 1 defused. How about the next one?
1 2 4 8 16 32
That's number 2. Keep going!
1 311
Halfway there!
Nice, it worked, and this was pretty fast too given how similar it was to phase 2! You can continue to Part 4 here.
Related Posts: