1.什么是内核对象
内核对象是由内核分配的一块内存,它只能被内核访问。
这块内存存储着一个保存着内核对象信息的数据结构。
2.如何访问内核对象
应用程序不能直接访问内核对象,只能通过Windows API间接访问和操作内核对象。
那些用于操作内核对象的API会返回一个标识内核对象的句柄,应用程序通过该句柄和API函数来操作和访问内核对象。
3.内核对象的数据成员
既然内核对象是数据结构,那么它一定相应的数据成员。
(1) 引用计数:
它用来记录有多少个应用程序在使用该内核对象,它该值为0时系统会销毁该内核对象。
内核对象是由内核所有,换句话说即使创建内核对象的进程已经结束了,所创建的内核对象仍可能存在。
内核对象可以被多个进程同时使用,句柄值通常会不一样,但是引用的内容是同一块。因此内核对象的存在时间通常会比进程的存在时间长。内核对象中有一个值用来保存当前使用该内核对象的进程数,这就是使用计数。这样可以确保在没有进程引用该对象时系统中不保留任何内核对象。
(2) 安全:
它用来描述谁拥有这个对象(通常是创建者),以及哪些用户和组可以访问或不可访问该对象
在操作对象前进程必须先提交操作请求,该请求能否成功是则“安全”来控制的。
对象的创建都可以通过“安全”来阻止那些未授权的用户访问该对象。
3.判断一个对象是否是内核对象
判断一个对象是否是内核对象的最简单方法是查看创建该对象的函数
几乎所有的内核对象的创建函数都有一个 用于描述安全属性的“安全描述符”作为参数。如下:
HANDLE CreateFileMapping(
HANDLE hFile,
PSECURITY_ATTRIBUTES psa,
DWORD flProtect,
DWORD dwMaximumSizeHigh,
DWORD dwMaximumSizeLow,
PCTSTR pszName);
参数psa就是来控制访问权限的。当访问一个现有的文件映射内核对象时需要设定要对对象执行什么操作,如: HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping"),其中FILE_MAP_READ说明是对内核对象执行读操作,"MyFileMapping"是内核对象的名字。
注:当查看创建内核对象的函数返回值时,必须格外小心。特别要注意的是,当调用CreateFile函数失败时,返回值为INVALID_HANDLE_VALUE(值为-1)。其他的内核函数返回值为NULL。
4.进程的句柄表(A Process' Kernel Object Handle Table)
进程在初始化后,系统会为它分配一个句柄表,记录它所用到的内核对象
Index | Pointer to Kernel Object Memory Block | Access Mask (DWORD of Flag Bits) | Flags |
1 | 0x???????? | 0x???????? | 0x???????? |
2 | 0x???????? | 0x???????? | 0x???????? |
… | … | … | … |
句柄可能理解为上图中的Index,在进程句柄表中它相当于内核对象的指针的索引。
句柄是进程相关的不同的进程不能简单的传递句柄来共享内核对象,因为句柄它只是一个索引,它的值在不同进程中所索引的内核对象不同 所以不是简单的通过传递句柄来共享内核对象。
5.关闭内核对象
无论是不是你创建的内核对象,当你使用完该对象后都应该通过CloseHandle来关闭内核对象
BOOL CloseHandle(HANDLE hobject);
//Set handle to NULL after CloseHandle().
CloseHandle 会对对象的引用计数减1 然后清空进程句柄表中相应的项。
当引用计数减到0时系统会销毁该内核对象。
6.不关闭内核对象?
如果不关闭内核对象则内核对象的引用计数不会减少,也就不会被系统销毁,从而会产生内存泄露。
幸好当程序结束后系统会保证释放程序所占用的所有资源,系统遍历当前进程的句柄表,然后对句柄表中的逐个项调用CloseHandle
#include <windows.h>
#include <stdio.h>
int main(void)
{
SECURITY_ATTRIBUTESsa;
ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));
sa.nLength =sizeof(SECURITY_ATTRIBUTES);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; //使得创建的句柄可继承
STARTUPINFO si;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
PROCESS_INFORMATION pi;
ZeroMemory(&pi, sizeof(pi));
HANDLE h = CreateMutex(&sa, FALSE, "fuck");
if(GetLastError()== ERROR_ALREADY_EXISTS)
{
printf("run error\n");
system("pause");
return 0;
}
else{
//使得创建的新进程 RunAsDate.EXE 继承父进程中可以继承的内核句柄,如前面创建的互斥句柄 h
CreateProcess(NULL,"RunAsDate", NULL, NULL,TRUE, 0, NULL, NULL, &si, &pi);
printf("run\n");
system("pause");
return 0;
}
}
ps:运行上面的程序之后,会创建一个RunAsDate进程,把父进程关闭,再运行父进程会提示run error,表明我们的互斥对象已经被RunAsDate子进程继承,并且关闭父进程,对于互斥对象没有影响.
简单的说,这个函数获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本.
1. 简单区分内核对象和其他对象的方法:创建需要安全信息的多半是内核对象。
2. 每个进程有一个内核对象表,表的每一项是一个简单结构,包括真实内核对象地址和访问权限等。用户代码持有的内核对象句柄其实是对象表中对应项的索引。因此如果CloseHandle关闭一个对象后没有清空变量,且在对象表的同样位置恰好又创建了一个新的内核对象,对之前没清空的无效变量的访问会造成bug。(比如对同一个句柄多调用了一次CloseHandle导致另一个内核对象被关闭。)
3. 进程退出时,会释放各种内存、内核对象、GDI对象等。
4. 跨进程使用内核对象的理由:跨进程传输:用文件映像对象实现共享内存、邮件槽和命名管道实现数据通信、信号量和互斥量进行同步等。
5. 跨进程使用内核对象的三种方式:对象句柄继承、命名内核对象、复制对象句柄。
6. 对象句柄继承:创建内核对象的时候可以指定SECURITY_ATTRIBUTES. bInheritHandle表示可继承(任何时候可以使用SetHandleInformation修改可继承性等属性),创建子进程时指定CreateProcess的参数bInheritHandles为TRUE,则子进程从父进程的对象表中拷贝所有可继承的对象到自己的对象表的相同表位置中(因为是新创建的所以对应的索引号还未被使用)(并增加引用计数),因为表项结构被完全拷贝且内核对象实际地址在地址空间后2G的内核地址段中,所以拷贝过来的表项完全有效,进而父子进程的可继承内核对象的句柄值完全相同,于是只要以任何方式将要继承的对象的句柄值跨进程交给子进程(创建子进程时的命令行参数、环境变量、共享内存、消息等手段),则后者可以使用。
7. 命名内核对象:要访问已经存在的命名内核对象,可以使用CreateXXX或者OpenXXX,后者在对象不存在的时候返回NULL。如果打开了一个已经存在的命名对象,在打开时为API指定的对象名以外的参数被忽略。注意,一个进程打开同一对象两次,除了增加引用两次外,返回的句柄值是不同的,需要分别关闭一次,即打开和关闭完全对称(很合理的行为)。在Vista及以上的系统,对象名可以包括在命名空间下,避免被低授权用户访问。
8. 复制对象句柄:DuplicateHandle。