许多用户都有过用视窗系统自带的任务管理器查看所有进程的经验,并且非常多人都认为在任务管理器中隐藏进程是不可能的。而实际上,进程隐 藏是再简单不过的事情了。有许多可用的方法和参考源码能达到进程隐藏的目的。令我惊奇的是只有非常少一部分的木马 使用了这种技术 。估 计1000个木马 中仅有1个是进程隐藏的。我认为木马 的作者太懒了,因为隐藏进程需要进行的额外工作仅仅是对原始码的拷贝-粘贴。所以我们 应该期待即将到来的会隐藏进程的木马 。
自然地,也就有必要研究进程隐藏的对抗技术 。杀毒软件和防火墙制造商就像他们的产品不能发现隐藏进程相同落后了。在少之又少的免费工 具中,能够胜任的也只有Klister(仅运行于视窗系统 2000平台)了。所有其他公司关注的只有金钱(俄文译者kao注:不完全正确,FSecure的 BlackLight Beta也是免费的)。除此之外,所有的这些工具都能非常容易的anti掉。
用程式 实现隐藏进程探测技术 ,我们有两种选择:
* 基于某种探测原理找到一种隐藏的方法;
* 基于某个程式 找到一种隐藏的方法,这个要简单一些。
购买商业软件产品的用户不能修改程式 ,这样能确保其中绑定的程式 的安全 运行。因此第2种方法提到的程式 就是商业程式 的后门(rootkits )(例如hxdef Golden edition)。唯一的解决方案是创建一个免费的隐藏进程检测的开源项目,这个程式 使用几种不同的检测方法,这样可 以发现使用某一种方法进行隐藏的进程。所有一个用户都能抵挡某程式 的捆绑程式 ,当然那要得到程式 的原始码并且按照自己的意愿进行修 改。
在这篇文章中我将讨论探测隐藏进程的基本方法,列出该方法的示例代码,并创建一个能够检测上面我们提到的隐藏进程的程式 。
在用户态(ring 3)检测
我们从简单的用户态(ring 3)检测开始,不使用驱动。事实上,每一个进程都会留下某种活动的痕迹,根据这些痕迹,我们就能检测到隐 藏的进程。这些痕迹包括进程打开的句柄、窗口和创建的系统 对象。要避开这种检测技术 是非常简单的,不过这样做需要留意进程留下所有痕 迹,这种模式没有被用在所有一个公研发行的后门(rootkits)上。(不幸的是内部版本没有对我开放)。用户态方法容易实现,使用安全 , 并且能够得到非常好的效果,因此这种方法不应该被忽略。
首先我们定义一下用到的数据,如下:
Code:
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
Info: 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函数枚举打开的句柄来构建进程列表。
Code:
{
Acquiring the list of processes by using list of opened handles.
Returns only ProcessId.
}
procedure GetHandlesProcessList(var List: PListStruct);
var
Info: 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构建进程列表。
Code:
{
Acquiring the list of processes by using list of windows.
Returns only ProcessId.
}
procedure Get视窗系统ProcessList(var List: PListStruct);
function Enum视窗系统Proc(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
Enum视窗系统(@Enum视窗系统Proc, dword(@List));
end;
几乎没有人会隐藏窗口,因此这种检查能检测某些进程,不过我们不应该相信这种检测。
直接通过系统 调用得到进程列表。
在用户态隐藏进程,一个普遍的做法是使用代码注入(code-injection)技术 和在所有进程中拦截ntdll.dll中的ZwQuerySystemInformation函数
。
ntdll中的函数实际上对应着系统 内核中的函数和系统 调用(视窗系统 2000 中的2Eh中断或视窗系统 XP中的sysenter指令),因此大多数简单
又有效的关于那些用户级的隐藏进程的检测方法就是直接使用系统 调用而不是使用API函数。
视窗系统 XP中ZwQuerySystemInformation函数的替代函数看起来是这个样子:
Code:
{
ZwQuerySystemInformation for 视窗系统 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;
由于不同的系统 调用机制,视窗系统 2000的这部分代码看起来有些不同。
Code:
{
Системный вызов ZwQuerySystemInformation для 视窗系统 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
Info: 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)来说所有存在的进程的句柄都是能访问的。另外,视窗系统 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
HandlesInfo: PSYSTEM_HANDLE_INFORMATION_EX;
ProcessInfo: PROCESS_BASIC_INFORMATION;
hProcess : dword;
tHandle: dword;
r, l : integer;
NewItem: PProcessRecord;
Info: PJOBOBJECT_BASIC_PROCESS_ID_LIST;
Size: dword;
THRInfo: 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;