How to hack a .NET Core game
During my vacation, I spent some time with an MMORPG that I had not played before. I was enjoying playing the game but when I had to get back to work I realized that I won’t have time to play this game If I also want to make progress on my stuff like my game engine and my game.
The idea in my head was “What if I could write a program to make the gameplay itself?”. I played around a bit with this idea and like with many pictures like this it piqued my interest just enough for me to investigate what that kind of program would look like as well as if I have the skills to pull it off, so I rolled up my sleeves and got to work.
Disclaimer: In this post I will not explain how to hack any specific application or game, I will just mention some of the approaches that people who make cheats and bots use so that we can learn about them. No game was harmed in the process of making this.
Disclaimer: I do not promise that this is 100% bulletproof and works for everything. I am sure there are countermeasures to this that would require you to be even more clever. I have however not investigated how to protect yourself from this and will save that for another article. So if you care about this then consider the title a clickbait.
With that said let’s get into what I came up with!
Background
Some background information before starting this. Before doing this small project I had no previous experience with hacking or reverse engineering anything so what I did was just blindly test different stuff that I learned along the way.
The target game I had in mind was built using .NET Core using FNA and was an open-source game. The reason I mention this is because many of the things I tried just simply won’t work because of the technology the game was built with which we will get more into.
Another thing before we start that perhaps is good to know is that there are two common approaches to design your hack that I am aware of, it can either be external or internal where external hints at a standalone application using the platform APIs to manipulate another process on the machine where internal injects itself into the process and attacks it from within.
This article will go through the steps I went through to investigate and eventually land on a solution. I will not go overly in-depth into any of the techniques, if something specific needs to be clarified however just tell me and I will try to answer to the best of my knowledge.
Attempt 1: Memory hacking
So the first thing I tried which I also knew about before because it is not only used for reverse engineering was to manipulate the game’s memory. If you have ever been curious about cheats and perhaps googled around a bit I’m sure many of you know a program called “Cheat Engine”.
Many people are under the impression that this is a tool for people who want to make cheats but don’t know how or just want to create small scripts. This is not true, Cheat Engine is a very valuable tool used for memory scanning and debugging. It is capable of scanning the memory of running processes on your computer as well as debugging the disassembled source code of the executable of your choice.
The most simple way to manipulate a game using this piece of software is to open your target process within Cheat Engine followed by searching for a known value within the application. A common example is if you currently have 100 health within your game you would scan for the value 100 within Cheat Engine, this would bring up thousands upon thousands of results of every address in memory that contains the value 100. To narrow it down Cheat Engine allows you to create a new scan for each variable that changed, so for example if you take some damage within the game and now have 70 health you would perform a new scan for all values that were 100 but are now 70.
This is a process you do over and over again until you have a manageable amount of addresses left. What you then can do is to select the addresses and manipulate them, for example manually changing the value to 200, if you found the correct health variable within your game your health would now be 200 because you set that address in memory to that value.
Let us say that you managed to find the correct address and change it to 200. Well, now you’ve manipulated and “hacked” the game. But what happens if you restart the game? There is no guarantee that they will get the same address in memory upon restart. This is where another technique called pointer scanning comes into play.
Cheat Engine comes with a feature that allows you to save all pointers into what is called a pointer map, this is a file with a snapshot of the game’s memory at that specific given time.
You can then scan that pointer map for a specific pointer and compare it between different runs of the game. What this essentially allows you to do is to find static offsets within the memory where things will always be located at.
I will not go into the details on how to perform this but in the end, you will find a pointer that looks something like this within the user interface of Cheat Engine:
So this is a chain of pointers that ends up with a value of 1000 within the loaded application. The interesting part that the pointer scan can do for us is that it scanned through a chain of addresses that ends up giving us our desired value. With this available, we have a static offset inside our target program that will persist between runs and we can write a program to change this value for us so that we can change our health value whenever we want.
How to access memory addresses through code
So how we make use of this information we found in Cheat Engine is quite simple. All we have to do is to get the base address for the application we are targetting, in the example image above this is the address for the “cheatengine-x86_64-SSE4-AVX2.exe” string that is used because I opened the Cheat Engine process for this example.
So to find this address we can use the Windows API which has a handy function ready for us called CreateToolhelp32Snapshot
which gives us a snapshot of a target process which includes its heaps, modules, and threads.
We can use this function to get the process id (PID) for our target process as well as all the modules within a given process. With this information, we can simply iterate over all the modules and once we find a module we care about take that module’s address and use that as a base address.
The PID can then be used with the function OpenProcess
which gives us an open handle to the specified process. This allows us to use the functions ReadProcessMemory
as well as WriteProcessMemory
. The two functions mentioned last allow us to Read and Write to a process’s internal memory which allows us to manipulate the memory of the process.
#include <Windows.h>
#include <TlHelp32.h>
DWORD GetProcessId(const wchar_t ProcessName)
{
DWORD ProcessId = 0;
HANDLE SnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, ProcessId);
if (Snapshothandle != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 ProcessEntry = { 0 };
ProcessEntry .dwSize = sizeof(ProcessEntry);
if (Process32First(Snapshothandle, &ProcessEntry))
{
do
{
if (!_wcsicmp(ModuleEntry.szModule, ProcessEntry))
{
ProcessId = ProcessEntry.th32ProcessID;
break;
}
} while (Process32Next(Snapshothandle, &ProcessEntry));
}
}
CloseHandle(Snapshothandle);
return ProcessId ;
}
uintptr_t GetModuleBaseAddress(DWORD ProcessId, const wchar_t ModuleName)
{
uintptr_t ModuleBaseAddress = 0;
HANDLE SnapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, ProcessId);
if (Snapshothandle != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 ModuleEntry = { 0 };
ModuleEntry.dwSize = sizeof(ModuleEntry);
if (Module32First(Snapshothandle, &ModuleEntry))
{
do
{
if (!_wcsicmp(ModuleEntry.szModule, ModuleName))
{
ModuleBaseAddress = (uintptr_t)ModuleEntry.modBaseAddr;
break;
}
} while (Module32Next(Snapshothandle, &ModuleEntry));
}
}
CloseHandle(Snapshothandle);
return ModuleBaseAddress;
}
int main()
{
DWORD ProcessId = GetProcessId(L"MyGame.exe");
uintptr_t ModuleBaseAddress = GetModuleBaseAddress(ProcessId, L"MyGame.exe");
HANDLE ProcessHandle = 0;
ProcesHandle = OpenProcess(PROCESS_ALL_ACCESS, NULL, ProcessId);
uintptr_t PointerBaseAddress = ModuleBaseAddress + 0x00E036B0;
uintptr_t TargetAddress = 0;
int Offsets[] = { 0x148, 0xA8, 0x88, 0x48, 0x8, 0x58, 0x0 };
int NumberOfOffsets = 7;
for(int Index = 0; Index < NumberOfOffsets; ++Index)
{
ReadProcessMemory(ProcessHandle, (BYTE *)TargetAddress, &TargetAddress, sizeof(TargetAddress), 0);
TargetAddress += Offsets[Index];
}
// TargetAddress now contains the address!
int NewHealthValue = 5000;
WriteProcessMemory(ProcessHandle, (BYTE *)TargetAddress, &NewHealthValue, sizeof(NewHealthValue), 0);
return 0;
}
Why attempt #1 doesn’t work for .NET Core
Unfortunately, even though it takes a lot of work to even get this far this method of reverse engineering does not work for a .NET Core process.
The reason is that the .NET compiler produces code that can run on the CLR (Common Language Runtime) which handles the stuff for you like garbage collection, runtime type checking, and reference checking.
Unlike languages like C/C++ which produces machine code that is not managed hence nothing is done for you in a sense.
So in practice, this means that even if we do this type of scanning we won’t get static offsets within the executable because the code that is executed is not part of it in a sense and is prone to change when the CLR performs just in time compilation for example.
So if you were to try this, most of the addresses you would find wouldn’t even be inside your executable but inside the coreclr.dll which will have different pointer offsets each time.
So, back to the drawing board.
Attempt 2: Signature Scanning
The next thing I investigated was if I were to do signature scanning on the application’s code to find a sequence of bytes that I have previously identified to mean something I know.
In the end, this will give the same result as attempt 1 but we use a different method to arrive at the final address. Instead of scanning for data itself, we are scanning for the code. At this point, I didn’t think too much that this will result in the same problems as attempt 1.
So Cheat Engine also contains a memory view that allows you to see the code part of the process, it can look something like this:
This view also allows you to set breakpoints and step around the application while it’s running. I used this to find the method for GetPlayer
in the application I used. This requires you to understand some assembly as well as experiment a lot.
Once you find something interesting you can use the sequence of bytes in the second column of the image in as that code’s signature. There are many tools developed for Cheat Engine to help you with this. The tool I used allowed me to right-click an address and export a signature for it. That kind of signature looks something like this: 48 8d 64 24 ? 48 8b 45 ? 48 8d 4d
.
Something weird you may notice is that there are question marks within the generated sequence, these are wildcards that exist because while the address may be moved around in memory the actual code will stay the same hence we use wildcards to aid us with finding these offsets even though the address space is updated between runs.
Some implementations use Masks to help with parsing the string which means to accompany the pattern above with a string like xxxx?xxx?xxx
.
So an example of how this would look within code would be very similar to the first implementation.
Important when doing this is that you only scan parts of memory that matter. What this means is that we can use the function VirtualQuery
which will retrieve memory information of that memory region only yielding valid memory regions which will save us time.
Here it depends a bit if you are writing internal or external code, the example above was an external application that will modify the memory of the target application externally but below we are utilizing an internal application that is injected into the target as a DLL. The reason for this is that when utilizing DLL injection we are becoming a part of the target process meaning that we have access to its memory from within itself.
char* _Scan(char* Pattern, char* Mask, char* Begin, intptr_t Size)
{
intptr_t PatternLength = strlen(Mask);
for (int I = 0; I < Size; I++)
{
bool Found = true;
for (int J = 0; J < PatternLength; J++)
{
if (Mask[J] != '?' && Pattern[J] != *(char*)((intptr_t)Begin + I + J))
{
Found = false;
break;
}
}
if (Found)
{
return (Begin + I);
}
}
return 0;
}
char *Scan(char *Pattern, char *Mask, char *Begin, intptr_t Size)
{
char *Match { 0 };
MEMORY_BASIC_INFORMATION MBI = { 0 };
for (char *Current = Begin; Current < Begin + Size; Current += MBI.RegionSize)
{
if (!VirtualQuery(Current, &MBI, sizeof(MBI)) || MBI.State != MEM_COMMIT || MBI.Protect == PAGE_NOACCESS) continue;
Match = _Scan(Pattern, Mark, Current, MBI.RegionSize);
if (Match != 0)
{
break;
}
}
return Match;
}
Why attempt #2 doesn’t work for .NET Core
In addition to what’s mentioned about why attempt 1 didn’t work is that .NET running its applications on the CLR is that it doesn’t produce machine code. Instead, it produces a special type of bytecode known to the CLR called Intermediate Langue (IL). This is later run within the CLR which takes this IL code and translates it into native instructions that the CPU can understand, this process is known as Just In Time (JIT) Compilation.
This is just as it sounds, a compiler that takes the code and compiles it to machine code as it is executed the first time. There is, of course, more to this if you were to dig down deeper into the CLR but for our purposes, this means that our signatures may not look the same each run as they are compiled on the fly. This can produce insanely long signatures (it did in my case) which takes a long time to look for.
For example the main one I wanted to work on looked like this:
ff 15 ? ? ? ? 48 8b 80 ? ? ? ? 48 83 c4 ? c3 cc cc cc cc cc cc cc cc cc cc 56 48 83 ec ? 48 8b f1 ff 15 ? ? ? ? 48 8d 88 ? ? ? ? 48 8b d6 ff 15 ? ? ? ? 90 48 83 c4 ? 5e c3 cc cc cc cc cc cc cc cc cc cc cc 48 83 ec ? ff 15 ? ? ? ? 48 8b 80 ? ? ? ? 48 83 c4 ? c3 cc cc cc cc cc cc cc cc cc cc 48 83 ec
And unfortunately it wasn’t consistent between runs.
Attempt 3: CLR Hosting
At this point, I had spent a lot of hours investigating the subject and getting real frustrated as nothing I was throwing at the game worked. I started reading up about a concept called function hooking which is used to change the game’s instructions to make it call your function before or after executing the normal code or simply ignore the original altogether and just execute your code.
At this point I understood that I can’t manipulate the game’s memory through static memory offsets, I had to change my strategy a bit. I stumbled upon a concept called CLR Hosting.
What this technique means is that you boot up a CLR instance to get into a managed application space and execute your code there. If you were to do this from inside a DLL injected into an application this means that your code will now run alongside the already managed application and you will have access to everything like you would in a normal C# application.
My hypothesis here was that if I can execute managed code from here I would be able to use reflection in C# to modify the code in the target application. The question is how do you do this?
The irony in it all was that all along Microsoft already had a code sample available for how you would do this available among their interop tests.
The code is very simple and all it does is find the handle to the coreclr.dll
which is the module that is running our code then get its CLR Host and execute the target function inside the target dll using the method ExecuteInDefaultAppDomain
because we are a DLL injected into the target process the same and main AppDomain as the rest of the code that we’ve been wanting to manipulate.
HMODULE CoreCLRModule;
CoreCLRModule = GetModuleHandleA("coreclr.dll");
if (!CoreCLRModule)
{
printf("ERROR - CoreCLR.dll could not be found");
return -1;
}
ICLRRuntimeHost2* RuntimeHost;
FnGetCLRRuntimeHost PFNGetCLRRuntimeHost =
(FnGetCLRRuntimeHost)::GetProcAddress(CoreCLRModule, "GetCLRRuntimeHost");
if (!PFNGetCLRRuntimeHost)
{
printf("ERROR - GetCLRRuntimeHost not found");
return -1;
}
HRESULT hr = PFNGetCLRRuntimeHost(IID_ICLRRuntimeHost2, (IUnknown**)&RuntimeHost);
if (FAILED(hr))
{
printf("ERROR - Failed to get ICLRRuntimeHost2 instance");
return -1;
}
hr = RuntimeHost->Start();
if (FAILED(hr))
{
printf("ERROR - Failed to start the runtime");
return -1;
}
DWORD ExitCode = -1;
hr = RuntimeHost->ExecuteInDefaultAppDomain(L"MyHack.dll", L"Namespace.Class", L"Method", L"Arguments", &ExitCode);
if (FAILED(hr))
{
printf("Assembly execution failed!");
return -1;
}
To combine this with reflection, there are handy tools that play heavily upon the fact that .NET is compiled into IL code and can rebuild the C# code written in a very accurate way which you can inspect to find out more information. An example of such a program is ILSpy which I show an example screenshot of below alongside Visual Studio.
Final notes and future reading
Once again I do not promote or condone cheating or hacking. I simply did this as an experiment and to learn more about it.
If you are interested in learning more about this type of stuff and perhaps want to get into it yourself I can recommend a book called “Game Hacking: Developing Autonomous Bots for Online Games” by Nick Cano. It goes more in-depth about the techniques that I mention here as well as covers more things you can do.
In addition to that, there are very good information and communities available for free on the internet if you just google.
I think this was a very fun experience and I like something that Nick Cano mention early in his book that goes something along the lines that within every game there is another game available in addition to the one in the title, the cat and mouse game of wits between game developers and hackers. I think this is a fun way to look at it, almost like there is more to a game than the features you are supposed to interact with, more content that you can explore and even make yourself. This gives me the happy thought that nothing is stopping you from using this kind of technique to patch parts of a game that you are unhappy with or you want to improve.