Finishing what Intel started — Building an Arduino-powered anti-cheat

In 2007, during Research@Intel Day, an interesting tech demo took place; but in spite of the project being featured prominently in the media, users throwing money at their screens, and the concept being genuinely fascinating, it never came to fruition. Maybe it was the right idea at the wrong time. Maybe Intel didn’t pour enough man-power into it. Maybe it was the economy on the down-swing. Who knows. But it unquestionably fizzled out of existence…
schluessler-Resized
What was it? A hardware-based anti-cheat system dubbed “Fair Online Gaming” developed by Intel engineers Travis Schluessler and Daniel Pohl, pictured to the right. At the time, I only found out about it because I was working in the same domain: the Cyberathlete Amateur League had begun prototyping their client-side anti-cheat and I helped develop the first incarnation of the CAL-AC alongside Wim Barelds (one of the creators of zBlock) and Andres “misty” Gunaratne (now with Fantasy Esports). The CPL was acquired by WoLong Ventures and CAL went along with it. Professional and casual gamers alike left CAL for ESEA, CEVO, and the ESL. After a not-too-shabby career, I decided to quit gaming and go back to school. The story doesn’t end there, but let’s rewind a little. How does an anti-cheat like Intel’s FOG work, and, more importantly, why do we need a hardware anti-cheat?

Software Sucks

It’s not fair to accuse Intel of completely dropping the ball here. After all, FOG was, in some ways, a vision of the future that got many things right (and a few wrong, but more on that later). First of all, it was a hardware-based anti-cheat solution. Software ACs rely on a few methods of detecting whether or not a player is cheating and most of these methods are, unfortunately, quite easily beatable. Let’s look at a couple.

Signature-based detection

Signature-based detection (SBD) is a relatively simple method of virus or cheat detection. SBD begins when a change is noticed either on (a) the file system (e.g. a new file is downloaded from the internet) or (b) in user-space memory. Suppose we have a large database of known cheats. This database consists of some identifiable “fingerprint” that the cheat possesses when either on disk or in memory. For example, given the following database:

Cheat name Fingerprint
SuperWallhack 0xAH 0x44 0x32
Aimbot2000 0x00 0x04 0xFF
...

And given the following memory layout (note the highlighted sections):

[code highlight=”3,4,5″]

0x33
0xF0
0x00
0x04
0xFF
0x58
0x33
0xF0
0x58
0x47

[/code]

We can conclude that the player is aimbotting. To do this search, variations of Boyer-Moore are used; often times in conjunction with suffix trees or suffix arrays. As long as the fingerprint is long- and unique-enough, collisions should be a rare happenstance.

The drawback of SBD is, unsurprisingly, that it requires a database. For a virus to be detected, it must have already been detected (or, at the very least, isolated) in the past. This is not only problematic for anti-virus software, but exponentially more problematic for anti-cheat software. Consider a private cheat that is sold through various grey markets online. The “benefit” of a virus is that it behaves unruly: no computer user wants a virus on their PC. This means that whenever a PC is infected with an “under the radar” virus, it quickly becomes quarantined and recorded in an AV database. This is not the case with cheats. Players that want to cheat have an interest in keeping their cheats private (lest they get caught). Therefore, it is virtually impossible for any SBD-like methods to catch them.

Furthermore, SBD has another drawback. Consider cheats or viruses that are non-static in nature; that is, use self-modifying code (e.g. are polymorphic or metamorphic in nature), or download encrypted parts of their own source code over the internet. Catching viruses with SBD methods might be a first step, but as far as cheats are concerned, the method seems untenable from the get-go.

So, in conclusion:

  • False positives? None, given a large-enough key. Collisions should realistically never happen.
  • False negatives? Many. Almost all purchasable cheats will not be caught as they update fairly regularly.
  • How to beat? If you’re writing a cheat yourself, don’t release the source code. If you do release the code, make it polymorphic or metamorphic.

Monitoring API Calls

Monitoring API calls is a more heuristic approach used by anti-cheat software. A cheat may hook into, say, a rendering function with code that looks like the following (this code uses the Microsoft Detours library and assumes a double F(double) function signature):

[cpp]
#define ADDRESS 0xDEADB33F // this is the address where our targeted function begins
double (__cdecl* originalFunction)(double); // pointer to the function we are going to hook, must be declared same as original

// modified function code that is going to be executed before continuing to the code of original function
double hookedFunction(double a) {
std::cout << “original function: argument = “<< a << std::endl; // we can access arguments passed to original function
a = 42; // modify arguments
return originalFunction(a); // before returning to normal execution of function
}

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved) {
switch (dwReason) {
case DLL_PROCESS_ATTACH:
// magic
originalFunction = (double(__cdecl*)(double))DetourFunction((PBYTE)ADDRESS, (PBYTE)hookedFunction);
break;
}
return TRUE;
}
[/cpp]

To prevent such hooking, anti-cheat software may implement a white-list and monitor OS API calls like DetourFunction, LoadLibrary, CreateRemoteThread, and WriteProcessMemory. After all, some applications that hook into games are not malicious. Consider Fraps, the Mumble overlay, OBS, etc. If an application is not white-listed, bad things happen. Case in point: ENB series — a shader beautifier — was ban-worthy for several months. Hundreds of Dark Souls II players were unjustly banned on Steam when using a DLL proxy instead of the vanilla DirectX.

More advanced methods, e.g. virtual method table hooking, are still undetected (and will most likely always be undetected). Hooking vtables is difficult, but not impossible. The most difficult part is finding the method offsets (which often change from update to update). Since VAC3 has implemented some modules that may prevent internal vtable hooking (via sanity checks), this has not stopped cheaters from vtable hooking third-party libraries like Direct3D, OpenGL, and DirectInput. Here’s a code snippet that does just that:

[cpp]
class CGame;
class cVMT;
class cD3D;

class CGame {
public:
cVMT* pVMT; //0x0000

};//Size=0x0004

class cVMT {
public:
DWORD pD3D; //0x0000

};//Size=0x0004

const DWORD_PTR dwAddy = reinterpret_cast<DWORD_PTR>(GetModuleHandleA(“shaderapidx9.dll”)) + 0x1A0A78;
CGame* pGame = (CGame*)dwAddy;

typedef HRESULT(WINAPI* tEndScene)(LPDIRECT3DDEVICE9 pDevice);
tEndScene pEndScene;

HRESULT WINAPI hkEndScene(LPDIRECT3DDEVICE9 pDevice) {
_asm pushad;
MessageBox(0, L”Hello world from EndScene”, 0, MB_OK);
_asm popad;
return pEndScene(pDevice);
}

DWORD WINAPI vThread() {
DWORD_PTR dwEndScene = NULL;

while (pGame != NULL) {
pEndScene = (HRESULT(WINAPI*)(LPDIRECT3DDEVICE9 pDevice)) *(DWORD_PTR*)(pGame->pVMT->pD3D + 0xA8);

VirtualProtect((LPVOID)(pGame->pVMT->pD3D + 0xA8), sizeof(DWORD_PTR), PAGE_EXECUTE_READWRITE, &dwEndScene);

while (TRUE) {
*(DWORD_PTR*)(pGame->pVMT->pD3D + 0xA8) = (DWORD_PTR)hkEndScene;
}

VirtualProtect((LPVOID)(pGame->pVMT->pD3D + 0xA8), sizeof(DWORD_PTR), dwEndScene, &dwEndScene);
}
return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
::CreateThread(0, 0, (LPTHREAD_START_ROUTINE)vThread, 0, 0, 0);
}
return TRUE;
}
[/cpp]

0xA8 happens to be the magic number here — it’s the offset for IDirect3DDevice9::EndScene, so anything we draw will be drawn on top of the current frame. An AC could monitor VirtualProtect or CreateThread, but a lot of legitimate code use these. Not only that, but there are many ways to cleverly get around using them. Often times, for example, cheat writers use undocumented features of an OS. It goes without saying that as cheats bury themselves in ring1 and ring0, they become virtually impossible to catch by non-invasive methods. So how does API monitoring look?

  • False positives? Assuming a perfect whitelist, none. Real-life cases show otherwise. Carrying heavy penalties can hurt innocent people and going back on a ban can hurt credibility.
  • False negatives? Many. Most purchasable cheats don’t rely on naive injection methods.
  • How to beat?
    1. Don’t write memory in the game’s space
    2. Don’t use obvious hooking methods
    3. Do run it in ring0

Stochastic Methods

And finally, we have stochastic methods. Stochastic methods of anti-cheat detection are based on past behavior and a well-defined probabilistic model. Even though these kinds of methods often catch the most disruptive of players, it would be a grave mistake to rely solely on a probabilistic model. Consider, for example, the following video:

https://www.youtube.com/watch?v=sZs_VYbjBlc&feature=youtu.be

The 15%-20% aim enhancement shown in the video can hardly be noticed by a human viewer, let alone be considered an outlier in a probabilistic model. This is how GameBlocks’ FairFight works — a popular XBOX anti-cheat tool. According to the company,

Fairfight uses two overlapping and mutually supportive approaches to identify cheaters: algorithmic analysis of player statistics and server-side cheat detection. FairFight uses algorithmic models to evaluate gameplay against multiple statistical markers to identify cheating as it occurs. FairFight crosschecks these indicators using objective server-side reporting tools. It takes action when both approaches correlate to cheating — and because you establish FairFight’s tolerances and the in-game actions to be taken against the hacker, you are in control of your game like never before. Learn more about integrating FairFight Into your game today.

Unfortunately, a tightly-tuned cheat could never be caught. In some cases, simply using SBD would be more advantageous than a purely stochastic method.

  • False positives? Depends on the tolerance.
  • False negatives? Depends on the tolerance.
  • How to beat? Make sure your cheat’s behavior is not an outlier.

Hardware Rocks

But let’s get back to Intel’s FOG. Here’s how it was supposed to work:

The fundamental premise behind the Fair Online Gaming technology research is that platform hardware can help detect cheating in online games. If this technology makes its way into PCs, gamers enjoying money making games who want to play without cheaters can choose to turn it on and play with other gamers that also turn the technology on in their systems. The Fair Online Gaming technology itself is a collection of hardware, firmware and game software running on client PCs and servers. In our labs, we’ve tested the concept’s effectiveness at detecting entire classes of cheating, not just specific cheats.

I previously mentioned that Intel made a few mistakes, so here they are. First of all, making a vendor-specific anti-cheat creates a rift in the gaming community. Some gamers are diehard AMD fans, others Intel; some nVidia, some ATI, and so on. A cursory glance at the diverse demographic will yield the fact that a hardware anti-cheat would have to be vendor-agnostic. Second of all, Intel’s Fair Online Gaming was (when developed) using Intel’s “Active Management Technology” feature. A major part of AMT is the programmable (read: flashable) firmware. A hardware solution should not be bound by any PC-specific software constraints. It should work out of the box without any client-side software, lest we open the can of worms that is client-side anti-cheat. In short, what we should have is something like this:

Hardware Anti-cheat

The Prototype

What would this device look like? Could I actually build it? Before I did any development, I quickly scribbled down some notes; it had to meet the following requirements:

  • Price: somewhere between 30 and 100 dollars
  • Size: portable
  • Ports: USB in, USB out, Ethernet out
  • Input latency: < 1ms

Just a few months ago, I knew very little about Arduino, but I started doing some research. I wasn’t sure what the hardware platform would be and I was skeptical about reaching a sub-1ms latency, but I was absolutely sure that I could do some kind of “USB passthrough”. Ironically, what I thought would be tough to do ended up being trivial and what I thought would be trivial ended up being difficult. I settled on the Arduino Mega ADK (which has a host USB shield embedded) and an Ethernet shield. What followed was relatively painless. I used the USB_Host_Shield_20 library to grab mouse states from a Razer mouse I connected to my Mega ADK board. Using the OnMouseMove callback, I was getting a stream of dx and dy values. About 2000 lines of code later, I had an almost-fully-functioning prototype, temporarily dubbed “Valkyrie” . Here’s the class definition:

[cpp]
class Valkyrie : public UniversalReportParser {
public:
void Setup();
void Loop();
protected:
virtual void Parse(HID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf);
private:
// C++ is so fucking stupid
static void Heartbeat();
HIDMouseReport *in;
HIDMouseReport out;
EthernetClient client;
Timer timer;
};

#endif // VALKYRIE_H_INCLUDED
[/cpp]
There’s nothing too fancy about it, just a lot of housekeeping. It was basically a fancy USB HID pass-through:
[cpp]
void Valkyrie::Parse(HID *hid, bool is_rpt_id, uint8_t len, uint8_t *buf) {
in = (HIDMouseReport *) buf;

// in/out transaction
out.dx = in->dx;
out.dy = in->dy;
out.buttons = in->buttons;
out.wheel = in->wheel;
out.i0 = in->i0;
out.i1 = in->i1;
out.i2 = in->i2;
out.i3 = in->i3;

// send packet to AVR controller
Serial.write((uint8_t *) &out, SMALL);
// … and to AC server
client.sendData(String(out.dx) + ” ” + String(out.dy));
}
[/cpp]

The real magic happens in the LUFA-powered firmware that enables the Arduino to be “seen” as an HID device (specifically a mouse). Some still-pending issues are making the mouse wheel work and making mouse buttons 4 and 5 work. Here’s a picture of the real thing:
Close-up

Testing Counter-Strike: Global Offensive

To test the prototype, I wrote a small server-plugin for CSGO that relays player view angles back to a local server (this would be the anti-cheat server in the diagram above):
[cpp]
public Action:OnPlayerRunCmd(client, &buttons, &impulse, Float:vel[3], Float:angles[3], &weapon, &subtype, &cmdnum, &tickcount, &seed, mouse[2]) {
if (prev_x[client] != angles[0] || prev_y[client] != angles[1]) {
new data[128];
Format(data, 127, “%f:%f::%f:%f”, angles[0], angles[1], prev_x[client], prev_y[client] );
prev_x[client] = angles[0];
prev_y[client] = angles[1];
SocketSend(socket, data);
}
}
[/cpp]

As soon as I started getting data from both the Arduino as well as the game server, I had a realization: “holy crap, this is working.” First if all, I could catch triggerbots with 100% accuracy. This alone has amazing ramifications. Triggerbots don’t only affect first person shooters like CS and the Battlefield series, but they can be generalized to DOTA2 and League of Legends where new “instant click” or “instant ability” cheats have started cropping up — cheats that simulate either mouse button presses or keyboard presses.

The data started pouring in and I started graphing it. For example, here’s the y-axis movement of the mouse (straight from the optical sensor) alongside the angle movement in-game — both are given in differentials.
Graphs
Obviously, the two don’t “line up” perfectly and nfortunately, the two will never be isomorphic. Windows (and OSX/Linux) tends to provide various mouse features that consist of smoothing, acceleration, deceleration, interpolation, etc. Furthermore, the “raw” graph tends to be noisier. This is due to the fact that mice can fire new events up to 1000 times a second, whereas Counter-Strike’s best tick rate is about 128 ticks/second. One way of “tightening” the data is to simply look at whether or not the mouse is moving in a positive or negative (x or y) direction ignoring the actual distance the mouse moved. Given this constraint (1 = moving up, -1 = moving down, 0 = stationary), we have this:
Graph2
Why this graph is interesting is that we clearly see the causal relationship between the Arduino data (in red; fresh from the mouse) and the game data (in blue). When this causal relationship breaks, the player is cheating. The test? A resounding success.

Conclusions & Future Developments

So what are the next steps and what did I learn? Well first of all, I learned that Intel has really good ideas — FOG seemed awesome 8 years ago and building my own version of it is both humbling as well as exhilarating. I also learned that the Arduino community is the absolute best. The amount of help and resources that are out there is beyond staggering. I didn’t pick the easiest “first hardware project ever” but I couldn’t’ve done it without the code-base and tutorials that are already out there.

What are the next steps? Well, I need to brush up on some signal processing and figure out a good way of weeding out the aimbotters from the non-aimbotters, I need to implement HMAC (or a variation of it) to make sure that the device itself is tamper-proof, I need to fix some bugs in the firmware that are screwing up overflow bits in the HID report, and I need to also make sure that buttons 3, 4, and 5 work on every mouse I hook up to the Arduino.

And even though I’ve been away from gaming for a while now, I also learned that this may be the device the gaming community needs right now. Among cheating allegations at the highest levels in CS:GO, as well as new cheats coming out for mainstream games like DOTA2 and League of Legends — not to mention the fact that the cheating industry is growing at a ridiculous pace — it’s becoming clear that something needs to be done. Who knows, maybe there’s a Kickstarter on the horizon…