Windows核心编程——》第三章 内核对象

1.什么是内核对象

内核对象是由内核分配的一块内存,它只能被内核访问

这块内存存储着一个保存着内核对象信息数据结构


2.如何访问内核对象

应用程序不能直接访问内核对象,只能通过Windows API间接访问和操作内核对象

那些用于操作内核对象的API会返回一个标识内核对象的句柄,应用程序通过该句柄和API函数来操作和访问内核对象。


3.内核对象的数据成员

既然内核对象是数据结构,那么它一定相应的数据成员。

1     引用计数:

  它用来记录有多少个应用程序在使用该内核对象,它该值0时系统会销毁该内核对象

   内核对象是由内核所有,换句话说即使创建内核对象的进程已经结束了,所创建的内核对象仍可能存在


内核对象可以被多个进程同时使用,句柄值通常会不一样,但是引用的内容是同一块。因此内核对象的存在时间通常会比进程的存在时间长。内核对象中有一个值用来保存当前使用该内核对象的进程数,这就是使用计数。这样可以确保在没有进程引用该对象时系统中不保留任何内核对象。

 

2    安全:

  它用来描述谁拥有这个对象(通常是创建者),以及哪些用户和组可以访问或不可访问该对象

  在操作对象前进程必须先提交操作请求,该请求能否成功是则“安全”来控制的

      对象的创建都可以通过“安全”来阻止那些未授权的用户访问该对象。



3.判断一个对象是否是内核对象

判断一个对象是否是内核对象的最简单方法是查看创建该对象的函数

几乎所有的内核对象的创建函数都有一个 用于描述安全属性的“安全描述符”作为参数。如下:

HANDLE CreateFileMapping(

   HANDLE hFile,

   PSECURITY_ATTRIBUTES psa,

   DWORD flProtect,

   DWORD dwMaximumSizeHigh,

   DWORD dwMaximumSizeLow,

   PCTSTR pszName);


参数psa就是来控制访问权限的。当访问一个现有的文件映射内核对象时需要设定要对对象执行什么操作,如: HANDLE hFileMapping = OpenFileMapping(FILE_MAP_READ, FALSE, "MyFileMapping"),其中FILE_MAP_READ说明是对内核对象执行读操作,"MyFileMapping"是内核对象的名字。

   注:我们可以通过对象的创建函数中是否有安全描述符区分是内核对象还是一般的用户对象


注:当查看创建内核对象的函数返回值时,必须格外小心。特别要注意的是,当调用CreateFile函数失败时返回值为INVALID_HANDLE_VALUE(值为-1)其他的内核函数返回值为NULL

4.进程的句柄表(A Process' Kernel Object Handle Table)

进程在初始化后,系统会为它分配一个句柄表记录它所用到的内核对象

Index

Pointer to Kernel Object Memory Block

Access Mask

(DWORD of Flag Bits)

Flags

1

0x????????

0x????????

0x????????

2

0x????????

0x????????

0x????????

 

句柄可能理解为上图中的Index,在进程句柄表中它相当于内核对象的指针的索引

句柄是进程相关的不同的进程不能简单传递句柄来共享内核对象,因为句柄只是一个索引它的值在不同进程中所索引的内核对象不同 所以不是简单的通过传递句柄来共享内核对象


5.关闭内核对象

无论是不是你创建的内核对象,当你使用完该对象后都应该通过CloseHandle来关闭内核对象

 

BOOL CloseHandle(HANDLE hobject);

//Set handle to NULL after CloseHandle().

 

  CloseHandle 会对对象的引用计数减然后清空进程句柄表中相应的项

  当引用计数减到0时系统会销毁该内核对象

6.不关闭内核对象?

如果不关闭内核对象则内核对象的引用计数不会减少,也就不会被系统销毁,从而会产生内存泄露

幸好当程序结束后系统会保证释放程序所占用的所有资源,系统遍历当前进程的句柄表,然后对句柄表中的逐个项调用CloseHandle



3跨越进程边界共享内核对象
 
  方法:继承性命名复制内核对象三种方法
 
3.1 对象句柄的继承性
  有当进程具有父子关系时,才能使用对象句柄的继承性。父进程必须执行若干个操作步骤如下:
 
  1)首先,当父进程创建内核对象时,必须向系统指明,它希望对象的句柄是个可继承的句柄。请记住,虽然内核对象句柄具有继承性,但是内核对象本身不具备继承性。若要创建能继承的句柄,父进程必须指定一个SECURITY_ATTRIBUTES结构,并将此结构中的成员变量bInheritHandle设为 TRUE。如果返回的句柄是不能继承的,那么句柄表中的标志位是0x00000000。如果将bInheritHandle为TRUE,那么该标志位将被置为0x00000001。
 
  2) 创建子进程时,需要将子进程设置为可以继承父进程的可继承句柄值。就是函数CreateProcess()中的参数bInheritHandles设置为 TRUE。当bInheritHandles为TRUE时,系统会遍历父进程句柄表,将可继承的句柄拷贝到子进程
   注:
   1. 关闭内核对象时,子进程不必首先终止运行,但是父进程也不必首先终止运行,实际上,
    CreateProcess函数返回后,父进程可以立即关闭对象的句柄,而不影响子进程对该对象进行操作的能力。(关闭顺序随意
       2.子进程创建后父进程创建的新的可继承内核对象的句柄不会被拷贝到子进程中
例如:

#include <windows.h>

#include <stdio.h>

int main(void)

{

       SECURITY_ATTRIBUTESsa;

       ZeroMemory(&sa, sizeof(SECURITY_ATTRIBUTES));

       sa.nLength =sizeof(SECURITY_ATTRIBUTES);

       sa.lpSecurityDescriptor = NULL;

       sa.bInheritHandle = TRUE; //使得创建的句柄可继承

       STARTUPINFO si;

       ZeroMemory(&si, sizeof(si));

       si.cb = sizeof(si);

       PROCESS_INFORMATION pi;

       ZeroMemory(&pi, sizeof(pi));

      

       HANDLE h = CreateMutex(&sa, FALSE, "fuck");

       if(GetLastError()== ERROR_ALREADY_EXISTS)

       {

              printf("run error\n");

              system("pause");

              return 0;

       }

       else{

//使得创建的新进程 RunAsDate.EXE 继承父进程中可以继承的内核句柄,如前面创建的互斥句柄 h

              CreateProcess(NULL,"RunAsDate", NULL, NULL,TRUE, 0, NULL, NULL, &si, &pi);

              printf("run\n");

              system("pause");

              return 0;

       }  

}

ps:运行上面的程序之后,会创建一个RunAsDate进程,把父进程关闭,再运行父进程会提示run error,表明我们的互斥对象已经被RunAsDate子进程继承,并且关闭父进程,对于互斥对象没有影响.


 3)子进程确定它期望的内核对象的句柄值的方法有:
 
       1.将句柄值作为命令行参数传给子进程,子进程的初始化代码将解析命令行(通常是调用_stscanf_s),并提取句柄。子进程获得句柄值之后,就会拥有和父进程一样的内核对象访问权限。
ps:句柄继承之所以能够实现,唯一原因就是“共享的内核对象”的句柄值在父进程和子进程中是完全一样的。这正是父进程能将句柄值作为命令行参数来传递的原因。
 
       2.让父进程等待子进程完成初始化(使用第9章介绍的WaitForInputIdlee函数),然后,父进程可以将一条消息发送或展示在子进程中的一个线程创建的窗口中
 
       3.让父进程将一个环境变量添加给它的环境程序块。该变量的名字是子进程知道要查找的某种信息,而变量的值则是内核对象要继承的值。这样,当父进程生成子进程时,子进程就继承父进程的环境变量,并且能够非常容易地调用GetEnvironmentVariable函数,以获取被继承对象的句柄值。如果子进程要生成另一个子进程,那么使用这种方法是极好的,因为环境变量可以被再次继承
 
 3.2改变句柄标志
    如果创建了一个可继承的内核对象,但又不想某个子进程继承,就可以调用函数
    修改标志位,bool SetHandleInformation(HANDLE hObject, DWORD dwMask, DWORD dwFlags);
    读取标志位,BOOL GetHandleInformation(HANDLE hObj, PDWORD pdwFlags);
 
3.3命名对象
    1、多进程共享内核对象的第二种方法是使用命名对象,即创建内核对象时为内核对象命名。如函数:
    HANDLE CreateMutex(PSECURITY_ATTRBUTES psa, BOOL bInitialOwner,PCTSTR pszName);
    当已经创建了同名的内核对象,系统会先判断对象类型是否一样,当前进程是否有访问权,如果类型一样,且有访问权的话,系统将复制已经存在的那个对象的信息到进程的句柄表,并返回一个句柄(不同进程的句柄一般不同)。否则的话调用失败,返回NULL。
 
    既然内核对象创建函数这样设置,我们如何判断是否创建了新的内核对象呢?方法是在调用创建函数后马上调用GetLastError()函数
  代码为:if(GetLastError() == ERROR_ALREADY_EXISTS)
 
注:用命名对象的方法去创建内核对象时可能会出现不同类型的对象用同一个名字,那么后面创建对象时将失败。例如:
  HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("MyObj"));
  HANDLE hSem = CreateSemaphore(NULL, 1, 1,  TEXT("MyObj"));
  以上代码CreateSemaphore函数调用肯定会返回NULL,因为已经有一个同名的互斥量对象了,执行上述代码后,如果检查GetLastError函数的返回值会发现错误代码6(ERROR_INVALID_HANDLE).
  因为内核对象用同一个内核空间,且内核对象的名字不能相同
 
因此我们可以防止运行一个应用程序的多个实例。方法是在Main函数中,用CreateMutex()函数创建一个命名对象,再马上调用GetlastError()来判断是否已经存在这个互斥对象。如果存在,则退出程序。
例如:
#include <Windows.h>
#include <stdio.h>
int main(void)
{
//为了确保名称的唯一性,建议创建一个GUID,并将这个GUID的字符串形式作为自己的对象名称使用.
HANDLE h = CreateMutex(NULL, FALSE, TEXT("{5ACCA58C-8EAB-410E-9263-C5ADAD0D8F9B}"));
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
MessageBox(NULL, "The program is arealy run", "fuck", MB_OK|MB_ICONERROR);
return 0;
}
system("pause");
}
 
   2、我们也可以调用Open函数来打开内核对象。如函数:
   HANDLE OpenMutex(DWORD dwDesiredAccess, BOOL bInheritHandle, PCTSTR pszName);
   Open函数会判断内核中是否有参数pszName所指定的名字的内核对象,如果有,并且对象的类型相同,且当前线程有访问权限,那么系统将拷贝内核信息到进程,且内核计数加一。否则返回0.错误代码为:ERROR_FILE_NOT_FOUND。
 
3.4终端服务器的命名空间
    当有终端服务器时,终端服务器会提供一个全局的命名空间和一个会话的命名空间。这样可以避免相同应用程序的两个或多个会话之间出现互相干扰的情况。指定名字空间的方法如下:
CreateMutex(NULL,FALSE,"Global\\MyName"); \\全局命名空间
CreateMutex(NULL,FALSE,"Local\\MyName");  \\局部命名空间
 
3.5复制对象句柄
     共享内核对象的另一种方法是复制对象句柄,函数是BOOL DupicateHandle()。此函数主要是将一个线程的句柄信息复制到另一个进程的句柄表中。新生成的句柄值与原句柄可能不同
 
书中举了两个例子。一个是将一个进程的句柄复制到另一个句柄。另一个是当一个进程有对某一个内核对象的读写功能时,有一个函数要对这个内核对象进行读操作。那么为了程序的健壮性,可以先复制生成一个只读的句柄,再将这个句柄传给这个函数。(具体代码见本书本节)。
 
     当一个进程复制了句柄到另一个进程时,如何通知另一个进程,这个内核对象可用呢? 方法是使用窗口消息或某种别的I P C机制
 
详述复制对象句柄方法
1。跨进程边界共享内核对象的最后一招是使用DuplicateHandle函数:
BOOL DuplicateHandle( 
  HANDLE hSourceProcessHandle, 
  HANDLE hSourceHandle, 
  HANDLE hTargetProcessHandle, 
  LPHANDLE lpTargetHandle, 
  DWORD dwDesiredAccess, 
  BOOL bInheritHandle, 
  DWORD dwOptions);
简单的说,这个函数获得一个进程的句柄表中的一个记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本.
具体应用如下:
#include <stdio.h>
#include <windows.h>
int main(void)
{
//进程
A
创建一个名为
"MYOBJ"的
互斥量
HANDLE hObjInProcessA = CreateMutex(NULL, FALSE, "MYOBJ");
//判断,如果已经创建了这个互斥量就打印
"run error\n"
if(GetLastError() == ERROR_ALREADY_EXISTS)
{
printf("run error\n");
system("pause");
return 0;
}
//获得进程B的进程句柄(7036为进程B的PID,随便弄了个做了下试验)
HANDLE hProcessB = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 7036);
//
 hObjInProcessB是相对于
进程
B的句柄,他标示的对象就由"进程A中代码引用的
hObjInProcessA"所标示的对象.
HANDLE hObjInProcessB;
DuplicateHandle(GetCurrentProcess(),
hObjInProcessA,
hProcessB,
&hObjInProcessB,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
//收尾工作.
//ps:
hObjInProcessB千万不能关掉
CloseHandle(hProcessB);
CloseHandle(hObjInProcessA);
printf("run \n");
system("pause");
return 0;
}
以上程序第一次运行的时候把互斥量句柄
hObjInProcessA复制到进程B的句柄表中,用变量名
hObjInProcessB来保存.
此时A退出,B不退出,
再运行A,就会打印 run error 的提示,表明互斥量复制成功,已存在于B进程中.
.
2。还可以通过另一种方式来使用
DuplicateHandle:假设一个进程有对一个文件映射对象的读写权限.在程序中的某个位置,我们要调用一个函数,并希望它对文件映像对象进行只读访问.为了使应用程序变得更健壮,可以使用
DuplicateHandle为现有的对象创建一个新句柄,并确保这个这个新句柄只有只读权限.然后,把这个只读权限的句柄传递给函数.
示例代码:
HANDLE hFileMapRW = CreateFileMapping(INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
10240,
NULL);
HANDLE hFileMapRO;
DuplicateHandle(GetCurrentProcess(), hFileMapRW,
GetCurrentProcess(), &hFileMapRO,
FILE_MAP_READ, FALSE, 0);
//调用自己的函数
ReadFromTheFileMapping(hFileMapRO);
CloseHandle(hFileMapRO);
CloseHandle(hFileMapRW);
 
 
 
 

1.         简单区分内核对象和其他对象的方法:创建需要安全信息的多半是内核对象。

2.         每个进程有一个内核对象表,表的每一项是一个简单结构,包括真实内核对象地址和访问权限等。用户代码持有的内核对象句柄其实是对象表中对应项的索引。因此如果CloseHandle关闭一个对象没有清空变量,且在对象表的同样位置恰好又创建了一个新的内核对象对之前没清空的无效变量的访问会造成bug。(比如对同一个句柄多调用了一次CloseHandle导致另一个内核对象被关闭。)

3.         进程退出时,会释放各种内存、内核对象、GDI对象等。

4.         跨进程使用内核对象的理由:跨进程传输:用文件映像对象实现共享内存邮件槽命名管道实现数据通信信号量和互斥量进行同步等。

5.         跨进程使用内核对象三种方式:对象句柄继承命名内核对象、复制对象句柄。

6.         对象句柄继承:创建内核对象的时候可以指定SECURITY_ATTRIBUTES. bInheritHandle表示可继承(任何时候可以使用SetHandleInformation修改可继承性等属性),创建子进程时指定CreateProcess的参数bInheritHandlesTRUE,则子进程从父进程的对象表中拷贝所有可继承的对象到自己的对象表的相同表位置中(因为是新创建的所以对应的索引号还未被使用(并增加引用计数),因为表项结构被完全拷贝且内核对象实际地址在地址空间后2G的内核地址段中,所以拷贝过来的表项完全有效,进而父子进程的可继承内核对象的句柄值完全相同,于是只要以任何方式将要继承的对象的句柄值跨进程交给子进程(创建子进程时的命令行参数、环境变量、共享内存、消息等手段),则后者可以使用。

7.         命名内核对象:要访问已经存在的命名内核对象,可以使用CreateXXX或者OpenXXX,后者在对象不存在的时候返回NULL。如果打开了一个已经存在的命名对象,在打开时为API指定的对象名以外的参数被忽略。注意,一个进程打开同一对象两次,除了增加引用两次外,返回的句柄值是不同的,需要分别关闭一次,即打开和关闭完全对称(很合理的行为)。在Vista及以上的系统,对象名可以包括在命名空间下,避免被低授权用户访问。

8.         复制对象句柄DuplicateHandle

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值