對內核的直接掛鉤
簡介有時在開發中,會遇到這樣一種情況,當非常需要對某些內核函數進行掛鉤時,而常規基於PE的掛鉤,往往達不到目的。在本文中將要探討的,是怎樣直接掛鉤內核函數,另外,在示例中,還要演示在係統中顯示為一個基本磁盤的可移動USB存儲設備,並在其上創建及管理多個分區(因為這樣或那樣的原因,Windows既不允許,也不能識別可移動存儲設備上的多個分區,所以我們要“欺騙”一下係統)。因為本文中的示例隻用作演示目的,所以隻對一個函數進行了掛鉤,但可對文中闡述的方法進行擴展,以處理多個函數(例如,工程中可能需要直接掛鉤好幾個NDIS庫中的函數)。再者,你應該清楚地認識到,本文是在講述直接掛鉤,而不是研究USB存儲,所以,用作示例的問題當然還可有其他的方法來解決。
我們的問題
USB設備在係統中表示的方式,定義在STORAGE_DEVICE_DESCRIPTOR結構的RemovableMedia字段中,此結構通常會在USBSTOR.SYS響應IOCTL_STORAGE_QUERY_PROPERTY請求時返回。如果設備生產商想讓此設備顯示為一個基本磁盤,會在驅動程序中設置STORAGE_DEVICE_DESCRIPTOR 結構中RemovableMedia字段,並在響應IOCTL_STORAGE_QUERY_PROPERTY請求時返回FALSE。由此,設備在係統中就顯示為一個基本磁盤,而DISK.SYS也不知道它實際上是在與硬盤,還是在與一個USB設備打交道。
因此,如果我們掛鉤USBSTOR.SYS中的IRP_MJ_DEVICE_CONTROL子程序,隻需簡單地修改IOCTL_STORAGE_QUERY_PROPERTY請求的返回值,就能在係統中把可移動磁盤顯示為一個基本磁盤,這可通過以下的代碼來完成:
typedef NTSTATUS (__stdcall*ProxyDispatch) (IN PDEVICE_OBJECT device,IN PIRP Irp);
ProxyDispatch realdispatcher;
//代理函數
NTSTATUS Dispatch(IN PDEVICE_OBJECT device,IN PIRP Irp)
{
NTSTATUS status=0; ULONG a=0;PSTORAGE_PROPERTY_QUERY query;
PSTORAGE_DEVICE_DESCRIPTOR descriptor;
PIO_STACK_LOCATION loc= IoGetCurrentIrpStackLocation(Irp);
if(loc->Parameters.DeviceIoControl.IoControlCode
==IOCTL_STORAGE_QUERY_PROPERTY)
{
query=(PSTORAGE_PROPERTY_QUERY) Irp->AssociatedIrp.SystemBuffer;
if(query->PropertyId==StorageDeviceProperty)
{
descriptor=(PSTORAGE_DEVICE_DESCRIPTOR) Irp->AssociatedIrp.SystemBuffer;
status=realdispatcher(device,Irp);
descriptor->RemovableMedia=FALSE;
return status;
}
}
return realdispatcher(device,Irp);
}
//代碼中的其他地方……
realdispatcher=(ProxyDispatch) driver->MajorFunction[IRP_MJ_DEVICE_CONTROL];
driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]=Dispatch;
正如你所看到的,一個可移動USB設備能非常簡單地在係統中顯示為一個基本磁盤,然而,還有一點小小的“並發症”——隻有當你在USB接口中插入一個設備時,係統才會加載USBSTOR.SYS,直到拔出設備後,才會卸載它,因此,我們不能預先對USBSTOR.SYS進行掛鉤——必須先插入一個設備。如果我們在USBSTOR.SYS已經處理了IOCTL_STORAGE_QUERY_PROPERTY請求之後,才對它進行掛鉤,那麼為時已晚了。我們也不能插入一個設備,掛鉤USBSTOR.SYS,拔掉它,接著再插入;當你拔出設備時,USBSTOR.SYS也卸載了,掛鉤隻會白費力氣。所以,要對USBSTOR.SYS進行掛鉤,最適當的時機是在當它準備創建設備對象時,一方麵,我們知道USBSTOR.SYS已經加載了,另一方麵,此時IOCTL_STORAGE_QUERY_PROPERTY請求還並未被處理。如果我們能設法捕捉到USBSTOR.SYS對IoCreateDevice()的調用,那麼接下來的事情就簡單多了——IoCreateDevice()接受一個指向新創建設備的DRIVER_OBJECT的指針作為參數,因此,我們就可在驅動程序的MajorFunction[IRP_MJ_DEVICE_CONTROL]中替換掉一個指針。
為了達到上述目的,我們準備在IoCreateDevice()的可執行代碼中插入一些指令,以便直接掛鉤,也就是所謂的“通過覆蓋的掛鉤”。事實上,隻有通過掛鉤ntoskrnl.exe的導出索引,才能完成此項任務,但是,本文要講述的是有關直接掛鉤,所以,我們準備對IoCreateDevice()進行直接掛鉤。然而,知己知彼,百戰百勝,先了解一下相關的事情,總是有好處的,那就先來了解一下中斷掛鉤吧。
處理中斷與異常
為響應硬件中斷或異常,CPU保存了當前運行線程的執行上下文,並把執行流程轉到一個特殊的內核模式程序中——稱為“處理程序”。執行上下文保存的方式,依賴於中斷模式的特權級;如果中斷代碼是非特權級的,處理器必須切換到特權堆棧和代碼段,以便可以執行一個內核模式的處理程序,因此,CPU在轉換執行流程到相應的處理程序之前,會把用戶模式的SS、ESP、EFLAGS、CS寄存器值(所有入棧均按上述順序),加上返回地址,壓入到內核堆棧上;另外,如果是發生異常,CPU也可以在棧頂的返回地址上,再壓入一個錯誤代碼。如果中斷代碼是特權級的,堆棧切換就沒有必要了,因此,在這種情況下,隻有EFLAGS、CS和返回地址,也許可能還有錯誤代碼被壓入到堆棧中;此時,SS和ESP寄存器不會保存在堆棧上。
每一個中斷及異常都有著與之關聯的號碼,稱為向量,共有256個中斷向量。所有中斷與異常處理程序的地址,都存儲在一個稱為“中斷描述符表”(IDT)的內核模式的數據結構中。通常,在一台對稱多處理(SMP)計算機上,每個處理器都有其自己的IDT,但在整個係統中,所有中斷與異常處理程序的地址,對所有CPU而言,都是一樣的。每個IDT入口點關聯到它對應的向量,且在每個IDT中,都可以保存中斷門描述符、陷阱門描述符、任務門描述符。中斷與陷阱門描述符的二進製形式,可用如下的結構來表示:
struct GATE
{
WORD OffsetLow;
WORD Selector;
WORD Unused:8;
WORD Type:5;
WORD DPL:2;
WORD Present:1;
WORD OffsetHigh;
} ;
如上所示,中斷與陷阱門描述符的二進製表示形式,與調用門描述符非常相似。而中斷與陷阱門的不同之處,在於當中斷或異常處理程序開始處理時,EFLAGS寄存器中IF標誌的狀態。如果中斷或異常是通過一個中斷門引發的,IF標誌會被處理器自動清除;如果中斷或異常是通過一個陷阱門引發的,則IF標誌不會受到影響。在其他方麵,中斷與陷阱門是一樣的——這也不足為奇,因為它們都是用同樣的結構來描述的,但任務門描述符的二進製形式就不相同了。另外,因為性能的原因,在Windows NT中,所有的用戶過程都運行於一個單任務的上下文中,所以在IDT中,還有一些任務門描述符,它們主要保留用於“異常的情況”,如係統崩潰;它們的任務是保證係統可以有足夠長時間,在CPU重設自身之前,拋出一個藍屏錯誤。
現在,要來說一下異常了,IDT的頭32個入口點負責與異常處理程序打交道(它們對特定向量的映射,已被Intel預先定義好了),異常在此可歸類為陷阱(Trap)、錯誤(Fault)與異常終止(Abort)。異常終止類的異常不允許失敗的任務繼續執行下去,有關的一個典型例子就是機器檢查異常(INT 0x12);而陷阱與錯誤則允許失敗的任務在異常被處理之後,繼續執行下去。陷阱與錯誤的不同之處,在於保存在堆棧上的返回地址不同;在錯誤類的異常情況下,這個地址指向導致異常的指令,也就是說,在異常處理程序返回控製之後,會試圖執行前麵失敗的指令,有關的一個典型例子就是頁麵錯誤異常(INT 0xE);而在陷阱類的異常情況下,返回地址將指向緊跟在導致異常指令後的下一條指令,有關的典型例子如調試斷點異常(INT 3)。
一個調試異常(INT 1)就本身而言,是個非常有意思的異常——依據不同的異常原因,它可以被陷阱或錯誤異常拋出。通常,一個調試異常可被以下任一原因拋出:
Ø 執行時的斷點
Ø 內存訪問的斷點
Ø IO端口訪問的斷點
Ø 一般偵測情況(會設置EFLAGS寄存器的TF標誌,甚至於每條指令的執行,都可以拋出一個調試異常)
Ø 任務切換(此處與Windows的任務切換無關)
Ø INT 1指令
在1至4的情況中,INT 1是作為一個錯誤被拋出,而在其他情況中,它是作為一個陷阱被拋出,而一般可通過來自DR6寄存器的INT 1處理程序,來找出拋出異常的原因。一個調試異常能由多個原因產生,例如,設置了TF標誌的執行斷點,在這種情況下,執行斷點比TF標誌具有更高的優先級,因此,INT 1是作為一個錯誤拋出,而不是作為一個陷阱。
那麼,有了掛鉤函數之後,上麵這些東西都能做些什麼呢?我們將要把目標函數開始處的頭幾個字節(8個字節就足夠了),複製到從非分頁池裏分配的數組中,再掛鉤INT 1與INT 3的處理程序,並寫入一個0xCC操作碼(其代表INT 3指令)至目標函數的開始處。這樣,當目標函數準備執行它的第一條指令時,就會觸發我們被代理過的INT 3處理程序,而我們INT 3處理程序開始執行時的堆棧布局,可用下麵的結構來描述:
struct INTTERUPT_STACK
{
ULONG InterruptReturnAddress;
ULONG SavedCS;
ULONG SavedFlags;
ULONG FunctionReturnAddress;
ULONG Argument;
};
在堆棧頂部,CPU設置了一個幀,以用於響應一個INT 3指令,也就是一個INT 3處理程序應該返回控製,加上CS及EFLAGS寄存器標誌的地址值;而目標函數應該返回控製的地址緊接其後;另外,函數參數的數組在堆棧上,正位於返回地址之下(所以從實踐經驗來說,把所有的參數當作ULONG,還是有道理的,這樣我們就能
在需要時把它們轉換成它們實際的類型)。在這一點上,我們就能做任何想做的事了——我們可以檢查或修改函數參數、修改返回地址,也就是那些通常在掛鉤函數之後可以做的事情。但對我們目前的任務來說,我們隻對第一個參數感興趣,也就是傳遞給IoCreateDevice()的PDRIVER_OBJECT。
在被我們代理的INT 3處理程序返回之前,它將會把棧頂結構中的InterruptReturnAddress字段,修改為我們複製的帶有指令的數組,並設置SaveFlags字段中的TF標誌。我們的INT 3處理程序返回之後,保存在堆棧上的InterruptReturnAddress和SavedFlags字段,將會分別彈出至EIP與EFLAGS寄存器中。由此,執行流程將會從我們複製的指令數組處繼續執行,而且,我們一旦修改了TF標誌,它將會以單步模式繼續下去,也就是說,在每條指令執行時,都會拋出INT
1。
如果INT 1的拋出,是因為設置了TF標誌,那它將會被當作一個陷阱來處理。因此,在數組中第一條指令執行之後,就會觸發我們代理過的INT 1處理程序,而保存在堆棧上的EIP將會指向數組中的第二條指令。這樣,從保存在棧頂的返回地址中,減去我們數組的地址,就可以得到執行過的指令大小,因此,在我們的INT 1處理程序返回前,它將會修改返回地址為目標函數起始地址(+)執行過的指令大小,並清除保存在堆棧上的EFLAGS中的TF標誌。由此,執行流程將會從目標函數的第二條指令處開始繼續,而我們的INT 1處理程序返回之後,TF標誌也被清除了。換句話來說,目標函數將會繼續執行下去,好像什麼事也沒有發生過一樣。
明顯地,我們的方法似乎有點複雜了,讓人難以理解,但實際上,我們隻不過換了種方式來做而已。例如,我們可以複製目標函數起始處的一些指令到我們的數組中,並通過一個JMP指令覆蓋掉目標函數的起始地址,這樣,執行程序就能跳到我們的掛鉤代碼中來了。如果這樣做的話,我們還要計算出目標函數內的偏移量,以確定我們的掛鉤代碼執行完後,從目標函數哪條指令開始恢複執行,所以,就還要算出指令大小。可是,說起來容易,做起來難啊,要像上述這樣來做,將必須寫一個完整的反匯編程序,而且,複雜的事還在後麵,指令還可能涉及到與特定指令位置相關的內存,這種情況下,我們必須在重定位之後,調整指令的操作數。換句話來說,如果我們選擇把函數開始處覆寫為一個JMP,而不是INT
3指令,我們的程序將會非常大,95%的代碼都要用於處理反匯編,而不是掛鉤本身。因此,對INT 1與INT 3進行掛鉤,是更加合情合理的事情,隻要利用好INT 1與INT 3,想要CPU做什麼,都不是問題了。
現在,來看一下實際的工作。
解決我們的問題
針對我們特定的工程,可在DriverEntry()中進行所有與掛鉤相關的工作,下麵來看一下代碼:
//這個子程序掛鉤並恢複IDT,
//必須保證這個函數隻運行在一個CPU上,
//因此我們在整個執行過程中屏蔽了中斷以避免上下文切換。
void HookIDT()
{
ULONG handler1,handler2,idtbase,tempidt,a;
UCHAR idtr[8];
//取得地址以便寫入到IDT
handler1=(ULONG)&replacementbuff[0];
handler2=(ULONG)&replacementbuff[32];
//分配臨時的內存,這應該為我們的第一步,從此時開始,我們屏蔽了中斷直到返回,
//我們不想冒險調用任何不是我們自己編寫的代碼。
//(理論上來說,這個代碼可能會在我們未知的情況下重新打開中斷,那可就……)
tempidt=(ULONG)ExAllocatePool(NonPagedPool,2048);
_asm
{
cli
sidt idtr
lea ebx,idtr
mov eax,dword ptr[ebx+2]
mov idtbase,eax
}
//檢查是否已掛鉤IDT,
//如果是,重新打開中斷並返回。
for(a=0;a<IdtsHooked;a++)
{
if(idtbases[a]==idtbase)
{
_asm sti
ExFreePool((void*)tempidt);
KeSetEvent(&event,0,0);
PsTerminateSystemThread(0);
}
}
_asm
{
//現在,將要加載IDT的副本到IDTR寄存器。
//以個人的經驗來看,修改內存,再由IDTR寄存器進行指向,是不安全的。
mov edi,tempidt
mov esi,idtbase
mov ecx,2048
rep movs
lea ebx,idtr
mov eax,tempidt
mov dword ptr[ebx+2],eax
lidt idtr
//現在,我們能安全地修改IDT了,準備好。
mov ecx,idtbase
//掛鉤INT 1
add ecx,8
mov ebx,handler1
mov word ptr[ecx],bx
shr ebx,16
mov word ptr[ecx+6],bx
//掛鉤INT 3
add ecx,16
mov ebx,handler2
mov word ptr[ecx],bx
shr ebx,16
mov word ptr[ecx+6],bx
//重新加載原始IDT
lea ebx,idtr
mov eax,idtbase
mov dword ptr[ebx+2],eax
lidt idtr
sti
}
//添加我們剛才掛鉤的IDT地址至已掛鉤的IDT列表
idtbases[IdtsHooked]=idtbase;
IdtsHooked++;
ExFreePool((void*)tempidt);
KeSetEvent(&event,0,0);
PsTerminateSystemThread(0);
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT driver,IN PUNICODE_STRING path)
{
ULONG a;PUCHAR pool=0;
UCHAR idtr[8];HANDLE threadhandle=0;
//以機器碼填充數組
replacementbuff[0]=255;replacementbuff[1]=37;
a=(long)&replacementbuff[6];
memmove(&replacementbuff[2],&a,4);
a=(long)&INT1Proxy;
memmove(&replacementbuff[6],&a,4);
replacementbuff[32]=255;replacementbuff[33]=37;
a=(long)&replacementbuff[38];
memmove(&replacementbuff[34],&a,4);
a=(long)&BPXProxy;
memmove(&replacementbuff[38],&a,4);
//保存INT 1與INT 3處理程序的原始地址
_asm
{
sidt idtr
lea ebx,idtr
mov ecx,dword ptr[ebx+2]
//保存INT1
add ecx,8
mov ebx,0
mov bx,word ptr[ecx+6]
shl ebx,16
mov bx,word ptr[ecx]
mov Int1RealHandler,ebx
//保存INT3
add ecx,16
mov ebx,0
mov bx,word ptr[ecx+6]
shl ebx,16
mov bx,word ptr[ecx]
mov BPXRealHandler,ebx
}
//掛鉤INT 1與INT 3的處理程序,必須在覆寫NDIS之前完成。
//把HookUnhookIDT()作為一個單獨的線程運行,直到所有的IDT都進行了掛鉤。
KeInitializeEvent(&event,SynchronizationEvent,0);
RtlZeroMemory(&idtbases[0],64);
a=KeNumberProcessors[0];
while(1)
{
PsCreateSystemThread(&threadhandle,
(ACCESS_MASK) 0L,0,0,0,
(PKSTART_ROUTINE)HookIDT,0);
KeWaitForSingleObject(&event,
Executive,KernelMode,0,0);
if(IdtsHooked==a)
break;
}
KeSetEvent(&event,0,0);
//填充結構
a=(ULONG)&IoCreateDevice;
HookedFunctionDescriptor.RealCode=a;
pool=ExAllocatePool(NonPagedPool,8);
memmove(pool,a,8);
HookedFunctionDescriptor.ProxyCode=(ULONG)pool;
//現在進行覆寫內存
_asm
{
//在覆寫之前去掉保護
mov eax,cr0
push eax
and eax,0xfffeffff
mov cr0,eax
//插入斷點(0xCC操作碼)
mov ebx,a
mov al,0xcc
mov byte ptr[ebx],al
//恢複保護
pop eax
mov cr0,eax
}
return 0;
}
讓我們先來解釋一下上述動作,一開始,我們用非直接跳轉指令,填充了兩個內存塊——在掛鉤IDT之後將會用到。但有些東西似乎從邏輯上解釋不了,當試圖寫入函數地址本身到IDT中時,總會產生藍屏,然而,如果寫入帶有非直接跳轉指令的數組地址到IDT中時,也就是說,使執行流程跳到我們的函數中,就一切正常,真是讓人不解啊。接下來,把INT 1與INT 3實際處理程序的地址保存在全局變量中,再對IDT進行掛鉤,此處需格外小心。
正如前麵所說過的,在一部SMP電腦上,每個處理器都有其自己的IDT,但隨著Intel超線程技術的出現,一個支持超線程技術的CPU,會被係統當作兩個獨立的CPU,因此,不得不對係統中的所有IDT進行掛鉤,所以要創建運行HookIDT()的線程,直到係統中所有IDT都被掛鉤了。
一開始,HookIDT()分配了內存,以便複製IDT的內容——但就個人經驗來看,寫入內存,再由IDTR寄存器進行指向,是不安全的,即使中斷已被屏蔽。因此,我們複製IDT到分配的內存中,並使用LIDT指令,加載一個指向此內存的指針到IDTR寄存器中,這樣,我們就能安全地修改原始IDT;完成之後,會用原始IDT地址來重新加載IDTR。從HookIDT()發現IDT還未被掛鉤,到修改並重新加載IDT,它都運行在同一個CPU上,所以我們就可以屏蔽中斷,以避免上下文切換。然而,所有的工作,都隻應在為臨時IDT分配內存之後進行,為什麼呢?因為,在我們這個例子中,調用任何不是我們自己編寫的代碼,都是不明智的行為——如果這些代碼重新打開中斷,很可能會把我們攪得一團糟。因此,我們要避免調用任何不是我們自己編寫的代碼——正如大家所看到的,甚至我們在分配用於複製原始IDT內容的內存時,都用的是REP
MOVS指令,而不是常用的memcpy()。
在對IDT中的INT 1與INT 3處理程序進行掛鉤之後,我們把目標函數(即IoCreateDevice())的頭八個字節,複製到我們從非分頁池中分配的內存中,並在目標函數的起始處插入0xCC操作碼。在此目標函數的可執行代碼存放於隻讀內存中,因此,在我們可覆寫函數之前,要麼在頁表中修改頁麵保護,要麼清除CR0寄存器中的WP標誌(此處為簡單起見,我們選擇清除WP標誌)。以上操作完成之後,當每次有對IoCreateDevice()的調用發生時,我們掛鉤於INT 3的代碼就會執行了。
現在,讓我們來看一下掛鉤INT 1與INT 3的代碼。
//此函數保證我們的掛鉤工作正常
ULONG __stdcall INT1check(INTTERUPT_STACK * savedstack)
{
ULONG offset=0,stepping=savedstack->SavedFlags&0x100;
//如果INT 1是因為單步之外的其他原因被拋出,返回0。
//因為執行流程最終仍會到達真正的INT 1處理程序。
if(!stepping)return 0;
//檢查單步是否與我們的掛鉤有關,否則,返回0。
if(savedstack->InterruptReturnAddress<=
HookedFunctionDescriptor.ProxyCode)
return 0;
if(savedstack->InterruptReturnAddress>=
HookedFunctionDescriptor.ProxyCode+8)
return 0;
//在堆棧上修改返回地址,清除TF標誌。
offset=savedstack->InterruptReturnAddress-
HookedFunctionDescriptor.ProxyCode;
savedstack->InterruptReturnAddress=
HookedFunctionDescriptor.RealCode+offset;
savedstack->SavedFlags &=0xfffffeff;
//清除DR6
_asm
{
mov eax,0
mov dr6,eax
}
return 1;
}
ULONG __stdcall BPXcheck(INTTERUPT_STACK * savedstack)
{
PDRIVER_OBJECT driver;char buff[1024]; HANDLE handle=0;
PUNICODE_STRING unistr=(PUNICODE_STRING)&buff[0];ULONG a=0;
//如果斷點與我們的掛鉤無關,返回0。
if(savedstack->InterruptReturnAddress!= HookedFunctionDescriptor.RealCode+1)
return 0;
//使INT 1返回到我們複製的代碼,並設置TF標誌。
savedstack->SavedFlags|=0x100;
savedstack->InterruptReturnAddress=
HookedFunctionDescriptor.ProxyCode;
//所有x86相關的工作都已完成,
//現在來進行實際的工作。
driver=(PDRIVER_OBJECT)savedstack->Arg;
if(ObOpenObjectByPointer(driver,0, NULL, 0,
0,KernelMode,&handle))return 1;
ZwQueryObject(handle,1,buff,256,&a);
if(!unistr->Buffer){ZwClose(handle);return 1;}
if(_wcsicmp(unistr->Buffer,L"\\Driver\\USBSTOR"))
{ZwClose(handle);return 1;}
ZwClose(handle);
a=(ULONG)driver->MajorFunction[IRP_MJ_DEVICE_CONTROL];
if(a==(ULONG)Dispatch)return 1;
realdispatcher=(ProxyDispatch)a;
driver->MajorFunction[IRP_MJ_DEVICE_CONTROL]=Dispatch;
return 1;
}
_declspec(naked) INT1Proxy()
{
_asm
{
pushfd
pushad
mov ebx,esp
add ebx,36
push ebx
call INT1check
cmp eax,0
je fin
popad
popfd
iretd
fin: popad
popfd
jmp Int1RealHandler
}
}
_declspec(naked) BPXProxy()
{
_asm
{
pushfd
pushad
mov ebx,esp
add ebx,36
push ebx
call BPXcheck
cmp eax,0
je fin
popad
popfd
iretd
fin: popad
popfd
jmp BPXRealHandler
}
}
當有一個對IoCreateDevice()的調用發生時,會觸發BPXProxy()函數。函數BPXProxy()保存了寄存器與標誌值,並在開始執行時把ESP值壓入棧,接著調用BpxCheck(),因此,BpxCheck()收到一個指向我們前麵所提過的INTTERUPT_STACK結構的指針作為參數。首先,通過把結構的InterruptReturnAddress與目標函數的地址進行對比,BpxCheck()將會檢查INT 3的調用,是否與我們的掛鉤有關;如果不是,它返回0;否則,它把InterruptReturnAddress修改為我們複製過去的帶有指令的數組,並設置SavedFlags字段中的TF標誌。至此,我們就可以做與掛鉤相關的工作了,在我們的例子中,將檢查傳遞給IoCreateDevice()的PDEVICE_OBJECT是否為\\Driver\\USBSTOR(其意味著USBSTOR.SYS已經加載)的其中一個,並把IRP_MJ_DEVICE_CONTROL處理程序替換為我們函數的地址——當然,是在它還未被替換時。現在,我們已可以監視由係統發送給USBSTOR的所有IRP_MJ_DEVICE_CONTROL請求了,也即完成了我們的最初目標。在BpxCheck()返回之後,中斷的處理方式依賴於它的返回值,如果它回返0,我們把控製傳給INT
3真正的處理程序,否則,我們僅僅帶著IRETD指令返回,因此,執行流程將會從帶有指令數組的開始處恢複執行。一旦我們修改了TF標誌,它將會以單步模式恢複執行,也就是說,INT1Proxy()得到了調用。
有關INT1Proxy()的實現,幾乎與BPXProxy()一樣,唯一的不同之處,是它調用了INT1Check(),而不是BpxCheck()。首先,INT1Check()檢查保存在堆棧上的EFLAGS寄存器中的TF標誌,如果它發現INT 1是因為單步之外的其他原因被拋出的,它將返回0(因為前麵也提到,INT 1可由多種原因拋出);否則,它將檢查返回地址是否位於我們複製的指令數組中某處,如果也不是,還是返回0——畢竟,其他程序在調試時,也會打開TF標誌;如果是,從堆棧上的返回地址中減去數組中地址,就得到了目標函數的第一條指令大小(也就是剛執行過的那條指令),緊接著修改堆棧上的返回地址為目標函數起始地址(+)它的第一條指令大小,清除保存在堆棧上的DR6寄存器和EFLAGS中的TF標誌,並返回1。這樣一來,如果INT
1是因為其他原因拋出的,那麼與我們的掛鉤無關,INT1Proxy()會將控製傳到INT 1真正的處理程序中,否則,它帶著IRETD指令返回,所以,目標函數(IoCreateDevice())將會繼續執行,好像什麼事也沒發生過一樣。
要運行示例的代碼,你必須創建一個按需啟動的服務,並在命令行中手工啟動它。當你在這個服務運行期間插入一個USB存儲設備時,你將看到一個基本磁盤標誌,而不是一個可移動磁盤,因此,如果打開控製麵板中的磁盤管理,將可以在其上創建多個分區了。
注意:此示例程序使用了Windows 2000 DDK來構建,因此,它會把KeNumberProcessors導出符號當作一個指針;如果你在使用XP DDK,KeNumberProcessors會被當作一個變量,這樣,示例程序就通不過編譯了。然而,這些問題隻存在於編譯期間,示例程序在Windows 2000與Windows XP上都工作正常,而不管你用的是什麼DDK版本。
結論
盡管在我們的例子中,隻掛鉤了一個函數,但可擴展這種方法以用於處理多個函數,此外,我們也作了一個大膽的假設——目標函數的首指令不為JMP。但在實際應用中,還是覺得有必要檢查一下——如果目標函數首指令剛好為JMP呢,以對代碼作出調整(所做的隻是計算出將要跳轉到的指令位置,並在此進行掛鉤),換句話來說,你可以按自己的想法對示例代碼進行調整,以滿足現實工作中工程的特定需要。
最後更新:2017-04-03 15:21:56
上一篇:
zoj 長沙 Bizarre Routine 模擬
下一篇:
開博首記
可滑動的係統狀態欄控製麵板(wifi,bluetooth,數據通信,聲音,自動旋轉)
ASP.NET Core中的依賴注入(5): ServiceProvider實現揭秘 【總體設計 】
政府安全資訊精選 2017年第十三期 網信辦發布《互聯網新聞信息服務新技術新應用安全評估管理規定》;Facebook頒布新廣告政策,加強內容安全
resin服務器支持SSI相關配置
cygwin完整版下載地址
Greenplum 激活standby master失敗後的異常修複
Oracle中的number類型
[原創]systemtap腳本分析係統中dentry SLAB占用過高問題
vb6截取控製台命令執行結果的代碼
想去學習千鋒PHP,PHP究竟有哪些優點?