歡迎來到 黑吧安全網 聚焦網絡安全前沿資訊,精華內容,交流技術心得!

檢測隱藏進程

來源:www.hack58.com 作者:佚名 時間:2006-04-23 TAG: 我要投稿
Detection of the hidden processes

俄語原文:http://wasm.ru/article.php?article=hiddndt

俄文翻譯:kao

http://community.reverse-engineering.net/viewtopic.php?t=4685

中文翻譯: prince

后期校驗:firstrose

=============================

偵測隱藏進程
[C] Ms-Rem
2002-2005 wasm.ru - all rights reserved and reversed

許多用戶都有過用Windows自帶的任務管理器查看所有進程的經驗,并且很多人都認為在任務管理器中隱藏進程是不可能的。而實際上,進程隱藏是再簡單不過的事情了。有許多可用的方法和參考源碼可以達到進程隱藏的目的。令我驚奇的是只有很少一部分的木馬使用了這種技術。估計1000個木馬中僅有1個是進程隱藏的。我認為木馬的作者太懶了,因為隱藏進程需要進行的額外工作僅僅是對源代碼的拷貝-粘貼。所以我們應該期待即將到來的會隱藏進程的木馬。

自然地,也就有必要研究進程隱藏的對抗技術。殺毒軟件和防火墻制造商就像他們的產品不能發現隱藏進程一樣落后了。在少之又少的免費工具中,能夠勝任的也只有Klister(僅運行于Windows 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
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函數枚舉打開的句柄來構建進程列表。

Code:

{
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構建進程列表。

Code:

{
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函數的替代函數看起來是這個樣子:

Code:

{
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的這部分代碼看起來有些不同。

Code:

{
Системный вызов 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函數獲得。以上所說的功能實現代碼如下:

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標志。

在內核中使用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()
{
__asm
{
pushad
mov ecx, 0x176
rdmsr
mov OldSyscall, eax
mov eax, NewSyscall
xor edx, edx
wrmsr
popad
}
}

恢復原始的系統調用管理器代碼:

Code:

void XpSyscallUnhook()
{
__asm
{
pushad
mov ecx, 0x176
mov eax, OldSyscall
xor edx, edx
wrmsr
xor eax, eax
mov OldSyscall, eax
popad
}
}

Windows XP的另外一個特性是它既可以使用sysenter也可以使用int 2Eh來進行系統調用,所以我們要替換這兩種情況下的系統調用管理器。

我們的新的系統調用管理器應該得到當前進程的EPROCESS指針,并且如果是一個新的進程,我們要把這個新的進程加到我們的進程列表中。
新的系統調用管理器代碼如下:

Code:

void __declspec(naked) NewSyscall()
{
__asm
{
pushad
pushfd
push fs
mov di, 0x30
mov fs, di
mov eax, fs:[0x124]
mov eax, [eax + 0x44]
push eax
call CollectProcess
pop fs
popfd
popad
jmp OldSyscall
}
}

得到進程列表的這段代碼應該在某個時間段內工作,所以我們有這樣的問題:如果在列表中的進程被刪除掉,在隨后的時間內我們將保留一些

無效指針,結果就是檢測隱藏進程失敗或者導致系統BSOD。解決這個問題的辦法是,用PsSetCreateProcessNotifyRoutine函數注冊我們的回調

函數,這個回調函數將會在系統創建或者銷毀一個進程的時候被調用。當進程被銷毀時,我們也應該把它從我們的表中刪除掉。
回調函數的原型如下:

Code:

VOID
(*PCREATE_PROCESS_NOTIFY_ROUTINE) (
IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create
);

安裝回調函數的代碼如下:

Code:

PsSetCreateProcessNotifyRoutine (NotifyRoutine, FALSE);

取消回調函數的代碼:

Code:

PsSetCreateProcessNotifyRoutine (NotifyRoutine, TRUE);

這里有一個問題,回調函數總是在系統被銷毀的時候創建,因此我們不可能直接在這個回調函數中刪除進程列表中的相應進程。這樣我們就要

用系統的work items,首先調用IoAllocateWorkItem函數為work item分配內存,然后調用IoQueueWorkItem函數(俄文翻譯者kao注:這一句我

不太確定...)將任務放置到工作線程隊列中。在處理過程中我們不僅僅從進程列表中刪除掉已經終止的進程,而且還要加入新創建的線程。處

理代碼如下:

Code:

void WorkItemProc(PDEVICE_OBJECT DeviceObject, PWorkItemStruct Data)
{
KeWaitForSingleObject(Data->pEPROCESS, Executive, KernelMode, FALSE, NULL);

DelItem(&wLastItem, Data->pEPROCESS);

ObDereferenceObject(Data->pEPROCESS);

IoFreeWorkItem(Data->IoWorkItem);

ExFreePool(Data);

return;
}

void NotifyRoutine(IN HANDLE ParentId,
IN HANDLE ProcessId,
IN BOOLEAN Create)
{
PEPROCESS process;
PWorkItemStruct Data;

if (Create)
{
PsLookupProcessByProcessId(ProcessId, &process);

if (!IsAdded(wLastItem, process)) AddItem(&wLastItem, process);

ObDereferenceObject(process);

} else
{
process = PsGetCurrentProcess();

ObReferenceObject(process);

Data = ExAllocatePool(NonPagedPool, sizeof(TWorkItemStruct));

Data->IoWorkItem = IoAllocateWorkItem(deviceObject);

Data->pEPROCESS = process;

IoQueueWorkItem(Data->IoWorkItem, WorkItemProc, DelayedWorkQueue, Data);
}

return;
}

這是一個相對可靠的隱藏進程的檢測方式,然而雖然沒有進程能夠不倚賴系統調用,但還是有一些進程可以在很長一段時間處于等待狀態不進

行系統調用,我們無法檢測出這樣的進程。

只要想做,躲避開這種檢測方式還是很容易的。想要做到這一點,那就需要改變隱藏進程的系統調用方式(重定向到另外一個中斷或者GDT中的

調用門)。在Windows XP下做這個工作是相當簡單的,因為可以給ntdll.dll中的KiFastSystemCall函數打補丁和創建一個相應的系統調用門。

在Windows 2000平臺下就稍微有點難度了,因為int 2Eh調用分散遍及整個ntdll,但是找到并patch所有的地方也并不是很復雜。綜上所述,依

賴于這種檢測方式可不是聰明之舉。

通過遍歷句柄表得到進程列表。
如果你曾經嘗試過利用刪除PsActiveProcesses鏈表中的進程節點來隱藏進程,可能你會注意到當你調用ZwQuerySystemInformation函數枚舉句

柄的時候,隱藏進程的句柄也會被枚舉出來,并且還能被檢測出它的ProcessId。這是因為為了方便枚舉句柄,所有的句柄表都是由一個雙向鏈

表HandleTableList維護的。Windows 2000下HANDLE_TABLE結構在鏈表中的偏移等于0x054,Windows XP下為0x01C,鏈表由

HandleTableListHead開始。HANDLE_TABLE結構包括它的宿主進程的指針(QuotaProcess),Windows 2000下這個偏移等于0x00C,Windows XP

下這個偏移為0x004。通過遍歷這個句柄鏈表我們就可以構建進程列表了。

首先我們得找到HandleTableListHead。反匯編內核顯示它的引用定位在函數的深處,所以前面我們用過的反匯編代碼的方法已經不能在這里使

用了。要找到HeadleTableListHead,我們要注意到HandleTableListHead是一個全局的內核變量,因此它一定是在內核文件的某一個段

(Section)里面,并且HandleTableList的其他成員是在動態分配的內存中,所以總是受到內核地址空間的限制。根據這些,我們需要得到任

何一個進程的HandleTable的指針,然后遍歷鏈表直到找到定位在這個內核地址空間的成員,那么這個成員就是HandleTableListHead了。

我們使用ZwQuerySystemInformation和SystemModuleInformation類計算系統內核的基址和大小。它將返回一個所有已經加載了的模塊的描述符

表,并且這個表的第一個成員始終是"system"。綜上所述,查找HandleTableListHead的代碼如下:

Code:

void GetHandleTableListHead()
{
PSYSTEM_MODULE_INFORMATION_EX Info = GetInfoTable(SystemModuleInformation);
ULONG NtoskrnlBase = (ULONG)Info->Modules[0].Base;
ULONG NtoskrnlSize = Info->Modules[0].Size;
PHANDLE_TABLE HandleTable = *(PHANDLE_TABLE *)((ULONG)PsGetCurrentProcess() + HandleTableOffset);
PLIST_ENTRY HandleTableList = (PLIST_ENTRY)((ULONG)HandleTable + HandleTableListOffset);
PLIST_ENTRY CurrTable;

ExFreePool(Info);

for (CurrTable = HandleTableList->Flink;
CurrTable != HandleTableList;
CurrTable = CurrTable->Flink)
{
if ((ULONG)CurrTable > NtoskrnlBase && (ULONG)CurrTable < NtoskrnlBase + NtoskrnlSize)
{
HandleTableListHead = CurrTable;
break;
}
}
}

這段代碼是非常通用的,它可以運行于任何Windows NT版本的系統上,并且不僅可以用來查找HandleTableListHead,也可以用于其他類似的結

構。

得到HandleTableListHead地址后我們就可以遍歷句柄表并基于這些信息來構建進程列表了。

Code:

void ScanHandleTablesList()
{
PLIST_ENTRY CurrTable;
PEPROCESS QuotaProcess;

for (CurrTable = HandleTableListHead->Flink;
CurrTable != HandleTableListHead;
CurrTable = CurrTable->Flink)
{
QuotaProcess = *(PEPROCESS *)((PUCHAR)CurrTable - HandleTableListOffset + QuotaProcessOffset);
if (QuotaProcess) CollectProcess(QuotaProcess);
}
}

F-Secure Black Light和KProcCheck的最后一個版本用的就是這種檢測方法。我想你將會很輕松地找到對付這種檢測的方法。

通過掃描PspCidTable得到進程列表。

有一件有趣的事情需要注意:如果僅僅把進程節點從PsActiveProcesses鏈表中刪除,它不能夠防止使用API函數OpenProcess打開進程。這樣就

有一種檢測進程的方法就是嘗試窮舉Pid然后調用OpenProcess。我不推薦這個方法,因為它沒有任何優點,我甚至想說這是一種“狗屁”方案

。不過它的存在意味著在系統中除了通過PsActiveProcesses得到進程列表之外還可以通過調用OpenProcess。當窮舉ProcessId的時候我們會注

意到一個進程可以被幾個不同的Pid打開,這暗示可能存在著有點像HANDLE_TABLE的另一個進程列表。為了證明這個的猜想,我們來看看

ZwOpenProcess函數:

Code:

PAGE:0049D59E ; NTSTATUS __stdcall NtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID ClientId)
PAGE:0049D59E public NtOpenProcess
PAGE:0049D59E NtOpenProcess proc near
PAGE:0049D59E
PAGE:0049D59E ProcessHandle = dword ptr 4
PAGE:0049D59E DesiredAccess = dword ptr 8
PAGE:0049D59E ObjectAttributes= dword ptr 0Ch
PAGE:0049D59E ClientId = dword ptr 10h
PAGE:0049D59E
PAGE:0049D59E push 0C4h
PAGE:0049D5A3 push offset dword_413560 ; int
PAGE:0049D5A8 call sub_40BA92
PAGE:0049D5AD xor esi, esi
PAGE:0049D5AF mov [ebp-2Ch], esi
PAGE:0049D5B2 xor eax, eax
PAGE:0049D5B4 lea edi, [ebp-28h]
PAGE:0049D5B7 stosd
PAGE:0049D5B8 mov eax, large fs:124h
PAGE:0049D5BE mov al, [eax+140h]
PAGE:0049D5C4 mov [ebp-34h], al
PAGE:0049D5C7 test al, al
PAGE:0049D5C9 jz loc_4BE034
PAGE:0049D5CF mov [ebp-4], esi
PAGE:0049D5D2 mov eax, MmUserProbeAddress
PAGE:0049D5D7 mov ecx, [ebp+8]
PAGE:0049D5DA cmp ecx, eax
PAGE:0049D5DC jnb loc_520CDE
PAGE:0049D5E2 loc_49D5E2:
PAGE:0049D5E2 mov eax, [ecx]
PAGE:0049D5E4 mov [ecx], eax
PAGE:0049D5E6 mov ebx, [ebp+10h]
PAGE:0049D5E9 test bl, 3
PAGE:0049D5EC jnz loc_520CE5
PAGE:0049D5F2 loc_49D5F2:
PAGE:0049D5F2 mov eax, MmUserProbeAddress
PAGE:0049D5F7 cmp ebx, eax
PAGE:0049D5F9 jnb loc_520CEF
PAGE:0049D5FF loc_49D5FF:
PAGE:0049D5FF cmp [ebx+8], esi
PAGE:0049D602 setnz byte ptr [ebp-1Ah]
PAGE:0049D606 mov ecx, [ebx+0Ch]
PAGE:0049D609 mov [ebp-38h], ecx
PAGE:0049D60C mov ecx, [ebp+14h]
PAGE:0049D60F cmp ecx, esi
PAGE:0049D611 jz loc_4CCB88
PAGE:0049D617 test cl, 3
PAGE:0049D61A jnz loc_520CFB
PAGE:0049D620 loc_49D620:
PAGE:0049D620 cmp ecx, eax
PAGE:0049D622 jnb loc_520D0D
PAGE:0049D628 loc_49D628:
PAGE:0049D628 mov eax, [ecx]
PAGE:0049D62A mov [ebp-2Ch], eax
PAGE:0049D62D mov eax, [ecx+4]
PAGE:0049D630 mov [ebp-28h], eax
PAGE:0049D633 mov byte ptr [ebp-19h], 1
PAGE:0049D637 loc_49D637:
PAGE:0049D637 or dword ptr [ebp-4], 0FFFFFFFFh
PAGE:0049D63B loc_49D63B:
PAGE:0049D63B
PAGE:0049D63B cmp byte ptr [ebp-1Ah], 0
PAGE:0049D63F jnz loc_520D34
PAGE:0049D645 loc_49D645:
PAGE:0049D645 mov eax, PsProcessType
PAGE:0049D64A add eax, 68h
PAGE:0049D64D push eax
PAGE:0049D64E push dword ptr [ebp+0Ch]
PAGE:0049D651 lea eax, [ebp-0D4h]
PAGE:0049D657 push eax
PAGE:0049D658 lea eax, [ebp-0B8h]
PAGE:0049D65E push eax
PAGE:0049D65F call SeCreateAccessState
PAGE:0049D664 cmp eax, esi
PAGE:0049D666 jl loc_49D718
PAGE:0049D66C push dword ptr [ebp-34h] ; PreviousMode
PAGE:0049D66F push ds:stru_5B6978.HighPart
PAGE:0049D675 push ds:stru_5B6978.LowPart ; PrivilegeValue
PAGE:0049D67B call SeSinglePrivilegeCheck
PAGE:0049D680 test al, al
PAGE:0049D682 jnz loc_4AA7DB
PAGE:0049D688 loc_49D688:
PAGE:0049D688 cmp byte ptr [ebp-1Ah], 0
PAGE:0049D68C jnz loc_520D52
PAGE:0049D692 cmp byte ptr [ebp-19h], 0
PAGE:0049D696 jz loc_4CCB9A
PAGE:0049D69C mov [ebp-30h], esi
PAGE:0049D69F cmp [ebp-28h], esi
PAGE:0049D6A2 jnz loc_4C1301
PAGE:0049D6A8 lea eax, [ebp-24h]
PAGE:0049D6AB push eax
PAGE:0049D6AC push dword ptr [ebp-2Ch]
PAGE:0049D6AF call PsLookupProcessByProcessId
PAGE:0049D6B4 loc_49D6B4:

正如你看到的,這段代碼拷貝給定的指針,檢查是否指向用戶地址空間,核對訪問權限和是否有“SetDebugPrivilege”的權限,然后從

CLIENT_ID結構中找到ProcessId并傳遞給PsLookupProcessByProcessId函數,PsLookupProcessByProcessId的功能是得到ProcessId的EPROCESS

。函數的其余部分對我們來說沒什么用,現在我們來看看PsLookupProcessByProcessId:

Code:

PAGE:0049D725 public PsLookupProcessByProcessId
PAGE:0049D725 PsLookupProcessByProcessId proc near
PAGE:0049D725
PAGE:0049D725
PAGE:0049D725 ProcessId = dword ptr 8
PAGE:0049D725 Process = dword ptr 0Ch
PAGE:0049D725
PAGE:0049D725 mov edi, edi
PAGE:0049D727 push ebp
PAGE:0049D728 mov ebp, esp
PAGE:0049D72A push ebx
PAGE:0049D72B push esi
PAGE:0049D72C mov eax, large fs:124h
PAGE:0049D732 push [ebp+ProcessId]
PAGE:0049D735 mov esi, eax
PAGE:0049D737 dec dword ptr [esi+0D4h]
PAGE:0049D73D push PspCidTable
PAGE:0049D743 call ExMapHandleToPointer
PAGE:0049D748 mov ebx, eax
PAGE:0049D74A test ebx, ebx
PAGE:0049D74C mov [ebp+ProcessId], STATUS_INVALID_PARAMETER
PAGE:0049D753 jz short loc_49D787
PAGE:0049D755 push edi
PAGE:0049D756 mov edi, [ebx]
PAGE:0049D758 cmp byte ptr [edi], 3
PAGE:0049D75B jnz short loc_49D77A
PAGE:0049D75D cmp dword ptr [edi+1A4h], 0
PAGE:0049D764 jz short loc_49D77A
PAGE:0049D766 mov ecx, edi
PAGE:0049D768 call sub_4134A9
PAGE:0049D76D test al, al
PAGE:0049D76F jz short loc_49D77A
PAGE:0049D771 mov eax, [ebp+Process]
PAGE:0049D774 and [ebp+ProcessId], 0
PAGE:0049D778 mov [eax], edi
PAGE:0049D77A loc_49D77A:
PAGE:0049D77A push ebx
PAGE:0049D77B push PspCidTable
PAGE:0049D781 call ExUnlockHandleTableEntry
PAGE:0049D786 pop edi
PAGE:0049D787 loc_49D787:
PAGE:0049D787 inc dword ptr [esi+0D4h]
PAGE:0049D78D jnz short loc_49D79A
PAGE:0049D78F lea eax, [esi+34h]
PAGE:0049D792 cmp [eax], eax
PAGE:0049D794 jnz loc_52388A
PAGE:0049D79A loc_49D79A:
PAGE:0049D79A mov eax, [ebp+ProcessId]
PAGE:0049D79D pop esi
PAGE:0049D79E pop ebx
PAGE:0049D79F pop ebp
PAGE:0049D7A0 retn 8

以上我們所看到的,證實了存在像HANDLE_TABLE一樣組織結構的第2個進程列表。這個表叫做PspCidTable,它包括進程和線程的列表,

PsLookupProcessThreadByCid函數和PsLookupThreadByThreadId函數都用到了這個表。我們看到,句柄和句柄表的指針被傳遞給了

ExMapHandleToPointer函數,該函數(在句柄有效的情況下)返回一個指向描述給定句柄的表的一個元素 - HANDLE_TABLE_ENTRY。當我們用

PDBdump分析完ntoskrnl.pdb并且得到分析日志后,會得到如下結果:

Code:

struct _HANDLE_TABLE_ENTRY {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ void* Object;
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long ObAttributes;
/*<thisrel this+0x0>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY_INFO* InfoTable;
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Value;
/*<thisrel this+0x4>*/ /*|0x4|*/ unsigned long GrantedAccess;
/*<thisrel this+0x4>*/ /*|0x2|*/ unsigned short GrantedAccessIndex;
/*<thisrel this+0x6>*/ /*|0x2|*/ unsigned short CreatorBackTraceIndex;
/*<thisrel this+0x4>*/ /*|0x4|*/ long NextFreeTableEntry;
};// <size 0x8>

We can recover HANDLE_TABLE_ENTRY structure from this:
我們還原一下HANDLE_TABLE_ENTRY結構的C代碼:

Code:

typedef struct _HANDLE_TABLE_ENTRY
{
union
{
PVOID Object;
ULONG ObAttributes;
PHANDLE_TABLE_ENTRY_INFO InfoTable;
ULONG Value;
};

union
{
union
{
ACCESS_MASK GrantedAccess;
struct
{
USHORT GrantedAccessIndex;
USHORT CreatorBackTraceIndex;
};
};

LONG NextFreeTableEntry;
};
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;

怎么使用它呢?首先,我們比較感興趣的是Object域的內容,它是被句柄描述的目標指針和這個表的給定元素的用法標志(我將稍后解釋這句

話)。GrantedAccess域指定了通過這個句柄對目標的訪問權限許可,這個很有趣。比如,以只讀方式打開一個文件,修改這個域之后就可以寫

這個文件了。這個方法可以用在對一些正在被讀/寫的文件的訪問上(比如,正在被其他進程鎖定的文件)。應該回到我們的問題上來了 - 通

過對PspCidTable的分析得到進程句柄列表。

要分析它我們得了解句柄表的格式,這樣才能遍歷這個列表。在這個地方Windows 2000和Windows XP有著巨大的不同。由于句柄表格式不盡相

同,所以我們應該把操作系統分類進行分析。

因為Windows 2000的句柄表相對簡單一些,所以我們先分析它。先來看看ExMapHandleToPointer函數:

Code:

PAGE:00493285 ExMapHandleToPointer proc near
PAGE:00493285
PAGE:00493285
PAGE:00493285 HandleTable = dword ptr 8
PAGE:00493285 Handle = dword ptr 0Ch
PAGE:00493285
PAGE:00493285 push esi
PAGE:00493286 push [esp+Handle]
PAGE:0049328A push [esp+4+HandleTable]
PAGE:0049328E call ExpLookupHandleTableEntry
PAGE:00493293 mov esi, eax
PAGE:00493295 test esi, esi
PAGE:00493297 jz short loc_4932A9
PAGE:00493299 push esi
PAGE:0049329A push [esp+4+HandleTable]
PAGE:0049329E call ExLockHandleTableEntry
PAGE:004932A3 neg al
PAGE:004932A5 sbb eax, eax
PAGE:004932A7 and eax, esi
PAGE:004932A9 loc_4932A9:
PAGE:004932A9 pop esi
PAGE:004932AA retn 8
PAGE:004932AA ExMapHandleToPointer endp

這里我們調用搜索HANDLE_TABLE的函數ExMapHandleToPointer以及設置Lock Bit的ExLockHandleTableEntry函數。要了解句柄表的內部結構我

們必須反匯編這些函數。先從ExpLookupHandleTableEntry函數開始:

Code:

PAGE:00493545 ExpLookupHandleTableEntry proc near
PAGE:00493545
PAGE:00493545
PAGE:00493545 HandleTable = dword ptr 0Ch
PAGE:00493545 Handle = dword ptr 10h
PAGE:00493545
PAGE:00493545 push esi
PAGE:00493546 push edi
PAGE:00493547 mov edi, [esp+Handle]
PAGE:0049354B mov eax, 0FFh
PAGE:00493550 mov ecx, edi
PAGE:00493552 mov edx, edi
PAGE:00493554 mov esi, edi
PAGE:00493556 shr ecx, 12h
PAGE:00493559 shr edx, 0Ah
PAGE:0049355C shr esi, 2
PAGE:0049355F and ecx, eax
PAGE:00493561 and edx, eax
PAGE:00493563 and esi, eax
PAGE:00493565 test edi, 0FC000000h
PAGE:0049356B jnz short loc_49358A
PAGE:0049356D mov eax, [esp+HandleTable]
PAGE:00493571 mov eax, [eax+8]
PAGE:00493574 mov ecx, [eax+ecx*4]
PAGE:00493577 test ecx, ecx
PAGE:00493579 jz short loc_49358A
PAGE:0049357B mov ecx, [ecx+edx*4]
PAGE:0049357E test ecx, ecx
PAGE:00493580 jz short loc_49358A
PAGE:00493582 lea eax, [ecx+esi*8]
PAGE:00493585 loc_493585:
PAGE:00493585 pop edi
PAGE:00493586 pop esi
PAGE:00493587 retn 8
PAGE:0049358A loc_49358A:
PAGE:0049358A xor eax, eax
PAGE:0049358C jmp short loc_493585
PAGE:0049358C ExpLookupHandleTableEntry endp

除此之外,我們來看看從ntoskrnl.pdb中得到的HANDLE_TABLE結構:

Code:

struct _HANDLE_TABLE {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long Flags;
/*<thisrel this+0x4>*/ /*|0x4|*/ long HandleCount;
/*<thisrel this+0x8>*/ /*|0x4|*/ struct _HANDLE_TABLE_ENTRY*** Table;
/*<thisrel this+0xc>*/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;
/*<thisrel this+0x10>*/ /*|0x4|*/ void* UniqueProcessId;
/*<thisrel this+0x14>*/ /*|0x4|*/ long FirstFreeTableEntry;
/*<thisrel this+0x18>*/ /*|0x4|*/ long NextIndexNeedingPool;
/*<thisrel this+0x1c>*/ /*|0x38|*/ struct _ERESOURCE HandleTableLock;
/*<thisrel this+0x54>*/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;
/*<thisrel this+0x5C>*/ /*|0x10|*/ struct _KEVENT HandleContentionEvent;
}; // <size 0x6c>

根據這些數據我們用C語言還原這個結構:

Code:

typedef struct _WIN2K_HANDLE_TABLE
{
ULONG Flags;
LONG HandleCount;
PHANDLE_TABLE_ENTRY **Table;
PEPROCESS QuotaProcess;
HANDLE UniqueProcessId;
LONG FirstFreeTableEntry;
LONG NextIndexNeedingPool;
ERESOURCE HandleTableLock;
LIST_ENTRY HandleTableList;
KEVENT HandleContentionEvent;
} WIN2K_HANDLE_TABLE , *PWIN2K_HANDLE_TABLE ;

顯而易見,句柄表由對象表的三個層次的索引組成。現在我們再來看看ExLookhandleTableEntry函數:

Code:

PAGE:00492E2B ExLockHandleTableEntry proc near
PAGE:00492E2B
PAGE:00492E2B
PAGE:00492E2B var_8 = dword ptr -8
PAGE:00492E2B var_4 = dword ptr -4
PAGE:00492E2B HandleTable = dword ptr 8
PAGE:00492E2B Entry = dword ptr 0Ch
PAGE:00492E2B
PAGE:00492E2B push ebp
PAGE:00492E2C mov ebp, esp
PAGE:00492E2E push ecx
PAGE:00492E2F push ecx
PAGE:00492E30 push ebx
PAGE:00492E31 push esi
PAGE:00492E32 xor ebx, ebx
PAGE:00492E34 loc_492E34:
PAGE:00492E34 mov eax, [ebp+Entry]
PAGE:00492E37 mov esi, [eax]
PAGE:00492E39 test esi, esi
PAGE:00492E3B mov [ebp+var_8], esi
PAGE:00492E3E jz short loc_492E89
PAGE:00492E40 jle short loc_492E64
PAGE:00492E42 mov eax, esi
PAGE:00492E44 or eax, 80000000h // set WIN2K_TABLE_ENTRY_LOCK_BIT
PAGE:00492E49 mov [ebp+var_4], eax
PAGE:00492E4C mov eax, [ebp+var_8]
PAGE:00492E4F mov ecx, [ebp+Entry]
PAGE:00492E52 mov edx, [ebp+var_4]
PAGE:00492E55 cmpxchg [ecx], edx
PAGE:00492E58 cmp eax, esi
PAGE:00492E5A jnz short loc_492E64
PAGE:00492E5C mov al, 1
PAGE:00492E5E loc_492E5E:
PAGE:00492E5E pop esi
PAGE:00492E5F pop ebx
PAGE:00492E60 leave
PAGE:00492E61 retn 8
PAGE:00492E64 loc_492E64:
PAGE:00492E64 mov eax, ebx
PAGE:00492E66 inc ebx
PAGE:00492E67 cmp eax, 1
PAGE:00492E6A jb loc_4BC234
PAGE:00492E70 mov eax, [ebp+HandleTable]
PAGE:00492E73 push offset unk_46D240 ; Timeout
PAGE:00492E78 push 0 ; Alertable
PAGE:00492E7A push 0 ; WaitMode
PAGE:00492E7C add eax, 5Ch
PAGE:00492E7F push 0 ; WaitReason
PAGE:00492E81 push eax ; Object
PAGE:00492E82 call KeWaitForSingleObject
PAGE:00492E87 jmp short loc_492E34
PAGE:00492E89 loc_492E89:
PAGE:00492E89 xor al, al
PAGE:00492E8B jmp short loc_492E5E
PAGE:00492E8B ExLockHandleTableEntry endp

這段代碼檢查了HANDLE_TABLE_ENTRY結構的Object成員的第31位,設置該位,如果該位被設置,意味著等待HANDLE_TABLE的

HandleContentionEvent。對我們來說設置TABLE_ENTRY_LOCK_BIT才是最重要的,因為它是目標地址的一部分,如果標志位沒有設置,我們就會

得到無效句柄。現在我們明白了句柄表的格式,可以寫代碼來遍歷這個表了:

Code:

void ScanWin2KHandleTable(PWIN2K_HANDLE_TABLE HandleTable)
{
int i, j, k;
PHANDLE_TABLE_ENTRY Entry;

for (i = 0; i < 0x100; i++)
{
if (HandleTable->Table)
{
for (j = 0; j < 0x100; j++)
{
if (HandleTable->Table[j])
{
for (k = 0; k < 0x100; k++)
{
Entry = &HandleTable->Table[j][k];

if (Entry->Object)
ProcessObject((PVOID)((ULONG)Entry->Object | WIN2K_TABLE_ENTRY_LOCK_BIT));
}
}
}
}
}
}

這段代碼處理了所有表中的成員,并且為每一個成員調用了ProcessObject函數。ProcessObject函數檢測成員類型并且恰當地處理了它們。這

個函數代碼如下:

Code:

void ProcessObject(PVOID Object)
{
POBJECT_HEADER ObjectHeader = OBJECT_TO_OBJECT_HEADER(Object);

if (ObjectHeader->Type == *PsProcessType) CollectProcess(Object);

if (ObjectHeader->Type == *PsThreadType) ThreadCollect(Object);
}

我們已經了解了Windows 2000下的句柄表結構,現在開始分析Windows XP的表結構。從反匯編ExpLookupHandleTableEntry函數開始:

Code:

PAGE:0048D3C1 ExpLookupHandleTableEntry proc near
PAGE:0048D3C1
PAGE:0048D3C1
PAGE:0048D3C1 HandleTable = dword ptr 8
PAGE:0048D3C1 Handle = dword ptr 0Ch
PAGE:0048D3C1
PAGE:0048D3C1 mov edi, edi
PAGE:0048D3C3 push ebp
PAGE:0048D3C4 mov ebp, esp
PAGE:0048D3C6 and [ebp+Handle], 0FFFFFFFCh
PAGE:0048D3CA mov eax, [ebp+Handle]
PAGE:0048D3CD mov ecx, [ebp+HandleTable]
PAGE:0048D3D0 mov edx, [ebp+Handle]
PAGE:0048D3D3 shr eax, 2
PAGE:0048D3D6 cmp edx, [ecx+38h]
PAGE:0048D3D9 jnb loc_4958D6
PAGE:0048D3DF push esi
PAGE:0048D3E0 mov esi, [ecx]
PAGE:0048D3E2 mov ecx, esi
PAGE:0048D3E4 and ecx, 3 // ecx - table level
PAGE:0048D3E7 and esi, not 3 // esi - pointer to first table
PAGE:0048D3EA sub ecx, 0
PAGE:0048D3ED jnz loc_48DEA4
PAGE:0048D3F3 lea eax, [esi+eax*8]
PAGE:0048D3F6 loc_48D3F6:
PAGE:0048D3F6 pop esi
PAGE:0048D3F7 loc_48D3F7:
PAGE:0048D3F7 pop ebp
PAGE:0048D3F8 retn 8
PAGE:0048DEA4 loc_48DEA4:
PAGE:0048DEA4 dec ecx
PAGE:0048DEA5 mov ecx, eax
PAGE:0048DEA7 jnz loc_52F57A
PAGE:0048DEAD shr ecx, 9
PAGE:0048DEB0 mov ecx, [esi+ecx*4]
PAGE:0048DEB3 loc_48DEB3:
PAGE:0048DEB3 and eax, 1FFh
PAGE:0048DEB8 lea eax, [ecx+eax*8]
PAGE:0048DEBB jmp loc_48D3F6
PAGE:0052F57A loc_52F57A:
PAGE:0052F57A shr ecx, 13h
PAGE:0052F57D mov edx, ecx
PAGE:0052F57F mov ecx, [esi+ecx*4]
PAGE:0052F582 shl edx, 13h
PAGE:0052F585 sub eax, edx
PAGE:0052F587 mov edx, eax
PAGE:0052F589 shr edx, 9
PAGE:0052F58C mov ecx, [ecx+edx*4]
PAGE:0052F58F jmp loc_48DEB3

再來看看ntoskrnl.pdb中的HANDLE_TABLE:

Code:

struct _HANDLE_TABLE {
// static data ------------------------------------
// non-static data --------------------------------
/*<thisrel this+0x0>*/ /*|0x4|*/ unsigned long TableCode;
/*<thisrel this+0x4>*/ /*|0x4|*/ struct _EPROCESS* QuotaProcess;
/*<thisrel this+0x8>*/ /*|0x4|*/ void* UniqueProcessId;
/*<thisrel this+0xc>*/ /*|0x10|*/ struct _EX_PUSH_LOCK HandleTableLock[4];
/*<thisrel this+0x1c>*/ /*|0x8|*/ struct _LIST_ENTRY HandleTableList;
/*<thisrel this+0x24>*/ /*|0x4|*/ struct _EX_PUSH_LOCK HandleContentionEvent;
/*<thisrel this+0x28>*/ /*|0x4|*/ struct _HANDLE_TRACE_DEBUG_INFO* DebugInfo;
/*<thisrel this+0x2c>*/ /*|0x4|*/ long ExtraInfoPages;
/*<thisrel this+0x30>*/ /*|0x4|*/ unsigned long FirstFree;
/*<thisrel this+0x34>*/ /*|0x4|*/ unsigned long LastFree;
/*<thisrel this+0x38>*/ /*|0x4|*/ unsigned long NextHandleNeedingPool;
/*<thisrel this+0x3c>*/ /*|0x4|*/ long HandleCount;
/*<thisrel this+0x40>*/ /*|0x4|*/ unsigned long Flags;
/*<bitfield this+0x40>*/ /*|0x1|*/ unsigned char StrictFIF0:1;
}; // <size 0x44>

利用以上信息還原該結構:

Code:

typedef struct _XP_HANDLE_TABLE
{
ULONG TableCode;
PEPROCESS QuotaProcess;
PVOID UniqueProcessId;
EX_PUSH_LOCK HandleTableLock[4];
LIST_ENTRY HandleTableList;
EX_PUSH_LOCK HandleContentionEvent;
PHANDLE_TRACE_DEBUG_INFO DebugInfo;
LONG ExtraInfoPages;
ULONG FirstFree;
ULONG LastFree;
ULONG NextHandleNeedingPool;
LONG HandleCount;
LONG Flags;
UCHAR StrictFIFO;
} XP_HANDLE_TABLE, *PXP_HANDLE_TABLE;

從上面的表中可以很明顯看出ExpLookupHandleTableEntry函數從HANDLE_TABLE結構中得到TableCode的值,并基于其低2位的內容計算出表的層

次數。其余的位指向第1層表。因此Windows XP下的HANDLE_TABLE可以擁有1到3個層次,每一個層次的表的大小為1FFh。當表中記錄的數量增加

時,系統會自動增加層數。很明顯,當表中記錄的數量超過0x200的時候表就會擁有兩層,當大于0x40000時增加到第3層。不知道當銷毀對象時

系統會不會減少表的層數,我沒有注意到這件事。

Windows XP下沒有ExLockHandleTableEntry函數,因此表中相應的模塊被定位在ExMapHandleToPointer函數中。反匯編這個函數看看它做了什

么?

Code:

PAGE:0048F61E ExMapHandleToPointer proc near
PAGE:0048F61E
PAGE:0048F61E
PAGE:0048F61E var_8 = dword ptr -8
PAGE:0048F61E var_4 = dword ptr -4
PAGE:0048F61E HandleTable = dword ptr 8
PAGE:0048F61E Handle = dword ptr 0Ch
PAGE:0048F61E
PAGE:0048F61E mov edi, edi
PAGE:0048F620 push ebp
PAGE:0048F621 mov ebp, esp
PAGE:0048F623 push ecx
PAGE:0048F624 push ecx
PAGE:0048F625 push edi
PAGE:0048F626 mov edi, [ebp+Handle]
PAGE:0048F629 test di, 7FCh
PAGE:0048F62E jz loc_4A2A36
PAGE:0048F634 push ebx
PAGE:0048F635 push esi
PAGE:0048F636 push edi
PAGE:0048F637 push [ebp+HandleTable]
PAGE:0048F63A call ExpLookupHandleTableEntry
PAGE:0048F63F mov esi, eax
PAGE:0048F641 test esi, esi
PAGE:0048F643 jz loc_4A2711
PAGE:0048F649 mov [ebp+var_4], esi
PAGE:0048F64C loc_48F64C:
PAGE:0048F64C mov ebx, [esi]
PAGE:0048F64E test bl, 1
PAGE:0048F651 mov [ebp+var_8], ebx
PAGE:0048F654 jz loc_508844
PAGE:0048F65A lea eax, [ebx-1]
PAGE:0048F65D mov [ebp+Handle], eax
PAGE:0048F660 mov eax, [ebp+var_8]
PAGE:0048F663 mov ecx, [ebp+var_4]
PAGE:0048F666 mov edx, [ebp+Handle]
PAGE:0048F669 cmpxchg [ecx], edx
PAGE:0048F66C cmp eax, ebx
PAGE:0048F66E jnz loc_50884C
PAGE:0048F674 mov eax, esi
PAGE:0048F676 loc_48F676:
PAGE:0048F676 pop esi
PAGE:0048F677 pop ebx
PAGE:0048F678 loc_48F678:
PAGE:0048F678 pop edi
PAGE:0048F679 leave
PAGE:0048F67A retn 8
PAGE:0048F67A ExMapHandleToPointer endp

ExpLookuphandleTableEntry函數返回指向HANDLE_TABLE_ENTRY的指針后,我們要檢查Object域的低字節,如果是被設置了的,說明是被清除了

的(俄文翻譯者kao注:希望我翻譯的對...),如果該位沒有被設置,我們要等到它被設置了為止。因此當得到對象地址的時候我們不應該設

置高位(Windows 2000平臺),而是要清除低位。綜上所述,掃描表的代碼如下:

Code:

void ScanXpHandleTable(PXP_HANDLE_TABLE HandleTable)
{
int i, j, k;
PHANDLE_TABLE_ENTRY Entry;
ULONG TableCode = HandleTable->TableCode & ~TABLE_LEVEL_MASK;

switch (HandleTable->TableCode & TABLE_LEVEL_MASK)
{
case 0 :
for (i = 0; i < 0x200; i++)
{
Entry = &((PHANDLE_TABLE_ENTRY)TableCode);

if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
break;

case 1 :
for (i = 0; i < 0x200; i++)
{
if (((PVOID *)TableCode))
{
for (j = 0; j < 0x200; j++)
{
Entry = &((PHANDLE_TABLE_ENTRY *)TableCode)[j];

if (Entry->Object) ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
}
}
break;

case 2 :
for (i = 0; i < 0x200; i++)
{
if (((PVOID *)TableCode))
{
for (j = 0; j < 0x200; j++)
{
if (((PVOID **)TableCode)[j])
{
for (k = 0; k < 0x200; k++)
{
Entry = &((PHANDLE_TABLE_ENTRY **)TableCode)[j][k];

if (Entry->Object)
ProcessObject((PVOID)((ULONG)Entry->Object & ~XP_TABLE_ENTRY_LOCK_BIT));
}
}
}
}
}
break;
}
}

我們已經明白對象表格式了。現在想要枚舉進程我們還需要得到PspCidTable的地址。也許你已經猜到了,我們要在

PsLookupProcessByProcessId函數中搜索,這個函數中的第1個函數調用包含著PspCidTable的地址。代碼如下:

Code:

void GetPspCidTable()
{
PUCHAR cPtr, pOpcode;
ULONG Length;

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

if (!Length) break;

if (*(PUSHORT)cPtr == 0x35FF && *(pOpcode + 6) == 0xE8)
{
PspCidTable = **(PVOID **)(pOpcode + 2);
break;
}
}
}

現在我們知道怎樣處理PspCidTable了,可以非常容易地查看到所有進程的表中的所有元素,分析那些屬于隱藏進程的對象,就像我們在用戶態

做的一樣,如果你已經理解了前面所講的東西,你一定可以做得到。

中文參考:《JIURL玩玩Win2k進程線程篇 HANDLE_TABLE》(http://jiurl.nease.net/document/JiurlPlayWin2k/PsHandleTable.htm)

感謝:firstrose, JIURL, linhanshi.
【聲明】:黑吧安全網(http://www.zjtpzs.live)登載此文出于傳遞更多信息之目的,并不代表本站贊同其觀點和對其真實性負責,僅適于網絡安全技術愛好者學習研究使用,學習中請遵循國家相關法律法規。如有問題請聯系我們,聯系郵箱[email protected],我們會在最短的時間內進行處理。
  • 最新更新
    • 相關閱讀
      • 本類熱門
        • 最近下載
        神秘东方电子游艺 赶会卖东西赚钱吗 北京11选五手机版 云南时时彩开奖 新浪爱彩竞彩足球比分即时比分 福建时时彩开奖时间 2018种抱子甘蓝赚钱吗 南粤风采36选7走势图 欧洲彩票大奖中奖号码 刷抖音 赚钱 深圳风采最新开奖公告官网 时时彩开奖直播网址 辽宁35选7大星彩票 安徽快3开奖结果今天3 北京赛车pk拾高手技巧 浙江11选5选号方法 15选5预测专家幸运