Computer games open up new worlds for us. And the world of cheats is one of them. Today we will go from theory to practice together and write our own cheat. If you want to learn how to hack executable files, then this can be a good exercise.
Types of cheats and tactics used
There are different types of cheats. You can divide them into several groups.- External - external cheats that work in a separate process. If we hide our external cheat by loading it into the memory of another process, it will turn into a hidden external.
- Internal - internal cheats that are built into the process of the game itself using an injector. After loading the game into memory, the cheat entry point is called in a separate thread.
- Pixelscan is a type of cheat that uses the screen image and pixel patterns to get the information you need from the game.
- Network proxy - cheats that use network proxies, which, in turn, intercept client and server traffic, receiving or changing the necessary information.
- Changing the memory of the game. The operating system API is used to find and change memory locations that contain the information we need (for example, lives, cartridges).
- Simulation of the player's actions: the application repeats the player's actions by clicking with the mouse in predetermined places.
- Interception of game traffic. There is a cheat between the game and the server. It intercepts data by collecting or modifying information in order to trick the client or server.
Writing a game in C
It is best to tell about cheats in practice. We will write our own little game on which we can practice. I will be writing a game in C #, but I will try to make the data structure as close as possible to a game in C ++. In my experience, cheating in C # games is very easy.The principle of the game is simple: you press Enter and you lose. Not very fair rules, right? Let's try to change them.
Let's start reverse engineering
Executable file of the game
We have a game file. But instead of the source code, we will study the memory and behavior of the application.
Let's start with the behavior of the game
Each time you press Enter, the player's lives are reduced by 15. The initial number of lives is 100.
We will study memory using the Cheat Engine. This is an application for finding variables inside the application memory, and also a good debugger. Restart the game and connect the Cheat Engine to it.
Connecting CE to the game
First, we get a list of all the values 85 in memory.
All values that CE found
Press Enter, and the life indicator will be equal 70. We will filter out all the values.
Value found
That's the value you want! Let's change it and press Enter to check the result.
Value changed
Screen of the game after we pressed Enter
The problem is that after restarting the game, the value will already be at a different address. There is no point in sifting it out every time. You must resort to an AOB (Array Of Bytes) scan.
Each time the application is opened again, due to address space randomization (ASLR), the structure describing the player will be in a new place. To find it, you must first discover the signature. A signature is a set of bytes that do not change in structure, by which you can search in application memory.
After several presses on Enter, the number of lives changed to 55. Find the desired value in memory again and open the region in which it is located.
Region of memory
The allocated byte is the beginning of our int32number. 37 00 00 00 - number 55 in decimal form.
I will copy a small region of memory and paste it into notepad for further study. Now let's restart the application and find the value in memory again. Copy the same memory region again and paste it into notepad. Let's start the comparison. The goal is to find bytes near this signature that will not change.
We start comparing bytes
Let's check the bytes before the structure.
Bingo!
As you can see, the allocated bytes have not changed, so you can try using them as a signature. The smaller the signature, the faster the scan will take. The signature 01 00 00 00 will obviously be too common in memory. Better to take 03 00 00 01 00 00 00. First, let's find it in memory.
Signature is not unique
The signature was found, but it is repeated. A more unique sequence is needed. Let's try ED 03 00 00 01 00 00 00.
To confirm the uniqueness, we get the following result:
The signature is unique
We need to find the indentation from the signature to get its starting address, not the address of lives. For now, let's save the found signature and postpone it for a while. Don't worry, we'll come back to it later.
Life cycle external
Using the function OpenProcess, external cheats get a handle for the desired process and make the necessary changes to the code (patching) or read and change variables inside the game's memory. The functions ReadProcessMemory and are used to modify the memory WriteProcessMemory.Since the dynamic allocation of data in memory makes it difficult to write the necessary addresses and constantly access them, you can use the AOB search technique. The life cycle of an external cheat looks like this:
- Find the process ID.
- Get a handle to this process with the required rights.
- Find addresses in memory.
- Patch something if needed.
- Render the GUI, if available.
- Read or modify memory as needed.
Writing an external cheat for your game
P / Invoke technology is used to call WinAPI functions from C #. To start working with these functions, you need to declare them in the code. I will take ready-made declarations from pinvoke.net. The first function would be OpenProcess.
Code:
[Flags]
public enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VirtualMemoryOperation = 0x00000008,
VirtualMemoryRead = 0x00000010,
VirtualMemoryWrite = 0x00000020,
DuplicateHandle = 0x00000040,
CreateProcess = 0x000000080,
SetQuota = 0x00000100,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
QueryLimitedInformation = 0x00001000,
Synchronize = 0x00100000
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(
ProcessAccessFlags processAccess,
bool bInheritHandle,
int processId);
The next function is ReadProcessMemory.
Code:
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
[Out] byte[] lpBuffer,
int dwSize,
out IntPtr lpNumberOfBytesRead);
Now the function for reading memory WriteProcessMemory.
Code:
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
int nSize,
out IntPtr lpNumberOfBytesWritten);
We are faced with a problem: to search for a pattern, it is necessary to collect all the memory regions of the process. To do this, we need a function and a structure. Function VirtualQueryEx:
Code:
[DllImport("kernel32.dll")]
static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength);
Structure MEMORY_BASIC_INFORMATION:
Code:
[StructLayout(LayoutKind.Sequential)]
public struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public uint AllocationProtect;
public IntPtr RegionSize;
public uint State;
public uint Protect;
public uint Type;
}
Now you can start writing the code for the cheat itself. The first step is to find a game.
Code:
private static int WaitForGame()
{
while (true)
{
var prcs = Process.GetProcessesByName("SimpleConsoleGame");
if (prcs.Length != 0)
{
return prcs.First().Id;
}
Thread.Sleep(150);
}
}
Then let's open the handle to our game.
Code:
private static IntPtr GetGameHandle(int id)
{
return WinAPI.OpenProcess(WinAPI.ProcessAccessFlags.All, false, id);
}
Let's put it all together in the starting code.
Code:
Console.Title = "External Cheat Example";
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("Waiting for game process..");
var processId = WaitForGame();
Console.WriteLine($"Game process found. ID: {processId}");
var handle = GetGameHandle(processId);
if (handle == IntPtr.Zero)
{
CriticalError("Error. Process handle acquirement failed.\n" +
"Insufficient rights?");
}
Console.WriteLine($"Handle was acquired: 0x{handle.ToInt32():X}");
Console.ReadKey(true);
We will find the process ID, then get its handle and, if anything, print an error message. The implementation is CriticalError(string) not that important.
After that, we can already go to the search for a pattern in memory. Let's create a general class that contains all the functions for working with memory. Let's call it MemoryManager. Then we will make a class MemoryRegion to describe the memory region. There is a MEMORY_BASIC_INFORMATION lot of unnecessary data that should not be passed on, so I moved them into a separate class.
Code:
public class MemoryRegion
{
public IntPtr BaseAddress { get; set; }
public IntPtr RegionSize { get; set; }
public uint Protect { get; set; }
}
That's all we need: the region's starting address, its size, and its protection. Now we get all regions of memory. How it's done?
- We get information about the memory region at the zero address.
- We check the status and protection of the region. If everything is in order, add it to the list.
- We receive information about the next region.
- We check and add it to the list.
- We continue in a circle.
Code:
public List<MemoryRegion> QueryMemoryRegions() {
long curr = 0;
var regions = new List<MemoryRegion>();
while (true) {
try {
var memDump = WinAPI.VirtualQueryEx(_processHandle, (IntPtr) curr, out var memInfo, 28);
if (memDump == 0) break;
if ((memInfo.State & 0x1000) != 0 && (memInfo.Protect & 0x100) == 0)
{
regions.Add(new MemoryRegion
{
BaseAddress = memInfo.BaseAddress,
RegionSize = memInfo.RegionSize,
Protect = memInfo.Protect
});
}
curr = (long) memInfo.BaseAddress + (long) memInfo.RegionSize;
} catch {
break;
}
}
return regions;
}
After receiving the regions, we will scan them for the presence of the pattern we need. The pattern consists of parts of two types - of known and unknown (changing bytes): eg 00 ?? ?? FB. Let's create an interface to describe these parts.
Code:
interface IMemoryPatternPart
{
bool Matches(byte b);
}
Now let's describe the part that has a known byte.
Code:
public class MatchMemoryPatternPart : IMemoryPatternPart
{
public byte ValidByte { get; }
public MatchMemoryPatternPart(byte valid)
{
ValidByte = valid;
}
public bool Matches(byte b) => ValidByte == b;
}
Let's do the same with the second type.
Code:
public class AnyMemoryPatternPart : IMemoryPatternPart
{
public bool Matches(byte b) => true;
}
Now let's parse the pattern from the string.
Code:
private void Parse(string pattern)
{
var parts = pattern.Split(' ');
_patternParts.Clear();
foreach (var part in parts)
{
if (part.Length != 2)
{
throw new Exception("Invalid pattern.");
}
if (part.Equals("??"))
{
_patternParts.Add(new AnyMemoryPatternPart());
continue;
}
if (!byte.TryParse(part, NumberStyles.HexNumber, null, out var result))
{
throw new Exception("Invalid pattern.");
}
_patternParts.Add(new MatchMemoryPatternPart(result));
}
}
As already done above, we check what type of part of the pattern it is, parse it, if necessary, and add it to the list. We need to check the work of this method.
Code:
var p = new MemoryPattern ("01 ?? 02 ?? 03 ?? FF");
Success!
Now we need to teach our MemoryManager memory to read.
Code:
public byte[] ReadMemory(IntPtr addr, int size)
{
var buff = new byte[size];
return WinAPI.ReadProcessMemory(_processHandle, addr, buff, size, out _) ? buff : null;
}
I first wrote a nice function using Linq to scan memory. But its implementation took a long time. Then I rewrote the method without using this technology, and everything worked much faster. Optimized function result:
Fast scan memory
The result of the original function:
Very slow memory scan
Now I will share the wisdom gained at this stage: do not be afraid to optimize your code. Libraries do not always provide the fastest solutions. Original function:
Code:
public IntPtr ScanForPatternInRegion(MemoryRegion region, MemoryPattern pattern)
{
var endAddr = (int) region.RegionSize - pattern.Size;
var wholeMemory = ReadMemory(region.BaseAddress, (int) region.RegionSize);
for (var addr = 0; addr < endAddr; addr++)
{
var b = wholeMemory.Skip(addr).Take(pattern.Size).ToArray();
if (!pattern.PatternParts.First().Matches(b.First()))
{
continue;
}
if (!pattern.PatternParts.Last().Matches(b.Last()))
{
continue;
}
var found = true;
for (var i = 1; i < pattern.Size - 1; i++)
{
if (!pattern.PatternParts[i].Matches(b[i]))
{
found = false;
break;
}
}
if (!found)
{
continue;
}
return region.BaseAddress + addr;
}
return IntPtr.Zero;
}
Fixed function (just use Array.Copy()).
Code:
public IntPtr ScanForPatternInRegion(MemoryRegion region, MemoryPattern pattern)
{
var endAddr = (int) region.RegionSize - pattern.Size;
var wholeMemory = ReadMemory(region.BaseAddress, (int) region.RegionSize);
for (var addr = 0; addr < endAddr; addr++)
{
var buff = new byte[pattern.Size];
Array.Copy(wholeMemory, addr, buff, 0, buff.Length);
var found = true;
for (var i = 0; i < pattern.Size; i++)
{
if (!pattern.PatternParts[i].Matches(buff[i]))
{
found = false;
break;
}
}
if (!found)
{
continue;
}
return region.BaseAddress + addr;
}
return IntPtr.Zero;
}
This function searches for a pattern within a memory region. The next function uses it to scan the memory of the entire process.
Code:
public IntPtr PatternScan(MemoryPattern pattern)
{
var regions = QueryMemoryRegions();
foreach (var memoryRegion in regions)
{
var addr = ScanForPatternInRegion(memoryRegion, pattern);
if (addr == IntPtr.Zero)
{
continue;
}
return addr;
}
return IntPtr.Zero;
}
Let's add two functions for reading and writing a 32-bit number into memory.
Code:
public int ReadInt32(IntPtr addr)
{
return BitConverter.ToInt32(ReadMemory(addr, 4), 0);
}
public void WriteInt32(IntPtr addr, int value)
{
var b = BitConverter.GetBytes(value);
WinAPI.WriteProcessMemory(_processHandle, addr, b, b.Length, out _);
}
Everything is now ready to search for a pattern and write the main cheat code.
Code:
var playerBase = memory.PatternScan (new MemoryPattern ("ED 03 00 00 01 00 00 00"));
We find the pattern in memory, then - the address of the player's lives.
Code:
var playerHealth = playerBase + 24;
We read the value of lives:
Code:
Console.WriteLine ($ "Current health: {memory.ReadInt32 (playerHealth)}");
Why not give the player nearly endless lives?
Code:
memory.WriteInt32 (playerHealth, int.MaxValue);
And again we count the player's lives for demonstration.
Code:
Console.WriteLine ($ "New health: {memory.ReadInt32 (playerHealth)}");
Checking
Let's start our cheat, then start the game.Everything works
Let's try to press Enter in the "game".
Lives have changed
The cheat works!
Writing your first injector
There are many ways to force the process to load our code. You can use DLL Hijacking, you can use SetWindowsHookEx, but we'll start with the simplest and most well-known function - LoadLibrary. LoadLibrary forces the process we need to load the library itself.We need a descriptor with the necessary rights. Let's start preparing for the injection. First, get the name of the library from the user.
Code:
Console.Write("> Enter DLL name: ");
var dllName = Console.ReadLine();
if (string.IsNullOrEmpty(dllName) || !File.Exists(dllName))
{
Console.WriteLine("DLL name is invalid!");
Console.ReadLine();
return;
}
var fullPath = Path.GetFullPath(dllName);
Then we ask the user for the process name and find its ID.
Code:
var fullPath = Path.GetFullPath(dllName);
var fullPathBytes = Encoding.ASCII.GetBytes(fullPath);
Console.Write("> Enter process name: ");
var processName = Console.ReadLine();
if (string.IsNullOrEmpty(dllName))
{
Console.WriteLine("Process name is invalid!");
Console.ReadLine();
return;
}
var prcs = Process.GetProcessesByName(processName);
if (prcs.Length == 0)
{
Console.WriteLine("Process wasn't found.");
Console.ReadLine();
return;
}
var prcId = prcs.First().Id;
This code will have problems with processes with the same name.
Now you can go to the first injection method.
Implementing the LoadLibrary injection
First, let's take a look at the principle of operation of this type of injector.- First, it reads the full path to the library from disk.
- Collects it into a string. Then we get the address LoadLibraryA(LPCSTR) with GetProcAddress(HMODULE, LPCSTR).
- Allocates memory for a string inside the application, writes it there.
- After that, it creates a stream at the address LoadLibraryA, passing the path as an argument.
- The Specify the import statement of The statement The statement The of of The of The of of of The statement The operation of The of of The the the the the To OpenProcess, ReadProcessMemory, WriteProcessMemory, GetProcAddress, GetModuleHandle, CreateRemoteThread, VirtualAllocEx.
WWW
Signatures can be easily found at pinvoke.net.The first step is to open a handle with full access to the process.
Code:
var handle = WinAPI.OpenProcess(WinAPI.ProcessAccessFlags.All,
false,
processID);
if (handle == IntPtr.Zero)
{
Console.WriteLine("Can't open process.");
return;
}
Let's convert our string to bytes.
Code:
var libraryPathBytes = Encoding.ASCII.GetBytes (libraryPath);
After that, you need to allocate memory for this line.
Code:
var memory = WinAPI.VirtualAllocEx(handle,
IntPtr.Zero,
256,
WinAPI.AllocationType.Commit | WinAPI.AllocationType.Reserve,
WinAPI.MemoryProtection.ExecuteReadWrite);
The process descriptor is passed to the function handle: _MAX_PATH (the maximum path size in Windows), it is equal to 256. We indicate that it is possible to write to memory, read it and execute it. We write the line inside the process.
Code:
WinAPI.WriteProcessMemory (handle, memory, libraryPathBytes, libraryPathBytes.Length, out var bytesWritten);
Since we will be using the function LoadLibraryA to load the library, we need to get its address.
Code:
var funcAddr = WinAPI.GetProcAddress (WinAPI.GetModuleHandle ("kernel32"), "LoadLibraryA");
Everything is ready to start the injection process. All that remains is to create a stream in the remote application:
Code:
var thread = WinAPI.CreateRemoteThread(handle, IntPtr.Zero, IntPtr.Zero, funcAddr, memory, 0, IntPtr.Zero);
The injector is ready, but we will test it only after writing a simple library.
Writing a framework for internal
Moving to C ++! Let's start with an entry point and a simple message via WinAPI. A must entry point A A AA A DLL the accept the the the the the aaa aa a three parameters The of The of The of of of of of The of of of of of of of The of The: HINSTANCE, DWORD, LPVOID.- HINSTANCE - refers to a library.
- DWORD Is the reason for calling the entry point (DLL loading and unloading).
- LPVOID Is a reserved value.
Code:
#include <Windows.h>
BOOL WINAPI DllMain(
_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved
)
{
return 0;
}
First, let's check why the entry point is being called.
Code:
if (fdwReason == DLL_PROCESS_ATTACH) {}
The argument fdwReason will be equal DLL_PROCESS_ATTACHif the library has just been attached to the process, or DLL_PROCESS_DETACHif it is in the process of being unloaded. For the test, we will display the message:
Code:
if(fdwReason == DLL_PROCESS_ATTACH)
{
MessageBox(nullptr, "Hello world!", "", 0);
}
Now we can check the injector and this library. We launch the injector, enter the name of the library and process.
Library loaded
Now let's write a simple class with a singleton to make the code beautiful.
Code:
#pragma once
class internal_cheat
{
public:
static internal_cheat* get_instance();
void initialize();
void run();
private:
static internal_cheat* _instance;
bool was_initialized_ = false;
internal_cheat();
};
Now the code itself. Default constructor and singleton.
Code:
internal_cheat::internal_cheat() = default;
internal_cheat* internal_cheat::get_instance()
{
if(_instance == nullptr)
{
_instance = new internal_cheat();
}
return _instance;
}
Here's a simple entry point code.
Code:
#include <Windows.h>
#include "InternalCheat.h"
BOOL WINAPI DllMain(
_In_ HINSTANCE hinstDLL,
_In_ DWORD fdwReason,
_In_ LPVOID lpvReserved
)
{
if(fdwReason == DLL_PROCESS_ATTACH)
{
auto cheat = internal_cheat::get_instance();
cheat->initialize();
cheat->run();
}
return 0;
}
I must say that the next part took the longest. One small mistake resulted in a huge waste of time. But I drew conclusions and will explain to you where you can make such a mistake and how to find it.
We need to find the pattern inside the game's memory. To do this, first we will iterate over all the memory regions of the application, then we will scan each of them. Below is the implementation of getting a list of memory regions, but only for your own process. I explained how it works earlier.
Code:
DWORD internal_cheat::find_pattern(std::string pattern)
{
auto mbi = MEMORY_BASIC_INFORMATION();
DWORD curr_addr = 0;
while(true)
{
if(VirtualQuery(reinterpret_cast<const void*>(curr_addr), &mbi, sizeof mbi) == 0)
{
break;
}
if((mbi.State == MEM_COMMIT || mbi.State == MEM_RESERVE) &&
(mbi.Protect == PAGE_READONLY ||
mbi.Protect == PAGE_READWRITE ||
mbi.Protect == PAGE_EXECUTE_READ ||
mbi.Protect == PAGE_EXECUTE_READWRITE))
{
auto result = find_pattern_in_range(pattern, reinterpret_cast<DWORD>(mbi.BaseAddress), reinterpret_cast<DWORD>(mbi.BaseAddress) + mbi.RegionSize);
if(result != NULL)
{
return result;
}
}
curr_addr += mbi.RegionSize;
}
return NULL;
}
For each region found, this code calls a function find_pattern_in_rangethat looks for a pattern in that region.
Code:
DWORD internal_cheat::find_pattern_in_range(std::string pattern, const DWORD range_start, const DWORD range_end)
{
auto strstream = istringstream(pattern);
vector<int> values;
string s;
First, the function parses the pattern.
Code:
while (getline(strstream, s, ' '))
{
if (s.find("??") != std::string::npos)
{
values.push_back(-1);
continue;
}
auto parsed = stoi(s, 0, 16);
values.push_back(parsed);
}
Then the scanning itself begins.
Code:
for(auto p_cur = range_start; p_cur < range_end; p_cur++ )
{
auto localAddr = p_cur;
auto found = true;
for (auto value : values)
{
if(value == -1)
{
localAddr += 1;
continue;
}
auto neededValue = static_cast<char>(value);
auto pCurrentValue = reinterpret_cast<char*>(localAddr);
auto currentValue = *pCurrentValue;
if(neededValue != currentValue)
{
found = false;
break;
}
localAddr += 1;
}
if(found)
{
return p_cur;
}
}
return NULL;
}
I used a vector from intto store the pattern data, which -1 means any byte can be there. I did this to simplify the search for the pattern, speed it up and not translate the same code from an external cheat.
Now a few words about the error I mentioned earlier. I was constantly rewriting the pattern search function until I decided to take a look at the memory region search function. The problem was that I was comparing memory protection completely wrong. Initial version:
Code:
if((mbi.State == MEM_COMMIT || mbi.State == MEM_RESERVE) &&
(mbi.Protect == PAGE_EXECUTE_READ ||
mbi.Protect == PAGE_EXECUTE_READWRITE)) { }
The code only accepted pages with read / executable memory and read / write / executable memory. He ignored the rest. The code was changed to this:
Code:
if((mbi.State == MEM_COMMIT || mbi.State == MEM_RESERVE) &&
(mbi.Protect == PAGE_READONLY ||
mbi.Protect == PAGE_READWRITE ||
mbi.Protect == PAGE_EXECUTE_READ ||
mbi.Protect == PAGE_EXECUTE_READWRITE)) { }
This function began to find all the memory pages it needed.
INFO
PAGE_READONLY can cause a fatal error while writing data, we always have VirtualProtect.I discovered this error when I started checking the memory pages in the application using Process Hacker and Cheat Engine. My pattern ended up in one of the very first play-protected memory regions, so it never was.
Now, having found the pattern, we can save it in the field of our class.
Code:
void internal_cheat::initialize()
{
if(was_initialized_)
{
return;
}
printf("\n\n[CHEAT] Cheat was loaded! Initializing..\n");
was_initialized_ = true;
player_base_ = reinterpret_cast<void*>(find_pattern("ED 03 00 00 01 00 00 00"));
printf("[CHEAT] Found playerbase at 0x%p\n", player_base_);
}
After that, the function will be called internal_cheat::run(), which should perform all the functions of the reader.
Code:
void internal_cheat::run()
{
printf("[CHEAT] Cheat is now running.\n");
const auto player_health = reinterpret_cast<int*>(reinterpret_cast<DWORD>(player_base_) + 7);
while(true)
{
*player_health = INT_MAX;
Sleep(100);
}
}
We just get the address of the player's lives from our pattern and set them to the maximum value ( INT_MAX) every 100ms.
Checking our cheat
We start the game, inject the library.Cheat is injected
Let's try to press the Enter button a couple of times.
Cheat works
Our lives don't change and everything works great!
Let's sum up
Any element of the game that is processed on our computer can be modified or completely removed. Unfortunately or fortunately, gaming companies do not always take care of anti-cheat, opening the way for us, cheaters.(c) xakep.ru