Background
I was working on an anti-cheat for a small indie game studio. My goal was to detect internal cheats, and then block external cheats from functioning by guarding pointers.
The Idea 💡
My thought process was if I can avoid updating the original pointer with the real value, no cheat would have a chance of sniping the pointer's real value. The way I did this was by caching the real value of the pointer, then writing an invalid user mode address to the pointer. Once any code tries to access the pointer, it should cause an STATUS_ACCESS_VIOLATION
. Then, I can loop the registers in the exception context record, and overwrite the invalid value with the real value. Then I can return EXCEPTION_CONTINUE_EXECUTION
, causing the CPU to re-execute the instruction but this time with the real value.
What is good about this approach is the real value is never obviously revealed to external memory. On top of that, we can account for when the pointer is updated by an instruction in legitimate memory.
Proof of Concept
LONG WINAPI exception_handler(EXCEPTION_POINTERS* ExceptionInfo)
{
static uint64_t previous_fault_address = 0;
static uint64_t previous_fault_value = 0;
static DWORD previous_rip = 0;
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_ACCESS_VIOLATION)
{
PCONTEXT context = ExceptionInfo->ContextRecord;
ULONG_PTR violationType = ExceptionInfo->ExceptionRecord->ExceptionInformation[0];
uint64_t faulting_address = (uint64_t)ExceptionInfo->ExceptionRecord->ExceptionInformation[1];
uint64_t rip_address = (uint64_t)ExceptionInfo->ContextRecord->Rip;
// ratchet method but works :P
Register registers[] =
{
{"RAX", &context->Rax},
{"RBX", &context->Rbx},
{"RCX", &context->Rcx},
{"RDX", &context->Rdx},
{"RSI", &context->Rsi},
{"RDI", &context->Rdi},
{"RBP", &context->Rbp},
{"RSP", &context->Rsp},
{"RIP", &context->Rip},
{"R8", &context->R8},
{"R9", &context->R9},
{"R10", &context->R10},
{"R11", &context->R11},
{"R12", &context->R12},
{"R13", &context->R13},
{"R14", &context->R14},
{"R15", &context->R15},
};
for (auto pointer : guarded_pointers)
{
// execute, redirect RIP to the real value
if (violationType == 8 && faulting_address == pointer.fake_value)
{
// Set the RIP to the corrected address
ExceptionInfo->ContextRecord->Rip = pointer.real_value;
return EXCEPTION_CONTINUE_EXECUTION;
}
else
{
// Loop through all registers
for (const auto& reg : registers)
{
if (*(reg.value) == pointer.fake_value)
{
// update the value
if (violationType == 1)
{
for (const auto& reg2 : registers)
{
if (reg2.name != reg.name && valid_address(reg2.value)) // attempt to find the new value
{
pointer.real_value = *(reg2.value);
*(reg.value) = pointer.real_value;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
// fix the value
*(reg.value) = pointer.real_value;
return EXCEPTION_CONTINUE_EXECUTION;
}
}
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
return EXCEPTION_CONTINUE_SEARCH;
}