Access checks from the kernel

Recently I’ve had to do some access checks on FILE_OBJECTs from a kernel driver (but not from the perspective of the current user). I couldn’t just pass the data to a usermode component as it would introduce delays in the I/O path especially for network files and I’m not operating against current user per-se. It just required time for research from reading/searching through windows-driver-samples which contains a small set of publicly available kernel source code to reverse engineering Nt/Se* related APIs, and coming to the conclusion that Windows security is very complex.

Acronyms

  • DACL (Discretionary Access Control List) – contains ACEs that allow/deny access to the securable object
  • ACE (Access Control Entry) – contains a set of access rights and a SID that identifies a trustee for whom the rights are allowed, denied, or audited
  • SID (Security IDentifier) – a unique value of variable length used to identify a trustee (user, group, computer accounts etc)

Access check overview

An overview of a FILE_OBJECT access check can be seen below in Fig 1, of which I obtained the logic from the book Windows Internals, a driver development book targeting Win 2k, and Nt debugging.

Fig 1 – Access check algorithm

1. Mandatory integrity check

Is the first pass efficient way to avoid the full access check. It determines access to a resource based on the principal (source) and securable object (target) integrity. A principal could be a process (integrity stored in the process token), and the object could be a file (integrity stored in the SACL). A principal with a Low Integrity level can’t write to an object with a Medium Integrity level, even if that object’s DACL allows write access to the principal. “Objects that lack an integrity label are treated as medium by the operating system; this prevents low-integrity code from modifying unlabeled objects” – MSDN

  • Process HI – Can R/W to H/M/L object
  • Process MI – Can R/W to M and L object
  • Process LI – Can R/W to LI object, and R M object

2. Loop through all ACEs in a DACL

You can use RtlGetAce() on a valid ACL and traverse through each ACE index. Since all ACE variants (ACCESS_ALLOWED_ACE, ACCESS_DENIED_ACE, SYSTEM_MANDATORY_LABEL_ACE, etc), all have a common header, you can cast to any of them and inspect ACE_HEADER.AceType to determine it’s type.

typedef struct _ACCESS_ALLOWED_ACE {
    ACE_HEADER Header;
    ACCESS_MASK Mask;
    ULONG SidStart;
} ACCESS_ALLOWED_ACE;

3. SID check in an ACE

Custom stuff to extract SIDs of user/group memberships and use of RtlEqualSid(). Since there are no kernel APIs to obtain a random user’s group membership SID, I R/E’d the Windows access checks and found “\Registry\Machine\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\<sid>\GroupMembership” which contains all user SIDs and inside the subkey you find a bunch of SID like objects, and through correlation of “whoami /all” they are all the group SIDs the user belongs to. One can use SecLookupAccountName() to convert a domain/user string to the SID value.

4. SID ACCESS_MASK checks

Bitmask operations. Files/directories/pipes have different meanings for different flags, but the underlying concept is the same, i.e.:

  • 0x0001 = FILE_READ_DATA (file and pipe) = FILE_LIST_DIRECTORY (dir)
  • 0x0002 = FILE_WRITE_DATA (file and pipe) = FILE_ADD_FILE (dir)
  • 0x0004 = FILE_APPEND_DATA (file) = FILE_CREATE_PIPE_INSTANCE (pipe) = FILE_ADD_SUBDIRECTORY(dir)

5. Check the ACE allow/deny type

Where ALLOWED* and DENIED* indicate variants such as ACCESS_ALLOWED_ACE_TYPE, ACCESS_ALLOWED_CALLBACK_ACE_TYPE, ACCESS_ALLOWED_COMPOUND_ACE_TYPE, etc. And if you notice in Fig 1, any explicit DENY on a user or group SID you’re part of, you’re denied flat out, as the kernel operates on ACEs in order found. So even if you’re part of 10 groups and 9 have direct positive ACEs, if even 1 has a direct negative ACE, you’re denied entirely (where direct means the member SID is actually in the ACE explicitly, and not inherited from a parent). ACE priority:

  1. Direct negative ace
  2. Direct positive ace
  3. Inherited negative ace
  4. Inherited positive ace

6. Check remaining access rights required

There are 3 states of an access when checking.

  • DesiredAccess – is what the user requests i.e. R|W|X
  • GrantedAccess – is what the ACEs gives him (additively) based on groups i.e. R|X|DEL
  • RemainingAccess – is what remains to be given i.e. W

And as seen in Fig 1, if any access remains after checking all the ACEs, you get denied regardless as there is no concept of partial privileges. Also if you’re granted all you’re accesses, and there are still ACE’s to be processed, you bail out early and get accepted.

Notice a potential security issue here, if someone (misconfigures) explicitly denies at the end of a set of ACEs, but the user get all his accesses before reaching the deny, he is granted access to the object. So any deny ACE should be done at the start of the ACL, but a lot of Win32 ACL API’s merely append to the end, hence why you’re supposed to rebuild it on such operations.

Access mask

ACCESS_MASK 32 bits – contain combinations of Generic, Standard, and Specific permissions

GENERIC (4) MISC (4) STANDARD (8) SPECIFIC (16)

GENERIC_* is a #define that acts as a generic layer to grant permissions to any type e.g.

  • GENERIC_READ for type files grant all read SPECIFIC permissions: READ_CONTROL, SYNCHRONIZE, FILE_READ_DATA, FILE_READ_ATTRIBUTES, FILE_READ_EA
  • GENERIC_READ for type registry keys grant all read permissions: READ_CONTROL, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY

SID

A SID (e.g. S-1-5-15-544) formats are S-R-I-S1-S2-S3-Sn, where R is the Revision, I is the IdentityAuthority, and S1 to Sn are the SubAuthorities.

The IdentityAuthority can be in either denary or hex formats i.e. “S-1-5-15” vs. “S-1-0x0100080000ff-15”. Where the IdentifierAuthority in format {0,0,0,0,0,5} == SECURITY_NT_AUTHORITY, and 0x0100080000ff is a custom IdentifierAuthority.

The SubAuthority (commonly known as RID (Relative IDentifier values)), are the authorities that issue the SID, and are an array inside the SID. In the above case the RID is 15. Sample SID explained S-1-5-15-544:

  • Revision is 1 (standard SID version)
  • IdentityAuthority is 5 (SECURITY_NT_AUTHORITY)
  • RID[0] is 15 (SECURITY_THIS_ORGANIZATION_RID)
  • RID[1] is 544 (DOMAIN_ALIAS_RID_ADMINS)

Security Descriptor

A SECURITY_DESCRIPTOR (SD) contains security information about an object, it contains the owner of the object and the DACL. SD’s come in 2 flavours as found through research:

  • As pointers to ACL data
  • Contiguously after the SD itself

The 2nd variant is not well documented, but the majority of SD operations you do using Win32 API typically use the pointer variant as that’s typically how it’s laid out in memory.

If you’re in the FileSystem I/O path, on Pre/Post Create/Access callback events, the data coming from the disk itself are in this relative format as that’s how the ACL is stored on disk, contiguously after the SD.

During the creation of this custom struct, I eventually came across PISECURITY_DESCRIPTOR_RELATIVE in ntifs.h which is exactly what we are looking for. Again not documented, but found through Ida/WinDbg, one can check the type of SECURITY_DESCRIPTOR (pointer or relative), by examining the common header in both SD->Control and checking bit 0x8000, which I eventually found in ntifs.h as SE_SELF_RELATIVE (0x8000)

Inheritance

Is how child objects inherit ACL permissions from parents, from files in folders inheriting folder ACL permissions, to threads created inheriting there parent process tokens/ACLs. The model is powerful but also introduces a lot complexity and issues like different object types receiving the same value grant permissions but behaving different. Here is an example which catches the idea.

We want a user to be able to execute files in a dir and all subfolders. So in that parent dir we apply OBJECT_INHERIT (0x1) on itself, and you would think to grant the parent dir FILE_EXECUTE (0x20), so all files can inherit that option.

  • So FILE_EXECUTE will only be inherited to child FILE object types, not to DIRECTORY objects, otherwise child dir would get FILE_TRAVERSE (0x20)
  • But applying FILE_TRAVERSE (0x20) to the parent is interpreted as the parent dir having FILE_TRAVERSE (0x20) permissions. Meaning the user is able to traverse that entire directory, but the goal was to grant execute permissions only.
  • To resolve this the parent ACL has to OR the FILE_TRAVERSE flag with INHERIT_ONLY_ACE (0x8) so: Grant FILE_EXECUTE 0x20 to user, with inheritance (0x1 | 0X8). Means only the child objects get 0x20 as it’s inheritable only. …. (todo, better explain)

Explicit permissions applied to an object override inheritance.

  • Bob is an intern in the finance department, the root folder contains all sorts of confidential data, but we need to give him access to a specific subfolder to do work.
  • We want to deny Bob from all WRITE permissions in all the root folder and subfolders, but allow Bob to WRITE in 1 specific deep subfolder. So set ACL INHERIT WRITE DENY Bob on the root folder, but explicitly ALLOW BOB WRITE on the specific subfolder.

WinDbg

Useful WinDbg commands for this type of work:

!error @@(status)
!acl @@(pAcl)
!sid @@(pSid) 
!sd @@(piSD)
!list

Warning – do not do bitmask operations with “?” it messes up unsigned/signed bit operations. Use “??” and cast result to to (unsigned int) to see the hex.

Prepare for BSODs when I/O comes at you at IRQL DISPATCH_LEVEL