第三章 内核对象
本章内容
3.1 何为内核对象
3.2 进程内核对象句柄表
3.3 跨进程边界共享内核对象
内核对象(kernel object)和句柄(handle)
内核对象用于管理进程,线程和文件等诸多类的大量资源。
3.1 何为内核对象
(内核对象的定义见下一段)
系统处理几种类型的内核对象, 令牌对象(access token), 事件对象, 文件对象, 文件映像对象,
I/O完成端口对象, 作业对象, 邮件槽(mailslot)对象 ,互斥量(mutex)对象, 管道(pipe)对象,
进程对象,信号量(semaphore)对象,线程对象,可等待的计时器(waitable timer)对象
线程池工厂(thread pool worker factory)对象等。
可以使用WinObj查看一个包含所有内核对象的列表。 (https://docs.microsoft.com/zh-cn/sysinternals/downloads/winobj)
每个内核对象都是一个内存块,他们由操作系统内核分配,并且其内存块的直接访问权限只能由操作系统内核。这个内存块是一个数据结构,维护着与对象相关的信息。
少数成员(安全描述和使用计数器)是所有对象都有的。大部分成员都不是共有的。
由于内核对象的数据结构只能由操作系统内核访问,所以应用程序不能在内存中定位这些数据结构并直接修改其内容。只能利用windows提供的函数来操作这些结构。
调用一个会创建内核对象的函数以后,会返回该对象的句柄handle,他表示了所创建的对象。
在32位系统中handle是一个32位值,在64位系统中是一个64位值。
这些句柄大部分是进程相关的, 不能直接将一个句柄传递给另一个进程的某一个线程来访问,可能会失败。
3.1.1 引用计数
内核对象的所有者是操作系统内核,而非进程。也就是我们的进程调用一个函数创建内核对象,然后进程终止,内核对象并不一定销毁。
大部分时候会销毁,但如果还有其他进程正在使用我们创建的内核对象,在其他进程停止使用前,它们是不会销毁的。
内核对象的生命周期可能长于创建他的那个进程。
操作系统根据每个内核对象的引用计数器来了解当前有多少个进程正在使用一个特定的内核对象。初始创建以后为1, 每被一个进程使用增1,反之减1.
对象使用计数器变为0以后,操作系统内核就会销毁该对象。
3.1.2 内核对象的安全性
内核对象使用一个安全描述符 (security descriptor ,SD)来保护。通常描述了,对象的拥有者,哪些组和用户被运行访问或使用此对象;哪些组和用户拒绝访问此对象。
安全描述符在编写服务程序(service)的时候使用。
创建内核对象的函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数,例如CreateFileMapping
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateFileMappingW(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCWSTR lpName
);
大部分应用程序只是为这个参数传入NULL,这样创建的内核对象具有默认的安全性,具体哪些默认的安全性取决于当前进程的安全令牌(security token)
SECURITY_ATTRIBUTES结构定义如下
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;
lpSecurityDescriptor成员和安全性相关。 以下例子可以对自己创建的内核对象加以访问限制
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa); // used for versioning
sa.lpSecurityDescriptor = pSD; // address of an initialized SD
sa.bInheritHandle = FALSE; // Discussed later
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, &sa,
PAGE_READWRITE, 0, 1024, TEXT("MyFileMapping"));
如果想访问一个现有的文件映像内核对象,以便从中读取数据,可以这样使用
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE,
TEXT("MyFileMapping"));
如果允许访问则返回一个句柄,否则返回NULL
调用GetLastError 返回值 5(ERROR_ACCESS_DENIED)
如果利用返回的句柄调用某个api而这个api需要的权限不仅仅是FILE_MAP_READ,同样会发送拒绝访问 错误。
老版本的应用程序不能在vista以上工作,很多时候是因为没有考虑这些安全性。
例如一个app在启动时要从一个注册表子项读取数据。正确的做法是调用RegOpenKeyEx,向其传入KEY_QUERY_VALUE,从而指定查询子项数据的权限。
如果在非管理员用户运行app的时候调用RegOpenKeyEx,并传入KEY_ALL_ACCESS则会失败。
Vista以上还需要考虑UAC。
GDI对象非内核对象。 判断是否内核对象主要看创建内核对象的函数是否有SECURITY_ATTRIBUTES参数。
例如CreateIcon 创建非内核对象
WINUSERAPI
HICON
WINAPI
CreateIcon(
_In_opt_ HINSTANCE hInstance,
_In_ int nWidth,
_In_ int nHeight,
_In_ BYTE cPlanes,
_In_ BYTE cBitsPixel,
_In_ CONST BYTE *lpbANDbits,
_In_ CONST BYTE *lpbXORbits);
3.2 进程内核对象句柄表
一个进程在初始化时,系统将为其分配一个句柄表(handle table)。这个句柄表仅供内核对象使用,不适用于GDI对象。
作者认为优秀的Windows程序员,必须理解如何管理进程的句柄表。虽然MSDN上暂无文档可参考。
进程句柄表是一个由数据结构组成的数组。每个结构包含一个指向内核对象的指针,一个访问掩码和一些标志。
3.2.1 创建一个内核对象
一个进程首次初始化的时候,其句柄表为空。但进程内的一个线程调用一个函数(比如CreateFileMapping)创建内核对象时 ,系统内核将为这个对象分配一块内存。
然后内核扫描进程的句柄表,并查找一个空白的记录项(empty entry),内核在索引1的位置找到空白记录项,并初始化。
指针成员指向内核对象的数据结构的地址, 访问掩码被设置成拥有完全访问权限,标志也会被设置。
一些常用的创建内核对象的函数
WINBASEAPI
__out_opt
HANDLE
WINAPI
CreateThread(
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,
__in SIZE_T dwStackSize,
__in LPTHREAD_START_ROUTINE lpStartAddress,
__in_opt __deref __drv_aliasesMem LPVOID lpParameter,
__in DWORD dwCreationFlags,
__out_opt LPDWORD lpThreadId
);
WINBASEAPI
HANDLE
WINAPI
CreateFileW(
_In_ LPCWSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateFileMappingW(
_In_ HANDLE hFile,
_In_opt_ LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
_In_ DWORD flProtect,
_In_ DWORD dwMaximumSizeHigh,
_In_ DWORD dwMaximumSizeLow,
_In_opt_ LPCWSTR lpName
);
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateSemaphoreW(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCWSTR lpName
);
任何创建内核对象的函数都会返回一个与进程相关的句柄。该句柄可以供同一个进程中运行的所有线程使用。
系统用索引来表示内核对象的信息保存在进程的句柄表中的具体位置,要得到实际的索引值,句柄值实际应该除以4(或者右移2位,以忽略windows内部使用的最后两位)。
所以在调试应用程序查看内核对象句柄的实际值时,会看到8之类的很小的值。
调用一个函数时,如果他接受一个内核对象句柄作为参数,就必须把Cr