我们前面说过几种隐藏进程的方法:
遍历进程活动链表(ActiveProcessLinks)
但还是不能防止别人通过各种方法来隐藏进程,所以下面来介绍一种通过暴力搜索内存枚举进程的方法。
一个进程要运行,必然会加载到内存中。基于这个事实,隐藏进程要在目标机运行,在内存中一定会存在对应的EPROCESS结构体。基于系统内存搜索的进程监测技术利用EPROCESS结构体特征找到EPROCESS地址指针进而输出进程信息,可以有效地对进程进行全面的监测。
那我们应该搜索进程的什么结构?进程的PEB(PEB(Process Environment Block)——进程环境块)。主要原因是PEB地址的高4位是相同的。
可以看到除了system,其他进程的PEB都是0x7ffdxxxx.为什么?
找到wrk源码,MiCreatePebOrTeb这个函数负责分配peb和teb的地址。
贴出关键代码:
//判断是不是分配PEB地址,通过与PEB的大小比较
if (Size == sizeof(PEB)
#if defined(_WIN64)
|| (Size == sizeof(PEB32))
#endif
) {
PVOID HighestVadAddress;
LARGE_INTEGER CurrentTime;
//#define MM_HIGHEST_VAD_ADDRESS ((PVOID)((ULONG_PTR)MM_HIGHEST_USER_ADDRESS – (64 * 1024)))
//kd> dd MmHighestUserAddress
//829b9714 7ffeffff 80000000 7fff0000 80741000
//HighestVadAddress = 7ffe0000
HighestVadAddress = (PVOID) ((PCHAR)MM_HIGHEST_VAD_ADDRESS + 1);
KeQueryTickCount (&CurrentTime);
//PAGE_SHIFT其实就是12,那么((X64K >> PAGE_SHIFT) – 1)就是0xF。
//下面产生的随机数值其实是跟当前的时间有关的。
CurrentTime.LowPart &= ((X64K >> PAGE_SHIFT) – 1);
if (CurrentTime.LowPart <= 1) {
CurrentTime.LowPart = 2;
}
//
// Select a varying PEB address without fragmenting the address space.
//PEB 地址要求从14 个地址中取出一个: 0x7FFE0000 – (2<<12 ) /
//0x7FFE0000– (3<<12 )/ 0x7FFE0000– (4<<12 ) …0x7FFE0000– (15<<12 )
//最后用CurrentTime.LowPart<<PAGE_SHIFT的范围就是在
//0x2000到0xf000中,这样HighestVadAddress 的地址就在7ffd1000到7ffde000之间的14种情况了。
HighestVadAddress = (PVOID) ((PCHAR)HighestVadAddress – (CurrentTime.LowPart << PAGE_SHIFT));
if (MiCheckForConflictingVadExistence (TargetProcess, HighestVadAddress, (PVOID) ((PCHAR) HighestVadAddress + ROUND_TO_PAGES (Size) – 1)) == FALSE) {
//MiCheckForConflictingVadExistence 函数用来检查指定进程的地址中从
//HighestVadAddress 到(PCHAR) HighestVadAddress +
//ROUND_TO_PAGES (Size) – 1 是否已经分配了. 如果没有分配, 返回FALSE.表
// 示分配成功了.
// Got an address …
//
*Base = HighestVadAddress;
goto AllocatedAddress;
}
}
Status = MiFindEmptyAddressRangeDown (&TargetProcess->VadRoot,
ROUND_TO_PAGES (Size),
((PCHAR)MM_HIGHEST_VAD_ADDRESS + 1),
PAGE_SIZE,
Base);
if (!NT_SUCCESS(Status)) {
//
// No range was available, deallocate the Vad and return the status.
//
UNLOCK_ADDRESS_SPACE (TargetProcess);
ExFreePool (Vad);
return Status;
}
下面是这个过程的内存示意图:
但是从上面在win7 32位的环境下看,7ffdf000 也在分配范围内。
这个问题差不多解决了。
EPROCESS结构体就在系统内存的非分页缓冲池中,可根据非分页池的空间确定搜索范围,首先判断内存是否开启PAE模式及相应内存模式的页面是否有效,再判断该页所存储的信息是否为EPROCESS地址指针,最后判断进程是否结束,如果是非结束输出进程信息。
无论是确定内存模式还是判断页面是否有效程序首先需由用户态进入内核态。所以这就用到我们的驱动。
由于在32位处理器架构下,对内存的访问限制在4GB以下的空间。为了突破4GB的限制,现在的32位处理器采用一种叫PAE(物理地址扩展)的技术,来实现对超出4GB空间的物理地址的访问。PAE实际上采用了36位的地址总线,这样理论上可以支持64GB内存空间的寻址,确定内存模式有两种方法:
①从MmIsAddressValid中查询内存模式
MmIsAddressValid函数的功能是判断地址是否有效,在PAE内存模式与普通内存模式其代码是不相同的,用Windows内核调试工具windbg的命令“u MmIsAddressValid 1 50”可显示汇编代码,Windows 7系统PAE内存模式代码为:
0: kd> u MmIsAddressValid l 20
nt!MmIsAddressValid:
83e2c717 8bff mov edi,edi
83e2c719 55 push ebp
83e2c71a 8bec mov ebp,esp
83e2c71c 8b5508 mov edx,dword ptr [ebp+8]
83e2c71f e8c6260a00 call nt!MiIsAddressValid (83ecedea)
83e2c724 5d pop ebp
83e2c725 c20400 ret 4
83e2c728 90 nop
83e2c729 90 nop
83e2c72a 90 nop
83e2c72b 90 nop
83e2c72c 90 nop
Windows7系统非PAE内存模式代码为:
8400c180 8bff mov edi,edi
8400c182 55 push ebp
8400c183 8bec mov ebp,esp
8400c185 8b4d08 mov ecx,dword ptr[ebp+8]
…
可知PAE内存模式第四指令为“8b5508”,而非PAE内存模式第四指令为“8b4d08”,可据此编写自定义函数HowPaging区分内存模式。
普通内存模式判断页面是否有效的方法:
X86将物理地址空间分为4KB的小块,称为内存页。如此算来,4GB的内存就有1024*1024个内存页了。非PAE模式,处理器使用二级索引结构来管理内存页,一维叫做页目录(Page Directory),二维叫做页表( Page Table ),一个页目录包含1024项,称为页目录项PDE(Page Direcoty Entry),每个页目录项指向一个页表,这样我们就有1024个页表。每个页表也有1024项,称为页表项PTE(Page Table Entry ),每个页表项指向4KB页。
如果我们访问的虚拟地址所在页在物理内存里,虚拟地址所在页相应的PDE,PTE都是有效的。将其交给CPU转换成物理地址后访问就可以了。如果页不在物理内存中,那对应的PDE,PTE都是无效的。逻辑地址到物理地址的转换是由处理器完成的。非PAE模式,一个32位的逻辑地址被分成下图所示的3部分。
处理器首先根据CR3找到页目录的物理地址,然后由逻辑地址的前10位来索引对应的PDE,接着的10位来索引页表的PTE,从而得到PTE指向的4KB物理内存页,逻辑地址的低12位用来指向内存页中具体的位置,相当于页内的一个偏移。不难得出由虚拟地址addr得到PDE指针的计算公式为:PDE指针=页目录基址+[addr >> (页表索引位数+页内偏移)] * PDE长度。由虚拟地址addr得到PTE指针的计算公式为:PTE指针=页表基址 + (addr>>页内偏移)*PTE长度。页表索引位数为10,页内偏移为12,由windbg命令dt_hardware_pte可知PDE、PTE长度为32位即4字节。由windbg命令!pte 0(求虚拟地址0x0对应的PDE和PTE虚拟地址)可知页目录基址为0xc0300000,页表基址为0xc0000000.即PDE指针=0xc0300000+(addr >> 22) * 4,PTE指针=0xc0000000+(addr>>12)*4.
判断PDE的0位是否为1, 如果不为1则PDE无效, 对应的1024个页面不在物理内存中, 虚拟地址递增4Mb (1024*4Kb). 如果 PDE 的0位为1, 表示对应的1024个页面全部或部分在物理内存中, 则判断PDE的7位是否为1, 如果为1则表示对应的1024个页面全部在物理内存中. 如果PDE的7位为0, 表示对应的1024个页面中部分页面在物理内存中, 则判断PTE的0位是否为1, 如果为1说明PTE有效, 页在物理内存中, 如果为0则PTE无效, 页面不在物理内存中, 虚拟地址按4kb步进.
PAE内存模式判断页面是否有效的方法:
PAE 模式, 将原来的使用二级索引结构扩展为三级索引结构, 也就是在原来的页目录和页表基础上再增加一级, 称为页目录指针表, 每项64位. 32 位线性地址被分割为如下三个部分: 2 位(位30和位31)的页目录指针表索引, 用来索引本地址在页目录指针表中的对应表项. 9位(位21-29)的页目录表索引, 用来索引本地址在页目录表中的对应表项. 9 位(位12-20)的页表索引, 用来索引本地址在页表中的对应表项. 12 位(位0-11)的页内偏移.
页表索引位数为9, 页内偏移为12. 由windbg命令dt _hardware_pte可知 PDE、PTE长度为64位即8字节. 由windbg命令!pte 0(求虚拟地址0x0 对应的PDE 和PTE 的 虚 拟 地 址)可 知 页 目 录 基 址 为0xc0600000, 页 表 基 址 为0xc0000000. PDE 指 针=0xc0600000 + (addr>>21)*8, PTE指针 = 0xc0000000 + (addr >> 12)* 8.判断PAE模式虚拟地址所在页面是否有效的函数如下:
PAGE_STATUS CheckPageValid(DWORD addr)
{ pde = 0xc0600000 + (addr>>21)*8;
if( (*(PULONG)pde & 0xl )==0) return
INVALID_ PDE;
if( (*(PULONG)pde & 0xl ) ! =0 &&
(*(PULONG)pde&
0x80)!=0) return VALID_PAGE;
if( (*(PULONG)pde & 0xl ) ! =0 &&
(*(PULONG)pde&
0x80) == 0)
{pte = 0xc0000000 + ( addr >> 12 ) * 8 ;
return ( (*(PULONG)pte & 0xl) !=0) ?
VALID_PAGE:INVALID_PTE ; } }
判断该页所存储的信息是否为EPROCESS地址指针的方法。
当获取内存页以后, 就需要判断该页所存储的信息是否为EPROCESS地址指针, 须满足两个条件.
(1) 每个进程的EPROCESS结构体的PEB变量成员保存着PEB地址指针, 而PEB地址指针前半部分都相同, 均为“0x7FFD”, 所以可以通过比较PEB地址指针的前半部分来判断是否为EPROCESS地址指针.
(2) Windows 系统的各种资源以对象(Object)的形式来组织, 而各种Object的共有的信息(对象类型、对象的引用计数、句柄数等信息)保存在OBJECT_HEADER与其他的几个结构中. 换而言之, 在对象管理器内部, 不同类型的对象具有相同的对象头(Object Header), 但Object Body部分却是不同的.
Object Header结构体如下:
+0x000 PointerCount : Int4B
+0x004 HandleCount :Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Type : Ptr32 _OBJECT_TYPE
+0x00c NameInfoOffset : UChar
……
Object Header偏移0x008处Type成员为对象类型值,相同类型的对象具有相同的值. 自Window 7开始, _OBJECT_HEADER及其之前的一些结构发生了变化.
lkd> dt _object_header
nt!_OBJECT_HEADER
+0x000 PointerCount : Int4B
+0x004 HandleCount :Int4B
+0x004 NextToFree : Ptr32 Void
+0x008 Lock : _EX_PUSH_LOCK
+0x00c TypeIndex : UChar
……
可以看到, 0x008处的指向_OBJECT_TYPE的指针已经没有了, 取而代之的是在0x00c 处的类型索引值. 但Windows7中添加了一个函数ObGetObjectType, 返回Object_type对象指针.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | void NTAPI DetectProcessBySearchMemory() { PULONG pSystem; ULONG ulPoint; ULONG ulPebAddr; ULONG ulPageSign; GetObGetObjectTypeAddress(); pSystem = (PULONG)PsGetCurrentProcess(); for ( ulPoint = 0x80000000 ;ulPoint < 0x90000000 ;ulPoint += 4 ) { if ( NT_SUCCESS( MmIsAddressInPageValid( (PLONG)ulPoint ,&ulPageSign ) ) ) { ulPebAddr = *(PULONG)ulPoint; if ( ( ulPebAddr & 0xffff0000 ) == gPEBTypeAddr ) //搜索PEB { if ( NT_SUCCESS( MmIsProcessAddr( (PULONG)(ulPoint - PEB_OFFSET) ) ) ) { ShowProcessInformation( (PULONG)( ulPoint - PEB_OFFSET ) ); } } } else if ( ulPageSign == PTE_INVALID ) //当前页无效 { if ( ulPoint != 0x80000000 ) { ulPoint -= 4; } ulPoint += 0x1000; } else if ( ulPageSign == PDE_INVALID ) //当前页目录无效 { if ( ulPoint != 0x80000000 ) { ulPoint -= 4; } ulPoint += 0x400000; } else { KdPrint(( "Error!" )); } } } NTSTATUS NTAPI MmIsProcessAddr( IN PULONG Address ) { ULONG ulProcessType; ULONG ulAddr; ULONG ObjectTypeAddr; ULONG TypeIndexAddr; ULONG typeIndex; PULONG ObjectType; ulAddr = (ULONG)Address; if ( !NT_SUCCESS( MmIsAddressInPageValid( Address ,NULL ) ) ) { //KdPrint(( "Address is invalid" )); return STATUS_UNSUCCESSFUL; } ObjectType = (PULONG)ObGetObjectType((PVOID)ulAddr); if ( !NT_SUCCESS( MmIsAddressInPageValid( (PULONG)ObjectType ,NULL ) ) ) { //KdPrint(( "ObjectTypeAddr is invalid" )); return STATUS_UNSUCCESSFUL; } if ( *ObjectType == GetProcessType() ) { return STATUS_SUCCESS; } return STATUS_UNSUCCESSFUL; } void NTAPI ShowProcessInformation( IN PULONG Address ) { PLARGE_INTEGER pExitTime; ULONG PID; PUCHAR pFileName; char output[50]; pExitTime = (PLARGE_INTEGER)( (ULONG)Address + EXIT_TIME_OFFSET ); if ( pExitTime->QuadPart != 0 ) { return; } PID = *(PULONG)( (ULONG)Address + PROCESS_ID_OFFSET ); pFileName = (PUCHAR)( (ULONG)Address + FILE_NAME_OFFSET ); KdPrint(( "ID:%d\t\tName:%s\n" ,PID ,pFileName )); _stprintf(output, "%d|%s&", PID, pFileName); strcat(res, output); } |
提高搜索效率的方法:
32 位Windows系统允许每个进程拥有自己的4GB逻辑地址空间, 即0x00000000到0xFFFFFFFF. 4G 的逻辑地址空间中, 可供用户操作的只有低位的2GB(是在用户模式下可操控的), 高位的2GB 是windows核心模式使用的, 是内核态的地址空间, 即0x80000000 到0xFFFFFFFF, 这里存放着整个内核的代码和所有的内核模块, 以及内核所维护的数据,包括EPROCESS 结 构 体. 因 此 搜 索 范 围 初 步 确 定 为0x80000000到0xFFFFFFFF的2G空间. 在2G系统内存搜索Eprocess结构体效率低, 因此必须清楚x86系统地址空间分布规律, 缩小搜索范围.
x86系统地址空间分布如下:
(1) 0x80000000–0x9FFFFFFF: 引导系统(Ntoskrnl.exe和Hal.dll)和非分页缓冲池初始部分的系统代码.
(2) 0xA0000000–0xA3FFFFFF: 系统映射视图(如Win32k.sys)或者会话空间.
(3) 0xA4000000–0xBFFFFFFF: 附加系统页表项(PTE)或附加系统高速缓存.
(4) 0xC0000000–0xC03FFFFF: 进程页表和页目录, 描述虚拟地址映射的结构.
(5) 0xC0400000–0xC07FFFFF: 超空间和进程工作集列表.
(6) 0xC0800000–0xC0BFFFFF: 未使用区域, 不可访问.
(7) 0xC0C00000–0xC0FFFFFF: 系统工作集链表, 描述系统工作集的工作集链表数据结构.
(8) 0xC1000000–0xE0FFFFFF: 系统高速缓存, 用来映射在系统高速缓存中打开的文件的虚拟空间.
(9) 0xE1000000–0xEAFFFFFF: 分页缓冲池, 可分页系统内存堆.
(10) 0xEB000000–0xFFBDFFFF: 系统页表项和非分页缓冲池.
(11) 0xFFBE0000–0xFFFFFFFF: 系统性故障转储信息和硬件抽象层(HAL)使用区域.
分页缓冲池上分配的内存可以交换到虚拟内存,当程序需要这些页面的时候, 再读到内存. 非分页缓冲池里分配的内存是不能交换到虚拟内存上面的, 假如放到分页缓冲池并被交换到磁盘上时可能会发生灾难性的后果, 进程的EPROCESS结构体就在非分页缓冲 池 中. 因 此 进 程 的EPROCESS 结 构 体 在(1)0x80000000–0x9FFFFFFF(引 导系 统(Ntoskrnl.exe 和Hal.dll)和非分页缓冲池初始部分的系统代码.)及(10)0xEB0000000–0xFFBDFFFF(系统页表项和非分页缓冲池)两块区域. 这样大缩小了搜索范围.
(1) 0x80000000–0x9FFFFFFF还包含了引导系统(Ntoskrnl.exe和Hal.dll),(10) 0xEB0000000–0xFFBDFFFF还包含了系统页表项. 若能将这部分内存空间剔除, 能进一步缩小搜索范围. 而非分页池的空间由两块区域组成, 常规的区域是已经申请好物理内存的从内核变量MmNonPagedPoolStart始, MmNonPagedPool Star+MmSizeOfNonPagedPoolInBytes为结束,其中内核变量MmSizeOfNonPagedPoolInBytes为 非换页内存池的大小. 另外含一块扩展区域, 由内核变量MmNonPagedPoolExpansionStart 与 内 核 变 量MmNonPagedPoolEnd指定, 只有在内存不够时采取分配申请和释放的操作.
内核中FS寄存器指向KPCR(内核处理器控制域)的结构, 这是一个很有用的结构, 偏移送0x34 处有KdVersionBlock变量, KdVersionBlock 对应的结构体应该是_DBGKD_GET_VERSION64, 其 中 变 量DebuggerDataList 对 应 的 结 构 体 应 该 是_KDDEBUGGER_DATA64,其中包含MmNonPaged PoolStar、 MmNonPagedPoolEnd 等内核变量. 部分代码如下:
_asm
{ mov eax,fs:[0x01C]
mov eax,[eax+0x34]
mov KdVersionBlock,eax }
pKdData64=(PKDDEBUGGER_DATA64)*((PULONG)KdVersionBlock->DebuggerDataList);
则 *(PULONG)pKdData64->MmNonPagedPoolStart为非分页池空间的常规区域起始值.