Windows核心编程之核心总结(第三章 内核对象)(2018.6.2)

2021-04-07 00:26

阅读:358

标签:Windows核心编程之核心总结

学习目标

第三章内核对象的概念较为抽象,理解起来着实不易,我不断上网找资料和看视频,才基本理解了内核对象的概念和特性,其实整本书给我的感觉就是完整代码太少了,没有多少实践的代码对内容的实现,而且书本给的源码例子,有太多我们不知道的知识,并且这些知识对本章主要内容来说是多余的,所以我们理解起来也非常困难。为了更好的学习这章,我补充了一些辅助性内容。这一章的学习目标:
1.Windows会话和安全机制
2.什么是内核对象?
3.使用计数和安全描述符
4.内核对象句柄表
5.创建内核对象
6.关闭内核对象
7.跨进程边界共享内核对象-使用对象句柄继承
8.跨进程边界共享内核对象-为对象命名
9.防止运行一个应用程序的多个实例
10.终端服务命名空间和专有命名空间
11.结合专有命名空间实现防止运行一个应用程序的多个实例
12.跨进程边界共享内核对象-复制对象句柄

Windows会话和安全机制

Vista系统开始,Windows就建立了session(会话)的概念。Windows系统启动后就建立session0(会话0),将公用服务载入session0(会话0)中,例如:通常将一些与硬件紧密相关的模块(如:中断处理程序等)、各种常用设备的驱动程序(声卡驱动、打印机驱动、显卡驱动)以及运行频率较高的模块(如:时钟管理、进程调度和许多模块所公用的一些基本操作),这些都放在内存,称为操作系统内核。系统启动后,第一个用户登陆了该系统,就建立起了session1(会话1),那么当你运行所有的应用程序,魔兽啊,飞车啊,绝地求生啊,这些应用程序都是在session1(会话1)下运行。会话下运行多个程序会涉及多任务,例如:多个进程并发执行,那么就通过进程管理隔离实现多任务。当有另一个用户远程登陆,那么建立起session2(会话2),session2(会话2)也有独有的应用程序---进程。如果依次有用户登录该系统,那么就顺序依次建立起新的会话,也依次拥有独有的应用程序---进程。采用这个设计后,系统核心组件就可以更好地与用户不慎启动的恶意软件隔离。不同用户的进程通过会话进行隔离,这就是多用户的过程,多用户是依靠会话进行隔离而实现用户之间相互独立,互不影响。这里引出了会话Session的概念后,就要考虑安全机制问题。假如张三登陆了这个系统,建立起了会话1,然后系统就会给张三一个会话令牌,这个令牌包含了它的用户信息,还有该用户访问的权限、属于什么组等信息。在会话1下, 张三运行了一个程序,系统会给这个程序分配一个令牌,这个程序令牌就是继承于会话建立时获得的会话令牌,然后这个程序想要打开一个文件(内核对象),这个文件(内核对象)就会有一个安全描述符(SD),系统会根据程序令牌和文件的安全描述符相互匹配,安全描述符含有创建的用户、哪些用户或组允许访问此对象,哪些用户或组拒绝访问此对象。匹配之后发现这个用户属于安全描述符的拒绝访问名单内,那么这个程序就无法打开该文件,否则能够打开文件。

什么是内核对象?

在系统和我们写的应用程序中,内核对象用于管理进程、线程和文件等诸多种类的大量资源。作为Windows开发人员,我们经常都要创建、打开和处理内核对象。当我们在会话中启动一个应用程序,那么当应用程序载入内存,就会生成一个进程(这是一个主调进程)。每个进程对应都有一个虚拟地址空间,然后由内存管理程序对虚拟地址空间和物理地址空间的转换。进程的虚拟内存空间分为内核层和应用层,每个内核对象,其实就是一块内存块,这个内存块位于操作系统的内核地址空间(内核层),而用户的应用程序运行在应用层,注意:这里都是说明在虚拟地址空间,然后会映射到真正的物理地址空间中。而内核对象是由操作系统内核分配的,并只能由操作系统内核访问。因此,应用程序不能直接操作内核对象,需要用Windows系统给定的函数来操作。每一个内核对象都有特定的创建函数和操作函数。所以,当一个主调进程里调用了创建一个内核对象函数,那么这个内核对象(内存块)就会在进程的虚拟内存空间的内核层里,实际映射到物理内存的操作系统内核区域。内核对象这个内存块是一个数据结构,其成员维护着与对象相关的信息。

使用计数和安全描述符

我们上节说到,内核对象这个内存块实际是一个数据结构,内核对象的结构分为两个部分:公用部分(安全描述符(security descriptor,SD),使用计数)和特有部分。其中特有部分,例如:进程内核对象有一个进程ID、一个基本的优先级和一个退出代码。使用计数是每一个内核对象都有的一个数据成员,当有一个内核对象被创建时,使用计数被设为1,当另一个进程获得对现有内核对象的访问后,使用计数就会递增,进程终止运行后,操作系统内核将自动递减此进程仍然打开的所有内核对象的使用计数,如果一旦内核对象的使用计数为0,操作系统内核就会销毁该内核对象。安全描述符描述了谁拥有内核对象,哪些组和用户被允许访问或使用此对象,哪些组和用户被拒绝访问或使用此对象。用于创建内核对象的所有函数几乎都有一个指向SECURITY_ATTRIBUTES结构的指针作为参数。下面给出这个结构的签名:

        typedef struct _SECURITY_ATTRIBUTES {
            DWORD  nLength;//结构的大小
            LPVOID lpSecurityDescriptor;//安全描述符
             BOOL   bInheritHandle;//表示所创建的内核对象是否可被继承,一般是具有父子关系的进程才可以继承
            } SECURITY_ATTRIBUTES;

如果想对我们创建的内核对象加以访问限制,就必须创建一个安全描述符。在Windows核心编程有这一内核对象的概念,而Windows程序设计又有着GDI对象(例如画笔,窗口,画刷,位图。)的概念,我们能知道的只有内核对象在内核层,而用户对象(例如:GDI对象)在应用层。那么我们要怎么区分一个对象是内核对象还是非内核对象?刚刚我们学习了安全描述符,每一个内核对象的创建函数基本都有一个SECURITY_ATTRIBUTES属性作为参数,所以很明显了。我们可以看创建对象的函数,如果创建对象的函数有安全描述符参数,那么这个函数创建的对象就是内核对象。

内核对象句柄表

一个进程在初始化时,系统为进程分配了一个内核对象句柄表。下图显示了一个进程的句柄表。可以看出内核对象句柄表是一个由数据结构组成的数组,每个结构都包含索引、指向一个内核对象内存块的指针、一个访问掩码和一些标志(例如:是否可被继承的标志,在创建内核对象就被指定了)。
技术分享图片

创建内核对象

一个进程首次初始化的时候,其内核对象句柄表为空。然后,当进程中的线程调用创建内核对象的函数时,比如CreateFileMapping,操作系统内核就为该对象分配一个内存块,并对它初始化。这时,操作系统内核对进程的句柄表进行扫描,找出一个空项。操作系统内核找到索引1位置上的结构并对它进行初始化。该指针成员将被设置为内核对象的数据结构的内存地址,访问屏蔽设置为全部访问权,同时,各个标志也作了设置。我列举以下部分创建内核对象的函数签名:

HANDLE CreateThread(
   PSECURITY_ATTRIBUTES psa,
   size_t dwStackSize,
   LPTHREAD_START_ROUTINE pfnStartAddress,
   PVOID pvParam,
   DWORD dwCreationFlags,
   PDWORD pdwThreadId);

HANDLE CreateFile(
   PCTSTR pszFileName,
   DWORD dwDesiredAccess,
   DWORD dwShareMode,
   PSECURITY_ATTRIBUTES psa,
   DWORD dwCreationDisposition,
   DWORD dwFlagsAndAttributes,
   HANDLE hTemplateFile);

HANDLE CreateFileMapping(
   HANDLE hFile,
   PSECURITY_ATTRIBUTES psa,
   DWORD flProtect,
   DWORD dwMaximumSizeHigh,
   DWORD dwMaximumSizeLow,
   PCTSTR pszName);

HANDLE CreateSemaphore(
   PSECURITY_ATTRIBUTES psa,
   LONG lInitialCount,
   LONG lMaximumCount,
   PCTSTR pszName);

我们可以看到这些创建内核对象的函数签名,参数都有一个SECURITY_ATTRIBUTES结构参数,然后返回一个内核对象句柄,这个句柄值其实就是作为内核对象句柄表的索引来使用的,所以这些句柄是与当前这个进程相关的,无法供其他进程使用,如果我们真的在其他进程中使用它,那么实际引用的只是那个进程的句柄表中位于同一个索引的内核对象----只是索引值相同而已。那么要得到实际的句柄表的索引值,内核对象句柄值应该除以4才得到索引值。

关闭内核对象

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

HRESULT CloseHandle( 
   HANDLE hHandle  
);

调用这个函数,函数内部会先检查主调进程的句柄表,看下主调进程对这个内核对象句柄是否有权访问。如果内核对象句柄是有效的,系统将获得内核对象的数据结构的地址,并将结构中的使用计数成员递减。如果使用计数变成0,内核对象将被销毁,并且清除对应内核对象句柄表中对应的记录项;如果使用计数递减后不为0,说明其他进程还在使用该内核对象,那么只清除对应内核对象句柄表中对应的记录项,不销毁内核对象。讲了这么多,有什么方法可以看见进程有多少个内核对象吗?当然有,微软提供了一个小工具:Process Explorer,下面图显示了我自己的应用程序,里面创建了一个名为"ydm"的互斥量内核对象,我将会关闭这个内核对象。下方的mutant类型这一行,就是我在内部创建的互斥量内核对象。
技术分享图片

跨进程边界共享内核对象-使用对象句柄继承

在每个进程中都有一个内核句柄表,这也就说明同一个内核对象其在不同的进程中其内核对象句柄值可能是不一样的。但是内核对象的作用很大程度上就在于能够在进程间共同访问,即跨进程边界共享内核对象。那我们怎么实现不同进程间共享同一个内核对象呢?Windows核心编程这本书给我们提供了三个方法实现这个功能。这一小节先讲使用对象句柄继承来实现跨进程边界共享内核对象。
只有在进程之间有一个父子关系时,才可以使用对象句柄继承。为了使子进程继承父进程的内核对象句柄表,必须执行以下几个步骤:
1.当父进程创建一个内核对象时,父进程必须向系统指出它希望这个对象的句柄是可继承的。注意,这里说的继承是指继承内核对象句柄,而非内核对象。为了创建一个可以继承的内核对象句柄,父进程必须分配并初始化一个SECURITY_ATTRIBUTES结构,并将这个结构的地址传递给具体的Create*创建内核对象函数。下面举个鲜明的例子:

SECURITY_ATTRIBUTES sa;//安全属性结构
sa.nLength=sizeof(sa);//结构大小
sa.lpSecurityDescriptor=NULL;//安全描述符
sa.bInheritHandle=TRUE;//指定内核对象是否可被继承

HANDLE hMutex=CreateMutex(&sa,FALSE,NULL);//创建一个互斥量内核对象

我们都知道内核对象句柄表的每个记录项含有索引、指向内核对象内存块的地址、访问掩码和标志,其中标志就是指是否可以继承。如果在创建内核对象的时候将NULL作为PSECURITY_ATTRIBUTES参数传入,则返回的句柄将是不可继承的,这个标志也会被设为0,如果bInheritHandle成员设为TRUE,则这个标志被设为1.
2.父进程生成子进程,通过在主调进程内调用CreateProcess函数完成。下面给出CreateProcess的函数签名:

BOOL WINAPI CreateProcess(
  _In_opt_    LPCTSTR               lpApplicationName,
  _Inout_opt_ LPTSTR                lpCommandLine,
  _In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_        BOOL                  bInheritHandles,
  _In_        DWORD                 dwCreationFlags,
  _In_opt_    LPVOID                lpEnvironment,
  _In_opt_    LPCTSTR               lpCurrentDirectory,
  _In_        LPSTARTUPINFO         lpStartupInfo,
  _Out_       LPPROCESS_INFORMATION lpProcessInformation
);

注意参数bInheritHandles,如果设为TRUE,子进程就会继承父进程的“可继承的内核对象句柄”的值,注意:如果是父进程的“不可继承的内核对象句柄”,那么子进程就不会继承到。我说过每个进程都有一个内核对象句柄表,子进程也不例外。系统在创建子进程后会分配一个新的、空白的内核对象句柄表。总的执行流程如下:系统会先遍历父进程的内核对象句柄表,对它的每一个记录项进行检查,凡是包含一个有效的“可继承的内核对象句柄”的项,都会被完整地复制到子进程的内核对象句柄表,在子进程的内核对象句柄表中,复制项的位置与它在父进程句柄表中的位置完全一样,这一特性意味着:在父进程和子进程中,对每一个内核对象进行标识的内核对象句柄值是完全一样的。除了复制内核句柄表的记录项,系统还会递增内核对象的使用计数,因为两个进程现在都在使用这个内核对象。记住一个要点:内核对象句柄的继承只会在生成子进程的时候发生,假如父进程后来又创建了新的内核对象,并同样将它们的句柄设为可继承的句柄,那么正在运行的子进程是不会继承这些新句柄的。前面都是先创建一个父进程的可继承的内核对象,父进程调用CreateProcess函数创建第一个子进程,然后系统自动将父进程可继承的内核对象句柄复制到子进程的内核对象句柄表中,然后创建第二个子进程,过程还是依然如此。但是我希望在创建第二个子进程时继承不到父进程的这一内核对象。简单来说,就是我们想控制哪些子进程能继承内核对象句柄,可以调用SetHandleInformation函数来改变已经创建好了的内核对象句柄的继承标志。那么只要在调用CreateProcess函数生成第二个子进程前调用SetHandleInformation函数关闭内核对象的继承标志,就可以实现我们目的啦。这个函数签名如下:

BOOL SetHandleInformation(
   HANDLE hObject,//标识了一个有效的内核对象句柄,为什么有效?因为还是需要主调进程有访问权限。
   DWORD dwMask,//告诉函数我们想更改哪个或者哪些标志
   DWORD dwFlags);//指出把标志设为什么

下面给出参数2,dwMask的两种取值:

HANDLE_FLAG_INHERIT 0x00000001  
If this flag is set, a child process created with the bInheritHandles parameter of CreateProcess set to TRUE will inherit the object handle. 
HANDLE_FLAG_PROTECT_FROM_CLOSE  0x00000002  
If this flag is set, calling the CloseHandle function will not close the object handle. 

1.如果要打开一个内核对象句柄的继承标志,可以这样写:

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);

2.要关闭这个标志,可以这样写:

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,0);

3.HANDLE_FLAG_PROTECT_FROM_CLOSE标志是告诉系统不允许关闭内核对象句柄:

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);//如果在这个函数之后调用CloseHandle关闭这个句柄就会报错

4.如果需要告诉系统允许关闭内核对象句柄,我们可以这样写:

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,0);//这时候在这个函数调用之后调用CloseHandle函数关闭内核对象句柄不会报错,成功关闭

5.我们可以通过GetHandleInformation函数获取指定内核对象句柄的当前标志。如果要检查一个内核对象句柄是否可以被继承,我们可以这样写:

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

后面内容晚点补充...

Windows核心编程之核心总结(第三章 内核对象)(2018.6.2)

标签:Windows核心编程之核心总结

原文地址:http://blog.51cto.com/12731497/2123554


评论


亲,登录后才可以留言!