OSRLogo
OSRLogoOSRLogoOSRLogo x OSR Custom Development Services
OSRLogo
x

Everything Windows Driver Development

x
x
x
GoToHomePage xLoginx
 
 

    Thu, 14 Mar 2019     118020 members

   Login
   Join


 
 
Contents
  Online Dump Analyzer
OSR Dev Blog
The NT Insider
The Basics
File Systems
Downloads
ListServer / Forum
  Express Links
  · The NT Insider Digital Edition - May-June 2016 Now Available!
  · Windows 8.1 Update: VS Express Now Supported
  · HCK Client install on Windows N versions
  · There's a WDFSTRING?
  · When CAN You Call WdfIoQueueP...ously

In Denial - Debugging STATUS_ACCESS_DENIED

Being the good citizen that I am, I decided to put my driver through a round of DC2 testing. Much to my surprise, DC2 ran and exited fast. As a matter of fact, it ran a little too fast, so I decided to fire up IrpTracker to see what sort of hideous I/O it was sending to my driver. Much to my surprise I was met with an entirely empty display. The DC2 test resulted in zero IRPs being sent to my device, which is not exactly useful.

 

At this point I flipped on the native API tracking feature of IrpTracker, which resulted in a new mystery - every single ZwCreateFile call made by DC2 failed within the I/O manager with STATUS_ACCESS_DENIED.

 

The create operation's parameters looked reasonable enough, so I decided to duplicate one of the creates in a test application. This, of course, worked perfectly fine, so it was time to find the point of failure within the I/O Manager and figure out what was going on.

 

Security in Windows is extremely well documented. Therefore, I will not attempt to review or summarize that documentation (an afternoon with Google will be far more beneficial than a couple of wasted paragraphs here). The point of this article is to demonstrate how to extract security information from the target system via a debugger, not how security works in general.

 

SeAccessCheck

The mother of all security functions in Windows is SeAccessCheck, which is the routine within the O/S used to determine if a particular caller has a particular access to a particular object. It was a safe bet that this routine was involved in DC2's inability to open my device, so I tried starting there:

 

0: kd> bp nt!seaccesscheck

0: kd> g

 

If you try this you'll quickly find that SeAccessCheck is one busy function. It's called practically non-stop from all over the O/S to validate access to things like files and registry keys. Clearly, putting a breakpoint at this high of a level was going to be entirely futile.

 

Back Up A Little

I find WinDbg to be a bit fussy with conditional breakpoints, so conditionalizing the SeAccessCheck breakpoint was out of the question. Instead, I decided to try and find the path through the Object Manager that resulted in the call to SeAccessCheck for named device objects. I figured there couldn't possibly be that many direct device opens going on (or opens of files on devices with the FILE_DEVICE_SECURE_OPEN bit set), so I was hopeful that this would yield better results.

 

Based on what I knew from other articles such as "Meandering through the Object Manager" (The NT Insider, March-April 2005), I knew IopParseDevice was the best place to start my searching. While this routine is called fairly often, it's far more manageable than SeAccessCheck. After a few attempts, I finally broke in on the correct stack (i.e. DC2 opening my device), and was ready to begin debugging (See Figure 1).

 

1: kd> kb

Args to Child

00000000 ae4bfc20 00000040 nt!IopParseDevice

0006f1a8 00000000 00000001 nt!ObOpenObjectByName+0x13c

0020ff88 02000000 0006f1a8 nt!IopCreateFile+0x5d4

0020ff88 02000000 0006f1a8 nt!IoCreateFile+0x38

0020ff88 02000000 0006f1a8 nt!NtCreateFile+0x31

0020ff88 02000000 0006f1a8 nt!KiFastCallEntry+0x12c

01012438 0020ff88 02000000 ntdll!KiFastSystemCallRet

0020ff88 02000000 0006f1a8 ntdll!ZwCreateFile+0xc

0006f1a8 0020ffec 77f2809e dc2+0x12438

0006f1a8 0020e543 00000000 kernel32!BaseThreadInitThunk+0xe

010123b0 0006f1a8 00000000 ntdll!_RtlUserThreadStart+0x23

 

1: kd> dt nt!_object_attributes 0006f1a8

 +0x000 Length        : 0x18

 +0x004 RootDirectory : (null)

 +0x008 ObjectName    : 0x0006f1c0 _UNICODE_STRING "\device\00000036"

 +0x00c Attributes    : 0x40

 +0x010 SecurityDescriptor : (null)

 +0x014 SecurityQualityOfService : (null)

Figure 1 - Getting Started

 

Back to SeAccessCheck

At this point I had two choices:

1. Begin stepping through the debugger until the magic status 0xC0000022 (STATUS_ACCESS_DENIED) showed up in the disassembly.

2. Reenable my SeAccessCheck breakpoint, hit GO, and hope I get lucky.

 

Since I try to avoid long, drawn out sessions of staring at assembly in the debugger, I decided to go with option two:

 

1: kd> bp nt!seaccesscheck

1: kd> g

Breakpoint 1 hit

nt!SeAccessCheck:

814a2320 8bff    mov    edi,edi

 

I immediately hit my breakpoint, and was pleased to see that it was indeed the correct call (See Figure 2).

 

1: kd> kb

Args to Child

88289ee0 844967d4 00000001 nt!SeAccessCheck

828208b0 00000000 844967b8 nt!IopParseDevice+0x63d

00000000 ae4bfc20 00000040 nt!ObpLookupObjectName+0x609

0006f1a8 00000000 00000001 nt!ObOpenObjectByName+0x13c

0020ff88 02000000 0006f1a8 nt!IopCreateFile+0x5d4

0020ff88 02000000 0006f1a8 nt!IoCreateFile+0x38

0020ff88 02000000 0006f1a8 nt!NtCreateFile+0x31

0020ff88 02000000 0006f1a8 nt!KiFastCallEntry+0x12c

01012438 0020ff88 02000000 ntdll!KiFastSystemCallRet

0020ff88 02000000 0006f1a8 ntdll!ZwCreateFile+0xc

0006f1a8 0020ffec 77f2809e dc2+0x12438

0006f1a8 0020e543 00000000 kernel32!BaseThreadInitThunk+0xe

010123b0 0006f1a8 00000000 ntdll!_RtlUserThreadStart+0x23

Figure 2 - Now We're Getting Somewhere

 

Moment of Truth

Next came the exciting part, "Had I guessed correctly and was my problem buried somewhere within SeAccessCheck?" Before going further, it was time to refresh my memory of the the SeAccessCheck prototype. As luck would have it, this process is fully documented (See Figure 3).

 

BOOLEAN

SeAccessCheck(

    IN PSECURITY_DESCRIPTOR SecurityDescriptor,

    IN PSECURITY_SUBJECT_CONTEXT SubjectSecurityContext,

    IN BOOLEAN SubjectContextLocked,

    IN ACCESS_MASK DesiredAccess,

    IN ACCESS_MASK PreviouslyGrantedAccess,

    OUT PPRIVILEGE_SET *Privileges OPTIONAL,

    IN PGENERIC_MAPPING GenericMapping,

    IN KPROCESSOR_MODE AccessMode,

    OUT PACCESS_MASK GrantedAccess,

    OUT PNTSTATUS AccessStatus);

 

Figure 3 - SeAccessCheck Prototype

 

According to the documentation, this routine will return TRUE if the caller described by the SubjectSecurityContext is allowed access to the object described by the SecurityDescriptor (more info on these parameters later), and FALSE otherwise. The target machine was an x86 machine, so I stepped out of the call and checked EAX for the result:

 

1: kd> bp /1 /c @$csp @$ra;g

Breakpoint 2 hit

nt!IopParseDevice+0x63d:

81565557 3245e3    xor    al,[ebp-0x1d]

1: kd> r @eax

eax=00000000

 

Aha! This revealed that SeAccessCheck was indeed failing. Now it was a matter of figuring out why.

 

Three Easy Pieces

I needed to extract the following three pieces of information from the debugger in order to determine why this access check failed:

 

1. The desired access of the caller

2. The Discretionary Access Control List (DACL) of the object

3. The security token of the caller

 

Luckily the first one was a gimme since DesiredAccess is the second parameter to ZwCreateFile. In my case this meant the caller wanted 0x02000000, which is MAXIMUM_ ALLOWED. Now I needed to determine who had what access to the object, so it was time to move on to the DACL for the target device.

 

Displaying the Device's DACL

The first parameter to the SeAccessCheck call is the SECURITY_DESCRIPTOR of the object being checked. For lack of a better term, a SECURITY_DESCRIPTOR, well, describes the security of an object. Part of this security description is the DACL of the device, which is what I was looking for.

 

Luckily, there is already a !sd command that nicely parses one of these structures, so the only thing I had to do was find the address of the SECURITY_DESCRIPTOR. Since I knew this was the first parameter that was passed to SeAccessCheck, I used the information from the earlier stack dump I had (See Figure 4).

 

Args to Child

88289ee0 844967d4 00000001 nt!SeAccessCheck

 

1: kd> !sd 88289ee0

...

->Owner : S-1-5-32-544

->Group : S-1-5-18

...

->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE

->Dacl : ->Ace[0]: ->AceFlags: 0x0

->Dacl : ->Ace[0]: ->AceSize: 0x18

->Dacl : ->Ace[0]: ->Mask : 0x001f01ff

->Dacl : ->Ace[0]: ->SID: S-1-5-32-544

 

->Dacl : ->Ace[1]: ->AceType: ACCESS_ALLOWED_ACE_TYPE

->Dacl : ->Ace[1]: ->AceFlags: 0x0

->Dacl : ->Ace[1]: ->AceSize: 0x14

->Dacl : ->Ace[1]: ->Mask : 0x001f01ff

->Dacl : ->Ace[1]: ->SID: S-1-5-18

 

->Dacl : ->Ace[2]: ->AceType: ACCESS_ALLOWED_ACE_TYPE

->Dacl : ->Ace[2]: ->AceFlags: 0x0

->Dacl : ->Ace[2]: ->AceSize: 0x14

->Dacl : ->Ace[2]: ->Mask : 0x001f01ff

->Dacl : ->Ace[2]: ->SID: S-1-5-20

...

Figure 4 - Finding the Address of the SECURITY_DESCRIPTOR

 

According to the DACL contained within this security descriptor, three principals have access to this object:

 

1. BUILTIN\ADMINISTRATORS (S-1-5-32-544) have FILE_ALL_ACCESS (0x001f01ff)

2. NT AUTHORITY\SYSTEM (S-1-5-18) has FILE_ALL_ACCESS (0x001f01ff)

3. NT AUTHORITY\NETWORK SERVICE has FILE_ALL_ACCESS (0x001f01ff)

 

Luckily, this was exactly what I expected. This driver's INF file had locked down its named device objects by using this SDDL string:

 

D:P(A;;GA;;;BA)(A;;GA;;;SY)(A;;GA;;;NS)

 

If you refer to the SDDL documentation in the SDK you will see that this string exactly describes the access applied to the object as seen in the debugger.

 

Other Ways to Get the DACL

There are at least two other ways of finding and displaying the DACL of an object in the debugger:

 

1. If the object is a device object, !devobj will display the address of the DACL, if there is one. !acl can then be used to display the DACL in the debugger. Note that DACLs are only applied to the named device objects in the stack.

2. You can find the SECURITY_DESCRIPTOR of an object by using !object, then examine the OBJECT_HEADER. See Determining the ACL of an Object in the WinDBG documentation for detailed instructions.

 

Getting the Token

The last piece of this puzzle was the token that was being used by the caller. By comparing the information in the caller's token with the information in the DACL of the device object, I was able to determine why the Security Reference Monitor (SRM) was denying the open.

 

Much like everything else in WinDBG, there are a few ways at getting this info. However, to get the clearest possible picture of what was going on, I decided to look at the SECURITY_ SUBJECT_CONTEXT passed by IopParseDevice to SeAccessCheck. Since this was the second parameter to the function, I again used the saved stack information (See Figure 5).

 

1: kd> kb

Args to Child

88289ee0 844967d4 00000001 nt!SeAccessCheck

 

1: kd> dt nt!_security_subject_context 844967d4

nt!_SECURITY_SUBJECT_CONTEXT

 +0x000 ClientToken    : 0xb3489af0

 +0x004 ImpersonationLevel : 3 ( SecurityDelegation )

 +0x008 PrimaryToken   : 0xaab283b0

 +0x00c ProcessAuditId : 0x000002cc

Figure 5 - Checking Out SECURITY_SUBJECT_CONTEXT

 

This was actually a fairly interesting SECURITY_SUBJECT_CONTEXT. The fact that ClientToken was non-NULL meant the thread making this call was impersonating, which means it was not running with the original token it inherited from its parent process.

 

Even though it was not strictly necessary for the original analysis, listed below is the thread's original token (i.e. PrimaryToken) that was shown by the debugger's !token command (See Figure 6).

 

1: kd> !token 0xaab283b0

_TOKEN aab283b0

TS Session ID: 0x3

User: S-1-5-21-1481837369-2907903028-1708812341-500

Groups:

00 S-1-5-21-1481837369-2907903028-1708812341-513

   Attributes - Mandatory Default Enabled

01 S-1-1-0

   Attributes - Mandatory Default Enabled

02 S-1-5-32-544

   Attributes - Mandatory Default Enabled Owner

03 S-1-5-32-545

   Attributes - Mandatory Default Enabled

04 S-1-5-4

   Attributes - Mandatory Default Enabled

05 S-1-5-11

   Attributes - Mandatory Default Enabled

06 S-1-5-15

   Attributes - Mandatory Default Enabled

07 S-1-5-5-0-35361231

   Attributes - Mandatory Default Enabled LogonId

08 S-1-2-0

   Attributes - Mandatory Default Enabled

09 S-1-5-64-10

   Attributes - Mandatory Default Enabled

10 S-1-16-12288

   Attributes - GroupIntegrity GroupIntegrityEnabled

11 S-1-16-12288

   Attributes - GroupIntegrity GroupIntegrityEnabledDesktop

Primary Group: S-1-5-21-1481837369-2907903028-1708812341-513

Privs:

00 0x00000001e Unknown Privilege     Attributes - Enabled Default

01 0x000000017 SeChangeNotifyPrivilege Attributes - Enabled Default

...

09 0x000000014 SeDebugPrivilege      Attributes - Enabled

...

19 0x00000001d SeImpersonatePrivilege  Attributes - Enabled Default

...

22 0x000000021 SeIncreaseWorkingSetPrivilege Attributes - Authentication ID: (0,21b91ea)

Impersonation Level:  Anonymous

TokenType:  Primary

Source: User32    TokenFlags: 0x81 ( Token in use )

Token ID: 21d530a    ParentToken ID: 0

Modified ID: (0, 21d53be)

RestrictedSidCount: 0    RestrictedSids: 00000000

Figure 6 - The Thread's Primary Token

 

As you can see, the owner of this token was a member of the Administrators group (Group 02). This means if this thread had not been impersonating, it would have been granted FILE_ALL_ACCESS to the device object. Let's check out the impersonation token to see what was happening (See Figure 7).

 

1: kd> !token 0xb3489af0

_TOKEN b3489af0

TS Session ID: 0x3

User: S-1-5-21-1481837369-2907903028-1708812341-500

Groups:

00 S-1-5-21-1481837369-2907903028-1708812341-513

    Attributes - Mandatory Default Enabled

01 S-1-1-0

    Attributes - Mandatory Default Enabled

02 S-1-5-32-544

   Attributes - Mandatory Default Enabled Owner

03 S-1-5-32-545

   Attributes - Mandatory Default Enabled

04 S-1-5-4

   Attributes - Mandatory Default Enabled

05 S-1-5-11

   Attributes - Mandatory Default Enabled

06 S-1-5-15

   Attributes - Mandatory Default Enabled

07 S-1-5-5-0-35361231

   Attributes - Mandatory Default Enabled LogonId

08 S-1-2-0

   Attributes - Mandatory Default Enabled

09 S-1-5-64-10

   Attributes - Mandatory Default Enabled

10 S-1-16-12288

   Attributes - GroupIntegrity GroupIntegrityEnabled

11 S-1-16-12288

   Attributes - GroupIntegrity GroupIntegrityEnabledDesktop

Primary Group: S-1-5-21-1481837369-2907903028-1708812341-513

Privs:

00 0x000000017 SeChangeNotifyPrivilege   Attributes - Enabled Default Authentication ID:  (0,21b91ea)

Impersonation Level:  Delegation

TokenType:    Impersonation

Source: User32     TokenFlags: 0x11

Token ID: 21d53a4  ParentToken ID: 21d530a

Modified ID:    (0, 21d53a1)

RestrictedSidCount: 1    RestrictedSids: b3489e3c

OriginatingLogonSession: 3e7

Figure 7 - How About the Impersonation Token

 

At first blush, the reason why this token was rejected isn't clear. The only noticeable difference between this and the previous token is that many of the privileges had been removed, but that should not affect this thread's ability to open an object since it is still a member of the Administrators group. So what's the deal?

 

Check the Restricted SIDs

Down near the bottom of the !token output is where the real heart of the problem lies: this token has a restricted SID. We'll define what that is in a moment, but first let's check it out and see what's in it:

 

1: kd> dd b3489e3c l1

b3489e3c b3489f14

1: kd> !sid @$p

SID is: S-1-1-0

 

In case you don't have all of the well-known SIDs memorized, this one happens to be the WORLD, or EVERYONE, SID.

 

When a token has a restricted SID in it, the caller is granted access only if both the "normal" SIDs and the restricted SIDs are given access to the object. For example, if the object was modified to give WORLD read access, this caller would be allowed to open the object for read access. However, since WORLD did not appear in any Allow ACEs in the DACL, the restricted SIDs check failed and the caller was denied access. Pseudo code for how this is achieved on a very, very high level would be something akin to Figure 8.

 

accessToCheck = DesiredAccess;

forall user and group SIDs {

       compute granted access;

}

if restricted SIDs {

accessToCheck = grantedAccess;

grantedAccess = 0;

       forall restricted SIDs {

           compute granted access;

       }

}

if granted access == 0 {

       return FALSE;

}

return TRUE;

 

 

Figure 8 - Pseudo-code for Checking SID Access

 

Another Way to Get the Token

If you want to check the token of a random thread or process, the token is available from the !process command. The default/primary token for the process is listed in the process information and each impersonating thread will have its impersonation token listed. Threads that are not impersonating are listed as such. See the example from a different instance of DC2 in Figure 9.

 

0: kd> !process 843b9020
PROCESS 843b9020
 ...
    Image: dc2.exe
    ...
    Token b1a9c530
    ...
        THREAD 8445da78 
        ...
        Impersonation token: aadae030 (Level Delegation)
        ...

        THREAD 84391cb8
        ...
        Not impersonating


Figure 9 - Another Means to Get A Token

 

The DC2 Solution

Now that I knew what I was dealing with, I quickly found the -im switch to DC2 that turns this behavior off. I suppose if I had paid closer attention to the documentation in the first place I could have saved myself some time, but what fun would that have been?

Be sure to check out the latest version of OSR's object viewer utility,OSR's object viewer utility, ObjDir v1.4. The security information dialog has been fully revamped to provide more insight into, and a finer control over, the security applied to objects in the namespace.

Related Articles
Enabling Debugging on the Local Machine for Windows XP®
You're Testing Me - Testing WDM/Win2K Drivers
Analyze This - Analyzing a Crash Dump
More on Kernel Debugging - KMODE_EXCEPTION_NOT_HANDLED
Making WinDbg Your Friend - Creating Debugger Extensions
Life Support for WinDbg - New Windows NT Support Tools
Life After Death? - Understanding Blue Screens
Special Win2K PnP Tracing and Checks
All About Lint - PC Lint and Windows Drivers
Bagging Bugs — Avoidance and Detection Tips to Consider

User Comments
Rate this article and give us feedback. Do you find anything missing? Share your opinion with the community!
Post Your Comment

Post Your Comments.
Print this article.
Email this article.
bottom nav links