2021-03-06Win3200
请注意,本文编写于 633 天前,最后修改于 204 天前,其中某些信息可能已经过时。

目录


1. 什么是内核对象?

像进程、线程、文件、互斥体、事件等在内核都有一个对应的结构体,这些结构体由内核负责管理。我们管这样的对象叫做内核对象。
只要看到LPSECURITY_ATTRIBUTES那么一定是内核对象

内核对象

2. 如何管理内核对象?

都有对应的内核结构

管理内核对象

3. 每个进程都有一个句柄表

当一个进程被初始化时,系统要为它分配一个句柄表。该句柄表只用于内核对象,不用于用户对象或GDI对象。

它只是个数据结构的数组。每个结构都包含一个指向内核对象的指针、一个访问屏蔽和一些标志。

索引指向内核对象内存块的指针访问屏蔽(标志位的DWORD)标志(标志位的DWORD
10 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?
20 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?0 x ? ? ? ? ? ? ? ?
............

句柄实际上就是一道防火墙,把用户层和内核层隔离开
句柄表

4. 多进程共享一个内核对象

OpenProcess可以打开别人创建好的内核对象,句柄表是私有的,2是计数器,是根据调用A的次数来定的,如果要关闭,也必须要为0

共享一个内核对象

5. 关闭内核对象

无论怎样创建内核对象,都要向系统指明将通过调用CloseHandle来结束对该对象的操作:

BOOL CloseHandle(HANDLE hobj);

该函数首先检查调用进程的句柄表,以确保传递给它的索引(句柄)用于标识一个进程实际上无权访问的对象。如果该索引是有效的,那么系统就可以获得内核对象的数据结构的地址,并可确定该结构中的使用计数的数据成员。如果使用计数是0 ,该内核便从内存中撤消该内核对象。

在CloseHandle返回之前,它会清除进程的句柄表中的项目,该句柄现在对你的进程已经无效,不应该试图使用它。无论内核对象是否已经撤消,都会发生清除操作。当调用CloseHandle 函数之后,将不再拥有对内核对象的访问权,不过,如果该对象的使用计数没有递减为 0 ,那么该对象尚未被撤消。这没有问题,它只是意味着一个或多个其他进程正在使用该对象。当其他进程停止使用该对象时(通过调用CloseHandle),该对象将被撤消。

对于内核对象来说,系统将执行下列操作:当进程终止运行时,系统会自动扫描进程的句柄表。如果该表拥有任何无效项目(即在终止进程运行前没有关闭的对象),系统将关闭这些对象句柄。如果这些对象中的任何对象的使用计数降为0 ,那么内核便撤消该对象。

6. 句柄是否”可以”被继承

继承句柄

安全描述符用于描述谁创建了该对象,谁能够访问或使用该对象,谁无权访问该对象。(安全描述符通常在编写服务器应用程序时使用,如果你编写客户机端的应用程序,那么可以忽略内核对象的这个特性。)

用于创建内核对象的函数几乎都有一个指向SECURITY_ ATTRIBUTES结构的指针作为其参数。

c
HANDLE CreateFileMapping(
   HANDLE hFile.
   PSECURITY_ATTRIBUTES psa,
   DWORD flProtect,
   DWORD dwMaximumSizeHigh,
   DWORD dwMaximuniSizeLow,
   PCTSTR pszNarne);

大多数应用程序只是为该参数传递NULL ,这样就可以创建带有默认安全性的内核对象。(默认安全性意味着对象的管理小组的任何成员和对象的创建者都拥有对该对象的全部访问权,而其他所有人均无权访问该对象。

SECURITY_ ATTRIBUTES结构
可以指定一个SECURITY_ ATTRIBUTES结构,对它进行初始化,并为该参数传递该结构的地址。SECURITY_ATTRIBUTES结构类似下面的样子:

c
typedef struct_SECURITY_ATTRIBUTES
{
   DWORD nLength,
   LPVOID lpSecurityDescriptor;
   BOOL bInherttHandle;
}SECURITY_ATTRIBUTES;

该结构包含的与安全性有关的成员实际上只有一个,即lpSecurityDescriptor。

许多情况下,在不同进程中运行的线程需要共享内核对象。下面是为何需要共享的原因:

  • 文件映射对象使你能够在同一台机器上运行的两个进程之间共享数据块。

  • 邮箱和指定的管道使得应用程序能够在连网的不同机器上运行的进程之间发送数据块。

  • 互斥对象、信标和事件使得不同进程中的线程能够同步它们的连续运行,这与一个应用程序在完成某项任务时需要将情况通知另一个应用程序的情况相同。

跨越进程边界共享内核对象有三种方法:

  1. 对象句柄的继承性
  2. 命名对象
  3. 复制对象句柄

对象句柄的继承性

只有当进程具有父子关系时,才能使用对象句柄的继承性。

首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。(虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。)

若要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构并对它进行初始化,然后将该结构的地址传递给特定的Create***函数。下面的代码用于创建一个互斥对象,并将一个可继承的句柄返回给它:

c
//创建一个互斥量,返回一个可以继承的句柄
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecuntyDescriptor = NULL;
sa.bInheritHandle = TRUE;
HANDLE hMutex = CreateMutex(&sa, FALSE,NULL);

存放在进程句柄表项目中的标志。每个句柄表项目都有一个标志位,用来指明该句柄是否具有继承性。如果将bInheritHandle 成员置为TRUE ,那么该标志位将被置为1 。

索引内核对象内存块的指针访问屏蔽(标志位的DWORD)标志(标志位的DWORD )
10 x F 0 0 0 0 0 0 00 x ? ? ? ? ? ? ? ?0 x 0 0 0 0 0 0 0 0
20 x 0 0 0 0 0 0 0 0(无)(无)
30 x F 0 0 0 0 0 1 00 x ? ? ? ? ? ? ? ?0 x 0 0 0 0 0 0 0 1

让父进程生成子进程
使用对象句柄继承性时要执行的下一个步骤是让父进程生成子进程。这要使用CreateProcess函数来完成:

c
BOOL CreateProcess(
   PCTSTR pszApplicationName,
   PTSTR pszCommandLine,
   PSECURITY_ATTRIBUTES psaProcess,
   PSECURITY_ATTRIBUTES psaThread,
   BOOL bInheritHandles,
   DWORD fdwCreale,
   PVOIO pvEnvironment,
   PCTSTR pszCurDir,
   PSTARTUPINFO psiStartInfo,
   PPROCESS_INFORMATION ppiProcInfo);

bInheritHandle参数传递TRUE ,那么子进程就可以继承父进程的可继承句柄值。当传递TRUE 时,操作系统就创建该新子进程,但是不允许子进程立即开始执行它的代码。当然,系统为子进程创建一个新的和空的句柄表,就像它为任何新进程创建句柄表那样。不过,由于将TRUE 传递给 了CreateProcessbInheritHandles 参数,因此系统要进行另一项操作,即它要遍历父进程的句柄表,对于它找到的包含有效的可继承句柄的每个项目,系统会将该项目准确地拷贝到子进程的句柄表中。该项目拷贝到子进程的句柄表中的位置将与父进程的句柄表中的位置完全相同。这个情况非常重要,因为它意味着在父进程与子进程中,标识内核对象所用的句柄值是相同的。

除了拷贝句柄表项目外,系统还要递增内核对象的使用计数,因为现在两个进程都使用该对象。如果要撤消内核对象,那么父进程和子进程必须调用 该对象上的CloseHandle函数,也可以终止进程的运行。子进程不必首先终止运行,但是父进程也不必首先终止运行。实际上,CreateProcess函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操作的能力。

子进程为了确定它期望的内核对象的句柄值,最常用的方法是将句柄值作为一个命令行参数传递给子进程,该子进程的初始化代码对命令行进行分析(通常通过调用sscanf函数来进行分析),并取出句柄值。一旦子进程拥有该句柄值,它就具有对该对象的无限访问权。句柄继承权起作用的唯一原因是,父进程和子进程中的共享内核对象的句柄值是相同的,这就是为什么父进程能够将句柄值作为命令行参数来传递的原因。

当然,可以使用其他形式的进程间通信,将已继承的内核对象句柄值从父进程传送给子进程。方法之一是让父进程等待子进程完成初始化,然后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中。

另一个方法是让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子进程时,子进程就继承父进程的环境变量,并且能够非常容易地调用GetEnvironmentVariable函数,以获取被继承对象的句柄值。如果子进程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承。

改变句柄的标志

有时会遇到这样一种情况,父进程创建一个内核对象,以便检索可继承的句柄,然后生成两个子进程。父进程只想要一个子进程来继承内核对象的句柄。

换句话说,有时可能想要控制哪个子进程来继承内核对象的句柄。

若要改变内核对象句柄的继承标志,可以调用SetHandleInformation函数

c
BOOL SetHandleInformation(
   HANDLE hObject,
   DWORD dwMask,
   DWORD dwFlags);

第一个参数hObject 用于标识一个有效的句柄。

第二个参数dwMask 告诉该函数想要改变哪个或那几个标志。目前有两个标志与每个句柄相关联:

c
   # define HANDLE FLAG_INHERIT             0x00000001

   # define HANDLE FLAG PROTECT FROM CLOSE  0x00000002

如果想同时改变该对象的两个标志,可以逐位用OR将这些标志连接起来。

第三个参数是dwFlags,用于指明想将该标志设置成什么值。

//打开一个内核对象句柄的继承标志
SetHandleInformation(hobj, HANDLE_FLAG_INHERIT,  HANDLE_FLAG_INHERIT);

//关闭一个内核对象句柄的继承标志
SetHandleInformation(hobj,HANDLE_FLAG_INHERIT, 0);
c
//HANDLE_FLAG_PROTECT_FROM_CLOSE标志用于告诉系统,该句柄不应该被关闭
SetHandleInformation(hobj,HANDLE_FLAG_PROTECT_FROM_CLOSE, HANDLE_FLAG_PROTECT_FROM_CLOSE);
//会引发异常
CloseHandle(hobj); //Exception is raised

如果一个线程试图关闭一个受保护的句柄,CloseHandle 就会产生一个异常条件。很少想要将句柄保护起来,使他人无法将它关闭。但是如果一个进程生成了子进程,而子进程又生成了孙进程,那么该标志可能有用。父进程可能希望孙进程继承赋予子进程的对象句柄。不过,子进程有可能在生成孙进程之前关闭该句柄。如果出现这种情况,父进程就无法与孙进程进行通信,因为孙进程没有继承该内核对象。通过将句柄标明为“受保护不能关闭”,那么孙进程就能继承该对象。

但是这种处理方法有一个问题。子进程可以调用下面的代码来关闭HANDLE_FLAG_PROTECT_FROM_CLOSE标志,然后关闭句柄。

c
SetHandleInformation(hobj, HANDLEMFLAG_PROlECl_FROM_CLOSE,  0);
//正常关闭句柄
CloseHandle(hobj);

检查句柄是否可以继承

GetHandleInformation函数

c
BOOL GetHandleInformation(
   HANDLE hObj,
   PDWORD pdwFlags);

该函数返回pdwFlags指向的DWORD 中特定句柄的当前标志的设置值。

了解句柄是否是可继承的代码:

c
DWORD dwFlags;
GetHandleInformation(hObj, &dwFlags);
BOOL fHandleIsInheritable = (0 != (dwFlags& HANDLE_FLAG_INHERIT));

本文作者:Na1r

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!