12

I'm trying to reverse the Dbgv.sys (x86 kernel driver) for the DbgView tool. It has this sub_10D4A function that is called almost at the beginning of the driver's DriverEntry function. It goes as such:

sub_10D4A snippet

The relevant piece of disassembly:

.text:00010D64                 mov     eax, ds:KeNumberProcessors
.text:00010D69                 mov     al, [eax]
.text:00010D6B                 cmp     al, 40h ; '@'
.text:00010D6D                 movsx   eax, al
.text:00010D70                 jl      short loc_10D74
.text:00010D72                 mov     eax, [eax] ; <-- line pointed out

What I don't understand is the construct that is pointed by the red arrow. If KeNumberProcessors global variable is larger than or equal to 64 CPU cores (or 40h) it will execute mov eax, [eax] instruction, which will try to read a DWORD from an address that is a number of CPUs, say 40h, which makes no sense.

Wouldn't it result in a BSOD? What's the intention there?

3 Answers 3

10

I still think this will create a BSOD, what's more I think that this is deliberate. It makes perfect sense to assume that this is deliberate once piecing all the puzzle pieces together. The source incompatibility will inevitably force that the developer notices the change of type for KeNumberProcessors from PCCHAR to CCHAR. The likeliest error is: error C2100: illegal indirection.

According to the PE header the file was created using WDK 7600.16385.1 (OS version), targeting XP (subsystem version), assuming we can trust best practices having been used to create the driver. However, the driver does run on Windows 2000 Professional (with SP4) as I verified. This is possible whenever the author is careful enough not to use functions unavailable on Windows 2000, but available at compile and link time for the nominal target.

Okay, so say you have an old driver targeting pre-XP Windows versions. You'll write this (some more code was necessary to force this code not to be optimized away):

CCHAR nCpus = *KeNumberProcessors;
PVOID lpBuf = ExAllocatePool(NonPagedPool, nCpus * 7);

when building this for Windows 2000 (free) as target (using WDK 6001.18002), we get for the line assigning nCpus and the subsequent one:

.text:00010519                 mov     eax, ds:__imp__KeNumberProcessors
; opcodes unrelated to processor number
.text:0001052E                 movsx   eax, byte ptr [eax]
.text:00010531                 imul    eax, 7
.text:00010534                 push    edi
.text:00010535                 push    eax             ; NumberOfBytes
.text:00010536                 push    ebx             ; PoolType
.text:00010537                 call    ds:__imp__ExAllocatePool@8 ; ExAllocatePool(x,x)
.text:0001053D                 mov     [ebp+lpBuf], eax

and when targeting Windows XP using the WDK 7600.16385.1 we get an error during compilation:

KNPs.cpp(102) : error C2100: illegal indirection

... which we fix by removing the * that dereferences KeNumberProcessors, which then gives us a successful compilation and the following code:

.text:00010519                 mov     eax, ds:__imp__KeNumberProcessors
; opcodes unrelated to processor number
.text:0001052E                 movsx   eax, byte ptr [eax]
.text:00010531                 imul    eax, 7
.text:00010534                 push    edi
.text:00010535                 push    eax             ; NumberOfBytes
.text:00010536                 push    ebx             ; PoolType
.text:00010537                 call    ds:__imp__ExAllocatePool@8 ; ExAllocatePool(x,x)
.text:0001053D                 mov     [ebp+lpBuf], eax

that's identical. And there is no indication of anything like in the .sys for DebugView that would deliberately cause a BSOD.

So my money is on: this is meant to cause a BSOD, knowing full well that this situation could realistically never occur.


Original answer below:

With the information I could dig up, I concur. This code would seem to lead to an inevitable BSOD.

Let's start by stating that the maximum available number of processors for Windows 2000 Server (Datacenter edition) was 32.

Based on the declaration for KeNumberProcessors, which is by the way deprecated in favor of KeQueryActiveProcessors:

#if (NTDDI_VERSION >= NTDDI_VISTA)
extern NTSYSAPI volatile CCHAR KeNumberProcessors;
#elif (NTDDI_VERSION >= NTDDI_WINXP)
extern NTSYSAPI CCHAR KeNumberProcessors;
#else
extern PCCHAR KeNumberProcessors;
#endif

that variable used to be a pointer prior to Windows XP. Looking at the above linked documentation you will find (relevant excerpts):

The KeNumberProcessors kernel variable is obsolete in Windows Vista with Service Pack 1 (SP1), Windows Server 2008, and later versions of Windows. KeNumberProcessors does not appear in WDK headers for WDK releases starting with Windows Vista SP1; however, the variable is still exported from the kernel, so drivers built for earlier platforms will not break

... and:

Starting with Windows XP, KeNumberProcessors is an 8-bit integer value that indicates the number of processors in the platform. In earlier versions of Windows, KeNumberProcessors is a pointer to an 8-bit integer value that indicates the number of processors in the platform.

Now, while I certainly may not have all the answers that the folks from Sysinternals/Microsoft have by glancing at the Windows source code, my guess is the following. This was meant to be a clever trick - relying on circumstantial knowledge - to accommodate both Windows XP and newer (where the variable is a CCHAR) as well as prior Windows versions, where it's a PCCHAR.

Older MSDN documentation is even more verbose:

Change in the Definition of KeNumberProcessors The kernel variable KeNumberProcessors indicates the number of active CPUs in the system on which a driver is running. In the Microsoft Windows XP versions of ntddk.h and wdm.h, the definition of KeNumberProcessors has been changed from a pointer to a value. The Microsoft® Windows® 2000 definition of this variable required that KeNumberProcessors be dereferenced (for example, *KeNumberProcessors). As a result of the changed definition of this variable, drivers built in the Windows XP build environment must not dereference this variable (for example, KeNumberProcessors).

Drivers that fail to use KeNumberProcessors correctly will get an "illegal indirection" error at compile time.

Note that regardless of the declaration used, drivers that properly reference KeNumberProcessors according to the environment in which they are built will work properly on both Windows 2000 and Windows XP. Thus, for instance, a driver built in the Windows 2000 build environment that dereferences KeNumberProcessors will get the proper value for this variable when the driver is run on either Windows 2000 or Windows XP.

Problem seems to be that they seem to have gotten it wrong, if I am not all mistaken. There's, however, also a chance they got it right and I am lacking some vital information they have access to :)

You can test the scenario with MSVC by using:

DWORD_PTR KeNumberProcessors = 0x80564321;
DWORD_PTR dsKeNumberProcessors = (DWORD_PTR)&KeNumberProcessors;
__asm {
    push eax
    mov eax, dsKeNumberProcessors
    mov al, [eax] // only affects AL -> AL := 0x21
    cmp al, 0x40
    movsx eax, al // garbles EAX
    pop eax
};

Stepping through it gives you a clue as to what's going on. I have to admit the movsx throws me off here.

Here's how I think it may have been meant to work

Read the byte at the address specified by KeQueryActiveProcessors. See if it's a value smaller than 0x40 and if so, stick with the interpretation as byte.

If bigger than 0x40, which happens whenever the least significant byte of the (Little Endian) address contained in KeQueryActiveProcessors (i.e. on Windows prior to XP) is big enough, we may assume that this is in fact a pointer and dereference it.

NB: re-read it: the least significant byte. So it's not the 0x80 in the base address of the kernel (the kernel is loaded at an address higher than 0x80000000) that matters here. For Windows 2000 I have found the following values for the least significant byte:

  • 0x70 (ntkrnlpa.exe, SP4)
  • 0xF0 (ntoskrnl.exe, SP4)
  • 0xC8 (ntkrnlpa.exe, RTM)
  • 0x30 (ntoskrnl.exe, RTM)

I didn't check any other kernel version, though. Problem here is that we already have a value smaller than 0x40 among the lot.

By the way: having an address beyond 0x40 could reasonably be enforced by having the byte be a member of a struct that is page-aligned.

Now the part that would in any case destroy this scenario is the movsx (sign extend). It overwrites what could have been the address of the CCHAR unconditionally, thereby creating a bogus address that should for all practical purposes cause a BSOD.

Anyway, the mechanism behind this may have been meant to be something like with the resources IDs which can be 16-bit unsigned integers or pointers to zero-terminated strings with the resource name.

3
  • Thanks. Can't upvote it yet because of the reputation. One question that comes to mind, wouldn't movsx sign extend 0x80 into '0xFFFFFF80`? Which would not be a "good" kernel address either.
    – MikeF
    Commented Jun 18, 2018 at 9:23
  • That's the point. The movsx seems to be the actual cause why this code looks like it always generates a BSOD. Would be interesting to look at this mechanism in a debugger on Windows 2000 and 2003 and compare.
    – 0xC0000022L
    Commented Jun 18, 2018 at 9:25
  • Yeah. The oldest OS I can go is WinXP in a VM. Although, this is not an old driver. I'm getting it now from the Sysinternals page when I download DbgView. Link in my OP.
    – MikeF
    Commented Jun 18, 2018 at 9:27
5

By definition, 32 bit systems cannot have more than 64 CPU cores (either virtual or physical). This is a hard limit on Microsoft's side for reasons unknown. One can assume those limitations are related to implementation details of the kernel such as the number of CPU core structures, etc.

Since this is a 32bit driver it'll only be allowed to run in a 32 bit windows version. Because of the limitation induced by Microsoft, drivers need not support more than 64 cores themselves, allowing / requiring a driver to handle these error conditions and unexpected situations to be using a BSOD.

If KeNumberProcessors global variable is larger than or equal to 64 CPU cores (or 40h) it will execute mov eax, [eax] instruction, which will try to read a DWORD from an address that is a number of CPUs, say 40h, which makes no sense.

Although your description is roughly accurate, there's another minor detail that makes this flow impossible you've neglected to note. Without it, this flow could've been functional.

That is the single assembly instruction just between the comparison and conditional jump: movsx eax, al. A move with sign extend from a al to eax. It makes sure EAX will not be a valid address to read from.

0
0

Yep seems like there'll be BSOD, but everybody is lucky to have KeNumberProcessors <= 0x40, so it never happens.

1
  • Thanks. Can't upvote it yet :( So I guess by definition if a system has more than 64 cores, they will be separated into groups, correct? KeNumberProcessors though seems like an old global variable that was used to determine the number of cores (now KeQueryActiveProcessors() function is recommended instead.) But would KeNumberProcessors cut off counting at 64 or is it a bug in that driver? Also I'm trying to twist my head into a pretzel trying to understand what C construct could've produced such an asm sequence?
    – MikeF
    Commented Jun 18, 2018 at 8:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.