Before we dig in, this post should not be construed as an attack on ESEA, anti-cheat software, or fair gaming in general. It is simply an analysis thereof, detailing what the ESEA driver does on your machine. Although analysis will make attack vectors clear and obvious, no code or detailed explanation of how to leverage these points will be given.
ESEA anti cheat has a long standing record of being difficult for cheat users and their developers to make it any significant amount of time without getting hit with a ban from their services. While ESEA contains other countermeasures then just SBD(signature based detection) alone, this writeup will focus on how they catch known cheat software in the wild, and not on lone wolf tampering.
When we look at other software based anti-cheat out there today, we see lots of obfuscation, window name checks, object/handle name checks, handle access checks, and internal or cross process virtual memory scans. In fact, we don't see much deviation from the same basic checks, some of which rely on poorly documented Windows APIs in order to carry out.
The other side of the coin we see not so much a "detect-and-react" scenario but an all out "prevention" scenario. As in, prevent the cheat software from even accessing game memory or game meta-data to carry out its job. These types of mechanisms are typically carried out with more invasive techniques, such as hooking system services, monitoring filesystem access, or making use of ObRegisterCallbacks, which was added after the advent of KPP(patchguard) so anti-virus vendors could restrict access to critical processes and threads, without having to hook system services.
In other cases it's just a mixture of both. Either way, the path to finding a known cheat signature, or preventing it from even being started, has a predictable and all too common set of events that will be followed in order to do so.
ESEA takes a much different, but yet much more effective approach. Perhaps at some point you downloaded some memory imaging software and you created a memory image file on your disk, dumping all contents of DRAM to one large file for forensics purposes, or maybe just because you were bored and wanted to poke through it?
This, in a sense, is close to what ESEA does, minus the the 8, 16 or 32gb file part.
Instead, ESEA maps physical memory pages frame by frame into a user space mapping and scans them at the byte granularity attempting to match signatures of known cheats. A driver is required to at the very least initiate this type of scan, or in some cases, to carry it out entirely from a kernel thread.
There are 4 ways using exported kernel functions to perform this type of scan. In fact there are really only 3, the 4th is just for the nitpickers who would do it in a totally unsupported and performance slaughtering way, we'll get to that.
1. The first way requires the following functions:
- ZwMapViewOfSection (can be user caller, see below)
The rundown: This way requires that the ZwOpenSection caller (must be kernel caller), opens the PhysicalMemory section object in the \Device\ object directory. If the handle is opened in the context of a user process, via KeStackAttachProcess, or an inbound IRP servicing a read/write/ioctl, then user code can use this section handle in subsequent calls to ZwMapViewOfSection, or the higher level MapViewOfFile. In this case, the offset argument, represents the physical page. This is undoubtedly the slowest way to perform an entire system sweep, due to the overhead ZwMapViewOfSection incurs. This is the method ESEA currently uses.
2. The second way requires the following functions:
The rundown: This method is probably the easiest and most straightforward of all options. It simply requires feeding MmMapIoSpace with a physical page and we are provided with a virtual mapping to probe. However since MmMapIoSpace is a kernel routine, it of course requires that our scan be carried out entirely in kernel mode.
3. The third way requires the following functions:
The rundown: MmMapMemoryDumpMdl, while exported, is not documented by Microsoft. The routine is only documented on 3rd party websites. Just build a MDL describing the physical page and pass it as the one and only argument. The routine will map the MDL for you. This routine has much less overhead than MmMapIoSpace let alone ZwMapViewOfSection, with that being said, it clocks in much faster when doing a full system scan.
4. The fourth method requires the following function:
The rundown: This method would be to manipulate the paging structures yourself. Not only would this be unwise, clumsy and idiotic, but in order for the memory manager to not asynchronously bring down the system with a memory corruption bugcheck, you'd have to make sure your work goes uninterrupted by disabling interrupts. Or make your own page tables. Either way you can't be running concurrent tasks on that CPU. Option 4 is certainly a crock pot of disaster unless you know what you are doing.
Let's take note how all 4 options require MmGetPhysicalMemoryRanges. This, in fact, is not optional. I've had a few people ask me questions like "well why can't I just get the amount of physically installed DRAM and scan from 0 to whatever?"
Simply put: Not all addresses put out on the system bus decode to DRAM.
If you have ever done any operating system, legacy bios, or UEFI development, you know all about this.
x86 type architecture follows Von Neumann style architecture. In contrast to something like Harvard, a Von Neumann central processing unit uses the same bus for everything. The stored program is fetched using the same bus, the memory ops carried out be it read or write, are put on the same bus, and the downstream peripherals decode certain ranges and either finish the bus cycle or forward it to a known decode range that can handle the transaction.
Northbridge(functions integrated on cpu today) registers, southbridge registers, and peripheral device registers are all accessed, from the perspective of the CPU, on the same bus. So if we arbitrarily put the address 0xffee0000 out on the bus, not knowing that this is the decode range belonging to some level triggered device that has a read cycle sensitive interrupt indicator, and we send out a read cycle before this device's ISR de-asserts it, the ISR chain will never know the device needed attention. Which would likely lead to a system halt or crash. Another example could be a low latency device that instead of using DMA, just manages an on chip FIFO buffer, so when the CPU is performing PIO on that device, a read cycle instantly makes the new data available on the 2, 4 or 8 byte FIFO buffer.
While the aforementioned cases are probably extreme, they can happen, but a much more likely scenario, would be a machine-check exception. A machine-check is generally not recoverable, and can be brought about by probing memory regions that the firmware said was off-limits.
Ok great, so how does Windows build this table? Answer: firmware. Your legacy bios or your UEFI system which bootstrapped your operating system. Long before the platform firmware even maps your bootloader stub, it detects physically installed DRAM modules, sets up legacy VGA decode ranges, disables transactions to unused ports, sets up ACPI tables, reports usable cores (maybe you disabled HT ;p) and then finally, jumps to your bootloader. Therefore, there is an interface for the OS to query, about what memory ranges belong to DRAM, what ranges are reclaimable, and what ranges are off limits. This information MUST be obeyed. There are no exceptions to this.
Otherwise, the effectiveness of this type of scan is superb. A target cheat module/executable does not even need to be running. It could very well be in file cache, and this means, it will inevitably be in one or more DRAM pages. If it was already run once, Windows is secretly caching a section object for it in case it loads again, in which case it will also be found. Modifies itself on the fly? If it's within the range of a mapped image, those memory writes will just fault in private pages, meaning the original and unmodified pages are still in physical memory.
All in all, this type of scan is an extremely effective means for wreaking havoc on software developed for cheating, and in the end it only boils down to one single function and the data obtained therein. The question is, can you trust said data?