Windows AppLocker Driver LPE Vulnerability – CVE-2024-21338

Windows AppLocker Driver LPE Vulnerability – CVE-2024-21338

Reading Time: 4 minutes

When I initially interviewed candidates for CF’s Windows Researchers position, one of the challenges I gave out was related to CVE-2024-21338. A Windows Kernel Elevation of Privileges, specifically an Untrusted Pointer Dereference vulnerability in the appid.sys driver. The driver is responsible for the AppLocker technology.

Back then, this vulnerability became famous thanks to Avast’s beautiful work on the Lazarus FudModule Rootkit.

This is the detailed write-up from our newly hired researcher; welcome to the team! 🙂

TL;DR

Following the detailed posts from Avast, the main takepoints of the vulnerability can be summed up as follows:

  1. The bug resides in the AppHashComputeImageHashInternal() function, which could be invoked by sending an IOCTL with value 0x22A018 to the device object named \\Device\Appid.
  2. The driver expects two pointers referenced from the IOCTL’s input buffer.
  3. This bug results in a powerful primitive, given that we have complete control of the instruction pointer and the data in the first argument via a callback.
  4. Based on the ACL present on the device object name, only the LOCAL SERVICE and AppIDSvc users have enough permission to send the target IoControlCode.
  5. The target driver, appid.sys, is not automatically loaded and requires sending an event to a specific AppLocker-related ETW provider.

Challenges

SMEP & kCFG

  • Although we have full control over the instruction pointer, we cannot just supply a user-mode pointer to directly execute our shellcode due to the presence of the Supervisor Mode Execution Prevention (SMEP) mitigation.
  • The callback function is an indirect call, so we must find a valid pointer in the kernel space to bypass the safeguard from Kernel Control Flow Guard (kCFG).

KASLR

Because this exploit would be run under the context of the LocalService user, KASLR is not a big problem here. With the help of the evergreen NtQuerySystemInformation() syscall, we can easily leak almost all the kernel addresses that are necessary for building the exploit.

Loading the target driver

Because the driver is not loaded by default, we can load it manually by either using the Service Manager or sending an event to an AppLocker-related ETW provider to kick off the AppID service.
For testing purposes, this driver was already loaded into the kernel space.

Exploitation

Root Cause Analysis

When an IOCTL with value 0x22A018 is received, the AipSmartHashImageFile() function is called to handle this control code. In Windows 11, the first parameter is expected to be a pointer of the following structure:

typedef struct _HASH_IMAGE_FILE {
    PVOID ImageContext;             // pointer to the context of hashing file image
    FILE_OBJECT *FileObject;        // kernel object pointer of file 
    PVOID CallbackTable;            // pointer to callback functions
    ULONGLONG Action;               // not so sure  
} HASH_IMAGE_FILE, *PHASH_IMAGE_FILE;

Then the AppHashComputeFileHashesInternal() function is called to compute the hash of the target file. The first two parameters passed to this function are user-controllable:

Later, after some initialization, the function AppHashComputeFileHashesInternal() calls AppHashComputeImageHashInternal() to get the hash value(s), and the first two parameters are passed directly to the target function.

Inside the AppHashComputeImageHashInternal() function, before computing the hash value of the target image, it performs a calling to 2 callback functions from the 2nd parameter pointer, and these pointers are entirely user controllable. Thus leading to a complete RIP takeover condition.

Overall, the graph call to vulnerable function looks like this:

SMEP & kCFG bypassing

Due to the presence of SMEP and kCFG, if we call a random ROP gadget or shellcode directly from the user mode, the kernel would bail out with a bug check. Instead, we must find some useful functions to help us perform a data-only attack.

After reading some articles (1, 2), I found that the function nt!SeSetAccessStateGenericMapping() is widely used to bypass kCFG. However, it requires the first parameter to point to a struct of size, at least 0x50:

Meanwhile, the target IOCTL requires the input pointer to be of size 0x20 (size of _HASH_IMAGE_FILE structure):

So I decided to find a gadget by myself.

Spending some time looking for small functions performing interesting things, based on the given constraints, I found a candidate that worked: nt!DbgkpTriageDumpRestoreState. This gadget allows to overwrite 8 bytes at offset 0x2078 of the first pointer (this structure is the ImageContext pointer) with the value at offset 0x10 (field CallbackTable in this case) of the input structure:

Building the exploit

There are two possible ways to exploit this bug using the gadget above:

  1. Set the PreviousMode field of a target KTHREAD, then abuse the NtReadProcessMemory/NtWriteProcessMemory syscalls to archive arbitrary read/write over the kernel space. In Avast’s report, Lazarus used this technique.
  2. Overwriting the _SEP_TOKEN_PRIVILEGES structure of the current process’s token to enable the SeDebugPrivilege and abuse this privilege to inject a shellcode in a privileged process. This way requires triggering the bug twice in order to overwrite both the Present and Enabled fields.

Although the write value is also the pointer to the CallbackTable field of the _HASH_IMAGE_FILE structure, it must be a valid pointer value. However, by utilizing the VirtualAlloc API, it’s still possible to make a suitable value for writing since both of the two methods presented above don’t require a (too) specific value. In my exploit, I have demonstrated both ways to get a SYSTEM shell.

Exploit Code

You can find the expliot code on GitHub

Demo

POC 1 – Abusing PreviousMode

POC 2 – Abusing SeDebugPrivilege

References

Share this post