Detouring Yourself Into a Ban: Reverse-Engineering AMD Anti-Lag+
contents
A symbol-level teardown of the Adrenalin 23.10.1 feature that got Counter-Strike 2 players VAC-banned - what it hooked, how it hooked it, and the single byte that turned it off.
In October 2023, AMD shipped Anti-Lag+ in Adrenalin 23.10.1. Within days, Counter-Strike 2 players on Radeon cards started getting VAC-banned. Valve’s statement was one sentence:
“AMD’s latest driver has made their “Anti-Lag/+” feature available for CS2, which is implemented by detouring engine dll functions. If you are an AMD customer and play CS2, DO NOT ENABLE ANTI-LAG/+; any tampering with CS code will result in a VAC ban. Once AMD ships an update we can do the work of identifying affected users and reversing their ban. @AMD
AMD pulled the feature in 23.10.2 a week later. There was never a public, symbol-level explanation of what was detoured or how. This post is that teardown - done entirely statically, from the two shipping installers, with 7-Zip and Ghidra. Nothing was executed; no hook was built or modified. The point is to understand why a signed, legitimate vendor feature was structurally indistinguishable from a cheat.
TL;DR:
- Anti-Lag+ is internally called
Delag/DELAG_NEXT. It lives in the Direct3D usermode drivers that load into the game process -amdxc64.dll(DX11/12) andatidxx64.dll(DX9/legacy) - not the kernel miniport. - It installs an inline trampoline detour via Microsoft Detours, but it doesn’t hook a
D3D export. It walks the game’s own call stack, finds a
callinstruction inside the game’s code, and patches that. - The injected payload is a QueryPerformanceCounter-paced
Sleep/spin on the game’s render thread - a Reflex-style latency reducer. - 23.10.2 didn’t remove the code. It flipped a whitelist value from
'1'to'0'in the driver’s application-profile database and deleted CS2’s profile entry. The hook engine is still sitting in the DLL, dormant.
Acquiring the patient
Two installers, both with valid AMD Authenticode signatures (so the third-party mirror of 23.10.1 is genuine):
| Version | Date | Anti-Lag+ | Role |
|---|---|---|---|
| 23.10.1 | Oct 11 2023 | active | the build that shipped the detour |
| 23.10.2 | Oct 19 2023 | disabled | the hotfix - our diff partner |
The Adrenalin installers are PE self-extractors with a ~1.2 GB overlay; 7-Zip opens them directly (576 files). No need to run anything:
7z x .\AMD-23.10.1.exe -o.\23101\ -ir!amdxc64.dll -ir!atidxx64.dll -ir!amdihk64.dll
The naïve approach - hash everything and diff - is useless here: the entire display driver
was rebuilt between the two releases (build folder B396516 → B396804), so ~130 binaries
differ by build stamp alone. You have to triage by function, not by hash.
Finding “Delag”
A string sweep of the usermode D3D drivers lights up immediately. amdxc64.dll is full of a
debug-config system that registers globals by name for a C:\DELAG\DELAG.INI override file -
and those names are a feature spec in themselves:
&DelagGlobals::g_uiREQUIRED_STACK_DEPTH_TO_HOOK
&DelagGlobals::g_bFOLLOW_ALL_JUMPS_WHEN_HOOKING
&DelagHooks::g_bATTEMPT_DELAYED_DETOURING_FIRST
&DelagGlobals::g_uiGAME_IS_WHITELISTED_FOR_DELAG_NEXT
&DelagGlobals::g_uiDELAG_WHITELISTING_TARGET_RELEASE
DELAG_NEXT: ADVANCED_INTERCEPT: Captured call stack with %i frames...
DELAG_NEXT: ADVANCED_INTERCEPT: Installing the advanced intercept at stack depth %i...
DELAG_NEXT: ADVANCED_INTERCEPT: MyDetourAttach(): failed with code %i !!!!!!!!
AntiLag+ latency meter is only available for the DX12 version of this game.
DELAG_NEXT = “Delag Next” = Anti-Lag**+**. MyDetourAttach, “Captured call stack”,
“Installing the advanced intercept at stack depth” - this is a hooking engine that decides
where to hook by walking a call stack. And shipping right alongside it: detoured.dll,
the Microsoft Detours marker DLL.
The same 64 Delag* strings appear, byte-identical, in atidxx64.dll (the DX9/legacy UMD).
The feature was statically linked into both Direct3D usermode drivers.
The targeting: hooking the game by walking its stack
Here is the heart of it - amdxc64.dll @ 0x1805abcc0, the “ADVANCED_INTERCEPT” installer,
lightly cleaned up from Ghidra’s output:
undefined1 Delag_InstallAdvancedIntercept(uint depth) // depth = g_uiREQUIRED_STACK_DEPTH_TO_HOOK
{
void *stack[32];
RtlCaptureStackBackTrace(0, 0x20, stack, 0); // capture the GAME's call stack
uint frames = ...;
if (frames <= depth) return 0; // "iDepth >= uiNumStackFrames -> abort"
byte *retaddr = stack[depth]; // a return address INSIDE the game
int call_size = GetCallInstructionSize(retaddr); // length-decode the preceding call
if (call_size == 0)
call_size = retaddr - ScanBackwardForCall(retaddr); // GetPreviousInstruction fallback
byte *target = retaddr - call_size; // the exact CALL site in the game's .text
if (call_size >= 5) return Delag_DetourGameCallSite(target); // 5-byte E9 detour
else return AdvancedDirectWithStepping(target); // relocate to make room
}
This is the whole problem in twelve lines. Anti-Lag+ runs as the D3D usermode driver, so it
is already executing on the game’s render thread. It calls RtlCaptureStackBackTrace, picks
the return address depth frames up - an address inside the game’s own engine code - and
back-computes the call instruction that produced it. That call, inside the game’s .text,
is what gets patched.
It is not hooking Present, not hooking a D3D export, not hooking anything AMD owns. It is
reaching up the stack into the game and rewriting the game’s own instructions.
The attach is textbook Microsoft Detours (amdxc64.dll @ 0x1805aac10):
uint MyDetourAttach(void *target, void *hookFn, void **trampoline)
{
DetourTransactionBegin();
DetourUpdateThread(GetCurrentThread());
*trampoline = target;
DetourAttach(trampoline, hookFn);
uint err = DetourTransactionCommit();
if (err == 0) GetModuleHandleExA(..., target, ...); // pin the patched (game) module
else printf("...MyDetourAttach(): failed with code %i", err);
return (err == 0);
}
What does the actual byte surgery? amdxc64 carries its own statically-linked copy of
Microsoft Detours, and DetourTransactionCommit writes the patch in place. Detours’ internals
are a slog to read, so here is the same operation in the clear, from a standalone sibling engine
that ships in the very same package - amdihk64.dll (“AMD inline-hook”), which gets its own
teardown in Part 2. Its commit routine (@ 0x1800014dc) is the
technique laid bare - and it’s exactly what Detours does, too:
// for each pending hook, with all other threads suspended:
target[0] = 0xE9; // JMP rel32 over the game's prologue...
*(int*)&target[1] = (trampoline+0x30) - (target+5); // ...to the trampoline
// trampoline+0x30: FF 25 <rip-rel32> = JMP qword [rip+disp] (absolute jump onward)
// slack filled with 0xCC
// then walk every suspended thread's CONTEXT and rewrite RIP if it was caught
// mid-patch (original <-> trampoline), so nothing resumes inside a half-written instruction
VirtualProtect(target, len, oldProt, &tmp);
FlushInstructionCache(GetCurrentProcess(), target, len);
ResumeThread(...);
In that commit: an E9 over a victim prologue, an FF 25 absolute jump in the trampoline,
VirtualProtect + FlushInstructionCache, and even thread-RIP fixups so it can hook hot code
safely. That’s the technique - and it’s what amdxc64’s own Detours commit does to CS2’s call
site too. If you showed this routine to an anti-cheat engineer with the symbols stripped, they
would call it a cheat. There is nothing to distinguish it.
The payload: what it actually did to the game
When the detour fires, the game’s call lands in AMD’s callback (@ 0x1805aa4b8) instead. It
RtlCaptureContexts the game’s registers into a TLS buffer, restores the originals the
trampoline saved, runs the payload, and RtlRestoreContexts back into the game. The payload
(@ 0x1805a0f10) is a frame pacer:
void Delag_FramePacingPayload(void)
{
EnterCriticalSection(&g_delagLock);
uint64 now = NowMicroseconds(); // QueryPerformanceCounter * 1e6 / QPF
// 16-entry ring buffer of recent frame timestamps
push_frame_time(now);
// last few inter-frame deltas -> stabilized frame time, with hysteresis:
bool act = (stable_frametime <= target*4/3) && (now - last_action < 100_000 /*us*/);
if (latest_interval < target*3/4) act = false;
if (act) {
uint64 deadline = now + per_frame_delay * multiplier;
while (WaitUntilDeadline(deadline) != 0) // STALL the game's own thread until deadline
;
}
LeaveCriticalSection(&g_delagLock);
}
WaitUntilDeadline is a hybrid wait: Sleep(1) while >~10 ms remain, Sleep(0) (yield) for
1–10 ms, and a busy-spin for the final sub-millisecond. The net effect is to delay the
start of the game’s CPU frame work so that input sampling and simulation happen as late as
possible relative to GPU present - collapsing the render-ahead queue and lowering
click-to-photon latency. Same goal as NVIDIA Reflex and the original driver-side Anti-Lag.
The idea is completely benign. The delivery - patching the game’s .text and stalling its
thread from a foreign module - is what VAC sees.
The gate, and a one-byte off-switch
Whether the detour installs at all is decided by Delag_CheckWhitelistAndEnable
(@ 0x1805952a0):
release = profile_query("g_uiDELAG_WHITELISTING_TARGET_RELEASE"); // driver rollout ceiling
game_wl = profile_query("g_uiGAME_IS_WHITELISTED_FOR_DELAG_NEXT"); // per-game whitelist value
if (game_wl != 0 && game_wl <= release) {
CreateThread(DelayedDetourInstaller, ...); // <-- spawns the worker that hooks the game
g_delagActive = 1;
return 1;
}
return 0;
Both values come from AMD’s application-profile database, atiapfxx.blb - the same system
that stores per-game settings like Dlg_LimitFPS, Bst_WListed (Boost whitelist),
Ris_BListed (Image Sharpening), and FreMux12WListed (Frame Generation). A game gets
Anti-Lag+ if, and only if, its profile is whitelisted at a release ≤ the driver’s ceiling.
Which means you can disable the whole feature without touching a single instruction. And that is exactly what 23.10.2 did.
Diffing atiapfxx.blb between the two drivers (it’s the only feature-relevant file whose
content, not just build stamp, changed: 500,744 → 503,128 bytes), the whitelist records
look like this:
00043 1B0 44 6C 67 4E 78 74 5F 57 4C 69 73 74 65 64 00 00 DlgNxt_WListed..
00043 1C0 02 00 00 00 [31] 00 00 00 5F 7C 00 00 ... value = 0x31 = '1' (23.10.1)
^^^^
00043 698 44 6C 67 4E 78 74 5F 57 4C 69 73 74 65 64 00 00 DlgNxt_WListed..
00043 6A8 02 00 00 00 [30] 00 00 00 FA 7C 00 00 ... value = 0x30 = '0' (23.10.2)
DlgNxt_WListed - “Delag-Next Whitelisted” - is the per-game Anti-Lag+ enable flag. The diff:
- 23.10.1: 18 game records, every one set to
'1'. - 23.10.2: 29 game records, every one set to
'0'. - CS2’s profile (
cs2.exe,CounterStrike2,CounterStrike2Profile) is present only in 23.10.1 - it was deleted outright in the hotfix;cs2.exewas even removed from the driver’s master application-name table (whilecsgo.exestayed). - 11 mostly-DX12, mostly-single-player titles (Starfield, Hogwarts Legacy, Spider-Man,
Returnal, …) were added - staged for the eventual re-enable, but all parked at
'0'.
So the famous “fix” was: flip every whitelist value from '1' to '0', and delete CS2’s
profile. The detour engine, the stack-walker, the Detours integration, the QPC pacer - all of
it is still sitting in 23.10.2’s amdxc64.dll, byte-for-byte (the gate function is at the same
address with identical logic). It just never gets switched on.
(One honest caveat: DlgNxt_WListed as a literal string appears only in the .blb - not in
any driver binary, including the 106 MB kernel module - so the name→global wiring is resolved
through the profile descriptor system, not a hardcoded strcmp. The identification rests on
the matching name, the proven sibling mechanism for Bst_WListed/Dlg_BListed, and the
perfectly-correlated 1→0 flip. We couldn’t xref a string that isn’t there.)
Why it was indistinguishable from a cheat
Anti-cheats verify the integrity of the game’s loaded modules: the bytes of cs2.exe and its
engine DLLs in memory should match what was loaded from disk. Anti-Lag+:
- runs as the D3D usermode driver inside the game process (legitimately loaded),
- walks the game’s call stack to find a function in the game’s own code, and
- overwrites that code’s prologue with a jump into an AMD DLL (a Detours patch - the
byte-level shape is shown in Part 2), then diverts the
game’s thread into that DLL where it hijacks/restores CPU context and calls
Sleep.
Steps 2–3 are operationally identical to an aimbot installing a hook. There is no signature that distinguishes “vendor latency optimization” from “cheat” at the memory-integrity level - both are unauthorized inline detours of the game image. VAC flagged the modified pages, and it was right to; the legitimacy of the patcher is simply invisible to an integrity check. That is the entire incident.
Epilogue: Anti-Lag 2
The redesign, Anti-Lag 2, fixes the root cause by inverting the direction of control. Instead of the driver reaching into the game, the game reaches out to the driver through an integrated SDK - the developer inserts AMD’s calls at the right points in their own frame loop, exactly like NVIDIA Reflex. Valve integrated Anti-Lag 2 into Counter-Strike 2 itself in 2024. Same end goal (pace the frame to cut input latency), but nothing patches protected memory, so there is nothing for VAC to flag.
The lesson is a clean one: the technique wasn’t malicious, but a detour is a detour. If your latency optimization is indistinguishable from a cheat at the byte level, the only safe place to put it is inside the game, with the game’s consent.
Function map
| Module | Address | Role |
|---|---|---|
amdxc64.dll | 0x1805952a0 | Delag_CheckWhitelistAndEnable - whitelist/release gate |
amdxc64.dll | 0x1805abcc0 | Delag_InstallAdvancedIntercept - stack-walk → game call site |
amdxc64.dll | 0x1805ab4e0 | Delag_DetourGameCallSite - direct E9 detour |
amdxc64.dll | 0x1805aac10 | MyDetourAttach - Microsoft Detours transaction |
amdxc64.dll | 0x1805aa4b8 | hook callback - context shim |
amdxc64.dll | 0x1805a0f10 | Delag_FramePacingPayload - QPC-paced wait |
amdxc64.dll | 0x1805a4c60 | Delag_WaitUntilDeadline - Sleep/spin |
amdihk64.dll | 0x1800014dc | trampoline commit in the sibling engine - E9 + trampoline + thread-RIP fixups (representative; amdxc64’s embedded Detours does the equivalent for Delag) |
Engine present identically in amdxc64.dll (DX11/12) and atidxx64.dll (DX9/legacy).
All analysis static: 7-Zip extraction, Ghidra via MCP. No code was executed; no hook built or
modified. Defensive/educational reverse engineering.