检测隐藏进程(二)
来源:互联网 发布:无聊了怎么办 知乎 编辑:程序博客网 时间:2024/04/28 04:25
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函数获得。以上所说的功能实现代码如下:
Code:
function GetNameByPid(Pid: dword): string;
var
hProcess, Bytes: dword;
Info: 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;
当然,用户态隐藏进程的检测方法不止这些,还能想一些稍微复杂一点的新方法(比如,用Set视窗系统HookEx函数对可访问进程的注入和当
我们的DLL并成功加载后对进程列表的分析),不过目前我们将用上面提到的方法来解决问题。这些方法的好处是他们能简单地编程实现,并
且除了能检测到用户态的隐藏进程,还能检测到少数的在内核态实现的隐藏进程... 要实现真正可靠的进程隐藏工具我们应该使用视窗系统
未公开的内核数据结构编写内核驱动程式。
内核态(Ring 0)的检测
恭喜你,我们终于开始进行内核态隐藏进程的分析。内核态的检测方法同用户态的检测方法的主要差别是所有的进程列表都没有使用API调用而
是直接来自系统内部数据结构。在这些检测方法下隐藏进程要困难得多,因为他们都是基于同视窗系统内核相同的原理实现的,并且从这些内核
数据结构中删除进程将导致该进程完全失效。
内核中的进程是什么?每一个进程都有自己的地址空间,描述符,线程等,内核的数据结构就涉及这些东西。每一个进程都是由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)。这些
偏移随着视窗系统系统版本的不同而不同,因此我们要在进程检测程式的代码中进行区分后得到他们正确的值(附录)。
所有一个通过API截取方式隐藏的进程都将被上面这个方法检测出来。不过如果进程是通过DKOM(直接处理内核对象 - Direct Kernel Object
Manipulation)方式隐藏,那这个方法就失效了,因为这种进程都被从进程链表中删掉了。
通过列举调度程式(scheduler)中的线程得到进程列表。
对付这种隐藏进程(俄文翻译kao注:这个地方原文写的比较模糊,作者大概的意思应该是“使用DKOM的方式检测隐藏进程”)的其中一种检测
方式是通过调度程式(scheduler)中的线程列表来得到进程列表。视窗系统 2000有三个维护线程的双向链表(KiWaitInListHead,
KiWaitOutListHead, KiDispatcherReadyListHead)。前面两个链表包含等待某种事件的线程,最后面的链表包含的是等待执行的线程。我们
处理这些链表,根据线程链表结构ETHREAD中的偏移就能得到一个线程的ETHREAD指针(俄文翻译kao注:原文中这句话实在是太难懂了,希望
我翻译的正确)。这个结构包括了非常多进程相关指针,也就是结构_KPROCESS *Process(0x44, 0x150)和结构_EPROCESS *ThreadsProcess
(0x22C, 这仅是视窗系统 2000中的偏移量)。前面两个指针对于一个线程的功能性没有所有影响,因此能非常容易修改他们来隐藏进程。相反,
第三个指针是当转换地址空间时调度程式(schedler)使用的指针,所以这个指针是不能修改的。我们就用他来找到拥有某个线程的进程。
Klister就是使用了这种检测方法,他的最大的缺点就是只能在视窗系统 2000平台上工作(不过在这个平台上某个补丁包也会让他失效)。导致
这个情况发生的原因就是这种程式使用了硬编码的线程链表地址,而在每个补丁包中这些地址可能都是不同的。
在程式中使用硬编码地址是非常糟糕的解决方案,操作系统的升级就会使你的程式无法正常工作,要尽量避免使用这种检测方法。所以应该通过
分析那些使用了这些链表的内核函数来动态地得到他们的地址。
首先我们试试看在视窗系统 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;
}
在视窗系统 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;
}
不幸的是,视窗系统 XP内核完全不同于视窗系统 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;
}
- 检测隐藏进程(二)
- 检测隐藏进程
- ring0检测隐藏进程
- ring0检测隐藏进程
- ring0检测隐藏进程
- 检测隐藏进程
- 隐藏进程检测
- 检测隐藏进程(一)
- 检测隐藏进程
- 使用PsSetCreateProcessNotifyRoutine检测隐藏进程
- vista下扫描内存检测隐藏进程
- PspCidTable句柄表辅助检测隐藏进程
- C++进程检测,发现进程路径,关闭进程(二)
- NT系统下木马进程的隐藏与检测
- NT系统下木马进程的隐藏与检测
- NT系统下木马进程的隐藏与检测(转)
- NT系统下木马进程的隐藏与检测
- 隐藏进程
- 产品经理的第一堂课:如何管理下属
- IE下js读取href值的相对路径和绝对路径的问题
- Internet Information Services 诊断工具
- 【策划方案手段】为本单位精心打造一场专业电视台效果的知识竞赛,如何搞?如何花钱最少?
- 如何恢复Powerpoint2010的侧栏
- 检测隐藏进程(二)
- 第1部分 接口型模式 第2章 接口型模式简介
- vs2005 快捷键
- 关于opencv中图像的通道数问题
- (转)无法将自定义控件安装到工具箱的解决办法
- 《UCD火花集》书评:从卖产品到卖用户体验
- asp.net 验证码
- postfix+devecot+openwebmail+apache
- "控件必须放在具有 runat=server 的窗体标记内"错误的解决方法