- Status
- Offline
- Joined
- Mar 3, 2026
- Messages
- 481
- Reaction score
- 7
Tired of GenTool's d3d8.dll hooks clashing with your patches? For anyone digging into C&C: Generals or Zero Hour, GenTool is the main hurdle for internal cheats. It uses two primary methods to stop you from patching the .text section or applying VMT/IAT hooks:
Evading VirtualProtect Hooks
If you check your cheat in x64dbg, you'll see VirtualProtect calls jumping straight into a d3d8.dll subroutine. GenTool effectively gates unauthorized protection changes. Since VirtualProtect eventually hits ZwProtectVirtualMemory in ntdll, we can resolve the original syscall by calculating the jump offset GenTool uses to back up the original function.
You can then #define VirtualProtect to use this TrueVirtualProtect wrapper. This skips the hook and restores your ability to patch memory without GenTool blocking the API call.
Neutralizing Engine Checksums
Even with memory access, GenTool will black-screen you if it detects a .text mismatch. This check runs in separate threads at random intervals. If you inject and don't patch, nothing happens—confirming it's a checksum comparison. The solution is simple: find the thread entry point in d3d8.dll and terminate any thread running that subroutine.
Implementation Notes
GenTool isn't that sophisticated; it's just a d3d8 wrapper. Once the threads are dead and the syscalls are clean, the game is wide open for reversing.
Have fun reversing, drop your crash logs below if you hit issues.
- Inline hooks on VirtualProtect and ZwProtectVirtualMemory.
- Background .text section checksum validation.
Evading VirtualProtect Hooks
If you check your cheat in x64dbg, you'll see VirtualProtect calls jumping straight into a d3d8.dll subroutine. GenTool effectively gates unauthorized protection changes. Since VirtualProtect eventually hits ZwProtectVirtualMemory in ntdll, we can resolve the original syscall by calculating the jump offset GenTool uses to back up the original function.
Code:
using ZwProtectVirtualMemory_t = NTSTATUS(__stdcall*)(HANDLE, PVOID*, PULONG, ULONG, PULONG);
BOOL TrueVirtualProtect(LPVOID base, SIZE_T amount, DWORD newProtect, PDWORD oldProtect)
{
static auto ZwProtectVirtualMemory =
reinterpret_cast<ZwProtectVirtualMemory_t>(GetProcAddress(LoadLibraryA("ntdll.dll"), "ZwProtectVirtualMemory"));
if (ZwProtectVirtualMemory == NULL) return FALSE;
static ZwProtectVirtualMemory_t OriginalZwProtectVirtualMemory = NULL;
if (OriginalZwProtectVirtualMemory == NULL)
{
if (*reinterpret_cast<uint8_t*>(ZwProtectVirtualMemory) == 0xE9)
{
uintptr_t ZwProtectVirtualMemoryAddress = reinterpret_cast<uintptr_t>(ZwProtectVirtualMemory);
int relativeJumpOffset = *reinterpret_cast<int*>(ZwProtectVirtualMemoryAddress + 1);
int absoluteJumpOffset = relativeJumpOffset + ZwProtectVirtualMemoryAddress + sizeof(uint8_t) + sizeof(uintptr_t);
// Skip to where GenTool backs up the original logic
OriginalZwProtectVirtualMemory = **reinterpret_cast<ZwProtectVirtualMemory_t**>(absoluteJumpOffset + 130);
}
else
{
OriginalZwProtectVirtualMemory = ZwProtectVirtualMemory;
}
}
return OriginalZwProtectVirtualMemory(GetCurrentProcess(), &base, &amount, newProtect, oldProtect) == 0;
}
You can then #define VirtualProtect to use this TrueVirtualProtect wrapper. This skips the hook and restores your ability to patch memory without GenTool blocking the API call.
Neutralizing Engine Checksums
Even with memory access, GenTool will black-screen you if it detects a .text mismatch. This check runs in separate threads at random intervals. If you inject and don't patch, nothing happens—confirming it's a checksum comparison. The solution is simple: find the thread entry point in d3d8.dll and terminate any thread running that subroutine.
Code:
Pattern patterns::gentool::ChecksumThread = Pattern(
"\xA1\x00\x00\x00\x00\x33\xC4\x50\x8D\x44\x24\x18\x64\xA3\x00\x00\x00\x00\x00\x00\x00\x00\x8B",
"x????xxxxxxxxx????????x"
);
const DWORD ThreadQuerySetWin32StartAddress = 9;
bool KillCodeChecksumThreads()
{
HANDLE threadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (threadSnapshot == INVALID_HANDLE_VALUE) return false;
uintptr_t codeChecksumEntryPoint = patterns::gentool::ChecksumThread.FindInModule("./d3d8.dll");
if (codeChecksumEntryPoint == NULL) { CloseHandle(threadSnapshot); return false; }
codeChecksumEntryPoint -= 19;
THREADENTRY32 currentThread { sizeof(currentThread) };
if (Thread32First(threadSnapshot, ¤tThread))
{
do {
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, currentThread.th32ThreadID);
if (hThread)
{
PVOID threadEntryPoint = NULL;
NtQueryInformationThread(hThread, (THREADINFOCLASS)ThreadQuerySetWin32StartAddress, &threadEntryPoint, sizeof(threadEntryPoint), NULL);
if (threadEntryPoint == (PVOID)codeChecksumEntryPoint)
{
TerminateThread(hThread, 0);
}
CloseHandle(hThread);
}
} while (Thread32Next(threadSnapshot, ¤tThread));
}
CloseHandle(threadSnapshot);
return true;
}
Implementation Notes
- Inject your bypass early, ideally before applying any patches.
- Make sure to link ntdll.lib for NtQueryInformationThread.
- This method completely nukes the integrity checks, meaning you can run your internal hooks without the infamous GenTool black screen.
GenTool isn't that sophisticated; it's just a d3d8 wrapper. Once the threads are dead and the syscalls are clean, the game is wide open for reversing.
Have fun reversing, drop your crash logs below if you hit issues.