进程和程序
一、 进程是动态的,程序是静态的:程序是有序代码的集合,进程是程序的执行。进程有核心态/用户态。
二、进程是暂时的,程序是永久的;进程是一个状态变化的过程,程序可以长久保存
三、进程和程序的组成不同,进程的组成包括程序、数据和进程控制块(即进程状态信息)
四、进程是内核对象 其ID是一个32位的无符号整数,用于在整个系统中唯一地标志该对象
内核对象
何为内核对象
每个内核对象都只是一个内存块,它是由操作系统内核分配的,并只能由操作系统内核访问。
系统4环结构
R3 用户态 所有api都通过ntdll调用到内核Api
R2、R1 设备驱动态
R0 内核态
内核对象数据结构
每个内存块是一个数据结构,其成员维护着与对象相关的信息。
少数成员是所有成员都有的。(安全描述符SecurityDescriptor,使用计数Counter)
大多数成员都是不同类型的对象所拥有的。
例如,
进程(进程ID pid,优先级privilege,退出码exitCode…)
文件对象(字节偏移量,共享模式,打开模式…)
句柄
应用程序通过windows函数操纵内核对象,用句柄标识创建的对象,
句柄实际是一个指针,他指向一块包含具体信息数据的内存。
进程id和进程句柄,进程id为系统唯一标识,进程句柄不同时刻打开句柄值是不一样。
使用计数Counter
内核对象数据结构共有的成员之一,管理特定内核对象的数量。
操作系统通过使用计数来管理内核对象。
1.当某类 内核对象初次使用时 使用计数为1,紧接在其他进程开始使用该内核对象时使用计数会累加,相反使用该内核对象进程销毁时,使用计数会递减。
2.当某类 内核对象使用计数为0时操作系统会销毁该内核对象。
安全描述符SecurityDescriptor
内核对象数据结构共有的成员之一,保证内核对象的安全性,决定谁能访问该内核对象。
内核对象的安全性
许多老版本windows应用程序在visita之后不能正常工作,都在于不使用安全性。
例如,注册表操作 RegOpenKeyEx ,正确做法是传入KEY_QUERY_VALUE,从而指定查询子项数据的权限。传入KEY_ALL_ACCESS,就忽略了安全性。
SECURYITY_ATTRIBUTES 结构体
Windows创建内核对象函数,必传入该结构体。
内核对象和用户对象的区别也在于是否传入了该结构体
typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; / /结构体的大小,可用SIZEOF取得 LPVOID lpSecurityDescriptor; / /安全描述符 BOOL bInheritHandle ;/ /安全描述的对象能否被新创建的进程继承 } SECURITY_ATTRIBUTES,* PSECURITY_ATTRIBUTES;
进程内核对象句柄表
一个进程初始化时,系统会为它分配一个句柄表。
进程的句柄表结构
空白句柄表
索引 | 指向内核对象内存块的指针 | 访问掩码(包含标志位的一个DWORD) | 标志 |
1 | 0x??? ??? | 0x??? ??? | 0x??? ??? |
2 | 0x??? ??? | 0x??? ??? | 0x??? ??? |
… | … | … | … |
句柄表 = 索引+内存指针+访问掩码+标志
索引:表示内核对象的信息保存在进程句柄表中的具体位置。
指向内核对象内存块的指针:
访问掩码:访问权限控制,类似网络掩码
访问掩码的功能是以压缩形式描述访问权限。 为简化访问管理,访问掩码包含一组四位(一般权限),这些权限通过使用函数 RtlMapGenericMask转换为一组更详细的权限。
标志:标识子进程是否继承其内核对象。
具体可参考:windows文件系统之访问掩码
创建一个内核对象
进程的创建
一个进程首次初始化的时候,其句柄表为空。当进程内的一个线程调用一个会创建内核对象的函数时,内核将为这个对象分配并初始化一个内存块。然后内核扫描进程的句柄表,查找一个空白的记录项(empty entry)。具体点说,指针成员会被设置成内核对象的数据结构的内部内存地址,访问掩码将被设置成拥有完全访问权限,标志也会设置。
句柄与进程的关系
用于创建内核对象的任何函数都会返回一个与进程相关的句柄,这个句柄由同一进程中运行的所有线程使用。
句柄表中的索引值,等于句柄值除以4(或右移两位)。
所以句柄值实际是作为进程句柄表的索引来使用的,所以这些句柄是与当前进程有关的,无法供其他进程使用。
句柄值 INVAILD_HANDLE_VALUE和NULL
INVAILD_HANDLE_VALUE:
部分内核函数在调用失败时会返回-1即无效值
例如 CreateFile
NULL:
大部分内核函数调用失败会返回NULL,第一个有效句柄值为4.
例如 CreateMutex
关闭内核对象
CloseHandle(HANDLE hObject)向系统表明我们已经结束使用内核对象
关闭内核对象过程
检查句柄表,验证句柄值标识的时进程确实有权访问的一个对象。
- 如果句柄有效,获得内核对象内存地址,并将使用计数(Counter)递减,如果使用计数为0则内核对象被销毁,内存清理。
- 如果句柄无效,
i.进程是正常运行的,CloseHandle会返回FALSE,而GetLastError会获得Invalid
ii.进程正在被调试,那么系统会抛出0xC0000008异常(指定了无效的句柄)。
正常CloseHandle后我们应当将hObject置为NULL
不然会存在野指针使用的问题,轻则会因为句柄表查找不到记录返回无效参数,重则会定位到错误类型的内核对象或者相同类型的内核对象的销毁,会造成内存损坏后果。
未调用CloseHandle可能会造成内存泄漏
仅在进程运行时会造成资源泄露,当进程退出后 系统先会自动扫描进程句柄表,然后关闭没有关闭的对象,并销毁那些计数为0的对象,保障一切被正确清除,适用内核对象和用户对象。
跨进程边界共享内核对象
很多时候不同进程中运行的线程需要共享内核对象。
例如:
- 利用文件映射对象,可以在同一机器上运行两个不同进程之间的共享数据块。
- 借助邮件槽和命名管道,在网络中的不同计算机运行的进程可以相互发送数据块。
- 互斥量,和信号量和事件允许不同进程中的线程同步执行。
使用对象句柄继承
通过安全描述符
指出其bInHeritHandle为True是可继承的。为NULL则不可继承。
表3-2 包含两个有效记录项的进程句柄表
索引 | 指向内核对象内存块的指针 | 访问掩码(包含标志位的一个DWORD) | 标志 |
1 | 0xF000000 | 0x??? ??? | 0x00000000 |
2 | 0x00000000 | 不可用 | 不可用 |
3 | 0xF0000000 | 0x??? ??? | 0x00000001 |
… | … | … | … |
1,3是有效的,1是不可继承,3是可继承的,2是销毁后的或者未创建的。 |
创建子进程过程
- 先创建新子进程
- 创建新的空白进程句柄表
- 从父进程复制有效的句柄表记录到子进程
- 增加内核对象计数
表3-3继承了父进程的“可继承句柄”之后的,子进程的句柄表的样子
索引 | 指向内核对象内存块的指针 | 访问掩码(包含标志位的一个DWORD) | 标志 |
1 | 0x0000000 | 不可用 | 不可用 |
2 | 0x00000000 | 不可用 | 不可用 |
3 | 0xF0000000 | 0x??? ??? | 0x00000001 |
… | … | … | … |
父进程的1,3记录 只有3因为可继承而被复制到子进程的句柄表中。 |
注意 正在运行的子进程是不会继承父进程新的句柄,只会在被创建时继承当时的父进程句柄
父子进程的内核对象,可通过将句柄值作为命令行参数传给子进程。
BOOL CreateProcess ( LPCTSTR lpApplicationName, LPTSTR lpCommandLine,//命令行参数 LPSECURITY_ATTRIBUTES lpProcessAttributes, LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATIONlpProcessInformation );
其他进程间通信技术
- WaitForInputIdle 父进程发消息给子进程的窗口
- 父进程在其环境块添加子进程知道名称的环境变量,
改变句柄的标志
SetHandleInformation 改变句柄的可继承性。
GetHandleInformation 检查句柄是否可继承。
BOOL SetHandleInformation( _In_ HANDLE hObject, _In_ DWORD dwMask, _In_ DWORD dwFlags ); BOOL GetHandleInformation( _In_ HANDLE hObject, _In_ DWORD dwFlags ); //设置可继承 #define HANDLE_FLAG_INHERIT 0x00000001 //告诉系统不允许关闭句柄,设置之后CloseHandle会抛异常 #define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002
为对象命名
Create*函数时传入pszName给内核对象命名。即使内核对象类型不同,仍然会导致创建同名的内核对象失败。
可以使用GUID工具使内核对象名唯一确定.
Create和Open区别
如果对象不存在,Create*会创建它;Open会调用失败而告终。
终端服务命名空间
每个客户端对话(Session)都会有一个自己命名空间,
确认进程在哪个终端服务会话中运行
ProcessIdToSessionId函数
DWORD processId = GetCurrentProcessId();//当前进程id DWORD sessionId; if(ProcessIdToSessionId(processId,&sessionId)){ _tprintf(TEXT("Process '%u' runs in Terminal Services session '%u'"),processId,sessionId);//该函数在tchar.h中 } else{ _tprintf(TEXT("Unable to get Terminal Services session ID for process '%u'"),processId); }
专有命名空间
我们可以自定义一个前缀(和Global和Local类似)并把它作为自己的专有命名空间使用,负责创建内核对象的服务器进程将定义一个边界描述符(Boundary Descriptor),以对命名空间的名称自身进行保护。
checkInstances函数
1、创建一个边界
2、如何将对应于本地管理员(Local Administrators)的一个安全描述符(security identifier, SID)和它关联起来
3、如何创建或者打开其名称被用途互斥量内核对象前缀的一个专有命名空间。
边界描述符将获得一个名称,但更重要的是,它会获得与它关联的一个特权用户组的SID,这样一来,Windows就可以确保只有在该用户隶属于这个特权组时,以其身份运行的应用程序才能在相同的边界中创建相同的使空间}
复制对象句柄
复制对象句柄可以通过调用函数DuplicateHandle,这个函数获得一个进程的句柄表中的记录项,然后在另一个进程的句柄表中创建这个记录项的一个副本。该函数最常见的一种用法可能涉及系统中同时运行的三个不同进程。举例说明该函数的工作方式:
进程S是源进程,它拥有对一个内核对象的访问权;
进程T是目标进程,它将获得对这个内核对象访问权。
进程C则是一个催化剂进程,它执行对DuplicateHandle的调用。
BOOL DuplicateHandle( HANDLE hSourceProcessHandle,// 拥有源句柄的那个进程的句柄。如源句柄从属于当前进程,则使用GetCurrentProcess HANDLE hSourceHandle, // 指定对象的现有句柄 HANDLE hTargetProcessHandle,// 即将拥有新对象句柄的一个进程的句柄。如源句柄从属于当前进程,则使用GetCurrentProcess LPHANDLE lpTargetHandle, // 指定用于装载新句柄的一个长整型变量 DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions );
实际中涉及三个不同进程时,复制对象句柄是很少使用的。通常只涉及两个进程的时候才会调用DuplicateHandle函数。如以下场景:
进程S能访问一个内核对象,并希望进程T也能访问这个对象,可以像如下方式使用
DuplicateHandle函数: /*** 在进程S中执行以下的代码 ***/ // 创建一个进程S能访问的互斥对象 HANDLE hObjInProcessS = CreateMutex(NULL, FALSE, NULL); // 得到进程T的进程句柄,dwProcessIdT是进程T的进程Id HANDLE hProcessT = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessIdT); // 相对进程T的未被初始化的句柄 HANDLE hObjInProcessT; // 使进程T能够访问互斥对象 DuplicateHandle(GetCurrentProcess(), hObjInProcessS, hProcessT, &hObjInProcessT, 0, FALSE, DUPLICATE_SAME_ACCESS); // 利用进程间通讯机制,在进程T中得到互斥对象句柄值 ...... // 当不需要与进程T通讯时,关闭得到的进程T句柄 CloseHandle(hProcessT); // 当进程S不再需要使用互斥对象时,也关闭之 CloseHandle(hObjInProcessS);
调用GetCurrentProcess()返回的是一个伪句柄,该句柄始终标识主调进程(即进程S)。当函数DuplicateHandle返回后,hObjInProcessT就是一个相对于进程T的句柄,它标识的对象就是进程S中hObjInProcessS所标识的对象。
注意在进程S中绝不要执行这句代码:
CloseHandle(hObjInProcessT);// Do not execute in processs