FPS game anti-cheat system design: drive layer to achieve countermeasures

Category: Tags: ,

Series of articles:

FPS game anti-cheat system design: signature scanning and heuristic scanning

FPS game anti-cheat system design: API call backtracking

This article will introduce the most common driving layer anti-cheating protection implementation principle, which is also the principle used by the current mainstream anti-cheating system BE EAC.

1. “Drive Protection”:

There are generally two kinds of so-called driver protection. One is hypervisor, which is very strong and takes over the common read and write APIs of the system kernel to prevent read and write. However, the knowledge involved is very complicated and should not be discussed in this article. The other set is commonly used stripping Handle callbacks, ordinary plug-ins need to use NtopenProcess to open a process handle to read and write the game process, and NtopenProcess will trigger the Windows ObRegisterCallbacks callback. At this time, as long as the driver layer captures these callbacks and strips the key permission handle (such as R/W) to prevent The process is read and written.

Key code:

NTSTATUS InstallCallBacks()
{
 NTSTATUS NtHandleCallback = STATUS_UNSUCCESSFUL;
 NTSTATUS NtThreadCallback = STATUS_UNSUCCESSFUL;
 OB_OPERATION_REGISTRATION OBOperationRegistration[2];
 OB_CALLBACK_REGISTRATION OBOCallbackRegistration;
 REG_CONTEXT regContext;
 UNICODE_STRING usAltitude;
 memset(&OBOperationRegistration, 0, sizeof(OB_OPERATION_REGISTRATION));
 memset(&OBOCallbackRegistration, 0, sizeof(OB_CALLBACK_REGISTRATION));
 memset(&regContext, 0, sizeof(REG_CONTEXT));
 regContext.ulIndex = 1;
 regContext.Version = 120;
 RtlInitUnicodeString(&usAltitude, L"1000");
 if ((USHORT)ObGetFilterVersion() == OB_FLT_REGISTRATION_VERSION)
 {
  OBOperationRegistration[1].ObjectType = PsProcessType;
  OBOperationRegistration[1].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
  OBOperationRegistration[1].PreOperation = MyHandleProcessCallbacks;
  OBOperationRegistration[1].PostOperation = HandleAfterCreat;
  OBOperationRegistration[0].ObjectType = PsThreadType;
  OBOperationRegistration[0].Operations = OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE;
  OBOperationRegistration[0].PreOperation = MyHandleThreadCallbacks;
  OBOperationRegistration[0].PostOperation = HandleAfterCreat;
  OBOCallbackRegistration.Version = OB_FLT_REGISTRATION_VERSION;
  OBOCallbackRegistration.OperationRegistrationCount = 2;
  OBOCallbackRegistration.RegistrationContext = &regContext;
  OBOCallbackRegistration.OperationRegistration = OBOperationRegistration;
  NtHandleCallback = ObRegisterCallbacks(&OBOCallbackRegistration, &g_CallbacksHandle); 
// Register The CallBack
  if (!NT_SUCCESS(NtHandleCallback))
  {
   if (g_CallbacksHandle)
   {
    ObUnRegisterCallbacks(g_CallbacksHandle);
    g_CallbacksHandle = NULL;
   }
   //DebugPrint("[DebugMessage] Failed to install ObRegisterCallbacks: 0x%08X.\n", NtHandleCallback);
   return STATUS_UNSUCCESSFUL;
  }
 }
 return STATUS_SUCCESS;
}

Key callback:

OB_PREOP_CALLBACK_STATUS MyHandleThreadCallbacks(PVOID RegistrationContext, POB_PRE_OPERATION_INFORMATION OperationInformation)
{
if (PID of the protected process == -1)
  return OB_PREOP_SUCCESS;
 ULONG ulProcessId = (ULONG)PsGetThreadProcessId((PETHREAD)OperationInformation->Object);
 ULONG myProcessId = (ULONG)PsGetThreadProcessId((PETHREAD)PsGetCurrentThread());
if (ID_ALIGN(ulProcessId) == ID_ALIGN(PID of the protected process) && myProcessId != ulProcessId)
 {
  if (OperationInformation->Operation == OB_OPERATION_HANDLE_CREATE)
  {
   if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & 
PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION)
 {
    OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_OPERATION;
   }
   if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_READ) == PROCESS_VM_READ)
 {
    OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_READ;
   }
   if ((OperationInformation->Parameters->CreateHandleInformation.OriginalDesiredAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE)
 {
    OperationInformation->Parameters->CreateHandleInformation.DesiredAccess &= ~PROCESS_VM_WRITE;
   }
  }
 }
 return OB_PREOP_SUCCESS;
}

As you can see, the R/W/OP permissions are stripped from the callback, and the R3 plug-in cannot use openprocess to read and write processes.

Next is the confrontation stage,

2. Confrontation

It is well known that the parent process of a process has the highest handle of this process, so it can be operated by injecting into these processes and then hijacking these handles. Refer to my previous method of bypassing antivirus software.

These handles become an annoying existence, so we have to find a way to strip these handles! Prevent plug-ins from using these handles for reading and writing operations!

There are three steps to peel off the handle:

1. Traverse all processes and get a handle with read and write permissions

2. Get the Eprocess of these processes, and then eprocess gets the handleable,

3. Stripping

First is the structure

typedef struct _SYSTEM_HANDLE_INFORMATION
{
 ULONG ProcessId;//Process identifier
 UCHAR ObjectTypeNumber;//Type of object opened
 UCHAR Flags;//Handle attribute flag
 USHORT Handle;//Handle value, which uniquely identifies a handle among the handles opened by the process
 PVOID Object;//This is the address of the EPROCESS corresponding to the handle
 ACCESS_MASK GrantedAccess;//Access permissions for handle objects
}SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX
{
 ULONG NumberOfHandles;
 SYSTEM_HANDLE_INFORMATION Information[655360];
}SYSTEM_HANDLE_INFORMATION_EX, * PSYSTEM_HANDLE_INFORMATION_EX;

Then through the operation of NtQuerySystemInformation id 0x10 to traverse all process handles

PSYSTEM_HANDLE_INFORMATION_EX QueryHandleTable()
{
 ULONG cbBuffer = sizeof(SYSTEM_HANDLE_INFORMATION_EX);
 LPVOID pBuffer = (LPVOID)ExAllocatePoolWithTag(NonPagedPool, cbBuffer, POOL_TAG);
 PSYSTEM_HANDLE_INFORMATION_EX HandleInfo = nullptr;
 if (pBuffer)
 {
  pfn_NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)0x10, pBuffer, cbBuffer, NULL);
  HandleInfo = (PSYSTEM_HANDLE_INFORMATION_EX)pBuffer;
 }
 return HandleInfo;
}

Then simply traverse the queried information, and then lock the process to get eprocess

for (int i = 0; i < HandleInfo->NumberOfHandles; i++)
  {
   //7 Is the process attribute
   if (HandleInfo->Information[i].ObjectTypeNumber == 7 || 
HandleInfo->Information[i].ObjectTypeNumber == 
OB_TYPE_INDEX_PROCESS || HandleInfo->Information[i].ObjectTypeNumber
 == OB_TYPE_INDEX_THREAD)
   {
    if (g_FlagProcessPid == (HANDLE)-1)
     break;
    if (HandleInfo->Information[i].ProcessId == 
(ULONG)g_FlagProcessPid || HandleInfo->Information[i].ProcessId == 4)
     continue;
    bool bCheck = ((HandleInfo->Information[i].GrantedAccess & PROCESS_VM_READ) == PROCESS_VM_READ ||
     (HandleInfo->Information[i].GrantedAccess & PROCESS_VM_OPERATION) == PROCESS_VM_OPERATION ||
     (HandleInfo->Information[i].GrantedAccess & PROCESS_VM_WRITE) == PROCESS_VM_WRITE);
    PEPROCESS pEprocess = (PEPROCESS)HandleInfo->
Information[i].Object;
    if (pEprocess) {
     HANDLE handle_pid = *(PHANDLE)((PUCHAR)pEprocess +
 g_OsData.UniqueProcessId);
     HANDLE handle_pid2 = *(PHANDLE)((PUCHAR)pEprocess + 
g_OsData.InheritedFromUniqueProcessId);
     if (bCheck && (handle_pid == g_FlagProcessPid || 
handle_pid2 == g_FlagProcessPid)) {
      pEprocess = NULL;
    NTSTATUS status = PsLookupProcessByProcessId
((HANDLE)HandleInfo->Information[i].ProcessId, &pEprocess);

//Got eprocess
     }
    }
   }
}

There is a handleable in the Eprocess structure called objtable. You can easily see it through windbg+ symbol table or direct online search:

Then use the ExEnumHandleTable function to perform the stripping operation, but please note that the second parameter of win7 and win10, which is EnumHandleProcedure, is different! Be sure to separate

  if (NT_SUCCESS(status)) {
       //DebugPrint("Full Acess Handle! pid: %d \n", HandleInfo->Information[i].ProcessId);
       PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE*)((PUCHAR)pEprocess + g_OsData.ObjTable);
       ExEnumHandleTable(HandleTable, g_isWin7 ? (DWORD64*)&StripHandleCallback_win7 : (DWORD64*)&StripHandleCallback_win10, (PVOID)HandleInfo->Information[i].Handle, NULL);
       ObDereferenceObject(pEprocess);
      }

WIN7:

WIN10:

Precautions:

This stripping cannot be stripped at the first time (for example, if you set createprocessnotify, the process will be stripped immediately after the process starts, which will cause the program to fail to run).

That’s it! Now any R3 plug-in can’t read and write your program effectively!

But! This is not over!

You have to face the following things:

1. Direct drive kernel read/write or injection with digitally signed external driver

2. Kernel read/write or injection of external drivers without digital signature

3. \Device\PhysicalMemory permission abuse

4. Virtual Machine

3. Upgrade confrontation
The above protection can only protect R3. The other party loads a driver to directly read and write the physical memory of your process. So I want to upgrade the confrontation. This part of the code will not be posted to prevent some companies from copying, and talk about the specific ideas :

1. Direct drive kernel read/write or injection with digitally signed external driver

This is best done. The external driver will definitely add VMP and whether there is a string of game process. You only need to judge whether there is a VMP packer or whether there is a program process string. If there is, upload it to the cloud for manual analysis. The digital signature is expensive, and it can be blacklisted after confirming that it is a plug-in.

2. Plug-in drivers without digital signatures. These are mainly drivers that exploit vulnerabilities, such as CPUZ, GPUZ, VMBOX, Speedfan, and other drivers. Those drivers are not strictly restricted to their own calling interface, which causes them to be called by other plug-ins. Write the shellcode of the plug-in driver to the physical memory, then fix the redirection and hook an API to start the plug-in driver. The feature is that there is no module and pchunter cannot detect.

There are probably several countermeasures for this:

1. Test communication

Because it is a manually mapped driver, they cannot use the IO code to communicate with the R3 plug-in body, so they must find another way. Common examples include but not limited to:

1. Create a new thread, communicate with anonymous channels and shared memory

Detection: Traverse all system threads, determine whether the start address of the thread, the RIP address is outside the normal driver module, determine whether the start byte of the thread is JMP EAX, if it is inseparable from the tenth, it is the malicious driver of the thread, of course this There are many false positives in the method.For example, Tencent TP likes ManualMap to go into the system space in order to hide its own code (I don’t know if it will not be done this way), resulting in false positives.

2. Hijack the normal IO handle and use it as an external io handle

Scan all the IO control handles of the normal drive to determine whether it is outside the normal drive module. If it is, it is hijacked. In addition, there is also a false alarm. All should not be blocked on the spot, but to upload memory information to continue analysis.

3.hook kernel function, used to transmit buff communication

Detection hook

2. Detection body

Some easily exploitable drivers have their own poolnames. If these pool names exist, let them restart the computer.

PiDDBCacheEntry: This is an undisclosed structure by Microsoft. This structure stores all loaded driver information, so you can traverse this structure to get the timestamp and then determine whether it is the timestamp of the used driver. If so, let them restart the computer and report it. .

Check event log: same as PiDDBCacheEntry.

3. \Device\PhysicalMemory permission abuse

\Device\PhysicalMemory is a handle that allows R3 programs to directly access physical memory

System permissions can have read and write process permissions

Administrator authority can only read but not write

user cannot read or write at all

For the plug-in, use the administrator to run the plug-in, and then abuse the physical memory handle to do the basic perspective function. Or pass the PPL protection, inject code into system.exe to get the highest read and write permissions.

In order to prevent this, the process is generally traversed through the handle table and found that the process with this physical memory handle is not system.exe and directly alarms and asks the user to exit before continuing

Do you think it is invulnerable?

There is also something called a hypervisor, which is called a virtual machine. A plug-in attacker can completely let the system enter his hypervisor, and then use EPT, NTP or msr hook to hijack the regular ssdt function and kernel function, and return your anti-cheating function to normal For example, the traversal handle mentioned earlier uses NtQuerySystemInformation, and the plug-in can hook this ssdt function to return you a fake thread list.

So a qualified anti-cheat is to be able to detect whether it is running in a virtual machine:

It’s actually very simple. You don’t need to detect the CPU ID, just detect the running time of the __rdtsc instruction. Because if the __rdtsc is not processed in the virtual machine, its __rdtsc time will be slower than the real machine. So you can directly pass the judgment_ _rdtsc execution time is abnormal, so as to determine whether it is in the virtual machine. Of course, there will be false positives, so try several times.

But if you judge the virtual machine, it will represent:

There will be problems with Microsoft’s hyperv-V service, problems with systems without hard drives, and problems with cloud computers.

So please confirm whether you want to make a virtual machine judgment. Of course, there is a better way to detect msr hook and ept hook. I will not go into it here. After all, there are very few people in the world who can independently develop virtual machines.

 

To sum up
The confrontation between anti-cheating and cheating is a cat-and-mouse game, and the technology is updated every year. For example, in 2020, the fist FPS and faceit have begun to hook globally to drive iat to prevent vulnerable driver loading.

It is impossible for this article to introduce all detection and anti-detection technologies. One day this article will be out of date. But this article should be inspiring for many people who want to learn anti-cheat systems.

 

Reviews

There are no reviews yet.

Be the first to review “FPS game anti-cheat system design: drive layer to achieve countermeasures”

Your email address will not be published. Required fields are marked *