Breaking CMU's Bomblab with Angr for Fun and Profit - Part 5
This is Part 5 on cracking CMU’s Bomblab with Angr. If you just stumbled upon this, I would recommend starting with part 1 here.
Phase 5
Let’s disassemble Phase 5:
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
gef➤ disas phase_5
Dump of assembler code for function phase_5:
0x0000000000401062 <+0>: push rbx
0x0000000000401063 <+1>: sub rsp,0x20
0x0000000000401067 <+5>: mov rbx,rdi
0x000000000040106a <+8>: mov rax,QWORD PTR fs:0x28
0x0000000000401073 <+17>: mov QWORD PTR [rsp+0x18],rax
0x0000000000401078 <+22>: xor eax,eax
0x000000000040107a <+24>: call 0x40131b <string_length>
0x000000000040107f <+29>: cmp eax,0x6
0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112>
0x0000000000401084 <+34>: call 0x40143a <explode_bomb>
0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112>
0x000000000040108b <+41>: movzx ecx,BYTE PTR [rbx+rax*1]
0x000000000040108f <+45>: mov BYTE PTR [rsp],cl
0x0000000000401092 <+48>: mov rdx,QWORD PTR [rsp]
0x0000000000401096 <+52>: and edx,0xf
0x0000000000401099 <+55>: movzx edx,BYTE PTR [rdx+0x4024b0]
0x00000000004010a0 <+62>: mov BYTE PTR [rsp+rax*1+0x10],dl
0x00000000004010a4 <+66>: add rax,0x1
0x00000000004010a8 <+70>: cmp rax,0x6
0x00000000004010ac <+74>: jne 0x40108b <phase_5+41>
0x00000000004010ae <+76>: mov BYTE PTR [rsp+0x16],0x0
0x00000000004010b3 <+81>: mov esi,0x40245e
0x00000000004010b8 <+86>: lea rdi,[rsp+0x10]
0x00000000004010bd <+91>: call 0x401338 <strings_not_equal>
0x00000000004010c2 <+96>: test eax,eax
0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119>
0x00000000004010c6 <+100>: call 0x40143a <explode_bomb>
0x00000000004010cb <+105>: nop DWORD PTR [rax+rax*1+0x0]
0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119>
0x00000000004010d2 <+112>: mov eax,0x0
0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41>
0x00000000004010d9 <+119>: mov rax,QWORD PTR [rsp+0x18]
0x00000000004010de <+124>: xor rax,QWORD PTR fs:0x28
0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140>
0x00000000004010e9 <+135>: call 0x400b30 <__stack_chk_fail@plt>
0x00000000004010ee <+140>: add rsp,0x20
0x00000000004010f2 <+144>: pop rbx
0x00000000004010f3 <+145>: ret
End of assembler dump.
We see strings_not_equal
, like in Phase 1, and also string_length
. Also, right after the string_length
call there is a comparison with 6, so we can assume that it wants 6 bytes of input. Let’s craft the input again in the same stack frame as the function by setting rdi
to our symbolic bitvector of 6 bytes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def phase_5(argv):
# Create an Angr project.
path_to_binary = argv[1] # :string
project = angr.Project(path_to_binary)
# Tell Angr where to start executing
start_addr = 0x00401062
initial_state = project.factory.blank_state(addr=start_addr)
fake_addr = 0x40000000
input_len = 6
phase_5_input = claripy.BVS('phase_5_input', input_len * 8)
initial_state.memory.store(fake_addr, phase_5_input)
initial_state.regs.rdi = fake_addr
Recall how we had to replace strings_not_equal
with our own custom implementation back in Phase 1 to avoid state explosion? I recently found out that there was a really simple way of doing this by using the built-in libc ones that Angr already helpfully provides. For instance, strings_not_equal
can simply be replaced with the libc strcmp
, whose definition can be found here. Similarly, string_length
is the same as the libc strlen
, which can be found here. So let us replace those using this method, to avoid state explosion:
1
2
3
4
5
6
7
8
strcmp = angr.SIM_PROCEDURES['libc']['strcmp']
strlen = angr.SIM_PROCEDURES['libc']['strlen']
strings_not_equal_symbol = 'strings_not_equal'
string_length_symbol = "string_length"
project.hook_symbol(strings_not_equal_symbol, strcmp())
project.hook_symbol(string_length_symbol, strlen())
Finally, we set our success address to be at the ret
statement, and the same avoid address as before:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Create a simulation manager initialized with the starting state
simulation = project.factory.simgr(initial_state)
success_addr = 0x004010f3 # right before ret
explode_addr = 0x0040143a # explode_bomb
simulation.explore(find=success_addr, avoid=explode_addr)
if simulation.found:
solution_state = simulation.found[0]
# Case symbolic value to bytes
solution = solution_state.se.eval(phase_5_input, cast_to=bytes)
print(solution)
else:
raise Exception('Could not find the solution')
Almost There?
Let’s try running it!
1
2
3
4
5
6
7
8
9
10
11
12
13
$ python solve.py bomb
WARNING | 2020-08-02 20:48:21,569 | 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 20:48:21,569 | 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 20:48:21,569 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-08-02 20:48:21,569 | 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 20:48:21,569 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-08-02 20:48:21,569 | angr.state_plugins.symbolic_memory | Filling register rbx with 8 unconstrained bytes referenced from 0x401062 (phase_5+0x0 in bomb (0x401062))
WARNING | 2020-08-02 20:48:21,579 | angr.state_plugins.symbolic_memory | Filling memory at 0x40000006 with 250 unconstrained bytes referenced from 0x40131b (string_length+0x0 in bomb (0x40131b))
WARNING | 2020-08-02 20:48:21,694 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeffd1 with 7 unconstrained bytes referenced from 0x401092 (phase_5+0x30 in bomb (0x401092))
WARNING | 2020-08-02 20:48:23,609 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 232 unconstrained bytes referenced from 0x401338 (strings_not_equal+0x0 in bomb (0x401338))
WARNING | 2020-08-02 20:48:23,610 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeffe7 with 1 unconstrained bytes referenced from 0x401338 (strings_not_equal+0x0 in bomb (0x401338))
CRITICAL | 2020-08-02 20:48:24,220 | angr.sim_state | The name state.se is deprecated; please use state.solver.
b'\t\x0f\x0e\x05\x06\x07'
We indeed got a solution, but those are not ASCII printable characters! Of course, we can pipe the input to the binary with something like pwntools, but this seems to not be the point of the assignment. This is where we introduce the new concept of adding constraints to our input.
Adding Constraints
We want to constrain our input bytes such that they are in the printable range. For simplicity, I will restrict it further to be smallest range that include the alphanumeric range, which includes a few non-alphanumeric characters as well. This will range from ‘0’ (0x30) to ‘z’ (0x7A), which you can quickly verify by pulling up an ASCII table with man ascii
.
We’ll add these constraints right before we build the simulation:
1
2
3
4
5
def constrain_printable(c):
return claripy.And(ord('0') <= c , c <= ord('z'))
for i in range(input_len):
initial_state.solver.add(constrain_printable(phase_5_input.get_byte(i)))
Now, we will either get a solution that is printable, or perhaps no solution at all. Let’s try:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ python solve.py bomb
WARNING | 2020-08-02 20:54:43,766 | 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 20:54:43,766 | 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 20:54:43,766 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-08-02 20:54:43,766 | 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 20:54:43,766 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-08-02 20:54:43,766 | angr.state_plugins.symbolic_memory | Filling register rbx with 8 unconstrained bytes referenced from 0x401062 (phase_5+0x0 in bomb (0x401062))
WARNING | 2020-08-02 20:54:43,796 | angr.state_plugins.symbolic_memory | Filling memory at 0x40000006 with 250 unconstrained bytes referenced from 0x40131b (string_length+0x0 in bomb (0x40131b))
WARNING | 2020-08-02 20:54:43,906 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeffd1 with 7 unconstrained bytes referenced from 0x401092 (phase_5+0x30 in bomb (0x401092))
WARNING | 2020-08-02 20:54:45,753 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 232 unconstrained bytes referenced from 0x401338 (strings_not_equal+0x0 in bomb (0x401338))
WARNING | 2020-08-02 20:54:45,753 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffeffe7 with 1 unconstrained bytes referenced from 0x401338 (strings_not_equal+0x0 in bomb (0x401338))
CRITICAL | 2020-08-02 20:54:46,356 | angr.sim_state | The name state.se is deprecated; please use state.solver.
b'9?>567'
Fantastic! Let’s try this input on the bomb:
1
2
3
4
5
6
7
8
9
10
11
12
13
$ ./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!
7 0
So you got that one. Try this one.
9?>567
Good work! On to the next...
Looks like it worked! Thanks for reading so far, and I hope you enjoyed it! You can continue on to Part 6 here.
Related Posts: