Bypassing anti-cheat software (part 1)
By Ricardo
Disclaimer: I do not endorse cheating. All of the following experiments have been done in a controlled environment with test accounts and may or may not be still valid by the time this is published. Also, please keep in mind this was just a proof-of-concept.
By now most of you know that I enjoy some random gaming every now and then - although I often prefer building game servers instead. However, there’s always something that annoyed me: anti-cheat software. I even stopped playing some games because of that, as they often have highly invasive anti-cheat tools - I’m looking at you, Valorant.
The biggest question is: how can you bypass it? And I don’t mean this by disabling it (because it often can be done), but by simply ignoring its existence altogether. You see, anti-cheat software is essentially that: a piece of software. The software, however, runs on hardware, and, if you can’t trust the hardware, you can’t trust the software. So instead of trying to bypass it using software tools, let’s do that with some custom/fancy/hacked hardware instead!
Story time!
Back in the 2000’s I used to play a game called Ragnarök Online. Yeah, I’m that old. It’s a simple MMORPG that gave me countless hours of fun back then. Nowadays it doesn’t have that many players, but it still has a very active community, including here in Brazil.
Anyway, the Brazilian spin of Ragnarok had some quirks and unfortunately was missing some quality of life features. One of them was a way of leaving your merchant shop open while offline - something that was already present in many other “alternative” (non-official) servers. I wanted to avoid that, and I realized that I could just send keystrokes to the game using classic Win32 API. Back then the game was “protected” by a tool called GameGuard, and it clearly didn’t care about such “hacks”. It has been since then, however, migrated to a much more modern tool called EasyAntiCheat. Mind you, this is the same anti-cheat software used in many popular (and way more recent) games, such as Apex, Fortnite, and many others.
A few years back my sister found a way of getting (actual) money using the game’s currency. I won’t go over the details as this most likely breaks the game’s terms of use, but all you need to know is that it was very, very time consuming. This often requires clicking hundreds (if not thousands) of times on the same NPC, which, as you may imagine, can become very annoying. Once I saw that, I asked her why didn’t she automate it with some sort of macro software, and she replied that she tried it, but unfortunately it didn’t work, and only “hardware macros” would work. Interesting.
So, after some research and testing, I realized that this anti-cheat tool is blocking all sorts of things, including keyboard and mouse macros (by software), as well as automation tools such as AutoHotKey. Fun fact, it blocks a VM from running the background, even if the game is running in the host - go figure. So it seems that software is highly monitored/controlled, which is something that was not done back in the GameGuard days. Welp.
To make it short, we can now assume that we cannot approach this by a software perspective. Another indicator that software is an issue here is that, as she told me before, hardware macros (ie. macros that are executed in the keyboard/mouse directly without any software) still work just fine. So if you have the money to spend, for sure you could automate it. Unfortunately these devices were pretty expensive back then, so that was a no-go for either of us. But for sure there must be a simpler/cheaper solution, right?
The Auto-Clicker ©
Well, have you ever disassembled a mouse? Please do if you didn’t, I’ll wait. Seriously, go for it. You won’t regret.
Even though mouse movement technology has evolved in the last decade or so, the clicking part of it buttons did not, and it is still based of simple micro-switches. This means that, if we would want to have something clicking for us all day, we could either have a small stepper motor clicking it. Not far from this:
However, I believe there’s a better way: what if we don’t have the buttons? What if we just replaced them with some small circuit simulating the closing of a circuit (which is what the button is doing)? And that’s what I did: I replaced the buttons with some wires, added a 555 timer to send logical high/low signals (which can be interpreted as press/release by the mouse’s IC) and some extra components to turn it on/off and adjust the speed.
After the prototyping phase was done, I built it. I ripped the guts out of an old mouse my sister wasn’t using anymore (I think it was broken somehow?) and added the necessary components to it:
The potentiometer on top is responsible for adjusting the frequency and the switch on the side is a toggle on/off for the circuit. Many parts of the mouse were removed, including the switches, wheel and light pipe. Here’s how it looks inside:
It for sure ain’t a pretty device, but it works. Just activate the switch and it’ll start clicking around. To define the frequency/speed of clicking, we can use the potentiometer I added to the circuit:
What the scope image above tells us is that the mouse button will be pressed for 240ms and then released for another 240ms, then repeat. Ideally these values would differ (as the “pressed” part of the click is much shorter than the “not pressed”), but this would further complicate my circuit if I recall correctly. To keep it simple, same values for both is more than enough. The 555 triggers the button press by simply acting just like the button and “shorting” (technically it is not shorting, I guess?) the correct pins.
By now you might be asking yourself how can this be useful if you’d also need to move the cursor to click on the correct screen items. Well, Ragnarok has a feature that you can move all in-game windows around, which means that you can position everything in the same location: the NPC, the confirmation button, the option you must select, everything. Add that to the auto-clicker mouse and… well, here, see for yourself:
What is happening here is that the player is doing some transactions with the NPC. These transactions require clicking the NPC, confirming stuff, selecting options, confirming more stuff, and so on. Since we can move the in-game windows, we can easily position everything on top of each other. Then, with a flip of a switch, the mouse starts clicking non-stop, always in the same location. And you are now free to watch some TV or something. This operation would often take the whole day, so being able to leave the computer unattended doing this is a nice thing. Downside is you can’t use the computer to do something else, but this is a topic for later.
The best part of this project was that the anti-cheat software did not detect it! From its perspective, the user has just a simple mouse and is clicking all day long, nothing major. We might discuss later how do detect (and avoid) this, but right now all you need to know is that from the game, and even the OS’s perspective, this is no different than any other mouse. The OS can’t differentiate this from any other standard USB mouse you have lying around. This is very important.
But… do we need a mouse for this? Can’t we emulate/fake it?
Bug’s Fake Mouse ©
What is a mouse? From the OS perspective (and specifically for USB devices), it is just a simple USB device that describes itself as a mouse. We can build that in many other ways. For example, we could just use a microcontroller bitbanging the USB 1.1 protocol with V-USB, or even a Raspberry Pi on USB Gadget Mode for this. And that’s what I did.
This version of the “mouse” is simply a Raspberry Pi Zero W running on USB Gadget mode, which allows it to emulate any USB device you want. This is a very nice feature that I have played with before when I was trying to smartify a hi-fi. To use it, the first thing you need to do is create a gadget device, and then define a descriptor - which in this case I used the following:
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x02, // Usage (Mouse)
0xA1, 0x01, // Collection (Application)
0x09, 0x01, // Usage (Pointer)
0xA1, 0x00, // Collection (Physical)
0x05, 0x09, // Usage Page (Button)
0x19, 0x01, // Usage Minimum (0x01)
0x29, 0x03, // Usage Maximum (0x03)
0x15, 0x00, // Logical Minimum (0)
0x25, 0x01, // Logical Maximum (1)
0x95, 0x03, // Report Count (3)
0x75, 0x01, // Report Size (1)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01, // Report Count (1)
0x75, 0x05, // Report Size (5)
0x81, 0x03, // Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x05, 0x01, // Usage Page (Generic Desktop Ctrls)
0x09, 0x30, // Usage (X)
0x09, 0x31, // Usage (Y)
0x16, 0x01, 0x80, // Logical Minimum (-32767)
0x26, 0xFF, 0x7F, // Logical Maximum (32767)
0x75, 0x10, // Report Size (16)
0x95, 0x02, // Report Count (2)
0x81, 0x02, // Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0, // End Collection
0xC0, // End Collection
// 52 bytes
Note: there are some online tools to help you decode a descriptor. I used this USB Descriptor and Request Parser for this. There’s also a nice tutorial here. And there’s the official docs about USB HID usage tables here as well.
This descriptor defines some very interesting things. For example, it defines the number of buttons (3
), as well as its values (0
and 1
). It also defines the X and Y varying from -32767
to +32767
, and being absolute values. This is a hack to simplify the movement, as doing relative X/Y depends a lot on the OS behavior, acceleration and how it interprets movements. The released version of this script will also fake a movement (straight line, but movement!) to make it “harder” to detect.
Another required script is one that receives the data from the network (over WiFi, why not) and sends it over USB. For simplicity, I’ll receive literally the bytes to send over USB, no extra encoding or packing, over UDP. That’s a very simple Python script:
#!/usr/bin/env python3
## Call with: python3 udp.py /dev/hidg1
## (or whatever your HID gadget device is)
import socket
import sys
UDP_IP = "0.0.0.0"
UDP_PORT = 60123
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((UDP_IP, UDP_PORT))
dev = open(sys.argv[1], "wb")
while True:
data, addr = sock.recvfrom(128)
print("RECV", addr, data)
dev.write(data)
dev.flush()
The data consists of 5 bytes:
Byte 0: xxxxxLMR
Byte 1: least significant bits of X
Byte 2: most significant bits of X
Byte 3: least significant bits of Y
Byte 4: most significant bits of Y
Every time our code sends those 5 bytes over UDP to the RPi, it’ll write it down to the gadget device as a report. This means that whatever position and button status we sent over the network will be interpreted in the host. The awesome part about this is that this is just like any other mouse, as the OS just sees USB data and interprets it. This also means that the game (and its anti-cheat software) won’t be able to differ it from any other standard mouse, which is exactly what we need.
Ok, but how do we know where to click?
The Full Proof-of-Concept
What if we automate game even further? What if we were able to locate mobs on the screen, figure out their position and send a mouse click there? Can we do that? Well, we can. Let’s imagine following execution flow:
- (At the game host) Mob images are loaded into an image processing script
- (At the game host) Every N milliseconds a game screenshot is taken and sent to the script
- (At the game host) The script processes the screenshot and locates the position of mobs in the screen
- (At the game host) The script then picks one of these mobs and sends click commands over the network (over UDP) to the script running on the RPi
- (At the RPi) The RPi then receives such commands and sends them to the USB Gadget raw interface, which translates to USB raw data and sends to the host
- (At the game host) The host receives the USB data and moves the cursor just like any real mouse would do
From the perspective of the game host, there’s nothing weird on it, besides a Python script and some periodic game screenshots. This is nothing that would normally trigger an anti-cheat software, so this is fine to run at the host - at least it was back when I tested!
The question is: does it work? Yes, it does.
In the video you can see the script is very slow: this is expected, my OpenCV skills are terrible. But the concept works: we can target a known mob and attack it. The way it works is by processing the game screenshots and doing template matching on it. Then we can use the confidence of those matches to figure out the best mob to attack. I won’t go over the details about how this works because OpenCV is something I have pretty much no knowledge of, and there’s a very big chance this is really bad code. It is, however, open source, and you’re free to use it as you want.
There’s a problem though: we can detect this. We have to main pieces in this design: one is running at a RPi, but the other one is running at the game host. It’s not uncommon for anti-cheat software to be very strict about what is running with the game, and a Python script with OpenCV libraries taking screenshots of the game in a periodic (and predictable!) manner is something that we can easily detect. Besides possible performance reasons, running the bot in the same machine you’re running the game makes it very traceable and detectable. Let’s discuss that.
Detecting, bypassing, repeat
Disclaimer: the following discussion is based off my own thoughts and ramblings, based on real and hypothetical scenarios. This is also taken from a developer’s perspective.
There are multiple ways of detecting what I just presented. I’d like to point out some ideas how to detect and block this bot, as well as ways of bypassing such ideas. This is not an extensive list as there are always other solutions, but here we go!
Input analysis
We can easily analyze the input data (ie. keystroke, click and movement timings, noise, etc). From that, we can detect if someone is clicking or typing too fast, or even moving too straight. This, however, can be easily bypassed by introducing random factors in the inputs. For example, when moving a mouse, we can “accelerate” and “slow down” the movement, as well as introduce errors in movement.
Introducing errors isn’t perfect though. There are some fun things we can use like Keystroke Dynamics that would allow the game to detect if the user is actually a person or a bot. This is, however, very complex for a game to implement (even more for games like Ragnarok), and this kind of biometric analysis tend to oscillate too much throughout the day due to multiple factors - sleepy people type slower, for example.
Behavior anomaly detection
This is an interesting one. Imagine we keep track of the user’s behaviors: game progression (level, mobs, etc), online time, chats. With this information, we could detect an anomaly, such as the user staying too many hours online, or not answering chat messages, progressing too fast, and so on. With this information, we can create a model of the expected user behavior and trigger alerts based on that. Fun fact, this technology is actually used in Site Reliability Engineering as a way of detecting systems behaving out of the expected ranges.
There are multiple downside of these approaches. The first one is the complexity of modeling the user behavior, as well the amount of online time that it requires. Creating this model requires a lot of gameplay data for each user, as they differ from each other. Sure, some stuff can be modeled “in general” (ie. for all players), but still. The second downside is the computational power required to create and validate the model for each player - this just wouldn’t (easily) work for millions of users at the same time.
Finally, this can be bypassed in the same way. We could, in theory, model the user behavior in the bot, and from there we can replicate it. That would be sufficient to bypass the protection.
Hardware shenanigans
We could always have dedicated game controllers that are harder to hack/modify. Yeah yeah, I know, this is pure madness, but hear me out.
In theory, you could use anti-tampering techniques, such the ones in POS, to avoid modification of the controller itself. This would block something like we did with the mouse, for example. Plus, you can use encrypted communication to block man-in-the-middle attacks, or even device emulation, as we did with the RPi. This solution, however, would be just an attempt to block hackers and bots, as there’s always a way.
This kind of reminds me of console controllers, to be honest. And it is, however, a pretty crazy/overkill solution - and an expensive one. Unless your game is amazing, noone is going to buy your custom controller only for it. Plus, you’d reduce your player count due to the expensiveness of the hardware. Bad idea.
The perfect anti-cheat?
Let me get something straight first:
There is no perfect anti-cheat software, and you are never 100% safe.
I learned something very important in software development: you are always trusting the layer under you. You trust your IDE, OS, compiler, etc. Your code trusts the OS API calls, which by then trust kernel, which will then trust the hardware. And there are hundreds, maybe thousands of layers between those. There’s a chain of trust, and software (in general) will always trust that such chain is intact. Anti-cheat software is no different, and it’ll also trust that the hardware below the OS is safe - or even that the human is actually a human (spoiler: more on that on part 2).
Honestly, my opinion about anti-cheat software is that, if you must have one, have it as non-intrusive as you can. Sure, you can have the classic memory and network ones, but try also to detect some hardware anomalies (such as mouse and keyboards too fast). And, on top of that, add game supervision - actual human supervision: if there’s a chance the user is cheating, get someone to watch the gameplay and confirm that for you. I mean, what could possibly go wrong? :)
The future
There’s a lot of work still to get done on this. I got some progress and made some very nice improvements in this project/research - and on its detectability, but unfortunately all of that is not yet ready to be disclosed. Everything will be explained in a part 2 of this post!
Soon, though. Very soon.
Until then, stay tuned!