Function Hooking with PE Imports

In this tutorial, you will be guided through the steps to model a function, and hook it using the linker metadata found in a PE32+ file.

Note

Details on actually loading a PE file can be found in this tutorial. It’s strongly recommended you complete that one first.

Consider the example tests/pe/pe.pe.c:

#include <stdio.h>

int main() {
    // Unusual number to help me find main()
    // Getting debug info out of PEs is a bit of a chore.
    int out = 0xc001d00d;
    puts("Hello, world!\n");
    return out;
}

We already explored actually loading the PE file it generates in this tutorial, but it invokes the library function puts, which will need extra handling.

You can build this into pe.amd64.pe using the following commands:

cd smallworld/tests
make pe/pe.amd64.pe

Let’s take a look at the PE metadata, specifically regarding puts:

$ objdump -x pe.amd64.pe | grep -B 10 'puts'

 00025bec	00025d18 00000000 00000000 0002623b 00025e90

	DLL Name: api-ms-win-crt-stdio-l1-1-0.dll
	vma:     Ordinal  Hint  Member-Name  Bound-To
	00025e90  <none>  0000  __acrt_iob_func
	00025e98  <none>  0000  __p__commode
	00025ea0  <none>  0000  __p__fmode
	00025ea8  <none>  0000  __stdio_common_vfprintf
	00025eb0  <none>  0000  fwrite
	00025eb8  <none>  0000  puts

The function is imported from api-ms-win-crt-stdio-l1-1-0.dll. We could load and harness an entire other library just to provide puts, but we’re not that interested in exercising the library. Let’s use a model instead.

To do that, we will need the following:

  • An execution stack, to support the call opcode, as well as some local variables.

  • A model of puts.

Adding a Stack

Setting up a stack is covered in this tutorial.

Let’s take a look at the disassembly for main:

$ objdump -d pe.amd64.pe | grep -A 10 '140001000:'
   140001000:	55                   	push   %rbp
   140001001:	48 83 ec 30          	sub    $0x30,%rsp
   140001005:	48 8d 6c 24 30       	lea    0x30(%rsp),%rbp
   14000100a:	e8 11 06 00 00       	call   0x140001620
   14000100f:	c7 45 fc 00 00 00 00 	movl   $0x0,-0x4(%rbp)
   140001016:	c7 45 f8 0d d0 01 c0 	movl   $0xc001d00d,-0x8(%rbp)
   14000101d:	48 8d 0d dc 0f 02 00 	lea    0x20fdc(%rip),%rcx        # 0x140022000
   140001024:	e8 a7 0d 02 00       	call   0x140021dd0
   140001029:	8b 45 f8             	mov    -0x8(%rbp),%eax
   14000102c:	48 83 c4 30          	add    $0x30,%rsp
   140001030:	5d                   	pop    %rbp

The concerning lines are the following:

mov     %ecx,0x10(%rbp)
mov     %rdx,0x18(%rbp)

These are accessing memory above the starting stack pointer, so we’ll need to allocate a little extra space:

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

# Push some padding onto the stack
stack.push_integer(0xFFFFFFFF, 8, "Padding")
stack.push_integer(0xFFFFFFFF, 8, "Padding")

# Push a return address onto the stack
stack.push_integer(0x7FFFFFF8, 8, "fake return address")

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

Adding a Puts Model

Let’s write our own puts model. We want to read a null-terminated string out of memory at the address specified at the argument register, and print it to stdout.

Let’s be a little careful. If someone feeds us an unterminated string, our model could fail in a variety of ways:

  • We could get an “unmapped read” error if we go off the edge of mapped memory

  • We could get a “symbolic value” error if we’re using angr and read past the edge of initialized memory.

The first case has fairly good error reporting. Let’s add a little more introspection for the second case:

# Define a puts model
class PutsModel(smallworld.state.models.Model):
    name = "puts"
    platform = platform
    abi = smallworld.platforms.ABI.NONE

    def model(self, emulator: smallworld.emulators.Emulator) -> None:
        # Reading a block of memory from angr will fail,
        # since values beyond the string buffer's bounds
        # are guaranteed to be symbolic.
        #
        # Thus, we must step one byte at a time.
        s = emulator.read_register("rcx")
        v = b""
        try:
            b = emulator.read_memory_content(s, 1)
        except smallworld.exceptions.SymbolicValueError:
            b = None
        while b is not None and b != b"\x00":
            v = v + b
            s = s + 1
            try:
                b = emulator.read_memory_content(s, 1)
            except smallworld.exceptions.SymbolicValueError:
                b = None
        if b is None:
            raise smallworld.exceptions.SymbolicValueError(f"Symbolic byte at {hex(s)}")
        print(v)

Note that, unlike the other tutorials that dealt with System-V compatible binaries, we reference the register rcx for our argument, following the Windows cdecl ABI.

Linking the Puts Model

We don’t have a fixed address for puts. Normally, that would be up to the program loader. Let’s just make up a nice round number.

# Configure the puts model at an arbitrary address
puts = PutsModel(code.address + 0x10000)
# Add the puts model to the machine
machine.add(puts)

We also need to do the program loader’s job and update our PE with the address of puts:

# Relocate puts
code.update_import("api-ms-win-crt-stdio-l1-1-0.dll", "puts", puts._address)

Putting it All Together

Combined, this harness can be found in the script tests/pe/pe.amd64.py

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
filename = (
    __file__.replace(".py", ".pe")
    .replace(".angr", "")
    .replace(".panda", "")
    .replace(".pcode", "")
)
with open(filename, "rb") as f:
    code = smallworld.state.memory.code.Executable.from_pe(
        f, platform=platform, address=0x10000
    )
    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)

stack.push_integer(0x10101010, 8, None)
cpu.rsp.set(stack.get_pointer())


# Configure _main model
class InitModel(smallworld.state.models.Model):
    name = "__main"
    platform = platform
    abi = smallworld.platforms.ABI.NONE

    def model(self, emulator: smallworld.emulators.Emulator) -> None:
        # Return
        pass


init = InitModel(code.address + 0x1620)
machine.add(init)


# Configure puts model
class PutsModel(smallworld.state.models.Model):
    name = "puts"
    platform = platform
    abi = smallworld.platforms.ABI.NONE

    def model(self, emulator: smallworld.emulators.Emulator) -> None:
        # Reading a block of memory from angr will fail,
        # since values beyond the string buffer's bounds
        # are guaranteed to be symbolic.
        #
        # Thus, we must step one byte at a time.
        s = emulator.read_register("rcx")
        v = b""
        try:
            b = emulator.read_memory_content(s, 1)
        except smallworld.exceptions.SymbolicValueError:
            b = None
        while b is not None and b != b"\x00":
            v = v + b
            s = s + 1
            try:
                b = emulator.read_memory_content(s, 1)
            except smallworld.exceptions.SymbolicValueError:
                b = None
        if b is None:
            raise smallworld.exceptions.SymbolicValueError(f"Symbolic byte at {hex(s)}")
        print(v)


puts = PutsModel(0x10000000)
code.update_import("api-ms-win-crt-stdio-l1-1-0.dll", "puts", puts._address)
machine.add(puts)

# Set entrypoint to "main"
cpu.rip.set(code.address + 0x1000)

# Emulate
emulator = smallworld.emulators.UnicornEmulator(platform)
emulator.add_exit_point(code.address + 0x1031)
final_machine = machine.emulate(emulator)
# for m in machine.step(emulator):
#    pass

This harness should print Hello, world!\n to the console.

Here is what running it looks like:

$ python3 pe.amd64.py
[+] starting emulation at 0x11000
[+] emulation complete
puts IAT at 25eb8 or 260ce
b'Hello, world!\n'

We do in fact see, Hello, world!\n printed to the console, so we harnessed pe.amd64.pe successfully.