Skip to content

11 Angr Sim Scanf

This is another pretty short one. In this exercise we are hooking the scanf call so that we don't have to mess with the inputs.

We want our hook to accept the same input that scanf receives, so we can check that out.

Scanf Input

In our hook function we will want to make sure that we accept three inputs:

  1. The format string
  2. The address for the first input
  3. The address for the second input

As we build the hook function we will want to create two bitvectors to represent the input. Since the format is %u for both, it means the input will be 32 bits in length.

Finally, we will need to create references to our bitvectors so that we can refer to them outside of the hook function. To do that we will use the globals plugin.

Building the Script

We start off as normal.

path_to_binary = "./11_angr_sim_scanf"
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,
    },
)

Next we will create our hook function. As before, we need to create a class that inherits from SimProcedure. We then need a run method that will automatically be called. We use the information we gathered above to define the input for the function as well as the bitvector sizes.

class ReplacementScanf(angr.SimProcedure):
    # Finish the parameters to the scanf function. Hint: 'scanf("%u %u", ...)'.
    # (!)
    def run(self, format_string, scanf0_address, scanf1_address):
        scanf0 = claripy.BVS("scanf0", 32)
        scanf1 = claripy.BVS("scanf1", 32)

And then store the bitvectors in the appropriate place in memory.

        self.state.memory.store(
            scanf0_address, scanf0, endness=project.arch.memory_endness
        )
        self.state.memory.store(
            scanf1_address, scanf1, endness=project.arch.memory_endness
        )

Finally we create references to the bitvectors in the globals plugin so that we can retrieve them later.

        self.state.globals["solution0"] = scanf0
        self.state.globals["solution1"] = scanf1

With that done we hook the scanf function and explore.

scanf_symbol = "__isoc99_scanf"
project.hook_symbol(scanf_symbol, ReplacementScanf(), replace=True)

simulation = project.factory.simgr(initial_state)
simulation.explore(find=is_successful, avoid=should_abort)

We then then evaluate the solutions, print them and check them.

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

    # Grab whatever you set aside in the globals dict.
    stored_solutions0 = solution_state.globals["solution0"]
    stored_solutions1 = solution_state.globals["solution1"]

    # solution = ???
    solution0 = solution_state.solver.eval(stored_solutions0)
    solution1 = solution_state.solver.eval(stored_solutions1)
    solution = f"{solution0} {solution1}"

    print(solution)
    run_binary(solution, path_to_binary)

Final Script

import sys

import pwn
import angr
import claripy


def main():
    path_to_binary = "./11_angr_sim_scanf"
    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,
        },
    )

    class ReplacementScanf(angr.SimProcedure):
        def run(self, format_string, scanf0_address, scanf1_address):
            scanf0 = claripy.BVS("scanf0", 32)
            scanf1 = claripy.BVS("scanf1", 32)
            self.state.memory.store(
                scanf0_address, scanf0, endness=project.arch.memory_endness
            )
            self.state.memory.store(
                scanf1_address, scanf1, endness=project.arch.memory_endness
            )
            self.state.globals["solution0"] = scanf0
            self.state.globals["solution1"] = scanf1

    scanf_symbol = "__isoc99_scanf"
    project.hook_symbol(scanf_symbol, ReplacementScanf(), replace=True)

    simulation = project.factory.simgr(initial_state)
    simulation.explore(find=is_successful, avoid=should_abort)

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

        stored_solutions0 = solution_state.globals["solution0"]
        stored_solutions1 = solution_state.globals["solution1"]

        solution0 = solution_state.solver.eval(stored_solutions0)
        solution1 = solution_state.solver.eval(stored_solutions1)
        solution = f"{solution0} {solution1}"

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