# Imports
import angr
import claripy
We push the input values to the stack and then pop them right before the function exits. Since no value is pushed onto the stack during the execution of the function, we can safely manipulate the stack in the way we planned.
Load the binary.
project = angr.Project('bomb64')
Define the address for the start of the execution path.
# The address where the symbolic execution shall begin. It is right after parsing of six numbers taken as input.
addr_start = 0x400F0A
# The address of the return of phase_2.
# We end it there so as to dump the stack and retrieve values before the stack frame is discarded.
addr_target = 0x400F3C
# The address of the first instruction of the explode_bomb function, which is to be avoided.
addr_bomb = 0x40143A
Define a blank state for the simulation, a state with most of it's data uninitialized. Pass the address where the state is initialized, along with the user input to be given via standard input.
state = project.factory.blank_state(addr=addr_start)
Push the symbolic values to be input on the stack.
# From the stack trace show in the picture in the blog,
# we know that the function allocates 4bytes per value and merges two in memory.
# => For values entered to be 1, 2, 4 and 8,
# Stack representation: 0x200000001
# 0x800000004
# Thus, for 4 input values, two values appear on the stack.
# Therefore,
# Thus, we push three symbolic values which will appear on the stack for six values as input.
for i in range(3):
state.stack_push(claripy.BVS("num_{}".format(i), 32))
Create a simulation manager with this blank state that would help us manage the symbolic execution.
simgr = project.factory.simulation_manager(state)
We call the explore method of the simulation manager, tasked with finding an execution path that reaches the target address and avoids the address which explodes the bomb.
simgr.explore(find=addr_target, avoid=addr_bomb, enable_veritesting=True)
We dereference the execution path "found" by the simulation manager and dump the stack.
found = simgr.found[0]
answer = []
# Only 3 iterations since when we pushed the input,
# the values were merged in a way that two numbers were in the same block,
# at the end and the beginning.
# This can be seen when each popped value is printed.
for i in range(3):
curr = found.solver.eval(found.stack_pop())
print("Popped value: {0}".format(hex(curr)))
# Masking is done using bit-wise operators to split the values merged in the block.
lower_end = curr & 0xffffffff
print("Decimal number on the lower end: {0}".format(lower_end))
answer.append(str(lower_end))
higher_end = curr>>32 & 0xffffffff
answer.append(' ')
print("Decimal number on the higher end: {0}\n".format(higher_end))
answer.append(str(higher_end))
answer.append(' ')
# Removes the space in the end.
''.join(answer[:-1])
That's the number sequence to be input to pass the second phase.
Since the output we got was correct, we don't need to add that constraint on the first value.