pwn.dog

The Control Plane: How One Byte in a Profile Blob Disabled Anti-Lag+

contents

Part 3 of the Anti-Lag+ teardown. Part 1 found the hook in the game; Part 2 tore down the engine that installs it. Both were about code. This part is about the thing that actually decides whether any of it runs - the data.


Here’s the detail that reframes the whole incident. When AMD “disabled” Anti-Lag+ in the 23.10.2 hotfix, they did not ship new hook logic, remove the detour engine, or patch a single instruction. We proved in Parts 1-2 that the gate function Delag_CheckWhitelistAndEnable is byte-identical across 23.10.1 and 23.10.2. The entire trampoline machinery is still sitting dormant in the shipped driver.

So what changed? One file. One byte per game. The switch lives in AMD’s per-application profile database, atiapfxx.blb - the driver’s control plane.

Where features are actually turned on

Anti-Lag, Radeon Boost, Image Sharpening, Frame Generation, Chill - none of these are simply “on.” Each is gated per-game by a profile. The profiles ship in atiapfxx.blb (~500 KB), which is loaded by the ADL layer (atiadlxx.dll / atiadlxy.dll) and handed to the D3D usermode driver, where amdxc64.dll!FUN_180182f50 walks each game’s properties and flips the matching feature flags:

// amdxc64 - apply one app profile to the per-game settings.  eq(a,b) := _stricmp(a,b)==0
while (i < count) {
    if (eq(name, "Dlg_BListed")     && val) cfg->delag_off   = 1;   // Anti-Lag blocklist
    if (eq(name, "Bst_WListed"))            cfg->boost_wl    = val; // Boost whitelist
    if (eq(name, "Ris_BListed")     && val) cfg->ris_off     = 1;   // Image Sharpening
    if (eq(name, "FreMux12WListed") && val) cfg->framegen_wl = 2;   // Frame Gen
    // ... Dlg_PFEnable, Dlg_LimitFPS, Bst_PFEnable, Chil_*, TS_BListed, VSyncControl ...
    name += align8(value_len + 0x1b);   // advance to the next record
}

These short tokens (Dlg_ = Delag/Anti-Lag, Bst_ = Boost, Ris_, Chil_ = Chill) are the property names baked into the blob. Anti-Lag+ specifically is gated by a release-versioned whitelist read in the same subsystem (from Part 1):

game_wl = profile("g_uiGAME_IS_WHITELISTED_FOR_DELAG_NEXT");  // per-game value
release = profile("g_uiDELAG_WHITELISTING_TARGET_RELEASE");   // driver rollout ceiling
if (game_wl != 0 && game_wl <= release)                       // <- the entire on/off test
    spawn_detour_installer();

So a game gets Anti-Lag+ if and only if its profile says so. To kill the feature, you don’t touch code - you edit the database.

The BWJE format

atiapfxx.blb starts with a four-byte magic and a version, followed by a content hash and an offset table into the records:

00000000  42 57 4A 45 01 00 00 00  F7 A5 FB C8 5E BA FA 8C   BWJE........^...
00000010  96 F9 FC 57 5A ED 8F D1  B6 90 47 15 14 01 00 00   ...WZ.....G.....
          ^^^^^^^^^^^                ^^^^^^^^^^^^^^^^^^^^^^^
          "BWJE" + ver=1             16-byte content hash (changes every build)

Each feature property is stored as a fixed record. The Anti-Lag+ whitelist entry is named DlgNxt_WListed (“Delag-Next Whitelisted”) and is 56 bytes:

000431B0  44 6C 67 4E 78 74 5F 57  4C 69 73 74 65 64 00 00   DlgNxt_WListed..
000431C0  02 00 00 00 [31]00 00 00  5F 7C 00 00 B0 48 00 00   ....1..._|...H..
                       ^^                                      value: 0x31 = '1'

The byte at offset +0x14 is the whitelist value, stored as an ASCII digit. There’s also a master application table mapping executables to profiles - and cs2.exe sits right next to its ancestors:

00034AD0  63 00 73 00 32 00 2E 00  65 00 78 00 65 00 00 00   c.s.2...e.x.e...
00034AE0  63 00 73 00 67 00 6F 00  2E 00 65 00 78 00 65 00   c.s.g.o...e.x.e.
                                                              (UTF-16: "cs2.exe", "csgo.exe")

The diff: one byte, times everything

Comparing the 23.10.1 (active) and 23.10.2 (disabled) blobs, the feature-relevant changes are surgical and unambiguous:

23.10.1 (active)23.10.2 (hotfix)
DlgNxt_WListed records18 games29 games
value at +0x14 (every record)0x31 = ‘1’ (whitelisted)0x30 = ‘0’ (off)
cs2.exe profilepresentdeleted
profile-gen stamp23.20.17.01_2023100423.20.17.03_20231016

Two independent kill switches, both thrown:

  1. The whitelist value was flipped '1' -> '0' for every game. Via the gate game_wl != 0 && game_wl <= release, a value of 0 means game_wl == 0, the test fails, and the detour installer never spawns. A global off-switch, in data.
  2. CS2’s profile was deleted outright - cs2.exe, CounterStrike2, CounterStrike2Profile exist only in 23.10.1, and cs2.exe was even pulled from the master table (while csgo.exe stayed). Belt and suspenders, specifically for CS2.

For good measure, 11 more (mostly DX12, single-player) titles were added to the profile set - staged for the eventual re-enable - but all parked at '0'.

Why a driver has a control plane at all

This design is genuinely sensible engineering. Shipping a per-game profile database lets AMD:

  • roll features out to a curated allowlist instead of “all games, hope for the best,”
  • tie a feature to a release (...TARGET_RELEASE) so a profile authored for build N only activates once the driver reaches N,
  • hotfix behavior without recompiling or re-signing the driver - just publish a new blob.

That last point is exactly what let the 23.10.2 hotfix happen in days rather than weeks. It’s also why the “fix” is invisible to a code-level diff: the dangerous machinery never moved.

There’s a sharper reason the whitelist had to exist, too. Part 1’s targeting keys off a fixed frame count (g_uiREQUIRED_STACK_DEPTH_TO_HOOK) to find the call site to patch - which assumes a specific, stable call graph in the target game. Recompile the game, flip a compiler flag, inline a function, and that depth lands somewhere else entirely. A blind heuristic that fragile can’t be unleashed on every title; it only works against builds someone validated by hand. So the per-game whitelist isn’t just caution - it’s a technical necessity of the targeting method, which loops Part 1’s brittleness right back to this control plane. (That causal link is my inference, not something the binary states outright.)

The series, in one line each

  • Part 1 - the hook: the D3D usermode driver stack-walks into CS2’s own code and inline-patches a call with Microsoft Detours.
  • Part 2 - the engine: a Detours-based trampoline library (amdihk64) in the same package - a sibling to the Delag detour - that also hooks raw input.
  • Part 3 - the switch: none of it runs unless a byte in atiapfxx.blb says so, and that byte is what AMD flipped to make the VAC bans stop.

The lesson worth keeping: when you reverse a feature, the code tells you how - but the control plane tells you whether. Anti-Lag+ was disabled by editing a database, and you’d never find that by reading disassembly alone.


All static analysis - 7-Zip extraction, hex diffing, and Ghidra over MCP. No code executed, no hooks built or modified. Companion to Part 1 and Part 2.