Reversing by example: API hooking — Nim, Ghidra & MinHook

Unrelated picture of a dragon
Photo by Tarik Haiga on Unsplash

Reverse engineering is a topic I find myself coming back to every now and then, usually completely forgetting everything I’d known! I want to demystify some of the techniques commonly used to reverse and modify applications for my future self as well as other beginners.

In this article, I want to focus on a demonstrating the concept of hooking an external function from within an injected DLL, and modifying its behaviour (a method which is quite popular with game modders, but has other use cases too!).

This will be presented as a step by step guide, by example. If you’d like to follow along, you’ll need a copy of Ghidra, Nim, and Visual C++. If you’d prefer to use a different compiled language, the implementation should be easily transferable.

First step: Writing a simple target application

Scenario — This is the real world application we’d like to modify the behaviour of. Except for demo purposes, we’re writing our own, and then pretending we don’t have access to the source code!

I’ve written the following C++ source to set this scenario up — I’m using C++ over Nim for this part, as the resulting binary will be easier to reason about in Ghidra, and probably closer to what you’d come across in the wild.

#include <iostream>
#include <array>
#include <sstream>

#include "windows.h"

const char* InjectionDll = "hook.dll";

__declspec(noinline) bool is_valid_code(int code)
{
std::array<int, 3> valid_codes = {1234, 4321, 8888};
for (const auto& c : valid_codes)
if (c == code)
return true;

return false;
}

int main()
{
auto dll = LoadLibraryA(InjectionDll);
if (!dll)
std::cout << "failed to load " << InjectionDll << std::endl;

std::string buffer;
int code;
while (true)
{
std::cout << "enter code to continue: ";

std::getline(std::cin, buffer);
auto ss = std::istringstream(buffer);

ss >> code;
if (is_valid_code(code))
break;

std::cout << "Wrong code :(" << std::endl;
}

std::cout << "Correct code!" << '\n'
<< "Press return to exit... ";
std::getline(std::cin, buffer);
}

There’s a couple of things going on here. Firstly we have the call to LoadLibraryA — Usually the target application won’t helpfully inject your augmented code but for the sake of avoiding the topic of injection, this one does!

Next, we prompt the user to enter a code — If they enter the correct code, they gain access to the application. Otherwise, we continue to prompt the user. The intention of this scenario is to use is_valid_code as a target function for the interception demonstration.

An important thing to note here, is that in order to intercept is_valid_code we must be able to guarantee the code generated is call'd into, instead of being inlined directly into main. I’ve used the MSVC specific extension __declspec(noinline) to enforce this.

Example program output
Example program output
Example output so far…

Next: Analysing the application with Ghidra

Scenario — The source code to target.exe has been lost. In order to modify the application, we’re going to write a DLL to hook and modify the behaviour of the is_valid_code function.

In order to achieve this, we need to understand a bit about the compiled application. Namely, where we can expect to find the function in memory at runtime. Enter Ghidra.

Ghidra (for anyone curious) is a reverse engineering toolset similar to IDA, and Cutter. It includes a dissassembler/decompiler and supports a wide array of architectures. It’s also free, being open sourced by the NSA in 2019.

To get started with Ghidra we’ll need to create a new project. Ghidra projects support multiple files, but we’ll be using just the one for target.exe — We can import this file into our project and see that Ghidra recognizes it as a 64-bit PE file.

Importing target.exe

Now we can open target.exe with the default analysis tool through the context menu of the imported file (right click). Ghidra will ask if it should analyze the application, so let’s do that. Using the default analysis options will be fine for this.

Ghidra main tool window

Finding where the function we’re interested in is can be tricky, but we’re making it simple for ourselves here. We can see that the string “Wrong code” appears in the output of our test application. Ghidra is able to recognize some of the strings in our application — This is available under the “Window” menu as “Defined Strings”.

Familiar strings

A handy functionality Ghidra includes for the strings it finds, is both the ability to jump to their location within the code listing, and to locations that reference that string. In this case we’re able to jump to location FUN_140001230:14000147 — Let’s take a look.

Finding where our string is referenced

Jumps us to…

An interesting function call, followed by a jump!

There’s a few things to unpack here. Firstly, the call to a Visual C++ imported function. We can infer from the name this might have something to do with the input stream — ie. the code we enter.

Next is a CALLto an unknown function, followed byTEST AL, AL — The ALregister is the least significant byte of the RAX/EAXregisters. This is where we’d expect to find the result of a boolean return value (ie. from FUN_1400011c0).

Following this, if the TESTresult of AL & AL did not set the zero flag (the return value was non-zero), then we jump to LAB_1400014e9. From here there’s no branching before we reach the the “Correct code” output, so we can be reasonably certain that we’ve found out target function.

We should investigate FUN_1400011c0 further, but for practical purposes we know that if we can make this function return true, we can enter any code to jump to the “Correct code” section.

There’s a couple of things to note down before we move on. The address of the function we’re going to hook (1400011c0). And the base address of the executable, which we can find using Ghidras Python console (accessed through the Windows menu) using currentProgram.getImageBase().

Base address

Now we’re ready to move on to implementing our hook!

Writing a DLL to modify the application behaviour

Scenario — We know where we can find our target function in memory. We know how it affects the application behaviour when we enter the code. And we can use this knowledge to implement a DLL that can modifiy that behaviour.

API hooking is a technique that involves patching the first several bytes of a function in memory with a jump to user defined code, while still retaining the ability to call the original function if needed. There are a few libraries that can help us with this — and they’ll handle the more complicated part of this process (ie. disassembling and relocating the original functionality).

I’ll be using MinHook (codeproject article here) for this, as it has a seamless Nim wrapper that’s easy to use. Let’s start by installing this, along with one other dependency we’ll be using to help write Windows API code.

nimble install winim
nimble install minhook

And finally, the implementation of the DLL — Let’s look at the code first. I’ll explain what’s going on next.

import winim/lean
import minhook
proc NimMain() {.cdecl, importc.}proc modifiedIsValidCode(_: int): bool = trueproc DllMain(_: HINSTANCE, reason: DWORD, _: LPVOID): BOOL {.stdcall, exportc, dynlib.} =
NimMain()
if reason == DLL_PROCESS_ATTACH:
var baseAddress = cast[int](GetModuleHandleA("target.exe"));
var isValidCode = cast[pointer](baseAddress + 0x11C0);
assert createHook(isValidCode, modifiedIsValidCode, nil) == mhOk
assert enableHook(isValidCode) == mhOk
return true

The first curious thing here is the import definition of NimMain — This is used to start up Nim’s garbage collector. This isn’t normally something we have to think about, but in this case we want to define DllMain specifically, and tell the Nim compiler not to include it’s own definition of main.

Next we have the definition for modifiedIsValidCode. This will be the implementation we will use to replace the function we found in Ghidra. Whatever code is passed in, we don’t care, we just accept it by returning true.

Now we get to the juicy part — installing the function hook. But, we need to understand that the memory addresses we’ve noted down (from Ghidra) are not guarenteed to be the same memory addresses we’ll find our function at, due to ASLR.

Resolving this problem is simple: we subtract the base address (0x140000000) from the address of the function (0x1400011c0). This gives us the offset from the base address (0x11c0). Now all we have to do is find the actual base address (via GetModuleHandleA), and then we can calculate the current address of function (baseAddress + 0x11c0).

Once we have a pointer to the current location of the function we want to hook, we can let MinHook do it’s magic! createHook takes three parameters: the pointer to the function we want to hook, the function we want to call back to, and a function pointer that will point to the original reassembled function, if we need to call into it (or nil).

Let’s compile this and see if we can get past the application prompt now!

nim c --app=lib --cpu=amd64 --nomain -o:./hook.dll hook.nim
Success!

Wrapping up

Hopefully this was a useful demonstration on how to make use of API hooking, this is definitely something I struggled quite a bit with as a beginner!

I should note that we could accomplish this goal much more easily by patching rather than hooking, but that’s something I’d like to look at in another article. I’ve also skimmed over the whole concept of DLL injection — another one for another time.

And that’s all I have for now! Suggestions for improvements are very welcome, I’m sure I could make a few things clearer.

Time to spam links!

Photo by Erik Mclean on Unsplash

Developer from the UK. I like making things. Video games, SRE, Retro tech.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store