鍵盤過濾之核心級Hook(二)

來源:互聯網
上載者:User
鍵盤過濾之核心級Hook(二)

如果不想讓鍵盤過濾驅動程式或回呼函數首先獲得按鍵,則必須比連接埠驅動更加底層一些。
早期版本的QQ反盜號驅動的原理是這樣的:使用者要輸入密碼時(比如把輸入焦點移動到了密碼框裡),就註冊一個中斷服務來接管鍵盤中斷,比如0x93中斷,之後按鍵就不關鍵驅動的事了。
首先就來介紹一下Hook鍵盤中斷。
1.中斷:IRQ和INT
學過電腦體繫結構的人都知道硬體往往是通過中斷來通知CPU某個事件的發生。比如按鍵按下了。但是中斷並不一定要有任何硬體的通知,一條指令就能使CPU“發生中斷”。比如,在一個.c檔案寫上:
_asm int 3
這樣的代碼常常來人工設定一個斷點,執行到這裡程式會中斷。int n(n為中斷號)可以觸發軟體中斷(軟體中斷又叫異常),觸發的本質是:是CPU的執行暫停,並跳到中斷處理函數,中斷處理函數已經事先儲存在記憶體中。同時,這些函數的首地址儲存在一個叫做IDT(中斷描述符表)的表中,每一個中斷號在這個表中都有一項。
一旦一個int n被執行,則CPU會到IDT中去尋找第n項。其中有一個中斷描述符,在這個描述符裡可以讀到一個函數的首地址,然後CPU就跳到這個首地址去執行了。當然,適當的處理之後一般都會回來繼續前面程式的執行。這就是中斷的過程。
真正的中斷一般被稱為IRQ。某個IRQ來自什麼硬體,這在很大程度上有規定的。比如IRQ1一定是PS/2鍵盤,只有少數幾個IRQ留給使用者自用。一個IRQ一般都需要一個中斷函數來處理,但是IRQ沒有中斷號那麼多,只有24個。IRQ的處理也是由中斷處理函數來處理的,這就需要一個IRQ號到中斷號的對應關係。這樣一個IRQ發生時,CPU才知道跳轉到哪裡。
在IOAIPC出現之後,這個對應關係變得可以修改,在Windows上,PS/2鍵盤按鍵或者釋放鍵發生一般都是int0x93,正是因為這個關係(IRQ1->int 0x93)被設定了的原因。
這樣我們就有了一個簡單的方案可以保護鍵盤中斷:修改int 0x93在IDT中儲存的地址。修改為我們自己寫的一個函數,那麼這個中斷一定是我們先截獲到,其他的過濾層都在我們之後了。

2.如何修改IDT
在一個應用程式中修改IDT由於許可權問題是做不到的,但是在核心程式中做起來是完全可行的。IDT的記憶體位址是不定的,但是可以通過一條指令sidt擷取。下面的代碼可以獲得中斷描述符表的地址。
請注意,在多核CPU上,每一個核心都有自己的IDT。因此,應該注意對每個核心擷取IDT。也就是說,必須確保下面的代碼在每個核心上都得到執行。
// 由於這裡我們必須明確一個域是多少位,所以我們預先定義幾個明
// 確知道多少位長度的變數,以避免不同環境下編譯的麻煩.
typedef unsigned char P2C_U8;
typedef unsigned short P2C_U16;
typedef unsigned long P2C_U32;

#define P2C_MAKELONG(low, high) \
 ((P2C_U32)(((P2C_U16)((P2C_U32)(low) & 0xffff)) | ((P2C_U32)((P2C_U16)((P2C_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))

// 從sidt指令獲得一個如下的結構。從這裡可以得到IDT的開始地址
#pragma pack(push,1)
typedef struct P2C_IDTR_ {
 P2C_U16 limit;  // 範圍
 P2C_U32 base;  // 基地址(就是開始地址)
} P2C_IDTR, *PP2C_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;
 P2C_U16 selector;
 P2C_U8 reserved;
 P2C_U8 type:4;
 P2C_U8 always0:1;
 P2C_U8 dpl:2;
 P2C_U8 present:1;
 P2C_U16 offset_high;
} P2C_IDTENTRY, *PP2C_IDTENTRY;
#pragma pack(pop)
有些人可能對這種成員變數之後帶單個冒號的結構體寫法不太習慣。帶有冒號的域稱為位域。這是這樣一種域:這個成員的寬度甚至小於一個位元組,只有1~7位。冒號之後的數字表示位元,比如type有4位,always有1位等。
中斷服務的跳轉地址實際上是一個32位的虛擬位址。但是這個地址被很奇怪地分開儲存了,高16位儲存在offset_High中,低16位儲存在offset_low中。
這裡沒有中斷號,那是應為中斷號就是這個表中的索引。因此,第0x93項這個結構,就是讀者所需要關心的。

3.替換IDT中的跳轉地址
寫一個函數來代替那個中斷服務地址是可以的,但是請注意這個函數的寫法。中斷的發生並不是用call跳轉過去的,所以也不能通過ret回來。一般的說,中斷應該用iret指令返回。但是為了避免更多問題,我們還是處理後跳轉原有的中斷處理函數入口,讓它來替換我們返回比較好。這時我們需要一段不含C編譯器產生的函數架構的純彙編代碼。讀者可以直接用asm彙編來寫,但是筆者在這裡使用了C語言嵌入彙編。請注意用__declspec(naked)修飾可以產生一個裸函數。下面這個函數是一個例子:
void *g_p2c_old = NULL;

__declspec(naked) p2cInterruptProc()
{
 __asm
 {
          pushad     // 儲存所有的通用寄存器
   pushfd     // 儲存標誌寄存器
   call p2cUserFilter // 調一個我們自己的函數。這個函數將實現
   // 一些我們自己的功能
   popfd     // 恢複標誌寄存器
   popad     // 恢複通用寄存器
   jmp g_p2c_old  // 跳到原來的中斷服務程式
 }
}
裸函數中什麼都沒有,所以也不能使用局部變數,只能全部使用內嵌彙編實現。但是讀者大多數還是習慣用C語言的,所以我們可以簡單的用彙編來實現一個C函數的調用。C函數可能會改變寄存器的內容,這可能是後面真正的中斷處理函數所不期望的。所以在調用的前後,分別儲存和恢複這些寄存器。
下面代碼直接替換了IDT中的0x93號中斷服務,包括獲得IDT地址和替換等。但是要注意的是,這些代碼只能運行在單核的,32,位作業系統上;如果有多核的話,sidt只能獲得當前CPU核IDT。請注意:這個函數不但能替換,也可以完成恢複。
// 這個函數修改IDT表中的第x93項,修改為p2cInterruptProc。
// 在修改之前要儲存到g_p2c_old中。
void p2cHookInt93(BOOLEAN hook_or_unhook)
{
 PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
 idt_addr += 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(("p2c: try to hook interrupt.\r\n"));
  // 如果g_p2c_old是NULL,那麼進行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_HIGH16_OF_32(p2cInterruptProc);
 }
 else
 {
  KdPrint(("p2c: try to recovery interrupt.\r\n"));
  // 如果g_p2c_old不是NULL,那麼取消hook.
  idt_addr->offset_low = P2C_LOW16_OF_32(g_p2c_old);
  idt_addr->offset_high = P2C_HIGH16_OF_32(g_p2c_old);
 }
 KdPrint(("p2c: the current address = %x.\r\n",
  (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high)));
}
利用IOAPIC重定位中斷處理函數
1.什麼是IOAPIC
IOAPIC是可以用於多個核心CPU的新型中斷控制器,所以或與應該理解為一種新的可程式化硬體。
IOAPIC的作用在於當一個IRQ發生時,這個硬體將負責決定將IRQ發送給哪個CPU核心,以及以何種形式發送等。IOAPIC是可以編程的,因此可以通過編程,也可以將PS/2鍵盤的硬體插斷要求(IRQ1)發送給某個CPU核心,讓核心的IDT中的某個中斷號對應中斷處理服務來處理。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.