首页
社区
课程
招聘
[原创]Windows内核学习笔记之进程(下)
发表于: 2021-12-16 16:20 17947

[原创]Windows内核学习笔记之进程(下)

2021-12-16 16:20
17947

上篇:Windows内核学习笔记之进程(上)

Windows执行体实现了一套对象机制来管理各种资源或实体。每种对象都有一个类型对象,类型对象定义了该类对象的一些特性和方法。对象管理器也定义了一个全局名字空间,提供了根据名称来解析对象的同一机制。类型对象通过提供自定义的Parse方法可以扩展此名字空间。对象管理器中的对象是执行体对象,它位于系统空间,考虑到安全性,在进程空间不能直接通过地址来引用它们。

在Windows系统中需要使用到句柄(handle)来管理进程中的对象引用。当一个进程利用名称来创建或打开一个对象时,将获得一个句柄,该句柄指向所创建或打开的对象。以后,该进程无须使用名称来引用对象,使用此句柄即可访问。这样即保证了安全性,也提高了引用对象的效率。当两个应用程序以共享方式打开了同一个文件,那么,它们将分别得到各种的句柄,且都可以通过句柄操作该文件。尽管两个应用程序得到的句柄的值并不相同,但是这两个句柄所指的文件却是同一个。因此,句柄只是一个对象引用,同一个对象在不同的环境下可能有不同的引用(句柄)值。

在Windows系统中,句柄是进程范围内的对象引用,换句话说,句柄仅在一个进程范围内才有效。一个进程的句柄传递给另一个进程后,句柄值将不再有效

实际上,Windows支持的句柄是一个索引,指向该进程句柄表中的一个表项。进程句柄表由EPROCESS结构中的ObjectTable域来指向。句柄表第一项索引是4,第二项索引是8,依次类推。一个进程的句柄表包含了所有已被该进程打开的对象的指针。ObjectTable的类型为HANDLE_TABLE,该结构定义如下:

TableCode域是一个指针,指向句柄表最高层表项页面,它的低2位代表了当前句柄表的层数,具体情况如下

0:句柄表只有一层,此时进程最多容纳512个句柄

1:句柄表有两层,此时进程最多可容易512 * 1024 个句柄

2:句柄表有三层,三层树结构最多可容纳的句柄数是512 * 1024 * 1024,但是Windows执行体限定每个进程的句柄数不得超过2^24=16777246

下图显示了这三种情形,实际上,在每个最底层句柄表页面中,第一个句柄表项都有特殊用途,真正供进程使用的句柄表项是511个

最低层句柄表每一项所指的都是一个句柄表项,句柄表项结构为HANDLE_TABLE_ENTRY,占8个字节,定义如下

Object指针所指的就是句柄所代表的内核对象,它的最低3位有特殊的含义

因此,想要获得句柄对象的地址需要将Object的低3位清0,但是此时所得到的对象地址指向的是对象头,偏移0x18的地址才是对象的真正地址。

接下来通过WinDbg来查找句柄对象来验证上述内容,首先使用如下代码来创建test进程,而该进程则是要得到进程PID为1488的进程句柄

此时这个进程的PID是一个记事本进程的PID

在WinDbg中查找test进程的EPROCESS地址

通过该地址来获得ObjectTable的值,该值保存了此进程HANDLE_TABLE的地址

根据该值来获得TableCode

该值的低2位为0,所以这是一个单层句柄表结构,所以TableCode所指的地址保存了512个HADLE_TABLE_ENTRY。而要得知打开的记事本进程对象是在句柄表中的位置,就需要通过句柄值进行索引,下图可以知道此次的句柄值为0x7E8,由于句柄的索引是从4开始每次递增4,所以将7E8除以4得到1FA就是句柄表的索引

有了索引就可以在句柄表中查找对应的句柄对应的对象头地址

此时将低3位清0就得到了对象头地址,偏移0x18处就是句柄对应的内核对象

偏移0x174的地址保存了进程对象的名称为notepad.exe,证明上述内容正确。

从上面的内容可以知道,要为一个进程创建句柄并将其插入句柄表的时候,需要为句柄准备一个临时的HANDLE_TABLE_ENTRY数据结构,使其指向这个对象的头部,然后将该结构插入到相应的句柄表中。这里就有两个问题,一个是填充HANDLE_TABLE_ENTRY数据结构,用来表示要插入的对象,另一个是在相应的句柄表中将其插入。

实现句柄插创建的函数为ObpCreateHandle,在函数最开始就是取出对象的对象类型和对象头赋值到相应的变量

接下来会判断传入的属性是否具有内核标记并把对象头赋给局部变量,这样就构造好了要插入句柄表的句柄表项,接下来就是要选择合适的句柄表

当不具有内核标记时候,会从当前进程中获取要插入的句柄表

如果具备内核标记,则选择内核句柄表作为要插入的句柄表,并且会判断当前进程是否是系统进程,如果不是则会调用函数KiStackAttachProcess附加上去

选择了合适的句柄表以及构造好了句柄表项以后,就要调用ExCreateHandle来将句柄表项插入到句柄表中,该函数会将要插入的句柄表地址和句柄表项地址依次作为参数入栈,返回值为相应的句柄

在ExCreateHandle中会调用ExpAllocateHandleTableEntry,该函数的第一个参数是要插入的句柄表项地址,第二个参数用来保存得到的句柄,返回值为新分配的句柄表项地址

如果分配成功,接下来就要把传入的句柄表项内容赋值到分配的句柄表项

最后,函数将句柄作为返回值赋给eax

在进程创建的系统调用NtCreateProcess中,会调用ObInsertObject来将句柄插入到相应的句柄表中。而该函数首先是对参数进行各种检测,随后就是通过调用ObpCreateHandle来完成句柄的插入

进程的句柄表是每个进程私有的,当前进程的句柄表中保存的句柄是无法被其他进程使用的。而系统同时维护了一张全局句柄表,该句柄表用来保存所有的进程以及线程。

在进程创建的系统调用中,会执行下面的代码来将新建的进程插入到全局句柄表中

由上面的内容可以知道,此时插入的这张全局句柄表名称叫做PspCidTable,并且作为作为返回值的句柄被用作赋值进程的PID。可想而知,进程的PID此时就是全局句柄表的索引。

还需要注意的是,此时插入到全局句柄表中的就是进程内核对象本身,而不是对象头,所以在全局句柄表中获取到的地址就是进程内核对象的地址。

接下来依然通过实验验证上面内容,首先打开一个记事本进程并且获取进程的PID,此时PID为1736,除以4得到0x1B2就是句柄表的索引

接下来使用WinDbg获取全局句柄表的地址

通过该地址获取TableCode

低2位为0,说明是单句柄表结构。在根据索引,获取句柄表项


将Object的低3位清0,得到的就是进程的内核对象地址

根据偏移0x174中保存的进程名为notepad.exe可以得知上述结论正确。

以下的这些常用函数获取相应的进程或线程的方式就是通过全局句柄表来实现的

PsLookupProcessThreadByCid

PsLookupProcessByProcessId

PsLookupThreadByThreadId

进程可以调用ExitProcess函数,从而"优雅地"退出。对于大部分进程,当进程的第一个线程从其主函数返回时,该线程的进程启动代码会代表该进程调用ExitProcess。"优雅地"这个词意味着载入该进程的DLL将有机会在接获进程即将退出的通知后,使用DLL_PROCESS_DETACH调用字节的DllMain函数执行一些工作。

ExitProcess只能由字节要求退出的进程调用。但如果用TerminateProcess函数,也可也以"不优雅"的方式中止进程,该函数还可以从进程外部调用。TerminateProcess要求使用PROCESS_TERMINATE访问掩码打开一个到进程的句柄,该句柄可能被允许也可能被拒绝。这也是某些进程(如Csrss)很难终止的原因(发出请求的用户无法获得具备所需掩码的句柄)。

此处"不优雅"意味着DLL将没机会执行代码,并且所有线程会被突然终止。某些情况下导致数据丢失,例如客户端缓存没机会将其中的数据写回到目标文件。

无论哪种方式,最终在内执行体都是通过NtTerminateProcess来结束进程的,在该函数中首先会获取当前进程的EPROCESS和当前线程的ETHREAD,并且会判断是否传入的进程句柄,如果传入了则会将局部变量var_HasHandle赋值为1

如果没有则会将进程句柄赋值为当前进程的句柄并把局部变量var_HasHandle赋值为0

调用ObReferenceObjectByHandle来获取进程内核对象EPROCESS

调用ExAcquireRundownProtection来获得进程销毁保护锁,并且会判断是否传入了进程句柄,如果传入了则会将句柄值与8或操作

调用函数来获取进程的线程对象


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-12-21 18:23 被1900编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回
//