09 Angr Hooks¶
Analyzing the Binary¶
Lets take a look at main.
Here is what I see:
- A spot in memory is cleared
- We are asked to enter a 16 character long string which is stored in memory at
0x804a054
- We then loop through the string and call
complex_function
on each character - We then call a function
check_equals_
- We then call complex function to loop over the predefined string providing it with a different
arg2
than the first time. - The program then reads in 16 more characters overwriting the first 16 characters
- It compares the new string to the encrypted pre-defined password string
- If the comparison is equal we get "Good Job."
Looking at complex_function
.
Nothing too special here. Lets check out the check_equals_
function.
The check_equals_
function will loop over each character and if the character equals the corresponding character in the pre-defined password a number is added to the total, if the total matches arg2
then the return is True
I think that covers everything. Let's start writing the script.
Building the Script¶
Because the requests for input are basic, we can use the entry state again! Lets build our project and initial state.
path_to_binary = "./09_angr_hooks"
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state(
add_options={
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
},
)
Because the check_equals_
will cause the branch number to explode, we will need to hook that portion of the function. To do that we will need to know the instruction address and then how many bytes of instructions to skip.
Let's look at the instructions that call the check_equals_
function.
This means that I want to set the start address on the sub
call at 0x80486a9
and I want it to resume regular function at 0x80486bb
. So the length will be 18 bytes.
check_equals_called_address = 0x80486A9
instruction_to_skip_length = 18
We will define a function that will replace the hooked portion of the program by using a decorator.
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
Next we will need to set the value of eax
replacing that portion of the check_equals_
function call. To do that we will use a claripy if statement that will set eax
to 1 if the result is true and 0 if false.
user_input_buffer_address = 0x804A054
user_input_buffer_length = 16
user_input_string = state.memory.load(
user_input_buffer_address, user_input_buffer_length
)
check_against_string = "XYMKBKUHNIQYNQXE".encode()
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32),
)
Finally we set up our simulation manager and let it go.
simulation = project.factory.simgr(initial_state)
simulation.explore(find=is_successful, avoid=should_abort)
Once done we can print out the solution.
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno()).decode()
run_binary(solution, path_to_binary)
else:
raise Exception("Could not find the solution")
Final Script¶
import angr
import claripy
import sys
import pwn
def main(argv):
path_to_binary = "./09_angr_hooks"
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state(
add_options={
angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
},
)
check_equals_called_address = 0x80486A9
instruction_to_skip_length = 18
@project.hook(check_equals_called_address, length=instruction_to_skip_length)
def skip_check_equals_(state):
user_input_buffer_address = 0x804A054
user_input_buffer_length = 16
user_input_string = state.memory.load(
user_input_buffer_address, user_input_buffer_length
)
check_against_string = "XYMKBKUHNIQYNQXE".encode()
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32),
claripy.BVV(0, 32),
)
simulation = project.factory.simgr(initial_state)
simulation.explore(find=is_successful, avoid=should_abort)
if simulation.found:
solution_state = simulation.found[0]
solution = solution_state.posix.dumps(sys.stdin.fileno()).decode()
run_binary(solution, path_to_binary)
else:
raise Exception("Could not find the solution")
def run_binary(solution, path_to_binary):
if type(solution) == str:
solution = bytes(solution, "utf-8")
print(f"[+] Solution found: {solution.decode()}")
print(" [|] Running binary")
elf = pwn.ELF(path_to_binary, checksec=False)
pty = pwn.process.PTY
io = elf.process(stdin=pty, stdout=pty, level="warn")
io.recvuntil(b":")
io.sendline(solution)
output = io.recvline().decode().splitlines()[0].strip()
print(f" [+] Output: {output}")
def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Good Job." in stdout_output
def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b"Try again." in stdout_output
if __name__ == "__main__":
main(sys.argv)