【内核原理与实现 009】句柄结构体,PspCidTable,PID/CID的由来
一、句柄结构体
typedef struct _HANDLE_TABLE_ENTRY {
union {
PVOID Object; // 指向句柄所代表的对象
ULONG ObAttributes; // 最低三位有特别含义,参见
// OBJ_HANDLE_ATTRIBUTES 宏定义
PHANDLE_TABLE_ENTRY_INFO InfoTable; // 各个句柄表页面的第一个表项
// 使用此成员指向一张表
ULONG_PTR Value;
};
union {
union {
ACCESS_MASK GrantedAccess; // 访问掩码
struct { // 当NtGlobalFlag 中包含
// FLG_KERNEL_STACK_TRACE_DB 标记时使用
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};
LONG NextFreeTableEntry; // 空闲时表示下一个空闲句柄索引
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
低32位:
Object & ~7 是句柄对应的内核对象的地址,Object 的低3位有特殊用途:第0位 OBJ_PROTECT_CLOSE 表示是否允许关闭句柄,但通过宏 ObpEncodeProtectClose 和 ObpGetHandleAttributes 将此位转移到 GrantedAccess 成员中,然后第0位被用作表示句柄表项锁,置1表示此句柄表项被锁住;第1位 OBJ_INHERIT 表示是否拷贝父进程的句柄表;第2位 OBJ_AUDIT_OBJECT_CLOSE 表示关闭该对象时是否产生审计事件。
高32位:
当句柄不指向内核对象时,NextFreeTableEntry 表示句柄表中下一个空闲句柄位置,也可以理解为该进程创建新句柄时的句柄值;当句柄指向内核对象时,GrantedAccess 表示该句柄的访问掩码。
上一篇博客研究句柄表初始化时,已经分析过 ExpAllocateHandleTable 函数,最低层句柄表第0项是特殊项,其 NextFreeTableEntry 成员的值是 EX_ADDITIONAL_INFO_SIGNATURE,即−2,而在wrk源码中,第0项的 InfoTable 成员初始化为0(书上说第0项的 InfoTable 指向 HANDLE_TABLE_ENTRY_INFO,我没找到依据).
二、通过句柄找内核对象
此部分内容来自书上P133页,书上内容有错,我已纠正。
有效句柄有四种情况:
- -1,表示当前进程
- -2,表示当前线程
- 负值,其绝对值为 System 进程的句柄表(ObpKernelHandleTable)中的索引
- 不超过 226 的正值,表示在进程句柄表中的索引
解释下第四种(书上错印成226),为什么是2的26次方?因为一个进程最多只能有 224 个句柄,而句柄值需要乘以4,所以最大的句柄值就是 226 了。
解析句柄的函数是 ObReferenceObjectByHandle ,它可能会调用我在上一篇博客分析过的 ExpLookupHandleTableEntry 函数。
ObReferenceObjectByHandle
NTSTATUS
ObReferenceObjectByHandle (
__in HANDLE Handle,
__in ACCESS_MASK DesiredAccess,
__in_opt POBJECT_TYPE ObjectType,
__in KPROCESSOR_MODE AccessMode,
__out PVOID *Object,
__out_opt POBJECT_HANDLE_INFORMATION HandleInformation
)
/*++
Routine Description:
Given a handle to an object this routine returns a pointer
to the body of the object with proper ref counts
通过句柄找内核对象
Arguments:
Handle - Supplies a handle to the object being referenced. It can
also be the result of NtCurrentProcess or NtCurrentThread
句柄,用于查找内核对象
DesiredAccess - Supplies the access being requested by the caller
调用者指定的内核对象访问权限
ObjectType - Optionally supplies the type of the object we
are expecting
可选地指定内核对象类型
AccessMode - Supplies the processor mode of the access
内核/用户
Object - Receives a pointer to the object body if the operation
is successful
内核对象输出参数
HandleInformation - Optionally receives information regarding the
input handle.
Return Value:
An appropriate NTSTATUS value
--*/
{
ACCESS_MASK GrantedAccess;
PHANDLE_TABLE HandleTable;
POBJECT_HEADER ObjectHeader;
PHANDLE_TABLE_ENTRY ObjectTableEntry;
PEPROCESS Process;
NTSTATUS Status;
PETHREAD Thread;
ObpValidateIrql("ObReferenceObjectByHandle");
Thread = PsGetCurrentThread ();
*Object = NULL;
//
// Check is this handle is a kernel handle or one of the two builtin pseudo handles
// 检查是不是-1或-2
//
if ((LONG)(ULONG_PTR) Handle < 0) {
//
// If the handle is equal to the current process handle and the object
// type is NULL or type process, then attempt to translate a handle to
// the current process. Otherwise, check if the handle is the current
// thread handle.
//
if (Handle == NtCurrentProcess()) {
// 如果句柄值是-1,即当前进程
// 检查类型是否合法
if ((ObjectType == PsProcessType) || (ObjectType == NULL)) {
// 获取当前进程内核对象
Process = PsGetCurrentProcessByThread(Thread);
GrantedAccess = Process->GrantedAccess;
// SeComputeDeniedAccesses 检查权限,返回0表示权限检查通过;
// 如果是内核调用,则不需要检查权限
if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
(AccessMode == KernelMode)) {
// 获取对象头
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Process);
// 如果指定了输出参数 HandleInformation ,就填入一些信息
if (ARGUMENT_PRESENT(HandleInformation)) {
HandleInformation->GrantedAccess = GrantedAccess;
HandleInformation->HandleAttributes = 0;
}
// 指针计数加一
ObpIncrPointerCount(ObjectHeader);
// 返回进程内核对象
*Object = Process;
ASSERT( *Object != NULL );
Status = STATUS_SUCCESS;
} else {
Status = STATUS_ACCESS_DENIED;
}
} else {
Status = STATUS_OBJECT_TYPE_MISMATCH;
}
return Status;
//
// If the handle is equal to the current thread handle and the object
// type is NULL or type thread, then attempt to translate a handle to
// the current thread. Otherwise, the we'll try and translate the
// handle
//
} else if (Handle == NtCurrentThread()) {
// 如果句柄值是-2,即当前线程,和进程的做法类似
if ((ObjectType == PsThreadType) || (ObjectType == NULL)) {
GrantedAccess = Thread->GrantedAccess;
if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
(AccessMode == KernelMode)) {
ObjectHeader = OBJECT_TO_OBJECT_HEADER(Thread);
if (ARGUMENT_PRESENT(HandleInformation)) {
HandleInformation->GrantedAccess = GrantedAccess;
HandleInformation->HandleAttributes = 0;
}
ObpIncrPointerCount(ObjectHeader);
*Object = Thread;
ASSERT( *Object != NULL );
Status = STATUS_SUCCESS;
} else {
Status = STATUS_ACCESS_DENIED;
}
} else {
Status = STATUS_OBJECT_TYPE_MISMATCH;
}
return Status;
} else if (AccessMode == KernelMode) {
//
// Make the handle look like a regular handle
//
// 如果句柄是负数且是内核模式,就取句柄绝对值
Handle = DecodeKernelHandle( Handle );
//
// The global kernel handle table
// 句柄值是负数,已经改成其绝对值了,从内核句柄表 ObpKernelHandleTable 里找
HandleTable = ObpKernelHandleTable;
} else {
//
// The previous mode was user for this kernel handle value. Reject it here.
//
// 用户模式不能访问系统进程句柄,返回非法句柄错误
return STATUS_INVALID_HANDLE;
}
} else {
// 获取当前进程的句柄表
HandleTable = PsGetCurrentProcessByThread(Thread)->ObjectTable;
}
// 此时 HandleTable 可能是普通进程句柄表,也可能是系统进程句柄表(ObpKernelHandleTable)
ASSERT(HandleTable != NULL);
//
// Protect this thread from being suspended while we hold the handle table entry lock
// 加锁的目的是防止我们操作句柄表时线程被挂起
//
KeEnterCriticalRegionThread(&Thread->Tcb);
//
// Translate the specified handle to an object table index.
// 通过句柄表找句柄表项,ExMapHandleToPointerEx 底层调用了 ExpLookupHandleTableEntry
//
ObjectTableEntry = ExMapHandleToPointerEx ( HandleTable, Handle, AccessMode );
//
// Make sure the object table entry really does exist
// 确保句柄表项确实存在
if (ObjectTableEntry != NULL) {
// 获取内核对象头
ObjectHeader = (POBJECT_HEADER)(((ULONG_PTR)(ObjectTableEntry->Object)) & ~OBJ_HANDLE_ATTRIBUTES);
//
// Optimize for a successful reference by bringing the object header
// into the cache exclusive.
// 优化:将内核对象弄到 cache 里,在wrk里,这个宏没啥用,但在后续windows里是一种优化手段
// 参考 _m_prefetch
ReadForWriteAccess(ObjectHeader);
// 检查类型是否合法
if ((ObjectHeader->Type == ObjectType) || (ObjectType == NULL)) {
// NtGlobalFlag 和调试有关,我不懂,2020年12月8日23:32:49
#if i386
if (NtGlobalFlag & FLG_KERNEL_STACK_TRACE_DB) {
GrantedAccess = ObpTranslateGrantedAccessIndex( ObjectTableEntry->GrantedAccessIndex );
} else {
GrantedAccess = ObpDecodeGrantedAccess(ObjectTableEntry->GrantedAccess);
}
#else
GrantedAccess = ObpDecodeGrantedAccess(ObjectTableEntry->GrantedAccess);
#endif // i386
// SeComputeDeniedAccesses 返回0表示权限检查通过
if ((SeComputeDeniedAccesses(GrantedAccess, DesiredAccess) == 0) ||
(AccessMode == KernelMode)) {
PHANDLE_TABLE_ENTRY_INFO ObjectInfo;
ObjectInfo = ExGetHandleInfo(HandleTable, Handle, TRUE);
//
// Access to the object is allowed. Return the handle
// information is requested, increment the object
// pointer count, unlock the handle table and return
// a success status.
//
// 允许访问内核对象。如果需要,返回句柄信息。增加指针引用计数,
// 解锁句柄表并返回成功。
//
// Note that this is the only successful return path
// out of this routine if the user did not specify
// the current process or current thread in the input
// handle.
//
// 除了获取当前进程线程以外,这是唯一的成功返回执行路径。
//
// 可选地返回句柄信息
if (ARGUMENT_PRESENT(HandleInformation)) {
HandleInformation->GrantedAccess = GrantedAccess;
HandleInformation->HandleAttributes = ObpGetHandleAttributes(ObjectTableEntry);
}
//
// If this object was audited when it was opened, it may
// be necessary to generate an audit now. Check the audit
// mask that was saved when the handle was created.
//
// 如果内核对象审计选项开启,可能需要生成审计。检查句柄创建时保存的审计掩码
//
// It is safe to do this check in a non-atomic fashion,
// because bits will never be added to this mask once it is
// created.
//
// 这里无须原子操作,因为掩码从初始化后就不变了
//
if ( (ObjectTableEntry->ObAttributes & OBJ_AUDIT_OBJECT_CLOSE) &&
(ObjectInfo != NULL) &&
(ObjectInfo->AuditMask != 0) &&
(DesiredAccess != 0)) {
ObpAuditObjectAccess( Handle, ObjectInfo, &ObjectHeader->Type->Name, DesiredAccess );
}
ObpIncrPointerCount(ObjectHeader);
ExUnlockHandleTableEntry( HandleTable, ObjectTableEntry );
KeLeaveCriticalRegionThread(&Thread->Tcb);
*Object = &ObjectHeader->Body;
ASSERT( *Object != NULL );
return STATUS_SUCCESS;
} else {
Status = STATUS_ACCESS_DENIED;
}
} else {
Status = STATUS_OBJECT_TYPE_MISMATCH;
}
ExUnlockHandleTableEntry( HandleTable, ObjectTableEntry );
} else {
Status = STATUS_INVALID_HANDLE;
}
KeLeaveCriticalRegionThread(&Thread->Tcb);
return Status;
}
三、插入句柄表
将一个对象插入到句柄表中的函数是ObInsertObject,其代码位于base\ntos\ob
obinsert.c 文件中。它首先对参数做各种检查,然后调用ObpCreateHandle 函数,为要插入
的对象创建一个句柄。ObpCreateHandle 函数又调用ExCreateHandle 来创建一个句柄表项,
并填充新建的表项。这些函数的代码比较直截了当,这里不再赘述。
四、对象计数
另外值得一提的是对象的引用计数。正如2.5.1 节的最后部分所讲,对象的引用有两
种来源:第一,在内核中直接通过对象地址来引用,这是通过ObReferenceObjectByPointer
来记录一次新的引用;第二,通过句柄来引用对象,这是由ObpIncrementHandleCount 函
数来检查并记录一次句柄引用。对于一个句柄,它的生命周期从被插入到句柄表中开始,
一直到它被关闭,在此过程中,对象的引用计数中包含有它的一份引用。在WRK 代码中,
我们可以看到,ObpCreateHandle 调用了ObpIncrementHandleCount,标志着新创建的句柄
引用了该对象;ObpCloseHandle 函数调用了ObpCloseHandleTableEntry,而后者又进一步
调用了ObpDecrementHandleCount,标志着一个句柄结束了相应对象的引用。另外,在一
个句柄上调用了ObReferenceObjectByHandle 函数以后,若该对象指针不再使用,则必须
调用ObDereferenceObject 函数。
五、进程ID、线程ID
创建进程和线程时,会调用 ExCreateHandle 在全局句柄表 PspCidTable 中创建一个句柄,句柄会指向相应的线程或进程内核对象,而句柄值就是进程ID或线程ID,这也就解释了为什么PID都是4的倍数,也确保了PID/CID的唯一性。
之前提到过,PspCidTable 是唯一一张遵循 FIFO 规则的句柄表,举例说明,PID=100的线程被杀死后,要等后续的空闲句柄项都用过一遍,它才能重新使用,就像一个队列一样。
PsLookupProcessThreadByCid、PsLookupProcessByProcessId 和PsLookupThreadByThreadId 函数可以很方便的从 PspCidTable 里找到进程/线程。