《Windows核心编程》读书笔记三 内核对象
2017-09-07 23:53
316 查看
第三章 内核对象
本章内容
3.1 何为内核对象
3.2 进程内核对象句柄表
3.3 跨进程边界共享内核对象
内核对象(kernel object)和句柄(handle)
内核对象用于管理进程,线程和文件等诸多类的大量资源。
系统处理几种类型的内核对象, 令牌对象(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位值。
这些句柄大部分是进程相关的, 不能直接将一个句柄传递给另一个进程的某一个线程来访问,可能会失败。
大部分时候会销毁,但如果还有其他进程正在使用我们创建的内核对象,在其他进程停止使用前,它们是不会销毁的。
内核对象的生命周期可能长于创建他的那个进程。
操作系统根据每个内核对象的引用计数器来了解当前有多少个进程正在使用一个特定的内核对象。初始创建以后为1, 每被一个进程使用增1,反之减1.
对象使用计数器变为0以后,操作系统内核就会销毁该对象。
安全描述符在编写服务程序(service)的时候使用。
创建内核对象的函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数,例如CreateFileMapping
大部分应用程序只是为这个参数传入NULL,这样创建的内核对象具有默认的安全性,具体哪些默认的安全性取决于当前进程的安全令牌(security token)
SECURITY_ATTRIBUTES结构定义如下
lpSecurityDescriptor成员和安全性相关。 以下例子可以对自己创建的内核对象加以访问限制
如果想访问一个现有的文件映像内核对象,以便从中读取数据,可以这样使用
调用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 创建非内核对象
作者认为优秀的Windows程序员,必须理解如何管理进程的句柄表。虽然MSDN上暂无文档可参考。
进程句柄表是一个由数据结构组成的数组。每个结构包含一个指向内核对象的指针,一个访问掩码和一些标志。
然后内核扫描进程的句柄表,并查找一个空白的记录项(empty entry),内核在索引1的位置找到空白记录项,并初始化。
指针成员指向内核对象的数据结构的地址, 访问掩码被设置成拥有完全访问权限,标志也会被设置。
一些常用的创建内核对象的函数
任何创建内核对象的函数都会返回一个与进程相关的句柄。该句柄可以供同一个进程中运行的所有线程使用。
系统用索引来表示内核对象的信息保存在进程的句柄表中的具体位置,要得到实际的索引值,句柄值实际应该除以4(或者右移2位,以忽略windows内部使用的最后两位)。
所以在调试应用程序查看内核对象句柄的实际值时,会看到8之类的很小的值。
调用一个函数时,如果他接受一个内核对象句柄作为参数,就必须把Create*函数返回的值传给它。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址,然后以一种恰当的方式来操纵内核对象的数据结构。
如果传入的句柄无效,GetLastError 会返回 ERROR_INVALID_HANDLE. 句柄值是进程相关的,关联进程句柄表中的一条索引值。如果被其他进程所使用,结果是未定义的。
函数创建内核对象,如果失败,通常返回的句柄值是NULL。 然而有几个函数返回-1 INVALID_HANDLE_VALUE 例如CreateFile。
以下几个是不正确的例子
该函数首先查询主进程的句柄表,验证“传给函数的句柄值”表示的是“进程确实有访问权限的一个对象”。如果句柄有效,系统将获得内核对象的数据结构的地址,并将结构中的“使用计数器”成员递减,如果计数器为0,内核对象将被销毁,并从内存中释放。
如果传递给CloseHandle是一个无效的句柄,可能情况 1 CloseHandle 返回FALSE 而GetLastError返回 ERROR_INVALID_HANDLE
如果进程正在调试将抛出0xc0000008 异常(指定了无效的句柄)
CloseHandle以后当前进程的句柄表中关于该句柄的记录会被消除,但是实际上该句柄不一定会被销毁(别的进程可能正在使用)。只有当其引用计数器为0才会被完全销毁。
所以在CloseHandle以后请将当前代码中保存句柄的变量设置为NULL
当进程终止时,操作系统自动扫描该进程的句柄表。如果有任何有效的记录,操作系统会为我们关闭这些对象句柄。
可以用任务管理器来检测进程的句柄数
如果Handles列显示的数字持续增长,可以使用Sysinternals提供的Process Explorer工具进行调查
(https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer)
可以根据下表框的列表查找可能leak的内核对象。因为只有一个命名对象能够创建,其他尝试只会单纯打开那个内核对象的实例。
a.利用文件映像对象,可以在同一台机器上运行的两个进程之间共享数据块。
b. 借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送数据块。
c. 互斥量,信号量和事件允许不同进程的线程同步执行。例如一个应用程序可能需要完成某一个任务以后向另一个应用程序发出通知。
为了操作系统的安全,OS将内核对象设计成进程相关。
windows中有3中机制允许进程共享内核对象:使用对象句柄继承;为对象命名;复制对象句柄。
步骤
1)父进程创建句柄的时候必须向系统指出该对象是可以继承的。(这里指的是对象句柄的继承)
父进程需要初始化SECURITY_ATTRIBUTES结构 再将其传递给Create函数。 以下代码创建了一个可继承的互斥量对象。
句柄表中每个记录项都有一个指明句柄是否可以继承的标志位。如果在创建句柄时传入的SECURITY_ATTRIBUTES结构参数为NULL返回的句柄是不可继承的,这个标志位为0.
将bInheritHandle成员设置为TRUE,将导致这个标志位被设为1.
以上句柄表中,索引号为3的内核对象可以被继承。索引号为1的内核对象不可继承,索引号为2的内核对象不可用。
2)为了使对象句柄继承,接下来是生成子进程。通过CreateProcess函数完成
bInheritHandles参数,通常情况传递FALSE(不希望子进程继承父进程句柄表中的“可继承句柄”)
如果传递TRUE,子进程就会继承父进程的“可继承的句柄值”。创建进程以后系统先建一张空的句柄表,然后遍历其父进程句柄表并复制所有可继承属性的内核对象。
并且复制项的位置和他在父进程表中的位置是完全一样的。主进程中的Handle值在子进程中仍然可用。
同时会增加被复制的内核对象的引用计数器。
表3是一个子进程的句柄表
1,2 项未初始化不可用
第三项是从父进程复制过来的内核对象,地址完全相同索引号也一致,并且标志是可继承。
内核对象的内容被保存在内核地址空间中--系统上所有运行的进程共享这个空间。(内核模式分区)
在32位系统下 是 0x8000 0000 到 0xFFFF FFFF之间。 对于64位系统,则是 0x0000 0400 0000 0000 到 0xFFFF FFFF FFFF FFFF之间。
子进程同样调用CreateProcess创建自己的孙进程,并将bInheritHandles参数设置为TRUE,孙进程也会继承该内核对象。并且3个进程的内核对象在句柄表中的内容和索引号一致。以此类推
对象继承只发生在创建进程的同时,如果子进程创建以后父进程又创建了新的内核对象则该对象并不被继承。
另外子进程本身并不会被通知从父进程那里继承了哪些内核对象(需要自行写代码通知)
通常用命令行参数传递。子进程的初始化代码将解析命令行并提取值。(因为内核对象具有相同的索引值和指针值)可以直接使用父句柄传递的句柄值
也可以等子进程创建完毕主线程发送一个消息给子进程中的一个线程处理(WaitForInputIdle)
另一种方式是在主进程所在的环境块添加环境变量设置继承句柄值,子进程会继承父进程的环境变量。通过调用GetEnvironmentVariable来获取继承到的内核对象的句柄值。
参考子进程继承父控制台的特例
(https://support.microsoft.com/en-us/help/190351/how-to-spawn-console-processes-with-redirected-standard-handles)
hObject一个有效的句柄
dwMask更改那些标志
dwFlags要设置的值。
例如
以下例子会触发一个异常(在调试器下)在运行模式下CloseHandle返回FALSE
关闭保护
获取内核对象的标志信息
获取一个内核对象是否允许被继承的例子
所有这些Create函数有一个Name参数,传入NULL表示匿名内核对象。可以使用继承技术
或者使用DuplicateHandle来实现进程间的对象共享。
所有的命名内核对象共享一个命名空间。以下例子CreateSemaphore函数会返回NULL
通过命名对象共享的一个例子
在进程A中创建了一个名字为“JeffMutex”的互斥量对象
HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
然后通过某进程生成了进程B(B不是A的子进程)在B进程中调用
HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
系统找到内核表中的JeffMutex判断类型,进行安全检查。都通过就会将记录赋值到进程B中的句柄表内,并且引用计数器+1
否则返回NULL
(可以采用CreateMutexEx设置信号量的访问权限。)
通过名称来实现内核对象共享时,进程B调用CreateMutex时,会向安全函数传递安全信息。如果已经存在一个指定名称的对象,这些参数就会被忽略。函数并不知道自己是创建了一个新的内核对象还是打开一个已有的。
可以调用GetLastError判断刚才是创建了一个还是打开了一个现有的。
运行以下代码的两个实例并查看运行结果
为了实现具名内核对象的共享,还可以使用一系列Open*函数
以上函数原型一致,最后一个参数指向内核对象的名称。并且该参数不能为NULL
如果在系统内核表中无法搜索到同名的内核对象则返回NULL GetLastError返回2 (ERROR_FILE_NOT_FOUND)
如果找到同名的内核对象但类型不同,返回NULL GetLastError返回6(ERROR_INVALID_HANDLE)
如果同名且类型相同,系统会检查访问权限。如果允许访问该内核对象,则会更新主调进程的句柄表,并使对象的使用计数器递增。
如果bInHeritHandle参数传入TRUE,那么返回的句柄就是“可继承”的
Create*如果不存在则创建, Open*如果不存在则返回NULL。
为了避免两个互不相干的程序冲突建议使用GUID来创建具名内核对象。
一个利用具名内核对象来防止应用程序创建多个实例的例子。
该程序只会运行一个实例。
每个客户端会话(Client session)都有自己的命名空间。对于两个会话正在运行同一个程序的时候,这样的安排可以避免彼此之间的干扰--一个会话不会访问另一个会话的对象,即使对象的名称相同。
关于会话和不同应用程序之间通信的问题参考 Impact of Session 0 Isolation on Services and Drivers in Windows Vista
(https://msdn.microsoft.com/en-us/library/windows/hardware/dn653293(v=vs.85).aspx)
因为系统服务默认启动与Session 0 ,而用户的应用会在一个新的Session中启动。如果和系统服务间的通信不在一个命名空间中就会可能出问题。
可以通过ProcessIdToSessonId函数知道我们的应用程序在哪个Terminal Services会话中运行。
一个服务(system service)的命名内核对象始终位于全局命名空间。默认情况下应用程序的命名内核对象在会话的命名空间内。
可以强制把命名对象放入全局命名空间"Global\"
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName"));
也可以显式把内核对象放入当前会话的命名空间 加上"Local\"前缀
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));
Global 和 Local是命名对象的保留字。除非强制一个特定的命名空间,否则不应在对象名中使用。 Session也是保留字,但是不能在当前Session中创建创建另外一个Session前缀的对象。GetLastError返回ERROR_ACCESS_DENIED
(保留关键字都是区分大小写的)
研究以下问题
1)如何创建一个边界
2)如何将对应于本地管理组(Local Administrators)的安全描述符(Security identifier, SID)和边界关联
3)如何创建或打开其名称被用作互斥量内核对象前缀的一个专有命名空间。
边界描述符会获得一个名称, 而且还会与一个特权用户组的SID相关联。 Windows就可以确保在用户隶属于这个权限时(SID),以其身份创建的应用程序才能在此SID对应的边界条件中创建相同的命名空间,从而访问在这个边界中创建的,以专有命名空间的名称作为前缀的内核对象。
如果由于SID泄漏,一个低权限的而已程序试图创建相同的边界描述符,那么当其试图创建或打开一个高权限账户保护的专有命名空间时,调用就会失败GetLastError返回
ERROR_ACCESS_DENIED.
原始代码 Singleton.cpp
运行结果
CheckInstances函数的几个步骤。
1)创建边界描述符
注意该函数的返回类型虽然是HANDLE,但是并非是一个内核对象的句柄。而是一个指针,指向了用户模式的结构,结构体中保护了边界的定义。
应该调用DeleteBoundaryDescriptor释放 (CloseHandle会失败)
2) 将一个特权用户组的SID与边界描述符关联起来:
在本例中创建了基于Local Administrator组的SID,并使用CreateWellKnownSid创建SID的描述符
3)创建专有命名空间
边界描述符添加的SID决定了谁能进入边界并创建命名空间。
是通过ConvertStringSecurityDescriptorToSecurityDescriptor创建的 关于安全描述符的语法接口参考MSDN
(https://msdn.microsoft.com/en-us/library/aa374928.aspx)
如果试图创建一个已有的命名空间,CreatePrivateNamespace将返回NULL, GetLastError将返回ERROR_ALREADY_EXISTS
此时调用OpenPrivateNamespace来打开命名空间
注意CreatePrivateNamespace和 OpenPrivateNamespace返回的伪HANDLE并非内核句柄, 调用ClosePrivateNamespace来关闭伪句柄。
4)进程终止前调用 DeleteBoundaryDescriptor 关闭边界
3.3.6 复制对象句柄
跨进程共享内核对象的最后一招是使用DuplicateHandle函数
获得一个进程句柄表中的记录项,在另一个进程的句柄表中创建这个记录项的一个副本。
参数1 和3 是进程内核对象的句柄。并且这两个句柄必须相对于调用DuplicateHandle函数的进程(hSourceProcessHandle 也可以调用DuplicateHandle)。 如果传递非进程句柄则会失败。
第二个参数是指向任何类型的内核对象的句柄。但是他的值一定不能和调用DuplicateHandle函数的那个进程相关。 必须和hSourceProcessHandle的这个进程有关。
函数最终会将源进程中的句柄信息复制到hTargetProcessHandle所表示的进程的句柄表中。
第四个参数是一个HANDLE指针,用于传递复制以后获得的句柄值。
后3个参数指定这个内核对象在目标进程中所对应的句柄表项,应该使用何种访问掩码和继承标志。 dwOptions可以为0 或者 DUPLICATE_SAME_ACCESS(保留同样的掩码) 和 DUPLICATE_CLOSE_SOURCE(关闭源进程中的句柄)
使用DuplicateHandle函数和继承有一个问题,目标进程不知道他现在能访问一个新的内核对象。必须通过某种进程间通信来通知目标进程。
一个DuplicateHandle的例子 两个进程互相拷贝
在本例中DuplicateHandle返回的句柄相对于进程T,因此绝对不要在进程S中释放此句柄 例如 CloseHandle(hObjInProcessT);
另一个例子在同一个进程中执行DuplicateHandle拷贝一个副本内核对象。注意创建和释放的周期
本章内容
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之类的很小的值。
调用一个函数时,如果他接受一个内核对象句柄作为参数,就必须把Create*函数返回的值传给它。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址,然后以一种恰当的方式来操纵内核对象的数据结构。
如果传入的句柄无效,GetLastError 会返回 ERROR_INVALID_HANDLE. 句柄值是进程相关的,关联进程句柄表中的一条索引值。如果被其他进程所使用,结果是未定义的。
函数创建内核对象,如果失败,通常返回的句柄值是NULL。 然而有几个函数返回-1 INVALID_HANDLE_VALUE 例如CreateFile。
以下几个是不正确的例子
HANDLE hMutex = CreateMutex(...); if (hMutex == INVALID_HANDLE_VALUE){ // we will never execute this code because // CreateMutex returns NULL if it fails. } HANDLE hFile = CreateFile(...); if (hFile == NULL) { // we will never execute this code because CreateFile // returns INVALID_HANDLE_VALUE (-1) if it fails. }
3.2.2 关闭内核对象
BOOL CloseHandle(HANDLE hobject);该函数首先查询主进程的句柄表,验证“传给函数的句柄值”表示的是“进程确实有访问权限的一个对象”。如果句柄有效,系统将获得内核对象的数据结构的地址,并将结构中的“使用计数器”成员递减,如果计数器为0,内核对象将被销毁,并从内存中释放。
如果传递给CloseHandle是一个无效的句柄,可能情况 1 CloseHandle 返回FALSE 而GetLastError返回 ERROR_INVALID_HANDLE
如果进程正在调试将抛出0xc0000008 异常(指定了无效的句柄)
CloseHandle以后当前进程的句柄表中关于该句柄的记录会被消除,但是实际上该句柄不一定会被销毁(别的进程可能正在使用)。只有当其引用计数器为0才会被完全销毁。
所以在CloseHandle以后请将当前代码中保存句柄的变量设置为NULL
当进程终止时,操作系统自动扫描该进程的句柄表。如果有任何有效的记录,操作系统会为我们关闭这些对象句柄。
可以用任务管理器来检测进程的句柄数
如果Handles列显示的数字持续增长,可以使用Sysinternals提供的Process Explorer工具进行调查
(https://docs.microsoft.com/zh-cn/sysinternals/downloads/process-explorer)
可以根据下表框的列表查找可能leak的内核对象。因为只有一个命名对象能够创建,其他尝试只会单纯打开那个内核对象的实例。
3.3 跨进程边界共享内核对象
需要跨进程共享的内核对象a.利用文件映像对象,可以在同一台机器上运行的两个进程之间共享数据块。
b. 借助邮件槽和命名管道,在网络中的不同计算机上运行的进程可以相互发送数据块。
c. 互斥量,信号量和事件允许不同进程的线程同步执行。例如一个应用程序可能需要完成某一个任务以后向另一个应用程序发出通知。
为了操作系统的安全,OS将内核对象设计成进程相关。
windows中有3中机制允许进程共享内核对象:使用对象句柄继承;为对象命名;复制对象句柄。
3.3.1 使用对象句柄继承
只有在进程之间有一个父-子关系的时候,才可以使用对象句柄继承。 父进程有一个或多个内核对象句柄可以使用,而且父进程创建一个子进程,并允许子进程访问父进程的内核对象。步骤
1)父进程创建句柄的时候必须向系统指出该对象是可以继承的。(这里指的是对象句柄的继承)
父进程需要初始化SECURITY_ATTRIBUTES结构 再将其传递给Create函数。 以下代码创建了一个可继承的互斥量对象。
SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; // Make the returned handle inheritable. HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
句柄表中每个记录项都有一个指明句柄是否可以继承的标志位。如果在创建句柄时传入的SECURITY_ATTRIBUTES结构参数为NULL返回的句柄是不可继承的,这个标志位为0.
将bInheritHandle成员设置为TRUE,将导致这个标志位被设为1.
以上句柄表中,索引号为3的内核对象可以被继承。索引号为1的内核对象不可继承,索引号为2的内核对象不可用。
2)为了使对象句柄继承,接下来是生成子进程。通过CreateProcess函数完成
WINBASEAPI BOOL WINAPI CreateProcessW( _In_opt_ LPCWSTR lpApplicationName, _Inout_opt_ LPWSTR lpCommandLine, _In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes, _In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes, _In_ BOOL bInheritHandles, _In_ DWORD dwCreationFlags, _In_opt_ LPVOID lpEnvironment, _In_opt_ LPCWSTR lpCurrentDirectory, _In_ LPSTARTUPINFOW lpStartupInfo, _Out_ LPPROCESS_INFORMATION lpProcessInformation );
bInheritHandles参数,通常情况传递FALSE(不希望子进程继承父进程句柄表中的“可继承句柄”)
如果传递TRUE,子进程就会继承父进程的“可继承的句柄值”。创建进程以后系统先建一张空的句柄表,然后遍历其父进程句柄表并复制所有可继承属性的内核对象。
并且复制项的位置和他在父进程表中的位置是完全一样的。主进程中的Handle值在子进程中仍然可用。
同时会增加被复制的内核对象的引用计数器。
表3是一个子进程的句柄表
1,2 项未初始化不可用
第三项是从父进程复制过来的内核对象,地址完全相同索引号也一致,并且标志是可继承。
内核对象的内容被保存在内核地址空间中--系统上所有运行的进程共享这个空间。(内核模式分区)
在32位系统下 是 0x8000 0000 到 0xFFFF FFFF之间。 对于64位系统,则是 0x0000 0400 0000 0000 到 0xFFFF FFFF FFFF FFFF之间。
子进程同样调用CreateProcess创建自己的孙进程,并将bInheritHandles参数设置为TRUE,孙进程也会继承该内核对象。并且3个进程的内核对象在句柄表中的内容和索引号一致。以此类推
对象继承只发生在创建进程的同时,如果子进程创建以后父进程又创建了新的内核对象则该对象并不被继承。
另外子进程本身并不会被通知从父进程那里继承了哪些内核对象(需要自行写代码通知)
通常用命令行参数传递。子进程的初始化代码将解析命令行并提取值。(因为内核对象具有相同的索引值和指针值)可以直接使用父句柄传递的句柄值
也可以等子进程创建完毕主线程发送一个消息给子进程中的一个线程处理(WaitForInputIdle)
另一种方式是在主进程所在的环境块添加环境变量设置继承句柄值,子进程会继承父进程的环境变量。通过调用GetEnvironmentVariable来获取继承到的内核对象的句柄值。
参考子进程继承父控制台的特例
(https://support.microsoft.com/en-us/help/190351/how-to-spawn-console-processes-with-redirected-standard-handles)
3.3.2 改变句柄的标志
SetHandleInformation可以改变内核对象句柄的继承标志。WINBASEAPI BOOL WINAPI SetHandleInformation( _In_ HANDLE hObject, _In_ DWORD dwMask, _In_ DWORD dwFlags );
hObject一个有效的句柄
dwMask更改那些标志
#define HANDLE_FLAG_INHERIT 0x00000001 #define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
dwFlags要设置的值。
例如
// open the inherit flag SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT); // clos the inherit flag SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);HANDLE_FLAG_PROTECT_FROM_CLOSE告知系统不允许关闭句柄
以下例子会触发一个异常(在调试器下)在运行模式下CloseHandle返回FALSE
SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE); CloseHandle(hObj);
关闭保护
SetHandleInformation(hObj, HANDLE_FLAG_PROTECT_FROM_CLOSE, 0);
获取内核对象的标志信息
WINBASEAPI BOOL WINAPI GetHandleInformation( _In_ HANDLE hObject, _Out_ LPDWORD lpdwFlags );
获取一个内核对象是否允许被继承的例子
DWORD dwFlags; GetHandleInformation(hObj, &dwFlags); BOOL fHandleIsInheritable = (0 != (dwFlags & HANDLE_FLAG_INHERIT));
3.3.3 为对象命名
垮进程共享内核对象的第二个方法是为对象命名。许多内核对象都可以命名。WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes,
_In_ BOOL bInitialOwner,
_In_opt_ LPCWSTR lpName
);
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateEventW(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_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
);
WINBASEAPI
_Ret_maybenull_
HANDLE
WINAPI
CreateWaitableTimerW(
_In_opt_ LPSECURITY_ATTRIBUTES lpTimerAttributes,
_In_ BOOL bManualReset,
_In_opt_ LPCWSTR lpTimerName
);
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
CreateJobObjectW(
_In_opt_ LPSECURITY_ATTRIBUTES lpJobAttributes,
_In_opt_ LPCWSTR lpName
);
所有这些Create函数有一个Name参数,传入NULL表示匿名内核对象。可以使用继承技术
或者使用DuplicateHandle来实现进程间的对象共享。
所有的命名内核对象共享一个命名空间。以下例子CreateSemaphore函数会返回NULL
通过命名对象共享的一个例子
在进程A中创建了一个名字为“JeffMutex”的互斥量对象
HANDLE hMutexProcessA = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
然后通过某进程生成了进程B(B不是A的子进程)在B进程中调用
HANDLE hMutexProcessB = CreateMutex(NULL, FALSE, TEXT("JeffMutex"));
系统找到内核表中的JeffMutex判断类型,进行安全检查。都通过就会将记录赋值到进程B中的句柄表内,并且引用计数器+1
否则返回NULL
(可以采用CreateMutexEx设置信号量的访问权限。)
通过名称来实现内核对象共享时,进程B调用CreateMutex时,会向安全函数传递安全信息。如果已经存在一个指定名称的对象,这些参数就会被忽略。函数并不知道自己是创建了一个新的内核对象还是打开一个已有的。
可以调用GetLastError判断刚才是创建了一个还是打开了一个现有的。
运行以下代码的两个实例并查看运行结果
int main(int argc, char* argv[]) { SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.lpSecurityDescriptor = NULL; sa.bInheritHandle = TRUE; HANDLE hMutex = CreateMutex(&sa, FALSE, TEXT("JeffMutex")); if (GetLastError() == ERROR_ALREADY_EXISTS) { //Opened a handle to an existing object. //sa.lpSecurityDescriptor and the second parameter // are ignored. printf("Opened a handle to an existing object.\n"); } else { //Created a brand new object. //sa.lpSecurityDescriptor and the second parameter //FALSE are used to construct the object. printf("Created a brand new object.\n"); } system("pause"); return 0; }
为了实现具名内核对象的共享,还可以使用一系列Open*函数
WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenMutexW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName ); WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenEventW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName ); WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenSemaphoreW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName ); WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenWaitableTimerW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpTimerName ); WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenFileMappingW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName ); WINBASEAPI _Ret_maybenull_ HANDLE WINAPI OpenJobObjectW( _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ LPCWSTR lpName );
以上函数原型一致,最后一个参数指向内核对象的名称。并且该参数不能为NULL
如果在系统内核表中无法搜索到同名的内核对象则返回NULL GetLastError返回2 (ERROR_FILE_NOT_FOUND)
如果找到同名的内核对象但类型不同,返回NULL GetLastError返回6(ERROR_INVALID_HANDLE)
如果同名且类型相同,系统会检查访问权限。如果允许访问该内核对象,则会更新主调进程的句柄表,并使对象的使用计数器递增。
如果bInHeritHandle参数传入TRUE,那么返回的句柄就是“可继承”的
Create*如果不存在则创建, Open*如果不存在则返回NULL。
为了避免两个互不相干的程序冲突建议使用GUID来创建具名内核对象。
一个利用具名内核对象来防止应用程序创建多个实例的例子。
int main(int argc, char* argv[]) { //// {89AE97EA-F13D-4104-B042-43C010033B48} HANDLE h = CreateMutex(NULL, FALSE, TEXT("{89AE97EA-F13D-4104-B042-43C010033B48}")); if (GetLastError() == ERROR_ALREADY_EXISTS) { // there is already an instance of this application running. // close the object and immediately return. CloseHandle(h); return (0); } system("pause"); CloseHandle(h); return 0; }
该程序只会运行一个实例。
3.3.4 终端服务命名空间
终端服务程序(service)除了有一全局的命名空间,所有客户端能访问的内核对象要放在这个命名空间中。每个客户端会话(Client session)都有自己的命名空间。对于两个会话正在运行同一个程序的时候,这样的安排可以避免彼此之间的干扰--一个会话不会访问另一个会话的对象,即使对象的名称相同。
关于会话和不同应用程序之间通信的问题参考 Impact of Session 0 Isolation on Services and Drivers in Windows Vista
(https://msdn.microsoft.com/en-us/library/windows/hardware/dn653293(v=vs.85).aspx)
因为系统服务默认启动与Session 0 ,而用户的应用会在一个新的Session中启动。如果和系统服务间的通信不在一个命名空间中就会可能出问题。
可以通过ProcessIdToSessonId函数知道我们的应用程序在哪个Terminal Services会话中运行。
int main(int argc, char* argv[]) { DWORD processID = GetCurrentProcessId(); DWORD sessionID; if (ProcessIdToSessionId(processID, &sessionID)){ printf("Process '%u' runs in Terminal Services session '%u'\n", processID, sessionID); } else{ // ProcessIdToSessionId might fail if you don't have enough rights // to access the process for which you pass the ID as parameter. // Notice that it is not the case here because we're using our own process ID. printf("Unable to get Terminal Services session ID for process '%u'\n", processID); } return 0; }
一个服务(system service)的命名内核对象始终位于全局命名空间。默认情况下应用程序的命名内核对象在会话的命名空间内。
可以强制把命名对象放入全局命名空间"Global\"
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Global\\MyName"));
也可以显式把内核对象放入当前会话的命名空间 加上"Local\"前缀
HANDLE h = CreateEvent(NULL, FALSE, FALSE, TEXT("Local\\MyName"));
Global 和 Local是命名对象的保留字。除非强制一个特定的命名空间,否则不应在对象名中使用。 Session也是保留字,但是不能在当前Session中创建创建另外一个Session前缀的对象。GetLastError返回ERROR_ACCESS_DENIED
(保留关键字都是区分大小写的)
3.3.5 专有命名空间
如果想确保我们的应用程序创建的内核对象名称永远不会和其他应用程序的名称冲突,或者想保护他们免遭劫持,可以定义一个自定义的前缀,并把他作为自己的专有命名空间使用,这和使用Global和Local前缀是相似的。 一个单例进程的例子,以一种更安全的方式来实现前面的例子研究以下问题
1)如何创建一个边界
2)如何将对应于本地管理组(Local Administrators)的安全描述符(Security identifier, SID)和边界关联
3)如何创建或打开其名称被用作互斥量内核对象前缀的一个专有命名空间。
边界描述符会获得一个名称, 而且还会与一个特权用户组的SID相关联。 Windows就可以确保在用户隶属于这个权限时(SID),以其身份创建的应用程序才能在此SID对应的边界条件中创建相同的命名空间,从而访问在这个边界中创建的,以专有命名空间的名称作为前缀的内核对象。
如果由于SID泄漏,一个低权限的而已程序试图创建相同的边界描述符,那么当其试图创建或打开一个高权限账户保护的专有命名空间时,调用就会失败GetLastError返回
ERROR_ACCESS_DENIED.
原始代码 Singleton.cpp
/* Module: Singleton.cpp Notices: Copyright (c) 2008 Jeffrey Richter & Christophe Nasarre */ #include "resource.h" #include "..\CommonFiles\CmnHdr.h" // common header for Windows Via C++ sample #include <windowsx.h> #include <Sddl.h> // for SID management #include <tchar.h> #include <strsafe.h> /////////////////////////////////////////////////////////////////////////////// // Main dialog HWND g_hDlg; // Mutex, boundary and namespace used to detect previous running instance HANDLE g_hSingleton = NULL; HANDLE g_hBoundary = NULL; HANDLE g_hNamespace = NULL; // Keep track whether or not the namespace wa created or open for clean-up BOOL g_bNamespaceOpened = FALSE; // Names of boundary and private namespace PCTSTR g_szBoundary = TEXT("3-Boundary"); PCTSTR g_szNamespace = TEXT("3-Namespace"); #define DETAILS_CTRL GetDlgItem(g_hDlg, IDC_EDIT_DETAILS) /////////////////////////////////////////////////////////////////////////////// // Adds a string to the "Details" edit control void AddText(PCTSTR pszFormat, ...) { va_list argList; va_start(argList, pszFormat); TCHAR sz[20 * 1024] ; Edit_GetText(DETAILS_CTRL, sz, _countof(sz)); _vstprintf_s( _tcschr(sz, TEXT('\0')), _countof(sz) - _tcslen(sz), pszFormat, argList); Edit_SetText(DETAILS_CTRL, sz); va_end(argList); } /////////////////////////////////////////////////////////////////////////////// void Dlg_OnCommand(HWND hwnd, int id, HWND hwndCtl, UINT codeNotify) { switch (id) { case IDOK: case IDCANCEL: // User has clicked on the exit button // Or dismissed the dialog with ESCAPE EndDialog(hwnd, id); break; } } /////////////////////////////////////////////////////////////////////////////// void CheckInstances() { // Create the boundary descriptor g_hBoundary = CreateBoundaryDescriptor(g_szBoundary, 0); // Create a SID corresponding to the Local Administrator group BYTE localAdminSID[SECURITY_MAX_SID_SIZE]; PSID pLocalAdminSID = &localAdminSID; DWORD cbSID = sizeof(localAdminSID); if (!CreateWellKnownSid( WinBuiltinAdministratorsSid, NULL, pLocalAdminSID, &cbSID) ) { AddText(TEXT("AddSIDToBoundaryDescriptor failed: %u\r\n"), GetLastError()); return; } // Associate the Local Admin SID to the boundary descriptor // --> only applications running under an administrator user // will be able to access the kernel objects in the same namespace if (!AddSIDToBoundaryDescriptor(&g_hBoundary, pLocalAdminSID)) { AddText(TEXT("AddSIDToBoundaryDescriptor failed: %u\r\n"), GetLastError()); return; } // Create the namespace for Local Administrators only SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = FALSE; if (!ConvertStringSecurityDescriptorToSecurityDescriptor( TEXT("D:(A;;GA;;;BA)"), SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) { AddText(TEXT("Security Descriptor creation failed: %u\r\n"), GetLastError()); return; } g_hNamespace = CreatePrivateNamespace(&sa, g_hBoundary, g_szNamespace); // Don't forget to release memory for the security descriptor LocalFree(sa.lpSecurityDescriptor); // Check the private namespace creation result DWORD dwLastError = GetLastError(); if (g_hNamespace == NULL) { // Nothing to do if access is denied // --> this code must run under a Local Administrator acount if (dwLastError == ERROR_ACCESS_DENIED) { AddText(TEXT("Access denied when create the namespace.\r\n")); AddText(TEXT(" You must be running as Administrator.\r\n\r\n")); return; } else { if (dwLastError == ERROR_ALREADY_EXISTS) { // if another instance has already created the namespace, // we need to open it instead. AddText(TEXT("CreatePrivateNamespace failed: %u\r\n"), dwLastError); g_hNamespace = OpenPrivateNamespace(g_hBoundary, g_szNamespace); if (g_hNamespace == NULL) { AddText(TEXT(" and OpenPrivateNamespace failed: %u\r\n"), dwLastError); return; } else { g_bNamespaceOpened = TRUE; AddText(TEXT(" but OpenPrivateNamespace succeeded\r\n\r\n")); } } else { AddText(TEXT("Unexpected error occured: %u\r\n\r\n"), dwLastError); return; } } } // Try to create the mutex object with a name // based on the private namespace TCHAR szMutexName[64]; StringCchPrintf(szMutexName, _countof(szMutexName), TEXT("%s\\%s"), g_szNamespace, TEXT("Singleton")); g_hSingleton = CreateMutex(NULL, FALSE, szMutexName); if (GetLastError() == ERROR_ALREADY_EXISTS) { // There is already an instance of this Singleton object AddText(TEXT("Another instance of Singleton is running:\r\n")); AddText(TEXT("--> Impossible to access application features.\r\n")); } else { // First time the Singleton object is created AddText(TEXT("First instance of Singleton:\r\n")); AddText(TEXT("--> Access application features now.\r\n")); } } /////////////////////////////////////////////////////////////////////////////// BOOL Dlg_OnInitDialog(HWND hwnd, HWND hwndFocus, LPARAM lParam) { chSETDLGICONS(hwnd, IDI_SINGLETON); // Keep track of the main dialog window handle g_hDlg = hwnd; // Check whether another instance is already running CheckInstances(); return TRUE; } /////////////////////////////////////////////////////////////////////////////// INT_PTR WINAPI Dlg_Proc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); } return FALSE; } /////////////////////////////////////////////////////////////////////////////// int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // Show main window DialogBox(hInstance, MAKEINTRESOURCE(IDD_SINGLETON), NULL, Dlg_Proc); // Don't forget to clean up and release kernel resource if (g_hSingleton != NULL) { CloseHandle(g_hSingleton); } if (g_hNamespace != NULL) { if (g_bNamespaceOpened) { // Open namespace ClosePrivateNamespace(g_hNamespace, 0); } else { ClosePrivateNamespace(g_hNamespace, PRIVATE_NAMESPACE_FLAG_DESTROY); } } if (g_hBoundary != NULL) { DeleteBoundaryDescriptor(g_hBoundary); } return 0; }
运行结果
CheckInstances函数的几个步骤。
1)创建边界描述符
// Create the boundary descriptor g_hBoundary = CreateBoundaryDescriptor(g_szBoundary, 0);
注意该函数的返回类型虽然是HANDLE,但是并非是一个内核对象的句柄。而是一个指针,指向了用户模式的结构,结构体中保护了边界的定义。
应该调用DeleteBoundaryDescriptor释放 (CloseHandle会失败)
2) 将一个特权用户组的SID与边界描述符关联起来:
// Associate the Local Admin SID to the boundary descriptor // --> only applications running under an administrator user // will be able to access the kernel objects in the same namespace if (!AddSIDToBoundaryDescriptor(&g_hBoundary, pLocalAdminSID)) { AddText(TEXT("AddSIDToBoundaryDescriptor failed: %u\r\n"), GetLastError()); return; }
在本例中创建了基于Local Administrator组的SID,并使用CreateWellKnownSid创建SID的描述符
3)创建专有命名空间
// Create the namespace for Local Administrators only SECURITY_ATTRIBUTES sa; sa.nLength = sizeof(sa); sa.bInheritHandle = FALSE; if (!ConvertStringSecurityDescriptorToSecurityDescriptor( TEXT("D:(A;;GA;;;BA)"), SDDL_REVISION_1, &sa.lpSecurityDescriptor, NULL)) { AddText(TEXT("Security Descriptor creation failed: %u\r\n"), GetLastError()); return; } g_hNamespace = CreatePrivateNamespace(&sa, g_hBoundary, g_szNamespace);
边界描述符添加的SID决定了谁能进入边界并创建命名空间。
是通过ConvertStringSecurityDescriptorToSecurityDescriptor创建的 关于安全描述符的语法接口参考MSDN
(https://msdn.microsoft.com/en-us/library/aa374928.aspx)
如果试图创建一个已有的命名空间,CreatePrivateNamespace将返回NULL, GetLastError将返回ERROR_ALREADY_EXISTS
此时调用OpenPrivateNamespace来打开命名空间
g_hNamespace = OpenPrivateNamespace(g_hBoundary, g_szNamespace);
注意CreatePrivateNamespace和 OpenPrivateNamespace返回的伪HANDLE并非内核句柄, 调用ClosePrivateNamespace来关闭伪句柄。
4)进程终止前调用 DeleteBoundaryDescriptor 关闭边界
3.3.6 复制对象句柄
跨进程共享内核对象的最后一招是使用DuplicateHandle函数
WINBASEAPI BOOL WINAPI DuplicateHandle( _In_ HANDLE hSourceProcessHandle, _In_ HANDLE hSourceHandle, _In_ HANDLE hTargetProcessHandle, _Outptr_ LPHANDLE lpTargetHandle, _In_ DWORD dwDesiredAccess, _In_ BOOL bInheritHandle, _In_ DWORD dwOptions );
获得一个进程句柄表中的记录项,在另一个进程的句柄表中创建这个记录项的一个副本。
参数1 和3 是进程内核对象的句柄。并且这两个句柄必须相对于调用DuplicateHandle函数的进程(hSourceProcessHandle 也可以调用DuplicateHandle)。 如果传递非进程句柄则会失败。
第二个参数是指向任何类型的内核对象的句柄。但是他的值一定不能和调用DuplicateHandle函数的那个进程相关。 必须和hSourceProcessHandle的这个进程有关。
函数最终会将源进程中的句柄信息复制到hTargetProcessHandle所表示的进程的句柄表中。
第四个参数是一个HANDLE指针,用于传递复制以后获得的句柄值。
后3个参数指定这个内核对象在目标进程中所对应的句柄表项,应该使用何种访问掩码和继承标志。 dwOptions可以为0 或者 DUPLICATE_SAME_ACCESS(保留同样的掩码) 和 DUPLICATE_CLOSE_SOURCE(关闭源进程中的句柄)
使用DuplicateHandle函数和继承有一个问题,目标进程不知道他现在能访问一个新的内核对象。必须通过某种进程间通信来通知目标进程。
一个DuplicateHandle的例子 两个进程互相拷贝
// All of the following code is excuted by process S. // Create a mutex object accessible by Process S. HANDLE hObjInProcessS = CreateMutex(NULL, FALSE, NULL); // Get a handle to Process T's kernel object. HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT); HANDLE hObjInProcessT; // An uninitialized handle relative to Process T. // Give Process T access to our mutex object. DuplicateHandle(GetCurrentProcess(), hObjInProcessS, hProcessT, &hObjInProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS); // Use some IPC mechanism to get the handle value of hObjInProcessS into Process T. // We no longer need to commicate with Process T. CloseHandle(hProcessT); // When Process S no longer needs to use the mutex, it should close it. CloseHandle(hObjInProcessS);
在本例中DuplicateHandle返回的句柄相对于进程T,因此绝对不要在进程S中释放此句柄 例如 CloseHandle(hObjInProcessT);
另一个例子在同一个进程中执行DuplicateHandle拷贝一个副本内核对象。注意创建和释放的周期
// Create a file-mapping object; the handle has read/write access. HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, 10240, NULL); // Create another handle to the file-mapping object; // the handle has read-only access. HANDLE hFileMapRO; //uninitialized. DuplicateHandle(GetCurrentProcess(), hFileMapRW, GetCurrentProcess(), &hFileMapRO, FILE_MAP_READ, FALSE, 0); // call the function that should only read from the file mapping. ReadFromTheFileMapping(hFileMapRO); // Close the read-only file-mapping object. CloseHandle(hFileMapRO); // We can still read/write the file-mapping object using hFileMapRW. // ... // When the main code doesn't access the file mapping anymore, // Close it. CloseHandle(hFileMapRW);
相关文章推荐
- 《Windows核心编程》读书笔记(六) 第9章 线程与内核对象的同步
- WINDOWS核心编程--读书笔记:第三章 内核对象
- 读书笔记----《windows核心编程》第三章 内核对象1(句柄与安全性)
- 《Windows核心编程 5th》读书笔记----第9章 用内核对象进行线程同步
- WINDOWS的内核对象——《windows核心编程》读书笔记
- windows核心编程--内核对象和句柄泄漏
- Windows核心编程 内核对象
- Windows核心编程--线程池内核对象触发调用函数
- Windows核心编程 内核对象
- 《Windows核心编程》——三 内核对象
- Windows核心编程:(一)内核对象
- windows程序设计 and windows核心编程(内核对象理论)
- Windows核心编程之3 内核对象
- 《Windows核心编程》读书笔记八 用户模式下的内核同步
- 【windows核心编程】 第三章 内核对象
- 《windows核心编程》学习笔记(一)内核对象
- 浅尝《Windows核心编程》之内核对象
- windows核心编程--内核对象和句柄泄漏
- 《Windows核心编程》---内核对象和进程基础
- windows程序设计 and windows核心编程(内核对象理论)