在安全领域内核开发的过程中,获取进程信息属于一个常见需求。
由于存在潜在的对抗动作,笔者在项目中选择了遍历PspCidTable的技术方案——这样做的好处有二:
- 可以尽量保证获取的列表不因为各种Hook或Callback被过滤掉。
- 如果要做对抗,恶意程序也需要遍历这张表,大家的对抗成本至少在一个层面上。
(顺便说一句,网上有很多通过PspCidTable暴力搜索的资料,建议各位看一下)
然而随之而来的也有一系列的问题:
- 为了保证兼容性,笔者直接遍历了很大一块内存地址空间。这导致获取到的一些指针实际上并不指向任何的EPROCESS。
- 即使指向的地址是合法的,但是本身并不是一个有效的EPROCESS结构。
解法
1. EPROCESS地址不合法导致蓝屏
通过观察不同的操作系统可以看到,进程的EPROCESS结构高地址基本和Idle或者System的EPROCESS高地址相同,因此我们可以通过验证潜在EPROCESS的高地址是否和Idle或System一致来快速排除非法的EPROCESS地址。
而通过观察系统的非分页内存起始地址可知,0xFFFFFA80`03602000开始为系统非分页内存,根据操作系统特性,这块地址是可以用MmIsAddressValid验证有效性的,而且由于是非分页内存,我们也不需要考虑这些内存被交换出去的情况。

So,总结一下,验证EPROCESS是否为合法地址的方式为:
- 64位置下其高6位、32位下其高1位地址与Idle或System的EPROCESS高位地址相同。
- MmIsAddressValid验证为true
2. 地址合法,但是本身不是一个EPROCESS结构
一开始笔者其实是以Pid或PPid是否能被4整除来验证是否为合法EPROCESS,但其实这种约束能力还是太弱,在遍历的过程中依然发现非常大的概率把不是EPROCESS的数据包含了进来。由于EPROCESS本身为一个非官方公开的结构,因此其在各个版本中EPROCESS结构会有细微的差别,这会导致一些关键字段的偏移不稳定。
因此需要另找兼容性强且稳定的方法。
通过观察x86与x64多个版本的Windows系统的EPROCESS结构:
- kd> dt _EPROCESS
- nt!_EPROCESS
- +0x000 Pcb : _KPROCESS
- …………
- +0x128 SectionObject : Ptr32 Void
- +0x12c SectionBaseAddress : Ptr32 Void
- …………
可以看到,其定义了一个SectionObject和SectionBaseAddress,而在所有系统上,这两个成员都是挨着的,换句话说,Offset(SectionBaseAddress) - sizeof(void*)一定是SectionObject的位置。而SectionObject,顾名思义——它是一个Section类型的对象:

如果我们可以检查SectionObject位置是否为一个类型为Section的对象,不就可以在一定程度上检查EPROCESS的合法性了吗?
然而前面也说过了, EPROCESS的成员结构在不同系统上是有变化的,我们如何稳定的获取SectionObject的位置呢?这需要引入一个Windows提供的函数:

Emmm,很好,虽然函数没有公开,但是系统导出了,这样我们就可以让系统自己告诉我们SectionBaseAddress的值是什么。然后我们拿着这个值在潜在的EPROCESS结构处搜索,在减一个指针长度的地方检查其指向的“对象”是否为Section类型就可以达到验证EPROCESS合法性的目的了。
So,再次总结一下:
- 调用PsGetProcessSectionBaseAddress获得SectionBaseAddress的值
- 搜索目标内存中包含这个值的位置
- 检查减1个指针长度位置处指向的“对象”是否为Section类型
最终方案
那么合并一下:
- 通过PspCidTable定位到包含潜在EPROCESS地址的表。
- 64位置下其高6位、32位下其高1位地址与Idel或System的EPROCESS高位地址相同。
- MmIsAddressValid验证为true。
- 检查潜在的EPROCESS是否为Process类型的对象。
- 调用PsGetProcessSectionBaseAddress获得SectionBaseAddress的值。
- 搜索目标内存中包含这个值的位置。
- 检查减1个指针长度位置处指向的“对象”是否为Section类型。
以上就是暴力搜索EPROCESS表有效项+不蓝屏的解决方案了。
Coding Time
- // TableEntry为通过PspCidTable获取到的潜在存放EPROCESS的表
- for (i = 0; i < 4096; TableEntry++, i++)
- {
- if (TableEntry && MmIsAddressValid(TableEntry))
- {
- ULONG_PTR Align = 0;
- PEPROCESS EProcess = NULL;
- NTSTATUS status = STATUS_UNSUCCESSFUL;
- UNICODE_STRING ProcessType = {0}, Type = {0};
- // 从TableEntry中抽取潜在EPROCESS
- EProcess = _ps_PsGetEProcessObjectFromTableEntry(TableEntry);
- // 检查是否为"看上去"合法的地址
- if(EProcess == 0 || !MmIsAddressValid(EProcess))
- {
- continue;
- }
- #ifdef _WIN64
- // 这里更大胆了一点,取了高5位(文中说高6位是比较保守的做法)
- Align = 0xFFFFF00000000000;
- #else
- Align = 0xF0000000;
- #endif
- if ( \
- (ULONG_PTR)((ULONG_PTR)EProcessIdle & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align) || \
- (ULONG_PTR)((ULONG_PTR)EProcessSystem & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align)
- )
- {
- RtlInitUnicodeString(&ProcessType, L"Process");
- status = _ob_ObjGetTypeUnicodeStringByObject(EProcess, &Type);
- // 检查潜在的EPROCESS是否为Process类型对象
- if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&ProcessType, &Type, TRUE))
- {
- UNICODE_STRING SectionType = {0};
- PUCHAR Address = EProcess;
- ULONG_PTR SectionBaseAddress = NULL, SectionObject = NULL;
- RtlFreeUnicodeString(&Type);
- RtlInitUnicodeString(&SectionType, L"Section");
- ARK_DECLARE_ROUTINE(PsGetProcessSectionBaseAddress);
- SectionBaseAddress = PsGetProcessSectionBaseAddress(EProcess);
- // first search `SectionBaseAddress` offset in `EPROCESS`
- // 搜索包含SectionBaseAddress值的地址
- while(Address <= Address + 0x500)
- {
- // find a possiable `SectionBaseAddress` position
- if (*(PULONG_PTR)(Address) == SectionBaseAddress)
- {
- // then get `SectionObject` in `EPROCESS`
- // this member usually just before `SectionBaseAddress`
- // 获取减一个指针长度处的对象
- SectionObject = *(PULONG_PTR)(Address - 1 * sizeof(PULONG_PTR));
- status = _ob_ObjGetTypeUnicodeStringByObject(SectionObject, &Type);
- // if it is a valid EPROCESS
- // 检查是否为Section类型对象
- if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&SectionType, &Type, TRUE))
- {
- RtlFreeUnicodeString(&Type);
- // 所有检查通过,为有效EPROCESS结构,加入到链表中
- _ps_PspInsertContextNode(ProcessContext, EProcess);
- }
- break;
- }
- ++Address;
- }
- }
- }
- }
- }
上述代码中的:_ps_PsGetEProcessObjectFromTableEntry和_ob_ObjGetTypeUnicodeStringByObject的作用分别是从传入的内存中安全的获取想要的内容,各位可以按照自己的需求实现。
效果
对了,差点忘记说了:
- Idle进程是不在PspCidTable中的,需要用PCR结构获取。
- System进程在PspCidTable中,但是他的SectionObject结构是空的,这个进程可以通过验证pid是否为4以及PsGetProcessImageFileName的结果是否为System来确认。