检测隐藏进程
现在有必要使用上面提到的函数而不是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函数获得。以上所说的功能实现代码如下:
Code:
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标志。
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函数获得。以上所说的功能实现代码如下:
Code:
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标志。
0
相关文章