DEV Community

Cover image for Tips for writing an Anti-Cheat
Igor Segalla
Igor Segalla

Posted on

Tips for writing an Anti-Cheat

If you have ever been curious about how an anti-cheat works or if you plan to write your own, then in this post I'm going to give you a few tips on how to detect and protect your application from these well-known cheaters.

It is worth noting that in this post I'm going to emphasize the client-side part, and not the server. To avoid future problems and have a totally secure application, the best you can do is to have a well written program that doesn't allow the exploitation of faults by third parties in any way.


Anti-Debugging

One of the best known and most used methods to protect a program from an intruder is to prevent users from debugging it and thus reverse engineer it.

Reverse engineering is the process of discovering the technological principles and functioning of a device, object or system, through the analysis of its structure, function and operation.

I made a collection of 12 different anti-debugging methods while working on my anti-cheat. All the ones I found are efficient and fully functional. Below you can check out these methods and use them as you like. Just keep in mind that the code with all the methods is in a separate Gist so as not to pollute this post.

Find Window

It is a very common method among the existing anti-cheats, which consists of searching for a process that is running by the name of the window. As an example, it is to make that whenever there is a window with the word hacker, the anti-cheat detects this process as malicious software (is quite common for this method to detect false positive).

I use two possible ways of Find Window: one of them is to check if there is a window with the keyword chosen by me; and the other one is to analyze all HANDLES made so far and check if it could be a malicious software.

Find by Window name

As previously said, this method is nothing more than using the FindWindow function of the Windows API and check if it has any window with the chosen words. If it returns a valid HANDLE, it means that the window has been found.

if( FindWindow( "hacker", NULL ) != NULL )
     HackerDetected();
Enter fullscreen mode Exit fullscreen mode

Deep search for a Window

This method does a deeper search for each opened process, trying to search for some pattern that identifies that program as malicious software.

We used the EnumWindows and EnumChildWindows function to iterate through the existing HANDLEs recursively.

EnumWindows( (WNDENUMPROC)EnumWindowsProc, ( LPARAM )nullptr );
void EnumWindowsProc( HWND hWnd, LPARAM lParam ) {
    EnumChildWindows(...);
}
Enter fullscreen mode Exit fullscreen mode

If we have the HANDLE of each window, and of each element within the window (every UI object in Win32 is a HANDLE), we can make some observations and an analysis together to check whether that is or isn't a malicious program. Observations such as:

  • Having a specfic text anywhere inside the window
  • Having an UI element with a specific Window Style (size, color)
  • Keyword is found in any Win32 UI element

You can take a cheat tool and identify the pattern of its UI, such as the size of the buttons, the style, the text etc, and so have our anti-cheat whenever it encounters these patterns, already identify it as a malicious program, for example.

Avoiding injected modules

Obviously, we don't want any DLL to be injected into our process and so it has access to our code while executing, memory etc.

To avoid this kind of problem, the solution is quite simple: we check all the modules present within our application, and if there is any unknown module in the middle, we detect it as an intruder and we expel it from our execution.

Duplicated process instance

A very common method that people use for handling a process's memory is using ReadProcessMemory and WriteProcessMemory. You must first perform an OpenProcess, to assure all the necessary privileges for improper memory manipulation in oder to use these two functions.

The OpenProcess function works by creating a new instance of our process, making it as if two distinct HANDLEs point to the same running process.

We check all instances related to the running process in order to avoid this, and if that instance is from an unknown process, we close the HANDLE of it, not allowing any more memory manipulation (either writing or reading).

Hooking Function

This protection is quite simple, but it can be pretty effective against cheaters who do not have as much knowledge in reverse engineering. This protection can also be added to the others mentioned in this post, therefore, getting even better.

The protection consists of simply checking if a function has been hooked, checking if the memory of the function we want to protect is using Assembly statements of type JMP.

bool __forceinline IsHookAPI( BYTE* pbFunction )
{
  //JMP
  if( pbFunction[0] == 0xE9 )
     return true;
  //JMP DWORD PTR DS:[...]
  if( pbFunction[0] == 0xFF && pbFunction[1] == 0x25 )
     return true;
   return false;
}
Enter fullscreen mode Exit fullscreen mode

To use the IsHookAPI function, we just call it by giving the memory address of the function we want to check. Example:

if( IsHookAPI( (BYTE*)&GetTickCount ) )
  //Do something...
Enter fullscreen mode Exit fullscreen mode

In the example above we are checking if the GetTickCount function was hooked to another place, and if it was, the check will return positive and so we can detect a cheater.

Checksum

This is one of the most used methods when it comes to protection, and it can be used in several places. What we do with this method, is basically to check the integrity of our file or memory, hoping that it matches the original. If this integrity check doesn't match the expected, we can conclude that there has been a change in the file/memory and identify the user as a cheater.

A checksum is a small-sized datum derived from a block of digital data for the purpose of detecting errors that may have been introduced during its transmission or storage. By themselves, checksums are often used to verify data integrity but are not relied upon to verify data authenticity. Source: Wikipedia

You can use this method in important files of your program, the executable file itself or even the section .text from your executable.

The .text section of the executable is where the program code is located. In it we have the permission to read and execute. Therefore, if any user makes any changes in this section, we know that the code has been tampered with.

Calculating the Checksum

The one I choose to use was CRC32, although, there are several ways to calculate the checksum of something. This kind of verification is widely used for error detection in network protocols and storage devices.

This kind of algorithm may not be the best choice for our case, but I chose it because it is relatively simple and very easy to implement.

int VerifyChecksum::GetChecksum( void* pBuffer, DWORD dwSize )
{
   //Calculate the checksum of .text section
   boost::crc_32_type result;
   result.process_bytes( pBuffer, dwSize );
   return result.checksum();
}
Enter fullscreen mode Exit fullscreen mode

If you do not want to implement it on your own, you can use the boost library, where we already have a function made for this.

Protecting function calls

As I mentioned at the beginning of this post, the best way not to need to protect the client side of your program, is to do as much as you can on the server side. However, there are still those things that we have to leave on the client side and we would not like in any way that the user could use.

What we do in this function is to check if the return address of the function we are protecting is within the scope of the program, and also, if it is being executed by a known thread. If any of these checks return negative, we know that the function may be being called by some external program, such as one .dll injected, and thus detect as an improper call.


You can avoid future headaches and be able to protect your project from cheaters with those simple tips. Although some of these methods are quite simple, the cheater must have a considerable level of reverse engineering, and be able to get through all the existing protections (which will be scattered throughout your program).

I emphasize once again, that the best way to protect your program, is to leave delicate workings to the server side, preventing your users from having access to resources that they should not have.

So, architect your server properly!🤗

Top comments (5)

Collapse
 
jeremyong profile image
Jeremy Ong

Pretty comprehensive list of techniques thanks! Another tactic is to encrypt sections of the executable and map them as writable to decrypt before first access at runtime.

Collapse
 
igorsegallafa profile image
Igor Segalla

Cool, this is a good approach. The problem on this, that I thought when I was developing the anti-cheat, it is that you will have to develop a tool to encrypt the sections (I didn’t want to make anything out of main executable) and this would difficult the executable debugging, when used to detect crashes, code failures etc.

Maybe on the future I would like to test it, to check if I would stay with my opinion haha.

Collapse
 
deta19 profile image
mihai

soo, from what i see, this aproach is for c/c++ or java? could i use it in scripting languages, such as php?

Collapse
 
igorsegallafa profile image
Igor Segalla

I used C++ for this, but I believe you could use Java yes. About the PHP, I am not sure, the most of functions that I used was from Windows API, maybe using it with C, could works.

Collapse
 
marcustoledo profile image
Marcus

Great post, it will add a lot to my reverse engineering studies 🇧🇷