Note: This analysis was performed approximately two months ago when I was significantly less experienced, particularly in cryptographic analysis, and even in how EAC communicates. Because of this, information may be slightly wrong. This post is partly from assumptions and static analysis as the AES logic is mostly unobfuscated and I didn't confirm the AES logic dynamically, hence the IOCTL codes and buffers. EasyAntiCheat rotates their encryption keys frequently as part of their update cycle, so some specifics may have changed in current builds. Diagrams are hosted on Imgur - if you're in the UK you may need a VPN to view them.
Over the past few days, I've been going deep into EasyAntiCheat's kernel driver, specifically trying to understand how their cryptographic protocol actually works. What started as basic hooking spiralled into a full analysis of their AES-256-CBC + ECDSA-P256 implementation and pulling keys out of obfuscated code.
On the topic of how good EAC actually is, I want to mention something. I've reversed not even half the driver so far, and even at that level I've been finding things that genuinely blew me away. There's one function I won't go into detail about here because it deserves its own dedicated writeup given how deep it goes but to anyone just starting out in this space, please understand that their detection surface is massive. They have hundreds of things you won't think of for the next couple years, no exaggeration.
This is also part one of a bigger series I'm working on. My end goal is to build a complete EAC emulator, not to bypass it, but to understand the system inside and out. I've spent months on this already but most of that has been theory. The emulator is a tool for me to learn, to make it easier to find vulnerabilities over time, and to really test how deep EAC's detection surface goes. People hype up EAC as this untouchable thing and honestly? It kind of is. But I want to see for myself just how difficult it really is, and document the whole process along the way. Part two is coming.
And while we're on the topic of how good EAC actually is, I want to mention something. I've reversed roughly 19% of the driver's functions so far (mind you, the driver has over 7500), and even at that level I've been finding things that genuinely blew me away. There's one function I won't go into detail about here because it deserves its own dedicated writeup given how deep it goes, but it essentially builds a graph of all reachable code paths so if execution ever happens outside of that reachable set, they can flag it. Think along the lines of a custom CFI implementation, but the way they've done it is different enough from standard kCFG that it warrants a full breakdown on its own. I've been doing this for a while now and that's something I never even thought of before. It's actually innovative. To anyone just starting out in this space, please understand that their detection surface is massive. They have hundreds of things you won't think of for the next couple years, no exaggeration.
I'm not trying to glaze EAC or look good for them. It's just genuinely well built, and there are a few other functions I found that I'm saving for future posts because they deserve their own writeups.
Initial Reconnaissance
EasyAntiCheat runs as a kernel-mode driver that talks to usermode through standard Windows IOCTL requests. My initial goal was simple: hook the driver's IOCTL handler, capture traffic, and figure out what data was being exchanged between usermode and the kernel.
The driver registers itself as \Driver\EasyAntiCheat_EOSSys and handles requests through IRP_MJ_DEVICE_CONTROL. I wrote a basic hook driver that intercepts calls, logs the IOCTL codes and buffer contents, then forwards everything to the original handler so nothing breaks. The hook used a background thread with async file writing to avoid blocking at high IRQL levels.
First run immediately showed several distinct IOCTL codes popping up repeatedly:
- 0x0022E023 - 44-byte bidirectional transfers (constant)
- 0x0022E017 - 816-byte transfers (periodic)
- 0x00226013 - Smaller query operations
- 0x0022E043 - 40-byte memory operations
The 44-byte packets caught my eye right away because of how frequent they were and how consistent the size was. I assumed these were encrypted game state packets or anti-cheat scan results. I was completely wrong about that, but chasing them led me to analysing EAC's entire crypto implementation, so it worked out.
Analysing the Cryptographic System
Starting with static analysis in IDA Pro, I traced the IOCTL handler to function sub_14007224C which processes incoming messages. The code showed a clear two-stage validation: message deserialization and structure validation, then cryptographic signature verification.
The signature verification function sub_140070ED8 was using Windows BCrypt APIs, but every single parameter was obfuscated. Algorithm names, key material, all of it encrypted to prevent static extraction. The function clearly took a message buffer, size, and signature as parameters, then did hash computation and ECDSA verification.
Decrypting Obfuscated Strings
EAC uses custom PRNG-based XOR encryption to hide all their cryptographic algorithm names and parameters. Each string is encrypted with a different seed and algorithm variant, so I had to reverse multiple decryption functions one by one.
The SHA256 algorithm name was stored as 8 encrypted bytes at address 0x1401A9160. The decryption used a specific xorshift variant with seed -1125228787. Running the decryption gave me "SHA256" in UTF-16LE encoding.
The ECDSA algorithm name was 16 bytes at 0x1401A9120, encrypted with an LCG using seed -1721366523 and a byteswap operation. This decrypted to "ECDSA_P256", confirming they're on the P-256 elliptic curve.
Public Key Extraction
The ECDSA public key was stored at 0x1401A9170 as 72 encrypted bytes in BCRYPT_ECDSA_PUBLIC_BLOB format. The encryption used another xorshift variant with seed -551316663. After decryption I got the complete public key with magic value 0x45435331 ("ECS1"), key size 32 bytes for P-256, and both X and Y coordinates of the public point.
This public key is what the driver uses to verify signatures from usermode, proving that usermode hasn't been hooked or messed with. The driver has the public key, usermode has the private key, so only legitimate usermode components can produce valid signatures.
Symmetric Encryption Discovery
The actual message decryption happens in function sub_1400715C8. This uses AES in CBC mode, again with everything obfuscated through custom PRNG encryption.
The algorithm name "AES" was 8 bytes at 0x1401A90F8, encrypted with an LCG using seed 1611801797 and the formula (-1189061 - 24595 * seed).
The 256-bit AES key itself was 32 bytes at 0x1401A90B8. This used an xorshift variant with seed -1479485563 (unsigned 0xA7F4CE05). After working through the decryption I pulled the complete key:
6a2e87818307d0021f34eaa7e3d61a48759862e85871e95d783c54180c1da3fb
The chaining mode was 32 bytes at 0x1401A9098, encrypted with yet another xorshift variant. Decrypted to "ChainingModeCBC", confirming CBC mode.
Additional properties at 0x1401A9038 and 0x1401A9058 provided the IV parameter name and other BCrypt config values, all following the same obfuscation pattern.
Complete Protocol Architecture
Here's the full cryptographic protocol as it emerged from analysis:
Encryption Layer:
- Algorithm: AES-256-CBC
- 256-bit key extracted from driver binary
- IV passed with each message
- Standard BCrypt implementation
Authentication Layer:
- Algorithm: ECDSA with P-256 curve
- Public key embedded in driver
- Signature verification on all messages
- SHA256 hash before signing
Message Flow:
- Usermode encrypts message with AES-256-CBC using hardcoded key
- Usermode computes SHA256 hash of ciphertext
- Usermode signs hash with ECDSA private key
- Complete message structure sent via IOCTL
- Driver verifies ECDSA signature using public key
- If signature valid, driver decrypts with AES-256-CBC
- Driver processes plaintext message
Signature verification makes sure usermode hasn't been hooked. Encryption handles confidentiality. It's a solid two-layer setup.
One thing to clarify: this crypto protocol applies specifically to the usermode-to-kernel communication path, where usermode sends commands and the kernel verifies they're legitimate. The driver enumeration data I found later flows the other direction, kernel-to-usermode, and appears to use a separate mechanism entirely. These are two distinct communication channels with different security models, not one unified system.
The Private Key Question
Obvious question: if the ECDSA private key lives in usermode, can't someone just extract it and sign their own packets, completely defeating this? Yes, absolutely. This isn't to say it is simple and easy, but if you're asking a yes or no question then there is your answer.
The Failed Decryption Attempt
With the complete crypto system in hand, I tried to decrypt the 44-byte 0x0022E023 packets. I had the AES key, I knew it was CBC mode, just needed to figure out the IV source.
I tried every reasonable IV derivation strategy. Zero IV, first 16 bytes as IV, deriving from the IOCTL code, using the key's first 16 bytes. Everything came back as complete garbage, entropy analysis showed the output was still basically random.
At the time, I thought 44 bytes isn't divisible by 16 so I just gave up on that but didn't even think the transport layer can wrap it however it wants. AES-CBC operates on 16-byte blocks and needs padding to the block size. A 44-byte packet would be 2 full blocks plus 12 bytes, meaning it would need to pad to 48 bytes (3 blocks). While the structure could theoretically have a header plus encrypted data, the size alone was a strong hint this wasn't a raw AES-CBC blob.
So either I had the wrong key, or these 44-byte packets weren't encrypted with this system at all.
The Realization
Going back to analyze what 0x0022E023 was actually doing, I noticed something important. The driver was generating different 44-byte responses each time, even for identical inputs. That's not how encryption works for deterministic data. This is challenge-response authentication.
The 44 bytes aren't encrypted data. They're authentication tokens exchanged between usermode and kernel to verify usermode hasn't been hooked or tampered with. The ECDSA signature verification I reversed? That's checking these tokens, not decrypting them.
Additionally, there's a dispatcher function which I named EAC::SelectHashAlgorithm, that lets the server swap the entire hashing algorithm for a session on the fly. It basically supports everything from MD4 and MD5 up to the full SHA-2 suite. If you're building an emulator and only account for one specific algorithm, you're flagged as soon as the server decides to rotate the algorithm ID.
The 44 bytes aren't encrypted data. They're authentication tokens exchanged between usermode and kernel to verify usermode hasn't been hooked or tampered with. The ECDSA signature verification I reversed? That's checking these tokens, not decrypting them.
Additionally, There's a dispatcher function which I named EAC::SelectHashAlgorithm, that lets the server swap the entire hashing algorithm for a session on the fly. It basically supports everything from MD4 and MD5 up to the full SHA-2 suite. If you're building an emulator and only account for one specific algorithm, you’re flagged as soon as the server decides to rotate the algorithm ID.
The real data channel turned out to be 0x0022E017 with those 816-byte packets I initially wrote off as less interesting.
Analyzing the Real Data Channel
I updated my hook to capture the full 816-byte output buffers from 0x0022E017 calls. Surprisingly, the output wasn't encrypted at all, it was plaintext system information in a structured format. This seems like it could be a weak point since an attacker could theoretically hook DeviceIoControl in usermode and modify the buffer before it gets sent out. My best guess is that EAC is relying on their usermode component's heavy obfuscation and anti-tampering to prevent MITM on this buffer, or there's a secondary integrity check I haven't found yet that validates this data before transmission. Either way, it's worth noting. Do note as well, I have not confirmed that usermode holds the other key but it makes the most sense especially since there are encrypted packets coming from it.
Decoding the Unicode strings showed:
- "NVIDIA Corporation"
- "DigiCert Trusted Root G4"
- "Microsoft Root Certificate Authority 2010"
- "Microsoft Windows"
The data structure mapped out clearly once I understood what I was looking at. Count fields at various offsets, Unicode strings for vendor names and certificate subjects, 20-byte SHA1 hashes, timestamp structures with year/month/day/hour/minute/second values. It looks like the AES implementation I reversed is reserved for a different packet type I haven't triggered yet, since the standard enumeration data flows unencrypted.
Certificate Thumbprint Verification
The 20-byte values were SHA1 certificate thumbprints. I verified this against my local system's certificate store. Hash DDFB16CD4931C973A2037D3FC83A4D7D775D05E4 matched DigiCert Trusted Root G4 exactly, including the correct validity dates.
But one specific hash showed up associated with a huge number of different drivers. I wrote a PowerShell script to enumerate all running drivers and check their code signing certificates. Hash 3B77DB29AC72AA6B5880ECB2ED5EC1EC6601D847 matched literally every single Microsoft-signed driver on the system.
EAC isn't collecting random certificate info but rather systematically going through every Microsoft-signed driver loaded on the system and collects a detailed system fingerprint including GPU vendor, certificates, and full driver inventory, then sends all of it to Epic's servers with every heartbeat. Overall, I assume this encryption could be used on their report packets or some other communication method they use.
If you're working in security research, feel free to reach out.
ACPI.sys, disk.sys, Ntfs.sys, Tcpip.sys, all the core Windows kernel drivers. Every one signed with certificate subject "CN=Microsoft Windows, O=Microsoft Corporation" and the same thumbprint.
EAC isn't collecting random certificate info. They're systematically going through every Microsoft-signed driver loaded on the system.
The Anti-Spoofing Technique
This is where everything clicked, it's about catching stub drivers pretending to be EasyAntiCheat. An attacker creates a fake EAC driver that responds to game queries, but it needs to handle IOCTL requests from the game and servers. The fake driver has to respond with believable data to not get flagged, and if it doesn't return proper system information, it's done.
EAC collects a detailed system fingerprint including GPU vendor, certificates, and full driver inventory, then sends all of it to Epic's servers with every heartbeat. Servers validate that the fingerprint matches the expected baseline for that Windows version, so any discrepancy points to emulation.
The count fields in the packet structure track stuff like total Microsoft-signed drivers loaded, total third-party drivers, certificate counts. A legitimate Windows 11 22H2 system has something like 87 Microsoft-signed drivers. If a fake driver reports only 30, or reports drivers that don't exist for that Windows version, the mismatch is obvious server-side.
The technique validates that EAC is running in a real environment, not that the environment is free of cheats. It's anti-tampering and anti-spoofing, making sure that what claims to be EasyAntiCheat actually is EasyAntiCheat.
What's Next
This is part one. The crypto reversal gives me the foundation, but there's way more to cover. Usermode protection, private key extraction, the emulator itself, and some of those functions I mentioned earlier that I'm saving for dedicated posts. The code reachability graph thing alone deserves its own writeup because it genuinely changed how I think about detection design.
I've been at this for months and I've only reversed about 19% of the driver's functions which is barely any progress compared to how much the driver truly contains. That should tell you something about the scale of what EAC has built. Every time I think I've mapped out the important stuff, I find another layer that makes me rethink things.
If you're working in anti-cheat or security research, feel free to reach out. Always down to talk about this stuff, whether it's offensive or defensive.