Deep dive into the Object creation flow in Windows - PART 3
Post-Initialization & Name Lookup.
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 third 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 explain the usefulness of each optional header covering quota charging, exclusive access, handle count databases and finally the name lookup phase which is the most complex part in my research. As the previous parts, all involved routines and data structures are mentioned in detail.
Introduction:
This part is a little different as there are no new terms or concepts to define before starting. Before detailing the name lookup phase, I must talk about some required steps that can't be skipped such as the conditions under which the system charges the object quota values for the calling process. In this part you will also see how the system finalizes the object initialization and what changes it performs to indicate so. The exclusive access is also described, and I will provide a detailed overview about it. Handle tracking via the help of handle count databases or the fast single-entry mechanism is also described step by step. The last section will cover the name lookup from A to Z.
Before starting, I must say that unfortunately due to an unexpected problem in IDA during the reversing, the saved work where all function pseudo codes were cleaned up gets corrupted. I have tried to clean it again to make it look better. Don't worry as all you need to know is well written in the next sections.
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 fourth and the fifth steps are covered in detail, the rest will be covered in the next part.
Quota charging:
Each process in the system has a known limit in terms of how much pool memory it can use. As I said in part 1, each Object is simply a block of memory allocated either from the paged or the non-paged pool as indicated by its corresponding type nt!_OBJECT_TYPE. The Object header can be preceded by an optional header that stores the required quota values that the system must charge for the process creating the Object. This header is called the quota info header, and it is declared by the kernel as nt!_OBJECT_HEADER_QUOTA_INFO.it stores three different values defined for each Object that are the page pool, the non-paged pool and finally the security descriptor quota. All of these must be charged for the process creating the Object.
Now. let's deal with details of charging the required quotas and what conditions the system checks to determine if it should charge them or not. Quota charging is totally implemented in one private Object Manager routine which is nt!ObpIncrementHandleCountEx( ). Before charging the Object quotas for the calling process, the system first removes the nt!OBJ_FLAG_NEW_OBJECT from the flags bitmask associated with its object header. As I said in part 1, this flag indicates that the object is not yet fully initialized. Before I continue, I see it mandatory to explain how this flag is used in conjunction with the previously described Creation information. The Object Header as indicated by its declaration, has a union that serves two different purposes. It is initially used to store a pointer to the creation information structure that is filled in by the system during the early steps of the creation flow. This structure is only needed until the system completes the object initialization and removes the nt!OBJ_FLAG_NEW_OBJECT and nt!ObpIncrementHandleCountEx( ) is used to perform this task. When the initialization is finished and the object is no longer considered new, the system overrides the previous creation information pointer setting a pointer to the quota block nt!_EPROCESS_QUOTA_BLOCK associated with the calling process in its place.
Firstly, the system charges the security descriptor's quota for the calling process if the object is securable (object security is described in detail in part 1). To determine the right amount to charge, the system calls nt!SeComputeQuotaInformationSize( ) which performs the calculation taking into account only the Primary Group and the Dacl and ensuring that the number of bytes charged is a multiple of 4.
After calculating the security descriptor's required quota, the system checks whether the calling process is the system process defined as nt!PsInitialSystemProcess to determine whether it's allowed to perform the charging or not. If the charging is allowed, it delegates the operation to nt!PspChargeQuota( ) which is a kernel routine that belongs to another executive manager not covered in this series of articles and it's responsible for charging quotas for processes.
The next step is charging both the paged and the non-paged pool quotas. The system gets them either from the quota info optional header or if it happened that the object doesn't have one, it gets them from the object type to which the object belongs. The charging operation is done similarly as the previous one via the help of nt!PspChargeQuota( ).
Finally, it's important to say that if the charging failed at any stage because the process quota limit is reached, nt!ObpIncrementHandleCountEx( ) stops its work and returns nt!STATUS_QUOTA_EXCEEDED to the caller indicating that an error has occurred somewhere during the quota charging phase.
Exclusive access:
As the name suggests, exclusive access means that only one process is allowed access the object through a HANDLE at same time (handles are beyond the scope of this part, they will detailed in the next one). Before granting such access to the caller, the system must do several checks to ensure that the target Object supports exclusive access and that it's not currently exclusively in use by some other process. The Object Manager checks first whether the Object flags bitmask nt!_OBJECT_HEADER::Flags includes nt!OBJ_FLAG_EXCLUSIVE_ACCESS that indicates whether the object supports exclusive access or not. Next, it checks whether the caller attributes stored in the creation information (this step is fully explained in part 1) include nt!OBJ_INHERIT which is incompatible with the exclusive access because it allows child processes to use the Object too. If one of the previous checks failed, the system denies such access to the object and doesn't move forward to the next step.
After ensuring that the object supports exclusive access and the caller is allowed to do so, the next step is checking whether the object is already in exclusive use by some other process. Another optional header is involved in this case that stores a reference to the process currently using the Object, it is called the process info header, and it's declared by the kernel as nt!_OBJECT_HEADER_PROCESS_INFO. The system allows exclusive access to caller depending on the results of the following checks. As I said in part 1, the process info header is only present if the caller attributes include nt!OBJ_EXCLUSIVE so it this attribute is not included, the system will not even check whether it's allowed to exclusively access the Object or not. The process info header stores the nt!_EPROCESS that corresponds to the process currently using the object. The caller is granted such use only in two cases. The first one is if there is no process currently monopolizing the object which is indicated by setting the nt!_OBJECT_HEADER_PROCESS_INFO::ExclusiveProcess to NULL. The next case is if the caller's process is the one currently set as the exclusive process. If no one of the previous checks succeeded, the system denies the access totally to the caller returning nt!STATUS_ACCESS_DENIED.
At the end and as you might notice in the previous figure, the system sets the caller's process' corresponding nt!_EPROCESS as the exclusive process to indicate that this object is currently in use exclusively. After that, the system moves forward and check whether the Object supports kernel access only as indicated by the presence of nt!OBJ_FLAG_KERNEL_ONLY (0x4) in its flags bitmask. In this case, the access is denied if the caller is a user-mode thread as indicated by the AccessMode parameter passed to nt!ObpIncrementHandleCountEx( ). This parameter's source is obviously the corresponding nt!_ETHREAD::PreviousMode member.
Handle count tracking:
Some objects have a special optional header called the handle info header that is declared by the kernel as nt!_OBJECT_HEADER_HANDLE_INFO. The system uses this header to track which processes are using are currently using the object through a HANDLE and how many handles each process has. The info header is not a declared as a structure as the other ones, it's a union that is used in two different ways. The handle count database pointer declared as nt!
_OBJECT_HANDLE_COUNT_DATABASE is used to store multi-instances of nt!_OBJECT_HANDLE_COUNT_ENTRY where each one is used to track the number of handles used by one process identified by its corresponding nt!_EPROCESS to access the object. The next member is declared as nt!_OBJECT_HANDLE_COUNT_ENTRY and is used in cases where there is only one process accessing the object through a handle.
_OBJECT_HANDLE_COUNT_DATABASE is used to store multi-instances of nt!_OBJECT_HANDLE_COUNT_ENTRY where each one is used to track the number of handles used by one process identified by its corresponding nt!_EPROCESS to access the object. The next member is declared as nt!_OBJECT_HANDLE_COUNT_ENTRY and is used in cases where there is only one process accessing the object through a handle.
Before adding a new entry to track the calling process handles to the object, the system ensures that the object type to which the object belongs has a non-NULL OpenProcedure( ) associated with it (nt!_OBJECT_TYPE::TypeInfo::OpenProcedure != NULL). If the check failed, nt!ObpIncrementHandleCountEx( ) immediately stops its work and returns nt!STATUS_UNSUCCESSFUL indicating that it has failed. Otherwise, the next step is preparing a handle count entry to use. The system delegates this task to nt!ObpLockHandleDataBaseEntry( ).
The behavior of nt!ObpLockHandleDataBaseEntry( ) depends on the whether the object flags bitmask includes nt!OBJ_FLAG_SINGLE_ENTRY that helps this routine deciding which member from the handle info optional header union to use. If this flag is set, the nt!_OBJECT_HEADER_HANDLE_INFO::SingleEntry is used. Otherwise, the nt!_OBJECT_HEADER_HANDLE_INFO::HandleCountDataBase is used instead. Before inserting a new entry, the system tries first to use a pre-existing free entry. If the previously mentioned flag is set, the system treats the single entry as free if one of the following two conditions is TRUE. The first one is if the entry's process member is set to NULL. In this case, the system simply overrides it and sets a pointer to the calling process corresponding nt!_EPROCESS in its place.
The second condition is if the entry's process is the same as the calling process. The system doesn't immediately use it as in the previous case. as you might notice in the first figure in this section, each handle count entry has an 8 bits lock member nt!_OBJECT_HANDLE_COUNT_ENTRY::LockCount which is used to prevent concurrent access to it. Since this member is only 8 bits, it can't exceed 0xFF or it will be erroneous. To evade breaking things, the system uses the entry if and only if the lock count doesn't reach 0xFF, otherwise it must move forward to create a new.
If nt!OBJ_FLAG_SINGLE_ENTRY is not set indicating that there are more than one process currently referencing the object through one or more handles, the system iterates through all entries found in the handle count database searching for either an entry having its process member set to NULL, so the system initializes it setting its lock count to 1 and its handle count member to 0, or an entry having its process member points to the calling process itself and its lock count under 0xFF so the system can increment it safely without breaking the entire locking mechanism.
If the system didn't find any suitable pre-existing entry to use, it must insert a new one to track the calling process handle count. To achieve this task, it calls nt!ObInsertHandleCount( ) that starts by calculating the new size of the handle count database. The calculation depends on the current state of the handle info header which is determined by the presence of the nt!OBJ_FLAG_SINGLE_ENTRY in the object flags. If there are only one entry, the system converts the format from single entry to multi-entries packed in a database. The system allocates a new buffer to hold the new database, then it initializes it setting its nt!_OBJECT_HANDLE_COUNT_DATABASE::CountEntries to 2. After that, it copies the pre-existing entry first then initializes the newly added entry as an empty one then it removes the single-entry flag from the object header. If nt!OBJ_FLAG_SINGLE_ENTRY is already unset, the system doesn't add room for just one additional entry, it adds space for up to 4 more entries all initialized as empty. It finally frees the old database memory block to prevent pool leaks. The last step is updating the handle info header setting the address of the newly allocated database as its nt!_OBJECT_HEADER_HANDLE_COUNT::HandleCountDataBase.
Finally, the newly inserted entry is initialized setting its lock count to 1, its handle count as 0 and then associate it with calling process setting it corresponding nt!_EPROCESS as its process member.
You might be asking why the main routine that implements all of the previously explained tasks is called nt!ObpLockHandleDataBaseEntry( ), and where the entry gets locked. The answer is very simple; the lock count of the entry is a n 8 bits unsigned integer that is incremented whenever someone locks the entry. Setting the lock count to 0 means the entry is not locked. Otherwise, it is considered locked. nt!ObpLockHandleDataBaseEntry( ) either sets the lock count to 1 or increments it which is how the system implements locking for handle count entries as I described.
After successfully preparing a handle count entry for the calling process, the next step is of course incrementing its handle count nt!_OBJECT_HANDLE_COUNT_ENTRY::HandleCount then unlocking it by decrementing its lock count nt!_OBJECT_HANDLE_COUNT_ENTRY::LockCount.
The final step covered in this section is atomically incrementing both the object's handle count stored in its main header nt!_OBJECT_HEADER::HandleCount and also the handle count stored in the corresponding type nt!_OBJECT_TYPE::TotalNumberOfHandles.
The OpenProcedure callback:
Before creating a handle entry and inserting it in the caller's handle table, the system first needs to ensure that the caller is allowed to reference the object through a handle. This task is achieved via the help of a special callback called the OpenProcedure( ).
Again, the same nt!ObpIncrementHandleCount( ) is the main routine involved in this step. It starts by calling the OpenProcedure( ) which is a callback defined for each object type, and it's called before creating a HANDLE (handles and handle tables will be covered in the next part) to the object. The result of that callback directs the system to either continue to the next step or returns to the caller with a failure.
You might notice that before calling the OpenProcedure( ), the system attaches to the calling process. Since this series of articles focuses on the creation path, the operation type that is checked in the previous figure is set to nt!ObpCreateHandle which indicates that the caller wants a handle to a new object not an existing one. The attachment is done through nt!KeStackAttahcProcess( ) that gives the calling thread the ability to access the virtual address space and the handle table of a given process. Of course, the system doesn't do any of the previous if the caller's process is the same as the current process as determined by the current thread's corresponding nt!_ETHREAD::Tcb::ApcState::Process.
If the OpenProcedure( ) failed, the system unlocks the handle count entry associated with the calling process through nt!ObpUnlockHandleDatabaseEntry( ) and then decrements the object handle count previously incremented.
Objects list:
Objects belonging to the same type are all linked in the same list during their creation phase which is indicated by setting the operation type parameter to nt!ObpCreateHandle. Each object has a creator info optional header that is used by the system to achieve the linking. This header is declared by the kernel as nt!_OBJECT_HEADER_CREATOR_INFO and the most important member it contains is the nt!_OBJECT_HEADER_CREATOR_INFO::TypeList which is a nt!_LIST_ENTRY.
Before linking the object to the previously mentioned list, it must be in a state where it is safe to do so. To ensure that this task is done in a mutual exclusive way, the system first acquires the object's push lock for exclusive access using nt!ExfAcquirePushLockExclusive( ). After that the Object's list entry is inserted at the tail of the object type's list of objects. At the end, the system obviously releases the previously acquired object's push lock to allow other threads to access it.
Name Lookup:
As I said in part 1, objects can be either named or unnamed. The object name can be either absolute or relative, and it is tracked by an optional header called the name info header which is declared as nt!_OBJECT_HEADER_NAME_INFO. This header contains the path name as a nt!_UNICODE_STRING and a parent object directory which is a special Object Manager type used solely to store other objects, and it is the main component that helps in the NT namespace construction. Object directories are declared as nt!_OBJECT_DIRECTORY.
The entry point of this phase is nt!ObpInsertOrLocateNamedObject( ) which is a wrapper that calls nt!ObpLookupObjectName( ) and deals with special cases. The first step in the name lookup is getting the authentication id that is used later to get the right device map (device maps are explained in detail in this section). As I said in the last part, the system almost always prioritizes the impersonation token over the primary one. It checks the current thread's corresponding nt!_ETHREAD::CrossThreadFlags.ActiveImperosnationInfo flag to determine whether it is currently impersonating another user or not. If this flag is set, the system acquires the thread's lock and checks it again because the thread can revert to its original context while the system was waiting for the lock acquisition. If the thread is still impersonating another user, the system extracts its impersonation token from the corresponding nt!_ETHREAD::ClientToken::ImpersonationData which stores all required impersonation data like the token's address and the impersonation level. The last step is incrementing the token's reference count to ensure that it's not deleted while nt!ObpLookupObjectName( ) is using it. Otherwise, if the current thread is not impersonating another user, the system simply uses the current process' primary token which is declared as a nt!_EX_FAST_REF so it can be referenced either through the fast way using nt!ObFastReferenceObject( ) or if it's impossible to do so, the system references it directly using nt!ObReferenceObjectWithTag( ). After deciding which access token to use, the system calls nt!SeQueryInformationToken( ) to get its authentication id. Finally, the access token is dereferenced to prevent a reference leak, and the system next step depends on the result returned by the last called routine. If nt!SeQueryInformationToken( ) failed to get the authentication id, nt!ObpLookupObjectName( ) stops its work here and returns to its caller with a failure.
After getting the current authentication id, the system checks whether it can ignore the case during the lookup. Case insensitive is a system setting controlled both globally by the Object Manager through nt!ObpCaseInsensitive which is a global Boolean, and per object type as each one has a special flag that controls this setting stored in its corresponding nt!_OBJECT_TYPE::TypeInfo::ObjectTypeFlags::CaseInsenetive. If case insensitive lookup is allowed, the system adds the nt!OBJ_CASE_INSENSITIVE in the handle attributes bitmask provided by the caller. Another attribute is checked by the system and also affects the lookup behavior which is nt!OBJ_FORCE_ACCESS_CHECK. This attribute directs the system to treats the request as a user-mode one whatever the real access mode was, and it plays a crucial role in the manual lookup during the parsing loop.
Now, it's time to start parsing the path name provided by the caller, and the first thing to do is deciding the root depending on whether the object name is absolute or relative to some object directory. If the name is an absolute one as indicated by passing NULL as the value of first parameter (the Parent Object handle) to nt!ObpLookupObjectName( ), the system checks whether the path name is not empty and ensures that it starts with '\'; otherwise, the path name is treated as invalid and nt!STATUS_OBJECT_PATH_SYNTAX_BAD is returned to the caller. Then, after validating the path name, the system handles a special case that happens when the last equals exactly "\". In this case, the system returns either the default namespace root nt!ObpRootDirectoryObject or if it's NULL, it returns the same object that is created before reaching this phase (the new object). If both are NULL, nt!STATUS_INVALID_PARAMETER is returned indicating that something went wrong.
Objects names can also be relative to some parent object which is generally of type nt!_OBJECT_DIRECTORY. If the caller's provided parent object handle is not NULL, the system gets its corresponding pointer using nt!ObReferenceObjectByHandle( ). If the path name is empty (nt!_UNICODE_STRING::Length == 0x0) and the parent object is an object directory, the system returns this same object to the caller. Otherwise, if the object name is not empty, the system checks it to determine its validity. If it starts with '\' it is treated as invalid and nt!ObpLookupObjectName( ) returns nt!STATUS_OBJECT_PATH_SYNTAX_BAD.
Before entering the parsing loop, the system must decide which root to start the parsing from. It can be either the default namespace root, the caller's provided parent object in the case of a relative path name or a special per session object directory called the Dos Devices directory which is stored in a kernel only data structure named nt!_DEVICE_MAP. The first and the second cases are straight forward but the last one needs some care from the system. Device maps are data structures used by the system to isolate object names in a session-by-session basis. Each session has its own device map that points to a separate dos-devices directory. This mechanism prevents name collision between different sessions. There is actually a global version of the dos-devices directory that is only accessible to processes having an integrity level equal to SYSTEM or Holding a special privilege that gives them the right to create a named object in the global namespace. You might be asking how the system knows that the path name is relative to the current dos-devices directory. The answer is through a special sub-path at the beginning of the object name which is "\??". To get the right device map to use, the system calls nt!nt!ObpReferenceDeviceMap( ) that uses the previously extracted authentication id to get the current session's device map only if the current thread is currently impersonating another user. Otherwise, that routine checks whether the current process already has a device map (nt!_EPROCESS::DeviceMap != NULL). If it doesn't, nt!ObpSetCurrentProcessDeviceMap( ) is called to associate a device map to the current process. If the current thread is currently impersonating another user, the previously mentioned authentication id is compared to the system authentication id which is 999 to determine whether the system device map nt!ObpSystemDeviceMap must be used. Otherwise, the system calls nt!SeGetLogonIdDeviceMap( ) that uses the lower 4 bits of the authentication id as an index into the global list nt!SepLogonSessions storing pointers to sessions. Before accessing the session data, it must be in a state where it is safe to do so. The same way, the lower 4 bits of the authentication id are used again as an index but into another list nt!SepRmDbLock that hold executive resources each one protecting a single session. The session lock is acquired then the system checks whether its dos-devices directory pointer is NULL or not. If it is NULL, nt!SeGetLogonIdDeviceMap( ) uses the session's authentication id to build the new dos-devices directory name and finally associates this directory with the session. After successfully getting the right Dos-Devices, If the path name equals exactly "\??\" or "\??", the system returns this object directory to the caller as it is. Otherwise, the system uses it as the starting point of the parsing loop.
After deciding the starting point that is going to be used as the Parent Object, the system can now enter the parsing loop. At each iteration nt!ObpLookupObjectName( ) starts by getting the ParseProcedure( ) from the Current Parent Object type nt!_OBJECT_TYPE::TypeInfo::ParseProcedure. Before continuing, I must explain the concept of private namespaces. Certain types of objects support their own private namespace that is managed by the Object Manager. To lookup an object name inside those namespaces, the system needs the help of a special callback that implements the lookup algorithm depending on how the namespace is constructed. This callback is called the ParseProcedure( ). The system implements manual lookup only for object directories, so if the Current Parent Object is of another type and doesn't have such procedure, the lookup fails returning nt!STATUS_OBJECT_TYPE_MISMATCH. Otherwise, the system, checks whether the extracted callback is compatible with the parent object type, then if so, it calls it providing the rest of the path name. The ParseProcedure( ) can return nt!STATUS_REPARSE which is a special status code that indicates that the routine has returned a different absolute path that needs to be parsed from the beginning using the default namespace root as the starting point even if the original path name was a relative one. This special status value can be returned several times, but the system has a pre-determined limit that if reached prevents it from parsing the path again so it just returns nt!STATUS_OBJECT_NAME_NOT_FOUND to the caller. Otherwise, the system next step depends on whether the parsing callback helper failed or not. If it has failed, the lookup stops there and the Current Parent Object is returned to the caller as the last-named object that nt!ObpLookupObjectName( ) has found. In the other hand, if it has succeeded, the system checks whether any object having that name is found. If yes, this object is returned to the caller, otherwise, the lookup is considered failed and nt!STATUS_OBJECT_NAME_NOT_FOUND is returned.
The next case is the manual lookup in which the system doesn't check the full path directly but instead it is parsed in a part-by-part basis. Each part is delimited by a starting '\' and an ending '\'. If the current part to lookup is empty, the system immediately fails the lookup and returns nt!STATUS_OBJECT_NAME_INVALID. Then before checking whether the current Parent Object which is in this case an object directory contains a child object having this part as its name, the system must ensure that the caller has the right to do so. As I said in the previous part, being a kernel mode caller grants you all rights without any checks, but if the caller is a user mode thread, it must either have the traverse privilege enabled which is indicated by the access state (nt!_ACCESS_STATE::Flags & TOKEN_HAS_TRAVERESE_PRIVILEGE != 0x0) or if this flag is not set, the system delegates this check to nt!nt!ObpCheckTraverseAccess( ). If the access is denied, the lookup ends there and nt!ObpLookupObjectName( ) returns to its caller with a failure.
Otherwise, the system calls nt!ObpLookupDirectoryEntry( ) that complete the lookup.
If this routine succeeded, the next step depends on whether the original path is totally parsed or not. If this is the last part to lookup, the system returns the found object to the caller. Otherwise, the system moves to the next path component but with an updated parent object set to the found object then repeats all of the previously explained steps. In the case where nt!ObpLookupDirectoryEntry( ) has failed, also the system behavior depends on whether this is the last component in the path. If the object name is totally parsed and the caller doesn't have the right to insert a new entry, the system returns nt!STATUS_ACCESS_DENIED. Otherwise, a new name entry is inserted in the current Parent Object directory and the new object's name info header is updated to include only the last path component which represents its correct name. To better understand the parsing loop, check out the following flow chart.
%20(1).png)
After performing the name lookup, if the system has found that the provided object name exists before, nt!ObpInsertOrLocateNamedObject( ) checks whether nt!OBJ_OPENIF is included in the caller provided attributes. If this attribute is not included, the system fails the operation returning nt!STATUS_OBJECT_NAME_COLLISION indicating that another object having the same name is found. Otherwise, all the steps starting from the access check until the post-initialization described in this part are performed again but for the found object not the newly created one. If something went wrong the system fails the entire operation.
Finally, after validating the object name, the system updates the security descriptor protecting the object using nt!ObAssignObjectSecurity( ) and providing the security descriptor associated with its parent object directory which is stored in its name info header. Of course, this step is only performed if the parent object directory to which the object name is relative is not NULL.
Conclusion:
That is all for now, I hope you have learned something from this article, stay tuned and wait for the final part in which handle tables and entries will be covered from A to Z.
























Comments
Post a Comment