Handles are pointers to system resources such as memory addresses, processes, threads, files, registry keys and device drivers.
You can think of them as C/C++ void pointers which can be cast to pointers to any other type.
In this case, the handle, void*, can be casted to a process, thread or an open file.
The system interprets handles differently depending on the context in which they are being used. In the context of processes: handle 0xA, might point to an open file in process 1 but point to a registry key in process 2. It is also possible for a handle to exist in one context but not in another.
Handles must be used within the context in which they are obtained.
Handles are not the real pointers to the resources. They are an abstraction.
This way the system can handle the complexities of the real pointers transparently to the handle users.
Windows definition for a handle is a union structure, HANDLE_TABLE_ENTRY.
The context in which the handle is used determines the definition that applies.
Note: The HANDLE_TABLE_ENTRY is an undocumented windows structure.
windows 11 23H2 Handle_Table_Entry x64
union _HANDLE_TABLE_ENTRY
{
volatile long long VolatileLowValue;
long long LowValue;
struct
{
struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable;
long long HighValue;
union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry;
struct _EXHANDLE LeafHandleValue;
};
long long RefCountField;
unsigned long long Unlocked:1;
unsigned long long RefCnt:16;
unsigned long long Attributes:3;
struct
{
unsigned long long ObjectPointerBits:44;
unsigned long GrantedAccessBits:25;
unsigned long NoRightsUpgrade:1;
unsigned long Spare1:6;
};
unsigned long Spare2;
};
In the context of a windows process, the Handle_Table_Entry is as follows:
windows 11 23H2 Process Context Handle_Table_Entry x64
struct _HANDLE_TABLE_ENTRY
{
unsigned long long ObjectPointerBits:44;
unsigned long GrantedAccessBits:25;
unsigned long NoRightsUpgrade:1;
unsigned long Spare1:6;
};
ObjectPointerBits
pointer to the opened object.
The object can be any of the system objects, drivers, files, registry keys, processes...
GrantedAccessBits
Permissions granted to the process on the object.
Each process has a structure called the HANDLE_TABLE.
The handle_table has the information necessary to access all the handles available to a process.
This includes handles opened while the process was in kernel mode such as when invoking a system call or a driver IOCTL.
Note: The ObjectPointerBits
field of a processes' EPROCESS structure points to the HANDLE_TABLE.
Note: The HANDLE_TABLE is an undocumented windows structure and its definition varies between windows releases.
windows 11 23H2 Handle_Table x64
struct HANDLE_TABLE
{
unsigned long NextHandleNeedingPool;
long ExtraInfoPages;
volatile unsigned long long TableCode;
struct _EPROCESS* QuotaProcess;
struct _LIST_ENTRY HandleTableList;
unsigned long UniqueProcessId;
union
{
unsigned long Flags;
struct
{
unsigned char StrictFIFO:1;
unsigned char EnableHandleExceptions:1;
unsigned char Rundown:1;
unsigned char Duplicated:1;
unsigned char RaiseUMExceptionOnInvalidHandleClose:1;
};
};
struct _EX_PUSH_LOCK HandleContentionEvent;
struct _EX_PUSH_LOCK HandleTableLock;
union
{
struct _HANDLE_TABLE_FREE_LIST FreeLists[1];
struct
{
unsigned char ActualEntry[32];
struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo;
};
};
};
Windows maintains a circular doubly linked list of all HANDLE_TABLES.
The linked list includes handle tables from all windows processes including the kernel itself.
The HandleTableList
field of the HANDLE_TABLE is a pointer to the next entry in the circular doubly linked lists.
Note:
The HandleTableList
does not point to the beginning of the next HANDLE_TABLE structure in the list.
It instead points to the HandleTableList
member.
The HANDLE_TABLE pointer can be calculated by subtracting the offset of HandleTableList
from the pointer.
nextTable = (HANDLE_TABLE*)( (unsigned long long)currentTable->HandleTableList - offsetof(HANDLE_TABLE, HandleTableList) );
The TableCode
field of the structure holds two key information:
Layout
:: bits 0 and 1 of the tableCode
int layout = HandleTable.TableCode & 3;
A page pointer
:: bits 2 to 63 for x64 and bits 2 to 31 for x86.
HANDLE_TABLE_ENTRIES* entries = HandleTable.TableCode ^ 3;
The page pointer is page aligned.
The TableCode layout section gives more information about the pointer section.
The layout can be either, 0, 1 or 2.
For each case, the number of handle entries varies as well as the interpretation of the pointer.
Case 0
In this case, the TableCode page pointer points to a page containing the handle_table_entries.
HANDLE_TABLE -> [Object_Handles_Page]
Each page is 4096 bytes and each Handle_Table_Entry is typically 16 bytes.
The first entry in the page is normally invalid.
with this the maximum number of handles that can be stored in the page is:
x64 : max_handles = (4096 / 16) - 1 = 255
x86 : max_handles = (4096 / 8) - 1 = 511
If a process opens more handles than can't fit in the current page, the process is automatically elevated to case 1.
Case 1
The TableCode page pointer points to a page containing a list of page pointers.
Each of the nested pointers point to a page containing the handle_table_entries.
HANDLE_TABLE -> [pointers_page] -> [Object_Handles_Page]
Case 2
The nesting level increases
HANDLE_TABLE -> [pointers_page] -> [pointers_page] -> [Object_Handles_Page]