Watch & Learn

Debugwar Blog

Step in or Step over, this is a problem ...

稳定暴力搜索EPROCESS结构获取系统进程列表

2021-11-21 11:09:09



在安全领域内核开发的过程中,获取进程信息属于一个常见需求。

由于存在潜在的对抗动作,笔者在项目中选择了遍历PspCidTable的技术方案——这样做的好处有二:

  1. 可以尽量保证获取的列表不因为各种Hook或Callback被过滤掉。
  2. 如果要做对抗,恶意程序也需要遍历这张表,大家的对抗成本至少在一个层面上。

(顺便说一句,网上有很多通过PspCidTable暴力搜索的资料,建议各位看一下)

然而随之而来的也有一系列的问题:

  1. 为了保证兼容性,笔者直接遍历了很大一块内存地址空间。这导致获取到的一些指针实际上并不指向任何的EPROCESS
  2. 即使指向的地址是合法的,但是本身并不是一个有效的EPROCESS结构。

解法


1. EPROCESS地址不合法导致蓝屏


通过观察不同的操作系统可以看到,进程的EPROCESS结构高地址基本和Idle或者SystemEPROCESS高地址相同,因此我们可以通过验证潜在EPROCESS的高地址是否和IdleSystem一致来快速排除非法的EPROCESS地址。


而通过观察系统的非分页内存起始地址可知,0xFFFFFA80`03602000开始为系统非分页内存,根据操作系统特性,这块地址是可以用MmIsAddressValid验证有效性的,而且由于是非分页内存,我们也不需要考虑这些内存被交换出去的情况。


So,总结一下,验证EPROCESS是否为合法地址的方式为:

  1. 64位置下其高6位、32位下其高1位地址与IdleSystemEPROCESS高位地址相同。
  2. MsIsAddressValid验证为true

2. 地址合法,但是本身不是一个EPROCESS结构


一开始笔者其实是以PidPPid是否能被4整除来验证是否为合法EPROCESS,但其实这种约束能力还是太弱,在遍历的过程中依然发现非常大的概率把不是EPROCESS的数据包含了进来。由于EPROCESS本身为一个非官方公开的结构,因此其在各个版本中EPROCESS结构会有细微的差别,这会导致一些关键字段的偏移不稳定。

因此需要另找兼容性强且稳定的方法。

通过观察x86与x64多个版本的Windows系统的EPROCESS结构:

  1. kd> dt _EPROCESS  
  2. nt!_EPROCESS  
  3.    +0x000 Pcb              : _KPROCESS  
  4.    …………  
  5.    +0x128 SectionObject    : Ptr32 Void  
  6.    +0x12c SectionBaseAddress : Ptr32 Void  
  7.    …………  

可以看到,其定义了一个SectionObjectSectionBaseAddress,而在所有系统上,这两个成员都是挨着的,换句话说,Offset(SectionBaseAddress) - sizeof(void*)一定是SectionObject的位置。而SectionObject,顾名思义——它是一个Section类型的对象:


如果我们可以检查SectionObject位置是否为一个类型为Section的对象,不就可以在一定程度上检查EPROCESS的合法性了吗?

然而前面也说过了, EPROCESS的成员结构在不同系统上是有变化的,我们如何稳定的获取SectionObject的位置呢?这需要引入一个Windows提供的函数:


Emmm,很好,虽然函数没有公开,但是系统导出了,这样我们就可以让系统自己告诉我们SectionBaseAddress的值是什么。然后我们拿着这个值在潜在的EPROCESS结构处搜索,在减一个指针长度的地方检查其指向的“对象”是否为Section类型就可以达到验证EPROCESS合法性的目的了。

So,再次总结一下:

  1. 调用PsGetProcessSectionBaseAddress获得SectionBaseAddress的值
  2. 搜索目标内存中包含这个值的位置
  3. 检查减1个指针长度位置处指向的“对象”是否为Section类型

最终方案


那么合并一下:

  1. 通过PspCidTable定位到包含潜在EPROCESS地址的表。
  2. 64位置下其高6位、32位下其高1位地址与IdelSystemEPROCESS高位地址相同。
  3. MsIsAddressValid验证为true。
  4. 检查潜在的EPROCESS是否为Process类型的对象。
  5. 调用PsGetProcessSectionBaseAddress获得SectionBaseAddress的值。
  6. 搜索目标内存中包含这个值的位置。
  7. 检查减1个指针长度位置处指向的“对象”是否为Section类型。

以上就是暴力搜索EPROCESS表有效项+不蓝屏的解决方案了。

Coding Time


  1. // TableEntry为通过PspCidTable获取到的潜在存放EPROCESS的表  
  2. for (i = 0; i < 4096; TableEntry++, i++)  
  3. {  
  4.     if (TableEntry && MmIsAddressValid(TableEntry))  
  5.     {  
  6.         ULONG_PTR Align = 0;  
  7.         PEPROCESS EProcess = NULL;  
  8.         NTSTATUS status = STATUS_UNSUCCESSFUL;  
  9.         UNICODE_STRING ProcessType = {0}, Type = {0};  
  10.   
  11.         // 从TableEntry中抽取潜在EPROCESS  
  12.         EProcess = _ps_PsGetEProcessObjectFromTableEntry(TableEntry);  
  13.         // 检查是否为"看上去"合法的地址  
  14.         if(EProcess == 0 || !MmIsAddressValid(EProcess))  
  15.         {  
  16.             continue;  
  17.         }  
  18.   
  19. #ifdef _WIN64  
  20.         // 这里更大胆了一点,取了高5位(文中说高6位是比较保守的做法)  
  21.         Align = 0xFFFFF00000000000;  
  22. #else  
  23.         Align = 0xF0000000;  
  24. #endif  
  25.   
  26.         if ( \  
  27.             (ULONG_PTR)((ULONG_PTR)EProcessIdle & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align) || \  
  28.             (ULONG_PTR)((ULONG_PTR)EProcessSystem & Align) == (ULONG_PTR)((ULONG_PTR)EProcess & Align)  
  29.         )  
  30.         {  
  31.             RtlInitUnicodeString(&ProcessType, L"Process");  
  32.             status = _ob_ObjGetTypeUnicodeStringByObject(EProcess, &Type);  
  33.             // 检查潜在的EPROCESS是否为Process类型对象  
  34.             if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&ProcessType, &Type, TRUE))  
  35.             {  
  36.                 UNICODE_STRING SectionType = {0};  
  37.                 PUCHAR Address = EProcess;  
  38.                 ULONG_PTR SectionBaseAddress = NULL, SectionObject = NULL;  
  39.                 RtlFreeUnicodeString(&Type);  
  40.                 RtlInitUnicodeString(&SectionType, L"Section");  
  41.                 ARK_DECLARE_ROUTINE(PsGetProcessSectionBaseAddress);  
  42.                 SectionBaseAddress = PsGetProcessSectionBaseAddress(EProcess);  
  43.                 // first search `SectionBaseAddress` offset in `EPROCESS`  
  44.                 // 搜索包含SectionBaseAddress值的地址  
  45.                 while(Address <= Address + 0x500)  
  46.                 {  
  47.                     // find a possiable `SectionBaseAddress` position  
  48.                     if (*(PULONG_PTR)(Address) == SectionBaseAddress)  
  49.                     {  
  50.                         // then get `SectionObject` in `EPROCESS`  
  51.                         // this member usually just before `SectionBaseAddress`  
  52.                         // 获取减一个指针长度处的对象  
  53.                         SectionObject = *(PULONG_PTR)(Address - 1 * sizeof(PULONG_PTR));  
  54.                         status = _ob_ObjGetTypeUnicodeStringByObject(SectionObject, &Type);  
  55.                         // if it is a valid EPROCESS  
  56.                         // 检查是否为Section类型对象  
  57.                         if (NT_SUCCESS(status) && 0 == RtlCompareUnicodeString(&SectionType, &Type, TRUE))  
  58.                         {  
  59.                             RtlFreeUnicodeString(&Type);  
  60.                             // 所有检查通过,为有效EPROCESS结构,加入到链表中  
  61.                             _ps_PspInsertContextNode(ProcessContext, EProcess);  
  62.                         }  
  63.                         break;  
  64.                     }  
  65.                     ++Address;  
  66.                 }  
  67.             }      
  68.         }  
  69.     }  
  70. }  

上述代码中的:_ps_PsGetEProcessObjectFromTableEntry_ob_ObjGetTypeUnicodeStringByObject的作用分别是从传入的内存中安全的获取想要的内容,各位可以按照自己的需求实现。

效果