Skip to content

06 Angr Symbolic Dynamic Memory

Analyzing the Binary

Lets take a look at the main function.

Analyzing the Binary

In 05 we were able to overwrite the memory directly because we knew the address. We can't do that this time because the program is using malloc. Which allocates memory and then returns a pointer: man.

We can't overwrite the addresses in memory, but we can overwrite the pointers. What we will have to do is write our bitvectors to an open memory position and then overwrite buffer0 and buffer1 with the addresses to our input.

Building the Script

Nothing new here.

path_to_binary = "./06_angr_symbolic_dynamic_memory"
project = angr.Project(path_to_binary)

start_address = 0x08048699
initial_state = project.factory.blank_state(
    addr=start_address,
    add_options={
        angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
        angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
    },
)

Now lets create our two bitvectors.

input_length_in_bits = 8 * 8
password0 = claripy.BVS("password0", input_length_in_bits)
password1 = claripy.BVS("password1", input_length_in_bits)

We need to determine four addresses. The addresses for buffer0 and buffer1, also the addresses where we are going to store our input. I don't know of a good strategy for picking an unused portion of memory... would love to hear if there is a best practice for this. Until I have a better way, I just go to an unused portion and call it good. For this I decided to use this region of memory:

Memory Alloc

With the location decided, we define our four addresses. Then store the bitvectors to the memory locations, then store pointers into the two buffers. Instead of putting in the endianess myself, I will rely on Angr to provide that info.

mem_address0 = 0x804D020
mem_address1 = 0x804D030
buffer0_addr = 0xABCC8A4
buffer1_addr = 0xABCC8AC

initial_state.memory.store(mem_address0, password0)
initial_state.memory.store(mem_address1, password1)

initial_state.memory.store(buffer0_addr, mem_address0, 8, endness=project.arch.memory_endness)
initial_state.memory.store(buffer1_addr, mem_address1, 8, endness=project.arch.memory_endness)

Create the simulation manager and let it go, and we are done with another one!

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(argv):
    path_to_binary = "./06_angr_symbolic_dynamic_memory"
    project = angr.Project(path_to_binary)

    start_address = 0x08048699
    initial_state = project.factory.blank_state(
        addr=start_address,
        add_options={
            angr.options.SYMBOL_FILL_UNCONSTRAINED_MEMORY,
            angr.options.SYMBOL_FILL_UNCONSTRAINED_REGISTERS,
        },
    )

    input_length_in_bits = 8 * 8
    password0 = claripy.BVS("password0", input_length_in_bits)
    password1 = claripy.BVS("password1", input_length_in_bits)

    mem_address0 = 0x804D020
    mem_address1 = 0x804D030
    buffer0_addr = 0xABCC8A4
    buffer1_addr = 0xABCC8AC
    initial_state.memory.store(mem_address0, password0)
    initial_state.memory.store(mem_address1, password1)
    initial_state.memory.store(buffer0_addr, mem_address0, 8, endness=project.arch.memory_endness)
    initial_state.memory.store(buffer1_addr, mem_address1, 8, endness=project.arch.memory_endness)

    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, cast_to=bytes).decode()
        solution1 = solution_state.solver.eval(password1, cast_to=bytes).decode()
        solution = " ".join([solution0, solution1])

        run_binary(solution, path_to_binary)
    else:
        raise Exception("Could not find the solution")


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