FPS game anti-cheat system design: API call backtracking

Category: Tags: ,

Series of articles:

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

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

0x01 Preface

I am currently making an anti-cheat system for csgo games. The well-known anti-cheat systems for games include TP/NP/BE and EAC, but there is almost no information about anti-cheat systems. Firstly, because there are very few people engaged in binary security, secondly, this aspect is a “trade secret” in some companies “. So I want to show you some of my anti-cheating ideas for games.

0x02 Common injection methods for game plug-ins

At present, most game plug-ins are no longer the previous createremotethread + loadlibary injection method, because most anti-cheating have their own minifilter file filter driver and imageloadcallback mirror loading callback to judge. Most anti-cheat software does this kind of operation in this filter hook:

if(!CheckFileCertificateByR3(FilePatch)){
  //Return the file path to r3, r3 judges whether the digital 
signature of the file is in the whitelist digital signature 
(such as Microsoft digital signature), if it is a whitelisted file, 
let it go, if it is not a whitelisted file, block it
  //Not a whitelisted file... blocked
  block;
}
//Release
pass;

Therefore, it is particularly difficult for plug-ins to directly inject into the game through dll. Therefore, most plug-ins use a fileless injection method. The so-called fileless injection method is to directly open up a memory space in the game process, write the shellcode of the plug-in dll, then manually repair the input table, then parse the pe file header to get dllmain, and then use createremotethread, apc or hook. Let the game execute this memory address, so that the plug-in is injected.

The specific code is as follows (copied from google):

//The following code is from Google
void InjectorDLLByManualMap(const char* filepath, HANDLE hProcess)
{
    LPVOID lpBuffer;
    HANDLE hFile;
    DWORD dwLength;
    DWORD dwBytesRead;
    DWORD dwThreadId;
    ULONG_PTR lpReflectiveLoader;
    LPVOID lpRemoteDllBuffer;
    //open a file
    hFile = CreateFileA(filepath, GENERIC_READ, 0, NULL, OPEN_EXISTING, 
FILE_ATTRIBUTE_NORMAL, NULL);
    //Get file size
    dwLength = GetFileSize(hFile, NULL);
    lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength);
    //Read file
    ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL);
    //Repair import table
    dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer);
    //Allocate a memory space to the game process
    lpRemoteDllBuffer = VirtualAllocEx
(hProcess, NULL, dwLength, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    //Write the file shellcode to the allocated memory space
    WriteProcessMemory(hProcess, lpRemoteDllBuffer, lpBuffer, dwLength, NULL)
    lpReflectiveLoader = (ULONG_PTR)lpRemoteDllBuffer + dwReflectiveLoaderOffset;
    //Start process
    CreateRemoteThread(hProcess, NULL, 1024*1024, 
(LPTHREAD_START_ROUTINE)lpReflectiveLoader, NULL, NULL, &dwThreadId)
}

Its characteristics are: the memory flags are PAGE_EXECUTE_READWRITE, MEM_PRIVATE, no files, no modules, minifilter and imageload callbacks will not be triggered, and plug-in modules cannot be enumerated in the normal way, and the concealment is very high.

0x03 Detect memory loading plug-in

The previous method looks very “invincible”, but in fact it can also be confronted because its characteristics are also very obvious:

The memory attribute is MEM_PRIVATE, and the memory flag is PAGE_EXECUTE_READWRITE. The size will be very large.

So there are several detection methods:

1. Brute force search for PE headers, most of these memory-loaded dlls have pe headers. A memory attribute of mem_private actually has a pe header, which means it is a plug-in. At present, most anti-cheats have this mechanism

Plug-in countermeasure: Erase pe head. Not only pe head, but also all pe features.

2.createthreadcallbacks gets the thread address and judges whether the thread address is in the memory of a memory attribute of mem_private. If it is, it means that it is a plug-in.

Plug-in countermeasures: do not create threads, use hooks to start plug-ins.

3. API call backtracking. Plug-ins always call some api addresses. We can trace back who called the api address, and then determine whether the memory attribute of the calling place is mem_private. There are two methods, one is to hook all key apis, in The hook part uses _returnaddres() to get the call address (in fact, it reads the ESP/RSP register).The second type is to trigger an exception through int3 breakpoints, and use the exception handling function to handle the exception and determine the caller.

Plug-in countermeasure: The first inline hook method, directly write the jump to skip the hook, such as when you hook:

jmp Your hook address

push ebp

push eax

call xxxx;

The plug-in can be called directly from push ebp, and you can bypass it without calling your jmp

The second type of plug-in countermeasure currently has no special countermeasures. Unless the plug-in constructs its own api function to call the lower-level api. Of course, we can confuse the address of the original low-level api, which will be discussed later.

0x04 realizes call backtracking.

In order to implement call backtracking, we need to implement the following steps:

1. Set the exception handler to catch the exception, the code is as follows:

AddVectoredExceptionHandler

2. Copy the original API address to your own memory area, and then fill the original API address as int, the code is as follows:

LPVOID pHOOKAdress;
	pHOOKAdress = Megrez_GetProAdress(pszModuleName, pszProcName);
	vecInt3HookedAdress.push_back((DWORD)pHOOKAdress);		//For detection
	if (pHOOKAdress == 0)
	{
		return 0;
	}
	DWORD dProSize = 0;
	LPBYTE pTemp = (LPBYTE)pHOOKAdress;
	BYTE bTemp = 0;
	for (dProSize = 0; ; )
	{
		bTemp = *pTemp++;
		dProSize++;
		if (bTemp == 0xcc)
		{
			break;
		}
	}
	DWORD dFileSize = dProSize - 1;
	PVOID pNewAddr = VirtualAlloc(NULL, dFileSize, MEM_COMMIT, 
PAGE_EXECUTE_READWRITE);
	if (pNewAddr == NULL)
	{
		return 0;
	}
	Megrez_SetMemoryAttr(pHOOKAdress, dProSize);
	memcpy(pNewAddr, pHOOKAdress, dProSize - 1);
	memset(pHOOKAdress, 0xcc, 1);
	memset((PBYTE)pHOOKAdress + 1, 0xc3, 1);
	memset((PBYTE)pHOOKAdress + 2, 0x90, dProSize - 1  -2);
	memset((PBYTE)pHOOKAdress + 2 + dProSize - 1 - 2 - 1, 0xcc, 1);
	//memset((PBYTE)pHOOKAdress + 2 + dProSize - 3 - 2 , 0xcc, 2);
	mapAdress.insert(pair<DWORD, DWORD>((DWORD)pHOOKAdress, (DWORD)pNewAddr));
	Megrez_SetMemoryAttr(pHOOKAdress, dProSize);
	Megrez_SetMemoryAttr(pNewAddr, dFileSize);

In this way, the original api function will become int3. When called, it will trigger an int3 exception, which is then caught by our exception handling.

3. Query the memory information of the abnormal location. If it is the code called by the meme_private person, report it to the server. The code is as follows (remember, the caller address is saved under the x32 bit is esp, and the caller address is saved under the x64 bit is rsp. ):

 size_t sizeQuery = VirtualQuery((PVOID)caller_function, lpBuffer, sizeof(MEMORY_BASIC_INFORMATION));
	bool non_commit = lpBuffer->State != MEM_COMMIT;
	bool foreign_image = lpBuffer->Type != MEM_IMAGE && lpBuffer->RegionSize > 0x2000;
	bool spoof = *(PWORD)caller_function == 0x23FF; // jmp qword ptr [rbx],This is to prevent being deceived
	return sizeQuery || non_commit || foreign_image || spoof; //return

After handling the exception, we need to jump to the original saved api memory and call it normally (set the memory address saved by eip).

ExceptionInfo->ContextRecord->Eip = mapAdress
[(DWORD)ExceptionInfo->ExceptionRecord->ExceptionAddress];
#ifdef DEBUG
        WCHAR _buf[256] = { 0 };
        swprintf_s(_buf, 256, L"eIP:0x%08X\n", ExceptionInfo->ContextRecord->Eip);
        OutputDebugStringW(_buf);
#endif
        //The exception has been processed, 
and the next exception handler must be called to handle the exception.
        return EXCEPTION_CONTINUE_EXECUTION;
    }
    //Call the next processor
    return EXCEPTION_CONTINUE_SEARCH;

As you can see, in this way, you get the information of the api caller and make a judgment.

(Part of the code refers to BE)

In this way, a thing that can detect most of the memory loading plug-ins is done (who calls who will be detected)

Reviews

There are no reviews yet.

Be the first to review “FPS game anti-cheat system design: API call backtracking”

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