偵測隱藏程序的強文
阿新 • • 發佈:2018-11-11
分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow
也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!
俄語原文: 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
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 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
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)來說所有存在的程序的控制代碼都是可以訪問的。另外,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
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;
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;
當然,使用者態隱藏程序的檢測方法不止這些,還可以想一些稍微複雜一點的新方法(比如,用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:00