内核对象是一个由Kernel申请的内存块,并且只能由Kernel接触

这个块是一个结构体,它的成员包含了内核对象的信息。一些成员 例如(安全描述符、使用计数等)是所有内核对象都有的。

内核对象不能有程序直接修改,调用函数创建内核对象时 会返回一个Handle

用户通过传递Handle给对应的函数,来修改对应的内核对象 在32位系统Handle为32位,64位系统Handle为64位

注意Handle只在当前Process有效,如果通过IPC 将Handle传递给,其他进程。在调用函数时会失败, 更差的话,可能会在进程索引表的相同位置创建一个不同的内核对象

内核对象由Kernel所有,而不是进程。这意味着当一个进程结束时, 如果它创建的内核对象正在被别的进程所使用,那么这个内核对象就 不会被销毁

内核对象通过它的使用计数器来记录,它被多少个进程使用。 当一个内核对象被创建时,它的使用计数器被设置为1 当一个进程退出时,它所使用的所有内核对象的计数会自动-1 当内核对象的计数为0时,内核会销毁这个内核对象

security

内核结构被安全描述符所保护。它指定了内核对象的所属者和所属组,以及那些用户和组可以使用该内核对象。

基本上所有的创建内核对象的函数都会有一个参数:指向SECURITY_ATTRIBUTES结构体的指针。

SECURITY_ATTRIBUTES定义如下

typedef struct _SECURITY_ATTRIBUTES {
    DWORD nLength;
    LPVOID lpSecurityDescriptor;
    BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;

SECURITY_ATTRIBUTES中只有lpSecurityDescriptor与权限有关。定义并初始化一个SECURITY_ATTRIBUTES如下:

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"));

除了内核对象,程序中还会使用一些其他的对象如menu、Window、mouse cursor 、burshes、font等这些对象是用户对象或者GDI ( Graphical Device Interface),不是内核对象。如何分别内核对象和其他对象就看创建对象的时候有无lpSecurityDescriptor参数,来设定对象的权限。

进程的内核对象句柄表

当一个进程初始化的时候,系统分配一个句柄表。接下来的内容未被文档化,所以不一定准确,同时在不同的Windows版本中可能不同。

句柄表由Index, 指向内核对象的指针、Access Mask、Flags组成。

当进程初始化时,句柄表为空。当创建一个内核对象时,系统为内核对象申请空间并初始化,然后扫描进程句柄表找到一个空的Index,当句柄表为空的时候,找到的第一个位置是1,然后在句柄表中设置对应的内核指针、设置Access Mask为Full mask,同时设置Flag

创建内核对象的函数返回的Handle值需要右移两位,内核对象在进程句柄表中的真实Index。所以在调试程序时可能看到如4、8等很小的Handle值。这里注意句柄是未文档化的,具体含义可能会在之后改变

当传递错误的Handle值给对应的函数时,会返回错误同时GetLastError会return 6 (ERROR_INVALIED_HANDLE)。

因为Handle值实际上作为进程句柄表的Index来使用,所以Handle传递给其他进程的时候,就只是使用其他进程句柄表对应Index上的值。

创建系统内核失败返回的Handle通常是0(NULL),这也是为什么第一个有效句柄值是4.但是有一些函数例如CreateFile在失败时会返回-1 (INVALID_HANDLE_VALUE)。所以像下面这样的代码是错误的

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
} // re

关闭内核对象

使用CLoseHandle来关闭内核对象。

CloseHandle函数首先检查进程句柄表确认当前是否有足够的权限访问对应的内核对象,如果权限足够,系统获得内核对象的地址,同时减少内核对象的usage count。如果usage count减少到0,系统就会销毁内核对象。

如果无效的地址传递给CloseHandle,在正常情况下return false,同时GetLastError返回ERROR_INVALID_HANDLE 但是在调试中,系统抛出一个0xC0000008 异常。这里能不能反沙箱、反调试

当你使用CloseHandle后无论,对应的内核对象的usage count是否为0,在进程句柄表中这个Index已经被清空了,不应该再尝试使用这个Handle。

如果在CloseHandle后继续使用Handle,可能会发生两种情况。

  • Handle对应的Index没有对应的数据,返回错误

  • Handle对应的Index,在CloseHandle到使用该Handle之间,被一个新创建的内核对象使用了。这个使用使用该Handle程序并不会报错,如果新创建的内核对象和CloseHandle的内核对象内型还相同的话,就更难以发现了。这里很像UAF和Heap Spray

在进程结束时,系统会扫描进程句柄表中的所有内核对象,对它们的Usage Count -1,这保证了当进程结束的时候,不会存在内核对象泄漏。

跨进程使用内核对象

一些需要跨进程使用内核对象的方法

  • File-mapping 对象运行不同的进程使用同一块内存KAV

  • Mailslots 和 named pipes 允许跨进程、跨机器传递数据

  • Mutesxes、semaphores(信号量)、events允许不同进程的线程进行同步操作

内核对象只在进程中使用,使得进程的安全性得以保证。想要操作一个内核对象必须先申请权限,通过设置严格的权限可以保护内核对象不被其他进程使用。

总共有三个方法跨进程使用内核对象

对象句柄继承

对象句柄继承只在父子进程之间有效。

在一种情况下:当父进程想要创建一个子进程,且同时想要子进程使用自己的内核对象。这个时候就能使用对象句柄继承。

首先在创建内核对象的时候,就必须要设置这个内核对象可以被继承。但是实际上被继承的不是内核对象,而是内核对象的句柄

通过设置SECURITY_ATTRIBUTES中的bInheritHanlde成员为True,创建的句柄就是可以继承的。

代码如下:

SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE; // Make the returned handle inheritable.
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);

进程的Handle Table如下,通过设置bIneritHanlde修改了Hanlde Table中的Flag,每个Handle Table etry有一个Flag bit 标识这个句柄是否被可以被继承。如果传递的为NULL的话,这个bit为0.

在这张表中Handle1不能被继承、Handle3能被继承。

BOOL CreateProcess(
    PCTSTR pszApplicationName,
    PTSTR pszCommandLine,
    PSECURITY_ATTRIBUTES psaProcess,
    PSECURITY_ATTRIBUTES psaThread,
    BOOL bInheritHandles,
    DWORD dwCreationFlags,
    PVOID pvEnvironment,
    PCTSTR pszCurrentDirectory,
    LPSTARTUPINFO pStartupInfo,
    PPROCESS_INFORMATION pProcessInformation);

第二个点是当使用CreateProcess创建子进程的时候,bInheritHandles参数需要设置。当设置bInheritHandles参数为True时,在新进程的代码运行之前,首先系统会为新进程创建一个Hanlde Table,同时因为bIneritHandles设置为True,所以系统还会扫描父进程的Handle Table把每个设置了Inheritable的Handle复制一份到子进程的Handle Table中。复制的时候保持Index不变。

在复制的时候,系统对于每个内核对象的Usage Count + 1.

子进程创建孙进程的时候如果设置了bIneritHandles为True,同样的流程也会再走一遍。

注意Handle的复制只会在创建子进程的时候发生,如果在创建进程之后父进程创建了新的内核对象,子进程是无法使用的。

对象句柄继承有一个奇怪的特性,子进程不知道父进程继承了哪些Index给自己。常见的方式就是通过设置命令行参数,然后子进程通过stscanf_s来获得句柄值。 当然也可是使用WaitForInputIdle等待子进程初始化完成之后,通过其他通信方式将Handle值传递给子进程。 另外一个技巧是使用环境变量,子进程可以通过GetEnvironmentVariable获得环境变量。,当子进程还要创建孙进程时,这个方法比较好是因为环境变量可以被反复继承。

改变句柄Flag

通过SetHandleInformation函数可以修改Hanlde的Flag。

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

第一个参数指向需要修改的句柄,第二个参数是你想要修改的Flag,第三个函数是你想要把Flag修改为什么。

目前有每个Handle都关联两个Flag

#define HANDLE_FLAG_INHERIT 0x00000001
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002

可以通过位运算同时改变多个Flag的值。

要设置句柄运行继承代码如下

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);

关闭句柄奇偶代码如下

SetHandleInformation(hObj, HANDLE_FLAG_INHERIT, 0);

HANDLE_FLAG_PROTECT_FROM_CLOSE 标志告诉系统,当前Handle不允许被关闭,在调试是如果尝试关闭该Handle会抛出异常。

通过GetHandleInformation可以获得Handle的flag值,如果要看一个句柄是否运行继承,代码如下

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

命名对象

第二种跨进程使用内核对象的方法就是命名对象。不是所有的对象可以命名。下面这些对象就可以被命名

HANDLE CreateMutex(
    PSECURITY_ATTRIBUTES psa,
    BOOL bInitialOwner,
    PCTSTR pszName);
HANDLE CreateEvent(
    PSECURITY_ATTRIBUTES psa,
    BOOL bManualReset,
    BOOL bInitialState,
    PCTSTR pszName);
HANDLE CreateSemaphore(
    PSECURITY_ATTRIBUTES psa,
    LONG lInitialCount,
    LONG lMaximumCount,
    PCTSTR pszName);
HANDLE CreateWaitableTimer(
    PSECURITY_ATTRIBUTES psa,
    BOOL bManualReset,
    PCTSTR pszName);
HANDLE CreateFileMapping(
    HANDLE hFile,
    PSECURITY_ATTRIBUTES psa,
    DWORD flProtect,
    DWORD dwMaximumSizeHigh,
    DWORD dwMaximumSizeLow,
    PCTSTR pszName);

这些函数的最后一个参数都是