Mod Against the Machine - External Process Modding
Read this page before moving to Mod Against the Machine - DLL Injection
Concept
When a game is compiled it is converted into machine code and when loaded into memory that machine code gets loaded into RAM as instructions and base static memory. As the game runs the static memory and instructions begin to allocate heap memory for game objects including player, enemies and other elements of the game.
Using cheat engine you can scan this memory for values that you wish to modify such as health. You could search for all values in memory that are "100" and then begin narrowing down which ones are health based on changing your health mid-game.
When you do determine this address of memory, it is likely a member of a larger struct that represents the player. This memory address will change on each game loop so you cannot use it directly. Instead you must begin to search for what other addresses contain a value that is the address of the health pointer, ie a pointer to it.
Many of those pointers will actually reference the player struct with an offset into the property that is the health and you need to note the offsets as those are consistent and static.
You then use cheat engine to continue narrowing down proper address values using in game actions and reboots and reverse the pointer lookups until you reach a static portion of memory (these are marked as green). Once you have the static memory address you can then drill down to the proper player address and property offsets on any game launch.
External process hacking involves actually running a separate process that invokes read and write privileges to the process you wish to modify. It takes a snapshot of the process's memory and you can then dive into the data and make modifications when you run the external program.
But this has its drawbacks as it must be loaded as an external program that has permission to modify memory of the game.
This can be extended to overwriting instructions as well, such as replacing functions with NOP instructions to disable in-game operations like recoil.
1. Modifying Memory Values
Demonstrates how to find a running process, its internal module and modify any value in memory starting from a static base address. This rewrites the active weapon's ammo count.
#include "stdafx.h"
#include <iostream>
#include <vector>
#include <Windows.h>
#include "proc.h"
int main()
{
//Get ProcId of the target process
DWORD procId = GetProcId(L"ac_client.exe");
//Getmodulebaseaddress
uintptr_t moduleBase = GetModuleBaseAddress(procId, L"ac_client.exe");
//Get Handle to Process
HANDLE hProcess = 0;
hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, procId);
//Resolve base address of the pointer chain
uintptr_t dynamicPtrBaseAddr = moduleBase + 0x10f4f4;
std::cout << "DynamicPtrBaseAddr = " << "0x" << std::hex << dynamicPtrBaseAddr << std::endl;
//Resolve our ammo pointer chain
std::vector<unsigned int> ammoOffsets = { 0x374, 0x14, 0x0 };
uintptr_t ammoAddr = FindDMAAddy(hProcess, dynamicPtrBaseAddr, ammoOffsets);
std::cout << "ammoAddr = " << "0x" << std::hex << ammoAddr << std::endl;
//Read Ammo value
int ammoValue = 0;
ReadProcessMemory(hProcess, (BYTE*)ammoAddr, &ammoValue, sizeof(ammoValue), nullptr);
std::cout << "Curent ammo = " << std::dec << ammoValue << std::endl;
//Write to it
int newAmmo = 9999;
WriteProcessMemory(hProcess, (BYTE*)ammoAddr, &newAmmo, sizeof(newAmmo), nullptr);
//Read out again
ReadProcessMemory(hProcess, (BYTE*)ammoAddr, &ammoValue, sizeof(ammoValue), nullptr);
std::cout << "New ammo = " << std::dec << ammoValue << std::endl;
getchar();
return 0;
}
// proc.h
#pragma once
#include <vector>
#include <windows.h>
#include <TlHelp32.h>
DWORD GetProcId(const wchar_t* procName);
uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName);
uintptr_t FindDMAAddy(HANDLE hProc, uintptr_t ptr, std::vector<unsigned int> offsets);
// proc.cpp
#include "stdafx.h"
#include "proc.h"
DWORD GetProcId(const wchar_t* procName)
{
DWORD procId = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap != INVALID_HANDLE_VALUE)
{
PROCESSENTRY32 procEntry;
procEntry.dwSize = sizeof(procEntry);
if (Process32First(hSnap, &procEntry))
{
do
{
if (!_wcsicmp(procEntry.szExeFile, procName))
{
procId = procEntry.th32ProcessID;
break;
}
} while (Process32Next(hSnap, &procEntry));
}
}
CloseHandle(hSnap);
return procId;
}
uintptr_t GetModuleBaseAddress(DWORD procId, const wchar_t* modName)
{
uintptr_t modBaseAddr = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, procId);
if (hSnap != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 modEntry;
modEntry.dwSize = sizeof(modEntry);
if (Module32First(hSnap, &modEntry))
{
do
{
if (!_wcsicmp(modEntry.szModule, modName))
{
modBaseAddr = (uintptr_t)modEntry.modBaseAddr;
break;
}
} while (Module32Next(hSnap, &modEntry));
}
}
CloseHandle(hSnap);
return modBaseAddr;
}
uintptr_t FindDMAAddy(HANDLE hProc, uintptr_t ptr, std::vector<unsigned int> offsets)
{
uintptr_t addr = ptr;
for (unsigned int i = 0; i < offsets.size(); ++i)
{
ReadProcessMemory(hProc, (BYTE*)addr, &addr, sizeof(addr), 0);
addr += offsets[i];
}
return addr;
}
// stdafx.h : include file for standard system include files,
// or project specific include files that are used frequently, but
// are changed infrequently
//
#pragma once
#include "targetver.h"
#include <stdio.h>
#include <tchar.h>
// TODO: reference additional headers your program requires here
// stdafx.cpp : source file that includes just the standard includes
// stdafx.obj will contain the pre-compiled type information
#include "stdafx.h"
// TODO: reference any additional headers you need in STDAFX.H
// and not in this file
#pragma once
// Including SDKDDKVer.h defines the highest available Windows platform.
// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and
// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h.
#include <SDKDDKVer.h>
2. Overwriting Instructions
Demonstrates how to overwrite instructions in memory with new instructions to replace functions or code that runs during gameplay. Recoil is converted to NOP instructions, Health is converted to incremement instead of decrement.
All the windows headers were moved to stdafx to take advantage of precompiled headers.
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include "proc.h"
#include "mem.h"
int main()
{
HANDLE hProcess = 0;
uintptr_t moduleBase = 0, localPlayerPtr = 0, healthAddr = 0;
bool bHealth = false, bAmmo = false, bRecoil = false;
const int newValue = 9999;
//Get ProcId of the target process
DWORD procId = GetProcId(L"ac_client.exe");
if (procId)
{
//Get Handle to Process
hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, procId);
//Getmodulebaseaddress
moduleBase = GetModuleBaseAddress(procId, L"ac_client.exe");
//Resolve address
localPlayerPtr = moduleBase + 0x10f4f4;
//Resolve base address of the pointer chain
healthAddr = FindDMAAddy(hProcess, localPlayerPtr, { 0xF8 });
}
DWORD dwExit = 0;
while (GetExitCodeProcess( hProcess, &dwExit ) && dwExit == STILL_ACTIVE)
{
//Health continuous write
if (GetAsyncKeyState(VK_NUMPAD1) & 1)
{
bHealth = !bHealth;
}
//unlimited ammo patch
if (GetAsyncKeyState(VK_NUMPAD2) & 1)
{
bAmmo = !bAmmo;
if (bAmmo)
{
//FF 06 = inc [esi]
mem::PatchEx((BYTE*)(moduleBase + 0x637e9), (BYTE*)"\xFF\x06", 2, hProcess);
}
else
{
//FF 0E = dec [esi]
mem::PatchEx((BYTE*)(moduleBase + 0x637e9), (BYTE*)"\xFF\x0E", 2, hProcess);
}
}
//no recoil NOP
if (GetAsyncKeyState(VK_NUMPAD3) & 1)
{
bRecoil = !bRecoil;
if (bRecoil)
{
mem::NopEx((BYTE*)(moduleBase + 0x63786), 10, hProcess);
}
else
{
//50 8D 4C 24 1C 51 8B CE FF D2; the original stack setup and call
mem::PatchEx((BYTE*)(moduleBase + 0x63786), (BYTE*)"\x50\x8D\x4C\x24\x1C\x51\x8B\xCE\xFF\xD2", 10, hProcess);
}
}
//Continuous write
if (bHealth)
{
mem::PatchEx((BYTE*)healthAddr, (BYTE*)&newValue, sizeof(newValue), hProcess);
}
Sleep(10);
}
std::cout << "Process not found, press enter to exit\n";
getchar();
}
// mem.h
#pragma once
#include "stdafx.h"
#include <windows.h>
namespace mem
{
void PatchEx(BYTE* dst, BYTE* src, unsigned int size, HANDLE hProcess);
void NopEx(BYTE* dst, unsigned int size, HANDLE hProcess);
}
// mem.cpp
#include "stdafx.h"
#include "mem.h"
void mem::PatchEx(BYTE* dst, BYTE* src, unsigned int size, HANDLE hProcess)
{
// changes perms of the memory, changes the value, then changes perms back
DWORD oldprotect;
VirtualProtectEx(hProcess, dst, size, PAGE_EXECUTE_READWRITE, &oldprotect);
WriteProcessMemory(hProcess, dst, src, size, nullptr);
VirtualProtectEx(hProcess, dst, size, oldprotect, &oldprotect);
}
void mem::NopEx(BYTE* dst, unsigned int size, HANDLE hProcess)
{
BYTE* nopArray = new BYTE[size];
memset(nopArray, 0x90, size);
PatchEx(dst, nopArray, size, hProcess);
delete[] nopArray;
}
Next
Lets look into injecting a DLL into the memory of a running process so we can manipulate its memory and instructions directly.
More Reading
https://www.youtube.com/watch?v=YaFlh2pIKAg
https://www.youtube.com/watch?v=wiX5LmdD5yk
https://www.youtube.com/watch?v=UMt1daXknes
https://github.com/techiew/EldenRingModLoader
https://github.com/techiew/EldenRingMods/blob/master/ModUtils.h