| |
许多用户都有过用Windows自带的任务管理器查看所有进程的经验,并且很多人都认为在任务管理器中隐藏进程是不可能的。而实际上,进程隐藏是再简单不过的事情了。有许多可用的方法和参考源码可以达到进程隐藏的目的。令我惊奇的是只有很少一部分的木马使用了这种技术。估计1000个木马中仅有1个是进程隐藏的。我认为木马的作者太懒了,因为隐藏进程需要进行的额外工作仅仅是对源代码的拷贝-粘贴。所以我们应该期待即将到来的会隐藏进程的木马。
自然地,也就有必要研究进程隐藏的对抗技术。杀毒软件和防火墙制造商就像他们的产品不能发现隐藏进程一样落后了。在少之又少的免费工具中,能够胜任的也只有Klister(仅运行于Windows 2000平台)了。所有其他公司关注的只有金钱(俄文译者kao注:不完全正确,FSecure的BlackLight Beta也是免费的)。除此之外,所有的这些工具可以很容易的anti掉。
用程序实现隐藏进程探测技术,我们有两种选择: * 基于某种探测原理找到一种隐藏的方法; * 基于某个程序找到一种隐藏的方法,这个要简单一些。
购买商业软件产品的用户不能修改程序,这样可以保证其中绑定的程序的安全运行。因此第2种方法提到的程序就是商业程序的后门(rootkits)(例如hxdef Golden edition)。唯一的解决方案是创建一个免费的隐藏进程检测的开源项目,这个程序使用几种不同的检测方法,这样可以发现使用某一种方法进行隐藏的进程。任何一个用户都可以抵挡某程序的捆绑程序,当然那要得到程序的源代码并且按照自己的意愿进行修改。
在这篇文章中我将讨论探测隐藏进程的基本方法,列出该方法的示例代码,并创建一个能够检测上面我们提到的隐藏进程的程序。
在用户态(ring 3)检测 我们从简单的用户态(ring 3)检测开始,不使用驱动。事实上,每一个进程都会留下某种活动的痕迹,根据这些痕迹,我们就可以检测到隐藏的进程。这些痕迹包括进程打开的句柄、窗口和创建的系统对象。要避开这种检测技术是非常简单的,但是这样做需要留意进程留下所有痕迹,这种模式没有被用在任何一个公开发行的后门(rootkits)上。(不幸的是内部版本没有对我开放)。用户态方法容易实现,使用安全,并且能够得到很好的效果,因此这种方法不应该被忽略。
首先我们定义一下用到的数据,如下:
以下是引用片段: type PProcList = ^TProcList; TProcList = packed record NextItem: pointer; ProcName: array [0..MAX_PATH] of Char; ProcId: dword; ParrentId: dword; end; | 使用ToolHelp API获得所有进程列表 定义一下获得进程列表的函数。我们要比较这个结果和通过其他途径得到的结果:
Code:
以下是引用片段: { Acquiring list of processes by using ToolHelp API. } procedure GetToolHelpProcessList(var List: PListStruct); var Snap: dword; Process: TPROCESSENTRY32; NewItem: PProcessRecord; begin Snap := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if Snap <> INVALID_HANDLE_VALUE then begin Process.dwSize := SizeOf(TPROCESSENTRY32); if Process32First(Snap, Process) then repeat GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := Process.th32ProcessID; NewItem^.ParrentPID := Process.th32ParentProcessID; lstrcpy(@NewItem^.ProcessName, Process.szExeFile); AddItem(List, NewItem); until not Process32Next(Snap, Process); CloseHandle(Snap); end; end; |
很明显,这不会发现任何隐藏进程,所以这个函数只可以用来做探测隐藏进程的参考。
通过使用Native API获得进程列表 再深一个层次的扫描我们要通过Native API ZwQuerySystemInformation获得进程列表。虽然在这个级别(ring 0)什么也发现不了,但是我们
仍然应该检查一下。(prince注:有点令人费解,原文如下:The next scanning level will be acquisition a list of processes through ZwQuerySystemInformation (Native API). It is improbable that something will be found out at this level but we should check it anyway.)
Code:
以下是引用片段: { Acquiring list of processes by using ZwQuerySystemInformation. } procedure GetNativeProcessList(var List: PListStruct); var Inf PSYSTEM_PROCESSES; NewItem: PProcessRecord; Mem: pointer; begin Info := GetInfoTable(SystemProcessesAndThreadsInformation); Mem := Info; if Info = nil then Exit; repeat GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); lstrcpy(@NewItem^.ProcessName, PChar(WideCharToString(Info^.ProcessName.Buffer))); NewItem^.ProcessId := Info^.ProcessId; NewItem^.ParrentPID := Info^.InheritedFromProcessId; AddItem(List, NewItem); Info := pointer(dword(info) + info^.NextEntryDelta); until Info^.NextEntryDelta = 0; VirtualFree(Mem, 0, MEM_RELEASE); end; |
通过进程打开的句柄获得进程列表。 许多隐藏进程无法隐藏他们打开的句柄,因此我们可以通过使用ZwQuerySystemInformation函数枚举打开的句柄来构建进程列表。
以下是引用片段: { Acquiring the list of processes by using list of opened handles. Returns only ProcessId. } procedure GetHandlesProcessList(var List: PListStruct); var Inf PSYSTEM_HANDLE_INFORMATION_EX; NewItem: PProcessRecord; r: dword; OldPid: dword; begin OldPid := 0; Info := GetInfoTable(SystemHandleInformation); if Info = nil then Exit; for r := 0 to Info^.NumberOfHandles do if Info^.Information[r].ProcessId <> OldPid then begin OldPid := Info^.Information[r].ProcessId; GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := OldPid; AddItem(List, NewItem); end; VirtualFree(Info, 0, MEM_RELEASE); end; |
到现在我们已经可能发现一些东西了,但是我们不应该依赖于像隐藏进程一样简单的隐藏句柄的检查结果,尽管有些人甚至忘记隐藏他们。
通过列举创建的窗口来得到进程列表。 可以将那在系统中注册窗口的进程用GetWindowThreadProcessId构建进程列表。
以下是引用片段: { Acquiring the list of processes by using list of windows. Returns only ProcessId. } procedure GetWindowsProcessList(var List: PListStruct); function EnumWindowsProc(hwnd: dword; PList: PPListStruct): bool; stdcall; var ProcId: dword; NewItem: PProcessRecord; begin GetWindowThreadProcessId(hwnd, ProcId); if not IsPidAdded(PList^, ProcId) then begin GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := ProcId; AddItem(PList^, NewItem); end; Result := true; end; begin EnumWindows(@EnumWindowsProc, dword(@List)); end; |
几乎没有人会隐藏窗口,因此这种检查可以检测某些进程,但是我们不应该相信这种检测。
直接通过系统调用得到进程列表。 在用户态隐藏进程,一个普遍的做法是使用代码注入(code-injection)技术和在所有进程中拦截ntdll.dll中的ZwQuerySystemInformation函数。 ntdll中的函数实际上对应着系统内核中的函数和系统调用(Windows 2000 中的2Eh中断或者Windows XP中的sysenter指令),因此大多数简单又有效的关于那些用户级的隐藏进程的检测方法就是直接使用系统调用而不是使用API函数。
Windows XP中ZwQuerySystemInformation函数的替代函数看起来是这个样子:
以下是引用片段: { ZwQuerySystemInformation for Windows XP. } Function XpZwQuerySystemInfoCall(ASystemInformationClass: dword; ASystemInformation: Pointer; ASystemInformationLength: dword; AReturnLength: pdword): dword; stdcall; asm pop ebp mov eax, $AD call @SystemCall ret $10 @SystemCall: mov edx, esp sysenter end; |
由于不同的系统调用机制,Windows 2000的这部分代码看起来有些不同。
以下是引用片段: { Системный вызов ZwQuerySystemInformation для Windows 2000. } Function Win2kZwQuerySystemInfoCall(ASystemInformationClass: dword; ASystemInformation: Pointer; ASystemInformationLength: dword; AReturnLength: pdword): dword; stdcall; asm pop ebp mov eax, $97 lea edx, [esp + $04] int $2E ret $10 end; |
现在有必要使用上面提到的函数而不是ntdll来枚举系统进程了。实现的代码如下:
Code:
以下是引用片段: { Acquiring the list of processes by use of a direct system call ZwQuerySystemInformation. } procedure GetSyscallProcessList(var List: PListStruct); var Inf PSYSTEM_PROCESSES; NewItem: PProcessRecord; mPtr: pointer; mSize: dword; St: NTStatus; begin mSize := $4000; repeat GetMem(mPtr, mSize); St := ZwQuerySystemInfoCall(SystemProcessesAndThreadsInformation, mPtr, mSize, nil); if St = STATUS_INFO_LENGTH_MISMATCH then begin FreeMem(mPtr); mSize := mSize * 2; end; until St <> STATUS_INFO_LENGTH_MISMATCH; if St = STATUS_SUCCESS then begin Info := mPtr; repeat GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); lstrcpy(@NewItem^.ProcessName, PChar(WideCharToString(Info^.ProcessName.Buffer))); NewItem^.ProcessId := Info^.ProcessId; NewItem^.ParrentPID := Info^.InheritedFromProcessId; Info := pointer(dword(info) + info^.NextEntryDelta); AddItem(List, NewItem); until Info^.NextEntryDelta = 0; end; FreeMem(mPtr); end; |
这种方法能检测几乎100%的用户态的后门(rootkits),例如hxdef的所有版本(包括黄金版)。
通过分析相关的句柄得到进程列表。 基于枚举句柄的方法。这个方法的实质并不是查找进程打开的句柄,而是查找同该进程相关的其他进程的句柄。这些句柄可以是进程句柄也可以是线程句柄。当找到进程句柄,我们就可以用ZwQueryInformationProcess函数得到进程的PID。对于线程句柄,我们可以通过ZwQueryInformationThread得到进程ID。存在于系统中的所有进程都是由某些进程产生的,因此父进程拥有他们的句柄(除了那些已经被关闭的句柄),对于Win32子系统服务器(csrss.exe)来说所有存在的进程的句柄都是可以访问的。另外,Windows NT大量使用Job objects(prince: 任务对象?姑且这么翻译吧,有不妥的地方请指教),任务对象可以关联进程(比如属于某用户或服务的所有进程),因此当找到任务对象的句柄,我们就可以利用它得到与之关联的所有进程的ID。使用QueryInformationJobObject和信息类的函数JobObjectBasicProcessIdList就可以实现上述功能。利用分析进程相关的句柄得到进程列表的实现代码如下:
Code:
以下是引用片段: { Acquiring the list of processes by analyzing handles in other processes. } procedure GetProcessesFromHandles(var List: PListStruct; Processes, Jobs, Threads: boolean); var HandlesInf PSYSTEM_HANDLE_INFORMATION_EX; ProcessInf PROCESS_BASIC_INFORMATION; hProcess : dword; tHandle: dword; r, l : integer; NewItem: PProcessRecord; Inf PJOBOBJECT_BASIC_PROCESS_ID_LIST; Size: dword; THRInf THREAD_BASIC_INFORMATION; begin HandlesInfo := GetInfoTable(SystemHandleInformation); if HandlesInfo <> nil then for r := 0 to HandlesInfo^.NumberOfHandles do if HandlesInfo^.Information[r].ObjectTypeNumber in [OB_TYPE_PROCESS, OB_TYPE_JOB, OB_TYPE_THREAD] then begin hProcess := OpenProcess(PROCESS_DUP_HANDLE, false, HandlesInfo^.Information[r].ProcessId); if DuplicateHandle(hProcess, HandlesInfo^.Information[r].Handle, INVALID_HANDLE_VALUE, @tHandle, 0, false, DUPLICATE_SAME_ACCESS) then begin case HandlesInfo^.Information[r].ObjectTypeNumber of OB_TYPE_PROCESS : begin if Processes and (HandlesInfo^.Information[r].ProcessId = CsrPid) then if ZwQueryInformationProcess(tHandle, ProcessBasicInformation, @ProcessInfo, SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then if not IsPidAdded(List, ProcessInfo.UniqueProcessId) then begin GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := ProcessInfo.UniqueProcessId; NewItem^.ParrentPID := ProcessInfo.InheritedFromUniqueProcessId; AddItem(List, NewItem); end; end; OB_TYPE_JOB : begin if Jobs then begin Size := SizeOf(JOBOBJECT_BASIC_PROCESS_ID_LIST) + 4 * 1000; GetMem(Info, Size); Info^.NumberOfAssignedProcesses := 1000; if QueryInformationJobObject(tHandle, JobObjectBasicProcessIdList, Info, Size, nil) then for l := 0 to Info^.NumberOfProcessIdsInList - 1 do if not IsPidAdded(List, Info^.ProcessIdList[l]) then begin GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := Info^.ProcessIdList[l]; AddItem(List, NewItem); end; FreeMem(Info); end; end; OB_TYPE_THREAD : begin if Threads then if ZwQueryInformationThread(tHandle, THREAD_BASIC_INFO, @THRInfo, SizeOf(THREAD_BASIC_INFORMATION), nil) = STATUS_SUCCESS then if not IsPidAdded(List, THRInfo.ClientId.UniqueProcess) then begin GetMem(NewItem, SizeOf(TProcessRecord)); ZeroMemory(NewItem, SizeOf(TProcessRecord)); NewItem^.ProcessId := THRInfo.ClientId.UniqueProcess; AddItem(List, NewItem); end; end; end; CloseHandle(tHandle); end; CloseHandle(hProcess); end; VirtualFree(HandlesInfo, 0, MEM_RELEASE); end; |
不幸的是,上面提到的这些方法有些只能得到进程ID,而不能得到进程名字。因此,我们还需要通过进程ID得到进程的名称。当然,当这些进 程是隐藏进程的时候我们就不能使用ToolHelp API来实现。所以我们应该访问进程内存通过读取该进程的PEB得到进程名称。PEB地址可以用ZwQueryInformationProcess函数获得。以上所说的功能实现代码如下:
以下是引用片段: function GetNameByPid(Pid: dword): string; var hProcess, Bytes: dword; Inf PROCESS_BASIC_INFORMATION; ProcessParametres: pointer; ImagePath: TUnicodeString; ImgPath: array[0..MAX_PATH] of WideChar; begin Result := ’’; ZeroMemory(@ImgPath, MAX_PATH * SizeOf(WideChar)); hProcess := OpenProcess(PROCESS_QUERY_INFORMATION or PROCESS_VM_READ, false, Pid); if ZwQueryInformationProcess(hProcess, ProcessBasicInformation, @Info, SizeOf(PROCESS_BASIC_INFORMATION), nil) = STATUS_SUCCESS then begin if ReadProcessMemory(hProcess, pointer(dword(Info.PebBaseAddress) + $10), @ProcessParametres, SizeOf(pointer), Bytes) and ReadProcessMemory(hProcess, pointer(dword(ProcessParametres) + $38), @ImagePath, SizeOf(TUnicodeString), Bytes) and ReadProcessMemory(hProcess, ImagePath.Buffer, @ImgPath, ImagePath.Length, Bytes) then begin Result := ExtractFileName(WideCharToString(ImgPath)); end; end; CloseHandle(hProcess); end; | 当然,用户态隐藏进程的检测方法不止这些,还可以想一些稍微复杂一点的新方法(比如,用SetWindowsHookEx函数对可访问进程的注入和当我们的DLL并成功加载后对进程列表的分析),但是现在我们将用上面提到的方法来解决问题。这些方法的优点是他们可以简单地编程实现,并且除了可以检测到用户态的隐藏进程,还可以检测到少数的在内核态实现的隐藏进程... 要实现真正可靠的进程隐藏工具我们应该使用Windows未公开的内核数据结构编写内核驱动程序。
内核态(Ring 0)的检测 恭喜你,我们终于开始进行内核态隐藏进程的分析。内核态的检测方法同用户态的检测方法的主要区别是所有的进程列表都没有使用API调用而是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多,因为它们都是基于同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函数得到进程列表:
以下是引用片段:
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。像这样:
以下是引用片段: PsActiveProcessHead = *(PVOID *)((PUCHAR)PsGetCurrentProcess + ActiveProcessLinksOffset + 4); | 现在就可以遍历这个双向链表来创建进程列表了:
以下是引用片段: PVOID GetEprocessProcessList(ULONG *Mem, Size) { 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; }
|
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; } |
|
|
|
|