Windows內核實驗005 Inline Hook

準備工作

尋找Inline Hook的返回地址

在這裏插入圖片描述

假設我們現在要HOOK KiFastCallEntry這個內核函數,讓所有的程序在進入零環之前先跳到我們自己的代碼。

但是會出現一個問題,我們可以從三環的地址跳到零環的地址空間,因爲內核層的2GB內存是系統共用的。但是在Inline Hook返回的時候,是無法從零環返回到三環的。用戶層的2GB空間每個進程都有一份,而這個函數會被所有進程頻繁調用。

如果返回的時候,別的進程在調用這個函數,那麼Inline Hook就會跳到那個進程的地址空間。

既然不能用三環的地址,那就只能找一塊比較穩定的零環的內存區域讓它返回。爲了讓代碼減少不確定性,我們可以用GDTR中的未使用的內存區域。

接着用windbg查看一下gdt表

在這裏插入圖片描述

我們就使用80b95120這塊未使用的GDT表的位置來存放hook的返回地址。在使用之前,需要先確認一下內存屬性是否是可讀可寫可執行。

kd> !pte 80b95120
                    VA 80b95120
PDE at C0602028            PTE at C0405CA8
contains 0000000000193063  contains 0000000000B95163
pfn 193       ---DA--KWEV  pfn b95       -G-DA--KWEV

可以看到這塊內存目前是可讀可寫可執行的。

那麼我們就可以將自己的代碼複製到這塊內存空間中,然後讓KiFastCallEntry跳轉到80b95120的位置執行自己的代碼,執行完成之後再返回。

編寫代碼

首先查看一下_KiFastCallEntry的函數地址(我的pdb符號文件下載失敗,只能用windbg看要掛鉤的函數地址了)

kd> x nt!_KiFastCallEntry
83e7c0c0          nt!KiFastCallEntry (<no parameter info>)

接着查看一下反彙編

kd> u 83e7c0c0          
nt!KiFastCallEntry:
83e7c0c0 b923000000      mov     ecx,23h
83e7c0c5 6a30            push    30h
83e7c0c7 0fa1            pop     fs
83e7c0c9 8ed9            mov     ds,cx
83e7c0cb 8ec1            mov     es,cx
83e7c0cd 648b0d40000000  mov     ecx,dword ptr fs:[40h]
83e7c0d4 8b6104          mov     esp,dword ptr [ecx+4]
83e7c0d7 6a23            push    23h

可以發現第一行剛好是五個字節,那麼我們就可以在這裏下鉤子跳轉到之前準備好的地址,然後再跳回到下一行地址83e7c0c5處

接着我們來計算偏移,用要跳轉的目標地址83e7c0c5減去起始地址80b95120再減5=0x32E6FA0

接着準備一個數組,將準備好的數據放進數組裏,然後拷貝到GDT表的跳轉地址中

char code[64] =
{
	0xb9,0x23,0x00,0x00,0x00,			//mov     ecx,23h
	0xE9,0xA0,0x6F,0x2E,0x03			//E9 0x32E6FA0 
};

然後我們還需要知道code數組的地址,直接在VS中下斷點查看即可。我這裏的code數組的地址是0x403018。接着將數組循環拷貝到要HOOK的目標地址中。

p = (char*)0x80b95120;
	for (i=0;i<10;i++)
	{
		*p = code[i];
        p++;
	}

動態變化的返回地址

接着我們運行寫好的程序,然後查看一下被修改地址處的反彙編

kd> u 80b95120
80b95120 b923000000      mov     ecx,23h
80b95125 e9a06f2e03      jmp     nt!KiFastCallEntry+0xa (83e7c0ca)
80b9512a b980000000      mov     ecx,80h
80b9512f 0038            add     byte ptr [eax],bh
80b95131 51              push    ecx
80b95132 b980000000      mov     ecx,80h
80b95137 004051          add     byte ptr [eax+51h],al
80b9513a b980000000      mov     ecx,80h
//起始地址(GDT表空閒地址):80b95120
//HOOK的函數地址(KiFastCallEntry):0x83e7c0c0 
//返回地址:83e7c0c5

我們發現mov ecx,23這句是對的,但是jmp跳轉的目標地址居然是錯誤的,相差了五個字節。
在這裏插入圖片描述

原因在於起始地址(GDT表空閒地址):80b95120被Code數組的前四個字節被恢復寄存器環境的mov ecx,0x23填充了,導致代碼的位置也相對的往後移了五個字節。這就產生了一個問題,我們每次去HOOK新函數的時候,都要手動將偏移值重新計算一遍,這顯然是不可取的。

JmpTargetAddr

既然如此我們就來手動實現一個跳轉到目標地址的函數。借用一個寄存器來保存需要跳轉的地址,然後直接用jmp指令跳轉過去。這樣就可以省去每次手工計算偏移的步驟了。

在這裏插入圖片描述

我們利用83e840cd這個地址作爲跳轉的目標地址,然後用ecx來保存跳轉地址。當跳轉回83e840cd時,ecx會被重新賦值,不會出現任何問題。

代碼如下:

void __declspec(naked) JmpTargetAddr()
{
	__asm
	{
		mov ecx, 0x23;		 //保存現場環境
		push 0x30;			 //保存現場環境
		pop fs;				 //保存現場環境
		mov ds, ecx;		 //保存現場環境
		mov es, ecx;		 //保存現場環境


		mov ecx, 0x83e840cd; //返回地址
		jmp ecx;			 //跳轉到返回地址
	}
}

運行程序,接着查看一下GDT表起始位置的反彙編

kd> u 80b95120
80b95120 b923000000      mov     ecx,23h
80b95125 6a30            push    30h
80b95127 0fa1            pop     fs
80b95129 668ed9          mov     ds,cx
80b9512c 668ec1          mov     es,cx
80b9512f b9cd40e883      mov     ecx,offset nt!KiFastCallEntry+0xd (83e840cd)
80b95134 ffe1            jmp     ecx
80b95136 cc              int     3
//起始地址(GDT表空閒地址):80b95120
//HOOK的函數地址(KiFastCallEntry):0x83e840c0
//返回地址:83e840cd

可以看到現在我們的跳轉的目標地址就和預期寫的是一致的了,沒有出現之前的地址動態變化的問題。

Inline Hook基本框架

接下來我們要修改KiFastCallEntry函數的前五個字節,讓它跳轉到我們自己的地址。首先來查看一下前五個字節的頁面屬性

kd> x nt!_KiFastCallEntry
83e840c0          nt!KiFastCallEntry (<no parameter info>)
kd> !pte 83e840c0          
                    VA 83e840c0
PDE at C06020F8            PTE at C041F420
contains 00000000001D9063  contains 0000000003E84121
pfn 1d9       ---DA--KWEV  pfn 3e84      -G--A--KREV

可以看到這一塊內存是可讀不可寫的,這樣的話我們需要先關掉CPU的寫保護。代碼如下:

	//關閉寫保護
	__asm
	{
		mov eax, cr0;
		and eax, not 10000h;
		mov cr0, eax;
	}
	
	//開啓寫保護
	__asm
	{
		mov eax, cr0;				
		or eax, 10000h;   			
		mov cr0, eax;				 
		iretd;
	}

接着再修改目標函數的前五個字節,讓它跳到我們之前準備好的地址

//偏移0xfcd1105b
char code[] = { 0xE9,0x5B,0x10,0xD1,0xFC };
//修改要HOOK的函數前五個字節
memcpy((void*)0x83e840c0, code, sizeof(code));

最後運行程序,並且在windbg中查看一下KiFastCallEntry處的代碼

kd> x nt!_KiFastCallEntry
83e840c0          nt!KiFastCallEntry (<no parameter info>)
kd> u 83e840c0
nt!KiFastCallEntry:
83e840c0 e95b10d1fc      jmp     80b95120
83e840c5 6a30            push    30h
83e840c7 0fa1            pop     fs
83e840c9 8ed9            mov     ds,cx
83e840cb 8ec1            mov     es,cx
83e840cd 648b0d40000000  mov     ecx,dword ptr fs:[40h]
83e840d4 8b6104          mov     esp,dword ptr [ecx+4]
83e840d7 6a23            push    23h

可以看到現在這個函數已經成功跳到我們指定的GDT表的位置0x80b95120。接着再來看一下GDT表的位置0x80b95120處的代碼。

kd> u 0x80b95120
80b95120 b923000000      mov     ecx,23h
80b95125 6a30            push    30h
80b95127 0fa1            pop     fs
80b95129 668ed9          mov     ds,cx
80b9512c 668ec1          mov     es,cx
80b9512f b9cd40e883      mov     ecx,offset nt!KiFastCallEntry+0xd (83e840cd)
80b95134 ffe1            jmp     ecx
80b95136 cc              int     3

現在我們已經完成了一個Inline Hook的基本框架,當有進程調用KiFastCallEntry時,會跳轉到我們指定的函數地址,然後再返回。框架完成以後,在HOOK過程中想做什麼事,就完全由自己決定了。

示例代碼

示例代碼如下:

#include "pch.h"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

void JmpTargetAddr();

//偏移0xfcd1105b
char code[] = { 0xE9,0x5B,0x10,0xD1,0xFC };
int i;
char* p;

//起始地址(GDT表空閒地址):80b95120
//HOOK的函數地址(KiFastCallEntry):0x83e840c0
//返回地址:83e7c0c5
void __declspec(naked) IdtEntry()
{
	//將自己的函數拷貝的GDT表空閒地址
	p = (char*)0x80b95120;
	for (i = 0; i < 64; i++)
	{
		*p = ((char*)JmpTargetAddr)[i];
		p++;
	}
	//關閉寫保護
	__asm
	{
		mov eax, cr0;
		and eax, not 10000h;
		mov cr0, eax;
	}
	
	//修改要HOOK的函數前五個字節
	memcpy((void*)0x83e840c0, code, sizeof(code));


	//開啓寫保護
	__asm
	{
		mov eax, cr0;				
		or eax, 10000h;   			
		mov cr0, eax;				 
		iretd;
	}
} 


void __declspec(naked) JmpTargetAddr()
{
	__asm
	{
		mov ecx, 0x23;		 //恢復現場環境
		push 0x30;			 //恢復現場環境
		pop fs;				 //恢復現場環境
		mov ds, cx;			 //恢復現場環境
		mov es, cx;			 //恢復現場環境

		mov ecx, 0x83e840cd; //返回地址
		jmp ecx;			 //跳轉到返回地址
	}
}


void go()
{
		__asm int 0x20;
}

//eq 80b95500 0040ee00`00081040
int main()
{
	if ((DWORD)IdtEntry != 0x401040)
	{
		printf("wrong addr:%p", IdtEntry);
		exit(-1);
	}
	go();
	//printf("%p\n", g_num);
	system("pause");
}

實戰HOOK KiTrap01

接着我們找另外一個函數進行HOOK。

在這裏插入圖片描述

查看PC Hunter中IDT表的1號中斷函數,1號中斷是單步調試,當CPU的eflags的TF標誌位被置1時,就會產生一個單步中斷,然後調用Debug函數。調試器的單步斷點原理也在於此。

無需計算偏移的Inline Hook方法

之前我們在修改目標函數的前五個字節的時候需要計算跳轉的偏移,這個非常不方便。這一次我們用另外的不需要計算偏移的方法來完成Inline Hook。利用下面兩條彙編指令:

push 0x12345678;
ret;

這種方法的缺點就是需要HOOK的位置有6個字節的空間。

首先,查看一下要HOOK的函數地址反彙編

kd> x nt!_KiTrap01
83e85150          nt!KiTrap01 (<no parameter info>)
kd> u 83e85150
nt!KiTrap01:
83e85150 6a00            push    0
83e85152 66c74424020000  mov     word ptr [esp+2],0
83e85159 55              push    ebp
83e8515a 53              push    ebx
83e8515b 56              push    esi
83e8515c 57              push    edi
83e8515d 0fa0            push    fs
83e8515f bb30000000      mov     ebx,30h

我們將要HOOK的地址定爲0x83e85152,返回地址則爲它的下一句

首先修改JmpTargetAddr中的代碼爲跳回返回地址

void __declspec(naked) JmpTargetAddr()
{
	__asm
	{
		mov word ptr[esp + 2], 0;		 //恢復現場環境
		push 0x83e85159;				 //跳轉到返回地址
		ret;							 //跳轉到返回地址
	}
}

接着在返回之前打印出當前的ESP,查看一下產生單步異常的堆棧環境,同樣需要藉助一個內核的內存地址

push eax;
mov eax, ss:[esp];
mov ds : [0x80b953f0],eax;				//GDT表的空閒位置 用於保存內核變量
pop eax;

最後修改要HOOK的函數地址,跳轉到我們自己的地址

char code[] = { 0x68,0x20,0x51,0xB9,0x80,0xC3,0x90 };
memcpy((void*)0x83e85152, code, sizeof(code));

運行程序,檢查一下起始地址和被HOOK的函數地址的彙編代碼,都是正常的

kd> x nt!_KiTrap01
83e85150          nt!KiTrap01 (<no parameter info>)
kd> u 83e85150
nt!KiTrap01:
83e85150 6a00            push    0
83e85152 682051b980      push    80B95120h
83e85157 c3              ret
83e85158 90              nop
83e85159 55              push    ebp
83e8515a 53              push    ebx
83e8515b 56              push    esi
83e8515c 57              push    edi
kd> u 80B95120
80b95120 50              push    eax
80b95121 368b0424        mov     eax,dword ptr ss:[esp]
80b95125 3ea3f053b980    mov     dword ptr ds:[80B953F0h],eax
80b9512b 58              pop     eax
80b9512c 66c74424020000  mov     word ptr [esp+2],0
80b95133 685951e883      push    offset nt!KiTrap01+0x9 (83e85159)
80b95138 c3              ret
80b95139 cc              int     3

在這裏插入圖片描述

另外在PC Hunter中也檢測到我我們掛的鉤子

示例代碼

最後附上代碼

#include "pch.h"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

void JmpTargetAddr();

//偏移0xfcd1105b
char code[] = { 0x68,0x20,0x51,0xB9,0x80,0xC3,0x90 };
int i;
char* p;

//起始地址(GDT表空閒地址):80b95120
//HOOK的函數地址(KiTrap01):0x83e85152
//返回地址:83e85159
void __declspec(naked) IdtEntry()
{
	//將自己的函數拷貝的GDT表空閒地址
	p = (char*)0x80b95120;
	for (i = 0; i < 64; i++)
	{
		*p = ((char*)JmpTargetAddr)[i];
		p++;
	}
	//關閉寫保護
	__asm
	{
		mov eax, cr0;
		and eax, not 10000h;
		mov cr0, eax;
	}
	
	memcpy((void*)0x83e85152, code, sizeof(code));


	//開啓寫保護
	__asm
	{
		mov eax, cr0;				
		or eax, 10000h;   			
		mov cr0, eax;				 
		iretd;
	}
} 


void __declspec(naked) JmpTargetAddr()
{
	__asm
	{
		push eax;
		mov eax, ss:[esp+4];
		mov ds : [0x80b953f0],eax;				//GDT表的空閒位置 用於保存內核變量
		pop eax;

		mov word ptr[esp + 2], 0;		 //恢復現場環境
		push 0x83e85159;				 //跳轉到返回地址
		ret;							 //跳轉到返回地址
	}
}


void go()
{
		__asm int 0x20;
}

//eq 80b95500 0040ee00`00081040
int main()
{
	if ((DWORD)IdtEntry != 0x401040)
	{
		printf("wrong addr:%p", IdtEntry);
		exit(-1);
	}
	go();
	//printf("%p\n", g_num);
	system("pause");
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章