《Windows核心编程》之“内核对象”
2016-08-19 11:17
176 查看
内核对象(Kernel Object)是Winodws操作系统中最核心的一个概念,本文将对整个抽象的概念进行一些探索。
一、什么事内核对象
1,内核对象是对操作系统各项资源的抽象。
Windows操作系统采用的是面向对象的编程模式,它将其管理的各项资源,都抽象为各种对象。比如:file、event、process、thread、iocompletationport、mailslot、mutex等。
使用wintellect公司的sysinternals套件中的“winobj.exe”工具可以查看Windows Kernel Object的相关信息。其中,在它的目录树中的“ObjectType”中列出了各种内核对象的名称。需要注意的是,创建内核对象的函数名和内核对象的名称并都不是相似的,比如:文件映射对象,它的对象类型为session,而它是通过“CreateFileMapping”函数来创建的。
2,内核对象是内核代码中的一种数据结构。
Windows操作系统代码是由C语言(部分汇编)实现的。与C++语言的class不同,C语言的对象就是一个“数据结构”+操作该数据结构的相关函数。
在驱动编程中,DriverObject、DeviceObject等,我们查看它的定义(在wdm.h文件中),发现它们就是一个数据结构。事实上,其他内核对象在代码层面来说,也都是一中数据结构。
题外话:C语言面向对象的封装性明显不如C++。
3,内核对象的属性
从代码层面来说,内核对象的属性就是表示该内核对象的结构体的各个成员。当然,不同的内核对象,对应不同的结构体,它们的成员各部相同。但是,也有那么几项所有内核对象都具有的属性。
1)使用计数(usage count)
数据结构是静态的,内核对象是操作系统在运行时保有的一种活的资源,操作系统需要管理内核对象的生死。此外,内核层代码是被所有应用层进程共享,操作系统需要管理这种共享资源。
操作系统是怎样来管理内核对象的呢?最基础的一个机制,正是“使用计数”。大致规则是:内核对象被初次创建,计数为1;内核对象被另一个进程获取,计数加1;内核对象被一个进程释放,计数减1;内核对象计数为0时,系统会销毁它。
该属性由操作系统维护,应用程序开发者只需知道这个机制即可。
2)安全描述符(Security Descriptor,SD)
创建任一内核对象时,都需要传入一个“PSECURITY_ATTRIBUTES”结构体参数。它代表的就是该内核对象的SD。
大多数时候,应用程序都不关心安全性,一般传“NULL”进去即可。少数如操作注册表、打开IO端口等,可以参考MSDN文档,选择传入的参数。
注:我们在调用形如“Create***”之类的Windows API时,如果它有“PSECURITY_ATTRIBUTES”这么一个参数,那么它99%的可能就是内核对象啦。
二、进程的“内核句柄表”
我们知道,内核代码运行在ring0层,用户代码运行在ring3层。内核对象作为一种内核代码,用户层的应用程序是不能直接访问它的。那么,应用程序需要如何标记和操作内核对象呢?
答案是:应用程序通过句柄来标记内核对象,通过Windows APIs来操作内核对象。
1,句柄(handle)
句柄是内核对象在某个具体的进程空间的一种标记。它在winnt.h文件中被定义:
1)在32位的系统中,句柄是一个32位的值;在64位系统中,句柄是一个64位的值。
2)句柄的值与具体的进程有关。
同一个内核对象在不同的进程中的句柄值不一定相同。(一般来说都不同)
2,内核句柄表
1)一个进程在初始化的时候,系统会为它分配一个句柄表。最开始的时候,它的句柄表示空的。
2)每次进程的某个线程调用“Create***”成功后,内核句柄表会增加一项;调用CloseHandle会删除相应项。
3)内核句柄表是形如下面的一个表格:
4)介绍使用“任务管理器”和“procexp.exe”工具查看进程的句柄表信息
三、跨进程边界共享内核对象
前面提到,每个进程都有自己的内核句柄表。这个表中的项,都是在进程内部创建的内核对象。有没有可能,A进程获得B进程创建的内核对象?
答案是,可能的。因为,内核对象是寄居在内核层,而内核层被所有进程共享。
下面就介绍三种跨进程共享内核对象的方法。
1,对象句柄继承
子进程可以继承父进程的内核对象句柄。其中,需要设置对象句柄的标志位——可继承性。
2, 创建命名对象
形如“Create***”的创建内核对象的Windows API,大多提供了一个参数“PCTSTR pszName”。
1)该参数一般是最后一个参数,创建匿名对象时,传入NULL即可。
2)PCTSTR - Pointer Const Text String(STR),从该参数的类型可知,需要传一个const char*或const wchar_t *给它。此外,pszName表示它是一个以0为终止符的字符串(string with zero)。它的最大长度为MAT_PATH (260)。
3)有了pszName,我们就可以在其他进程,通过形如“Open***”的Windows API打开这个已经存在内核中的命名对象。这个“Open***”与“Create***”类似,最后一个参数也表示对象名。
4)Microsoft没有提供任何手段来保障内核对象名的唯一性,也就是,我们创建的内核对象极有可能同名。如果同名,“Create***”调用会失败,返回NULL。
注:一般地,“Create***”创建内核对象失败,会返回“INVALID_HANDLE_VALUE”,而“Open***”找不到命名对象,会返回NULL,它们的返回值不相等。
5)Jeffrey建议使用GUID作为内核对象名,保证我们创建的内核对象名的唯一性。
3,终端服务命名空间
适用于服务器
4,专有命名空间(private namespace)
一般地,所有内核对象的名字都处于同一个名字空间,因此,很容易导致内核对象名字冲突。为了解决名字冲突的问题,除了使用GUID,还有一个方法,就是创建专有命名空间。(这个貌似跟C++的namespace的作用很像,不知道是不是互相借鉴的)
我们通过创建命名对象的方式,如mutex,可以实现单实例应用程序(singleton)。而黑客也可通过抢先创建同名内核对象的方式,阻止单实例应用程序的启动,实现Dos攻击(Don't Serve,拒绝服务)。
如果使用private namespace,那么就能避免这中方式的攻击。下面将结合singleton.exe示例来揭示如何使用private namespace。
四、Singleton.exe示例
我们主要分析示例代码中的“CheckInstances”函数,理解以下三个问题:
1)如何创建一个边界(boundary);
2)如何将对应于本地管理员(Local Administrator)的一个安全描述符(security identifier, SID)和它关联起来。即“关联boundary和SID”。
3)如何创建或打开其名称被用作互斥量内核对象前缀的一个专有命名空间。
它的原理就是:只有处于同一个特权组的用户才能创建相同的命名空间。它可以防止低特权的恶意程序创建相同的boundary。
代码如下:
五、第3中跨进程共享内核对象的方法
复制对象句柄 —— 使用DuplicateHandle函数。简单地说,这个函数获取一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录向的一个副本。
注:GetCurrentProcess可以返回当前进程的句柄,但是它是一个伪句柄。其值为-1,GetCurrentThread返回的也是伪句柄其值为-2,它们并不在句柄表中而仅仅代表当前进程和当前线程。
一、什么事内核对象
1,内核对象是对操作系统各项资源的抽象。
Windows操作系统采用的是面向对象的编程模式,它将其管理的各项资源,都抽象为各种对象。比如:file、event、process、thread、iocompletationport、mailslot、mutex等。
使用wintellect公司的sysinternals套件中的“winobj.exe”工具可以查看Windows Kernel Object的相关信息。其中,在它的目录树中的“ObjectType”中列出了各种内核对象的名称。需要注意的是,创建内核对象的函数名和内核对象的名称并都不是相似的,比如:文件映射对象,它的对象类型为session,而它是通过“CreateFileMapping”函数来创建的。
2,内核对象是内核代码中的一种数据结构。
Windows操作系统代码是由C语言(部分汇编)实现的。与C++语言的class不同,C语言的对象就是一个“数据结构”+操作该数据结构的相关函数。
在驱动编程中,DriverObject、DeviceObject等,我们查看它的定义(在wdm.h文件中),发现它们就是一个数据结构。事实上,其他内核对象在代码层面来说,也都是一中数据结构。
题外话:C语言面向对象的封装性明显不如C++。
3,内核对象的属性
从代码层面来说,内核对象的属性就是表示该内核对象的结构体的各个成员。当然,不同的内核对象,对应不同的结构体,它们的成员各部相同。但是,也有那么几项所有内核对象都具有的属性。
1)使用计数(usage count)
数据结构是静态的,内核对象是操作系统在运行时保有的一种活的资源,操作系统需要管理内核对象的生死。此外,内核层代码是被所有应用层进程共享,操作系统需要管理这种共享资源。
操作系统是怎样来管理内核对象的呢?最基础的一个机制,正是“使用计数”。大致规则是:内核对象被初次创建,计数为1;内核对象被另一个进程获取,计数加1;内核对象被一个进程释放,计数减1;内核对象计数为0时,系统会销毁它。
该属性由操作系统维护,应用程序开发者只需知道这个机制即可。
2)安全描述符(Security Descriptor,SD)
创建任一内核对象时,都需要传入一个“PSECURITY_ATTRIBUTES”结构体参数。它代表的就是该内核对象的SD。
大多数时候,应用程序都不关心安全性,一般传“NULL”进去即可。少数如操作注册表、打开IO端口等,可以参考MSDN文档,选择传入的参数。
注:我们在调用形如“Create***”之类的Windows API时,如果它有“PSECURITY_ATTRIBUTES”这么一个参数,那么它99%的可能就是内核对象啦。
二、进程的“内核句柄表”
我们知道,内核代码运行在ring0层,用户代码运行在ring3层。内核对象作为一种内核代码,用户层的应用程序是不能直接访问它的。那么,应用程序需要如何标记和操作内核对象呢?
答案是:应用程序通过句柄来标记内核对象,通过Windows APIs来操作内核对象。
1,句柄(handle)
句柄是内核对象在某个具体的进程空间的一种标记。它在winnt.h文件中被定义:
#ifdef STRICT typedef void *HANDLE;
1)在32位的系统中,句柄是一个32位的值;在64位系统中,句柄是一个64位的值。
2)句柄的值与具体的进程有关。
同一个内核对象在不同的进程中的句柄值不一定相同。(一般来说都不同)
2,内核句柄表
1)一个进程在初始化的时候,系统会为它分配一个句柄表。最开始的时候,它的句柄表示空的。
2)每次进程的某个线程调用“Create***”成功后,内核句柄表会增加一项;调用CloseHandle会删除相应项。
3)内核句柄表是形如下面的一个表格:
4)介绍使用“任务管理器”和“procexp.exe”工具查看进程的句柄表信息
三、跨进程边界共享内核对象
前面提到,每个进程都有自己的内核句柄表。这个表中的项,都是在进程内部创建的内核对象。有没有可能,A进程获得B进程创建的内核对象?
答案是,可能的。因为,内核对象是寄居在内核层,而内核层被所有进程共享。
下面就介绍三种跨进程共享内核对象的方法。
1,对象句柄继承
子进程可以继承父进程的内核对象句柄。其中,需要设置对象句柄的标志位——可继承性。
2, 创建命名对象
形如“Create***”的创建内核对象的Windows API,大多提供了一个参数“PCTSTR pszName”。
1)该参数一般是最后一个参数,创建匿名对象时,传入NULL即可。
2)PCTSTR - Pointer Const Text String(STR),从该参数的类型可知,需要传一个const char*或const wchar_t *给它。此外,pszName表示它是一个以0为终止符的字符串(string with zero)。它的最大长度为MAT_PATH (260)。
3)有了pszName,我们就可以在其他进程,通过形如“Open***”的Windows API打开这个已经存在内核中的命名对象。这个“Open***”与“Create***”类似,最后一个参数也表示对象名。
4)Microsoft没有提供任何手段来保障内核对象名的唯一性,也就是,我们创建的内核对象极有可能同名。如果同名,“Create***”调用会失败,返回NULL。
注:一般地,“Create***”创建内核对象失败,会返回“INVALID_HANDLE_VALUE”,而“Open***”找不到命名对象,会返回NULL,它们的返回值不相等。
5)Jeffrey建议使用GUID作为内核对象名,保证我们创建的内核对象名的唯一性。
3,终端服务命名空间
适用于服务器
4,专有命名空间(private namespace)
一般地,所有内核对象的名字都处于同一个名字空间,因此,很容易导致内核对象名字冲突。为了解决名字冲突的问题,除了使用GUID,还有一个方法,就是创建专有命名空间。(这个貌似跟C++的namespace的作用很像,不知道是不是互相借鉴的)
我们通过创建命名对象的方式,如mutex,可以实现单实例应用程序(singleton)。而黑客也可通过抢先创建同名内核对象的方式,阻止单实例应用程序的启动,实现Dos攻击(Don't Serve,拒绝服务)。
如果使用private namespace,那么就能避免这中方式的攻击。下面将结合singleton.exe示例来揭示如何使用private namespace。
四、Singleton.exe示例
我们主要分析示例代码中的“CheckInstances”函数,理解以下三个问题:
1)如何创建一个边界(boundary);
2)如何将对应于本地管理员(Local Administrator)的一个安全描述符(security identifier, SID)和它关联起来。即“关联boundary和SID”。
3)如何创建或打开其名称被用作互斥量内核对象前缀的一个专有命名空间。
它的原理就是:只有处于同一个特权组的用户才能创建相同的命名空间。它可以防止低特权的恶意程序创建相同的boundary。
代码如下:
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 account if (dwLastError == ERROR_ACCESS_DENIED) { AddText(TEXT("Access denied when creating 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")); } }
五、第3中跨进程共享内核对象的方法
复制对象句柄 —— 使用DuplicateHandle函数。简单地说,这个函数获取一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录向的一个副本。
注:GetCurrentProcess可以返回当前进程的句柄,但是它是一个伪句柄。其值为-1,GetCurrentThread返回的也是伪句柄其值为-2,它们并不在句柄表中而仅仅代表当前进程和当前线程。
相关文章推荐
- Windows核心编程笔记---内核对象
- Windows核心编程 第3章 内核对象
- (摘自windows核心编程之用内核对象进行线程同步)
- 《Windows核心编程》笔记2 --- 内核对象二
- Windows核心编程:内核对象
- 《Windows核心编程 5th》读书笔记----第9章 用内核对象进行线程同步
- WINDOWS核心编程--读书笔记:第三章 内核对象
- windows核心编程第三章学习事件内核对象代码
- (摘自windows核心编程之用内核对象进行线程同步)
- windows核心编程-9.用内核对象进行线程同步
- 【windows核心编程】 第三章 内核对象
- 《windows核心编程》学习笔记 内核对象
- windows核心编程 第3章 内核对象
- 《windows核心编程》学习笔记(一)内核对象
- Windows核心编程 第3章 内核对象
- 《Windows核心编程》之“内核对象同步”
- 浅尝《Windows核心编程》之内核对象
- windows程序设计 and windows核心编程(内核对象理论)
- windows核心编程-用内核对象进行线程同步
- 关于Windows核心编程中的内核对象