Mapping Memory

In this tutorial, you will be guided through the steps to harness a snippet of binary code that requires memory, specifically a program stack. This time, we’ve got a total of three lines of assembly.

BITS 64;
; This function takes 7 64-bit arguments and returns the sum of the 1st (rdi), 3rd (rdx), 5th(r8), and 7th([rsp+8].
; 
        add     rdi, rdx
        lea     rax, [rdi+r8]
        add     rax, QWORD [rsp+8]

This example can be found in tests/square/square.amd64.s. You will need to run the following command to produce tests/square/square.amd64.bin:

cd smallworld/tests
make square/square.amd64.bin

As in the first tutorial, we will use basic_harness.py to get an initial look at what the harness requires:

$ python3 examples/basic_harness.py tests/stack/stack.amd64.bin
[+] seed=123456 digest of changes made to machine: 9fb43f6a9e58f4de95300b1d9bd9192a
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123457 digest of changes made to machine: 75958496f96ec41a114200a663eb7ea5
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123458 digest of changes made to machine: 9114b586cab688e3e2eaac7764d5ff3d
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123459 digest of changes made to machine: d8f683125ac66437da87e2a4147d92ef
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123460 digest of changes made to machine: d3ec6c1e68474fcf8988c62594e636a8
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123461 digest of changes made to machine: 9c8309440489f444ed253841cf9505d1
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123462 digest of changes made to machine: 8626e15c4f93a52b02b6fc07588ca5a9
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123463 digest of changes made to machine: 7378c46011e4c55bae506b6a74f819b5
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123464 digest of changes made to machine: 5accb97bcc9569d3998eedf946120964
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731
[+] seed=123465 digest of changes made to machine: af522c74f58a93272e6d4dcd4de77271
[!] emulation stopped - reason: Invalid memory read (UC_ERR_READ_UNMAPPED)
[+] captured trace of 3 instructions, res=TraceRes.ER_FAIL trace_digest=8b193141518456a42d3bf49a5ab2d731

This reports failures due to unmapped memory. If we look back at our assembly, this makes sense. The final instruction dereferences memory relative to the register rsp.

SmallWorld does not make any assumptions about what memory is available to the program. We have allocated code using Executable.from_filepath(), but basic_harness.py defines no other memory.

To successfully harness, we need to create an execution stack, and add it to our harness. Details of this interface can be found in Memory Objects.

# Stack needs the following parameters:
#
# - The target platform; determines a few behaviors.
# - The start address, here set to 0x2000
# - The size, here set to 0x4000
stack = smallworld.state.memory.stack.Stack.for_platform(platform, 0x2000, 0x4000)
machine.add(stack)

From the assembly, we know that our input is eight bytes wide, and starts eight bytes after the stack pointer. We need to push that value onto the stack, and set rsp appropriately:

# Push our argument value onto the stack
stack.push_integer(0x44444444, 8, None)

# Push a fake return value onto the stack,
# accounting for the 8-byte gap
stack.push_integer(0xFFFFFFFF, 8, "fake return address")

# Configure the stack pointer
sp = stack.get_pointer()
cpu.rsp.set(sp)

We can create the script tests/stack.amd64.py to perform these changes:

import logging

import smallworld

# Set up logging and hinting
smallworld.logging.setup_logging(level=logging.INFO)

# Define the platform
platform = smallworld.platforms.Platform(
    smallworld.platforms.Architecture.X86_64, smallworld.platforms.Byteorder.LITTLE
)

# Create a machine
machine = smallworld.state.Machine()

# Create a CPU
cpu = smallworld.state.cpus.CPU.for_platform(platform)
machine.add(cpu)

# Load and add code into the state
code = smallworld.state.memory.code.Executable.from_filepath(
    __file__.replace(".py", ".bin")
    .replace(".angr", "")
    .replace(".panda", "")
    .replace(".pcode", ""),
    address=0x1000,
)
machine.add(code)

# Create a stack and add it to the state
stack = smallworld.state.memory.stack.Stack.for_platform(platform, 0x2000, 0x4000)
machine.add(stack)

# Set the instruction pointer to the code entrypoint
cpu.rip.set(code.address)

# Initialize argument registers
cpu.rdi.set(0x11111111)
cpu.rdx.set(0x22222222)
cpu.r8.set(0x33333333)

# Push a return address and an extra argument onto the stack
stack.push_integer(0x44444444, 8, None)
stack.push_integer(0xFFFFFFFF, 8, "fake return address")
stack.write_bytes(
    0x2500, b"\xff\xff\xff\xff"
)  # ensure writing below sp won't modify sp

# Configure the stack pointer
rsp = stack.get_pointer()
cpu.rsp.set(rsp)

# Emulate
emulator = smallworld.emulators.UnicornEmulator(platform)
emulator.add_exit_point(cpu.rip.get() + code.get_capacity())
final_machine = machine.emulate(emulator)

# read out the final state
cpu = final_machine.get_cpu()
print(cpu.eax)

This harness doesn’t take arguments; it assigns fixed values 0x11111111, 0x22222222, 0x33333333, and 0x44444444 to the relevant registers and memory.

Here is what running the new harness looks like:

$ python3 stack.amd64.py
[+] starting emulation at 0x1000
[+] emulation complete
Reg(eax,4)=0xaaaaaaaa

The sum of the four input numbers is 0xaaaaaaaa, so we harnessed stack.amd64.bin completely.