How to call 64-bit functions externally using C#

A few years ago I looked into the possibility of calling functions from an external process, and turns out you can do that so I did some research and found some code which was later used in one of my old projects that did stuff with 32-bit processes. I decided to go back and look at that old project and see if I can do the same with 64-bit processes.

Well turns out that calling 64-bit functions externally isn’t any different than what you would do with 32-bit the only thing you need is for the external process to be running in 64-bit mode and of course use 64-bit instructions.

For our demonstration we will compile a 64-bit process that will have a function that is going to get called externally from our external C# function, so here is a simple program I wrote:

// ConsoleApplication2.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>

bool wasCalled = false;

void __fastcall my_function_to_call() {
	std::cout << "function called" << std::endl;
	wasCalled = true;
}

int main()
{
	printf("my_function_to_call address: %p\n", my_function_to_call); // i love using printf and std::cout in the same function!
	while (!wasCalled) {

	}
	int number;
	std::cout << "please enter an interger value: ";
	std::cin >> number;
	std::cout << number << std::endl;
}

The program will output the function address of my_function_to_call to save us some reverse-engineering headaches and the program will keep looping until wasCalled is set to true.

The address to our function is 0x7FF781D4139D, so we need to call 0x7FF781D4139D in order to get past the while loop in the program.

Here is how you’d call that function externally using C#:

First you will need to reference two important namespaces.

using System.Runtime.InteropServices;
using System.Diagnostics;

We will use the DllImport attribute in System.Runtime.InteropServices to reference native functions that we will then use to call the function externally and System.Diagnostics to get the process we want to work with.

We will need three functions:


[DllImport("kernel32.dll")]
internal static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, int flAllocationType, int flProtect);

[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out UIntPtr lpNumberOfBytesWritten);

[DllImport("kernel32.dll")]
internal static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);

VirtualAllocEx allocates memory in the defined process, WriteProcessMemory writes to a specific memory address in the defined process and CreateRemoteThread creates a remote thread and that function will execute our assembly code.

Please read the following documentation pages provided by Microsoft as they explain it better than I ever will:

https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualallocex

https://docs.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory

https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread

We will call the following op code:


byte[] opcode = new byte[] { 
     0x48, 0xB8, 0x9D, 0x13, 0xD4, 0x81, 0xF7, 0x7F, 0x00, 0x00,
     0xFF, 0xD0,
     0xC3
};

All of this hexadecimal mess can be translated to:


48 B8 9D 13 D4 81 F7 7F 00 00  mov rax, 0x7FF781D4139D ; move the address 0x7FF781D4139D to rax
FF D0                          call rax                ; call the value stored in rax registry
C3                             retn                    ; returns, without this opcode the process will crash

And when we put it all together we get this:


static void Main(string[] args)
{
    IntPtr addr = IntPtr.Zero;
    UIntPtr bytesWritten = UIntPtr.Zero;
    IntPtr hThread = IntPtr.Zero;
    Process process = Process.GetProcessesByName("ConsoleApplication2")[0]; // the process we want to inject our opcode to
    byte[] opcode = new byte[] { 
        0x48, 0xB8, 0x9D, 0x13, 0xD4, 0x81, 0xF7, 0x7F, 0x00, 0x00, // mov rax, 0x7FF781D4139D
        0xFF, 0xD0, // call rax
        0xC3 // retn
    };
    // allocate memory
    if((addr = VirtualAllocEx(process.Handle, IntPtr.Zero, (uint)opcode.Length, 0x00001000, 0x0040)) == IntPtr.Zero)
    {
        Console.WriteLine("failed to allocate memory.");
        return;
    }
    // write memory
    if(!WriteProcessMemory(process.Handle, addr, opcode, (uint)opcode.Length, out bytesWritten))
    {
        Console.WriteLine("could not write to process' memory.");
        return;
    }
    uint lpThreadId = 0;
    // create thread
    if((hThread = CreateRemoteThread(process.Handle, IntPtr.Zero, 0, addr, IntPtr.Zero, 0, out lpThreadId)) == IntPtr.Zero)
    {
        Console.WriteLine("hThread value is 0x0.");
        return;
    }
}

That’s basically it though I haven’t covered what to do with different convention calls, closing handles externally and this beautiful thing called stack alignment but this is your problem now.

See you next time when I figure out how to call game functions in an emulator.

Avatar photo
Dennis Stanistan
Articles: 13