Journey Into the Object Manager Executive Subsystem: Handles
0x00 Abstract
Allowing direct access to named or un-named executive objects to user mode and kernel mode applications would be extremely dangerous as it would interfere with, and render the duty of the executive subsystems obsolete. As a result, this would put the whole system at risk and make the management of executive objects almost impossible.Additionally, executive objects resides in kernel memory, which means that if user mode applications could directly modify data structure in kernel memory this would also be extremely dangerous and chances of a BSOD would be high. Driver and kernel applications can obviously directly access these data structures, however, most of the objects are undocumented and therefore it is highly recommended not to do so. Instead drivers and kernel applications should (must) use kernel APIs exposed by the different executive subsystems. Finally, they should also increase (i.e. ObfReferenceObject) or decrease (i.e. ObfDereferenceObject) the executive object's reference count for retention purposes.
For all the aforementioned reasons, and there might be more, Windows uses handles to indirectly interact with executive objects from user mode applications. There are two main advantages of using handles, which are:
- A layer of abstraction to allow an indirect use of executive object without caring about the actual data structure; and
- A granular access control based on user and object permissions and object type, checked by the kernel
This is the second paper of my Windows Object Manager Executive Subsystem series. The first paper is here: Journey Into the Object Manager Executive Subsystem: Object Header and Object Type.
0x00 Handle Tables and Handle Table Entries
First, handles are stored per-process basis inside _HANDLE_TABLE structures. The reason behind why there are multiple tables is covered later in this section. The address of this top-level table is stored in the ObjectTable field of the _EPROCESS structure of the process. For this reason, handles are only meaningful for the process in which the handle was created.
kd> !process 0 0 notepad.exe
PROCESS ffffac87a2418480
SessionId: 2 Cid: 0d18 Peb: 3c26fab000 ParentCid: 13f4
DirBase: 32be2000 ObjectTable: ffff800da2582b40 HandleCount: 228.
Image: notepad.exe
kd> ?? ((nt!_EPROCESS*)@@(0xffffac87a2418480))->ObjectTable
struct _HANDLE_TABLE * 0xffff800d`a2582b40
+0x000 NextHandleNeedingPool : 0x400
+0x004 ExtraInfoPages : 0n0
+0x008 TableCode : 0xffff800d`a59fc000
+0x010 QuotaProcess : 0xffffac87`a2418480 _EPROCESS
+0x018 HandleTableList : _LIST_ENTRY [ 0xfffff803`10455ae0 - 0xffff800d`a350c058 ]
+0x028 UniqueProcessId : 0xd18
+0x02c Flags : 0
+0x02c StrictFIFO : 0y0
+0x02c EnableHandleExceptions : 0y0
+0x02c Rundown : 0y0
+0x02c Duplicated : 0y0
+0x02c RaiseUMExceptionOnInvalidHandleClose : 0y0
+0x030 HandleContentionEvent : _EX_PUSH_LOCK
+0x038 HandleTableLock : _EX_PUSH_LOCK
+0x040 FreeLists : [1] _HANDLE_TABLE_FREE_LIST
+0x040 ActualEntry : [32] ""
+0x060 DebugInfo : (null)
kd>
Note that drivers and kernel applications have their own "shared" handle tables and the top-level table is the ObpKernelHandleTable table. The address of this table can also be found via the _EPROCESS structure of the System process.
kd> !process 0 0 System
PROCESS ffffac879a881040
SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 006d4000 ObjectTable: ffff800d9e806ac0 HandleCount: 2836.
Image: System
kd> ?? (unsigned int64)((nt!_EPROCESS*)@@(0xffffac879a881040))->ObjectTable
unsigned int64 0xffff800d`9e806ac0
kd> dq nt!ObpKernelHandleTable L1
fffff803`0feeca98 ffff800d`9e806ac0
kd>
The HandleTableList field is a double-linked list of _HANDLE_TABLE_ENTRY structures. This list is populated with a new entry whenever a user mode or kernel mode application creates or opens an executive object. The _HANDLE_TABLE_ENTRY structure prototype is as follows:
kd> dt nt!_HANDLE_TABLE_ENTRY
+0x000 VolatileLowValue : Int8B
+0x000 LowValue : Int8B
+0x000 InfoTable : Ptr64 _HANDLE_TABLE_ENTRY_INFO
+0x008 HighValue : Int8B
+0x008 NextFreeHandleEntry : Ptr64 _HANDLE_TABLE_ENTRY
+0x008 LeafHandleValue : _EXHANDLE
+0x000 RefCountField : Int8B
+0x000 Unlocked : Pos 0, 1 Bit
+0x000 RefCnt : Pos 1, 16 Bits
+0x000 Attributes : Pos 17, 3 Bits
+0x000 ObjectPointerBits : Pos 20, 44 Bits
+0x008 GrantedAccessBits : Pos 0, 25 Bits
+0x008 NoRightsUpgrade : Pos 25, 1 Bit
+0x008 Spare1 : Pos 26, 6 Bits
+0x00c Spare2 : Uint4B
kd>
At first glance this structure is out of the ordinary and as shown this is due to multiple unions. Nevertheless, the idea is that the structure has 16 bytes. The first 8 bytes are used to store the address of the object in kernel memory as well as information about the handle and the second 8 bytes are used to store the generic access rights assigned to the handle.
The above diagram provides a visual representation of the memory layout of the structure. The Unlocked field, which is one bit, is used by the kernel to lock or unlock the structure. The RefCnt field of 16 bits size is used as a backward reference count for the handle and starts at 0x7FFF for x64 systems. This means that if the RefCount is equal to 0x789e, there is 761 (0x7FFF - 0x789e) references to the handle. The Attributes field (i.e. audit, inherited and protected) is used to store the handle attributes. The audit handle attribute will dictate whether an event log is raised when the handle is closed. The inherited handle attribute dictates whether the handle will be duplicated into a child process created with the inheritance parameter set to TRUE. The protected attribute dictates whether the handle can be closed. Final part of the first 8 bytes is the ObjectPointerBits field which is used to store the 44 bits of the pointer of the object in memory. Finally, handles are not all providing the same access to an object and the desired access must be provided when requesting an handle. For this reason, the GrantedAccessBits field is used to store the generic access rights that the handle provide. Upon access to the object the generic access rights will be converted into specific access rights based on the object type.
As mentioned hereabove, the handle table is populated whenever an application creates or opens an executive object. For example, if an application creates a new event object the first thing that will be invoked is NtCreateEvent. This function will invoke two functions, first, the ObCreateObjectEx function with ExEventObjectType as object type in order to create an event object and allocate the object in the kernel memory pool, and, second, the ObInsertObject function, which among other things will invoke ObpCreateHandle. In the same fashion, if an application opens an event object, the NtOpenEvent function will be invoked, which in turn will invoke ObOpenObjectByNameEx with the ExEventObjectType as object type. If the object is actually found and that the caller has enough privileges to access the executive object, the ObpCreateHandle will be invoked in order to create a new handle. Regardless if the application creates or opens an executive object, ObpCreateHandle will invoke ExpAllocateHandleTableEntrySlow which is responsible to adding the newly created handle into the appropriate handle table.
Handles have to be stored in a handle table but these tables are not infinite in size, on the contrary they are limited to 4096 bytes. The reason behind that is because handle tables are allocated with the ExpAllocateTablePagedPool function, which allocates one memory page (i.e. 4096 bytes). As a result handle tables can only contain a maximum of 256 entries (4096 / 16), which is very low and definitely not enough for most applications. Instead, processes have multiple handle tables which are implemented as a three-level schema. The top-level table has 256 pointers to mid-level tables instead of 256 handle table entries, and each mid-level tables have 256 pointers to low-level handle tables, in which handle table entries are located. By following this logic, instead of having a limit of 256 entries, processes can have a maximum of 16,777,216 handle table entries (256 * 256 * 256).
Not all the mid-level and low-level tables are created and allocated during process initialisation, otherwise this would be a massive waste of memory space. Instead they are created on the fly when needed upon handle creation with either: ExpAllocateMiddleLevelTable, ExpAllocateLowLevelTable, ExpAllocateTablePagedPool depending on the need.
Regarding the top-level table, this one is created an allocated during process initialised and all of that takes place when the ObInitProcess function is invoked. If the function is invoked without specifying a parent _EPROCESS structure, the top-level table will be created by invoking the ExCreatehandleTable function, which will ultimately invoke the ExpAllocateTablePagedPool function in order to allocate the memory page in the kernel memory pool. If now a parent _EPROCESS structure is passed to the function, the top level table will be created by duplicating the parent process handle tables by invoking the ExDupHandleTable. Not all handles will be duplicated but instead only the ones with the inherited handle attribute set. Once the top-level table is created as well as any lower level table if needed, the top-level table is attached to the _EPROCESS structure of the newly created process.
0x00 From Handle to Object
At this stage one question remain: how the Object Manager can return an object when a handle (e.g. 0x000000000000008c) is provided? The answer to this question can be found by reverse engineering the ObReferenceObjectHandleWithTag function has this function, is responsible for returning an object based on an handle.First thing done by the function is to check if the handle provided is a user mode or a kernel mode handle (L40). If this is a user mode handle, the address of the _HANDLE_TABLE of the calling process is retrieved by invoking ObReferenceProcessHandleTable (L52) with a pointer to the caller _EPROCESS structure has single parameter. If this is a kernel mode handle, the operation is simpler has the address of the _HANDLE_TABLE is located stored in the ObpKernelHandleTable variable.
The above handles are considered as kernel mode handles, however they are neither user mode or kernel mode handles but instead pseudo handles. This is a specific case that is explained further at the end of this section.
If a kernel mode handle is provided, the value is modified with a bitwise AND operation (L121) in order to have a "normal" handle. The ExpLookupHandleTableEntry function is then invoked in order to get the _HANDLE_TABLE_ENTRY structure corresponding to the handle provided via the _HANDLE_TABLE of the process.
The address of the _OBJECT_HEADER structure of the object is then calculated (L168 or L157) with two bitwise operations against the first 8 bytes of the _HANDLE_TABLE_ENTRY structure, which as mentioned earlier represents the location of the object in the kernel memory pool.
Before returning the object, there are two other checks done by the function. First one being to compare the type of the object returned with the handle provided with the object type expected. For more information about how to get the type of an object based on the object header please refer to the Object Types section.
Lastly, the function checks if the caller has enough privileges to request the desired access with the ObpAuditObjectAccess function. If so, the object is returned by reference (L265), otherwise, the STATUS_ACCESS_DENIED (0xC0000022) status code is returned.
Based on all the above, if a breakpoint is set to the ExpLookupHandleTableEntry function, it is possible to locate the object in memory with _HANDLE_TABLE_ENTRY structure returned (stored inside RAX register).
kd> g
Breakpoint 3 hit
nt!ExpLookupHandleTableEntry:
fffff803`10160100 8b01 mov eax,dword ptr [rcx]
kd> gu
nt!ObpReferenceObjectByHandleWithTag+0xef:
fffff803`1015fc0f 488bf8 mov rdi,rax
kd> dq @rax L1
ffff800d`a25ee870 ac87a055`7eb4fa8d
kd> ?? ((int64)0xac87a0557eb4fa8d >> 0x10) & 0xfffffffffffffff0
unsigned int64 0xffffac87`a0557eb0
kd> !object 0xffffac87a0557eb0 + 0x30
Object: ffffac87a0557ee0 Type: (ffffac879a8ed6c0) File
ObjectHeader: ffffac87a0557eb0 (new version)
HandleCount: 1 PointerCount: 32071
Directory Object: 00000000 Name: \Endpoint {Afd}
kd>
0x03 Pseudo Handles
Pseudo handles were mentioned in the previous section. The idea is that these handles are not stored anywhere. Instead they are well known values used by the kernel to quickly identify objects. This was hinted in the previously paragraphs but the idea is that these pseudo handles are not stored anywhere. Instead they are well known values used by the kernel to quickly identify objects.Value | Object Type | Object Returned | Object Description |
---|---|---|---|
0xffffffffffffffff | Process | Current process | The Object Manager will return the _EPROCESS structure by invoking PsGetCurrentProcess |
0xfffffffffffffffe | Thread | Current thread | The Object Manager will return the _ETHREAD structure by invoking PsGetCurrentThread |
0xfffffffffffffffc | Token | Current process token | The Object Manager will return _EPROCESS->Token.Object |
0xfffffffffffffffb | Token | Current thread token | The Object Manager will return _ETHREAD->ImpersonationInfo.Token if set |
0xfffffffffffffffa | Token | Current thread effective token | The Object Manager will return the thread token if set otherwise will return the process token |