windows核心编程---内核对象
内核对象
-概述
1.每个内核对象都只是一个内存块,由操作系统内核分配,只能由操作系统内核访问。
2.这个内存块是一个数据结构,其成员维护着与对象相关的信息。
3.不能直接更改这些结构,可以利用windows提供的一组函数,以最恰当的方式来操纵这些结构。
4.创建一个内核对象后,会返回一个句柄,它标识了所创建的对象,可以将这个句柄想象为一个不透明的值。可由进程中任何线程使用。32位windows中,句柄是32位值。64位windows中,是64位。
5.句柄是进程相关的。
6.内核对象的内容被保存在内核地址空间中,系统上运行的所有进程都共享这个空间。
-使用计数
1.内核对象的所有者是操作系统内核,而非进程。内核对象的生命期可能长于创建它的那个进程。
2.操作系统内核知道当前有多少进程正在使用一个特定的内核对象,因为每个对象都包含一个使用计数。
3.初次创建时,使用计数位1。另一进程获得现有内核对象访问后,使用计数会递增。进程终止运行后,操作系统内核将自动递减此进程仍然打开的所有内核对象的使用计数。如果一旦对象的使用计数变为0,操作系统内核会销毁该对象。这样,保证系统中不存在没有被任何进程引用的内核对象。
-内核对象的安全性
1.内核对象可用一个安全描述符来保护。安全描述符描述了谁拥有对象,哪些组和用户被允许访问或使用此对象。哪些组和用户被拒绝访问此对象。
2.用于创建内核对象的函数几乎都有一个指向 SECURITY_ATTRIBUTES结构的指针作为参数。
为此参数传入NULL时,创建的内核对象具有默认的安全性。具体包括哪些默认的安全性,要取决于当前进程的安全令牌。
3.
typedef struct _SECURITY_ATTRIBUTES
{
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
}
SECURITY_ATTRIBUTES;
4.使用举例
SECURITY_ATTRIBUTES sa;
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = FALSE;
HANDLE hFileMapping = CreateFileMapping(
INVALID_HANDLE_VALUE,
&sa,...);
5.内核对象和用户对象
常见的用户对象:菜单,窗口,鼠标光标,画刷,字体。
判断一个对象是不是内核对象,最简单的方式是查看创建这个对象的函数。几乎所有创建内核对象的函数都允许我们指定安全属性信息参数。
-进程内核对象句柄表
1.一个进程在初始化时,系统将为它分配一个句柄表。这个句柄表仅供内核对象使用,不适用于用户对象或GDI对象。
2.句柄表结构猜测
一个数据结构组成的数组
每个结构由,一个指向内核对象的指针,一个访问掩码,一些标志组成。
-创建一个内核对象
1.进程首次初始化时,句柄表为空。
2.进程内的一个线程调用一个会创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块。
然后,内核扫描进程的句柄表,查找一个空白记录项。
3.用于创建内核对象的任何函数都会返回一个与进程相关的句柄。这个句柄可由同一进程中运行的所有线程使用。系统用索引来表示内核对象的信息保存在进程句柄表中的具体位置。要得到实际的索引值,句柄值应除以4。
4.调用一个函数时,如果它接受一个内核对象句柄作为参数,就须把Create*函数返回的值传给它。在内部,这个函数会查找进程的句柄表,获得目标内核对象的地址,然后以一种恰当的方式来操纵对象的数据结构。
5.由于句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前这个进程相关的。无法供其它进程使用。
6.调用函数来创建一个内核对象时,调用失败,一般返回NULL。但也有几个函数调用失败返回-1(INVALID_HANDLE_VALUE)
7.用于创建内核对象的函数总是返回具有完全访问权限的句柄。如果想限制一个句柄的访问权限,可使用这些函数的扩展版本。
-关闭内核对象
无论用什么方式创建内核对象,都要用CloseHandle向系统表明已经结束使用对象。
BOOL CloseHandle(HANDLE hobject);
在内部,该函数先检查主调进程的句柄表,验证“传给函数的句柄值”标识的是“进程确实有权访问的一个对象”。如句柄是有效的,系统将获得内核对象数据结构的地址,并将结构中的“使用计数”成员递减。如果使用计数变为0,内核对象将被销毁,并从内存中去除。
在CloseHandle返回前,它会清除进程句柄表中对象的记录项–这个句柄此后对我们的进程来说是无效的,不要再试图使用它。无论内核对象是否销毁,这个清除过程都会发生。
-自动释放
进程终止运行时,操作系统会确保此进程所使用的所有资源都被释放。对于内核对象,操作系统执行以下操作,进程终止时,系统自动扫描该进程的句柄表。如在表中有任何有效的记录项,操作系统会为我们关闭这些对象句柄。只要这些对象中有一个的使用计数递减至0,内核就会销毁对象。
以上进程终止的资源释放,适用于所有内核对象,资源,内存块。
-跨进程边界共享内核对象
使用对象句柄继承
为对象命名
复制对象句柄
-使用对象句柄继承
1.只有在进程间有一个父子关系时,才可使用对象句柄继承。
这时,父进程有一个或多个内核对象句柄可以使用,而且父进程决定生成一个子进程,并允许子进程访问父进程的内核对象。
2.要在子进程中继承父进程的内核对象
2.1.父进程创建一个内核对象时,父进程须向系统指出它希望这个对象的句柄是可以继承的。
2.2.为了创建一个可继承的句柄,父进程须分配并初始化一个SECURITY_ATTRIBUTES结构,并将这个结构的地址传给具体的Create函数。
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
// 默认安全性
sa.lpSecurityDescriptor = NULL;
// 可继承
sa.bInheritHandle = TRUE;
HANDLE hMutex = CreateMutex(&sa, FALSE, NULL);
2.3.由父进程生成子进程
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
);
bInheritHandles为TRUE时,操作系统会创建新的子进程,但不允许子进程立即执行它的代码。系统会为子进程创建一个新的空白进程句柄表,系统接着会遍历父进程的句柄表,对它的每一个记录项进行检查。凡是包含一个有效的“可继承的句柄“的项,会被完整复制到子进程的句柄表。在子进程的句柄表中,复制项的位置与它在父进程的句柄表中的位置是完全一样的。这意味这,在父进程和子进程中,对一个内核对象进行标识的句柄值是一样的。 系统还会递增内核对象的使用计数。
被子进程继承的内核对象句柄,具有相同的句柄值,访问掩码及相同的标志。
对象句柄的继承只发生在生成子进程时,后来,父进程又创建的可继承内核对象不会被继承。
3.改变句柄的标志
// 改变内核对象句柄的继承标志
BOOL SetHandleInformation
(
// 句柄
HANDLE hObject,
// 要更改的标志
// HANDLE_FLAG_INHERIT 句柄可继承性
// HANDLE_FLAG_PROTECT_FROM_CLOSE 禁止关闭句柄
DWORD dwMask,
// 标志的新值
DWORD dwFlags
);
BOOL GetHandleInformation
(
HANDLE hObject,
// 返回指定句柄当前标志
PDWORD pdwFlags
);
-为对象命名
1.创建命名对象
// 举例
HANDLE CreateMutex
(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,
// NULL 或 以0为终止符的名称字符串
PCTSTR pszName
);
Micorsoft没有提供专门的机制来保证为内核对象指定的名称是唯一的。
所有内核对象都共享同一个命名空间,即使类型不同。
创建内核对象时,若指定的对象名已经存在,创建会失败。
2.命名对象的共享
// 例
// 进程A
HANDLE hMutexProcessA = CreateMutex
(
NULL,
FALSE,
TEXT("JeffMutex")
);
// 进程B
HANDLE hMutexProcessB = CreateMutex
(
NULL,
FAlSE,
TEXT("JeffMutex")
);
进程B调用CreateMutex时,系统首先会查看是否存在一个名为”JeffMutex”的内核对象,如果存在。
检查对象的类型。
类型匹配后,执行一次安全检查,验证调用者是否拥有对该对象的完全访问权限。
权限验证通过后,系统会在进程B的句柄表中查找一个空白记录项,将其初始化为指向现有的内核对象。
如类型不匹配,安全性验证不通过,创建失败。
在进程B实现对进程A创建对象共享时,进程B用于创建内核对象函数的其余参数被忽略了。
3.共享操作
3.1.2中成功时,不会实际地创建一个互斥量对象。
会为进程B分配一个新的句柄值,标识内核中一个现有的互斥量对象。
在进程B的句柄表中用一个新的记录项来引用这个对象。这个互斥量对象的使用计数会递增。
3.2.创建操作成功返回时,验证是创建还是打开了一个对象
HANDLE hMutex = CreateMutex
(
&sa,
FALSE,
TEXT("JeffObj")
);
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
//
}
else
{
//
}
3.3.用Open*实现内核对象的共享
HANDLE OpenMutex
(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
// 以0为终止符的字符串,不可为NULL
PCTSTR pszName
);
函数将在 同一个内核对象命名空间搜索,以查找一个匹配的对象,
如没找到同名,返回NULL,错误码:ERROR_FILE_NOT_FOUND
如找到同名,但类型不匹配,返回NULL,错误码:ERROR_INVALID_HANDLE.
如找到同名,类型匹配,权限验证通过,则,更新主调进程的句柄表,并使对象的使用计数递增。
3.4.保证内核对象不重名
建议:
创建一个GUID,将这个GUID的字符串形式作为自己的对象名称使用。
3.5.终端服务命名空间
终端服务的情况和前面描述的稍微有所区别。正在运行终端服务的计算机中,有多个用于内核对象的命名空间。
其中一个是全局命名空间,所有客户端能访问的内核对象都要放在这个命名空间。
每个客户端会话都有一个自己的命名空间。
// 获取进程所在的终端服务会话
DWORD processID = GetCurrentProcessId();
DWORD sessionID;
ProcessIdToSessionId(processID, &sessionID);
一个服务的命名内核对象始终位于全局命名空间内。
默认下,客户端会话采用自己的命名空间。
也可显示把内核对象放入当前会话命名空间或全局命名空间。
HANDLE h = CreateEvent
(
NULL,
FALSE,
FALSE,
TEXT("Global\\MyName")
);
HANDLE h = CreateEvent
(
NULL,
FALSE,
FALSE,
TEXT("Local\\MyName")
);
3.6.专有命名空间
如想确保我们的应用程序创建的内核对象名称不会和其它应用程序的名称冲突,可定义一个自定义的前缀。
负责创建内核对象的服务器进程将定义一个边界描述符,以对命名空间的名称自身进行保护。
// 如何创建一个边界
// 如何将对应于本地管理员组的一个安全描述符和它关联起来
// 如何创建其名称被用作互斥量内核对象前缀的一个专有命名空间
// 如何打开其名称被用作互斥量内核对象前缀的一个专有命名空间
// 边界描述符将获得一个名称,也会获得它关联的一个特权用户组的SID
// 边界
// 用户-->用户组
// 用户-->基于用户运行的应用程序
// 创建边界描述符
// 返回值是一个指针而不是句柄,
// 基于此,此后应对返回值调用DeleteBoundaryDescriptor而不是CloseHandle
HANDLE CreateBoundaryDescriptor
(
//
PCTSTR pszName,
// 0
DWORD dwFlags
);
// 创建特权用户组的SID
BOOL WINAPI CreateWellKnownSid
(
_In_ WELL_KNOWN_SID_TYPE WellKnownSidType,
_In_opt_ PSID DomainSid,
_Out_opt_ PSID pSid,
_Inout_ DWORD *cbSid
);
BYTE localAdminSID[SECURITY_MAX_SID_SIZE];
PSID pLocalAdminSID = &localAdminSID;
DWORD cbSID = sizeof(localAdminSID);
// Win32 API创建SID
CreateWellKnownSid
(
WinBuiltinAdministratorsSid,
NULL,
pLocalAdminSID,
&cbSID
);
// 将一个特权用户组的SID与边界描述符关联
BOOL AddSIDToBoundaryDescriptor
(
HANDLE* phBoundaryDescriptor,
PSID pRequiredSid
);
// 创建安全属性
BOOL WINAPI ConvertStringSecurityDescriptorToSecurityDescriptor(
_In_ LPCTSTR StringSecurityDescriptor,
_In_ DWORD StringSDRevision,
_Out_ PSECURITY_DESCRIPTOR *SecurityDescriptor,
_Out_ PULONG SecurityDescriptorSize
);
// 创建专有命名空间
// 如试图创建的专有命名空间已经存在,返回NULL。错误码:ERROR_ALREADY_EXISTS。
HANDLE CreatePrivateNameSpace
(
// 供Windows使用,用于允许或禁止一个应用程序通过OpenPrivateNamespace来访问命名空间并在其中打开/创建对象
PSECURITY_ATTRIBUTES psa,
// 为边界描述符设置的SID决定了谁能进入边界并创建命名空间
PVOID pvBoundaryDescriptor,
// 用于创建内核对象的字符串前缀
PCTSTR pszAliasPrefix
);
HANDLE OpenPrivateNamespace
(
PVOID pvBoundaryDescriptor,
PCTSTR pszAliasPrefix
);
// CreatePrivateNamespace和OpenPrivateNamespace返回的HANDLE不是内核对象句柄,用ClosePrivateNamespace关闭
BOOLEAN ClosePrivateNamespace
(
HANDLE hNamespace,
// PRIVATE_NAMESPACE_FLAG_DESTROY 关闭后不可见
// 0 关闭后可见
DWORD dwFlags
);
专有命名空间相当于可供我们在其中创建内核对象的一个目录。
和其它目录一样,专有命名空间有一个和它关联的安全描述符(CreatePrivateNamespace时指定)。
和文件系统不同的是,这个命名空间没有父目录,没有名称。我们将“边界描述符”作为对这个边界进行引用的一个名称来使用。
我们为专有命名空间指定的名称是一个别名,只在进程内可见。其它进程可以打开同一个专有命名空间,并为它指定一个不同的别名。
-复制对象句柄
BOOL DuplicateHandle
(
// 源进程内核对象
HANDLE hSourceProcessHandle,
// 可为任何类型内核对象的句柄,这个句柄值与hSourceProcessHandle句柄所标识的那个进程相关
HANDLE hSourceHandle,
// 目标进程内核对象
HANDLE hTargetProcessHandle,
// 指向被复制内核对象在hTargetProcessHandle进程的句柄值
PHANDLE phTargetHandle,
// 内核对象在目标进程中所对应的句柄表项中 属性信息
DWORD dwDesiredAccess,
BOOL bInheritHandle,
// 0
// DUPLICATE_SAME_ACCESS(此时忽略dwDesiredAccess) 或 DUPLICATE_CLOSE_SOURCE(关闭源进程的句柄)组合
DWORD dwOptions
);
这个函数获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本。
在执行了DuplicateHandle后,调用此函数进程要通过某种方式来告知目标进程,现在拥有的对某内核对象的访问权限。
上一篇: jmeter函数开发实现返回随机字符串
推荐阅读
-
Windows核心编程:第9章 用内核对象进行线程同步
-
Windows核心编程:第8章 用户模式下的线程同步
-
windows核心编程01_错误处理
-
2.1 Windows核心编程-进程UAC下以管理员权限运行
-
2Java面向对象编程——6java核心类——5枚举类
-
基于visual c++之windows核心编程代码分析(47)实现交换网络的QQ号嗅探
-
Windows核心编程:第4章 进程
-
荐 夯实基础,彻底掌握js的核心技术(二):面向对象编程(Object Oriented Programming)
-
回炉重造之重读Windows核心编程-003-内核对象
-
1. 错误处理 -> Windows核心编程【第五版】