网络安全 频道

检测隐藏进程

在内核中使用ZwQuerySystemInformation函数得到进程列表。

我们先从最简单的方式开始,通过ZwQuerySystemInformation函数得到进程列表:

Code:

PVOID GetNativeProcessList(ULONG *MemSize)
{
ULONG PsCount = 0;
PVOID Info = GetInfoTable(SystemProcessesAndThreadsInformation);
PSYSTEM_PROCESSES Proc;
PVOID Mem = NULL;
PProcessRecord Data;

if (!Info) return NULL; else Proc = Info;

do
{
Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
PsCount++;
} while (Proc->NextEntryDelta);

*MemSize = (PsCount + 1) * sizeof(TProcessRecord);

Mem = ExAllocatePool(PagedPool, *MemSize);

if (!Mem) return NULL; else Data = Mem;

Proc = Info;
do
{
Proc = (PSYSTEM_PROCESSES)((ULONG)Proc + Proc->NextEntryDelta);
wcstombs(Data->ProcessName, Proc->ProcessName.Buffer, 255);
Data->Present = TRUE;
Data->ProcessId = Proc->ProcessId;
Data->ParrentPID = Proc->InheritedFromProcessId;
PsLookupProcessByProcessId((HANDLE)Proc->ProcessId, &Data->pEPROCESS);
ObDereferenceObject(Data->pEPROCESS);
Data++;
} while (Proc->NextEntryDelta);

Data->Present = FALSE;

ExFreePool(Info);

return Mem;
}

以这个函数做参考,任何内核态的隐藏进程都不会被检测出来,但是所有的用户态隐藏进程如hxdef是绝对逃不掉的。

在下面的代码中我们可以简单地用GetInfoTable函数来得到信息。为了防止有人问那是什么东西,下面列出完整的函数代码。

Code:

/*
Receiving buffer with results from ZwQuerySystemInformation.
*/
PVOID GetInfoTable(ULONG ATableType)
{
ULONG mSize = 0x4000;
PVOID mPtr = NULL;
NTSTATUS St;
do
{
mPtr = ExAllocatePool(PagedPool, mSize);
memset(mPtr, 0, mSize);
if (mPtr)
{
St = ZwQuerySystemInformation(ATableType, mPtr, mSize, NULL);
} else return NULL;
if (St == STATUS_INFO_LENGTH_MISMATCH)
{
ExFreePool(mPtr);
mSize = mSize * 2;
}
} while (St == STATUS_INFO_LENGTH_MISMATCH);
if (St == STATUS_SUCCESS) return mPtr;
ExFreePool(mPtr);
return NULL;
}

我认为这段代码是很容易理解的...

利用EPROCESS结构的双向链表得到进程列表。
我们又进了一步。接下来我们将通过遍历EPROCESS结构的双向链表来得到进程列表。链表的表头是PsActiveProcessHead,因此要想正确地枚举

进程我们需要找到这个并没有被导出的符号。在这之前我们应该知道System进程是所有进程列表中的第一个进程。在DriverEntry例程开始时我

们需要用PsGetCurrentProcess函数得到当前进程的指针(使用SC管理器的API或者ZwLoadDriver函数加载的驱动始终都是加载到System进程的

上下文中的),BLink在ActiveProcessLinks中的偏移将指向PsActiveProcessHead。像这样:

Code:

PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4);

现在就可以遍历这个双向链表来创建进程列表了:

Code:

PVOID GetEprocessProcessList(ULONG *MemSize)
{
PLIST_ENTRY Process;
ULONG PsCount = 0;
PVOID Mem = NULL;
PProcessRecord Data;

if (!PsActiveProcessHead) return NULL;

Process = PsActiveProcessHead->Flink;

while (Process != PsActiveProcessHead)
{
PsCount++;
Process = Process->Flink;
}

PsCount++;

*MemSize = PsCount * sizeof(TProcessRecord);

Mem = ExAllocatePool(PagedPool, *MemSize);
memset(Mem, 0, *MemSize);

if (!Mem) return NULL; else Data = Mem;

Process = PsActiveProcessHead->Flink;

while (Process != PsActiveProcessHead)
{
Data->Present = TRUE;
Data->ProcessId = *(PULONG)((ULONG)Process - ActPsLink + pIdOffset);
Data->ParrentPID = *(PULONG)((ULONG)Process - ActPsLink + ppIdOffset);
Data->SignalState = *(PULONG)((ULONG)Process - ActPsLink + 4);
Data->pEPROCESS = (PEPROCESS)((ULONG)Process - ActPsLink);
strncpy(Data->ProcessName, (PVOID)((ULONG)Process - ActPsLink + NameOffset), 16);
Data++;
Process = Process->Flink;

}

return Mem;
}

为了得到进程名称、ID和父进程ID,我们利用它们在EPROCESS结构中的偏移地址(pIdOffset, ppIdOffset, NameOffset, ActPsLink)。这些

偏移随着
Windows系统版本的不同而不同,因此我们要在进程检测程序的代码中进行区分后得到他们正确的值(附录)。

任何一个通过API截取方式隐藏的进程都将被上面这个方法检测出来。但是如果进程是通过DKOM(直接处理内核对象 - Direct Kernel Object

Manipulation)方式隐藏,那这个方法就失效了,因为这种进程都被从进程链表中删掉了。

通过列举调度程序(scheduler)中的线程得到进程列表。

对付这种隐藏进程(俄文翻译kao注:这个地方原文写的比较模糊,作者大概的意思应该是“使用DKOM的方式检测隐藏进程”)的其中一种检测

方式是通过调度程序(scheduler)中的线程列表来得到进程列表。
Windows 2000有三个维护线程的双向链表(KiWaitInListHead,

KiWaitOutListHead, KiDispatcherReadyListHead)。前面两个链表包含等待某种事件的线程,最后面的链表包含的是等待执行的线程。我们

处理这些链表,根据线程链表结构ETHREAD中的偏移就可以得到一个线程的ETHREAD指针(俄文翻译kao注:原文中这句话实在是太难懂了,希望

我翻译的正确)。这个结构包括了很多进程相关指针,也就是结构_KPROCESS *Process(0x44, 0x150)和结构_EPROCESS *ThreadsProcess

(0x22C, 这仅是
Windows 2000中的偏移量)。前面两个指针对于一个线程的功能性没有任何影响,因此可以很容易修改它们来隐藏进程。相反,

第三个指针是当切换地址空间时调度程序(schedler)使用的指针,所以这个指针是不能修改的。我们就用它来找到拥有某个线程的进程。

Klister就是使用了这种检测方法,它的最大的缺点就是只能在
Windows 2000平台上工作(但是在这个平台上某个补丁包也会让它失效)。导致

这个情况发生的原因就是这种程序使用了硬编码的线程链表地址,而在每个补丁包中这些地址可能都是不同的。

在程序中使用硬编码地址是很糟糕的解决方案,操作系统的升级就会使你的程序无法正常工作,要尽量避免使用这种检测方法。所以应该通过

分析那些使用了这些链表的内核函数来动态地得到它们的地址。

首先我们试试看在
Windows 2000平台上找出KiWaitInListHead和KiWaitOutListHead.使用链表地址的函数KeWaitForSingleObject代码如下:

Code:

.text:0042DE56 mov ecx, offset KiWaitInListHead
.text:0042DE5B test al, al
.text:0042DE5D jz short loc_42DE6E
.text:0042DE5F cmp byte ptr [esi+135h], 0
.text:0042DE66 jz short loc_42DE6E
.text:0042DE68 cmp byte ptr [esi+33h], 19h
.text:0042DE6C jl short loc_42DE73
.text:0042DE6E mov ecx, offset KiWaitOutListHead

我们使用反汇编器(用我写的LDasm)反汇编KeWaitForSingleObject函数来获得这些地址。当索引(pOpcode)指向指令“mov ecx,

KiWaitInListHead”,(pOpcode + 5)指向的就是指令“test al, al”,(pOpcode + 24)指向的就是“mov ecx, KiWaitOutListHead”。

这样我们就可以通过索引(pOpcode + 1)和(pOpcode + 25)正确地得到KiWaitInListHead和KiWaitOutListHead的地址了。搜索地址的代码

如下:

Code:

void Win2KGetKiWaitInOutListHeads()
{
PUCHAR cPtr, pOpcode;
ULONG Length;

for (cPtr = (PUCHAR)KeWaitForSingleObject;
cPtr < (PUCHAR)KeWaitForSingleObject + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);

if (!Length) break;

if (*pOpcode == 0xB9 && *(pOpcode + 5) == 0x84 && *(pOpcode + 24) == 0xB9)
{
KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 1);
KiWaitOutListHead = *(PLIST_ENTRY *)(pOpcode + 25);
break;
}
}

return;
}

Windows 2000平台下我们可以用同样的方法得到KiDispatcherReadyListHead, 搜索KeSetAffinityThread函数:

Code:

.text:0042FAAA lea eax, KiDispatcherReadyListHead[ecx*8]
.text:0042FAB1 cmp [eax], eax

搜索KiDispatcherReadyListHead函数的代码:

Code:

void Win2KGetKiDispatcherReadyListHead()
{
PUCHAR cPtr, pOpcode;
ULONG Length;

for (cPtr = (PUCHAR)KeSetAffinityThread;
cPtr < (PUCHAR)KeSetAffinityThread + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);

if (!Length) break;

if (*(PUSHORT)pOpcode == 0x048D && *(pOpcode + 2) == 0xCD && *(pOpcode + 7) == 0x39)
{
KiDispatcherReadyListHead = *(PVOID *)(pOpcode + 3);
break;
}
}

return;
}

不幸的是,
Windows XP内核完全不同于Windows 2000内核。XP下的调度程序(scheduler)只有两个线程链表:KiWaitListHead和

KiDispatcherReadyListHead。我们可以通过搜索KeDelayExecutionThread函数来查找KeWaitListHead:

Code:

.text:004055B5 mov dword ptr [ebx], offset KiWaitListHead
.text:004055BB mov [ebx+4], eax

搜索代码如下:

Code:

void XPGetKiWaitListHead()
{
PUCHAR cPtr, pOpcode;
ULONG Length;

for (cPtr = (PUCHAR)KeDelayExecutionThread;
cPtr < (PUCHAR)KeDelayExecutionThread + PAGE_SIZE;
cPtr += Length)
{
Length = SizeOfCode(cPtr, &pOpcode);

if (!Length) break;

if (*(PUSHORT)cPtr == 0x03C7 && *(PUSHORT)(pOpcode + 6) == 0x4389)
{
KiWaitInListHead = *(PLIST_ENTRY *)(pOpcode + 2);
break;
}
}

return;
}

0
相关文章