Skip to content

09 Angr Hooks

Analyzing the Binary

Lets take a look at main.

09 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.

09 complex

Nothing too special here. Lets check out the check_equals_ function.

09 check equals

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.

09 check equals

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)
Back to top