Deep dive into the Object creation flow in Windows - PART 1

Allocation & Pre-Initialization. 

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 first 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 talk about the first step in creating a new object in Windows covering how an Object gets allocated, its various components, object types, the object body initialization step then finally I will describe in detail how the system implements security for securable objects.

Introduction:

        Let's start with required definitions of some concepts I will be using in this article.

Object Manager:  a Windows kernel executive manager responsible for managing all objects, it tracks lifetimes of objects using a per object reference counter, this counter gets incremented whenever the object is accessed and should be decremented when the object is no longer needed; the Object Manager will free the Object and all its associated resources when its reference count reaches 0. this Manager is also responsible for initializing the object headers which are divided into optional ones and the main header. The Object Manager is integrated into the Windows kernel as a set of routines having names that start with either Ob (short for Object Manager) or Obp (Short for Object Manager internal).

Object:  a partially opaque entity used by the operating system to represent almost everything. There are many types of objects supported by Windows such as I/O endpoints like file system files and directories, volumes and disks, other physical and virtual devices, synchronization primitives like mutexes, semaphores, events, timers and so on. an Object is divided into the header part which is totally managed by the Object Manager and the body part which is managed by the executive manager responsible for that object type.

Object Type:  every object in the system must belong to a predefined object type created by an executive manager. Object types are kernel only data structures used to store different pieces of information common to all objects belonging to that specific type. these data structures hold the total number of objects that belong to them, a set of flags that determine the behavior of the Object Manager when it deals with objects that belong to that type like for example the presence of some optional headers and whether the object is securable or not, a mask that combine valid access rights for objects of that type and so many other data members.

Security Descriptor:  Windows implements a unified security model for all types of objects supported by the system. Securable objects have a special data structure associated with them that lists all allowed and denied rights and this data structure is called a security descriptor. Security descriptors contain other pieces of information including the Owner of the object, you will see in the next part how does being the owner grant you some access rights without any checks, they also contain a list used for access auditing that determines which access should be audited and under what condition. The integrity level of the object (this concept will be covered in the next parts) is also stored in the security descriptor.

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 first two steps are covered in detail, the rest will be covered in the next parts.

Object Allocation:

        In this section, the object allocation phase is covered in detail starting from capturing the creation information, determining the presence of each optional header, allocating the object memory buffer and finally filling in the main Object header.
          
        Before allocating the object memory buffer, the system creates a special kernel structure that stores all of the creation information provided by the caller, this data structure is called an Object Creation  
Information nt!_OB_CREATION_INFORMATION. the Object creation information structure is allocated either from a pre-defined lookaside list associated with the current processor control block nt!_KPRCB or if this method failed, it's allocated directly from the kernel pools. After allocating a buffer large enough to hold the creation information for the object, the system calls nt!ObpCaptureObjectCreationInformation( ) to capture the desired information.

                                      
        The Object creation information mainly include the members of the input nt!_OBJECT_ATTRIBUTES passed by the caller to the Object creation routine, the Handle attributes is the first element to copy to the creation information buffer but it's not copied as it is, nt!ObpCaptureObjectCreationInformation( ) removes the kernel attribute nt!OBJ_KERNEL from it if the caller is a user mode thread as indicated by its corresponding nt!_ETHREAD::PreviousMode; Then the capturing routine copies the security descriptor stored in the input Object Attributes to a kernel buffer and stores that buffer's address in the creation information buffer; After that, the input Security Quality of Service found in the same Object Attributes are validated ( checking if the impersonation level is valid ) then copied to the end of the creation information buffer. The next step is copying the Object name (if the caller has provided one) to a kernel buffer. Finally, if all the previous pieces of information are captured correctly, the system initializes both the paged and non-paged pool quota charges either explicitly using the caller passed quota values or implicitly using the Object Type pool quota values.

        After successfully capturing the creation Information, the Object Manager will allocate a kernel buffer large enough to hold the object. This task is implemented in nt!ObpAllocateObject( ) which uses the captured creation information to determine the Object layout in memory. Before allocating the buffer, the system must calculate the required size for the Object, the first step is checking for the presence of various optional headers, but before I detail this step I must talk about the very important nt!_OBJECT_HEADER::InfoMask member of the main object header which is used to indicate which headers are present and which are not. The InfoMask is a bit mask where each bit indicates the presence of a single optional header; if a bit is set, the corresponding header is present otherwise it's not. As I said before optional headers are placed before the main object header in memory, to find the suitable offset that must be subtracted from the start of the main header to get an optional header start address, the system uses a special pre-filled kernel array nt!ObpInfoMaskToOffset[ ] which stores all possible offsets for each header. To better understand this complex part let's give an example, the Handle Info header nt!_OBJECT_HEADER_HANDLE_INFO depending on my reversing of nt!ObpAllocateObject( ) is placed before the Name Info header nt!_OBJECT_HEADER_NAME_INFO,  this means that the position of the Handle Info Header relative to the main header is affected by the presence of the Name Info header, and it's the case for all other headers. Each header is affected by all other headers that are supposed to be below it in memory. Since optional headers are fixed in size and their order is predetermined by the Object Manager, it's possible to know all possible offsets of each header relative to the main header depending on the presence of each header that is below it in memory. Those offsets are the ones stored by the previously mentioned nt!ObpInfoMaskToOffset[ ] and to get the right offset of a given optional header the system uses the InfoMask again, only bits corresponding to the given header itself and all its following headers are used to construct the right index into the array of offsets. That offset is subtracted from the start address of main object header to get the start address of the optional header. This same mechanism is used to access all optional headers of any object in the system, the InfoMask serves two purposes, it indicates the presence of optional headers, and it also helps the system in determining each header's location relative to the main header. 

        
        Now I will describe briefly each optional header's purpose and what condition does the system check to determine its presence. The first header is the Process Info header nt!_OBJECT_HEADER_PROCESS_INFO which is used only if the caller has requested exclusive access to the object by including nt!OBJ_EXCLUSIVE in the passed Handle Attributes; this header stores the nt!_EPROCESS corresponding to the process currently monopolizing the object. Next, the system ensures that the current process (the caller's process) is neither the System Process nt!PsSystemProcess nor the Idle Process nt!PsIdleProcess then updates the InfoMask setting the bit corresponding to the Quota Info Header nt!_OBJECT_HEADER_QUOTA_INFO to indicate its presence.  After that, the flags bitmask nt!_OBJECT_TYPE::TypeInfo::ObjectTypeFlags of the object type to which the Object belongs is checked to see if the MaintainHandleCount bit is set; then depending on the check result, the system updates the InfoMask again to indicate the presence of the Handle Info Header nt!_OBJECT_HEADER_HANDLE_INFO. Just below the Handle Info header you will find the Name Info Header nt!_OBJECT_HEADER_NAME_INFO which is only present if the Object Name is not empty (_UNICODE_STRING::Lengh != 0x0). Finally, the last optional header that is added is the Creator Info Header nt!_OBJECT_HEADER_CREATOR_INFO which is used to link a new Object with the list of all objects belonging to the same type. This header is only present if the MaintainTypeList bit is set in Object Type flags bitmask.



        After determining which optional headers are present and which are not, it's time to allocate a pool buffer large enough to hold the Object. The system again needs to get some required information from the corresponding Object Type nt!_OBJECT_TYPE; this time it needs the pool type and tag to use for the allocation. The object buffer is allocated via one of the kernel pool allocators (nt!ExAllocatePool( ) family).


        If the allocation was successful, the system initializes 3 optional headers. The Quota Info Header is initialized using information from the Creation Information buffer. The Name Info Header is simply initialized using the Object Name. Finally, the id of the calling process is set as the creator of the Object by setting its ID in the Creator Info Header. All other present optional headers are left empty.


        The next step is to initialize the Object Flags nt!_OBJECT_HEADER::Flags in the main Object Header. nt!OBJ_FLAG_NEW_OBJECT is added to the flags to indicate that this Object is still in the initialization phase. Next, the system adds also OBJ_FLAG_SINGLE_ENTRY to the Flags if the Handle Info Header is present (handle count entries will be detailed in the next parts) to indicate that the header currently has room for only one entry. After that the Handle Attributes in the Object Creation Information are consulted. If OBJ_KERNEL is set, OBJ_FLAG_KERNEL is added to flags. If OBJ_KERNEL_ONLY (this is an undocumented flag I have guessed its macro name from the reversed code of nt!ObpAllocateObject( )) is set, the OBJ_FLAG_KERNEL_ONLY is also added to the flags. OBJ_FLAG_PARMANENT is added if OBJ_PARMANENT is set in the Handle Attributes. Then finally, the system will add OBJ_FLAG_EXCLUSIVE_ACCESS if OBJ_EXCLUSIVE is set.

        At the end of the allocation phase, the system increments the total number of objects in the corresponding Object Type, set the Object Creation Information as the nt!_OBJECT_HEADER::CreateInfo (the combination of the OBJ_FLAG_NEW_OBJECT flag and the CreateInfo indicates that the Object is not fully initialized yet, you will see in the next parts when does the system remove that flag) and initialize the reference count to 1 (_OBJECT_HEADER::PointerCount = 0x1).


Object Initialization:

        In the initialization phase, I will start by describing how the Object Body is initialized and which routines are responsible for this task; since body initialization depends on the object type, only few examples of initialization routines are mentioned to clarify this step. Next, I will deeply explain the necessary steps to create and initialize a security descriptor the assign it to the Object.

        The Object body initialization involves filling in the Object Body structure members which changes from type to type. For example event objects are represented by nt!_KEVENT; timer objects are represented by nt!_KTIMER; Kernel queue objects are represented by nt!_KQUEUE; async procedure call objects (APC for short) are represented by nt!_KAPC; deferred procedure call objects (DPC for short) are represented by nt!_KDPC and so on. Due to this difference between objects, it's not possible to initialize all of them with a single generalized routine. Consequently, each Object Type has its own initialization routine defined by the system, some of these routines are found in the following figure.

        
        After initializing the Body, the system moves forward in the initialization step. The next step is checking whether the Object is securable or not, this information is found in the corresponding Object Type. Those checks are implemented in nt!ObInsertObjectEx( ). This routine checks two different conditions to determine whether to assign a security descriptor to the Object or not. The first condition is the state of the SecurityRequired bit in the Object Type flags. The second condition is whether the Object is named. If Both conditions are FALSE; the object is treated as not securable, consequently, no security descriptor will be assigned to it even if caller has provided a non-null security descriptor it's ignored. Otherwise, if one of those conditions is TRUE, the Object is treated as a securable one and nt!ObInsertObjectEx( ) will build a security descriptor then assign it to it.


        If the caller has provided a security descriptor to be used for the Object, it's necessary to validate its system access control list. To perform this validation the system calls nt!RtlValidSecurityDescriptorSacl( ) which is just a wrapper that checks for the presence of SE_SACL_PRESENT in the security descriptor control member to determine whether it has a usable Sacl or not; if it has a non-null usable Sacl it forwards the call to nt!RtlValidAcl( ) that performs the actual validation check. 


        nt!RtlValidateAcl( ) starts by checking whether the Sacl's revision level is in the valid range which is between 2 and 4 inclusive. Next it ensures that the Sacl is aligned correctly. Then it checks if the size of the provided Sacl is valid (it must be at least the size of the ACL header nt!_ACL which is 8 bytes).

        
        After validating the revision level and the size, the system will validate all access control entries in the provided Sacl in terms of alignment, size and target trustee SID. The first step is checking whether the entry is properly aligned. Then the entry size nt!_ACE_HEADER::AceSize is checked to see whether it is compatible with the total size of the Sacl (the Sacl's size must be at least the sum of all entries sizes + sizeof(nt!_ACL)). After that, the system ensures that each access control entry's size is at least the size of the header part nt!_ACE_HEADER and the target trustee security identifier nt!_SID. The trustee SID is also validated as the number of sub-authorities is limited to 0xF


        Finally, the Sacl is considered valid if and only if all of the previous checks are passed, otherwise it's considered invalid. 
        
        After validating the provided security descriptor system access control list, nt!SeComputeAutoInheritByObjectType( ) is called to determine the integrity access policy of the Object and whether it can inherit some security attributes from its parent. The global list that stores per type access policies nt!SepMandatoryObjectTypePolicy is iterated to find the corresponding access policies which can include
nt!SYSTEM_MANDATORY_LABEL_NO_WRITE_UP that prevents lower integrity callers from writing data to the Object; nt!SYSTEM_MANDATORY_LABEL_NO_READ_UP which prevents lower integrity callers from reading data from the Object; and the last valid policy is nt!SYSTEM_MANDATORY_LABEL_NO_EXECUTE_UP which prevents lower integrity callers from executing the Object. The corresponding object type entry from the previously mentioned list also holds the parent inheritance modifier that controls which parent security attributes are inheritable. It covers the Owner, the Primary group, the Dacl and also the Sacl. 

        
        After extracting the required information from the object type policies list, the next step depends on whether a parent security descriptor exists. If a caller provided a non-null parent security descriptor that has a usable Dacl and Sacl ( both nt!SE_DACL_PRESENT and nt!SE_SACL_PRESENT are set in its control member ), the system sets nt!SE_INHERIT_DACL and nt!SE_INHERIT_SACL accordingly.



        The last step in this part is updating the security descriptor's integrity access policies. The system iterates through the Sacl's list of access control entries searching for a nt!SYSTEM_MANDATORY_LABEL_ACE_TYPE entry which holds the object integrity control information. If a suitable entry is found, it updates its AccessMask member nt!_SYSTEM_MANDATORY_LABEL_ACE::AccessMask (in this type of ACEs it represents the access policies) adding the previously extracted policies to it. (NOTE: this step is only done if the system is building the object security descriptor based on a caller provided one. So, if the caller didn't provide a non-null security descriptor the extracted access policies are ignored).

       
        Now it's time to setup the security descriptor's attributes. To achieve this task, nt!RtlpNewSecurityObject( ) is called. The first thing it does is determining the right source to get the Owner and Primary group security identifiers from. The same mechanism is used for both attributes. If the caller has provided a non-null security descriptor, its owner and primary group SIDs are used as they are. Otherwise, the system firstly tries to inherit those attributes from the parent security descriptor (if one is provided of course) if nt!SE_INHERIT_OWNER and nt!SE_INHERIT_GROUP are set in the previously extracted inheritance mask. Otherwise, if it cannot inherit them from the parent security descriptor because either it's either null or it doesn't have those attributes, the system last source to get the two SIDs is the caller access tokens (this concept is not covered in this part but it will be detailed in the next parts); if the routine is enforced to ignore the impersonation token because a non-null security descriptor having nt!SE_SERVER_SECURITY set in its control member is provided by the caller, it get the Owner and the Primary Group Sid from the caller's primary access token, otherwise it tries first to get them from the impersonation token if it exists, then if doesn't, it finally gets them from the primary token.


        The next phase implemented in nt!RtlpNewSecurityObject( ) is constructing both the Dacl and the Sacl based on the caller provided security descriptor and parent security descriptor. The two access control lists are constructed the same way with the help of nt!RtlpInheritAcl( ) and nt!RtlpInheritAcl2( ). First, the system copies all non-inheritable access control entries from the caller provided security descriptor, then it copies inheritable entries from the caller provided parent security descriptor. Finally, the Dacl is checked to see if it is empty (doesn't contain any ACEs) so the system can determine if it must use the default Dacl associated with the caller's access tokens.

         The final step is assigning the new Security descriptior to the object. This task is delegated to a special procedure associated with the Object Type called the SecurityProcedure( ).

NOTE: 
        the access state creation step is totally skipped because it will be covered in detail in the next part but depending on my reversing of nt!ObInsertObjectEx( ), this step precedes the security descriptor construction phase.


Conclusion:

       That is all for now, I hope you have learned something from this article, stay tuned and wait for the next part in which the access check phase will be covered from A to Z.




please check out my LinkedIn and GitHub accounts.

    


Comments