《Windows內核安全與驅動編程》-第八章-鍵盤的過濾學習-day5

鍵盤的過濾

8.6 Hook鍵盤中斷反過濾

​ 如果不想讓鍵盤驅過濾驅動程序或回調函數首先獲得按鍵,則必須比端口驅動更加底層一些。舉一個例子:

​ 早期版本的 QQ 反盜號驅動原理是這樣的: 在用戶要輸入密碼時(比如將輸入焦點移動到了密碼框裏), 就註冊一箇中斷服務來接管鍵盤中斷,比如 0x93 中斷,之後按鍵就不關鍵盤驅動的事了。爲此這個程序必須自己處理那些掃描碼,並得出用戶輸入了什麼密碼,然後交給QQ。這個過程就很難被截獲了。

8.6.1 中斷: IRQ和NT

_asm int n

​ 我們常常用這樣的代碼去人工的設置一個斷點,比如常見的 int 3int n 可以觸發軟件中斷(軟件中斷又叫異常),觸發的本質是:使 CPU 的執行暫停,並跳轉到中斷處理函數中,中斷處理函數已經實現保存在內存中。同時,這些函數的首地址保存在一個叫做 IDT(中斷描述符表) 的表中,每一箇中斷號都在這個表中有一項。

​ 一旦一個 int n 被執行,則 CPU 會到 IDT 中去查找第 n 項。其中有一箇中斷描述符,在這個描述符裏可以讀到一個函數的首地址,然後 CPU 就跳到這個首地址去執行了。在適當的處理之後一般都會回來繼續之前前面的程序。這就是中斷的過程。

​ 真正的硬件中斷一般被稱爲 IRQ 。某個 IRQ 來自什麼硬件是有規定的。比如 IRP1 一定是 PS/2鍵盤 ,只有少數幾個 IRQ 留給用戶自用。一個 IRQ 一般都需要一箇中斷處理函數來處理,但是 IRQ 並沒有中斷號那麼多。根據文檔, 可編程的 IRQ 只有24 個。IRQ 的處理也是由中斷處理函數來處理的,這就需要一個 IRQ 號到中斷號的對應關係。這樣當一個 IRQ 發生時,CPU 才知道要跳轉去哪裏執行。

​ 在 IOAIPC 出現之後,這個對應關係變得可以修改了。 在 Windows上, PS/2鍵盤 按鍵或者釋放鍵發生一般都是 int 0x93, 正因爲這個關係( IRQ1->int 0x93 )被設置了。

​ 這樣就有了一個簡單的方案可以保護鍵盤: 修改 int 0x93IDT 中保存的函數地址。修改爲我們自己寫的一個函數。那麼這個中斷一定是我們先截獲到,其他的過濾層都在我們之後了。

8.6.2 如何修改 IDT

​ 由於權限問題,在一個應用程序中修改 IDT 是做不到的,但是在內核程序中缺完全是可以做到的。 IDT 的內存地址是固定不變的,可以通過一條指令 sidt 獲取。

​ 注意,在多核 CPU 上,每個核心都有自己的 IDT,因此,應該注意對每個核心獲取 IDT。即要保證下面的代碼在每個核心上都得到執行。

//由於這裏必須明確一個域是多少位,所以我們預先定義幾個
//明確知道多少位長度的變量,以避免不同環境下的編譯的麻煩
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned long P2C_U32;

//通過 sidt 指令獲得一個如下結構。從這裏可以得到 IDT 的開始地址
//注意數據結構使用1字節對齊,避免對齊問題導致數據結構內容錯位
//這是給編譯器用的參數設置,有關結構體字節對齊方式的設置大概是指把原來對答齊方式設置壓棧,並設新的設置爲1
#pragma pack(push,1)
typedef struct P2C_IDTR_
{
    P2C_U16 limit;		//範圍
    P2C_U32 base		//開始地址
}P2C_IDTR,*PPP2C_IDTR
#pragma pack(pop)

//下面這個函數用 sidt 指令讀出一個 P2C_IDTR 結構,並返回 IDT 的地址
void *p2cGetIdt()
{
    P2C_IDTR idtr;
    //一句彙編指令讀取到 IDT 的位置
    _asm sidt idtr
    return (void *)idtr.base
}

​ 獲得了 IDT 的地址後,這個內存空間是一個數組,每個元素都有如下結構:

#pragma pack(push,1)
typedef struct P2C_IDT_ENTRY_ {
    	P2C_U16 offset_low;
    	PC2_U16 selector;
    	P2C_U8 reserved;
    	P2C_U8 type:4;
    	P2C_U8 alaways0:1;
    	P2C_U8 dpl:2;
    	P2C_U8 present:1;
    	P2C_U16 offset_high;
}P2C_IDTENTRY,*PP2C_IDRENTRY;
#pramga pack(pop)

​ 這種成員變量後帶單個冒號的結構體域稱位位域。這是這樣一種域: 這個成員的寬度至少1字節,只有1~7位。冒號之後的數字表示位數。比如 type alaways dpl present 分別有4 1 2 1位,它們加起來共有八位,所以他們實際佔的空間爲1字節。 顯然,這是一種C語言的強大,其他語言很少能這樣表示。

​ 中斷服務的跳轉地址實際上是一個 32 位的虛擬地址,但是這個地址被很奇特的憤慨保存。高16位保存在 offset_high 中,相應的還有低16位。

​ 這裏沒有中斷號,那是因爲中斷號就是這個表的索引。因此,第 0x93 項這個結構,就是我們要找的。

8.6.3 替換 IDT 中的跳轉地址

​ 寫一個函數來代替那個中斷服務地址是可以的,但是需要注意這個函數的寫法。中斷的發生並不是直接用 call 跳轉過去的。所以也不能通過 ret 回來。一般來說,中斷應該用 iret 指令返回。但是爲了避免更多的問題,我們還是處理後跳轉到原有的中斷處理函數入口,讓它來代替我們返回比較好。這時我們需要一段不含C編譯器生成的函數框架的純彙編代碼。我們可以使用 ASM 彙編來寫,也可以使用 C 語言內嵌彙編。

​ 使用 __declspec (naked) 修飾可以生成一個裸函數。 MS 的 C 編譯器不會再生成函數框架指令。下面給出例子:

__declsppec(naked) p2cInterruptProc()
{
    __asm
    {
        pushad					//保存所有的通用寄存器
        pushfd					//保存標誌寄存器
        call p2cUserFilter		//調用我們自己的函數
        						
        popfd					//返回標誌寄存器
        popad					//返回通用寄存器
        jmp g_p2c_old			//跳到原來的中斷服務程序
    }
}

​ 裸函數中什麼都沒有,所以也不能使用局部變量,只能用內嵌彙編來實現。但是大多數讀者還是習慣使用C 語言的,所以這裏我們簡單的用匯編來實現對一個 C 函數的調用。

​ 下面的代碼直接替換了 IDT 中的 0x93 號中斷服務,包括獲得 IDT 地址和替換等。但是要注意的是: 這些代碼只能運行在 單核、32位操作系統上;如果有多核的話,sidt 只能獲得當前 CPU 核的 IDT 。請注意,這個函數不但能完成替換,而且可以完成恢復。

//三個宏,便於取數據的高低字節部分,或者從高低字節部分組合數據
#define P2C_MAKELONG(low,high) \
((P2C_U32)(((P2C_U16)((P2C_U32)(low) & 0xffff)) | \
((P2C_U32)((P2C_U16)((P23_U32)(high) & 0xffff))) << 16))
#define P2C_LOW16_OF_32(data) \
((P2C_U16)(((P2C_U32)data) & 0xffff))
#define P2C_HIGH16_OF_32(data) \
((P2C_U16)(((P2C_U32)data)>>16))

//這個函數修改 IDT 表中的第 0x93 項,修改爲 p2cInterruptProc
//在修改之前要保存到 g_p2c_old 中
void p2cHookInt93(BOLLEAN hook_or_unhook)
{
    PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
    idt_addr += 0x93; //因爲獲得的是一個數組指針,所以直接加0x93
    KdPrint(("p2c: the current address = %x.\r\n",(void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
    if(hook_or_unhook)
    {
        KdPrint(("try to hook interrupt"));
        //進行hook
        g_p2c_old = (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
        idt_addr->offset_low = P2C_LOW16_OF_32(p2cInterruptProc);
        idt_addr->offset_high = P2C_LOW16_OF_32(p2cInterruptProc);
    }
    else
    {
        KdPrint("p2c: try to recovery interrupt.\r\n");
        //取消hook
        idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
        idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
    }
}

8.7 直接使用端口操作鍵盤

8.7.1 讀取鍵盤數據和命令端口

PS/2 鍵盤 的數據端口是0x60,直接讀取這個端口就能讀到數據。但是前提是,鍵盤必須處於可讀狀態。

​ 在驅動中沒有對端口的讀取進行限制,直接使用匯編指令就可以讀取。但是注意每次讀取只能讀一個字節。

// 定義一個字節
P2C_U8 sch;
_asm in al,0x60
_asm mov sch,al

​ 這段代碼將從 0x60讀取一個字節到sch中。如何確定鍵盤是否可讀呢?答案是讀取鍵盤的命令端口,如果讀出的值沒有 OBUFFER_FULL 標誌的話,則說明可以讀取。

​ 下面的代碼可以等待到鍵盤有數據可讀。

#define OBUFFER_FULL 0X02
#define IBUFFER_FULL 0X01
ULONG p2cWaitForKbRead()
{
    int i = 100;
    P2C_U8 mychar;
    do
    {
        _asm in al,0x60
		_asm mov sch,al
		KeStallExecutionProcessor(50);
		if(!(mychar & OBUFFER_FULL)) break;
    }while(i--);
    if(i) 
    	return TRUE;
    return FALSE;
}

​ 這段代碼就是設置一個100次的詢問,每次間隔50微秒。然後查看當前是否存在 OBUFFER_FULL 標誌。如果不存在,則返回 TRUE;

​ 同樣,鍵盤也不是隨時可以寫入數據的。下面的代碼可以等待到鍵盤可寫

ULONG p2cWaitForKbRead()
{
    int i = 100;
    P2C_U8 mychar;
    do
    {
        _asm in al,0x60
		_asm mov sch,al
		KeStallExecutionProcessor(50);
		if(!(mychar & IBUFFER_FULL)) break;
    }while(i--);
    if(i) 
    	return TRUE;
    return FALSE;
}

明日計劃

繼續學習驅動編程

翻譯工作

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章