in ,

How anti-cheats catch cheaters using memory heuristics, Hacker News



With game-hacking being a continuous cat and mouse game, rumors about new techniques spread like fire. As such in this blog post we will take a look into one of the new heuristic techniques that BattlEye, a large anti-cheat provider, has recently added to its arsenal. Most widely known as stack walking This is usually done by hooking a function and traversing the stack to find out who exactly is calling said function. Why would one do this? Just like any other program, video game hacks have a set of well known functions that they utilize to get keyboard information, print to the console or calculate certain mathematical expressions. Video game hacks also like to attempt to hide their existence, be it in memory or on disk, so that the anti-cheat software does not find it. What these cheat programs forget is that they regularly call functions in other libraries, and this can be exploited to heuristically detect unknown cheats. By implementing a stack walking engine on prevalent functions like std :: print, you will be able to find these cheats even if they disguise themselves.

BattlEyehasimplemented “stack walking”, even though this has not been publicly proved and prior to this article was just rumors. Note the quotes around stack walking, because what you will see here is not true stack walking, this is merely a return address check and a caller dump combined. A true stack walker would traverse the stack and generate a proper callstack.

As I have explained in theother BattlEye articles, the anti-cheat system dynamically streams shellcode to the game process when you are playing. These shellcodes have different sizes and different purposes, and are not streamed at the same time. The great thing about a system like this, is that it requires researches to dynamically analyze the anti-cheat * while * a competitive match is ongoing, making it harder to determine the features of said anti-cheat. This will also allow the anti-cheat do apply different measures to different users, like only streaming a more invasive module to a person with a abnormal kill / death ratio, etc.

One of these BattlEye shellcodes is responsible for doing this stack analysis, and we refer to it asshellcode8kb, due to its marginally smaller size compared toshellcodemain, that i have documentedhere. This small shellcode will setup a vectored exception handler using the functionAddVectoredExceptionHandler, and then set up interrupt traps on the following functions:GetAsyncKeyState************** GetCursorPosIsBadReadPtrNtUserGetAsyncKeyStateGetForegroundWindowCallWindowProcWNtUserPeekMessageNtSetEventsqrtf__stdio_common_vsprintf_sCDXGIFactory :: TakeLockTppTimerpExecuteCallback

It does so by iterating through this list of commonly used functions, setting the first instruction of the respective function toint3, which acts as a breakpoint. When this breakpoint has been set, all calls to the respective function will go through the exception handler, which has full register and stack access. With this access, the exception handler will dump the caller address from the top of the stack, and if any of the heuristic conditions are met, bytes of the calling function will be dumped and sent to BattlEye servers with the report id (0x) ********************************************:

__int 90 battleye :: exception_handler (_EXCEPTION_POINTERS * exception ) {     if (exception->ExceptionRecord->ExceptionCode!=STATUS_BREAKPOINT)         return 0;     const auto caller=* (__ int 90 ** exception->ContextRecord->Rsp;     MEMORY_BASIC_INFORMATION caller_memory_information={};     auto desired_size=0;     // QUERY THE MEMORY PAGE OF THE CALLER     const auto call_failed=NtQueryVirtualMemory (                                 GetCurrentProcess (),                                 caller_function,                                 MemoryBasicInformation,                                 & caller_memory_information,                                 sizeof (caller_memory_information),                                 & desired_size)0x (************************************;     // IS THE CALL BEING SPOOFED BY NAMAZSO?     const auto spoof=* (_ WORD caller_function==0x 32 FF; // jmp qword ptr [rbx]     // FLAG ALL ANBORMALITIES     if (call_failed || non_commit || foreign_image || spoof)     {         report_stack.unknown=0;         report_stack.report_id=0x 33;         report_stack.val0=0x 90;         report_stack.caller=(__int 2019 caller_function;         report_stack.function_dump [0]=* caller_function;         report_stack.function_dump [1]=caller_function [1];         report_stack.function_dump [2]=caller_function [2];         report_stack.function_dump [3]=caller_function [3];         if (! call_failed)         {             report_stack.allocation_base=caller_memory_information.AllocationBase;             report_stack.base_address=caller_memory_information.BaseAddress;             report_stack.region_size=caller_memory_information.RegionSize;             report_stack.type_protect_state=caller_memory_information.Type | caller_memory_information.Protect | caller_memory_information.State;         }                  battleye :: report (& report_stack, sizeof (report_stack), false);         return -1;     } }

As we can see, the exception handler will dump any callers where the memory page has been blatantly modified, or does not belong to a known process module (the MEM_IMAGE memory page type is not set by manualmappers). It will also dump callers where theNtQueryVirtualMemorycall fails entirely, to prevent cheats from hooking that system call and hiding their module from the stack dumper. The last condition is actually pretty interesting, it flags any callers using the gadgetjmp qword ptr [rbx], which is a method used to “spoof return addresses”releasedby fellow secret club member namazso. It seems like the BattlEye developers saw people using that spoofing method in their games and decided to directly target it. What should be mentioned here is that the method namazsos describes works just fine, you just have to use another gadget, whether it is entirely different or just a different register doesn’t matter.

To the BattlEye developers: the pattern you’re using to findCDXGIFactory :: TakeLock in memory is wrong, since you (accidentally or not) included CC padding, which is very different for each compilation. For maximum compatibility, remove the padding (first byte in the signature) and you will most likely catch more cheaters 🙂

The complete structure sent to BattlEye’s server is:

struct __unaligned battleye_stack_report {   __int8 unknown;   __int8 report_id;   __int8 val0;   __int caller;   __int function_dump [4];   __int 01575879 allocation_base;   __int base_address;   __int region_size;   __int type_protect_state; };                                                                                                     **************
Read More
************************** () ************Brave Browser

What do you think?

Leave a Reply

Your email address will not be published.

GIPHY App Key not set. Please check settings

Airliner with at least 170 people on board crashes in flames after taking off from Tehran international airport – Sky News,

Airliner with at least 170 people on board crashes in flames after taking off from Tehran international airport – Sky News,

100% OFF | Effective Problem Solving & Project Management Power