Deep dive into the Object creation flow in Windows - PART 2
Access check internals.
Caution:
Before I start, it's necessary to say that I am by no means a professional. This work is based solely on static analysis using IDA freeware 9.2 and my own knowledge. So, if you found any errors or gaps, please leave me a comment.
Summary:
This is the second part of my reverse engineering research focused on the internals of two Windows kernel executive managers, the Object Manager which is responsible for managing and tracking different aspects of objects; and the Security reference Monitor which is mainly responsible for the access check phase.In this part I will detail the access check phase covering the creation of the access state and its subject context, generic and standard rights, privileges, mandatory integrity control and finally normal vs maximum access check and as the previous part, all involved routines are mentioned.
Before you read this part it's better to read the previous one first.
Introduction:
Let's start with required definitions of some concepts I will be using in this article.
Security Reference Monitor: a Windows kernel executive manager responsible for controlling access to objects to ensure that only allowed callers can access them in the authorized way. It controls different aspects of the access check phase including but not limited to the creation and setup of the access state and its subject context used to represent the real-time state of an access check operation, the privilege check, the mandatory integrity control (A.K.A MIC), the Dacl access control which is divided into two different mechanisms, the normal and the maximum one and so on. The Security Reference Monitor is integrated into the operating as a set of routines having names that start with Se (short for Security) or Sep (short for Security Private).
Access Token: a Windows executive Object managed by the Security Reference Monitor. It is used to represent the security capabilities of a known user registered in the system. the kernel defines access tokens as instances of the nt!_TOKEN structure (in some documentations you may see that the structure is nt!_ACCESS_TOKEN but this one is defined as LPVOID by IDA so instead it's better to use nt!_TOKEN to easily identify its members). Access Tokens hold the user security identifier and all SIDs of groups it's a member on, default security attributes the system can assign to Objects created by callers having the access token such as the Owner, the Primary Group SID and the default Dacl, a set of privileges held by the user. This security object can be in a restricted state which contains less privileges and more SIDs called restricted SIDs, deny only SIDs which are used solely to deny access. Access Tokens can be either primary or impersonation, primary ones are assigned to processes, the other type is assigned to threads to temporarily use the identity of another user (the impersonation concept will be covered in detail in this part).
Privilege: a special type of rights that can be assigned to a user, they are not used to protect individual objects but instead the system uses them to give certain global rights to some user. The most famous right that is only grantable through a privilege is the ability to access the system access control list (A.K.A Sacl). The right to manipulate processes of higher integrity levels is also grantable through a very powerful privilege called the nt!SeDebugPrivilege. Users can change the owner of a given object if they hold the nt!SeTakeOwnership privilege, and so on. In the next sections you will see many examples related to the privilege check phase so don't worry if you didn't get it yet.
Access Control List: a special list held by the security descriptor of an Object which stores access control entries that control security related aspects. The Windows operating system supports two different types of access control lists. The first one is the Discretionary access control list (A.K.A Dacl) which stores entries controlling access to the Object by listing allowed and denied rights and to whom they are applied. The next type is called a system access control list (A.K.A Sacl) which stores entries controlling access auditing by listing which rights must be audited and under what conditions (success, failure or both), this list also stores a special control entry used for defining the object's integrity control level and policy which is used by the Security Reference Monitor to determine how to deal with requests coming from lower integrity level callers. The system defines access control lists using a data structure called nt!_ACL which represents only the header part, it is then followed by zero or more access control entries.
Access Control Entry: a special data structure used by the system to determine which rights are allowed or denied to which trustee (a security entity defined by the security sub-system) or to decide which access requests must be audited and under which conditions (success, failure or both). This data structure is divided into the header part defined by the system as nt!_ACE_HEADER and the body part that depends on the type of the entry, their types are well documented in the official MSDN documentation so I will not detail every single type.
High level Overview:
Before we go deeper, I prefer to present a high-level description of the object creation steps.
First, it's necessary to clarify that the system doesn't provide a single generalized routine to create an object, the creation routine depends on the object type, as each type has its own one. For example, to create an event object almost all developers use kernelbase!CreateEventW( ) (or its ANSI counterpart kernelbase!CreateEventA( )) which is just a wrapper that calls ntdll!NtCreateEvent( ) which transitions the execution to the kernel through a system call. To create an Object of a different type you can't use the same routine, it's mandatory to use that type's creation routine as defined by the system.
Despite it seems that the creation flow cannot be generalized from a user mode perspective, the operating system kernel defines a common flow used to create any type of object which I will divide into the following 6 major steps:
1- Object allocation: Objects are simply memory blocks allocated either from the kernel paged or non-paged pool as indicated by the type they belong to. The main Object Manager routine involved in this step is nt!ObpAllocateObject( ) that verifies some known conditions to determine the presence of each optional header, then it calculates the required buffer size that can hold the following 3 parts respectively: the present optional headers, the main object header and finally the object body.
2- Object initialization: after allocating a buffer large enough to hold the Object, the system passes to the initialization step which is divided into 2 major phases. The first one is the specific initialization that consists of initializing the object body using the corresponding Object Type's initialization routine. For example to initialize an event object the system uses nt!KeInitializeEvent( ). The next initialization phase is done by the Object Manager, and it consists of constructing then assigning a security descriptor to the new object.
3- Access check: after initializing the object and its security descriptor, a new kernel executive manager is involved in this step which is the Security Reference Monitor. In this step the system will check if the caller has the right to access the Object in the way he wishes. The Security Reference Monitor compares the caller's access token with the security descriptor associated with the Object.
4- Optional Headers setup: In this step the system fills in different optional headers associated with the Object each one serving a different purpose. It charges the required pool quotas and security descriptor's quota for the calling process, initialize the object for exclusive access if it is requested, inserts a handle count entry in the handle count database associated with the object to track the calling process handle then finally if this is a new Object it links it to the list of objects belonging to the same type.
5- Object name lookup: Objects can have a unique path name associated with them. This path must follow a strict convention defined by the operating system, otherwise it's treated as invalid. The name lookup involves parsing each part of the path and check whether it's a valid child path to its predecessor. This parsing is done either manually or via the help a special ParseProcedure( ) associated with the Object Type to which the Object belongs. This complex part will be described in detail in the next parts.
6- Object Handle: after initializing all parts of the Object and doing all the necessary checks, the system creates a handle entry and links it to that Object, it also ensures that this entry is only valid in the calling process context (this is achieved through separate handle tables) and that it grants only the requested access rights.
NOTE: In this part, only the third step is covered in detail, the rest will be covered in the next parts.
Access state:
Before starting the access check phase, it's necessary to know what inputs are needed to perform this operation. In the previous part I covered in detail the creation flow of the security descriptor that defines the security boundaries for an Object. As you might be guessing, the Security descriptor is a required input that the Security Reference Monitor needs before performing the access check. The next parameter is the caller's desired access which is used to represent the initial rights the caller wants, and it's also used by the Security Reference Monitor to track the remaining rights during the access check. As it tracks remaining rights, the system also tracks granted rights, so it's necessary to preserve them during the operation. In the next few sections, you will see that some rights are granted to the caller because its access tokens hold certain privileges; those privileges are stored in a nt!_PRIVILEGE_SET that needs to be filled in by the system during the access check phase. Finally, the most important information needed to perform the access check is the caller's access tokens that are stored in a data structure called nt!_SECURITY_SUBJECT_CONTEXT. All of those elements are packed in one single data structure called the access state, it is considered the heart of the whole process. The access state is defined by the kernel as nt!_ACCESS_STATE structure, you can see its full declaration in the following figure.
After describing the main components of the access state, it's time now to detail its creation flow. To create an access state the system calls a security routine called nt!SeCreateAccessStateEx( ). The first thing that this routine does is checking whether the desired access mask requested by the caller includes some generic rights then mapping them to specific rights. Before continuing, let's explain what generic rights are. Generic rights are placeholders divided into 4 categories the security subsystem provides. The first category is nt!GENERIC_READ which represents a combination of all rights that consist of extracting information from an Object. The next one is nt!GENERIC_WRITE which as the name suggests, represents a combination of all rights that consist of modifying the object. nt!GENERIC_EXECUTE is the next generic access right defined by the system, it represents a combination of all rights that consist of executing the object (execution can mean different things depending on the object type). The final one is nt!GENERIC_ALL that obviously represents all valid rights defined for that object type. You might be asking how the system knows which rights correspond to which generic right; the answer is through the Object Type. Object Types hold a special data structure defined as nt!_GENERIC_MAPPING that maps each one of the previously described generic rights to specific object rights, to convert the desired access mask from a generic one to a specific one the system calls nt!RtlMapGenericMask( ) which is a simple routine that uses the previously mentioned generic mapping structure.
After mapping generic rights to specific object ones, the next step is extracting the impersonation data from the current thread. nt!SeCreateAccessStateEx( ) firstly checks the current thread's cross flags (nt_ETHREAD::CrossThreadFlags.ActiveImpersonationInfo == 0x1) to determine whether the thread is currently impersonating another user or not. If it's not, no impersonation information is added to the access state. Otherwise, the thread's push lock is acquired for shared access using nt!ExfAcquirePushLockShared( ). If the thread's lock is acquired exclusively by another routine, nt!SeCreateAccessStateEx( ) will waits for it to be released. During this wait, the thread's impersonation state might change, so it's necessary to recheck the ActiveImpersonationInfo flag to determine whether the current thread is still impersonating another user or not. If the impersonation state doesn't change, extract the impersonation token and level from the nt!_ETHREAD::ClientSecurity.ImpersonationData of the current thread. This is a union that stores the impersonation level on the lowest 2 bits and the effective only modifier on the 3rd bit (it is ignored in this context) and the rest is for the token's address in memory. Finally, the extracted impersonation token's reference count is incremented using nt!ObfReferenceObject( ) then the system sets it as the nt!_SECURITY_SUBJECT_CONTEXT::ClientToken of the current subject context associated with the access state. The impersonation level is also set as the nt!_SECURITY_SUBJECT_CONTEXT::ImpersonationLevel of the same subject context.
The next step is setting the primary token of the Subject context. Before I continue, I must explain the concept of fast references because the primary token is linked to its process this way. Fast references are kernel unions declared as nt!_EX_FAST_REF, they are 8 bytes blocks holding the object reference count on the lowest 4 bits, and the object memory address on the rest. Since fast references reserve only 4 bits for the reference count, they can be used as long as it doesn't exceed 15. The next thing that is worth saying is that those unions can only be used for 16 bytes aligned objects; in other words objects having addresses which have their lower 4 bits unset (unused).
The system calls nt!ObFastReferenceObject( ) that tries to increment the reference count of the current process' token using the fast method described previously. If this method failed because the reference count exceeds 15, the normal method is used instead. Before the access token is referenced, the process to which it is assigned must be in a state where it's safe to do so. So, the system acquires the process' push lock, calls nt!ObReferenceObjectWithTag( ) to increment its reference count, then finally it releases the lock.
After successfully referencing the process' access token, it is assigned to the same subject context by setting it as the nt!_SECURITY_SUBJECT_CONTEXT::PrimaryToken member.
Next, nt!SeCreateAccessStateEx( ) sets nt!TOKEN_HAS_TRAVERSE_PRIVILEGE flag in the access state depending on whether the access token has the traverse privilege enabled or not (_TOKEN::Privileges.enabled & 0x800000 != 0x0). You might be asking since we have assigned two access tokens to the current subject context, which one is used for the check. The answer is that the security reference monitor always prioritizes the impersonation token over the primary one. The Primary Token is used only if there is no impersonation token (in the previous part I have mentioned that there is a case where the Primary token is used even if the impersonation one exists which is indicated by the presence of nt!SE_SERVER_SECURITY in the object security descriptor, but during the access check I didn't see any place where this flag is checked). The traverse privilege is used during the name lookup phase which is covered in the next parts.
Finally, the system initializes both nt!_ACCESS_STATE::RemainingDesiredAccess and nt!_ACCESS_STATE::OriginalDesiredAccess using the caller requested access mask (the difference between the two masks is that the second one remains constant but the first one changes whenever new rights gets granted), and nt!_ACCESS_STATE::PreviouslyGrantedAccess as 0 to indicate that no rights are granted yet.
Non-securable Objects:
Before detailing the access check step by step, let me briefly explain a special case you might be confused about it. In the previous part, the security descriptor construction and assignment were explained in detail. As I said, the system doesn't treat all Objects the same way in terms of security. Some objects are treated as non-securable, so the Object Manager doesn't assign a security descriptor to them. the question that anyone may ask is how the system deals with objects having a null security descriptor during the access check phase. The answer to this question is found in nt!ObCheckObjectAccess( ) and it is simple than you might be thinking.
This routine starts its work by getting the security descriptor associated with the Object using nt!ObpGetObjectSecurity( ). This last routine gets a special callback associated with the Object Type to which the Object belongs used to query/assign security descriptors to Objects; this callback is called a SecurityProcedure( ), if it is the same as the system default one nt!SeDefaultObjectMethod( ), the security descriptor is extracted directly from the main Object header nt!_OBJECT_HEADER::SecurityDescriptor and its reference count is incremented to prevent the system from freeing it while nt!ObCheckObjectAccess( ) is using it. Otherwise, nt!ObpGetObjectSecurity( ) calls the SecurityProcedure( ) the first time to determine the required size for the security descriptor, allocates a buffer large enough to hold it then finally calls the SecurityProcedure( ) again setting the operation code to nt!QuerySecurityDescriptor.
If the security descriptor is extracted successfully, the system checks whether it's NULL or not (extracting a null security descriptor doesn't mean the operation has failed, the operation is considered failed if the SecurityProcedure( ) returns an erroneous status code). If the security descriptor is NULL (the Object doesn't have a security descriptor protecting it) all requested rights are granted immediately without any sort of check.
At the end of this section, I hope that you understood how the security reference monitor handles access check requests when the Object lacks a security descriptor.
Kernel mode Access check:
Another case that is handled in a special way by the Security Reference Monitor during the access check phase is when the caller is a kernel mode thread as indicated by its corresponding nt!_ETHREAD::PreviousMode. in this case the system ignores both the object security descriptor and the caller's subject context, it grants all requested rights immediately without checking. The following figure is a code snippet from the implementation of nt!SeAccessCheckWithHint( ) that handles that special case.
You might notice that this routine behavior depends on the presence of nt!MAXIMUM_ALLOWED in the caller's requested access rights; That is totally true as this special bit doesn't represent a real access right, it's a placeholder that is used to direct the nt!SeAccessCheckWithHint( ) to grant all allowed rights; since the system doesn't do any sort of check when handling such case, so it grants all valid specific rights that are defined for that Object Type (nt!_OBJECT_TYPE::TypeInfo::GenericMapping::GenericAll) plus any rights included in the caller's desired access mask.
NOTE: the nt!MAXIMUM_ALLOWED is not included in the granted rights mask returned to the caller. RemainingDesiredAccess & 0xFDFFFFFF is used to mask it off and ensure that only bits representing real rights are included.
Subject Context validation & Token selection:
After detailing how the system handles two different special cases during the access check phase, it's time now to explain the general flow step by step. The first step is validating the impersonation level if the caller's subject context has an impersonation token. The impersonation level determines what the thread that is impersonating the user identified by the impersonation token can do with it. The first valid level is nt!SecurityAnonymous which prevents the thread from using the token at all. The second level is nt!SecurityIdentification that allows the impersonating thread to access the token attributes, but as indicated by nt!SeAccessCheckWithHint( ), this level is not valid for an access check. During the access check, the security reference monitor ensures that the impersonation level doesn't equal those two levels. Otherwise, it fails the entire operation returning nt!STATUS_BAD_IMPERSONATION_LEVEL.
The Next two levels are nt!SecurityImpersonation and nt!SecurityDelegation. Both of them are considered valid by the security subsystem. The only difference between the two is that the second one allows the use of the impersonation token for remote access check while the first one doesn't allow remote use.
After successfully validating the caller's impersonation level, the system checks whether the desired access mask is empty (contains 0 rights). If it is empty, the access check stops here, and its result depends on the previously granted access mask. Since the access check may starts with some pre-granted rights either set by the system or hardcoded by the caller (the previously granted access mask is a parameter to nt!SeAccessCheckWithHint( ) so the caller can easily hardcode it). If there are no pre-granted rights, the access is denied which is indicated by returning STATUS_ACCESS_DENIED. Otherwise, all pre-granted rights are considered granted without any modification.
If there are some additional rights included in the desired access mask provided by the caller, the next step in the access check phase is the mandatory integrity control (A.K.A MIC), but before that, the caller's access tokens contained in the provided subject context must be locked if they are not. Their locking state is indicated by a Boolean parameter passed to nt!SeAccessCheckWithHint( ). If they are not locked yet, the primary token's lock is acquired first then the impersonation token's lock is acquired next. After acquiring both locks, the system determines which token to use for the rest of the access check steps. As I said before the impersonation token is always prioritized over the primary one.
After locking the subject context and choosing the right token to use, the system continues to the mandatory integrity control. MIC is a fundamental step in the access check phase which denies some rights comparing the integrity level of the caller which is stored in its access token to the integrity level of the object which is stored in its security descriptor. Before diving deep into the MIC step, let me describe what integrity levels are supported by the system. There are 5 integrity levels defined by the security subsystem which are from lowest to highest: untrusted, low, medium, high and system. Integrity levels are defined by the system as special security identifiers nt!_SID called integrity labels having nt!SECURITY_MANDATORY_LABEL_AUTHORITY as their main authority and one relative identifier (A.K.A RID) which indicates the level. The MIC step is totally implemented in one single routine which is nt!SepMandatoryIntegrityCheck( ).
The Mandatory Integrity Control:
The first step in the MIC is getting the mandatory policies of the caller's access token; the system stores them in the corresponding nt!_TOKEN::MandatoryPolicy. If nt!TOKEN_MANDATORY_POLICY_NO_WRITE_UP is not included in the policies, no integrity control is done at all, and no rights are denied. Otherwise, the system gets the integrity SIDs of both the object identified by the security descriptor and the caller identified by the access token. The system searches for a suitable access control entry that holds the object integrity level and its access policies in the provided security descriptor's Sacl. If there is no suitable control entry or the Sacl is null or it's not usable (nt!SE_SACL_PRESENT is not included in the control member) or the found control entry has the nt!INHERIT_ONLY_ACE flag set which directs the security subsystem to ignore it during the access check, the default integrity SID nt!SepDefaultMandatorySid defined by the system is used to determine the object's integrity level and the access policies are hardcoded to include just nt!SYSTEM_MANDATORY_LABEL_NO_WRITE_UP which prevents lower integrity level callers from modifying the object. Otherwise, the security identifier stored in the found access control entry is used as it is and the access mask is treated as the access policies.
The next step is getting the integrity SID of the caller's access token. nt!SepMandatoryIntegrityCheck( ) first acquires the token's lock if it's not already acquired. Then it gets the integrity SID index stored in the corresponding nt!_TOKEN::IntegirityLevelIndex which represents the 0 based index of the SID in the list of security identifiers held by the token nt!_TOKEN::UserAndGroups. If the integrity SID index is -1, the system treats the access token as untrusted. Otherwise, the integrity SID is extracted from the SIDs list and used as it is for the rest of the MIC phase. Finally, the access token's lock is released to allow other routines to use it.
After getting both integrity SIDs, the same routine verifies their validity by checking whether their main authority is nt!SECURITY_MANDATORY_LABEL_AUTHORITY which is the only valid one for integrity labels. Next, the security subsystem extracts the integrity levels from both SIDs. It is determined by the value of the last sub authority stored in the SID sub-authorities list nt!_SID::SubAuthority[ ]. if the integrity SID doesn't contain any sub-authorities the level is treated as if it were untrusted nt!SECURITY_MANDATORY_UNTRUSTED_RID. If the caller's integrity level is greater than or equal to the object's integrity level, no rights are denied in this phase; the system sets the allowed rights to the combination of the object type's nt!_OBJECT_TYPE::TypeInfo::GenericMapping::GenericAll and the global standard rights nt!_GENERIC_MAPPING::GenericAll. Before continuing, I must explain what standard rights are. Standard rights are a special generic mapping structure defined by the security subsystem that combines all common rights applicable to any object no matter to what type it does belong. These rights are divided into 4 categories as any instance of nt!_GENERIC_MAPPING, the READ category includes only one right which nt!READ_CONTROL that allows the caller to read both the Dacl and the Owner of an Object. The next one is the WRITE category which includes 4 different rights which are self-explanatory, nt!WRITE_DAC, nt!WRITE_OWNER, nt!DELETE and finally nt!ACCESS_SYSTEM_SECURITY. Next is the EXECUTE category which includes only one right which is nt!SYNCHRONIZE that allows the caller to use the object in one of the wait routines defined by the system such as kernelbase!WaitForSingleObject( ). The last category is a mask combining all of the previous rights and it's stored in the nt!_GENERIC_MAPPING::GenericAll.
The next case is when the caller's integrity level is lower than the object's one. In this case the allowed rights depend totally on the object access policies. Since those policies are described in detail in the previous part, I will not explain them again. If nt!SYSTEM_MANDATORY_LABEL_NO_READ_UP is included, read access is totally denied by removing nt!StandardRights::GenericRead and the object type's nt!_OBJECT_TYPE::TypeInfo::GenercMapping::GenericRead from the allowed rights. nt!SYSTEM_MANDATORY_LABEL_NO_WRITE_UP is included, write access is totally denied by removing nt!StandardRights::GenericWrite and the object type's nt!_OBJECT_TYPE::TypeInfo::GenercMapping::GenericWrite from the allowed rights. Finally, if nt!SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP is included, execute access is totally denied by removing nt!StandardRights::GenericExecute and the object type's nt!_OBJECT_TYPE::TypeInfo::GenercMapping::GenericExecute from the allowed rights. So, to put things together, read access is only allowed if the access policies don't include nt!SYSTEM_MANDATORY_LABEL_NO_READ_UP, execute access is only allowed if and only if nt!SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP is not included; but that is not the case for write access; as depending on my reversing of nt!SepMandatoryIntegrityCheck( ) it requires the caller's integrity level to be greater than or equal to the object's one to be allowed even if the access policies mask doesn't include nt!SYSTEM_MANDATORY_LABEL_NO_WRITE_UP.
The last thing that nt!SepMandatoryIntegrityCheck( ) does is filling in a non-documented structure I named it nt!_MANDATORY_INTEGRITY_CONTROL_RESULT
depending on the result of the MIC phase.
After deciding which rights are denied and which are not, the system checks whether the caller's desired access mask includes any rights thar are not included in
the nt!_MANDATORY_INTEGRITY_CONTROL_RESULT::AllowedRights. If yes, the access is immediately denied and nt!SeAccessCheckWithHint( ) returns STATUS_ACCESS_DENIED. Otherwise, if no requested rights were denied during the M.I.C phase, the security subsystem jumps to the next step which is granting nt!WRITE_DAC and nt!READ_CONTROL which are known as the Owner rights.
Owner Rights & Fast SID Lookup:
Before checking whether nt!WRITE_DAC and nt!READ_CONTROL should be granted or not, we must get the Owner of the Object which is stored in its security descriptor as a security identifier nt!_SID. The Owner is stored in two different ways depending on the descriptor format which I previously said that it can be either absolute where the members are stored as direct pointers, or self-relative format where the members are stored one after the other following the header nt!_SECURITY_DESCRIPTOR_RELATIVE which stores their offsets.
After correctly extracting the Owner SID from the security descriptor, the security subsystem's next step is to determine whether the caller is the owner of the object by checking whether its access token contains the Owner SID. Before continuing I must detail the algorithm used by the system to perform this lookup as it doesn't directly iterate through all security identifiers found in the token, it uses first a technique I called it -Fast SID lookup- via the help of SID hash arrays nt!_SID_AND_ATTRIBUTES_HASH.
Fast SID lookup is a technique used by the system to minimize the complexity of searching for a random security identifier in an access token to O(1). The best way to understand how this algorithm works is to reverse engineer a routine called nt!RtlSidHashLookup( ) which takes two parameters, the first one is the token's SID hash array which contains 32 elements considered as pointers to hashes, but it only covers the first 64 identifiers, and the second one is the SID to lookup. The lookup is performed through a security identifier hash byte (SidHashByte for short); the system constructs this byte by getting the value of the last sub authority of the given SID then uses only the first byte which is divided into 2 parts (the lower and the higher 4 bits) both used as indexes into the token's hash array. The first part is used as an index into the lower half which represents the first 15 elements and the second one is used as an index into the next half which represents elements starting from index 16. The bitwise AND operation of the two corresponding hash array values is calculated, and the next step depends on the result.
If the result of the previous bitwise AND operation is 0, the SID is not covered by the given hash array, since SID hash arrays are limited in size and don't necessarily cover all identifiers, it's too early to make a final decision that the given SID is not present in the token. If the token contains more than 64 identifiers, the system checks each SID starting from the 64th one in the given Sid hash's nt!SID_AND_ATTRIBUTES_HASH::SidAttr which is a list of security identifiers and their corresponding attributes; If it didn't find any match it returns NULL to the caller indicating that the token doesn't contain the given SID.
If the result of the bitwise AND operation is different than 0, each byte of it represents a separate range of SIDs covering 8 identifiers. if the first and the second bytes are both equal to 0, the system switches to use the previously described slow lookup method iterating all SIDs starting from the 64th one to determine whether the token contains the given SID or not. Otherwise, the non-zero byte is considered the hash byte which is used as an index into the global list mapping hash bytes values to SID relative indexes declared by the system as nt!SidHashByteToIndexLookupTable[ ].
After getting the right index and the right range (the first byte corresponds to the first 8 SIDs, the second byte corresponds to the next 8 SIDs and so on ...), the system uses these two pieces of information to locate the right nt!_SID_AND_ATTRIBUTES within the given SID hash nt!_nt!_SID_AND_ATTRIBUTES_HASH::SidAttr. The extracted entry isn't directly returned to the caller, the system compares it's SID to the given one and then if and only if they are equal, it returns the same entry back to the caller. If the index we have gotten corresponds to another SID, the system updates the hash byte masking off the bit corresponding to the index and tries again several times. If it became 0; the next byte from the bitwise AND operation done previously is used if it's not 0 and of course the range is also shifted to cover the next 8 SIDs, but if it is 0, the system must switch to the slow method described previously.
NOTE: this algorithm is obviously complex, but I need you to be sure that not understanding it, will not affect your understanding of the principal parts of the access check.
After detailing how the system does determine whether the Owner security identifier is present in an access token or not. The system will grant nt!WRITE_DACL and nt!READ_CONTROL (if they are requested by the caller or nt!MAXIMUM_ALLOWED is included) if the access token contains the Owner SID in both its normal and restricted identifiers, and also the security descriptor Dacl mustn't contain any access control entry that targets the object's owner; nt!RtlpOwnerAcesPresent( ) is the system routine responsible for the previous check.
If the caller's provided desired access mask contains only nt!WRITE_DAC and nt!READ_CONTROL, the access check stops here. Otherwise, the system calls nt!SepAccessCheck( ) to continue checking the rest.
NOTE: the system will not grant any rights if the Owner SID found in the token has nt!SE_GROUP_USE_FOR_DENY_ONLY modifier.
Privilege check:
In this section I will present examples of some rights that are granted directly if the caller's access token holds some privileges. The first right among those is nt!ACCESS_SYSTEM_SECURITY which allows the caller to access the system access control list (A.K.A Sacl) associated with the object's security descriptor. To get this access, the token must hold nt!SeSecurityPrivilege in an enabled state as indicated by the corresponding nt!_TOKEN::Privileges::Enabled bit mask. The main routine used by the security subsystem to check whether a privilege is enabled or not is nt!SepPrivilegeCheck( ). if the caller has requested nt!ACCESS_SYSTEM_SECURITY but its token doesn't hold the required privilege, the access check is failed and nt!SepAccessCheck( ) returns nt!STATUS_PRIVILEGE_NOT_HELD.
The next right that can be directly granted through a corresponding privilege is nt!WRITE_OWNER which allows the caller to change the Owner SID stored in the object's security descriptor and as I previously explained, being the owner can grant you some special rights. To change the owner the caller's access token must hold the nt!SeTakeOwnershipPrivilege or the nt!SeRelabelPrivilege. If both are not enabled, the access will not be considered denied; and this is the difference between this right and the previous one. nt!ACCESS_SYSTEM_SECURITY cannot be granted through an access control entry in the Dacl, but the system can grant nt!WRITE_OWNER if the Dacl contains an entry that allows such right even if the access token doesn't hold any of nt!SeTakeOwnershipPrivilege and the nt!SeRelabelPrivilege.
NOTE: the security subsystem is not limited to only those two privileges; there are other ones that aren't checked during the object creation flow.
No Dacl vs Empty Dacl:
Another two special cases which seem the same or at least related, however the system handles them differently. The security descriptor protecting the object is considered not having a Dacl if nt!SE_DACL_PRESENT is not included in its control modifiers (_nt!_SECURITY_DESCRIOTOR::Control & SE_DACL_PRESENT == 0x0) or if the Dacl pointer is NULL (in case where the security descriptor is in absolute format) or the Dacl offset is 0 (in case where the security descriptor is in self-relative format). The security reference monitor handles this case depending on whether the caller's desired access mask contains the special nt!MAXIMUM_ALLOWED modifier. If this modifier is present the system grants all possible access rights combining the generic all of the type to which the object belongs and the caller's requested rights (of course the nt!MAXIMUM_ALLOWED is removed as this is not a valid right). You might be thinking that combining a mask containing every possible access to the object with the caller's requested mask has no effect. Actually, this is not true at all as I said before the nt!_OBJECT_TYPE::TypeInfo::GenericMapping::GenericAll combines only specific rights, but the caller may have requested some standard rights such as nt!WRITE_OWNER described in the previous section. If the caller's access token doesn't hold the required privileges to get this right directly, it will be granted immediately if the Dacl is NULL and this is achieved by adding the requested rights to the list of granted access. In the other hand, if nt!MAXIMUM_ALLOWED is not present, only the requested rights are granted. One important thing to mention is that the pre-granted rights described previously are also included in the final mask combining all granted rights.
The Dacl is considered empty if its corresponding nt!_ACL::AceCount is 0. Again, the system decided how to handle this case depending on whether nt!MAXIMUM_ALLOWED is present in the caller's provided desired access mask. if this modifier is not present, the access is immediately denied and nt!SepAccessCheck( ) returns nt!STATUS_ACCESS_DENIED. Otherwise, if nt!MAXIMUM_ALLOWED is present, the system denies the access if no rights were granted before reaching this step.
To summarize, the two cases described in this section are actually opposite. Having a null Dacl directs the system to grant every possible access, however, having an empty Dacl means that no access can be granted.
Dacl access control:
The last step in the access check phase is the Dacl access control which is implemented in two different kernel routines nt!SepNormalAccessCheck( ) and nt!SepMaximumAccessCheck( ). The system decides which one to use depending again on the presence of nt!MAXIMUM_ALLOWED. If the caller's provided desired access mask includes this modifier nt!SepMaximumAccessCheck( ) is used. Otherwise, the system uses nt!SepNormalAccessCheck( ). Both of these two routines work the same way, the only difference is that during the maximum check, all allowed rights are granted. However, in the normal access check, only the caller requested rights are checked by the system. During the Dacl access control, the system iterates through all of the access control entries skipping any entry having the nt!INHERIT_ONLY_ACE which indicates that it's solely used for inheritance not for the access check. deny only SIDs found in the caller's access token are ignored if the current control entry the system is checking is an allowed one. The result of the access check depends on whether all requested rights are granted or not for the normal check. In the maximum check case, the system aims to collect all allowed rights so in this step the access check can't be considered failed.
Assembling Privileges:
After finishing the access check and deciding which rights are granted, the system assembles all privileges it used to grant some rights using nt!SepAssemblePrivileges( ). The concerned privileges are only nt!SeSecurityPrivilege used to grant nt!ACCESS_SYSTEM_SECURITY, nt!SeTakeOwnershipPrivilege and nt!SeRelabelPrivilege used to grant nt!WRITE_OWNER. The system allocates a paged pool buffer large enough to hold the privilege set header nt!_PRIVILEGE_SET followed by one nt!LUID_AND_ATTRIBUTES for each privilege. Then, it initializes the header setting the right privileges count in the nt!_PRIVILEGE_SET::PrivilegeCount. Finally depending on which privileges are used to grant access, it initializes one or more nt!LUID_AND_ATTRIBUTES setting the attributes member to nt!SE_PRIVILEGE_USED_FOR_ACCESS.
NOTE: the access check can stop at any stage if all requested rights are granted.
Conclusion:
That is all for now, I hope you have learned something from this article, stay tuned and wait for the next part that will mainly focus on the name lookup phase.
References:
































Comments
Post a Comment