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
callopcode, 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.