轉(zhuǎn)載學(xué)習(xí)
原文:
http://blog.csdn.net/SpiderF/archive/2005/04/05/336594.aspx
在NT系列操作系統(tǒng)里讓自己“消失”
1. 內(nèi)容
2. 介紹
3. 文件
3.1 NtQueryDirectoryFile
3.2 NtVdmControl
4. 進(jìn)程
5. 注冊表
5.1 NtEnumerateKey
5.2 NtEnumerateValueKey
6. 系統(tǒng)服務(wù)和驅(qū)動
7. 掛鉤和擴(kuò)展
7.1 權(quán)限
7.2 全局掛鉤
7.3 新進(jìn)程
7.4 DLL
8. 內(nèi)存
9. 句柄
9.1 命名句柄并獲得類型
10. 端口
10.1 Netstat, OpPorts和FPortWinXP下
10.2 OpPorts在Win2k和NT4下, FPort在Win2k下
11. 結(jié)束
=====[ 2. 介紹 ]==================================================
這篇文檔是在Windows NT操作系統(tǒng)下隱藏對象、文件、服務(wù)、進(jìn)程等的技術(shù)。這種方法是基于Windows API函數(shù)的掛鉤。
這篇文章中所描述的技術(shù)都是從我寫rootkit的研究成果,所以它能寫rootkit更有效果并且更簡單。這里也同樣包括了我的實踐。
在這篇文檔中隱藏對象意味著改變某些用來命名這些對象的系統(tǒng)函數(shù),使它們將忽略這些對象的名字。這樣一來我們改動的那些函數(shù)的返回值表示這些對象根本就不存在。
最基本的方法(除去少數(shù)不同的)是我們用原始的參數(shù)調(diào)用原始的函數(shù),然后我們改變它們的輸出。
在這篇文章里將描述隱藏文件、進(jìn)程、注冊表鍵和鍵值、系統(tǒng)服務(wù)和驅(qū)動、分配的內(nèi)存還有句柄。
=====[ 3. 文件 ]========================================
在有很多種隱藏文件使系統(tǒng)無法發(fā)現(xiàn)的可能。我們只使用改變API的方法,而沒使用那些比如涉及到文件系統(tǒng)的技術(shù)。這樣會更容易些因為我們無法知道文件系統(tǒng)工作的獨特性。
=====[ 3.1 NtQueryDirectoryFile ]=============================
在WINNT里在某些目錄中尋找某個文件的方法是枚舉它里面所有的文件和它的子目錄下的所有文件。文件的枚舉是使用NtQueryDirectoryFile函數(shù)。
NTSTATUS NtQueryDirectoryFile(
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG FileInformationLength,
IN FILE_INFORMATION_CLASS FileInformationClass,
IN BOOLEAN ReturnSingleEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartScan
);
對我們來說重要的參數(shù)是FileHandle,FileInformation和FileInformationClass。FileHandle是從NtOpenFile獲得的目錄對象句柄。FileInformation是一個指針,指向函數(shù)要寫入需要的數(shù)據(jù)的已分配內(nèi)存。FileInformationClass決定寫入FileImformation的記錄的類型。
FileInformationClass是一個變化的枚舉類型,我們只需要其中4個值來枚舉目錄內(nèi)容:
#define FileDirectoryInformation 1
#define FileFullDirectoryInformation 2
#define FileBothDirectoryInformation 3
#define FileNamesInformation 12
要寫入FileInformation的FileDirecoryInformation記錄的結(jié)構(gòu):
typedef struct _FILE_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
FileFullDirectoryInformation:
typedef struct _FILE_FULL_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
WCHAR FileName[1];
} FILE_FULL_DIRECTORY_INFORMATION, *PFILE_FULL_DIRECTORY_INFORMATION;
FileBothDirectoryInformation:
typedef struct _FILE_BOTH_DIRECTORY_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
LARGE_INTEGER CreationTime;
LARGE_INTEGER LastAccessTime;
LARGE_INTEGER LastWriteTime;
LARGE_INTEGER ChangeTime;
LARGE_INTEGER EndOfFile;
LARGE_INTEGER AllocationSize;
ULONG FileAttributes;
ULONG FileNameLength;
ULONG EaInformationLength;
UCHAR AlternateNameLength;
WCHAR AlternateName[12];
WCHAR FileName[1];
} FILE_BOTH_DIRECTORY_INFORMATION, *PFILE_BOTH_DIRECTORY_INFORMATION;
FileNamesInformation:
typedef struct _FILE_NAMES_INFORMATION {
ULONG NextEntryOffset;
ULONG Unknown;
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
這個函數(shù)在FileInformation中寫入這些結(jié)構(gòu)的一個列表。對我們來說在這些結(jié)構(gòu)類型中只有3個變量是重要的。
NextEntryOffset是這個列表中項的偏移地址。第一個項在地址FileInformation+0處,所以第二個項在地址是FileInformation+第一個項的NextEntryOffset。最后一個項的NextEntryOffset是0。
FileName是文件全名。
FileNameLength是文件名長度。
如果我們想要隱藏一個文件,我們需要分別通知這4種類型,對每種類型的返回記錄我們需要和我們打算隱藏的文件比較名字。如果我們打算隱藏第一個記錄,我們可以把后面的結(jié)構(gòu)向前移動,移動長度為第一個結(jié)構(gòu)的長度,這樣會導(dǎo)致第一個記錄被改寫。如果我們想要隱藏其它任何一個,只需要很容易的改變上一個記錄的NextEntryOffset的值就行。如果我們要隱藏最后一個記錄就把它的NextEntryOffset改為0,否則NextEntryOffset的值應(yīng)為我們想要隱藏的那個記錄和前一個的NextEntryOffset值的和。然后修改前一個記錄的Unknown變量的值,它是下一次搜索的索引。把要隱藏的記錄之前一個記錄的Unknown變量的值改為我們要隱藏的那個記錄的Unkown變量的值即可。
如果沒有原本應(yīng)該可見的記錄被找到,我們就返回STATUS_NO_SUCH_FILE。
#define STATUS_NO_SUCH_FILE 0xC000000F
=====[ 3.2 NtVdmControl ]========================================
不知什么原因DOS的枚舉NTVDM能夠通過函數(shù)NtVdmControl也能獲得文件的列表。
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
ConcrolCode標(biāo)明了在緩沖區(qū)ControlData中申請數(shù)據(jù)的子函數(shù)。如果ControlCode為VdmDiretoryFile那么這個函數(shù)的功能將和FileInformation設(shè)置為FileBothDirectoryInformation的函數(shù)NtQueryDirectoryFile功能一樣。
#define VdmDirectoryFile 6
這時的ControlData的用法就和FileInformation一樣。這里唯一的不同就是我們不知道緩沖區(qū)的長度。所以我們需要手動來計算它的長度。我們把所有記錄的NextEntryOffset和最后一個記錄的FileNameLength還有0X5E(最后一個記錄除去文件名的長度)。隱藏的方法和前面提到的使用NtQueryDirectoryFile的方法一樣。
=====[ 4. 進(jìn)程 ]========================================
各種進(jìn)程信息是通過NtQuerySystemInformation獲取的。
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
SystemInformationClass標(biāo)明了我們想要獲得的信息的類別,SystemInformation是一個指向函數(shù)輸出緩沖區(qū)的指針,SystemInformationLength是這個緩沖區(qū)的長度,ReturnLength是寫入字節(jié)的數(shù)目。
對于正在運行的進(jìn)程的枚舉我們使用設(shè)置為SystemProcessesAndThreadsInformation的SystemInformationClass。
#define SystemInformationClass 5
在SystemInformation的緩沖區(qū)中返回的數(shù)據(jù)結(jié)構(gòu)是:
typedef struct _SYSTEM_PROCESSES {
ULONG NextEntryDelta;
ULONG ThreadCount;
ULONG Reserved1[6];
LARGE_INTEGER CreateTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER KernelTime;
UNICODE_STRING ProcessName;
KPRIORITY BasePriority;
ULONG ProcessId;
ULONG InheritedFromProcessId;
ULONG HandleCount;
ULONG Reserved2[2];
VM_COUNTERS VmCounters;
IO_COUNTERS IoCounters; // Windows 2000特有的
SYSTEM_THREADS Threads[1];
} SYSTEM_PROCESSES, *PSYSTEM_PROCESSES;
隱藏進(jìn)程和隱藏文件方法基本一樣,就是改動我們需要隱藏的記錄的前一個記錄的NextEntryDelta。通常我們不用隱藏第一個記錄,因為它是空閑進(jìn)程(Idle process)。
=====[ 5. 注冊表 ]========================================
Windows的注冊表是一個很大的樹形數(shù)據(jù)結(jié)構(gòu),對我們來說里面有兩種重要的記錄類型需要隱藏。一種類型是注冊表鍵,另一種是鍵值。因為注冊表的結(jié)構(gòu),隱藏注冊表鍵不象隱藏文件或進(jìn)程那么麻煩。
=====[ 5.1 NtEnumerateKey ]===============================
因為注冊表的結(jié)構(gòu)我們不能請求某個指定部分所有鍵的列表。我們只能在注冊表某個部分通過查詢指定鍵的索引以獲得它的信息。這里提供了NtEnumerateKey。
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
KeyHandle是已經(jīng)用索引標(biāo)明我們想要從中獲取信息的子鍵的句柄。KeyInformationClass標(biāo)明了返回信息類型。數(shù)據(jù)最后寫入KeyInformaiton緩沖區(qū),緩沖區(qū)長度為KeyInformationLength。寫入的字節(jié)數(shù)由ResultLength返回。
我們需要意識到的最重要的東西是如果我們隱藏了某個鍵,在這個鍵之后的所有鍵的索引都會改變。因為我們是通過高位的索引來獲取鍵的信息,并通過低位的索引來請求這個鍵。所以我們必須記錄之前有多少個記錄被隱藏,然后返回正確的值。
讓我們來看個例子。假設(shè)我們在注冊表中有一些鍵名字是A,B,C,D,E和F。它們的索引從0開始,也就是說索引4對應(yīng)鍵E。現(xiàn)在我們?nèi)绻胍[藏鍵B,被掛鉤過的應(yīng)用程序用索引4調(diào)用NtEnumerateKey時我們應(yīng)該返回F鍵的信息因為有一個索引改變了。現(xiàn)在問題是我們不知道是否會有索引被改變。如果我們不注意索引的改變而對于索引4的請求仍然返回鍵E而不是鍵F的話,很有可能在我們用索引1請求時什么都返回不了或者返回鍵C。這兩種情況都會導(dǎo)致錯誤。這就是為什么我們要注意索引的改變。
現(xiàn)在如果我們通過用索引0到Index重新調(diào)用函數(shù)來記錄轉(zhuǎn)移我們可能會等待一段時間(在1GHz處理器上普通的注冊表就得等10秒種那么長的時間)。所以我們不得不想出一種更加巧妙的方法。
我們知道鍵是按字母排序的(除了引用外)。如果我們忽略引用(我們不需要隱藏)我們能使用以下方法記錄改變。我們通過字母排序列出我們想要隱藏的鍵名的列表(使用RtlCompareUnicodeString),然后當(dāng)應(yīng)用程序調(diào)用NtEnumerateKey時我們不需要用不可變的變量重新調(diào)用它,而能夠找到用索引標(biāo)明的記錄的名字。
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
String1和String2是將要比較的字符串,CaseInSensitive在不忽略大小寫時被設(shè)置為True。
函數(shù)結(jié)果描述String1和String2的關(guān)系:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
現(xiàn)在我們需要找到一個邊緣項。我們在列表中對用索引標(biāo)明的鍵按字母比較名字。邊緣項是在我們列表中最后一個較短的名字。我們知道轉(zhuǎn)移最多是我們列表中邊緣項的數(shù)量。但并不是所有我們列表中的項都是注冊表中有效的鍵。所以我們不得不請求我們列表中達(dá)到邊緣項的所有的在注冊表中這個部分的項。這些通過調(diào)用NtOpenKey來完成。
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
KeyHandle是高位的鍵的句柄,我們使用NtEnumerateKey的這個值。DesaireAccess是訪問權(quán)力。KEY_ENUMERATE_SUB_KEYS是它的正確的值。ObjectAttributes描述了我們要打開的子鍵(包括了它的名字)。
#define KEY_ENUMERATE_SUB_KEYS 8
如果NtOpenKey返回0表示打開成功,意味著這個來自我們列表中的鍵是存在的。被打開的鍵通過NtClose來關(guān)閉。
NTSTATUS NtClose(
IN HANDLE Handle
);
對每次NtEnumareteKey的調(diào)用我們要計算的改變,數(shù)量上等同于我們列表中存在于注冊表指定部分的鍵的數(shù)量。然后我們把改變的數(shù)量加到變量Index,最后調(diào)用原始的NtEnumerateKey。
我們使用KeyInformationClass的KeyBasicInformation來獲得用索引標(biāo)明的鍵的名字。
#define KeyBasicInformation 0
NtEnumerateKey在KeyInformation緩沖區(qū)中返回這個結(jié)構(gòu):
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
這里我們只需要的東西是Name和它的長度NameLength。
如果沒有被轉(zhuǎn)移的索引的記載我們就返回錯誤STATUS_EA_LIST_INCONSISTENT。
#define STATUS_EA_LIST_INCONSISTENT 0x80000014
=====[ 5.2 NtEnumerateValueKey ]============================
注冊表鍵值不是按字母分類的。幸運的是在一個鍵里鍵值的數(shù)目比較少,所以我們可以通過重調(diào)的方法來獲得改變的數(shù)目。用來獲取一個鍵值信息的API是NtEnumerateValueKey。
NTSTATUS NtEnumerateValueKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
OUT PVOID KeyValueInformation,
IN ULONG KeyValueInformationLength,
OUT PULONG ResultLength
);
KeyHandle也是等級高的鍵的句柄。Index是所給鍵中鍵值的索引。KeyValueInformationClass描述信息的類型,保存在KeyValueInformation緩沖區(qū)中,緩沖區(qū)以字節(jié)為大小為KeyValueInformationLength。寫入字節(jié)的數(shù)量返回在ResultLength中。
我們通過用0到Index的所有索引重調(diào)函數(shù)計算轉(zhuǎn)移。鍵值的名字通過把KeyValueInformationClass設(shè)置為KeyValueBasicInformation來獲取。
#define KeyValueBasicInformation 0
然后我們獲取在KeyValueInformation緩沖區(qū)中接下來的數(shù)據(jù)結(jié)構(gòu):
typedef struct _KEY_VALUE_BASIC_INFORMATION {
ULONG TitleIndex;
ULONG Type;
ULONG NameLength;
WCHAR Name[1];
} KEY_VALUE_BASIC_INFORMATION, *PKEY_VALUE_BASIC_INFORMATION;
這里我們只對Name和NameLength感興趣。
如果這里沒有被轉(zhuǎn)移的索引記載我們就返回錯誤STATUS_NO_MORE_ENTRIES。
#define STATUS_NO_MORE_ENTRIES 0x8000001A
=====[ 6. 系統(tǒng)服務(wù)和驅(qū)動 ]====================================
系統(tǒng)服務(wù)和驅(qū)動是通過4個獨立的API函數(shù)枚舉的。它們在每個Windows版本中的聯(lián)系都不一樣。所以我們必須掛鉤所有4個函數(shù)。
BOOL EnumServicesStatusA(
SC_HANDLE hSCManager,
DWORD dwServiceType,
DWORD dwServiceState,
LPENUM_SERVICE_STATUS lpServices,
DWORD cbBufSize,
LPDWORD pcbBytesNeeded,
LPDWORD lpServicesReturned,
LPDWORD lpResumeHandle
);
BOOL EnumServiceGroupW(
SC_HANDLE hSCManager,
DWORD dwServiceType,
DWORD dwServiceState,
LPBYTE lpServices,
DWORD cbBufSize,
LPDWORD pcbBytesNeeded,
LPDWORD lpServicesReturned,
LPDWORD lpResumeHandle,
DWORD dwUnknown
);
BOOL EnumServicesStatusExA(
SC_HANDLE hSCManager,
SC_ENUM_TYPE InfoLevel,
DWORD dwServiceType,
DWORD dwServiceState,
LPBYTE lpServices,
DWORD cbBufSize,
LPDWORD pcbBytesNeeded,
LPDWORD lpServicesReturned,
LPDWORD lpResumeHandle,
LPCTSTR pszGroupName
);
BOOL EnumServicesStatusExW(
SC_HANDLE hSCManager,
SC_ENUM_TYPE InfoLevel,
DWORD dwServiceType,
DWORD dwServiceState,
LPBYTE lpServices,
DWORD cbBufSize,
LPDWORD pcbBytesNeeded,
LPDWORD lpServicesReturned,
LPDWORD lpResumeHandle,
LPCTSTR pszGroupName
);
這里最重要的是lpService,它指向保存服務(wù)列表的緩沖區(qū)。而指向結(jié)果中記錄個數(shù)的lpServicesReturned也很重要。輸出緩沖區(qū)中的數(shù)據(jù)結(jié)構(gòu)取決于函數(shù)類型。函數(shù)EnumServicesStatusA和
EnumServicesGroupW返回這個結(jié)構(gòu):
typedef struct _ENUM_SERVICE_STATUS {
LPTSTR lpServiceName;
LPTSTR lpDisplayName;
SERVICE_STATUS ServiceStatus;
} ENUM_SERVICE_STATUS, *LPENUM_SERVICE_STATUS;
typedef struct _SERVICE_STATUS {
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;
函數(shù)EnumServicesStatusExA和EnumServicesStatusExW返回這個:
typedef struct _ENUM_SERVICE_STATUS_PROCESS {
LPTSTR lpServiceName;
LPTSTR lpDisplayName;
SERVICE_STATUS_PROCESS ServiceStatusProcess;
} ENUM_SERVICE_STATUS_PROCESS, *LPENUM_SERVICE_STATUS_PROCESS;
typedef struct _SERVICE_STATUS_PROCESS {
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
DWORD dwProcessId;
DWORD dwServiceFlags;
} SERVICE_STATUS_PROCESS, *LPSERVICE_STATUS_PROCESS;
我們只對lpServiceName感興趣因為它是系統(tǒng)服務(wù)的名字。所有記錄都有靜態(tài)的大小,所以我們想要隱藏一個的話就需要將之后所有記錄向前移它的大小。這里我們必須區(qū)分SERVICE_STATUS和SERVICE_STATUS_PROCESS的大小。
=====[ 7. 動態(tài)掛鉤和擴(kuò)展 ]=====================================
為達(dá)到預(yù)想的效果我們需要掛鉤所有正在運行的進(jìn)程和所有將要被創(chuàng)建的進(jìn)程。所有新進(jìn)程都必須在它們運行第一條指令前被掛鉤,否則它們就能夠在被掛夠前看到被隱藏的對象。
=====[ 7.1 權(quán)限 ]=============================================
首先我們得知道我們至少獲得管理員administrator權(quán)限來獲得進(jìn)入所有正在運行的進(jìn)程。最好的可能是將我們的進(jìn)程當(dāng)做系統(tǒng)服務(wù)來運行,因為它運行與SYSTEM用戶權(quán)限下。為安裝服務(wù)我們首先得獲取特殊的權(quán)限。
獲取SeDebugPrivilege的權(quán)限是很有用的,通過調(diào)用OpenProcessToken、LookupPrivilegeValue
和AdjustTokenPrivileges來完成。
BOOL OpenProcessToken(
HANDLE ProcessHandle,
DWORD DesiredAccess,
PHANDLE TokenHandle
);
BOOL LookupPrivilegeValue(
LPCTSTR lpSystemName,
LPCTSTR lpName,
PLUID lpLuid
);
BOOL AdjustTokenPrivileges(
HANDLE TokenHandle,
BOOL DisableAllPrivileges,
PTOKEN_PRIVILEGES NewState,
DWORD BufferLength,
PTOKEN_PRIVILEGES PreviousState,
PDWORD ReturnLength
);
代碼如下:
#define SE_PRIVILEGE_ENABLED 0x0002
#define TOKEN_QUERY 0x0008
#define TOKEN_ADJUST_PRIVILEGES 0x0020
HANDLE hToken;
LUID DebugNameValue;
TOKEN_PRIVILEGES Privileges;
DWORD dwRet;
OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,hToken);
LookupPrivilegeValue(NULL,"SeDebugPrivilege",&DebugNameValue);
Privileges.PrivilegeCount=1;
Privileges.Privileges[0].Luid=DebugNameValue;
Privileges.Privileges[0].Attributes=SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken,FALSE,&Privileges,sizeof(Privileges),
NULL,&dwRet);
CloseHandle(hToken);
=====[ 7.2 全局掛鉤 ]=======================================
枚舉進(jìn)程通過前面提到的API函數(shù)NtQuerySystemInformation來完成。因為系統(tǒng)中還有一些內(nèi)部native進(jìn)程,所以使用重寫函數(shù)第一個指令的方法來掛鉤。對每個正在運行的進(jìn)程我們需要做的都一樣。首先在目標(biāo)進(jìn)程里分配一部分內(nèi)存用來寫入我們用來掛鉤函數(shù)的新代碼,然后把每個函數(shù)開始的5個字節(jié)改為跳轉(zhuǎn)指令(jmp),這個跳轉(zhuǎn)會轉(zhuǎn)為執(zhí)行我們的代碼。所以當(dāng)被掛鉤的函數(shù)被調(diào)用時跳轉(zhuǎn)指令能立刻被執(zhí)行。我們需要保存每個函數(shù)開始被改寫的指令,需要它們來調(diào)用被掛鉤函數(shù)的原始代碼。保存指令的過程在"掛鉤Windows API"的3.2.3節(jié)有描述。
首先通過NtOpenProcess打開目標(biāo)進(jìn)程并獲取句柄。如果我們沒有足夠權(quán)限的話就會失敗。
NTSTATUS NtOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
ProcessHandle是指向保存進(jìn)程對象句柄的指針。DesiredAccess應(yīng)該被設(shè)置為PROCESS_ALL_ACCESS。我們要在ClientId結(jié)構(gòu)里設(shè)置UniqueProcess為目標(biāo)進(jìn)程的PID,UniqueThread應(yīng)該為0。被打開的句柄可以通過NtClose關(guān)閉。
#define PROCESS_ALL_ACCESS 0x001F0FFF
現(xiàn)在我們?yōu)槲覀兊拇a分配部分內(nèi)存。這通過NtAllocateVirtualMemory來完成。
NTSTATUS NtAllocateVirtualMemory(
IN HANDLE ProcessHandle,
IN OUT PVOID BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG AllocationSize,
IN ULONG AllocationType,
IN ULONG Protect
);
ProcessHandle是來自NtOpenProcess相同參數(shù)。BaseAddress是一個指針,指向被分配虛擬內(nèi)存基地址的開始處,它的輸入?yún)?shù)應(yīng)該為NULL。AllocationSize指向我們要分配的字節(jié)數(shù)的變量,同樣它也用來接受實際分配的字節(jié)數(shù)大小。最好把AllocationType在設(shè)置成MEM_COMMIT之外再加上MEM_TOP_DOWN因為內(nèi)存要在接近DLL地址的盡可能高的地址分配。
#define MEM_COMMIT 0x00001000
#define MEM_TOP_DOWN 0x00100000
然后我們就可以通過調(diào)用NtWriteVirtualMemory來寫入我們的代碼。
NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
BaseAddress是NtAllocateVirtualMemory返回的地址。Buffer指向我們要寫入的字節(jié),BufferLength是我們要寫入的字節(jié)數(shù)。
現(xiàn)在我們來掛鉤單個進(jìn)程。被加載入所有進(jìn)程的動態(tài)鏈接庫只有ntdll.dll。所以我們要檢查被導(dǎo)入進(jìn)程要掛鉤的函數(shù)是否來自ntdll.dll。但是這些來自其它DLL的函數(shù)所在的內(nèi)存可能已經(jīng)被分配,這時重寫它的代碼會在目標(biāo)進(jìn)程里導(dǎo)致錯誤。這就是我們必須去檢查我們要掛鉤的函數(shù)來自的動態(tài)鏈接庫是否被目標(biāo)進(jìn)程加載的原因。
我們需要通過NtQueryInformationProcess獲取目標(biāo)進(jìn)程的PEB(進(jìn)程環(huán)境塊)。
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
我們把ProcessInformationClass設(shè)置為ProcessBasicInformation,然后PROCESS_BASIC_INFORMATION結(jié)構(gòu)會返回到ProcessInformation緩沖區(qū)中,大小為給定的ProcessInformationLength。
#define ProcessBasicInformation 0
typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
KAFFINITY AffinityMask;
KPRIORITY BasePriority;
ULONG UniqueProcessId;
ULONG InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
PebBaseAddress就是我們要尋找的東西。在PebBaseAddress+0C處是PPEB_LDR_DATA的地址。這些通過調(diào)用NtReadVirtualMemory來獲得。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
變量和NtWriteVirtualMemory的很相似。
在PPEB_LDR_DATA+01C處是InInitializationOrderModuleList的地址。它是被加載進(jìn)進(jìn)程的動態(tài)鏈接庫的列表。我們只對這個結(jié)構(gòu)中的一些部分感興趣。
typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
PVOID Next,
PVOID Prev,
DWORD ImageBase,
DWORD ImageEntry,
DWORD ImageSize,
...
);
Next是指向下一個記錄的指針,Prev指向前一個,最后一個記錄的會指向第一個。ImageBase是內(nèi)存中模塊的地址,ImageEntry是模快的入口點,ImageSize是它的大小。
對所有我們想要掛鉤的庫我們需要獲得它們的ImageBase(比方調(diào)用GetModuleHandle或者LoadLibrary)。然后把這個ImageBase和InInitializationOrderModuleList的ImageBase比較。
現(xiàn)在我們已經(jīng)為掛鉤準(zhǔn)備就緒。因為我們是掛鉤正在運行的進(jìn)程,所以可能我們正在改寫代碼的同時代碼被執(zhí)行,這時就會導(dǎo)致錯誤。所以首先我們就得停止目標(biāo)進(jìn)程里的所有線程。它的所有線程列表可以通過設(shè)置了SystemProcessAndThreadInformation的NtQuerySystemInformation來獲得。有關(guān)這個函數(shù)的描述參考第4節(jié)。但是還得加入SYSTEM_THREADS結(jié)構(gòu)的描述,用來保存線程的信息。
typedef struct _SYSTEM_THREADS {
LARGE_INTEGER KernelTime;
LARGE_INTEGER UserTime;
LARGE_INTEGER CreateTime;
ULONG WaitTime;
PVOID StartAddress;
CLIENT_ID ClientId;
KPRIORITY Priority;
KPRIORITY BasePriority;
ULONG ContextSwitchCount;
THREAD_STATE State;
KWAIT_REASON WaitReason;
} SYSTEM_THREADS, *PSYSTEM_THREADS;
對每個線程調(diào)用NtOpenThread獲取它們的句柄,通過使用ClientId。
NTSTATUS NtOpenThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId
);
我們需要的句柄被保存在ThreadHandle。我們需要把DesiredAccess設(shè)置為THREAD_SUSPEND_RESUME。
#define THREAD_SUSPEND_RESUME 2
ThreadHandle用來調(diào)用NtSuspendThread。
NTSTATUS NtSuspendThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
被掛起的進(jìn)程就可以被改寫了。我們按照"掛鉤Windows API"里3.2.2節(jié)里描述的方法處理。唯一的不同是使用其它進(jìn)程的函數(shù)。
掛鉤完后我們就可以調(diào)用NtResumeThread恢復(fù)所有線程的運行。
NTSTATUS NtResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
=====[ 7.3 新進(jìn)程 ]================================================
感染所有正在運行的進(jìn)程并不能影響將要被運行的進(jìn)程。我們可以每隔一定時間獲取一次進(jìn)程的列表,然后感染新的列表里的進(jìn)程。但這種方法很不可靠。
更好的方法是掛鉤新進(jìn)程開始時肯定會調(diào)用的函數(shù)。因為所有系統(tǒng)中正在運行的進(jìn)程都已經(jīng)被掛鉤,所以這種方法不會漏掉任何新的進(jìn)程。我們可以掛鉤NtCreateThread,但這不是最簡單的方法。我們可以掛鉤NtResumeThread,因為它也是每當(dāng)新進(jìn)程創(chuàng)建時被調(diào)用,它在NtCreateThread之后被調(diào)用。
唯一的問題在于,這個函數(shù)并不只在新進(jìn)程被創(chuàng)建時調(diào)用。但我們能很容易解決這點。NtQueryInformationThread能給我們指定線程是屬于哪個進(jìn)程的信息。最后我們要做的就是檢查進(jìn)程是否已經(jīng)被掛鉤了。這通過讀取我們要掛鉤的函數(shù)的開始5個字節(jié)來完成。
NTSTATUS NtQueryInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ThreadInformationClass是信息分類,在這里它被設(shè)置為ThreadBasicInformation。ThreadInformation是保存結(jié)果的緩沖區(qū),大小按字節(jié)計算為ThreadInformationLength。
#define ThreadBasicInformation 0
對ThreadBasicInformation返回這個結(jié)構(gòu):
typedef struct _THREAD_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PNT_TIB TebBaseAddress;
CLIENT_ID ClientId;
KAFFINITY AffinityMask;
KPRIORITY Priority;
KPRIORITY BasePriority;
} THREAD_BASIC_INFORMATION, *PTHREAD_BASIC_INFORMATION;
ClientId是線程所屬進(jìn)程的PID。
現(xiàn)在我們來感染新進(jìn)程。問題就是新進(jìn)程的地址空間中只有ntdll.dll,其他的模塊在調(diào)用NtResumeThread之后被加載。有幾種方法可以解決這個問題,比方說我們可以掛鉤一個名為LdrInitializeThunk的API函數(shù),它在進(jìn)程初始化時被調(diào)用。
NTSTATUS LdrInitializeThunk(
DWORD Unknown1,
DWORD Unknown2,
DWORD Unknown3
);
首先我們先運行原始的代碼,然后掛鉤新進(jìn)程里所有要掛鉤的函數(shù)。但最好對LdrInitializeThunk解除掛鉤,因為這個函數(shù)在之后要被調(diào)用很多次,我們并不需要重新再掛鉤所有的函數(shù)。這時在程序執(zhí)行第一個指令前所有工作已經(jīng)完成。這就是為什么在我們掛鉤它之前它沒有機(jī)會調(diào)用任何一個被掛鉤過的函數(shù)的原因。
對自己掛鉤和動態(tài)掛鉤正在運行的進(jìn)程一樣,只是這里我們不需要關(guān)心正在運行的線程。
=====[ 7.4 DLL ]================================================
系統(tǒng)中每個進(jìn)程都是一份ntdll.dll拷貝。這意味著我們可以在進(jìn)程初始化階段掛鉤這個模塊里的任意一個函數(shù)。但是來自其它模塊比如kernel32.dll或advapi32.dll的函數(shù)該怎么辦呢?還有一些進(jìn)程只有ntdll.dll,其他模塊都是在進(jìn)程被掛鉤之后在運行過程中才被動態(tài)加載的。這就是我們還得掛鉤加載新模塊的函數(shù)LdrLoadDll的原因。
NTSTATUS LdrLoadDll(
PWSTR szcwPath,
PDWORD pdwLdrErr,
PUNICODE_STRING pUniModuleName,
PHINSTANCE pResultInstance
);
這里對我們來說最重要的是pUniModuleName,它保存模塊名字。當(dāng)調(diào)用成功后pResultInstance保存模塊地址。
我們首先調(diào)用原始的LdrLoadDll然后掛鉤被加載模塊里所有函數(shù)。
=====[ 8. 內(nèi)存 ]===========================================
當(dāng)我們正在掛鉤一個函數(shù)時我們會修改它開始的字節(jié)。通過調(diào)用NtReadVirtualMemory任何人都可以檢測出函數(shù)被掛鉤。所以我們還要掛鉤NtReadVirtualMemory來防止檢測。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
我們修改了我們掛鉤的函數(shù)開始的字節(jié)并且為我們新的代碼分配了內(nèi)存。我們就需要檢查時候有人讀取了這些代碼。如果我們的代碼出現(xiàn)在BaseAddress到BaseAddress+BufferLength中我們就需要在緩沖區(qū)中改變它的一些字節(jié)。
如果有人在我們分配的內(nèi)存中查詢字節(jié)我們就返回空的緩沖區(qū)和錯誤STATUS_PARTIAL_COPY。這個值用來表示被請求的字節(jié)并沒有完全被拷貝到緩沖區(qū)中,它也同樣被用在當(dāng)請求了未分配的內(nèi)存時。這時ReturnLength應(yīng)該被設(shè)為0。
#define STATUS_PARTIAL_COPY 0x8000000D
如果有人查詢被掛鉤的函數(shù)開始的字節(jié)我們就調(diào)用原始代碼并拷貝原始代碼里開始的那些字節(jié)到緩沖區(qū)中。
現(xiàn)在新進(jìn)程已無法通過讀取它的內(nèi)存來檢測是否被掛鉤了。同樣如果你調(diào)試被掛鉤的進(jìn)程調(diào)試器也會用問題,它會顯示原始代碼,但卻執(zhí)行我們的代碼。
為了使隱藏更完美,我們還要掛鉤NtQueryVirtualMemory。這個函數(shù)用來獲取虛擬內(nèi)存的信息。我們掛鉤它來防止探測我們分配的虛逆內(nèi)存。
NTSTATUS NtQueryVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
OUT PVOID MemoryInformation,
IN ULONG MemoryInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
MemoryInformationClass標(biāo)明了返回數(shù)據(jù)的類別。我們對開始的2種類型感興趣。
#define MemoryBasicInformation 0
#define MemoryWorkingSetList 1
對MemoryBasicInformation返回這個結(jié)構(gòu):
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
ULONG RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
每個區(qū)段都有它的大小RegionSize和它的類型Type。空閑內(nèi)存的類型是MEM_FREE。(區(qū)段對象就是文件映射對象,是可被映射到一個進(jìn)程的虛逆地址空間的對象)
#define MEM_FREE 0x10000
如果我們代碼之前一個區(qū)段的類型是MEM_FREE我們就在它的RegionSize加上我們代碼的區(qū)段的大小。如果我們代碼之后的區(qū)段的類型也是MEM_FREE那么就在之前區(qū)段的RegionSize上再加上之后的空閑區(qū)段的大小。
如果我們代碼之前的區(qū)段是其它類型,我們就對我們代碼的區(qū)段返回MEM_FREE。它的大小根據(jù)之后的區(qū)段來計算。
對MemoryWorkingSetList返回這個結(jié)構(gòu):
typedef struct _MEMORY_WORKING_SET_LIST {
ULONG NumberOfPages;
ULONG WorkingSetList[1];
} MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;
NumberOfPages是WorkingSetList中列項的數(shù)目。這個數(shù)字應(yīng)該減少一些。我們在WorkingSetList中找到我們代碼的區(qū)段然后把之后記錄前移。WorkingSetList是按DWORD排列的數(shù)組,每個元素的高20位標(biāo)明了區(qū)段地址,低12位是標(biāo)志。
=====[ 9. 句柄 ]=========================================
用類SystemHandleInformation來調(diào)用NtQuerySystemInformation會在_SYSTEM_HANDLE_INFORMATION_EX結(jié)構(gòu)中獲取所有被打開的句柄的數(shù)組。
#define SystemHandleInformation 0x10
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX {
ULONG NumberOfHandles;
SYSTEM_HANDLE_INFORMATION Information[1];
} SYSTEM_HANDLE_INFORMATION_EX, *PSYSTEM_HANDLE_INFORMATION_EX;
ProcessId標(biāo)明了擁有句柄的進(jìn)程。ObjectTypeNumber是句柄類型。NumberOfHandles是Information數(shù)組中元素的數(shù)量。隱藏其中一項是很麻煩的,我們要去掉所有之后的元素并減少NumberOfHandles。去掉之后所有元素是必須的,因為數(shù)組中句柄是按ProcessId分組的。這意味著一個來自同一個進(jìn)程中的所有句柄都在一塊兒。對于一個進(jìn)程變量Handle的數(shù)量是不斷增加的。
現(xiàn)在回想一下這個函數(shù)(NtQuerySystemInformation)使用SystemProcessAndThreadsInformation類來調(diào)用時返回的結(jié)構(gòu)_SYSTEM_PROCESSES。這里我們能夠看到每個進(jìn)程都有它自己的句柄的數(shù)量在HandleCount中。如果我們想要做得更完美我們就應(yīng)該修改HandleCount,因為用SystemProcessesAndThreadsInformation類調(diào)用這個函數(shù)時隱藏了不少句柄。但校正是非常浪費時間的。在系統(tǒng)正常運行的一小段時間里就會有很多句柄正在打開或關(guān)上。所以在對這個函數(shù)兩次緊挨著的調(diào)用句柄的數(shù)量被更改是很正常的,所以我們根本不需要改變HandleCount。
=====[ 9.1 命名句柄并獲取類型 ]===================================
隱藏句柄很麻煩,但找出哪個句柄該被隱藏更困難一些。比方說我們要隱藏一個進(jìn)程就要隱藏它的所有句柄并隱藏所有和它有聯(lián)系的句柄。我們比較句柄的ProcessId參數(shù)和想要隱藏的進(jìn)程的PID,如果它們相等就隱藏這個句柄。但是其它進(jìn)程的句柄在我們能比較任何東西之前不得不先命名。系統(tǒng)中句柄的數(shù)量通常很龐大,所以最好在嘗試命名之前先比較句柄類型。命名類型可以為我們不感興趣的句柄省不少時間。
命名句柄和句柄類型通過調(diào)用NtQueryObject來完成。
NTSTATUS ZwQueryObject(
IN HANDLE ObjectHandle,
IN OBJECT_INFORMATION_CLASS ObjectInformationClass,
OUT PVOID ObjectInformation,
IN ULONG ObjectInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ObjectHandle是我們想要獲取有關(guān)信息的句柄,ObjectInformationClass是信息類型,保存在以字節(jié)計算長度為ObjectInformationLength的緩沖區(qū)ObjectInformation中。
我們對OBJECT_INFORMATION_CLASS使用的類是ObjectNameInformation和ObjectAllTypesInformation。ObjectNameInfromation類在緩沖區(qū)中返回OBJECT_NAME_INFORMATION結(jié)構(gòu),而ObjectAllTypesInformation類返回OBJECT_ALL_TYPES_INFORMATION結(jié)構(gòu)。
#define ObjectNameInformation 1
#define ObjectAllTypesInformation 3
typedef struct _OBJECT_NAME_INFORMATION {
UNICODE_STRING Name;
} OBJECT_NAME_INFORMATION, *POBJECT_NAME_INFORMATION;
Name決定了句柄的名字。
typedef struct _OBJECT_TYPE_INFORMATION {
UNICODE_STRING Name;
ULONG ObjectCount;
ULONG HandleCount;
ULONG Reserved1[4];
ULONG PeakObjectCount;
ULONG PeakHandleCount;
ULONG Reserved2[4];
ULONG InvalidAttributes;
GENERIC_MAPPING GenericMapping;
ULONG ValidAccess;
UCHAR Unknown;
BOOLEAN MaintainHandleDatabase;
POOL_TYPE PoolType;
ULONG PagedPoolUsage;
ULONG NonPagedPoolUsage;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
typedef struct _OBJECT_ALL_TYPES_INFORMATION {
ULONG NumberOfTypes;
OBJECT_TYPE_INFORMATION TypeInformation;
} OBJECT_ALL_TYPES_INFORMATION, *POBJECT_ALL_TYPES_INFORMATION;
Name決定類型對象名字,類型對象緊跟在每個OBJECT_TYPE_INFORMATION結(jié)構(gòu)后面。下一個OBJECT_TYPE_INFORMATION結(jié)構(gòu)跟在這個Name后面,間隔4個字節(jié)。
SYSTEM_HANDLE_INFORMATION結(jié)構(gòu)中的ObjectTypeNumber是TypeInformation數(shù)組中的索引。
比較困難的是獲取其他進(jìn)程中句柄的名字。這里有兩種命名的可能性。一是通過調(diào)用NtDuplicateObject把句柄拷貝到我們的進(jìn)程中然后命名它。這種方法對某些特殊類型的句柄會失敗。但由于它失敗的次數(shù)比較少,所以我們采用這種方法。
NtDuplicateObject(
IN HANDLE SourceProcessHandle,
IN HANDLE SourceHandle,
IN HANDLE TargetProcessHandle,
OUT PHANDLE TargetHandle OPTIONAL,
IN ACCESS_MASK DesiredAccess,
IN ULONG Attributes,
IN ULONG Options
);
SourceHandle是我們想要拷貝的句柄,SourceProcessHandle是擁有SourceHandle的進(jìn)程的句柄。TargetProcessHandle是想要拷貝到的進(jìn)程的句柄,在這里是我們進(jìn)程的句柄。TargetHandle是指向保存原始句柄拷貝的指針。DesiredAccess應(yīng)該被設(shè)為PROCESS_QUERY_INFORMATION,Attributes和Options設(shè)為0。
第二種命名方法對所有句柄都有效,就是使用系統(tǒng)驅(qū)動。源代碼可以在http://rootkit.host.sk的OpHandle項目里找到。
=====[ 10. 端口 ]==========================================
枚舉打開端口最簡單的方法是調(diào)用AllocateAndGetTcpTableFromStack和AllocateAndGetUdpTableFromStack函數(shù),或者AllocateAndGetTcpExTableFromStack和AllocateAndGetUdpExTableFromStack函數(shù),它們都來自iphlpapi.dll。帶Ex的函數(shù)從Windows XP才開始有效。
typedef struct _MIB_TCPROW {
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
} MIB_TCPROW, *PMIB_TCPROW;
typedef struct _MIB_TCPTABLE {
DWORD dwNumEntries;
MIB_TCPROW table[ANY_SIZE];
} MIB_TCPTABLE, *PMIB_TCPTABLE;
typedef struct _MIB_UDPROW {
DWORD dwLocalAddr;
DWORD dwLocalPort;
} MIB_UDPROW, *PMIB_UDPROW;
typedef struct _MIB_UDPTABLE {
DWORD dwNumEntries;
MIB_UDPROW table[ANY_SIZE];
} MIB_UDPTABLE, *PMIB_UDPTABLE;
typedef struct _MIB_TCPROW_EX
{
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
DWORD dwProcessId;
} MIB_TCPROW_EX, *PMIB_TCPROW_EX;
typedef struct _MIB_TCPTABLE_EX
{
DWORD dwNumEntries;
MIB_TCPROW_EX table[ANY_SIZE];
} MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX;
typedef struct _MIB_UDPROW_EX
{
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwProcessId;
} MIB_UDPROW_EX, *PMIB_UDPROW_EX;
typedef struct _MIB_UDPTABLE_EX
{
DWORD dwNumEntries;
MIB_UDPROW_EX table[ANY_SIZE];
} MIB_UDPTABLE_EX, *PMIB_UDPTABLE_EX;
DWORD WINAPI AllocateAndGetTcpTableFromStack(
OUT PMIB_TCPTABLE *pTcpTable,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetUdpTableFromStack(
OUT PMIB_UDPTABLE *pUdpTable,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetTcpExTableFromStack(
OUT PMIB_TCPTABLE_EX *pTcpTableEx,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
DWORD WINAPI AllocateAndGetUdpExTableFromStack(
OUT PMIB_UDPTABLE_EX *pUdpTableEx,
IN BOOL bOrder,
IN HANDLE hAllocHeap,
IN DWORD dwAllocFlags,
IN DWORD dwProtocolVersion;
);
還有另外一種方法。當(dāng)程序創(chuàng)建了一個套接字并開始監(jiān)聽時,它就會有一個為它和打開端口的打開句柄。我們在系統(tǒng)中枚舉所有的打開句柄并通過NtDeviceIoControlFile把它們發(fā)送到一個特定的緩沖區(qū)中,來找出這個句柄是否是一個打開端口的。這樣也能給我們有關(guān)端口的信息。因為打開句柄太多了,所以我們只檢測類型是File并且名字是\Device\Tcp或\Device\Udp的。打開端口只有這種類型和名字。
如果你看一下iphlpapi.dll里函數(shù)的代碼,就會發(fā)現(xiàn)這些函數(shù)同樣調(diào)用NtDeviceIoControlFile并發(fā)送到一個特定緩沖區(qū)來獲得系統(tǒng)中所有打開端口的列表。這意味著我們要想隱藏端口只需要掛鉤NtDeviceIoControlFile函數(shù)。
NTSTATUS NtDeviceIoControlFile(
IN HANDLE FileHandle
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength
);
我們感興趣的成員變量有這幾個:FileHandle標(biāo)明了要通信的設(shè)備的句柄,IoStatusBlock指向接收最后完成狀態(tài)和請求操作信息的變量,IoControlCode是指定要完成的特定的I/O控制操作的數(shù)字,InputBuffer包含了輸入的數(shù)據(jù),長度為按字節(jié)計算的InputBufferLength,相似的還有OutputBuffer和OutputBufferLength。
=====[ 10.1 WinXP下使用Netstat OpPorts FPort ]=========================
在Windoes XP獲得所有打開端口的列表可以使用一些軟件比方OpPorts、FPort和Netstat。
這里程序用IoControlCode0x000120003調(diào)用了NtDeviceIoControlFile兩次。輸出緩沖區(qū)在第二次調(diào)用時被填滿。FileHandle的名字這里總是\Device\Tcp。InputBuffer因不同類型的調(diào)用而不同:
1) 為獲得MIB_TCPROW數(shù)組InputBuffer看起來是這樣:
第一次調(diào)用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次調(diào)用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
2) 為獲得MIB_UDPROW數(shù)組:
第一次調(diào)用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次調(diào)用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
3) 為獲得MIB_TCPROW_EX數(shù)組:
第一次調(diào)用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次調(diào)用:
0x00 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
4) 為獲得MIB_UDPROW_EX數(shù)組:
第一次調(diào)用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
第二次調(diào)用:
0x01 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x00 0x00 0x00 0x01 0x00 0x00
0x02 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00
你可以看到緩沖區(qū)只有少數(shù)字節(jié)不同。我們現(xiàn)在比較清晰地簡要說明一下:
我們感興趣的調(diào)用是InputBuffer[1]為0x04且InputBuffer[17]為0x01。只有使用這些輸入數(shù)據(jù)才能使OutputBuffer里為我們想要的表。如果我們想要獲得TCP端口信息我們就把InputBuffer[0]設(shè)為0x00,想獲得UDP端口信息就把它設(shè)為0x01。如果我們還需要額外的輸出表(MIB_TCPROW_EX或MIB_UDPROW_EX)我們就在第二次調(diào)用里把InfputBufer[16]設(shè)為0x02。
如果我們發(fā)現(xiàn)使用了這幾個參數(shù)的調(diào)用我們就修改輸出緩沖區(qū)。獲取輸出緩沖區(qū)中ROW的數(shù)量可以很容易根據(jù)ROW的大小分開IoStatusBlock結(jié)構(gòu)里的Infotmation變量。隱藏其中一個ROW就會變的容易,只需要用之后的ROW改寫他并刪掉最后一個ROW。不要忘了修改OutputBufferLength和IoStatusBlock。
=====[ 10.2 Win2k和NT4下使用OpPorts, Win2k下使用FPort ]==========================
我們用IoControlCode0x00210012調(diào)用NtDeviceIoControlFile來判斷這個擁有類型File和名字\Device\Tcp或\Device\Udp是否是打開端口的句柄。
所以最先我們比較IoControlCode然后是類型和句柄名字。如果這些都符合就接著比較輸入緩沖區(qū)長度,它應(yīng)該和結(jié)構(gòu)TDI_CONNECTION_IN長度一樣,為0x18。輸出緩沖區(qū)的結(jié)構(gòu)是TDI_CONNECTION_OUT。
typedef struct _TDI_CONNETION_IN
{
ULONG UserDataLength,
PVOID UserData,
ULONG OptionsLength,
PVOID Options,
ULONG RemoteAddressLength,
PVOID RemoteAddress
} TDI_CONNETION_IN, *PTDI_CONNETION_IN;
typedef struct _TDI_CONNETION_OUT
{
ULONG State,
ULONG Event,
ULONG TransmittedTsdus,
ULONG ReceivedTsdus,
ULONG TransmissionErrors,
ULONG ReceiveErrors,
LARGE_INTEGER Throughput
LARGE_INTEGER Delay,
ULONG SendBufferSize,
ULONG ReceiveBufferSize,
ULONG Unreliable,
ULONG Unknown1[5],
USHORT Unknown2
} TDI_CONNETION_OUT, *PTDI_CONNETION_OUT;
具體判斷句柄是不是一個打開端口的方法請參考OpPorts的源代碼,在http://rookit.host.sk上可以找到。我們現(xiàn)在來隱藏指定端口。我們已經(jīng)比較過了InputBufferLength和IoControlCode,現(xiàn)在來比較RemoteAddressLength,對打開端口來說它總是3或4。最后要做的是比較OutputBufferBuffer里的ReceiveTsdus,用網(wǎng)絡(luò)上的端口和要隱藏的端口列表比較。區(qū)別TCP和UDP的做法是句柄的名字不一樣。在刪除了OutputBuffer、修改IoStatusBlock并返回STATUS_INVALID_ADDRESS后我們就已經(jīng)隱藏了這個端口了。
=====[ 11. 結(jié)束語 ]===============================================
具體細(xì)節(jié)請參考Hacker defender rootkit version 1.0.0的源代碼,在http://rootkit.host.sk和http://www.rootkit.com都可以找到。
在將來我還會加入更多有關(guān)的技術(shù)。這篇文檔的更新版本會改進(jìn)現(xiàn)有的方法和并加入新的思想。
特別感謝Ratter提供了很多完成這篇文檔和Hacker defender代碼所需要的技術(shù)。
===================================[ End ]==============================
后記:
其實只要我們對Windows的內(nèi)核有一定程度的了解我們都知道單純靠掛鉤函數(shù)是不能真正做到隱藏的,這樣做只不過是欺騙操作系統(tǒng)的使用者,卻欺騙不了操作系統(tǒng)自己。線程要想被運行就必須獲得時間片,將自己加入調(diào)度鏈表中,從而暴露自己。內(nèi)核維護(hù)一組被稱為調(diào)度程序數(shù)據(jù)庫的數(shù)據(jù)結(jié)構(gòu)來做出線程調(diào)度的決策。其中最重要的結(jié)構(gòu)是調(diào)度程序就緒隊列(KiDispatckerReadyListHead)。它里面有64個DWORD,分別對應(yīng)于32個線程優(yōu)先級的隊列,隊列包含處于就緒狀態(tài)的線程,正在等待調(diào)度執(zhí)行。還有兩個隊列KiWaitInListHead和KiWaitOutListHead保存著處于等待狀態(tài)的線程。可以很簡單的枚舉這3個鏈表中的所有元素從而列出系統(tǒng)中的所有線程。因此要想徹底從Windows系統(tǒng)里“消失”就要從Windows的內(nèi)核下手(Windows的內(nèi)核只負(fù)責(zé)線程調(diào)度,其它功能由執(zhí)行程序組件完成)。這個功能還有待完成。
水平有限,歡迎大家指出錯漏之處。QQ:27324838 Email:kinvis@hotmail.com