Skip to content

03 Angr Symbolic Registers

Analyzing the Binary

This time we see that the main calls a function get_user_input

Main

The get_user_input accepts three hex values as input

Get User Input

It then runs the inputs through three complex function calls, and then verifies that the output of those are all equal to zero. If that succeeds it prints Good Job..

So, our script will now have to provide three inputs, but there is a problem. Angr does not support reading multiple inputs in one call with scanf. The challenge is to figure out how to bypass that part of the code and insert our symbolic values manually.

Building the Script

The first step is to figure out where the simulation manager should start. Unlike the previous scripts we will not be using the entry_state, instead we will use blank_state and provide it the address where we want it to start. This should be after the call to scanf. Looking through the disassembly we see that the three inputs are saved to the registers eax (1), ebx (2), and edx (3).

Saving Input to Registers

We then have some cleanup of the stack and finally a return to the main function. If we start right after the three assignments, the solution will not be found because there is a stack check at the end of the function.

Stack Check

We have two options: we either need to build the stack up so that the check will pass, or we pick an entry spot after this call. I chose the latter option. Going back to the main function, it looks like we can jump in right after the get_user_input call at address 0x8048980.

Entry Address

Finally we have our starting address and can set up the state.

path_to_binary = "./03_angr_symbolic_registers"
project = angr.Project(path_to_binary)
start_address = 0x08048980
initial_state = project.factory.blank_state(
    addr=start_address,
    add_options={
        angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
        angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
    },
)

But, we need to set the values of the three registers to our symbolic values. To do that we are going to use claripy. Here we are defining three symbolic bitvectors, each 32 bits.

password_size_in_bits = 32
password0 = claripy.BVS("password0", password_size_in_bits)
password1 = claripy.BVS("password1", password_size_in_bits)
password2 = claripy.BVS("password2", password_size_in_bits)

Now that we have the bitvectors we can set the registers.

initial_state.regs.eax = password0
initial_state.regs.ebx = password1
initial_state.regs.edx = password2

Finally we can create the simulation manager and tell it to explore. Here I used the addresses, but in the final code below I used the callback functions. They both seem to run about the same speed.

simulation = project.factory.simgr(initial_state)
simulation.explore(find=0x80489EE, avoid=0x80489DC)

Final Script

import sys

import angr
import claripy
import pwn


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 main():
    path_to_binary = "./03_angr_symbolic_registers"
    project = angr.Project(path_to_binary)

    start_address = 0x08048980
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={
            angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
            angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
        },
    )
    password_size_in_bits = 32
    password0 = claripy.BVS("password0", password_size_in_bits)
    password1 = claripy.BVS("password1", password_size_in_bits)
    password2 = claripy.BVS("password2", password_size_in_bits)

    initial_state.regs.eax = password0
    initial_state.regs.ebx = password1
    initial_state.regs.edx = password2

    simulation = project.factory.simgr(initial_state)

    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

    simulation.explore(find=is_successful, avoid=should_abort)

    if simulation.found:
        solution_state = simulation.found[0]

        solution0 = solution_state.solver.eval(password0)
        solution1 = solution_state.solver.eval(password1)
        solution2 = solution_state.solver.eval(password2)

        solution = " ".join(
            map("{:x}".format, [solution0, solution1, solution2])
        )
        run_binary(solution, path_to_binary)
    else:
        raise Exception("Could not find the solution")


if __name__ == "__main__":
    main()
Back to top