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.

3 Replies to “DR (Debug Register) Safety/Reliability and Accounting Features in Windows 2003”

  1. I remember that problem on Windows 2000 well. I ran into it a couple times while trying to set hardware breakpoints on a stack variable while trying to reverse engineer a particular program, following which my box would spontaneously bluescreen after that same stack location tripped a breakpoint in one of the critical processes. Quite annoying.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.