windwos鍵盤的過濾(Hook鍵盤中斷反過濾與利用IOAPIC重定位修改處理函數)

原理:
    計算機硬件往往通過中斷來通知cpu某個事件的發生。
    int n可以觸發軟件中斷(軟件中斷又叫異常),而IDT(中斷描述符表)中,每一箇中斷號在這個表中都有一項,每一項的描述符中可以讀到一個函數首地址。
    硬件中斷一般被稱爲IRQ,比如IRQ1一定是PS/2鍵盤。一個IRQ一般都需要一箇中斷處理程序來處理,這就需要一個IRQ號到中斷號的對應關係。
    IOAIPC的作用在於當一個IRQ發生時,這個硬件將負責決定將IRQ發送給哪個CPU核心,以及以何種形式發送等。IOAIPC是可編程的,因此可以將PS/2鍵盤的硬件中斷請求(IRQ1)發送給某個CPU核心,讓該核心的IDT中的某個中斷對應的中斷處理服務程序來處理。

    所以我們用兩種方法來過濾鍵盤按鍵:修改IDT,或者修改IOAPIC重定位表。      

一、Hook鍵盤中斷反過濾

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

       早期版本的QQ反盜號驅動的原理是這樣的:用戶要輸入密碼時(比如把輸入焦點移動到了密碼框裏),就註冊一箇中斷服務來接管鍵盤中斷,比如0x93中斷,之後按鍵就不關鍵驅動的事了。

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中的某個中斷號對應中斷處理服務來處理。

 IOAPIC重定位表的偏移從0x10開始,到0x3f結束,一共48個。IRQ1對應表項所在的寄存器偏移爲0x12,0x13,中斷號保存在0x12寄存器的低8位(1個字節),修改這個就可修改爲其他字節,windows下面默認爲0x93

以下爲兩種情況的代碼

///
/// @file         ps2intcap.c
/// @author    wowocock,crazy_chu
/// @date       2009-1-27
///

#include <ntddk.h>

// 這一句存在,則本程序編譯爲替換INT0x93的做法。如果
// 不存在,則爲IOAPIC重定位做法。
 //#define BUILD_FOR_IDT_HOOK

// 由於這裏我們必須明確一個域是多少位,所以我們預先定義幾個明
// 確知道多少位長度的變量,以避免不同環境下編譯的麻煩.
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;
}

#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)

#define OBUFFER_FULL 0x02
#define IBUFFER_FULL 0x01

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

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

// 首先讀端口獲得按鍵掃描碼打印出來。然後將這個掃
// 描碼寫回端口,以便別的應用程序能正確接收到按鍵。
// 如果不想讓別的程序截獲按鍵,可以寫回一個任意的
// 數據。
void p2cUserFilter()
{
    static P2C_U8 sch_pre = 0;
	P2C_U8	sch;
	p2cWaitForKbRead();
    _asm in al,0x60
    _asm mov sch,al
    KdPrint(("p2c: scan code = %2x\r\n",sch));
   //  把數據寫回端口,以便讓別的程序可以正確讀取。
	if(sch_pre != sch)
	{
		sch_pre = sch;
        _asm mov al,0xd2
        _asm out 0x64,al
		p2cWaitForKbWrite();
        _asm mov al,sch
        _asm out 0x60,al
	}
}

void *g_p2c_old = NULL;

int __declspec(naked) p2cInterruptProc()
{
	__asm
	{
		pushad					// 保存所有的通用寄存器
		pushfd					// 保存標誌寄存器
		call p2cUserFilter	// 調一個我們自己的函數。 這個函數將實現
								    // 一些我們自己的功能
		popfd					// 恢復標誌寄存器
		popad					// 恢復通用寄存器
		jmp	g_p2c_old		// 跳到原來的中斷服務程序
	}
}

// 這個函數修改IDT表中的第0x93項,修改爲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)));
}

// 在idt表中找到一個空閒的idtentry的位置。然後返回這個id.這是爲
// 了能填入我們新的鍵盤中斷處理入口。如果找不到就返回0。這
// 種情況下無法安裝新的中斷處理。
P2C_U8 p2cGetIdleIdtVec()
{
    P2C_U8 i;
    PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();

    // 從vec20搜索到2a即可。
	for(i=0x20;i<0x2a;i++)
	{
        // 如果類型爲0說明是空閒位置,返回即可。
        if(idt_addr[i].type == 0)
		{
			return i;
		}
	}
    return 0;
}


P2C_U8 p2cCopyANewIdt93(P2C_U8 id,void *interrupt_proc)
{
    // 我們寫入一個新的中斷門。這個門完全拷貝原來的0x93
    // 上的idtentry,只是中斷處理函數的地址不同。
    PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
    idt_addr[id] = idt_addr[0x93];
    idt_addr[id].offset_low = P2C_LOW16_OF_32(interrupt_proc);
    idt_addr[id].offset_high = P2C_HIGH16_OF_32(interrupt_proc);
    return id;
}

// 搜索IOAPIC獲得鍵盤中斷,或者設置這個值。
P2C_U8 p2cSeachOrSetIrq1(P2C_U8 new_ch)
{
    // 選擇寄存器。選擇寄存器雖然是32位的寄存器,但是隻使用
    // 低8位,其他的位都被保留。
	P2C_U8 *io_reg_sel;

    // 窗口寄存器,用來讀寫被選擇寄存器選擇的值,是32位的。
	P2C_U32 *io_win;
	P2C_U32 ch,ch1;

    // 定義一個物理地址,這個地址爲0xfec00000。正是IOAPIC
    // 寄存器組在Windows上的開始地址
	PHYSICAL_ADDRESS	phys ;
	PVOID paddr;
	RtlZeroMemory(&phys,sizeof(PHYSICAL_ADDRESS));
	phys.u.LowPart = 0xfec00000;

    // 物理地址是不能直接讀寫的。MmMapIoSpace把物理地址映射
    // 爲系統空間的虛擬地址。0x14是這片空間的長度。
	paddr = MmMapIoSpace(phys, 0x14, MmNonCached);

    // 如果映射失敗了就返回0.
	if (!MmIsAddressValid(paddr))
		return 0;

    // 選擇寄存器的偏移爲0
	io_reg_sel = (P2C_U8 *)paddr;
    // 窗口寄存器的偏移爲0x10.
	io_win = (P2C_U32 *)((P2C_U8 *)(paddr) + 0x10);

    // 選擇第0x12,剛好是irq1的項
	*io_reg_sel = 0x12;
	ch = *io_win;

    // 如果new_ch不爲0,我們就設置新值。並返回舊值。
    if(new_ch != 0)
    {
        ch1 = *io_win;
        ch1 &= 0xffffff00;
        ch1 |= (P2C_U32)new_ch;
        *io_win = ch1;
        KdPrint(("p2cSeachOrSetIrq1: set %2x to irq1.\r\n",(P2C_U8)new_ch));
    }

    // 窗口寄存器裏讀出的值是32位的,但是我們只需要
    // 一個字節就可以了。這個字節就是中斷向量的值。
    // 一會我們要修改這個值。
    ch &= 0xff;
	MmUnmapIoSpace(paddr, 0x14);
    KdPrint(("p2cSeachOrSetIrq1: the old vec of irq1 is %2x.\r\n",(P2C_U8)ch));
	return (P2C_U8)ch;
}

void p2cResetIoApic(BOOLEAN set_or_recovery)
{
    static P2C_U8 idle_id = 0;
    PP2C_IDTENTRY idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
    P2C_U8 old_id = 0;

    if(set_or_recovery)
    {
        // 如果是設置新的ioapic定位,那麼首先在g_p2c_old中保存
        // 原函數的入口。
        idt_addr = (PP2C_IDTENTRY)p2cGetIdt();
        idt_addr += 0x93;
        g_p2c_old = (void *)P2C_MAKELONG(idt_addr->offset_low,idt_addr->offset_high);
 
        // 然後獲得一個空閒位,將irq1處理中斷門複製一個進去。
        // 裏面的跳轉函數填寫爲我們的新的處理函數。
        idle_id = p2cGetIdleIdtVec();
        if(idle_id != 0)
        {
            p2cCopyANewIdt93(idle_id,p2cInterruptProc);
            // 然後重新定位到這個中斷。
            old_id = p2cSeachOrSetIrq1(idle_id);
            // 在32位WindowsXP下這個中斷默認應該是定位到0x93的。
            ASSERT(old_id == 0x93);
        }
    }
    else
    {
        // 如果是要恢復...
        old_id = p2cSeachOrSetIrq1(0x93);
        ASSERT(old_id == idle_id);
        // 現在那個中斷門沒用了,設置type = 0使之空閒
        idt_addr[old_id].type = 0;
    }
}

#define  DELAY_ONE_MICROSECOND  (-10)
#define  DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000)
#define  DELAY_ONE_SECOND (DELAY_ONE_MILLISECOND*1000)

void p2cUnload(PDRIVER_OBJECT drv)
{
	LARGE_INTEGER interval;
#ifdef BUILD_FOR_IDT_HOOK
    p2cHookInt93(FALSE);
#else
    p2cResetIoApic(FALSE);
#endif
    KdPrint (("p2c: unloading\n")); 
	// 睡眠5秒。等待所有irp處理結束
	interval.QuadPart = (5*1000 * DELAY_ONE_MILLISECOND);		
	KeDelayExecutionThread(KernelMode,FALSE,&interval);
}

NTSTATUS DriverEntry( 
                     IN PDRIVER_OBJECT DriverObject, 
                     IN PUNICODE_STRING RegistryPath 
                     ) 
{ 
   // ULONG i; 
	__asm int 3;
    KdPrint (("p2c: entering DriverEntry\n")); 
    // 卸載函數。
    DriverObject->DriverUnload = p2cUnload;
#ifdef BUILD_FOR_IDT_HOOK
    p2cHookInt93(TRUE);
#else
    p2cResetIoApic(TRUE);
#endif
    return  STATUS_SUCCESS; 
}


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