快捷搜索:   nginx

检测隐藏进程(2)


是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多,因为它们都是基于同Windows内核相同的原理实现的,并且从这些内核

数据结构中删除进程将导致该进程完全失效。

内核中的进程是什么?每一个进程都有自己的地址空间,描述符,线程等,内核的数据结构就涉及这些东西。每一个进程都是由EPROCESS结构

描述,而所有进程的结构都被一个双向循环链表维护。进程隐藏的一个方法就是改变进程结构链表的指针,使得链表枚举跳过自身达到进程隐

藏的目的。避开进程枚举并不影响进程的任何功能。无论怎样,EPROCESS结构总是存在的,对一个进程的正常功能来说它是必要的。在内核态

检测隐藏进程的主要方法就是对这个结构的检查。

我们应该定义一下将要储存的进程信息的变量格式。这个变量格式应该很方便地存储来自驱动的数据(附录)。结构定义如下:

Code:

typedef struct _ProcessRecord
{
ULONG Visibles;
ULONG SignalState;
BOOLEAN Present;
ULONG ProcessId;
ULONG ParrentPID;
PEPROCESS pEPROCESS;
CHAR ProcessName[256];
} TProcessRecord, *PProcessRecord;

应该为这些结构分配连续的大块的内存,并且不设置最后一个结构的Present标志。

在内核中使用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;
}

最困难的是查找KiDispatcherReadyListHead。主要的问题是KiDispatcherReadyListHead的地址并没有被任何一个导出的函数使用。因此就要

用更加复杂的搜索算法搞定它。就从KiDispatchInterrupt函数开始,我们感兴趣的地方只有这里:

Code:

.text:00404E72 mov byte ptr [edi+50h], 1
.text:00404E76 call sub_404C5A
.text:00404E7B mov cl, 1
.text:00404E7D call sub_404EB9

这段代码中的第一个函数调用指向的就是包含KiDispatcherReadyListHead引用的函数。尽管如此,搜索KiDispatcherReadyListHead的地址却

变的更加复杂,因为这个函数的相关代码在Windows XP SP1和SP2中是不同的。在SP2中它是这个样子:

Code:

.text:00404CCD add eax, 60h
.text:00404CD0 test bl, bl
.text:00404CD2 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:00404CD9 jnz loc_401F12
.text:00404CDF mov esi, [edx+4]

And in SP1:
SP1中是这样的:

Code:

.text:004180FE add eax, 60h
.text:00418101 cmp [ebp+var_1], bl
.text:00418104 lea edx, KiDispatcherReadyListHead[ecx*8]
.text:0041810B jz loc_418760
.text:00418111 mov esi, [edx]

仅仅查找一个“lea”指令是不可靠的,因此我们也应该检查“lea”后面的指令(LDasm中的IsRelativeCmd函数)。搜索

KiDispatcherReadyListHead的全部代码如下:

Code:

void XPGetKiDispatcherReadyListHead()
{
PUCHAR cPtr, pOpcode;
PUCHAR CallAddr = NULL;
ULONG Length;

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

if (!Length) return;

if (*pOpcode == 0xE8 && *(PUSHORT)(pOpcode + 5) == 0x01B1)
{
CallAddr = (PUCHAR)(*(PULONG)(pOpcode + 1) + (ULONG)cPtr + Length);
break;
}
}

if (!CallAddr || !MmIsAddressValid(CallAddr)) return;

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

if (!Length) return;

if (*(PUSHORT)pOpcode == 0x148D && *(pOpcode + 2) == 0xCD && IsRelativeCmd(pOpcode + 7))
{
KiDispatcherReadyListHead = *(PLIST_ENTRY *)(pOpcode + 3);
break;
}
}

return;
}

找到线程链表地址之后我们就可以非常简单地枚举出那些进程了,代码如下:

Code:

void ProcessListHead(PLIST_ENTRY ListHead)
{
PLIST_ENTRY Item;

if (ListHead)
{
Item = ListHead->Flink;

while (Item != ListHead)
{
CollectProcess(*(PEPROCESS *)((ULONG)Item + WaitProcOffset));
Item = Item->Flink;
}
}

return;
}

CollectProcess是一个非常有用的函数,它可以增加一个进程到进程列表中去。

通过拦截系统调用得到进程列表。

任何一个进程都要通过API来和系统进行交互,而大多数交互都通过系统调用传递给了内核。当然,进程也可以不使用任何API而存在,但是这

样一来它也就不能做任何有用(或有害)的事情。一般而言,我们的思路是使用系统调用管理器拦截系统调用,然后得到管理器中当前进程的

EPROCESS指针。应该在某段时间收集指针列表,这个表不会包含信息收集时没有使用任何系统调用的进程(比如,进程的线程都处于等待状态

)。

Windows 2000平台使用2Eh中断进行系统调用,因此我们需要修改IDT中的相应的中断描述符来拦截系统调用,这就要用sidt指令得到IDT在内存

中的位置。该指令返回这样一个结构:

Code:

typedef struct _Idt
{
USHORT Size;
ULONG Base;
} TIdt;

修改2Eh中断向量的代码如下:

Code:

void Set2kSyscallHook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, NewSyscall
mov ebx, Idt.Base
xchg [ebx + 0x170], si
rol esi, 0x10
xchg [ebx + 0x176], si
ror esi, 0x10
mov OldSyscall, esi
sti
popad
}
}

当然在卸载驱动之前还要保存原始状态的信息:

Code:

void Win2kSyscallUnhook()
{
TIdt Idt;
__asm
{
pushad
cli
sidt [Idt]
mov esi, OldSyscall
mov ebx, Idt.Base
mov [ebx + 0x170], si
rol esi, 0x10
mov [ebx + 0x176], si
sti
xor eax, eax
mov OldSyscall, eax
popad
}
}

Windows XP使用sysenter/sysexit指令(出现在Pentium 2处理器中)实现系统调用。这些指令的功能由model-specific registers(MSR)控制

。系统调用管理器的地址保存在MSR寄存器,SYSENTER_EIP_MSR(0x176)中。用rdmsr指令读取MSR寄存器,同时设置ECX = 要读取的寄存器的号

码,结果保存在两个积存器EDX:EAX中。在我们这里,SYSENTER_EIP_MSR积存器是32位积存器,所以EDX为0,EAX内是系统调用管理器的地址。

同样地,我们也可以用wrmsr指令写MSR积存器。有一个地方需要注意:当写32位MSR积存器的时候,EDX应该被清空,否则将引起异常并且导致

系统立即崩溃。

考虑到所有的事情之后,替代系统调用管理器的代码如下:

Code:

void SetXpSyscallHook()
顶(0)
踩(0)

您可能还会对下面的文章感兴趣:

最新评论