Newbie's Game Hacking Notes (ft. Turbo Overkill)
A terse-comments blog of making myself more comfortable with Cheat Engine and the basics of cheats/trainers developement. Practice is based on the awesome Turbo Overkill FPS (v1.35).
Infinite Ammo
An example for Uzis:
- “First Scan”: value of the current ammo counter.
- Shoot!
- “Next Scan”: value of the updated ammo counter. Repeat 1-3 until you’re confident in the match.
- “Add selected addresses to the addresslist”.
- “Find out what writes to this address”.
- Shoot!
- “Show this address in the dissasembler”.
- Search for code that writes to this address (it probably dereference a pointer with
[]
). - “Tools” → “Auto Assemble” → “Template” → “AOB (Array of Bytes) Injection”:
{ Game : Turbo Overkill.exe
Version:
Date : 2024-05-04
Author : snovvcrash
This script gives infinite Uzis ammo
}
[ENABLE]
aobscanmodule(INF_AMMO_UZIS,GameAssembly.dll,89 5F 30 0F 84 46 FF FF FF) // should be unique
alloc(newmem,$1000,INF_AMMO_UZIS)
label(code)
label(return)
newmem:
code:
//mov [rdi+30],ebx // original
//mov [rdi+30],00000258 // patched: set ammo to 600 (0x258)
push rbx
mov ebx,[rax+08]
mov [rdi+30],ebx
pop rbx
je GameAssembly.dll+3D7CFC
jmp return
INF_AMMO_UZIS:
jmp newmem
nop 4
return:
registersymbol(INF_AMMO_UZIS)
[DISABLE]
INF_AMMO_UZIS:
db 89 5F 30 0F 84 46 FF FF FF
unregistersymbol(INF_AMMO_UZIS)
dealloc(newmem)
{
// ORIGINAL CODE - INJECTION POINT: GameAssembly.dll+3D7DAD
GameAssembly.dll+3D7D91: 2B DE - sub ebx,esi
GameAssembly.dll+3D7D93: 48 8B 80 B8 00 00 00 - mov rax,[rax+000000B8]
GameAssembly.dll+3D7D9A: 8B 48 08 - mov ecx,[rax+08]
GameAssembly.dll+3D7D9D: 78 08 - js GameAssembly.dll+3D7DA7
GameAssembly.dll+3D7D9F: 3B D9 - cmp ebx,ecx
GameAssembly.dll+3D7DA1: 7E 06 - jle GameAssembly.dll+3D7DA9
GameAssembly.dll+3D7DA3: 8B D9 - mov ebx,ecx
GameAssembly.dll+3D7DA5: EB 02 - jmp GameAssembly.dll+3D7DA9
GameAssembly.dll+3D7DA7: 33 DB - xor ebx,ebx
GameAssembly.dll+3D7DA9: 80 7F 64 00 - cmp byte ptr [rdi+64],00
// ---------- INJECTING HERE ----------
GameAssembly.dll+3D7DAD: 89 5F 30 - mov [rdi+30],ebx
// ---------- DONE INJECTING ----------
GameAssembly.dll+3D7DB0: 0F 84 46 FF FF FF - je GameAssembly.dll+3D7CFC
GameAssembly.dll+3D7DB6: 3B 5F 68 - cmp ebx,[rdi+68]
GameAssembly.dll+3D7DB9: 0F 8D 3D FF FF FF - jnl GameAssembly.dll+3D7CFC
GameAssembly.dll+3D7DBF: C6 47 64 00 - mov byte ptr [rdi+64],00
GameAssembly.dll+3D7DC3: E9 0D FF FF FF - jmp GameAssembly.dll+3D7CD5
GameAssembly.dll+3D7DC8: 8B 5F 34 - mov ebx,[rdi+34]
GameAssembly.dll+3D7DCB: 3B DE - cmp ebx,esi
GameAssembly.dll+3D7DCD: 0F 8C FF 01 00 00 - jl GameAssembly.dll+3D7FD2
GameAssembly.dll+3D7DD3: 48 8B 05 36 CE 06 02 - mov rax,[GameAssembly.dll+2444C10]
GameAssembly.dll+3D7DDA: 83 B8 E0 00 00 00 00 - cmp dword ptr [rax+000000E0],00
}
Discover max ammo values of all the weapons:
- “Set breakpoint (Hardware Breakpoint)” on
mov ecx,[rax+08]
.RAX
probably points to a value of a structure with weapons stats. - Shoot!
- Examine the value of
RAX
. - “Tools” → “Dissect data/structures” → Value of
RAX
. - “Structures” → “Define new structure”.
Repeat the algorithm to create a script for any weapon changing the offsets to current and max ammo values.
Click to expand
!Refs
- Absolute beginner: Your first ammo script - FearLess Cheat Engine
- How to Make a Trainer with Cheat Engine [HuniePop Trainer Example] - YouTube
Movement Hacks
Infinite Jumps
To get infinite jumps I’ll make an assumption that there is a byte counter which can take values 0-2
:
- “First Scan”:
0
. - Single jump.
- “Next Scan”:
1
. - Double jump.
- “Next Scan”:
2
. Repeat 1-5 until you’re confident in the match. - “Find out what writes to this address” → “Show this address in the dissasembler”.
We probably want to look for INC
instruction and, for example, patch it to NOP
:
{ Game : Turbo Overkill
Version:
Date : 2024-05-03
Author : snovvcrash
This script gives infinite jumps
}
[ENABLE]
aobscanmodule(NOPs,GameAssembly.dll,FF 83 84 01 00 00) // should be unique
alloc(newmem,$1000,NOPs)
label(code)
label(return)
newmem:
code:
//inc [rbx+00000184]
jmp return
NOPs:
jmp newmem
nop
return:
registersymbol(NOPs)
[DISABLE]
NOPs:
db FF 83 84 01 00 00
unregistersymbol(NOPs)
dealloc(newmem)
{
// ORIGINAL CODE - INJECTION POINT: GameAssembly.dll+3F31BF
GameAssembly.dll+3F3196: 33 D2 - xor edx,edx
GameAssembly.dll+3F3198: 48 8B C8 - mov rcx,rax
GameAssembly.dll+3F319B: E8 20 34 FB FF - call GameAssembly.dll+3A65C0
GameAssembly.dll+3F31A0: 80 BB 5C 01 00 00 00 - cmp byte ptr [rbx+0000015C],00
GameAssembly.dll+3F31A7: 75 1C - jne GameAssembly.dll+3F31C5
GameAssembly.dll+3F31A9: 48 8B 43 18 - mov rax,[rbx+18]
GameAssembly.dll+3F31AD: 48 85 C0 - test rax,rax
GameAssembly.dll+3F31B0: 0F 84 99 02 00 00 - je GameAssembly.dll+3F344F
GameAssembly.dll+3F31B6: 80 B8 13 01 00 00 00 - cmp byte ptr [rax+00000113],00
GameAssembly.dll+3F31BD: 75 06 - jne GameAssembly.dll+3F31C5
// ---------- INJECTING HERE ----------
GameAssembly.dll+3F31BF: FF 83 84 01 00 00 - inc [rbx+00000184]
// ---------- DONE INJECTING ----------
GameAssembly.dll+3F31C5: 48 8B 4B 48 - mov rcx,[rbx+48]
GameAssembly.dll+3F31C9: 48 85 C9 - test rcx,rcx
GameAssembly.dll+3F31CC: 0F 84 7D 02 00 00 - je GameAssembly.dll+3F344F
GameAssembly.dll+3F31D2: 33 D2 - xor edx,edx
GameAssembly.dll+3F31D4: E8 F7 D9 70 01 - call GameAssembly.dll+1B00BD0
GameAssembly.dll+3F31D9: 84 C0 - test al,al
GameAssembly.dll+3F31DB: 0F 85 85 00 00 00 - jne GameAssembly.dll+3F3266
GameAssembly.dll+3F31E1: 38 05 F3 F8 18 02 - cmp [GameAssembly.dll+2582ADA],al
GameAssembly.dll+3F31E7: 8B BB 84 01 00 00 - mov edi,[rbx+00000184]
GameAssembly.dll+3F31ED: 75 13 - jne GameAssembly.dll+3F3202
}
Click to expand
Infinite Dashes
To get infinite dashes I’ll make an assumption that there is a (float) “dash recharge” value which probably resides nearby the jump counter. I will extract .NET assemblies from the IL2CPP GameAssembly.dll with Il2CppDumper and open Assembly-CSharp.dll in dnSpy.
Searching for “dash” gives me dashRechargeDelay
field with the offset of 0xF4
. In the same time, the jumpCount
has the offset of 0x184
.
Click to expand
I’ll calculate the address of dashRechargeDelay
relative to jumpCount
as:
>>> hex(0x26836B266C4-0x184+0xf4)
'0x26836b26634'
Now “Find out what accesses this address” and setting the recharge value to 0
gives me infinite dashes:
{ Game : Turbo Overkill
Version:
Date : 2024-05-06
Author : snovvcrash
This script gives infinite dashes
}
[ENABLE]
aobscanmodule(INF_DASHES,GameAssembly.dll,F3 0F 11 83 F4 00 00 00 0F) // should be unique
alloc(newmem,$1000,INF_DASHES)
label(code)
label(return)
newmem:
code:
//movss [rbx+000000F4],xmm0
mov [rbx+000000F4],0
jmp return
INF_DASHES:
jmp newmem
nop 3
return:
registersymbol(INF_DASHES)
[DISABLE]
INF_DASHES:
db F3 0F 11 83 F4 00 00 00
unregistersymbol(INF_DASHES)
dealloc(newmem)
{
// ORIGINAL CODE - INJECTION POINT: GameAssembly.dll+3EF4C7
GameAssembly.dll+3EF499: 48 85 C9 - test rcx,rcx
GameAssembly.dll+3EF49C: 0F 84 E8 02 00 00 - je GameAssembly.dll+3EF78A
GameAssembly.dll+3EF4A2: 48 8B 15 C7 AC 02 02 - mov rdx,[GameAssembly.dll+241A170]
GameAssembly.dll+3EF4A9: 45 33 C0 - xor r8d,r8d
GameAssembly.dll+3EF4AC: E8 3F DB FE FF - call GameAssembly.dll+3DCFF0
GameAssembly.dll+3EF4B1: 84 C0 - test al,al
GameAssembly.dll+3EF4B3: 74 0A - je GameAssembly.dll+3EF4BF
GameAssembly.dll+3EF4B5: F3 0F 10 05 33 BC 8F 01 - movss xmm0,[GameAssembly.dll+1CEB0F0]
GameAssembly.dll+3EF4BD: EB 08 - jmp GameAssembly.dll+3EF4C7
GameAssembly.dll+3EF4BF: F3 0F 10 05 E5 B8 8F 01 - movss xmm0,[GameAssembly.dll+1CEADAC]
// ---------- INJECTING HERE ----------
GameAssembly.dll+3EF4C7: F3 0F 11 83 F4 00 00 00 - movss [rbx+000000F4],xmm0
// ---------- DONE INJECTING ----------
GameAssembly.dll+3EF4CF: 0F 2F BB F0 00 00 00 - comiss xmm7,[rbx+000000F0]
GameAssembly.dll+3EF4D6: 0F 87 E9 01 00 00 - ja GameAssembly.dll+3EF6C5
GameAssembly.dll+3EF4DC: 80 3D 07 2F 19 02 00 - cmp byte ptr [GameAssembly.dll+25823EA],00
GameAssembly.dll+3EF4E3: 75 13 - jne GameAssembly.dll+3EF4F8
GameAssembly.dll+3EF4E5: 48 8D 0D EC BA 03 02 - lea rcx,[GameAssembly.dll+242AFD8]
GameAssembly.dll+3EF4EC: E8 1F 89 E4 FF - call GameAssembly.il2cpp_get_exception_argument_null+2A0
GameAssembly.dll+3EF4F1: C6 05 F2 2E 19 02 01 - mov byte ptr [GameAssembly.dll+25823EA],01
GameAssembly.dll+3EF4F8: 48 8B 05 D9 BA 03 02 - mov rax,[GameAssembly.dll+242AFD8]
GameAssembly.dll+3EF4FF: F3 0F 10 0D 25 BC 8F 01 - movss xmm1,[GameAssembly.dll+1CEB12C]
GameAssembly.dll+3EF507: 48 8B 88 B8 00 00 00 - mov rcx,[rax+000000B8]
}
Click to expand
The same approach can be followed to enable/disable the God Mode:
// Token: 0x020003C6 RID: 966
[Token(Token = "0x20003C6")]
public class Vitals : MonoBehaviourPun
{
// ...
// Token: 0x04001CF9 RID: 7417
[Token(Token = "0x4001CF9")]
[FieldOffset(Offset = "0x89")]
public bool godMode;
// ...
From Cheat Engine to a Standalone DLL
Having got the appropriate offsets, I can implement the AOB injection logic within a simple standalone DLL in C.
Here’s a PoC for toggling infinite jumps and dashes via F11
and F12
respectively:
#include <windows.h>
typedef struct HookTrampolineBuffers
{
BOOL enabled;
BYTE originalBytes[10];
BYTE patchBytes[10];
DWORD originalBytesSize;
UINT_PTR addressToHook;
} HookTrampolineBuffers;
UINT_PTR find_code_cave(UINT_PTR patchAddr)
{
LPVOID alloc = NULL;
UINT_PTR addr;
BOOL foundMem = FALSE;
for (addr = (patchAddr & 0xFFFFFFFFFFF70000) - 0x70000000;
addr < patchAddr + 0x70000000;
addr += 0x10000)
{
alloc = VirtualAlloc((LPVOID)addr, 0x1000, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (alloc == NULL) continue;
foundMem = TRUE;
break;
}
return foundMem ? (UINT_PTR)alloc : 0;
}
void init_trampoline(HookTrampolineBuffers* hook, UINT_PTR addressToHook, UINT_PTR jumpAddress)
{
BYTE trampoline[10] = {
0xE9, 0x00, 0x00, 0x00, 0x00, // jmp rel32
0x00, 0x00, 0x00, 0x00, 0x00
};
DWORD offset = jumpAddress - addressToHook - 5;
memcpy(&trampoline[1], &offset, sizeof(offset));
memset(&trampoline[5], 0x90, hook->originalBytesSize - 5);
memcpy(hook->patchBytes, &trampoline, hook->originalBytesSize);
}
void init_hook(HookTrampolineBuffers* hook, BYTE* originalBytes, DWORD originalBytesSize, BYTE* extraBytes, DWORD extraBytesSize, DWORD offset)
{
HMODULE hGameAssembly = GetModuleHandleA("GameAssembly.dll");
hook->enabled = FALSE;
hook->originalBytesSize = originalBytesSize;
memcpy(hook->originalBytes, originalBytes, hook->originalBytesSize);
UINT_PTR patchAddr = (UINT_PTR)hGameAssembly + offset;
hook->addressToHook = patchAddr;
UINT_PTR codeCave = find_code_cave(patchAddr);
if (codeCave)
{
if (extraBytes)
memcpy((BYTE*)codeCave, extraBytes, extraBytesSize);
BYTE jmp[5] = {0xE9, 0x00, 0x00, 0x00, 0x00};
DWORD ret = patchAddr - codeCave - extraBytesSize;
memcpy(&jmp[1], &ret, sizeof(ret));
memcpy((BYTE*)((PBYTE)codeCave + extraBytesSize), jmp, sizeof(jmp));
init_trampoline(hook, patchAddr, codeCave);
}
}
BOOL toggle_hook(HookTrampolineBuffers* hook)
{
DWORD oldProtect;
BOOL ret = FALSE;
if (VirtualProtect(
(LPVOID)(hook->addressToHook),
hook->originalBytesSize,
PAGE_EXECUTE_READWRITE,
&oldProtect))
{
if (!hook->enabled)
memcpy((BYTE*)(hook->addressToHook), hook->patchBytes, hook->originalBytesSize);
else
memcpy((BYTE*)(hook->addressToHook), hook->originalBytes, hook->originalBytesSize);
ret = TRUE;
}
VirtualProtect((LPVOID)(hook->addressToHook), hook->originalBytesSize, oldProtect, &oldProtect);
return ret;
}
DWORD WINAPI MainThread(LPVOID param)
{
HookTrampolineBuffers infJumpsHook = { 0 };
BYTE infJumpsOrigin[] = { 0xFF, 0x83, 0x84, 0x01, 0x00, 0x00 };
DWORD infJumpsOffset = 0x3F31BF;
init_hook(&infJumpsHook, infJumpsOrigin, sizeof(infJumpsOrigin), NULL, 0, infJumpsOffset);
HookTrampolineBuffers infDashesHook = { 0 };
BYTE infDashesOrigin[] = { 0xF3, 0x0F, 0x11, 0x83, 0xF4, 0x00, 0x00, 0x00 };
BYTE zeroDashRechargeDelay[] = { 0xC7, 0x83, 0xF4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
DWORD infDashesOffset = 0x3EF4C7;
init_hook(&infDashesHook, infDashesOrigin, sizeof(infDashesOrigin), zeroDashRechargeDelay, sizeof(zeroDashRechargeDelay), infDashesOffset);
while (TRUE)
{
if ((GetAsyncKeyState(VK_F11) & 0x1) && toggle_hook(&infJumpsHook))
infJumpsHook.enabled = !infJumpsHook.enabled;
else if ((GetAsyncKeyState(VK_F12) & 0x1) && toggle_hook(&infDashesHook))
infDashesHook.enabled = !infDashesHook.enabled;
Sleep(5);
}
return 0;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
CreateThread(0, 0, MainThread, hModule, 0, 0);
case DLL_PROCESS_DETACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
Click to expand