Is EAC still doing CR3 memes in 2025?
Intro
In today’s post, we will be analyzing the current behaviour of an anti-cheat feature (EAC/Apex Legends). Essentially, it’s designed to make it harder to interact with the address space of their protected games. In online cheating communities, it’s usually referred to as:
- CR3 Encryption
- CR3 Shuffling
- DTB Shuffling
- or simply „CR3“ (lol)
The Basics
In order to form a basic understanding, I will be explaining it in a simplified form:
- EasyAntiCheat.sys writes a malformed value into apex’s KProcess::DirectoryTableBase.
- During the next context switch, if windows scheduler decides to run one of apex’s threads, it will:
- – Read Apex’s KProcess::DirectoryTableBase value.
- – Attempt to write that to the CR3 register of the current core.
- – Your CPU will see that some reserved bits were set, so it’ll throw a #GP.
- EasyAntiCheat.sys will catch that exception using even more memes, and then:
- Perform analysis on the exception context.
- Decide whether it’s legit -> write the real DTB value.
- If not, then just keep fake value. This will lead to any manual translation or API call using that fake DTB to fail.
- In order to force cheaters into brute forcing apex’s DTB, they also randomized the real value. It used to change exactly every 10 seconds.
I hope these key terms helped some of you understand. Lastly, i also want to get into the real world implications of this anti-cheat feature. It’s easy to overlook things sometimes, which will end up getting yourself owned by these companies.
Real World Implications
- Will prevent MmCopyVirtualMemory (and also NtReadVirtualMemory + NtWriteVirtualMemory) from working: If EAC does not give your context a pass. If you’re wondering why those Nt* functions are also affected: They internally do an invocation of MmCopyVirtualMemory. So if it fails, they will too.
- Will make it harder to read memory externally: You will no longer have a static/predictable DTB value to use. But you’ll need that for interacting with the address space of the protected process.
- Will allow them to feed extra information into their heuristics engine: Just think about it. If you could catch anyone who attempts to load a decoy DTB from your KProcess, then what would you do? Exactly -> analyze the context, possibly even do a manual stack walk, and then do analysis on all that data.
This is not intended to be a complete list. There is certainly more to be listed here. I did prioritize these over the others because they carry a higher risk factor (imo).
Analysis – Current Behaviour
Let’s boot up some good old apex and see for ourselves! I will be running these tests on Windows 10 21H2 (Build 19044). My apex version shows as v3.0.4.33.5.
We will start by taking a look at PsActiveProcessListHead:
// 09.06.25 - 05:45
[EasyAntiCheat_] PID:8760, DTB:0x85d006000, IMAGE-BASE:0x6c0000, LDR:1, PEB:1
[dllhost.exe] PID:2204, DTB:0x2dd866000, IMAGE-BASE:0x7ff655a80000, LDR:1, PEB:1
[r5apex_dx12.ex] PID:11524, DTB:0x4000000136d78000, IMAGE-BASE:0x7ff7d26b0000
[gcupd.exe] PID:3448, DTB:0x217897000, IMAGE-BASE:0xe20000, LDR:0, PEB:0
[GameOverlayUI.] PID:17268, DTB:0x88890d000, IMAGE-BASE:0x5a0000, LDR:1, PEB:1
Code-Sprache: JavaScript (javascript)
Alright, it seems like they’re still messing with it. We can tell just from the looks that there are reserved bits set that shouldn’t normally be set. Let’s see if this fake DTB value happens to change over time:
// 09.06.25 - 06:12
!proc-info 11524
found process with name r5apex_dx12.ex.
Base: 0x7ff7d26b0000, DTB (IC?:0): 0x4000000136d78000 C: 0xDD260845
Code-Sprache: JavaScript (javascript)
Nope, it doesn’t.
What about randomization of the real CR3?
Interestingly, i could not monitor any changes to it. It stays constant throughout all of my gaming sessions. I’m currently 3 hours into one: unchanged since the game process was started.
// With real-time discovery of the real DTB value:
!proc-info 11524
found process with name r5apex_dx12.ex.
Base: 0x7ff7d26b0000, DTB (IC?:1): 0x2bb2e9000 C: 0xDD260845
// After 20 minutes:
!proc-info 11524
found process with name r5apex_dx12.ex.
Base: 0x7ff7d26b0000, DTB (IC?:1): 0x2bb2e9000 C: 0xDD260845
// ( Note that this one was from another game session, CR3 will obviously not stay constant between process restarts. It still clearly shows us that the effective CR3 has not changed.
// Logging of the value that's passed down to <strong>__writecr3</strong> in nt!SwapContext:
preview(PID): 23504, preview(IDX): 22
[CPU4][1] 0x482f1c000
[CPU4][2] 0x482f1c000
[CPU4][3] 0x482f1c000
[CPU4][4] 0x482f1c000
[CPU4][5] 0x482f1c000
[CPU4][6] 0x482f1c000
[CPU4][7] 0x482f1c000
[CPU4][8] 0x482f1c000
[CPU4][9] 0x482f1c000
[CPU4][10] 0x482f1c000
[CPU4][11] 0x482f1c000
[CPU4][12] 0x482f1c000
[CPU4][13] 0x482f1c000
[CPU4][14] 0x482f1c000
[CPU4][15] 0x482f1c000
[CPU4][16] 0x482f1c000
[CPU4][17] 0x482f1c000
[CPU4][18] 0x482f1c000
[CPU4][19] 0x482f1c000
[CPU4][20] 0x482f1c000
[CPU4][21] 0x482f1c000
[CPU4][22] 0x482f1c000
[CPU4][23] 0x482f1c000
[CPU4][24] 0x482f1c000
[CPU4][25] 0x482f1c000
[CPU5][0] 0x482f1c000
[CPU5][1] 0x482f1c000
[CPU5][2] 0x482f1c000
[CPU5][3] 0x482f1c000
[CPU5][4] 0x482f1c000
[CPU5][5] 0x482f1c000
[CPU5][6] 0x482f1c000
Code-Sprache: JavaScript (javascript)
I already have code in place that dynamically tracks the real, actually used DTB. This actually caused me to get bamboozled during one of my past tests. And made me believe that they disabled those memes entirely. But now i know that i got pranked by my own code.
Conclusion
- EAC still seems to put a malformed value in apex’s KProcess::DirectoryTableBase field. Meaning all those common APIs (both R0/R3) for interacting with virtual process memory will not work. You will also have to obtain the real value at least once. Please avoid brute forcing it even if it technically may seem suitable now. I’d also stay away from using NMIs for that purpose.
- It no longer randomizes the actually used, real CR3 value of apex. You will no longer have to maintain it, meaning all those brute forcing cheats have now been buffed. They don’t get their cheat lagging around every 5-10 seconds anymore.
- You still have extra work ahead if you plan to use native APIs on the game process, like NtRead/NtWriteVirtualMemory and MmCopyVirtualMemory. Also don’t forget their older detection methods for these, like performing analysis on a thread that was attached to the game via these APIs.
This part is still WIP.