Insecure by Design, Weaponizing Windows against User-Mode Anti-Cheats

The market for cheating in video games has grown year after year, incentivizing game developers to implement stronger anti-cheat solutions. A significant amount of game companies have taken a rather questionable route, implementing more and more invasive anti-cheat solutions in a desperate attempt to combat cheaters, still ending up with a game that has a large cheating community. This choice is understandable. A significant amount of cheaters have now moved into the kernel realm, challenging anti-cheat developers to now design mitigations that combat an attacker who shares the same privilege level. However, not all game companies have followed this invasive path, some opting to use anti-cheats that reside in the user-mode realm.

I've seen a significant amount of cheaters approach user-mode anti-cheats the same way they would approach a kernel anti-cheat, often unknowingly making the assumption that the anti-cheat knows everything, and thus approaching them in an ineffective manner. In this article, we'll look at how to leverage our escalated privileges, as a cheater, to attack the insecure design these unprivileged solutions are built on.

Introduction

When attacking anything technical, one of the first steps I take is to try and put myself in the mind of a defender. For example, when I reverse engineer software for security vulnerabilities, putting myself in the shoes of the developer allows me to better understand the design of the application. Understanding the design behind the logic is critical for vulnerability research, as this often reveals points of weakness that deserve a dedicated look over. When it comes to anti-cheats, the first step I believe any attacker should take is understanding the scope of the application.

What I mean by scope is posing the question, What can the anti-cheat "see" on the machine? This question is incredibly important in researching these anti-cheats, because you may find blind spots on a purely design level. For user-mode anti-cheats, because they live in an unprivileged context, they're unable to access much and must be creative while developing detection vectors. Let's take a look at what access unprivileged anti-cheats truly have and how we can mitigate against it.

Investigating Detection Vectors

The Filesystem

Most access in Windows is restricted by the use of security descriptors, which dictate who has permissions on an object and how to audit the usage of the object.

For example, if a file or directory has the Users group in its security descriptor, i.e

That means pretty much any user on the machine can can access this file or directory. When anti-cheats are in an unprivileged context, they're strictly limited in what they can access on the filesystem, because they're unable to ignore these security descriptors. As a cheater, this provides the unique opportunity to abuse these security descriptors against them to stay under their radars.

Filesystem access is a large part of these anti-cheats because if you run a program with elevated privileges, the unprivileged anti-cheat cannot open a handle to that process. This is due to the difference between their elevation levels, but not being able to open the process is far from being game over. The first method anti-cheats can use to "access" the process is by just opening the process's file. More often than not, the security descriptor for a significant amount of files will be accessible from an unprivileged context.

For example, typically anything in the "Program Files" directory in the C: drive has a security descriptor that permits for READ and EXECUTE access to the Users group or allows the Administrators group FULL CONTROL on the directory. Since the Users group is permitted access unprivileged processes are able to access files in these directories. This is relevant for when you launch a cheating program such as Cheat Engine, one detection vector they have is just reading in the file of the process and checking for cheating related signatures.

NT Object Manager Leaks

Filesystem access is not the only trick user-mode anti-cheats have. The next relevant access these anti-cheats have is a significant amount of query access to the current session. In Windows, sessions are what separates active connections to the machine. For example, you being logged into the machine is a session and someone connected to another user via a Remote Desktop will have their own session too. Unprivileged anti-cheats can see almost everything going on in the session that the anti-cheat runs in. This significant access allows anti-cheats to develop strong detection vectors for a lot of different cheats.

Let's see what unprivileged processes can really see. One neat way to view what's going on in your session is by the use of the WinObj Sysinternals utility. WinObj allows you to see a plethora of information about the machine without any special privileges.

WinObj can be run with or without Administrator privileges, however, I suggest using Administrator privileges during investigation because sometimes you cannot "list" one of the directories above, but you can "list" sub-folders. For example, even for your own session, you cannot "list" the directory containing the BaseNamedObjects, but you can access it directly using the full path \Session\[session #]\BaseNamedObjects.

User-mode anti-cheat developers hunt for information leaks in these directories because the information leaks can be significantly subtle. For example, a significant amount of tools in kernel expose a device for IOCTL communication. Cheat Engine's kernel driver "DBVM" is an example of a more well-known issue.

Since by default Everyone can access the Device directory, they can simply check if any devices have a blacklisted name. More subtle leaks happen as well. For example, IDA Pro uses a mutex with a unique name, making it simple to detect when the current session has IDA Pro running.

Window Enumeration

A classic detection vector for all kinds of anti-cheats is the anti-cheat enumerating windows for certain characteristics. Maybe it's a transparent window that's always the top window, or maybe it's a window with a naughty name. Windows provide for a unique heuristic detection mechanism given that cheats often have some sort of GUI component.

To investigate these leaks, I used Process Hacker's "Windows" tab that shows what windows a process has associated with it. For example, coming back to the Cheat Engine example, there are plenty of windows anti-cheats can look for.

User-mode anti-cheats can enumerate windows via the use of EnumWindows API (or the API that EnumWindows calls) which allow them to enumerate every window in the current session. There can be many things anti-cheats look for in windows, whether that be the class, text, the process associated with the window, etc.

NtQuerySystemInformation

The next most lethal tool in the arsenal of these anti-cheats is NtQuerySystemInformation. This API allows even unprivileged processes to query a significant amount of information about what's going on in your computer.

There's too much data provided to cover all of it, but let's see some of the highlights of NtQuerySystemInformation.

  1. NtQuerySystemInformation with the class SystemProcessInformation provides a SYSTEM_PROCESS_INFORMATION structure, giving a variety amount of info about every process.
  2. NtQuerySystemInformation with the class SystemModuleInformation provides a RTL_PROCESS_MODULE_INFORMATION structure, giving a variety amount of info about every loaded driver.
  3. NtQuerySystemInformation with the class SystemHandleInformation provides a SYSTEM_HANDLE_INFORMATION structure, giving a list of every open handle in the system.
  4. NtQuerySystemInformation with the class SystemKernelDebuggerInformation provides a SYSTEM_KERNEL_DEBUGGER_INFORMATION structure, which tells the caller whether or not a kernel debugger is loaded (i.e WinDbg KD).
  5. NtQuerySystemInformation with the class SystemHypervisorInformation provides a SYSTEM_HYPERVISOR_QUERY_INFORMATION structure, indicating whether or not a hypervisor is present.
  6. NtQuerySystemInformation with the class SystemCodeIntegrityPolicyInformation provides a SYSTEM_CODEINTEGRITY_INFORMATION structure, indicating whether or not various code integrity options are enabled (i.e Test Signing, Debug Mode, etc).

This is only a small subset of what NtQuerySystemInformation exposes and it's important to remember these data sources while designing a secure cheat.

Circumventing Detection Vectors

Time for my favorite part. What's it worth if I just list all the possible ways for user-mode anti-cheats to detect you, without providing any solutions? In this section, we'll look over the detection vectors we mentioned above and see different ways to mitigate against them. First, let's summarize what we found.

  1. User-mode anti-cheats can use the filesystem to get a wide variety of access, primarily to access processes that are inaccessible via conventional methods (i.e OpenProcess).
  2. User-mode anti-cheats can use a variety of objects Windows exposes to get an insight into what is running in the current session and machine. Often times there may be hard to track information leaks that anti-cheats can use to their advantage, primarily because unprivileged processes have a lot of query power.
  3. User-mode anti-cheats can enumerate the windows of the current session to detect and flag windows that match the attributes of blacklisted applications.
  4. User-mode anti-cheats can utilize the enormous amount of data the NtQuerySystemInformation API provides to peer into what's going on in the system.

Circumventing Filesystem Detection Vectors

To restate the investigation section regarding filesystems, we found that user-mode anti-cheats could utilize the filesystem to get access they once did not have before. How can an attacker evade filesystem based detection routines? By abusing security descriptors.

Security descriptors are what truly dictates who has access to an object, and in the filesystem this becomes especially important in regards to who can read a file. If the user-mode anti-cheat does not have permission to read an object based on its security descriptors, then they cannot read that file. What I mean is, if you run a process as Administrator, preventing the anti-cheat from opening the process and then you remove access to the file, how can the anti-cheat read the file? This method of abusing security descriptors would allow an attacker to evade detections that involve opening processes through the filesystem.

You may feel wary when thinking about restricting unprivileged access to your process, but there are many subtle ways to achieve the same result. Instead of directly setting the security descriptor of the file or folder, why not find existing folders that have restricted security descriptors?

For example, the directory C:\Windows\ModemLogs already prevents unprivileged access.

The directory by default disallows unprivileged applications from accessing it, requiring at least Administrator privileges to access it. What if you put your naughty binary in here?

Another trick to evade information leaks in places such as the Device directory or BaseNamedObjects directory is to abuse their security descriptors to block access to the anti-cheat. I strongly advise that you utilize a new sandbox account, however, you can set the permissions of directories such as the Device directory to deny access to certain security principles.

Using security descriptors, we can block access to these directories to prevent anti-cheats from traversing them. All we need to do in the example above is run the game as this new user and they will not have access to the Device directory. Since security descriptors control a significant amount of access in Windows, it's important to understand how we can leverage them to cut off anti-cheats from crucial data sources.

The solution of abusing security descriptors isn't perfect, but the point I'm trying to make is that there is a lot of room for becoming undetected. Given that most anti-cheats have to deal with a mountain full of compatibility issues, it's unlikely that most abuse would be detected.

Circumventing Object Detection and Window Detection

I decided to include both Object and Window detection in this section because the circumvention can overlap. Starting with Object detection, especially when dealing with tools that you didn't create, understanding the information leaks a program has is the key to evading detection.

The example brought up in investigation was the Cheat Engine driver's device name and IDA's mutex. If you're using someone elses software, it's very important that you look for information leaks, primarily because they may not be designed with preventing information leaks in mind.

For example, I was able to make TitanHide undetected by several user-mode anti-cheat platforms by:

  1. Utilizing the previous filesystem trick to prevent access to the driver file.
  2. Changing the pipe name of TitanHide.
  3. Changing the log output of TitanHide.

These steps are simple and not difficult to conceptualize. Read through the source code, understand detection surface, and harden these surfaces. It can be as simple as changing a few names to become undetected.

When creating your own software that has the goal of combating user-mode anti-cheats, information leaks should be a key step when designing the application. Understanding the internals of Windows can help a significant amount, because you can better understand different types of leaks that can occur.

Transitioning to a generic circumvention for both object detection and window detection is through the abuse of multiple sessions. The mentioned EnumWindows API can only enumerate windows in the current session. Unprivileged processes from one session can't access another session's objects. How do you use multiple sessions? There are many ways, but here's the trick I used to bypass a significant amount of user-mode anti-cheats.

  1. I created a new user account.
  2. I set the security descriptor of my naughty tools to be inaccessible by an unprivileged context.
  3. I ran any of my naughty tools by switching users and running it as that user.

Those three simple steps are what allowed me to use even the most common utilities such as Cheat Engine against some of the top user-mode anti-cheats. Unprivileged processes cannot access the processes of another user and they cannot enumerate any objects (including windows) of another session. By switching users, we're entering another session, significantly cutting off the visibility anti-cheats have.

A mentor of mine, Alex Ionescu, pointed out a different approach to preventing an unprivileged process from accessing windows in the current session. He suggested that you can put the game process into a job and then set the JOB_OBJECT_UILIMIT_HANDLES UI restriction. This would restrict any processes in the job from accessing "USER" handles of processes outside of the job, meaning that the user-mode anti-cheat could not access windows in the same session. The drawback of this method is that the anti-cheat could detect this restriction and could choose to crash/flag you. The safest method is generally not touching the process itself and instead evading detection using generic methods (i.e switching sessions).

Circumventing NtQuerySystemInformation

Unlike the previous circumvention methods, evading NtQuerySystemInformation is not as easy. In my opinion, the safest way to evade detection routines that utilize NtQuerySystemInformation is to look at the different information classes that may impact you and ensure you do not have any unique information leaks through those classes.

Although NtQuerySystemInformation does give a significant amount of access, it's important to note that the data returned is often not detailed enough to be a significant threat to cheaters.

If you would like to restrict user-mode anti-cheat access to the API, there is a solution. NtQuerySystemInformation respects the integrity level of the calling process and significantly limits access to Restricted Callers. These callers are primarily those who have a token below the medium integrity level. This sandboxing can allow us to limit a significant amount of access to the user-mode anti-cheat, but with the cost of a potential detection vector. The anti-cheat could choose to crash if its ran under a medium integrity level which would stop this trick.

When at a low integrity level, processes:

  1. Cannot query handles.
  2. Cannot query kernel modules.
  3. Can only query other processes with equal or lower integrity levels.
  4. Can still query if a kernel debugger is present.

When testing with Process Hacker, I found some games with user-mode anti-cheats already crashed, perhaps because they received an ACCESS_DENIED error for one of their detection routines. An important reminder as with the previous job limit circumvention method. Since this trick does directly touch the process, it is possible anti-cheats can simply crash or flag you for setting their integrity level. Although I would strongly suggest you develop your cheats in a manner that does not allow for detection via NtQuerySystemInformation, this sandboxing trick is a way of suppressing some of the leaked data.

Test Signing

If I haven't yet convinced you that user-mode anti-cheats are easy to circumvent, it gets better. On all of the user-mode anti-cheats I tested test signing was permitted, allowing for essentially any driver to be loaded, including ones you create.

If we go back to our scope, drivers really only have a few detection vectors:

  1. User-mode anti-cheats can utilize NtQuerySystemInformation to get basic information on kernel modules. Specifically, the anti-cheat can obtain the kernel base address, the module size, the path of the driver, and a few other properties. You can view exactly what's returned in the definition of RTL_PROCESS_MODULE_INFORMATION. This can be circumvented by basic security practices such as ensuring the data returned in RTL_PROCESS_MODULE_INFORMATION is unique or by modifying the anti-cheat's integrity level.
  2. User-mode anti-cheats can query the filesystem to obtain the driver contents. This can be circumvented by changing the security descriptor of the driver.
  3. User-mode anti-cheats can hunt for information leaks by your driver (such as using a blacklisted device name) to identify it. This can be circumvented by designing your driver to be secure by design (unlike many of these anti-cheats).

Circumventing the above checks will result in an undetected kernel driver. The information leak prevention can be difficult, however, if you know what you're doing, you should understand what APIs may leak certain information accessible by these anti-cheats.

I hope I have taught you something new about combating user-mode anti-cheats and demystified their capabilities. User-mode anti-cheats come with the benefit of having a less invasive product that doesn't infringe on the privacy of players, but at the same time are incredibly weak and limited. They can appear scary at first, given their usual advantage in detecting "internal" cheaters, but we must realize how weak their position truly is. Unprivileged processes are as the name suggests unprivileged. Stop wasting time treating these products as any sort of challenge, use the operating system's security controls to your advantage. User-mode anti-cheats have already lost, quit acting like they're any stronger than they really are.