DR (Debug Register) Safety/Reliability and Accounting Features in Windows 2003

As some of you may know, Windows 2000 and even XP suffered from multiple validation/sanitation lacks in DR handling during Context<->Trap Frame conversion. The former is the CONTEXT structure used by Win32, and the latter refers to the KTRAP_FRAME structure used in NT. Many APIs such as Set/GetThreadContext, NtContinue, VDM Stuff, User-mode APCs and User-mode Exception Handling as well as Win32k User-mode Callbacks will eventually convert from one form of the structure to the other. These structures contain the entire CPU state (the KTRAP_FRAME doesn’t contain FPU/NPX Stuff, this is saved on the thread’s kernel stack instead), such as segments, registers and eflags.

You can imagine that a really poorly written kernel would allow you to do something like this in user-mode:

Context.SegCs = KGDT_R0_CODE;
NtSetThreadContext(Thread, &Context); and this would save the Ring 0 CS Selector into the KTRAP_FRAME, which is used when returning back to user-mode, thus giving you Ring 0 access.

Of course, DaveC wasn’t that stupid.

The NT Kernel heavily validates (or “sanitizes” EFLAGS and the fs, ds, es, cs selectors, as well as ensures DR6 and DR7 are valid). However, older versions of Windows did not fully ensure the safety of these registers. In case you didn’t know, the DRs, or Debug Registers, are a series of 32-bit registers on the x86 CPU provided for hardware breakpoints and other debugger support. DR0, 1, 2 and 3 are used to hold the addresses of the hardware breakpoints, while DR6 is a status register, and DR7 is a control register.

Already, you can guess that you really don’t want user-mode to give you kernel-mode pointers in DR0-3. The kernel would be blissfully unware that you’ve just set breakpoints in kernel space, and crash when those pointers were hit. Windows 2000 does validate for this.

However, consider the scenario where the caller sets proper user-mode addresses. The kernel will allow this, and when those pointers are hit, the CPU will do a breakpoint, killing the process if no debugger is attached. Again, I insist that the CPU is entirely responsible for the exception. It has no knowledge of address spaces. This implies that these breakpoint addresses are global for the entire system. Windows 2000 allowed a lower-privilege application to set a debug register on on a specific address that would be hit in a remote process, and then crash that application. Careful crafting would allow the crash to be predictable, and exploitable, such as this advisory demonstrates.

This has long been fixed, and the entire way in which DR registers are handled has also been re-written to protect against some flaws that could happen under VDM or V8086 mode. The DISPATCHER_HEADER has a member called DebugActive, and it’s used for KTHREAD objects. This 1-byte value is actually a mask which represents which DR registers are valid for this thread. The masks are generated as follows:

//

// Thread Dispatcher Header DebugActive Mask

//

#define DR_MASK(x) 1 << x

#define DR_ACTIVE_MASK 0x10

#define DR_REG_MASK 0x4F

Notice, since there is no DR4 register, the 0x10 flag is actually used to specify whether debugging is actually active on the thread. Now if we take a look at KeContextToKframes, which converts a CONTEXT to a KTRAP_FRAME, the code is similar to this:

    /* Handle the Debug Registers */

    if ((ContextFlags & CONTEXT_DEBUG_REGISTERS) == CONTEXT_DEBUG_REGISTERS)

    {

        /* Loop DR registers */

        for (i = 0; i < 4; i++)

        {

            /* Sanitize the context DR Address */

            SafeDr = Ke386SanitizeDr(KiDrFromContext(i, Context), PreviousMode);

 

            /* Save it in the trap frame */

            *KiDrFromTrapFrame(i, TrapFrame) = SafeDr;

 

            /* Check if this DR address is active and add it in the DR mask */

            if (SafeDr) DrMask |= DR_MASK(i);

        }

 

        /* Now save and sanitize DR6 */

        TrapFrame->Dr6 = Context->Dr6 & DR6_LEGAL;

        if (TrapFrame->Dr6) DrMask |= DR_MASK(6);

 

        /* Save and sanitize DR7 */

        TrapFrame->Dr7 = Context->Dr7 & DR7_LEGAL;

        KiRecordDr7(&TrapFrame->Dr7, &DrMask);

 

        /* If we’re in user-mode */

        if (PreviousMode != KernelMode)

        {

            /* Save the mask */

            KeGetCurrentThread()->DispatcherHeader.DebugActive = DrMask;

        }

    }

Likewise, the converse function, KeContextFromKframes, uses the following blob:

    /* Handle debug registers */

    if ((Context->ContextFlags & CONTEXT_DEBUG_REGISTERS) ==

        CONTEXT_DEBUG_REGISTERS)

    {

        /* Make sure DR7 is valid */

        if (TrapFrame->Dr7 & ~DR7_RESERVED_MASK)

        {

            /* Copy the debug registers */

            Context->Dr0 = TrapFrame->Dr0;

            Context->Dr1 = TrapFrame->Dr1;

            Context->Dr2 = TrapFrame->Dr2;

            Context->Dr3 = TrapFrame->Dr3;

            Context->Dr6 = TrapFrame->Dr6;

 

            /* Update DR7 */

            Context->Dr7 = KiUpdateDr7(TrapFrame->Dr7);

        }

        else

        {

            /* Otherwise clear DR registers */

            Context->Dr0 =

            Context->Dr1 =

            Context->Dr3 =

            Context->Dr6 =

            Context->Dr7 = 0;

        }

    }

This new code ensures not only that DR7 and DR6 are valid, but also clears the DR registers if DR7 is invalid, as well as creates a specific per-thread mask specifying which DR registers are enabled and which are not, which protects from the random activation or use of DR addresses. Also, DR7 specifies which of the DRx registers are actually in use, so this information also needs to be kept into account. The KiUpdate/RecordDr7 routines are shown below:

ULONG

FASTCALL

KiUpdateDr7(IN ULONG Dr7)

{

    ULONG DebugMask = KeGetCurrentThread()->DispatcherHeader.DebugActive;

 

    /* Check if debugging is enabled */

    if (DebugMask & DR_ACTIVE_MASK)

    {

        /* Sanity checks */

        ASSERT((DebugMask & DR_REG_MASK) != 0);

        ASSERT((Dr7 & ~DR7_RESERVED_MASK) == DR7_OVERRIDE_MASK);

        return 0;

    }

 

    /* Return DR7 itself */

    return Dr7;

}

 

BOOLEAN

FASTCALL

KiRecordDr7(OUT PULONG Dr7Ptr,

            OUT PULONG DrMask)

{

    ULONG NewMask, Mask;

    UCHAR Result;

 

    /* Check if the caller gave us a mask */

    if (!DrMask)

    {

         /* He didn’t use the one from the thread */

         Mask = KeGetCurrentThread()->DispatcherHeader.DebugActive;

    }

    else

    {

        /* He did, read it */

        Mask = *DrMask;

    }

 

    /* Sanity check */

    ASSERT((*Dr7Ptr & DR7_RESERVED_MASK) == 0);

 

    /* Check if DR7 is empty */

    NewMask = Mask;

    if (*Dr7Ptr)

    {

        /* Assume failure */

        Result = FALSE;

 

        /* Check the DR mask */

        NewMask &= 0x7F;

        if (NewMask & DR_REG_MASK)

        {

            /* Set the active mask */

            NewMask |= DR_ACTIVE_MASK;

 

            /* Set DR7 override */

            *DrMask = DR7_OVERRIDE_MASK;

        }

        else

        {

            /* Sanity check */

            ASSERT(NewMask == 0);

        }

    }

    else

    {

        /* Check if we have a mask or not */

        Result = NewMask ? TRUE: FALSE;

 

        /* Update the mask to disable debugging */

        NewMask &= ~DR_ACTIVE_MASK;

        NewMask |= 0x80;

    }

 

    /* Check if caller wants the new mask */

    if (DrMask)

    {

        /* Update it */

        *DrMask = NewMask;

    }

    else

    {

        /* Check if the mask changed and update it directly */

        if (Mask != NewMask) KeGetCurrentThread()->DispatcherHeader.DebugActive;

    }

 

    /* Return the result */

    return Result;

}

The code above is from ReactOS and may contain bugs :). Some Macros/defines are missing but the overall point should be clear. Next time you’re debugging a thread and come across DebugActive having a value that you expected was TRUE or FALSE, hopefully this should give you some insight.

On another note, I have started working on the NDK article and hope to finish it by tomorrow.

GCC and Vista Incompatibility

Since ReactOS is still being built with GCC (unfortunately), some of our devs have started to report a problem when using the MinGW build under Windows Vista. The call to MapViewOfFileEx that the compiler users for precompiled header support fails, so the compilation fails for any project that uses a PCH.

This type of error might creep up in other system software as well, and it’s not really GCC’s fault for succumbing to it. If you look at the documentation for CreateFileMapping, you’ll notice this blurb in the Remarks section:

Creating a file mapping object from a session other than session zero requires the SeCreateGlobalPrivilege privilege. Note that this privilege check is limited to the creation of file mapping objects and does not apply to opening existing ones. For example, if a service or the system creates a file mapping object, any process running in any session can access that file mapping object provided that the caller has the required access rights.

Windows XP/2000: The requirement described in the previous paragraph was introduced with Windows Server 2003, Windows XP SP2 and Windows 2000 Server SP4.

Although this feature was added in SP2, the reason it doesn’t happen in Windows XP has to do with two changes in Vista. First, UAC means that programs don’t get the SeCreateGlobalPrivilege anymore, because they’re not running in administrator accounts anymore. Secondly, in Vista, Session 0 is now the SYSTEM account session, where the login screen and services are running. Therefore, any user processes will run in Session 1, even in a normal single-user system. These two factors combined mean that CreateFileMapping is now significantly reduced in functionality and that only services are allowed to create global shared memory.

There are three workarounds if you really need the functionality:

  1. Use the Microsoft Management Console (MMC) and the Local Security Policy Snap-In to give SeCreateGlobalPrivilege to the limited account.
  2. Write a wrapper program that executes with elevated rights and and uses RtlAcquire/AdjustPrivilege to get the privilege before running your target program (Such as gcc).
  3. Use the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Kernel\ObUnsecureGlobalNames string array to add the name of the section to the list. Hopefully your program isn’t randomizing the name. Adding this name will disable the kernel protection check.

Why Io/NtCreateFile Fails…

If you’ve ever had a call to NtCreateFile or IoCreateFail fail with the very helpful “STATUS_INVALID_PARAMETER” status code, you can now how exciting it can be to track down which flag exactly you might’ve messed up. Hopefully this snippet of code should help you manually validate your call (I’m not sure if the latest PREFast checks for these things when compiling). This code is from ReactOS.

/* Check if we need to check parameters, or if we’re user mode */

if ((AccessMode != KernelMode) || (Options & IO_CHECK_CREATE_PARAMETERS))

{

    /* Validate parameters */

    if ((FileAttributes & ~FILE_ATTRIBUTE_VALID_FLAGS) ||

        (ShareAccess & ~FILE_SHARE_VALID_FLAGS) ||

        (Disposition > FILE_MAXIMUM_DISPOSITION) ||

        (CreateOptions & ~FILE_VALID_OPTION_FLAGS) ||

         (CreateOptions &

         (FILE_SYNCHRONOUS_IO_ALERT | FILE_SYNCHRONOUS_IO_NONALERT) &&

         (!(DesiredAccess & SYNCHRONIZE))) ||

        ((CreateOptions &

         (FILE_SYNCHRONOUS_IO_NONALERT | FILE_SYNCHRONOUS_IO_ALERT)) ==

         (FILE_SYNCHRONOUS_IO_NONALERT | FILE_SYNCHRONOUS_IO_ALERT)) ||

        ((CreateOptions & FILE_DIRECTORY_FILE) &&

         !(CreateOptions & FILE_NON_DIRECTORY_FILE) &&

          ((CreateOptions & ~(FILE_DIRECTORY_FILE |

                              FILE_SYNCHRONOUS_IO_ALERT |

                              FILE_SYNCHRONOUS_IO_NONALERT |

                              FILE_WRITE_THROUGH |

                              FILE_COMPLETE_IF_OPLOCKED |

                              FILE_OPEN_FOR_BACKUP_INTENT |

                              FILE_DELETE_ON_CLOSE |

                              FILE_OPEN_FOR_FREE_SPACE_QUERY |

                              FILE_OPEN_BY_FILE_ID |

                              FILE_OPEN_REPARSE_POINT)) ||

          ((Disposition != FILE_CREATE) &&

           (Disposition != FILE_OPEN) &&

           (Disposition != FILE_OPEN_IF)))) ||

        ((CreateOptions & FILE_COMPLETE_IF_OPLOCKED) &&

         (CreateOptions & FILE_RESERVE_OPFILTER)) ||

        ((CreateOptions & FILE_NO_INTERMEDIATE_BUFFERING) &&

         (DesiredAccess & FILE_APPEND_DATA)))

    {

        /*

         * Parameter failure. We’ll be as unspecific as NT as to

         * why this happened though, to make debugging a pain!

         */

        DPRINT1(“File Create Parameter Failure!\n”);

        return STATUS_INVALID_PARAMETER;

    }

 

    /* Now check if this is a named pipe */

    if (CreateFileType == CreateFileTypeNamedPipe)

    {

        /* Make sure we have extra parameters */

        if (!ExtraCreateParameters) return STATUS_INVALID_PARAMETER;

 

        /* Get the parameters and validate them */

        NamedPipeCreateParameters = ExtraCreateParameters;

        if ((NamedPipeCreateParameters->NamedPipeType >

             FILE_PIPE_MESSAGE_TYPE) ||

            (NamedPipeCreateParameters->ReadMode >

             FILE_PIPE_MESSAGE_MODE) ||

            (NamedPipeCreateParameters->CompletionMode >

             FILE_PIPE_COMPLETE_OPERATION) ||

            (ShareAccess & FILE_SHARE_DELETE) ||

            ((Disposition < FILE_OPEN) || (Disposition > FILE_OPEN_IF)) ||

            (CreateOptions & ~FILE_VALID_PIPE_OPTION_FLAGS))

        {

            /* Invalid named pipe create */

            return STATUS_INVALID_PARAMETER;

        }

    }

    else if (CreateFileType == CreateFileTypeMailslot)

    {

        /* Make sure we have extra parameters */

        if (!ExtraCreateParameters) return STATUS_INVALID_PARAMETER;

 

        /* Get the parameters and validate them */

        MailslotCreateParameters = ExtraCreateParameters;

        if ((ShareAccess & FILE_SHARE_DELETE) ||

            !(ShareAccess & ~FILE_SHARE_WRITE) ||

            (Disposition != FILE_CREATE) ||

            (CreateOptions & ~FILE_VALID_MAILSLOT_OPTION_FLAGS))

        {

            /* Invalid mailslot create */

            return STATUS_INVALID_PARAMETER;

        }

    }

}

News and Todos…

I apologize for the lack of updates, but these upcoming weeks (until December 20th) are my finals, and I can’t afford to do badly on them, so I won’t probably have time to blog at all or post part 3 of my article on OpenRCE. I apologize for the disappointment.

However, here’s what I plan on writing on once I have some time:

1) Recognizing macros and other constructs in IDA for MSVC binaries. Will focus on Microsoft kernel-mode code.

2) Unveiling of the NDK and a sample background application that uses LPC.
3) A sample AFD (Ancilliary Function Driver) Client and Server, and its relationship to security/rootkit detection.

See you all soon!