解析Windows2000的IDT擴充機制
Author: Brief
E-Mail:
Brief@fz5fz.org
HomePage: http://www.fz5fz.org && http://www.safechina.net
Date:
08-05-2003
前言
今天我們談談Windows
2000下中斷機制的擴充,首先申明本文提到的技術並非本人發現的,只不過是我在學習Windows核心過程中的一點心得罷了,目的在於為和我一樣剛剛步入Windows底層學習的朋友提供一點實用的資料,同時也順帶記錄下自己的學習過程。如果您是Windows
Kernel高手,還望有時間能多多指點一下我們這些晚輩;如果您也是初學者,同樣歡迎到我們FZ5FZ網站來交流探討!那好吧,我們就直接進入正題,如果您對中斷還不怎麼瞭解,那眼前將是一次激動人心的旅程。
1>
Windows陷阱機制簡介
陷阱(Trap)是Windows系統中一種不可缺少的系統機制。當系統中發生中斷(硬體中斷或軟體中斷),異常時,處理器會捕捉這個動作,並將系統的控制轉移到一個固定的處理常式處,進行相應的操作處理。在處理器開始處理髮生的中斷或異常前,必須儲存一些處理器環境參數到堆棧中以備系統還原時使用。系統是通過一種稱為陷阱幀(Trap
Frame)的方式來實現的,它將系統中全部線程的環境資料儲存到核心堆棧(Kernel
Stack)中,在執行完後通過堆棧的出棧機制來恢複系統控制流程程中的執行點。核心中的陷阱機制分為中斷和異常。中斷是系統中隨即發生的非同步事件,與當前系統的處理器狀態無關。同時系統中的中斷可分為可屏蔽中斷和不可屏蔽中斷。而異常則是一種同步事件,在特定情況下異常可以重現,而中斷不可以。中斷又可以分為硬體中斷和軟體中斷。很明顯硬體中斷是與硬體相關的,比如I/O裝置執行的某些操作,處理器時鐘或硬體連接埠上的處理等。軟體中斷則是通過中斷指令int
xx引入的,它往往是應用程式在使用者模式執行後進入作業系統的代碼,這時系統為使用者提供了各種各樣的系統服務。比如我們上次提到的系統服務調用(System
Service Call),在Windows NT/2000下就是通過軟體中斷int 0x2e(System Service
Interrupt)來實現的,雖然在Windows
XP/2003下微軟使用了一種稱為“快速系統調用介面”來為使用者提供系統服務,不過大量的中斷服務仍然存在與系統之中的。
2>
中斷處理及其相關流程
此處我們討論的是與特定處理器相關的資料結構,所以會有一些移植方面的問題,本文僅針對Intel的x86
Family處理器,並且本文附帶的程式也只支援在Intel x86處理器上正常執行。何為IDT?IDT(Interrupt Descriptor
Table)稱為中斷描述符表。它是可容納8192個單元的數組,數組中的每個成員是稱之為“門”的長度為8位元組的段描述符。在IDT中門可分為三種:中斷門(Interrrupt
Gate),陷阱門(Trap Gate)和任務門(Task
Gate),但主要的是中斷門和陷阱門。而它們兩者之間也只有少許差別,我們在此只關心IDT中的中斷門,如果您對這方面比較感興趣,請查閱Intel處理器的相關文檔《Intel
Architecture Software Developer's Manual,Volume
3》。同時,在系統中存在一個中斷描述符表寄存器(IDTR),它包含了系統中斷描述符表的基地址和IDT的限制資訊,它於一條彙編指令sidt息息相關。在下文中我們將看到它是我們實現各種中斷描述符表擴充的基礎和關鍵!還有一點是需要注意的,在Windows系統中引入了分頁,分段和虛擬儲存機制後,就存在這一種調度機制,將需要執行的代碼和資料調入記憶體,將不需要的資料調到外存(輔助儲存空間,如硬碟等)。如果我們在執行某些代碼時發現了我們需要的資料不在記憶體中時,就會發出一個“缺頁中斷”,這時系統就會在IDT中搜尋這個中斷的ISR(Interrupt
Service
Routine,插斷服務常式),執行相應的調入工作。大家可以想象如果我們的中斷描述符表被調出到外存後會是什麼樣的結果?那時系統將無法定位“缺頁中斷”的服務常式,至此系統將會崩潰掉!
在中斷描述符表中,我們剛才提到了一個感興趣的寄存器IDTR,當然我們更關心對我們來說更直接的資料:IDT中的程式碼片段選取器(Code
Segment Selector),中斷執行代碼的位移量(Offset)和中斷描述符的許可權等級(Descriptor Privilege
Level)參數。下面我們看看中斷指令的執行流程,我們應該知道應用程式執行在使用者模式(Ring 3)下,而中斷描述符表則是存在於核心模式(Ring
0)才可以訪問的系統地址空間內的。在軟體中斷髮生後,也就是應用程式調用了某條軟體中斷指令後,處理器首先在IDT中檢索傳入的中斷號參數,找到響應的入口單元後就檢查中斷門的許可權等級參數,看是否允許Ring
3下的應用程式調用,這樣作業系統就為我們保留了對軟體中斷調用控制的權力,然而硬體中斷和異常是不會關注許可權方面的資訊。如果當前許可權等級(Current
Privilge Level,CPL)數值大於中斷門描述符需要的許可權(Descriptor Privilege
Level),也就是許可權不夠時會引發一個通用保護故障(General Protection
Fault),反之則進行處理器的切換從使用者堆棧到核心堆棧。現在是儲存線程環境的時候了,處理器將在使用者模式下的堆棧指標(SS:ESP)和標準的中斷幀(EFLAGS和CS:EIP)壓入堆棧。之後處理器進入我們的插斷服務常式,執行相關的代碼處理後通過彙編指令iretd返回到調用的應用程式。在指令iretd執行時,系統將儲存在堆棧中的線程環境資料出棧還原,待系統復原中斷指令執行前的環境後就接著執行應用程式的後續代碼。
3>
中斷相關資料結構
首先我們介紹一下前面我們提到的一條關鍵彙編指令sidt的相關資料結構。在執行指令sidt後,系統將會把中斷描述符表的基地址和限制(總共長六位元組)儲存在指令中指向的變數指標中,這就是我們進行IDT操作的入門口。
typedef
struct
_idtr
{
//定義中斷描述符表的限制,長度兩位元組;
short IDTLimit;
//定義中斷描述服表的基址,長度四位元組;
unsigned
int IDTBase;
}IDTR,*PIDTR;
當我們獲得了IDT的入口後,就會在中斷描述符表中檢索我們需要處理的中斷號對應的IDT單元,單元中包含了很多我們需要注意的資料結構,其中我們最為關心的是程式碼片段選取器,中斷代碼執行的位移量和特權等級等,那好我們先給出它的定義,在下文中我們將詳細討論它們的具體應用。
typedef
struct _idtentry
{
//中斷執行代碼位移量的底16位;
unsigned
short OffsetLow;
//選取器,也就是寄存器;
unsigned
short Selector;
//保留位,始終為零;
unsigned
char Reserved;
//IDT中的門的類型:包括中斷門,陷阱門和任務門;
unsigned
char Type:4;
//段標識位;
unsigned
char SegmentFlag:1;
//中斷門的許可權等級,0表示核心級,3表示使用者級;
unsigned
char DPL:2;
//呈現標誌位;
unsigned
char Present:1;
//中斷執行代碼位移量的高16位;
unsigned
short OffsetHigh;
}IDTENTRY,*PIDTENTRY;
4>
建立軟體中斷鉤子的作用
作為普通的Windows程式員,或許您需要的是熟悉對系統準系統的操作,以及對通用程式開發的熟練掌握。但對於一個有想法的Windows核心級分析開發人員來說,對系統底層的深入瞭解是非常必要的,同時也是非常重要的。Hook為我們創造了一個絕好的機會,它使我們瞭解系統內部運行機制的想法成為了一種可能。同時,書寫一個系統相關的監視程式可以自動的對系統內部操作進行記錄與分析。當然我們不能局限於對系統的瞭解,我們更渴望實施對系統的修改與擴充,改變系統原有的操作特性,注入我們需要的功能組件,讓系統做更適合我們自己,也是我們最希望看到的操作。前面我們曾經談到了建立系統服務調用的鉤子來截獲系統服務調用,同樣在Windows2000下,系統服務是通過系統服務中斷(System
Service Interrupt,int
0x2e)來實現的,通過截獲軟體中斷同樣可以達到監視並修改系統服務調用的功能。在此我們主要討論的是為軟體中斷建立鉤子,不過對於硬體中斷和異常也同樣不例外,我們同樣可以將本文提到的方法應用於硬體中斷和異常。比如我們也可以通過截獲鍵盤驅動的中斷調用來書寫核心級的鍵盤記錄器,它可以直接對每次擊鍵和釋放進行操作,效果是非常的明顯,不過這還需要使用到一些微軟為我們提供的與硬體中斷鉤子相關的函數。
5>
如何建立軟體中斷鉤子?
其實建立軟體中斷鉤子的過程應該是比較明顯了,下面我們將先簡要介紹一下建立Hook的過程,然後以實際代碼進行具體的講解。首先我們通過彙編指令sidt(sidt:
Store Interrupt Descriptor Table Register;lidt: Load Interrupt Descriptor Table
Register)來擷取IDT的基地址IDTBase,然後我們在中斷描述符表中搜尋我們需要HOOK的中斷號HOOKINTID,它應該是在0-255內的一個整數,雖然最新的Intel處理器聲稱支援8192個中斷描述符單元,但由於某些限制原因,仍然只能處理前256個中斷描述門。在找到我們需要Hook的中斷描述門後,將它原本的中斷執行代碼位移量(32位)儲存到一個全域變數OldISR中,以備我們在執行中斷處理或恢複IDT時使用。這樣新的IDT中對應中斷號的執行代碼位移量就指向了我們自己的處理代碼了。在我們的處理代碼NewISR中,注意先要儲存一些線程環境,在處理完我們額外添加的執行程式(Monitor,監視註冊表相關的16個系統服務調用)後,恢複現場並執行中斷門以前指向的程式碼。這樣,對外就看不出我們對中斷門做了什麼額外的處理,感覺和以前沒什麼兩樣!如果我們只是處理了我們添加的代碼而沒有繼續執行中斷門對應的以前的程式碼,那麼系統必將混亂甚至崩潰!同樣在我們卸載我們的軟體中斷鉤子時,就是進行了一個逆向工作。先擷取IDT的基地址,然後將儲存在全域變數中的舊的執行代碼地址位移量賦給對應中斷號的位移量單元(OffsetLow/OffsetHigh)。大概過程講得差不多了,相關程式為T-HookInt,我們再看看代碼吧!
VOID
HookInt(VOID)
{
//儲存IDT入口的基地址和限制資訊的資料結構;
IDTR idtr;
//記錄IDT數組的指標,通過它可以尋找到我們需要Hook中斷號對應的中斷門;
PIDTENTRY IdtEntry;
//彙編指令sidt,擷取IDT入口資訊;
__asm
sidt idtr;
//賦予IDT基地址值;
IdtEntry =
(PIDTENTRY)idtr.IDTBase;
//儲存中斷號HOOKINTID對應中斷門所指向的執行代碼位移量,以備執行中斷處理或恢複時使用;
OldISR
= ((unsigned int)IdtEntry[HOOKINTID].OffsetHigh << 16) |
(IdtEntry[HOOKINTID].OffsetLow);
//關中斷
__asm
cli
//更新執行代碼位移量的底16位;
IdtEntry[HOOKINTID].OffsetLow = (unsigned
short)NewISR;
//更新執行代碼位移量的高16位;
IdtEntry[HOOKINTID].OffsetHigh =
(unsigned short)((unsigned int)NewISR >> 16);
//開中斷
__asm
sti;
}
VOID
UnhookInt(VOID)
{
IDTR idtr;
PIDTENTRY IdtEntry;
__asm
sidt idtr;
IdtEntry = (PIDTENTRY)idtr.IDTBase;
__asm
cli
//恢複中斷號HOOKINTID對應中斷門執行代碼位移量的底16位;
IdtEntry[HOOKINTID].OffsetLow =
(unsigned
short)OldISR;
//恢複中斷號HOOKINTID對應中斷門執行代碼位移量的高16位;
IdtEntry[HOOKINTID].OffsetHigh
= (unsigned short)((unsigned int)OldISR >> 16);
__asm
sti;
}
VOID
__fastcall
Monitor()
{
……
//由於我們處理的中斷號為0x2e,
//對應於系統服務中斷(System
Service Interrupt),
//通過擷取eax寄存器中的數值來區分系統服務調用;
__asm mov
dwServiceId,eax;
//執行核心功能擷取當前進程的ID號;
dwProcessId = (unsigned
int)PsGetCurrentProcessId();
//提升當前IRQL,防止被中斷;
KeRaiseIrql(HIGH_LEVEL,&OldIrql);
switch(dwServiceId)
{
//如果eax對應的數值為0x23,
//則對應於Windows2000的ZwCreateKey系統服務調用;
case
0x23:
DbgPrint("ProcessId: %d
ZwCreateKey/n",dwProcessId);
break;
……
default:
break;
}
//恢複原始IRQL;
KeLowerIrql(OldIrql);
}
6>
添加軟體中斷的作用與原理
通過添加軟體中斷,我們可以擴充系統的功能,改變系統的很多操作行為。在前面我們介紹過為系統添加新的系統服務調用來擴充系統,通過添加新的軟體中斷同樣可以到達添加系統服務調用的目的,並且我們可以在新添的中斷處理常式中執行Ring
0層級的任意代碼,那是何等的讓人欣慰!
其實在IDT中,256個中斷門單元並不是被完全利用的,還剩下一些流給將來擴充使用的中斷門,我們可以自己給這些未使用的中斷門添加一些機製為我所用。其實添加軟體中斷的過程和前面我們詳細講解的添加軟體中斷鉤子有很多相似的地方,所以在此我就不做很詳細的介紹了。同樣是,首先獲得IDT的基地址,然後在中斷描述符表中尋找我們將要添加的中斷號對應的中斷門描述符,之後給相關的參數賦值,使其成為名副其實的軟體中斷門。這時我們就可以在應用程式中使用中斷指令int
xx來調用我們自己中斷門中的服務程式了。
7> 添加軟體中斷的實現過程
相關程式為T-ADDIG(Add
Interrupt
Gate),我們來看看代碼哈~
NTSTATUS
InstallIG()
{
……
//判斷我們想要添加的中斷是否已被佔用;
if(IdtEntry[ADDINTID].OffsetLow !=
0
|| IdtEntry[ADDINTID].OffsetHigh != 0)
{
return
STATUS_UNSUCCESSFUL;
}
//複製原始的中斷門描述資訊;
RtlCopyMemory(&OldIdtEntry,&IdtEntry[ADDINTID],sizeof(OldIdtEntry));
//關中斷
__asm
cli
//更新執行代碼位移量的底16位;
IdtEntry[ADDINTID].OffsetLow =
(unsigned
short)InterruptServiceRoutine;
//目的程式碼片段的段選取器,CS為8;
IdtEntry[ADDINTID].Selector =
8;
//保留位,始終為零;
IdtEntry[ADDINTID].Reserved =
0;
//門類型,0xe代表中斷門;
IdtEntry[ADDINTID].Type =
0xe;
//SegmentFlag設定0代碼為段;
IdtEntry[ADDINTID].SegmentFlag =
0;
//描述符許可權等級為3,允許使用者模式程式調用本中斷;
IdtEntry[ADDINTID].DPL =
3;
//呈現標誌位,設定為一;
IdtEntry[ADDINTID].Present =
1;
//更新執行代碼位移量的高16位;
IdtEntry[ADDINTID].OffsetHigh = (unsigned
short)((unsigned int)InterruptServiceRoutine >>
16);
//開中斷
__asm sti
return
STATUS_SUCCESS;
}
VOID
RemoveIG()
{
……
__asm
cli
//恢複我們修改過的中斷門描述符;
RtlCopyMemory(&IdtEntry[ADDINTID],&OldIdtEntry,sizeof(OldIdtEntry));
__asm
sti
}
extern
void
_cdecl
InterruptServiceRoutine(VOID)
{
unsigned
int Command;
//擷取eax寄存器中的數值,接受從使用者模式傳入的命令參數;
__asm mov
Command,eax;
//執行核心代碼,擷取作業系統版本號碼;
DbgPrint("NtBuildNumber ==
%d/n",(unsigned short)NtBuildNumber);
//中斷返回;
__asm
iretd;
}
後記
寫到這兒,我們只是介紹了擴充IDT的一些基本方法,當然還有很多更深入的,更值得我們研究的課題需要大家努力去探索。比如我們可以將T-HookInt擴充,不僅僅是監視系統註冊表操作相關的系統服務調用,不過在Windows
XP/2003上由於其內在機制的一些變更,所以通過Hook int
0x2e來截獲系統服務調用就不這麼現實了。當然還有基於IDT的核心級後門,可以通過添加新的軟體中斷為任意使用者提供SYSTEM權限等級的Command等。總之,探究Windows核心奧秘的旅行還未結束,或許這隻能算是一次起航罷了。
附錄:
由於本文相關的原始碼比較多,所以在此就不帖了,歡迎有興趣的朋友到我們首頁下載,謝謝~
關於我們:
FZ5FZ
主要從事網路/系統安全的學習與研究,深入編程技術的剖析與探討,堅持原創,追求共用。
FZ5FZ 首頁:http://www.fz5fz.org