這里windows和Linxu系列的PID 管理方式有所不同,windows中進程的PID和句柄沒有本質區別,根據句柄索引對象和根據PID或者TID查找進程或者線程的步驟也是一樣的。
句柄是針對進程而言,也就是句柄一定所屬於某個進程,為某個進程私有的資源。句柄的分配來自於進程私有的句柄表。而進程PID和線程TID和句柄的分配原理是一樣的,但是來源就不同了。進程PID來源於系統中一張全局的句柄表PSpcidtable。從級別上來說Pspcidtable和進程對象eprcess中的objecttable是一樣的,都是HANDLE_TABLE類型,只是兩者指向的句柄表不同。還有一個區別就是進程內的句柄表項中所指的對象是對象頭,要找到對象體還需要加偏移,而pspcidtable中的句柄項直接指向對象體。
換句話說,系統分配句柄和進程或者線程ID的源頭都是源自於句柄表。細心的人可能會發現進程ID其實都是4的倍數。這里的原因在之前的文章中已經分析過,這里不再贅述,今天主要結合WRK源代碼分析下句柄表的工作模式以及通過句柄或者進程PID來查找相應對象的過程。
句柄表有三種:
- 單層句柄表
- 兩層句柄表
- 三層句柄表
初始狀態下,在進程創建之初,只有單層句柄表,隨着進程打開文件數的增加,句柄表慢慢擴張,待單張句柄表無法容納進程擁有的句柄數的時候,系統就擴張句柄表為兩層句柄表,句柄表的單位就是一個頁面即4KB,句柄表項都是_HANDLE_TABLE_ENTRY類型,每一個占用8個字節,所以一個頁面可以存放2^12/2^3=512個句柄項,今天我們主要以兩層句柄表為例說明問題。兩層句柄表的架構如下:

在兩層句柄表下,需要有一個頁面在存放句柄表頁面的地址,這點類似於系統中的頁表基址,不過這里要簡單的多。TableLevel2表示具體句柄表的索引表,其中的每一項都指向一張具體的表。所以這里可以存放4096/4=1024項,所以二級句柄表供可以存放1024*512個句柄。
前面又所說過,句柄索引是以4遞增的,那么每個句柄表的“長度”即從數字的層面上來看為512*4,而這里每個句柄或者PID都可以認為是給定的長度。那么要判斷這個長度的終點落在那個句柄表並找到對應的句柄值就需要做一下工作:
1、找到句柄表在TableLevel2中的索引。
2、找到句柄項在句柄表中的偏移。
要確定終點落在哪一個句柄表中,判斷索引值包含幾個句柄表的長度即可,對句柄表的長度取整。假設句柄表的長度是 length = 512 * 4 ,對應到TableLevel2上就是length對應sizeof(PHANDLE_VALUE_INC)即4個字節。從全局的角度來看實際上是在TableLevel2層每個字節對應i = length/sizeof(PHANDLE_VALUE_INC),用索引值index/i即為索引值在level2中對應的字節。
要確定句柄值具體在某一張句柄表中的偏移就比較容易了,讓index % length即可。
有了上面的介紹,下面我們看下windows中是如何根據PID查找到具體的進程對象的呢?
我們從PsLookupProcessByProcessId函數入手,這是內核中的一個導出函數,可以根據進程ID找到具體的EProcess對象
NTSTATUS PsLookupProcessByProcessId( __in HANDLE ProcessId, __deref_out PEPROCESS *Process ) /*++ Routine Description: This function accepts the process id of a process and returns a referenced pointer to the process. Arguments: ProcessId - Specifies the Process ID of the process. Process - Returns a referenced pointer to the process specified by the process id. Return Value: STATUS_SUCCESS - A process was located based on the contents of the process id. STATUS_INVALID_PARAMETER - The process was not found. --*/ { PHANDLE_TABLE_ENTRY CidEntry; PEPROCESS lProcess; PETHREAD CurrentThread; NTSTATUS Status; PAGED_CODE(); Status = STATUS_INVALID_PARAMETER; CurrentThread = PsGetCurrentThread (); KeEnterCriticalRegionThread (&CurrentThread->Tcb); CidEntry = ExMapHandleToPointer(PspCidTable, ProcessId); if (CidEntry != NULL) { lProcess = (PEPROCESS)CidEntry->Object; if (lProcess->Pcb.Header.Type == ProcessObject && lProcess->GrantedAccess != 0) { if (ObReferenceObjectSafe(lProcess)) { *Process = lProcess; Status = STATUS_SUCCESS; } } ExUnlockHandleTableEntry(PspCidTable, CidEntry); } KeLeaveCriticalRegionThread (&CurrentThread->Tcb); return Status; } //此函數中最關鍵的一步是根據ExMapHandleToPointer函數獲取到了進程PID對應的HANDLE_TABLE_ENTRY結構,進入此函數看下: NTKERNELAPI PHANDLE_TABLE_ENTRY ExMapHandleToPointer ( __in PHANDLE_TABLE HandleTable, __in HANDLE Handle ) /*++ Routine Description: This function maps a handle to a pointer to a handle table entry. If the map operation is successful then the handle table entry is locked when we return. Arguments: HandleTable - Supplies a pointer to a handle table. Handle - Supplies the handle to be mapped to a handle entry. Return Value: If the handle was successfully mapped to a pointer to a handle entry, then the address of the handle table entry is returned as the function value with the entry locked. Otherwise, a value of NULL is returned. --*/ { EXHANDLE LocalHandle; PHANDLE_TABLE_ENTRY HandleTableEntry; PAGED_CODE(); LocalHandle.GenericHandleOverlay = Handle; if ((LocalHandle.Index & (LOWLEVEL_COUNT - 1)) == 0) { return NULL; } // // Translate the input handle to a handle table entry and make // sure it is a valid handle. // HandleTableEntry = ExpLookupHandleTableEntry( HandleTable, LocalHandle ); if ((HandleTableEntry == NULL) || !ExpLockHandleTableEntry( HandleTable, HandleTableEntry)) { // // If we are debugging handle operations then save away the details // if (HandleTable->DebugInfo != NULL) { ExpUpdateDebugInfo(HandleTable, PsGetCurrentThread (), Handle, HANDLE_TRACE_DB_BADREF); } return NULL; } // // Return the locked valid handle table entry // return HandleTableEntry; }
此函數做一些簡單變量的初始化后,就調用了ExpLookupHandleTableEntry函數,參數是進程對應的句柄表結構HandleTable和PID對應的handle
PHANDLE_TABLE_ENTRY ExpLookupHandleTableEntry ( IN PHANDLE_TABLE HandleTable, IN EXHANDLE tHandle ) /*++ Routine Description: This routine looks up and returns the table entry for the specified handle value. Arguments: HandleTable - Supplies the handle table being queried tHandle - Supplies the handle value being queried Return Value: Returns a pointer to the corresponding table entry for the input handle. Or NULL if the handle value is invalid (i.e., too large for the tables current allocation. --*/ { ULONG_PTR i,j,k; ULONG_PTR CapturedTable; ULONG TableLevel; PHANDLE_TABLE_ENTRY Entry = NULL; EXHANDLE Handle; PUCHAR TableLevel1; PUCHAR TableLevel2; PUCHAR TableLevel3; ULONG_PTR MaxHandle; PAGED_CODE(); // // Extract the handle index // Handle = tHandle; Handle.TagBits = 0; MaxHandle = *(volatile ULONG *) &HandleTable->NextHandleNeedingPool;//取下一頁的起始索引,在一層句柄表的情況下,這里作為最大索引限制 // // See if this can be a valid handle given the table levels. // if (Handle.Value >= MaxHandle) { return NULL; } // // Now fetch the table address and level bits. We must preserve the // ordering here. // CapturedTable = *(volatile ULONG_PTR *) &HandleTable->TableCode;//獲取句柄表的最高層頁面的地址。 // // we need to capture the current table. This routine is lock free // so another thread may change the table at HandleTable->TableCode // TableLevel = (ULONG)(CapturedTable & LEVEL_CODE_MASK);//取句柄表級數 CapturedTable = CapturedTable - TableLevel;//這里是為何???? // // The lookup code depends on number of levels we have // switch (TableLevel) { case 0: // // We have a simple index into the array, for a single level // handle table // TableLevel1 = (PUCHAR) CapturedTable; // // The index for this level is already scaled by a factor of 4. Take advantage of this // //注意這里tablelevel1已經不是PHANDLE_TABLE_ENTRY類型而是puchar類型,即相當於一個uchar類型的指針,那么把tablelevel1作為數組地址取值,一個元素就占用一個字節 //因為PID 本身是作為4增長,而一個表項是占用8字節,所以只需要讓PID *2即可 Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[Handle.Value * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)]; break; case 1: // // we have a 2 level handle table. We need to get the upper index // and lower index into the array // TableLevel2 = (PUCHAR) CapturedTable; i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);//這里取i為實際的PID 在某個句柄表中的偏移 Handle.Value -= i;//然后用值減去這個偏移,得到的應該是整數個句柄表的最后一位PID j = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));//用value除以一個句柄表容納的 TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];//這里就取二級句柄表的地址 Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)]; break; case 2: // // We have here a three level handle table. // TableLevel3 = (PUCHAR) CapturedTable; i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC); Handle.Value -= i; k = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY)); j = k % (MIDLEVEL_COUNT * sizeof (PHANDLE_TABLE_ENTRY)); k -= j; k /= MIDLEVEL_COUNT; TableLevel2 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel3[k]; TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j]; Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)]; break; default : _assume (0); } return Entry; }
這里MaxHandle = *(volatile ULONG *) &HandleTable->NextHandleNeedingPool;是取當前進程句柄表中的下次擴展時候用到的首個句柄索引,也就是現有的句柄索引肯定都要小於此值,那么根據這個來判斷句柄值是否合法。
然后 CapturedTable = *(volatile ULONG_PTR *) &HandleTable->TableCode獲取最高層索引表的地址。前面也提到了句柄表有單層,雙層,三層。
TableLevel = (ULONG)(CapturedTable & LEVEL_CODE_MASK);就是取TableCode最后兩位,即句柄表的級數
CapturedTable = CapturedTable - TableLevel;屏蔽最低兩位,此時CapturedTable 即指向一級句柄表
然后就開始了一個switch選擇,就是根據具體的層數來做具體的工作。單層情況下很簡單,直接取數組的相應表項即可。這里我們重點分析下case 1的情況即兩層句柄表的情況。
TableLevel2 = (PUCHAR) CapturedTable; i = Handle.Value % (LOWLEVEL_COUNT * HANDLE_VALUE_INC);//這里取i為實際的PID 在某個句柄表中的偏移 Handle.Value -= i;//然后用值減去這個偏移,得到的應該是整數個句柄表的最后一位PID j = Handle.Value / ((LOWLEVEL_COUNT * HANDLE_VALUE_INC) / sizeof (PHANDLE_TABLE_ENTRY));//用value除以一個句柄表容納的句柄值的長度用於獲取在上一級表中的偏移,因為要獲得char類型,所以有了后面的sizeof (PHANDLE_TABLE_ENTRY)
TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];//這里就取二級句柄表的地址 Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)]; break;
這里TableLevel2就指向句柄索引表且是PUCHAR類型,這種類型有一個好處就是按單字節增長。
i就是取得PID值終點所在的句柄表中的偏移。
下面讓Handle.value-i就是去掉偏移后,那么剩下的應該就是整數個句柄表的長度即n*512*4
然后根據前面我們的分析,j就是hanle.value在TableLevel2中對應的字節數。
TableLevel1 = (PUCHAR) *(PHANDLE_TABLE_ENTRY *) &TableLevel2[j];//這里就取二級句柄表的地址
由於數組從0開始,那么TableLevel2[j]表示整數個句柄表的下一個句柄表對應的地址起始處,為什么這么說呢?因為TableLevel2是Puchar類型,TableLevel2[j]只能取一個字節,所以這里做了&操作取出地址,然后轉化成
PHANDLE_TABLE_ENTRY *,之后再取值就可以取出句柄表的地址,然后再次轉化成PUCHAR.
到此已經找到了句柄索引對應的句柄表的起始地址,下面就該按照偏移找到具體的句柄表項了。
Entry = (PHANDLE_TABLE_ENTRY) &TableLevel1[i * (sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)];
現在TableLevel1已經指向了具體的句柄表,一個HANDLE_TABLE_ENTRY 8個字節,而句柄值是按照4遞增,也即是平均來看,句柄索引距離1對應sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC)=2字節
那么i自然就對應於i*sizeof (HANDLE_TABLE_ENTRY) / HANDLE_VALUE_INC),注意這里並不是從0開始的,因為對底層句柄表的索引0有其他的用途,所以這里我們也不用-1.找到HANDLE_TABLE_ENTRY的起始地址后,轉化成PHANDLE_TABLE_ENTRY即可。。
這樣就找到了HANDLE_TABLE_ENTRY,然后HANDLE_TABLE_ENTRY的第一項就是一個指向對應對象的指針,直接取出即找到對應的對象。
lProcess = (PEPROCESS)CidEntry->Object;
注意:
這里entry->object是直接取到了對象體,但是在普通的句柄情況下,這里只能取到對象頭,最后根據相應的偏移取到對象體,關於對象參考另一篇文章!還有前面紅色字體那個地方,筆者的確不曉得什么意思,知道的老師們還請多多指點!
三層句柄表和兩層原理類似,只是多了一此索引,這里就不重復分析。只是這里開發者的想法的確是難懂,筆者描述的也不知道是否清楚,但是我實在是不能想到更好的辦法去描述!!