Programmers write functions all the time, but these days, they rarely rely solely on their own code. In fact, a vast number of functions in most programs are reused from other developers’ work. This is thanks to standard libraries, third-party libraries, and frameworks. These resources often come with clean documentation, helpful README files, and instructions on how to build, install, and use them. They’re incredibly useful and let developers get their projects off the ground quickly.

But have you ever wondered how you could call a function embedded in someone else’s program? For example, what if you wanted to move your character in a game, draw a new GUI object, or print something to the in-game chat—directly from your own executable? Since programs and games are often closed-source, they don’t provide documentation listing their functions. Fortunately, this is where reverse engineering steps in to save the day.

In a previous post we demonstrated how to find a function responsible for parsing commands and, as a side effect, identified a function that prints text to the console. If you haven’t read that post yet, it might be a good idea to check it out before proceeding with this one.

Understanding Function Calls and Calling Conventions

When calling a function in your own program, as a programmer, you need to know a few details:

  • The name of the function
  • The parameters the function accepts
  • The return type (optional but helpful)

However, when dealing with a third-party application, things get more interesting. During compilation, information such as types and function names is often lost (assuming no debug symbols are present and the program is compiled directly to assembly). This makes it impossible to call the function by name. Instead, you will need a slightly different set od details:

  • The function’s location (its address)
  • The parameters the function accepts and the calling convention
  • The return type (and the calling convention to know where the return value is located)

Two question might come to mind: what is calling convention, and why it is enough to use just the function name in your own code, without needing the function’s location?

When you call a function that accepts parameters, there must be an agreed-upon method for the caller to pass those parameters to the callee. This agreement is named the calling convention. For example, parameters can be passed by pushing them onto the stack, either from left to right or right to left. Another question arises: who cleans the stack after the function executes — the caller or the callee? Parameters may also be passed through registers, depending on the strategy. Similarly, the function’s return value must have a designated location, such as a specific register or memory location.

Respecting the calling convention is critical so the callee knows how to interpret the parameters passed by the caller and where to place the return value. When writing your own application, you don’t usually need to worry about this, because the compiler takes care of it. It ensures that the caller adheres to the calling convention expected by the callee.

Similarly, you don’t need to know the function’s address when calling it from your own program because the function name is automatically resolved to its address by the linker.

Obtaining required information

First, we need to gather information about the function we want to call. The most important piece is the function’s location. Whether it is a function to attack a monster, collect an item or print a message to the console, the first step is to pinpoint place in the assembly code.

As mentioned earlier, in this post, we are focusing on the function that prints a message to the console. The steps to locate this function were already covered in previous post so we will skip them here.

As a side note, for this particular game the address of this function will remain the same no matter how many time you restart the game (assuming you are using the same game binary). This is because the game is always loaded at the same address by the operating system — 0x00400000, which is the default base address for x86 Windows executables. If you are curious why this happens, you can learn more here. For modern applications, however, a mechanism known as ASLR (Address Space Layout Randomization) is used, which loads the program image at a different base address on each run or system restart. This mechanism won’t be covered here.

The prologue of the function we are interested in looks like this:

Print to console assembly

Now we need to determine the number of parameters the function accepts, their types, and calling convention it uses. Thanks to modern decompilers like Ghidra or IDA, this is relatively straightforward for less complex functions. Take a look at the decompilation output:

Print to console

As you can see, Ghidra deduced that the function accepts two parameters (a this pointer and a pointer of undefined type) and uses the __thiscall calling convention. Additionally, the funtion doesn’t return anything, so the return type is void. While modern tools are fantastic for simplifying our work, this is an educational post, so it’s worthwhile to understand how to reach similar conclusions manually.

To start, it’s helpful to examine how the function is called: Assembly code to call the print to console There are multiple calls to this function, but they mostly follow the same pattern. A PUSH instruction places a pointer to the text to be printed onto the stack, and an address (0x0789d58) is moved into the ECX register just before the call. After the function executes, the caller doesn’t clean the stack, suggesting the callee handles it.

From this, we can deduce that the function accepts two parameters: the message to print (pushed onto the stack) and a mysterious address (placed in the ECX register). Let’s explore the most common x86 calling conventions.

thiscall:

On the Microsoft Visual C++ compiler, the this pointer is passed in ECX and it is the callee that cleans the stack, mirroring the stdcall convention used in C for this compiler and in Windows API functions. When functions use a variable number of arguments, it is the caller that cleans the stack (cf. cdecl).

and for stdcall:

The stdcall calling convention is a variation on the Pascal calling convention in which the callee is responsible for cleaning up the stack, but the parameters are pushed onto the stack in right-to-left order, as in the _cdecl calling convention. Registers EAX, ECX, and EDX are designated for use within the function. Return values are stored in the EAX register.

It seems that Ghidra was indeed correct in deducing the calling convention! One last thing to verify is whether the callee cleans the stack.

End of the callee

Looking at the last instruction of the callee, we see ret 4. This indicates that after executing this instruction, 4 bytes will be added to the stack pointer, effectively cleaning up the 4 bytes pushed by the caller.

You might be wondering what the this parameter is and why it’s passed to the function as a parameter. If you’ve ever used a C-like language that supports object-oriented programming, you’re probably familiar with the concept. Here’s a quick recap:

#include <iostream>

class Cat
{
private:
	int age;
public:
	Cat(int _age) : age{_age} {}
	
	int getAge()
	{
		return this->age;
	}
};

int main() {
	Cat cat{15};
	
	std::cout << cat.getAge();
	return 0;
}

This is a simple C++ program, that constructs a Cat object, sets its age to 15, and then print the age to the standard output. Nothing too fancy. Take a look at the this->age expression. In C++, you can use this inside a class member function to get a pointer to the current class instance, even though it is not explicitly passed as a parameter. This is thanks to the magic of the compiler. On the assembly level the getAge function might look like this:

int getAge(Cat* this)
{
	return this->age;
}

And the invocation of the function would look like this:

std::cout << Cat::getAge(&cat);

As you can see, even though the this parameter isn’t explicitly passed in your code, the compiler automatically includes it. That’s why on the assembly level, it looks like the parameter is passed explicitly.

Call the function

Armed with the information we gathered earlier, we can finally attempt to call the function. To do this, we will create our own .dll file, inject it into game process, and call the function that prints something to the console. Explaining injection in detail deserves its own post, so for now, we will use existing tools. For now, all you need to know is that once you inject a .dll into a process, it becomes a part of it. This means the .dll has access to the entire process memory, all the program functions, and more - just as if the game were your own program. If you are interested in learning more about injection, you can read about it here.

For this example, we will use the Microsoft Visual Studio IDE and its compiler to create the project. I selected the DLL template as a base. Here is the complete code:

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

typedef void(__thiscall* Print_Function)(void* this_ptr, const char* msg);
Print_Function print_to_console = (Print_Function)0x0431d40;
const char* msg = "Visit https://letsreverse.net/";

void main_func()
{
    print_to_console((void*)0x0789D58, msg);
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        main_func();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Once the .dll is injected, the DllMain function is triggered with the DLL_PROCESS_ATTACH reason. In this entry point, we invoke main_func, which makes a call to the game’s function. It uses the this pointer we identified earlier (0x0789D58) and passes msg, the message we want to print to the console.

The two lines that might seem mysterious at first:

typedef void(__thiscall* Print_Function)(void* this_ptr, const char* msg);
Print_Function print_to_console = (Print_Function)0x0431d40;

are actually straightforward. The first line creates a typedef for a function pointer named Print_Function. This function accepts two parameters: void* and const char*. The convention is __thiscall, which instructs the compiler to place the first parameter this to ECX register and push the second parameter msg onto the stack. Additionally, the compiler knows that it doesn’t need to clean up the stack afterward, as this will be handled by the callee. You can find more details about all calling conventions supported by Microsoft’s compiler here.

The second line actually tells the compiler that the print_to_console function is located at address 0x0431d40. We know this from the first image attached to this post.

Testing the DLL

Finally, we can test our .dll. For this, we will use an injector tool Cheat Engine. It’s free and open-source, but be cautious during installation — avoid agreeing to any adware. If you’re unfamiliar with the process of injecting a .dll, check out this short video.

Result of our work

And there you have it! Our .dll works perfectly, and we can see the desired string displayed in the game console.

In a future post, we’ll explore the concept of function hooking—modifying existing game functions. This will allow us to alter the function that parses user commands, enabling us to add custom commands and execute specific actions when the user types the appropriate input into the console.