Original link: https://astralvx.com/index.php/2019/07/30/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.
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)
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:
Direct negative ace
Direct positive ace
Inherited negative ace
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 most ACL API’s merely append to the end, so be aware.
ACCESS_MASK
ACCESS_MASK 32 bits – contain combinations of Generic, Standard, and Specific permissions
GENERIC_MAPPING in the access mask is a (layer) that simply overrides other types and grants permissions
GENERIC_READ for files grant all read SPECIFIC permissions: READ_CONTROL, SYNCHRONIZE, FILE_READ_DATA, FILE_READ_ATTRIBUTES, FILE_READ_EA
GENERIC_READ for registry keys grant all read permissions: READ_CONTROL, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY
SID
SID 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. And for SID S-1-5-15-544.
Revision is 1
IdentityAuthority is 5
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 or 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. But if you’re in the FileSystem I/O path, on Pre/Post Create/Access 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
A bit difficult to explain, so here is an example which catches the idea.
To grant FILE_EXECUTE (0x20) to a user via ACL inheritance when files created in a dir, apply OBJECT_INHERIT (0x1) to the parent dir.
But this will only be inherited to child FILE object types, not to DIRECTORY objects, otherwise child dir would get FILE_TRAVERSE (0x20)
However applying 0x20 causes the parent dir to have FILE_TRAVERSE (0x20) on itself, to resolve this OR the 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.
Explicit permissions applied to an object override inheritance.
e.g. We want to deny Bob from all DELETE permissions in all folder/subfolders, but allow Bob to DELETE in 1 specific deep subfolder. So set INHERIT DELETE DENY Bob on the root folder, but explicitly ALLOW BOB DELETE 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