Breaking CMU's Bomblab with Angr for Fun and Profit - Part 7
Welcome to the final part of cracking CMU’s Bomblab with Angr. If you are new here, I would recommend starting with part 1 here.
Secret Phase
We knew about the secret phase because we literally saw the string “Wow! You’ve defused the secret stage!” from Angr’s output for Phase 1, because the string was located right after the compare string for Phase 1in memory. If we do info func
in GDB, we indeed see a secret_phase
and fun7
function.
1
2
3
4
5
gef➤ info func
[...]
0x0000000000401204 fun7
0x0000000000401242 secret_phase
[...]
Let’s check fun7
first:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
gef➤ disas fun7
Dump of assembler code for function fun7:
0x0000000000401204 <+0>: sub rsp,0x8
0x0000000000401208 <+4>: test rdi,rdi
0x000000000040120b <+7>: je 0x401238 <fun7+52>
0x000000000040120d <+9>: mov edx,DWORD PTR [rdi]
0x000000000040120f <+11>: cmp edx,esi
0x0000000000401211 <+13>: jle 0x401220 <fun7+28>
0x0000000000401213 <+15>: mov rdi,QWORD PTR [rdi+0x8]
0x0000000000401217 <+19>: call 0x401204 <fun7>
0x000000000040121c <+24>: add eax,eax
0x000000000040121e <+26>: jmp 0x40123d <fun7+57>
0x0000000000401220 <+28>: mov eax,0x0
0x0000000000401225 <+33>: cmp edx,esi
0x0000000000401227 <+35>: je 0x40123d <fun7+57>
0x0000000000401229 <+37>: mov rdi,QWORD PTR [rdi+0x10]
0x000000000040122d <+41>: call 0x401204 <fun7>
0x0000000000401232 <+46>: lea eax,[rax+rax*1+0x1]
0x0000000000401236 <+50>: jmp 0x40123d <fun7+57>
0x0000000000401238 <+52>: mov eax,0xffffffff
0x000000000040123d <+57>: add rsp,0x8
0x0000000000401241 <+61>: ret
End of assembler dump.
Nothing out of the ordinary here, it is entirely self contained, so we can blackbox it.
Let’s now disassemble secret_phase
:
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
gef➤ disas secret_phase
Dump of assembler code for function secret_phase:
0x0000000000401242 <+0>: push rbx
0x0000000000401243 <+1>: call 0x40149e <read_line>
0x0000000000401248 <+6>: mov edx,0xa
0x000000000040124d <+11>: mov esi,0x0
0x0000000000401252 <+16>: mov rdi,rax
0x0000000000401255 <+19>: call 0x400bd0 <strtol@plt>
0x000000000040125a <+24>: mov rbx,rax
0x000000000040125d <+27>: lea eax,[rax-0x1]
0x0000000000401260 <+30>: cmp eax,0x3e8
0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42>
0x0000000000401267 <+37>: call 0x40143a <explode_bomb>
0x000000000040126c <+42>: mov esi,ebx
0x000000000040126e <+44>: mov edi,0x6030f0
0x0000000000401273 <+49>: call 0x401204 <fun7>
0x0000000000401278 <+54>: cmp eax,0x2
0x000000000040127b <+57>: je 0x401282 <secret_phase+64>
0x000000000040127d <+59>: call 0x40143a <explode_bomb>
0x0000000000401282 <+64>: mov edi,0x402438
0x0000000000401287 <+69>: call 0x400b10 <puts@plt>
0x000000000040128c <+74>: call 0x4015c4 <phase_defused>
0x0000000000401291 <+79>: pop rbx
0x0000000000401292 <+80>: ret
End of assembler dump.
Pretty standard, no new gotchas here. We can start executing after the strtol
function call, and then set a symbolic int to rax
:
1
2
3
4
5
6
7
8
9
10
11
def secret_phase(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
# Tell Angr where to start executing
start_addr = 0x0040125a # right after strtol
initial_state = project.factory.blank_state(addr=start_addr)
num = claripy.BVS('secret_phase_input', 4 * 8) # 32 bits
initial_state.regs.rax = num
The success address will be right before ret as usual. In order to get the result, we cast the symbolic output to an int.
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 = 0x00401292 # 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 int
solution = solution_state.se.eval(num, cast_to=int)
print(solution)
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
import angr
import claripy
import sys
def secret_phase(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
# Tell Angr where to start executing
start_addr = 0x0040125a # right after strtol
initial_state = project.factory.blank_state(addr=start_addr)
num = claripy.BVS('secret_phase_input', 4 * 8) # 32 bits
initial_state.regs.rax = num
# Create a simulation manager initialized with the starting state
simulation = project.factory.simgr(initial_state)
success_addr = 0x00401292 # 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 int
solution = solution_state.se.eval(num, cast_to=int)
print(solution)
else:
raise Exception('Could not find the solution')
if __name__ == '__main__':
secret_phase(sys.argv)
If we run it, we get the following:
1
2
3
4
5
6
7
8
9
$ python solve.py bomb
WARNING | 2020-08-04 00:07:02,736 | angr.state_plugins.symbolic_memory | The program is accessing memory or registers with an unspecified value. This could indicate unwanted behavior.
WARNING | 2020-08-04 00:07:02,736 | 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-04 00:07:02,736 | angr.state_plugins.symbolic_memory | 1) setting a value to the initial state
WARNING | 2020-08-04 00:07:02,736 | angr.state_plugins.symbolic_memory | 2) adding the state option ZERO_FILL_UNCONSTRAINED_{MEMORY,REGISTERS}, to make unknown regions hold null
WARNING | 2020-08-04 00:07:02,737 | angr.state_plugins.symbolic_memory | 3) adding the state option SYMBOL_FILL_UNCONSTRAINED_{MEMORY_REGISTERS}, to suppress these messages.
WARNING | 2020-08-04 00:07:02,737 | angr.state_plugins.symbolic_memory | Filling memory at 0x7fffffffffefff8 with 8 unconstrained bytes referenced from 0x401291 (secret_phase+0x4f in bomb (0x401291))
CRITICAL | 2020-08-04 00:07:02,812 | angr.sim_state | The name state.se is deprecated; please use state.solver.
22
We got the answer fairly quickly! If you had done Bomblab before and did the secret phase, you would remember that fun7
was a tricky little recursive function, but Angr just crushed it so easily!
Now, we get to the question of how the secret stage can actually be triggered. Unfortunately, I could not really find a way to do this symbolically with Angr due to the state explosion problem. This is because there is quite a big gap between when the input is set, and when the condition to go into the secret phase is satisfied.
To make it clearer about what I mean, I decided to just tell you how it works, because ultimately this is a series on learning about Angr and not learning how to reverse. Basically, read_line
keeps track of a global variable num_input_strings
which increments by 1 each time it is called. It then uses this as an index into a global buffer, and this buffer is used to store the input by the user for each stage. When num_input_strings
is 6 (i.e we cleared all stages) we perform the following check in phase_defused
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x00000000004015d8 <+20>: cmp DWORD PTR [rip+0x202181],0x6 # 0x603760 <num_input_strings>
0x00000000004015df <+27>: jne 0x40163f <phase_defused+123>
0x00000000004015e1 <+29>: lea r8,[rsp+0x10]
0x00000000004015e6 <+34>: lea rcx,[rsp+0xc]
0x00000000004015eb <+39>: lea rdx,[rsp+0x8]
0x00000000004015f0 <+44>: mov esi,0x402619
0x00000000004015f5 <+49>: mov edi,0x603870
0x00000000004015fa <+54>: call 0x400bf0 <__isoc99_sscanf@plt>
0x00000000004015ff <+59>: cmp eax,0x3
0x0000000000401602 <+62>: jne 0x401635 <phase_defused+113>
0x0000000000401604 <+64>: mov esi,0x402622
0x0000000000401609 <+69>: lea rdi,[rsp+0x10]
0x000000000040160e <+74>: call 0x401338 <strings_not_equal>
0x0000000000401613 <+79>: test eax,eax
0x0000000000401615 <+81>: jne 0x401635 <phase_defused+113>
It just happens that the global buffer used by Phase 4 is the one that is pointed to by the rdi
argument for sscanf
, 0x603870
. This is the most clear if you use dynamic analysis to inspect the value of the buffer after setting a breakpoint at this point, where you will see your Phase 4 input in the buffer.
The format string used for sscanf
is as follows:
1
2
gef➤ x/s 0x402619
0x402619: "%d %d %s"
So we only continue down if we extract all three tokens, and with the third token being “DrEvil”:
1
2
gef➤ x/s 0x402622
0x402622: "DrEvil"
This means that all we need to do to trigger the secret phase is to change our Phase 4 input from 7 0
to 7 0 DrEvil
!
Trying it out on the actual binary:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ ./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 DrEvil
So you got that one. Try this one.
9?>567
Good work! On to the next...
4 3 2 1 6 5
Curses, you've found the secret phase!
But finding it and solving it are quite different...
22
Wow! You've defused the secret stage!
Congratulations! You've defused the bomb!
Hooray, we did it!
Through this series, we learned about what symbolic execution is, how path explosion is the main pitfall for symbolic execution, and various strategies for combating it. I personally learned a lot by doing this and felt much more comfortable with Angr than when I initially started out. I hope that you found this useful, and as always feedback and comments are greatly appreciated!
Related Posts: