轉載學習
原文:
http://blog.csdn.net/SpiderF/archive/2005/04/05/336594.aspx
在NT系列操作系統里讓自己“消失”
1. 內容
2. 介紹
3. 文件
3.1 NtQueryDirectoryFile
3.2 NtVdmControl
4. 進程
5. 注冊表
5.1 NtEnumerateKey
5.2 NtEnumerateValueKey
6. 系統服務和驅動
7. 掛鉤和擴展
7.1 權限
7.2 全局掛鉤
7.3 新進程
7.4 DLL
8. 內存
9. 句柄
9.1 命名句柄并獲得類型
10. 端口
10.1 Netstat, OpPorts和FPortWinXP下
10.2 OpPorts在Win2k和NT4下, FPort在Win2k下
11. 結束
=====[ 2. 介紹 ]==================================================
這篇文檔是在Windows NT操作系統下隱藏對象、文件、服務、進程等的技術。這種方法是基于Windows API函數的掛鉤。
這篇文章中所描述的技術都是從我寫rootkit的研究成果,所以它能寫rootkit更有效果并且更簡單。這里也同樣包括了我的實踐。
在這篇文檔中隱藏對象意味著改變某些用來命名這些對象的系統函數,使它們將忽略這些對象的名字。這樣一來我們改動的那些函數的返回值表示這些對象根本就不存在。
最基本的方法(除去少數不同的)是我們用原始的參數調用原始的函數,然后我們改變它們的輸出。
在這篇文章里將描述隱藏文件、進程、注冊表鍵和鍵值、系統服務和驅動、分配的內存還有句柄。
=====[ 3. 文件 ]========================================
在有很多種隱藏文件使系統無法發現的可能。我們只使用改變API的方法,而沒使用那些比如涉及到文件系統的技術。這樣會更容易些因為我們無法知道文件系統工作的獨特性。
=====[ 3.1 NtQueryDirectoryFile ]=============================
在WINNT里在某些目錄中尋找某個文件的方法是枚舉它里面所有的文件和它的子目錄下的所有文件。文件的枚舉是使用NtQueryDirectoryFile函數。
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
);
對我們來說重要的參數是FileHandle,FileInformation和FileInformationClass。FileHandle是從NtOpenFile獲得的目錄對象句柄。FileInformation是一個指針,指向函數要寫入需要的數據的已分配內存。FileInformationClass決定寫入FileImformation的記錄的類型。
FileInformationClass是一個變化的枚舉類型,我們只需要其中4個值來枚舉目錄內容:
#define FileDirectoryInformation 1
#define FileFullDirectoryInformation 2
#define FileBothDirectoryInformation 3
#define FileNamesInformation 12
要寫入FileInformation的FileDirecoryInformation記錄的結構:
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;
這個函數在FileInformation中寫入這些結構的一個列表。對我們來說在這些結構類型中只有3個變量是重要的。
NextEntryOffset是這個列表中項的偏移地址。第一個項在地址FileInformation+0處,所以第二個項在地址是FileInformation+第一個項的NextEntryOffset。最后一個項的NextEntryOffset是0。
FileName是文件全名。
FileNameLength是文件名長度。
如果我們想要隱藏一個文件,我們需要分別通知這4種類型,對每種類型的返回記錄我們需要和我們打算隱藏的文件比較名字。如果我們打算隱藏第一個記錄,我們可以把后面的結構向前移動,移動長度為第一個結構的長度,這樣會導致第一個記錄被改寫。如果我們想要隱藏其它任何一個,只需要很容易的改變上一個記錄的NextEntryOffset的值就行。如果我們要隱藏最后一個記錄就把它的NextEntryOffset改為0,否則NextEntryOffset的值應為我們想要隱藏的那個記錄和前一個的NextEntryOffset值的和。然后修改前一個記錄的Unknown變量的值,它是下一次搜索的索引。把要隱藏的記錄之前一個記錄的Unknown變量的值改為我們要隱藏的那個記錄的Unkown變量的值即可。
如果沒有原本應該可見的記錄被找到,我們就返回STATUS_NO_SUCH_FILE。
#define STATUS_NO_SUCH_FILE 0xC000000F
=====[ 3.2 NtVdmControl ]========================================
不知什么原因DOS的枚舉NTVDM能夠通過函數NtVdmControl也能獲得文件的列表。
NTSTATUS NtVdmControl(
IN ULONG ControlCode,
IN PVOID ControlData
);
ConcrolCode標明了在緩沖區ControlData中申請數據的子函數。如果ControlCode為VdmDiretoryFile那么這個函數的功能將和FileInformation設置為FileBothDirectoryInformation的函數NtQueryDirectoryFile功能一樣。
#define VdmDirectoryFile 6
這時的ControlData的用法就和FileInformation一樣。這里唯一的不同就是我們不知道緩沖區的長度。所以我們需要手動來計算它的長度。我們把所有記錄的NextEntryOffset和最后一個記錄的FileNameLength還有0X5E(最后一個記錄除去文件名的長度)。隱藏的方法和前面提到的使用NtQueryDirectoryFile的方法一樣。
=====[ 4. 進程 ]========================================
各種進程信息是通過NtQuerySystemInformation獲取的。
NTSTATUS NtQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
SystemInformationClass標明了我們想要獲得的信息的類別,SystemInformation是一個指向函數輸出緩沖區的指針,SystemInformationLength是這個緩沖區的長度,ReturnLength是寫入字節的數目。
對于正在運行的進程的枚舉我們使用設置為SystemProcessesAndThreadsInformation的SystemInformationClass。
#define SystemInformationClass 5
在SystemInformation的緩沖區中返回的數據結構是:
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;
隱藏進程和隱藏文件方法基本一樣,就是改動我們需要隱藏的記錄的前一個記錄的NextEntryDelta。通常我們不用隱藏第一個記錄,因為它是空閑進程(Idle process)。
=====[ 5. 注冊表 ]========================================
Windows的注冊表是一個很大的樹形數據結構,對我們來說里面有兩種重要的記錄類型需要隱藏。一種類型是注冊表鍵,另一種是鍵值。因為注冊表的結構,隱藏注冊表鍵不象隱藏文件或進程那么麻煩。
=====[ 5.1 NtEnumerateKey ]===============================
因為注冊表的結構我們不能請求某個指定部分所有鍵的列表。我們只能在注冊表某個部分通過查詢指定鍵的索引以獲得它的信息。這里提供了NtEnumerateKey。
NTSTATUS NtEnumerateKey(
IN HANDLE KeyHandle,
IN ULONG Index,
IN KEY_INFORMATION_CLASS KeyInformationClass,
OUT PVOID KeyInformation,
IN ULONG KeyInformationLength,
OUT PULONG ResultLength
);
KeyHandle是已經用索引標明我們想要從中獲取信息的子鍵的句柄。KeyInformationClass標明了返回信息類型。數據最后寫入KeyInformaiton緩沖區,緩沖區長度為KeyInformationLength。寫入的字節數由ResultLength返回。
我們需要意識到的最重要的東西是如果我們隱藏了某個鍵,在這個鍵之后的所有鍵的索引都會改變。因為我們是通過高位的索引來獲取鍵的信息,并通過低位的索引來請求這個鍵。所以我們必須記錄之前有多少個記錄被隱藏,然后返回正確的值。
讓我們來看個例子。假設我們在注冊表中有一些鍵名字是A,B,C,D,E和F。它們的索引從0開始,也就是說索引4對應鍵E。現在我們如果想要隱藏鍵B,被掛鉤過的應用程序用索引4調用NtEnumerateKey時我們應該返回F鍵的信息因為有一個索引改變了。現在問題是我們不知道是否會有索引被改變。如果我們不注意索引的改變而對于索引4的請求仍然返回鍵E而不是鍵F的話,很有可能在我們用索引1請求時什么都返回不了或者返回鍵C。這兩種情況都會導致錯誤。這就是為什么我們要注意索引的改變。
現在如果我們通過用索引0到Index重新調用函數來記錄轉移我們可能會等待一段時間(在1GHz處理器上普通的注冊表就得等10秒種那么長的時間)。所以我們不得不想出一種更加巧妙的方法。
我們知道鍵是按字母排序的(除了引用外)。如果我們忽略引用(我們不需要隱藏)我們能使用以下方法記錄改變。我們通過字母排序列出我們想要隱藏的鍵名的列表(使用RtlCompareUnicodeString),然后當應用程序調用NtEnumerateKey時我們不需要用不可變的變量重新調用它,而能夠找到用索引標明的記錄的名字。
NTSTATUS RtlCompareUnicodeString(
IN PUNICODE_STRING String1,
IN PUNICODE_STRING String2,
IN BOOLEAN CaseInSensitive
);
String1和String2是將要比較的字符串,CaseInSensitive在不忽略大小寫時被設置為True。
函數結果描述String1和String2的關系:
result > 0: String1 > String2
result = 0: String1 = String2
result < 0: String1 < String2
現在我們需要找到一個邊緣項。我們在列表中對用索引標明的鍵按字母比較名字。邊緣項是在我們列表中最后一個較短的名字。我們知道轉移最多是我們列表中邊緣項的數量。但并不是所有我們列表中的項都是注冊表中有效的鍵。所以我們不得不請求我們列表中達到邊緣項的所有的在注冊表中這個部分的項。這些通過調用NtOpenKey來完成。
NTSTATUS NtOpenKey(
OUT PHANDLE KeyHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes
);
KeyHandle是高位的鍵的句柄,我們使用NtEnumerateKey的這個值。DesaireAccess是訪問權力。KEY_ENUMERATE_SUB_KEYS是它的正確的值。ObjectAttributes描述了我們要打開的子鍵(包括了它的名字)。
#define KEY_ENUMERATE_SUB_KEYS 8
如果NtOpenKey返回0表示打開成功,意味著這個來自我們列表中的鍵是存在的。被打開的鍵通過NtClose來關閉。
NTSTATUS NtClose(
IN HANDLE Handle
);
對每次NtEnumareteKey的調用我們要計算的改變,數量上等同于我們列表中存在于注冊表指定部分的鍵的數量。然后我們把改變的數量加到變量Index,最后調用原始的NtEnumerateKey。
我們使用KeyInformationClass的KeyBasicInformation來獲得用索引標明的鍵的名字。
#define KeyBasicInformation 0
NtEnumerateKey在KeyInformation緩沖區中返回這個結構:
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
} KEY_BASIC_INFORMATION, *PKEY_BASIC_INFORMATION;
這里我們只需要的東西是Name和它的長度NameLength。
如果沒有被轉移的索引的記載我們就返回錯誤STATUS_EA_LIST_INCONSISTENT。
#define STATUS_EA_LIST_INCONSISTENT 0x80000014
=====[ 5.2 NtEnumerateValueKey ]============================
注冊表鍵值不是按字母分類的。幸運的是在一個鍵里鍵值的數目比較少,所以我們可以通過重調的方法來獲得改變的數目。用來獲取一個鍵值信息的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緩沖區中,緩沖區以字節為大小為KeyValueInformationLength。寫入字節的數量返回在ResultLength中。
我們通過用0到Index的所有索引重調函數計算轉移。鍵值的名字通過把KeyValueInformationClass設置為KeyValueBasicInformation來獲取。
#define KeyValueBasicInformation 0
然后我們獲取在KeyValueInformation緩沖區中接下來的數據結構:
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感興趣。
如果這里沒有被轉移的索引記載我們就返回錯誤STATUS_NO_MORE_ENTRIES。
#define STATUS_NO_MORE_ENTRIES 0x8000001A
=====[ 6. 系統服務和驅動 ]====================================
系統服務和驅動是通過4個獨立的API函數枚舉的。它們在每個Windows版本中的聯系都不一樣。所以我們必須掛鉤所有4個函數。
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,它指向保存服務列表的緩沖區。而指向結果中記錄個數的lpServicesReturned也很重要。輸出緩沖區中的數據結構取決于函數類型。函數EnumServicesStatusA和
EnumServicesGroupW返回這個結構:
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;
函數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感興趣因為它是系統服務的名字。所有記錄都有靜態的大小,所以我們想要隱藏一個的話就需要將之后所有記錄向前移它的大小。這里我們必須區分SERVICE_STATUS和SERVICE_STATUS_PROCESS的大小。
=====[ 7. 動態掛鉤和擴展 ]=====================================
為達到預想的效果我們需要掛鉤所有正在運行的進程和所有將要被創建的進程。所有新進程都必須在它們運行第一條指令前被掛鉤,否則它們就能夠在被掛夠前看到被隱藏的對象。
=====[ 7.1 權限 ]=============================================
首先我們得知道我們至少獲得管理員administrator權限來獲得進入所有正在運行的進程。最好的可能是將我們的進程當做系統服務來運行,因為它運行與SYSTEM用戶權限下。為安裝服務我們首先得獲取特殊的權限。
獲取SeDebugPrivilege的權限是很有用的,通過調用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 全局掛鉤 ]=======================================
枚舉進程通過前面提到的API函數NtQuerySystemInformation來完成。因為系統中還有一些內部native進程,所以使用重寫函數第一個指令的方法來掛鉤。對每個正在運行的進程我們需要做的都一樣。首先在目標進程里分配一部分內存用來寫入我們用來掛鉤函數的新代碼,然后把每個函數開始的5個字節改為跳轉指令(jmp),這個跳轉會轉為執行我們的代碼。所以當被掛鉤的函數被調用時跳轉指令能立刻被執行。我們需要保存每個函數開始被改寫的指令,需要它們來調用被掛鉤函數的原始代碼。保存指令的過程在"掛鉤Windows API"的3.2.3節有描述。
首先通過NtOpenProcess打開目標進程并獲取句柄。如果我們沒有足夠權限的話就會失敗。
NTSTATUS NtOpenProcess(
OUT PHANDLE ProcessHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId OPTIONAL
);
ProcessHandle是指向保存進程對象句柄的指針。DesiredAccess應該被設置為PROCESS_ALL_ACCESS。我們要在ClientId結構里設置UniqueProcess為目標進程的PID,UniqueThread應該為0。被打開的句柄可以通過NtClose關閉。
#define PROCESS_ALL_ACCESS 0x001F0FFF
現在我們為我們的代碼分配部分內存。這通過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相同參數。BaseAddress是一個指針,指向被分配虛擬內存基地址的開始處,它的輸入參數應該為NULL。AllocationSize指向我們要分配的字節數的變量,同樣它也用來接受實際分配的字節數大小。最好把AllocationType在設置成MEM_COMMIT之外再加上MEM_TOP_DOWN因為內存要在接近DLL地址的盡可能高的地址分配。
#define MEM_COMMIT 0x00001000
#define MEM_TOP_DOWN 0x00100000
然后我們就可以通過調用NtWriteVirtualMemory來寫入我們的代碼。
NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
BaseAddress是NtAllocateVirtualMemory返回的地址。Buffer指向我們要寫入的字節,BufferLength是我們要寫入的字節數。
現在我們來掛鉤單個進程。被加載入所有進程的動態鏈接庫只有ntdll.dll。所以我們要檢查被導入進程要掛鉤的函數是否來自ntdll.dll。但是這些來自其它DLL的函數所在的內存可能已經被分配,這時重寫它的代碼會在目標進程里導致錯誤。這就是我們必須去檢查我們要掛鉤的函數來自的動態鏈接庫是否被目標進程加載的原因。
我們需要通過NtQueryInformationProcess獲取目標進程的PEB(進程環境塊)。
NTSTATUS NtQueryInformationProcess(
IN HANDLE ProcessHandle,
IN PROCESSINFOCLASS ProcessInformationClass,
OUT PVOID ProcessInformation,
IN ULONG ProcessInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
我們把ProcessInformationClass設置為ProcessBasicInformation,然后PROCESS_BASIC_INFORMATION結構會返回到ProcessInformation緩沖區中,大小為給定的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的地址。這些通過調用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的地址。它是被加載進進程的動態鏈接庫的列表。我們只對這個結構中的一些部分感興趣。
typedef struct _IN_INITIALIZATION_ORDER_MODULE_LIST {
PVOID Next,
PVOID Prev,
DWORD ImageBase,
DWORD ImageEntry,
DWORD ImageSize,
...
);
Next是指向下一個記錄的指針,Prev指向前一個,最后一個記錄的會指向第一個。ImageBase是內存中模塊的地址,ImageEntry是模快的入口點,ImageSize是它的大小。
對所有我們想要掛鉤的庫我們需要獲得它們的ImageBase(比方調用GetModuleHandle或者LoadLibrary)。然后把這個ImageBase和InInitializationOrderModuleList的ImageBase比較。
現在我們已經為掛鉤準備就緒。因為我們是掛鉤正在運行的進程,所以可能我們正在改寫代碼的同時代碼被執行,這時就會導致錯誤。所以首先我們就得停止目標進程里的所有線程。它的所有線程列表可以通過設置了SystemProcessAndThreadInformation的NtQuerySystemInformation來獲得。有關這個函數的描述參考第4節。但是還得加入SYSTEM_THREADS結構的描述,用來保存線程的信息。
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;
對每個線程調用NtOpenThread獲取它們的句柄,通過使用ClientId。
NTSTATUS NtOpenThread(
OUT PHANDLE ThreadHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
IN PCLIENT_ID ClientId
);
我們需要的句柄被保存在ThreadHandle。我們需要把DesiredAccess設置為THREAD_SUSPEND_RESUME。
#define THREAD_SUSPEND_RESUME 2
ThreadHandle用來調用NtSuspendThread。
NTSTATUS NtSuspendThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
被掛起的進程就可以被改寫了。我們按照"掛鉤Windows API"里3.2.2節里描述的方法處理。唯一的不同是使用其它進程的函數。
掛鉤完后我們就可以調用NtResumeThread恢復所有線程的運行。
NTSTATUS NtResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG PreviousSuspendCount OPTIONAL
);
=====[ 7.3 新進程 ]================================================
感染所有正在運行的進程并不能影響將要被運行的進程。我們可以每隔一定時間獲取一次進程的列表,然后感染新的列表里的進程。但這種方法很不可靠。
更好的方法是掛鉤新進程開始時肯定會調用的函數。因為所有系統中正在運行的進程都已經被掛鉤,所以這種方法不會漏掉任何新的進程。我們可以掛鉤NtCreateThread,但這不是最簡單的方法。我們可以掛鉤NtResumeThread,因為它也是每當新進程創建時被調用,它在NtCreateThread之后被調用。
唯一的問題在于,這個函數并不只在新進程被創建時調用。但我們能很容易解決這點。NtQueryInformationThread能給我們指定線程是屬于哪個進程的信息。最后我們要做的就是檢查進程是否已經被掛鉤了。這通過讀取我們要掛鉤的函數的開始5個字節來完成。
NTSTATUS NtQueryInformationThread(
IN HANDLE ThreadHandle,
IN THREADINFOCLASS ThreadInformationClass,
OUT PVOID ThreadInformation,
IN ULONG ThreadInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ThreadInformationClass是信息分類,在這里它被設置為ThreadBasicInformation。ThreadInformation是保存結果的緩沖區,大小按字節計算為ThreadInformationLength。
#define ThreadBasicInformation 0
對ThreadBasicInformation返回這個結構:
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是線程所屬進程的PID。
現在我們來感染新進程。問題就是新進程的地址空間中只有ntdll.dll,其他的模塊在調用NtResumeThread之后被加載。有幾種方法可以解決這個問題,比方說我們可以掛鉤一個名為LdrInitializeThunk的API函數,它在進程初始化時被調用。
NTSTATUS LdrInitializeThunk(
DWORD Unknown1,
DWORD Unknown2,
DWORD Unknown3
);
首先我們先運行原始的代碼,然后掛鉤新進程里所有要掛鉤的函數。但最好對LdrInitializeThunk解除掛鉤,因為這個函數在之后要被調用很多次,我們并不需要重新再掛鉤所有的函數。這時在程序執行第一個指令前所有工作已經完成。這就是為什么在我們掛鉤它之前它沒有機會調用任何一個被掛鉤過的函數的原因。
對自己掛鉤和動態掛鉤正在運行的進程一樣,只是這里我們不需要關心正在運行的線程。
=====[ 7.4 DLL ]================================================
系統中每個進程都是一份ntdll.dll拷貝。這意味著我們可以在進程初始化階段掛鉤這個模塊里的任意一個函數。但是來自其它模塊比如kernel32.dll或advapi32.dll的函數該怎么辦呢?還有一些進程只有ntdll.dll,其他模塊都是在進程被掛鉤之后在運行過程中才被動態加載的。這就是我們還得掛鉤加載新模塊的函數LdrLoadDll的原因。
NTSTATUS LdrLoadDll(
PWSTR szcwPath,
PDWORD pdwLdrErr,
PUNICODE_STRING pUniModuleName,
PHINSTANCE pResultInstance
);
這里對我們來說最重要的是pUniModuleName,它保存模塊名字。當調用成功后pResultInstance保存模塊地址。
我們首先調用原始的LdrLoadDll然后掛鉤被加載模塊里所有函數。
=====[ 8. 內存 ]===========================================
當我們正在掛鉤一個函數時我們會修改它開始的字節。通過調用NtReadVirtualMemory任何人都可以檢測出函數被掛鉤。所以我們還要掛鉤NtReadVirtualMemory來防止檢測。
NTSTATUS NtReadVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
OUT PVOID Buffer,
IN ULONG BufferLength,
OUT PULONG ReturnLength OPTIONAL
);
我們修改了我們掛鉤的函數開始的字節并且為我們新的代碼分配了內存。我們就需要檢查時候有人讀取了這些代碼。如果我們的代碼出現在BaseAddress到BaseAddress+BufferLength中我們就需要在緩沖區中改變它的一些字節。
如果有人在我們分配的內存中查詢字節我們就返回空的緩沖區和錯誤STATUS_PARTIAL_COPY。這個值用來表示被請求的字節并沒有完全被拷貝到緩沖區中,它也同樣被用在當請求了未分配的內存時。這時ReturnLength應該被設為0。
#define STATUS_PARTIAL_COPY 0x8000000D
如果有人查詢被掛鉤的函數開始的字節我們就調用原始代碼并拷貝原始代碼里開始的那些字節到緩沖區中。
現在新進程已無法通過讀取它的內存來檢測是否被掛鉤了。同樣如果你調試被掛鉤的進程調試器也會用問題,它會顯示原始代碼,但卻執行我們的代碼。
為了使隱藏更完美,我們還要掛鉤NtQueryVirtualMemory。這個函數用來獲取虛擬內存的信息。我們掛鉤它來防止探測我們分配的虛逆內存。
NTSTATUS NtQueryVirtualMemory(
IN HANDLE ProcessHandle,
IN PVOID BaseAddress,
IN MEMORY_INFORMATION_CLASS MemoryInformationClass,
OUT PVOID MemoryInformation,
IN ULONG MemoryInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
MemoryInformationClass標明了返回數據的類別。我們對開始的2種類型感興趣。
#define MemoryBasicInformation 0
#define MemoryWorkingSetList 1
對MemoryBasicInformation返回這個結構:
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;
每個區段都有它的大小RegionSize和它的類型Type。空閑內存的類型是MEM_FREE。(區段對象就是文件映射對象,是可被映射到一個進程的虛逆地址空間的對象)
#define MEM_FREE 0x10000
如果我們代碼之前一個區段的類型是MEM_FREE我們就在它的RegionSize加上我們代碼的區段的大小。如果我們代碼之后的區段的類型也是MEM_FREE那么就在之前區段的RegionSize上再加上之后的空閑區段的大小。
如果我們代碼之前的區段是其它類型,我們就對我們代碼的區段返回MEM_FREE。它的大小根據之后的區段來計算。
對MemoryWorkingSetList返回這個結構:
typedef struct _MEMORY_WORKING_SET_LIST {
ULONG NumberOfPages;
ULONG WorkingSetList[1];
} MEMORY_WORKING_SET_LIST, *PMEMORY_WORKING_SET_LIST;
NumberOfPages是WorkingSetList中列項的數目。這個數字應該減少一些。我們在WorkingSetList中找到我們代碼的區段然后把之后記錄前移。WorkingSetList是按DWORD排列的數組,每個元素的高20位標明了區段地址,低12位是標志。
=====[ 9. 句柄 ]=========================================
用類SystemHandleInformation來調用NtQuerySystemInformation會在_SYSTEM_HANDLE_INFORMATION_EX結構中獲取所有被打開的句柄的數組。
#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標明了擁有句柄的進程。ObjectTypeNumber是句柄類型。NumberOfHandles是Information數組中元素的數量。隱藏其中一項是很麻煩的,我們要去掉所有之后的元素并減少NumberOfHandles。去掉之后所有元素是必須的,因為數組中句柄是按ProcessId分組的。這意味著一個來自同一個進程中的所有句柄都在一塊兒。對于一個進程變量Handle的數量是不斷增加的。
現在回想一下這個函數(NtQuerySystemInformation)使用SystemProcessAndThreadsInformation類來調用時返回的結構_SYSTEM_PROCESSES。這里我們能夠看到每個進程都有它自己的句柄的數量在HandleCount中。如果我們想要做得更完美我們就應該修改HandleCount,因為用SystemProcessesAndThreadsInformation類調用這個函數時隱藏了不少句柄。但校正是非常浪費時間的。在系統正常運行的一小段時間里就會有很多句柄正在打開或關上。所以在對這個函數兩次緊挨著的調用句柄的數量被更改是很正常的,所以我們根本不需要改變HandleCount。
=====[ 9.1 命名句柄并獲取類型 ]===================================
隱藏句柄很麻煩,但找出哪個句柄該被隱藏更困難一些。比方說我們要隱藏一個進程就要隱藏它的所有句柄并隱藏所有和它有聯系的句柄。我們比較句柄的ProcessId參數和想要隱藏的進程的PID,如果它們相等就隱藏這個句柄。但是其它進程的句柄在我們能比較任何東西之前不得不先命名。系統中句柄的數量通常很龐大,所以最好在嘗試命名之前先比較句柄類型。命名類型可以為我們不感興趣的句柄省不少時間。
命名句柄和句柄類型通過調用NtQueryObject來完成。
NTSTATUS ZwQueryObject(
IN HANDLE ObjectHandle,
IN OBJECT_INFORMATION_CLASS ObjectInformationClass,
OUT PVOID ObjectInformation,
IN ULONG ObjectInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
ObjectHandle是我們想要獲取有關信息的句柄,ObjectInformationClass是信息類型,保存在以字節計算長度為ObjectInformationLength的緩沖區ObjectInformation中。
我們對OBJECT_INFORMATION_CLASS使用的類是ObjectNameInformation和ObjectAllTypesInformation。ObjectNameInfromation類在緩沖區中返回OBJECT_NAME_INFORMATION結構,而ObjectAllTypesInformation類返回OBJECT_ALL_TYPES_INFORMATION結構。
#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結構后面。下一個OBJECT_TYPE_INFORMATION結構跟在這個Name后面,間隔4個字節。
SYSTEM_HANDLE_INFORMATION結構中的ObjectTypeNumber是TypeInformation數組中的索引。
比較困難的是獲取其他進程中句柄的名字。這里有兩種命名的可能性。一是通過調用NtDuplicateObject把句柄拷貝到我們的進程中然后命名它。這種方法對某些特殊類型的句柄會失敗。但由于它失敗的次數比較少,所以我們采用這種方法。
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的進程的句柄。TargetProcessHandle是想要拷貝到的進程的句柄,在這里是我們進程的句柄。TargetHandle是指向保存原始句柄拷貝的指針。DesiredAccess應該被設為PROCESS_QUERY_INFORMATION,Attributes和Options設為0。
第二種命名方法對所有句柄都有效,就是使用系統驅動。源代碼可以在http://rootkit.host.sk的OpHandle項目里找到。
=====[ 10. 端口 ]==========================================
枚舉打開端口最簡單的方法是調用AllocateAndGetTcpTableFromStack和AllocateAndGetUdpTableFromStack函數,或者AllocateAndGetTcpExTableFromStack和AllocateAndGetUdpExTableFromStack函數,它們都來自iphlpapi.dll。帶Ex的函數從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;
);
還有另外一種方法。當程序創建了一個套接字并開始監聽時,它就會有一個為它和打開端口的打開句柄。我們在系統中枚舉所有的打開句柄并通過NtDeviceIoControlFile把它們發送到一個特定的緩沖區中,來找出這個句柄是否是一個打開端口的。這樣也能給我們有關端口的信息。因為打開句柄太多了,所以我們只檢測類型是File并且名字是\Device\Tcp或\Device\Udp的。打開端口只有這種類型和名字。
如果你看一下iphlpapi.dll里函數的代碼,就會發現這些函數同樣調用NtDeviceIoControlFile并發送到一個特定緩沖區來獲得系統中所有打開端口的列表。這意味著我們要想隱藏端口只需要掛鉤NtDeviceIoControlFile函數。
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標明了要通信的設備的句柄,IoStatusBlock指向接收最后完成狀態和請求操作信息的變量,IoControlCode是指定要完成的特定的I/O控制操作的數字,InputBuffer包含了輸入的數據,長度為按字節計算的InputBufferLength,相似的還有OutputBuffer和OutputBufferLength。
=====[ 10.1 WinXP下使用Netstat OpPorts FPort ]=========================
在Windoes XP獲得所有打開端口的列表可以使用一些軟件比方OpPorts、FPort和Netstat。
這里程序用IoControlCode0x000120003調用了NtDeviceIoControlFile兩次。輸出緩沖區在第二次調用時被填滿。FileHandle的名字這里總是\Device\Tcp。InputBuffer因不同類型的調用而不同:
1) 為獲得MIB_TCPROW數組InputBuffer看起來是這樣:
第一次調用:
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
第二次調用:
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數組:
第一次調用:
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
第二次調用:
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數組:
第一次調用:
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
第二次調用:
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數組:
第一次調用:
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
第二次調用:
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
你可以看到緩沖區只有少數字節不同。我們現在比較清晰地簡要說明一下:
我們感興趣的調用是InputBuffer[1]為0x04且InputBuffer[17]為0x01。只有使用這些輸入數據才能使OutputBuffer里為我們想要的表。如果我們想要獲得TCP端口信息我們就把InputBuffer[0]設為0x00,想獲得UDP端口信息就把它設為0x01。如果我們還需要額外的輸出表(MIB_TCPROW_EX或MIB_UDPROW_EX)我們就在第二次調用里把InfputBufer[16]設為0x02。
如果我們發現使用了這幾個參數的調用我們就修改輸出緩沖區。獲取輸出緩沖區中ROW的數量可以很容易根據ROW的大小分開IoStatusBlock結構里的Infotmation變量。隱藏其中一個ROW就會變的容易,只需要用之后的ROW改寫他并刪掉最后一個ROW。不要忘了修改OutputBufferLength和IoStatusBlock。
=====[ 10.2 Win2k和NT4下使用OpPorts, Win2k下使用FPort ]==========================
我們用IoControlCode0x00210012調用NtDeviceIoControlFile來判斷這個擁有類型File和名字\Device\Tcp或\Device\Udp是否是打開端口的句柄。
所以最先我們比較IoControlCode然后是類型和句柄名字。如果這些都符合就接著比較輸入緩沖區長度,它應該和結構TDI_CONNECTION_IN長度一樣,為0x18。輸出緩沖區的結構是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上可以找到。我們現在來隱藏指定端口。我們已經比較過了InputBufferLength和IoControlCode,現在來比較RemoteAddressLength,對打開端口來說它總是3或4。最后要做的是比較OutputBufferBuffer里的ReceiveTsdus,用網絡上的端口和要隱藏的端口列表比較。區別TCP和UDP的做法是句柄的名字不一樣。在刪除了OutputBuffer、修改IoStatusBlock并返回STATUS_INVALID_ADDRESS后我們就已經隱藏了這個端口了。
=====[ 11. 結束語 ]===============================================
具體細節請參考Hacker defender rootkit version 1.0.0的源代碼,在http://rootkit.host.sk和http://www.rootkit.com都可以找到。
在將來我還會加入更多有關的技術。這篇文檔的更新版本會改進現有的方法和并加入新的思想。
特別感謝Ratter提供了很多完成這篇文檔和Hacker defender代碼所需要的技術。
===================================[ End ]==============================
后記:
其實只要我們對Windows的內核有一定程度的了解我們都知道單純靠掛鉤函數是不能真正做到隱藏的,這樣做只不過是欺騙操作系統的使用者,卻欺騙不了操作系統自己。線程要想被運行就必須獲得時間片,將自己加入調度鏈表中,從而暴露自己。內核維護一組被稱為調度程序數據庫的數據結構來做出線程調度的決策。其中最重要的結構是調度程序就緒隊列(KiDispatckerReadyListHead)。它里面有64個DWORD,分別對應于32個線程優先級的隊列,隊列包含處于就緒狀態的線程,正在等待調度執行。還有兩個隊列KiWaitInListHead和KiWaitOutListHead保存著處于等待狀態的線程。可以很簡單的枚舉這3個鏈表中的所有元素從而列出系統中的所有線程。因此要想徹底從Windows系統里“消失”就要從Windows的內核下手(Windows的內核只負責線程調度,其它功能由執行程序組件完成)。這個功能還有待完成。
水平有限,歡迎大家指出錯漏之處。QQ:27324838 Email:kinvis@hotmail.com