掛鉤Windows API(zt)

掛鉤Windows API
--------------------------------------------------------------------------------
 來源:xfocus.net  類別:安全文獻  日期:2004-3-10 12:30:43


 
===========================[ 掛鉤Windows API ]==================

SoBeIt

Author: Holy_Father <[email protected]>
Version: 1.1 english
Date: 6.10.2002


=====[ 1. 內容 ]=============================================

1. 內容
2. 介紹
3. 掛鉤方法
3.1 運行前掛鉤
3.2 運行時掛鉤
3.2.1 使用IAT掛鉤本進程
3.2.2 改寫入口點掛鉤本進程
3.2.3 保存原始函數
3.2.4 掛鉤其它進程
3.2.4.1 DLL注入
3.2.4.2 獨立的代碼
3.2.4.3 原始修改
4. 結束語


=====[ 2. 介紹 ]====================================================

這篇文章是有關在OS Windows下掛鉤API函數的方法。所有例子都在基於NT技術的Windows版本NT 4.0及以上有效(Windows NT 4.0, Windows 2000, Windows XP)。可能在其它Windows系統也會有效。
你應該比較熟悉Windows下的進程、彙編器、PE文件結構和一些API函數,才能明白這篇文章裏的內容。
這裏使用"Hooking API"這個術語表示對API的完全修改。當調用被掛鉤的API時,我們的代碼能立刻被執行。我將寫下完全的掛鉤過程。


=====[ 3. 掛鉤方法 ]==============================================

一般來說我們的目的是用我們的代碼取代一些函數裏的代碼。這些問題有時可以在進程運行前解決。這些大多數時候可以用我們運行的用戶級進程來完成,目的可以是修改程序的行爲。舉個例子應用程序的破解,比方說有些程序會在啓動時需要原光盤,我們想要不用光盤就啓動它。如果我們修改獲取驅動類型的函數我們就可以讓程序從硬盤啓動。
當我們掛鉤系統進程時(比如說服務)這些不可能做到或者我們不打算這麼做,或者在這個例子裏我們不知道哪個進程纔是目標。這時我們就要用到動態掛鉤(在運行時掛鉤)的技術。使用的例子有rootkit或者病毒裏的反殺毒軟件的技術。


=====[ 3.1 運行前掛鉤 ]===========================================

這裏修改我們想要修改函數來自的物理模塊(大多數時候是.exe或.dll)。在這裏我們至少有3種可能的做法。
第一種可能是找到函數的入口點然後重寫它的代碼。這會因爲函數的大小而受限制,但我們能動態加載其它一些模塊(API LoadLibrary),所以應該足夠了。
內核函數(kernel32.dll)是通用的因爲Windows中每個進程都有這個模塊的拷貝。另一個好處是如果我們知道哪些模塊在某版本中會修改,我們可以在一些API如LoadLibraryA中使用直接的指針。這是因爲kernel模塊在內存中地址在相同Windows版本中是固定的。我們同樣也能用動態加載的模塊的作用。在這裏它的初始化部分在加載進內存後立刻就運行。在新模塊的初始化部分我們不受限制。
第二種可能是在模塊中被代替的函數只是原函數的擴展。然後我們選擇要麼修改開始的5個字節爲跳轉指令或者改寫IAT。如果改爲跳轉指令,那麼將會改變指令執行流程轉爲執行我們的代碼。如果調用了IAT記錄被修改的函數,我們的代碼能在調用結束後被執行。但模塊的擴展沒那麼容易,因爲我們必須注意DLL首部。
下一個是修改整個模塊。這意味着我們創建自己的模塊版本,它能夠加載原始的模塊並調用原始的函數,當然我們對這個不感興趣,但重要的函數都是被更新的。這種方法對於有的模塊過大有幾百個導出函數的很不方便。


=====[ 3.2 運行時掛鉤 ]==========================================

在運行前掛鉤通常都非常特殊,並且是在內部面向具體的應用程序(或模塊)。如果我們更換了kernel32.dll或ntdll.dll裏的函數(只在NT操作系統裏),我們就能完美地做到在所有將要運行的進程中替換這個函數。但說來容易做起來卻非常難,因爲我們不但得考慮精確性和需要編寫比較完善的新函數或新模塊,但主要問題是隻有將要運行的進程才能被掛鉤(要掛鉤所有進程只能重啓電腦)。另一個問題是如何進入這些文件,因爲NT操作系統保護了它們。比較好的解決方法在進程正在運行時掛鉤。這需要更多的有關知識,但最後的結果相當不錯。在運行中掛鉤只對能夠寫入它們的內存的進程能成功。爲了能寫入它自己我們使用API函數WriteProcessMemory。現在我們開始運行中掛鉤我們的進程。


=====[ 3.2.1 使用IAT掛鉤本進程 ]===================================

這裏有很多種可能性。首先介紹如何用改寫IAT掛鉤函數的方法。接下來這張圖描述了PE文件的結構:

+-------------------------------+ - offset 0
| MS DOS標誌("MZ") 和 DOS塊 |
+-------------------------------+
| PE 標誌 ("PE") |
+-------------------------------+
| .text | - 模塊代碼
| 程序代碼 |
| |
+-------------------------------+
| .data | - 已初始化的(全局靜態)數據
| 已初始化的數據 |
| |
+-------------------------------+
| .idata | - 導入函數的信息和數據
| 導入表 |
| |
+-------------------------------+
| .edata | - 導出函數的信息和數據
| 導出表 |
| |
+-------------------------------+
| 調試符號 |
+-------------------------------+

這裏對我們比較重要的是.idata部分的導入地址表(IAT)。這個部分包含了導入的相關信息和導入函數的地址。有一點很重要的是我們必須知道PE文件是如何創建的。當在編程語言裏間接調用任意API(這意味着我們是用函數的名字來調用它,而不是用它的地址),編譯器並不直接把調用連接到模塊,而是用jmp指令連接調用到IAT,IAT在系統把進程調入內存時時會由進程載入器填滿。這就是我們可以在兩個不同版本的Windows裏使用相同的二進制代碼的原因,雖然模塊可能會加載到不同的地址。進程載入器會在程序代碼裏調用所使用的IAT裏填入直接跳轉的jmp指令。所以我們能在IAT裏找到我們想要掛鉤的指定函數,我們就能很容易改變那裏的jmp指令並重定向代碼到我們的地址。完成之後每次調用都會執行我們的代碼了。這種方法的缺點是經常有很多函數要被掛鉤(比方說如果我們要在搜索文件的API中改變程序的行爲我們就得修改函數FindFirstFile和FindNextFile,但我們要知道這些函數都有ANSI和WIDE版本,所以我們不得不修改FindFirstFileA、FindFirstFileW、FindNextFileA和FileNextFileW的IAT地址。但還有其它類似的函數如FindFirstFileExA和它的WIDE版本FindFirstFileExW,也都是由前面提到的函數調用的。我們知道FindFirstFileW調用FindFirstFileExW,但這是直接調用,而不是使用IAT。再比如說ShellAPI的函數SHGetDesktopFolder也會直接調用FindFirstFilwW或FindFirstFileExW)。如果我們能獲得它們所有,結果就會很完美。
我們通過使用imagehlp.dll裏的ImageDirectoryEntryToData來很容易地找到IAT。

PVOID ImageDirectoryEntryToData(
IN LPVOID Base,
IN BOOLEAN MappedAsImage,
IN USHORT DirectoryEntry,
OUT PULONG Size
);

在這裏Base參數可以用我們程序的Instance(Instance通過調用GetModuleHandle獲得):

hInstance = GetModuleHandleA(NULL);

DirectoryEntry我們可以使用恆量IMAGE_DIRECTORY_ENTRY_IMPORT。

#define IMAGE_DIRECTORY_ENTRY_IMPORT 1

函數的結果是指向第一個IAT記錄指針。IAT的所有記錄是由IMAGE_IMPORT_DESCRIPTOR定義的結構。所以函數結果是指向IMAGE_IMPORT_DESCRIPTOR的指針。

typedef struct _IMAGE_THUNK_DATA {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} ;
} IMAGE_THUNK_DATA,*PIMAGE_THUNK_DATA;

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
PIMAGE_THUNK_DATA OriginalFirstThunk;
} ;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
PIMAGE_THUNK_DATA FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR,*PIMAGE_IMPORT_DESCRIPTOR;

IMAGE_IMPORT_DESCRIPTOR裏的Name成員變量是模塊名字的指針。如果我們想要掛鉤某個函數比如是來自kernel32.dll我們就在導入表裏找屬於名字kernel32.dll的描述符號。我們先調用ImageDirectoryEntryToData然後找到名字是"kernel32.dll"的描述符號(可能不只一個描述符號是這個名字),最後我們在這個模塊的記錄裏所有函數的列表裏找到我們想要的函數(函數地址通過GetProcAddress函數獲得)。如果我們找到了就必須用VirtualProtect函數來改變內存頁面的保護屬性,然後就可以在內存中的這些部分寫入代碼了。在改寫了地址之後我們要把保護屬性改回來。在調用VirtualProtect之前我們還要先知道有關頁面的信息,這通過VirtualQuery來實現。我們可以加入一些測試以防某些函數會失敗(比方說如果第一次調用VirtualProctect就失敗了,我們就沒辦法繼續)。

PCSTR pszHookModName = "kernel32.dll",pszSleepName = "Sleep";
HMODULE hKernel = GetModuleHandle(pszHookModName);
PROC pfnNew = (PROC)0x12345678, //這裏存放新地址
pfnHookAPIAddr = GetProcAddress(hKernel,pszSleepName);

ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
(PIMAGE_IMPORT_DESCRIPTOR)ImageDirectoryEntryToData(
hKernel,
TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize
);

while (pImportDesc->Name)
{
PSTR pszModName = (PSTR)((PBYTE) hKernel + pImportDesc->Name);
if (stricmp(pszModName, pszHookModName) == 0)
break;
pImportDesc++;
}

PIMAGE_THUNK_DATA pThunk =
(PIMAGE_THUNK_DATA)((PBYTE) hKernel + pImportDesc->FirstThunk);

while (pThunk->u1.Function)
{
PROC* ppfn = (PROC*) &pThunk->u1.Function;
BOOL bFound = (*ppfn == pfnHookAPIAddr);

if (bFound)
{
MEMORY_BASIC_INFORMATION mbi;
VirtualQuery(
ppfn,
&mbi,
sizeof(MEMORY_BASIC_INFORMATION)
);
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
PAGE_READWRITE,
&mbi.Protect)
)

*ppfn = *pfnNew;

DWORD dwOldProtect;
VirtualProtect(
mbi.BaseAddress,
mbi.RegionSize,
mbi.Protect,
&dwOldProtect
);
break;
}
pThunk++;
}

調用Sleep(1000)的結果如例子所示:

00407BD8: 68E8030000 push 0000003E8h
00407BDD: E812FAFFFF call Sleep

Sleep: ;這是跳轉到IAT裏的地址
004075F4: FF25BCA14000 jmp dword ptr [00040A1BCh]

原始表:
0040A1BC: 79 67 E8 77 00 00 00 00

新表:
0040A1BC: 78 56 34 12 00 00 00 00

所以最後會跳轉到0x12345678。


=====[ 3.2.2 改寫入口點掛鉤本進程 ]==================

改寫函數入口點開始的一些字節這種方法相當簡單。就象改變IAT裏的地址一樣,我們也要先修改頁面屬性。在這裏對我們想要掛鉤的函數是一開始的5個字節。爲了之後的使用我們用動態分配MEMORY_BASIC_INFORMATION結構。函數的起始地址也是用GetProcAddress來獲得。我們在這個地址裏插入指向我們代碼的跳轉指令。接下來程序調用Sleep(5000)(所以它會等待5秒鐘),然後Sleep函數被掛鉤並重定向到new_sleep,最後它再次調用Sleep(5000)。因爲新的函數new_sleep什麼都不做並直接返回,所以整個程序只需要5秒鐘而不是10秒種。


.386p
.model flat, stdcall

includelib lib/kernel32.lib
Sleep PROTO :DWORD
GetModuleHandleA PROTO :DWORD
GetProcAddress PROTO :DWORD,:DWORD
VirtualQuery PROTO :DWORD,:DWORD,:DWORD
VirtualProtect PROTO :DWORD,:DWORD,:DWORD,:DWORD
VirtualAlloc PROTO :DWORD,:DWORD,:DWORD,:DWORD
VirtualFree PROTO :DWORD,:DWORD,:DWORD
FlushInstructionCache PROTO :DWORD,:DWORD,:DWORD
GetCurrentProcess PROTO
ExitProcess PROTO :DWORD


.data

kernel_name db "kernel32.dll",0
sleep_name db "Sleep",0
old_protect dd ?

MEMORY_BASIC_INFORMATION_SIZE equ 28

PAGE_READWRITE dd 000000004h
PAGE_EXECUTE_READWRITE dd 000000040h
MEM_COMMIT dd 000001000h
MEM_RELEASE dd 000008000h


.code
start:
push 5000
call Sleep

do_hook:
push offset kernel_name
call GetModuleHandleA
push offset sleep_name
push eax
call GetProcAddress
mov edi,eax ;最後獲得Sleep地址

push PAGE_READWRITE
push MEM_COMMIT
push MEMORY_BASIC_INFORMATION_SIZE
push 0
call VirtualAlloc
test eax,eax
jz do_sleep
mov esi,eax ;爲MBI結構分配內存

push MEMORY_BASIC_INFORMATION_SIZE
push esi
push edi
call VirtualQuery ;內存頁的信息
test eax,eax
jz free_mem

call GetCurrentProcess
push 5
push edi
push eax
call FlushInstructionCache ;只是爲了確定一下:)

lea eax,[esi+014h]
push eax
push PAGE_EXECUTE_READWRITE
lea eax,[esi+00Ch]
push [eax]
push [esi]
call VirtualProtect ;我們要修改保護屬性,這樣才能夠寫入代碼
test eax,eax
jz free_mem

mov byte ptr [edi],0E9h ;寫入跳轉指令
mov eax,offset new_sleep
sub eax,edi
sub eax,5
inc edi
stosd ;這裏是跳轉地址

push offset old_protect
lea eax,[esi+014h]
push [eax]
lea eax,[esi+00Ch]
push [eax]
push [esi]
call VirtualProtect ;恢復頁保護屬性

free_mem:
push MEM_RELEASE
push 0
push esi
call VirtualFree ;釋放內存
do_sleep:
push 5000
call Sleep
push 0
call ExitProcess
new_sleep:
ret 004h
end start


第二次調用Sleep的結果是這樣:

004010A4: 6888130000 push 000001388h
004010A9: E80A000000 call Sleep


Sleep: ;這裏是跳轉到IAT裏的地址
004010B8: FF2514204000 jmp dword ptr [000402014h]

tabulka:
00402014: 79 67 E8 77 6C 7D E8 77

Kernel32.Sleep:
77E86779: E937A95788 jmp 0004010B5h

new_sleep:
004010B5: C20400 ret 004h


=====[ 3.2.3 保存原始函數 ]=====================================

更多時候我們需要的不僅僅是掛鉤函數。比方說也許我們並不想取代給定的函數而只是想檢查一下它的結果,或者也許我們只是想在函數被使用特定的參數來調用時才取代原函數。比較好的例子有前面提過的通過取代FindXXXFile函數來完成隱藏文件。所以如果我們想要隱藏指定的文件並且不想被注意的話,就得對其它所有文件只調用沒有被修改過的原始函數。這對使用修改IAT的方法時是很簡單的,爲調用原始函數我們可以用GetProcAddress獲得它的原始地址,然後直接調用。但修改入口點的方法就會有問題,因爲修改了函數入口點的5個字節,使我們破壞了原函數。所以我們必須保存開始的那些指令。這將用到以下的技術。
我們知道我們要修改開始的5個字節但不知道里麪包含多少條指令以及指令的長度。我們得爲開始那些指令保留足夠的內存空間。16個字節應該足夠了,因爲函數開始時通常沒有多長的指令,很可能根本就用不到16個字節。整個被保留的內存用0x90(0x90=nop)來填滿。下一個5個字節預留給將在之後填入的跳轉指令。

old_hook: db 090h,090h,090h,090h,090h,090h,090h,090h
db 090h,090h,090h,090h,090h,090h,090h,090h
db 0E9h,000h,000h,000h,000h


現在我們已準備好拷貝開始的指令。爲獲得指令長度的代碼相當麻煩,這就是我們得使用已完成的引擎的原因。它是由Z0MBiE寫的。傳入參數是我們要獲得長度的指令的地址。輸出參數在eax裏。


; LDE32, Length-Disassembler Engine, 32-bit, (x) 1999-2000 Z0MBiE
; special edition for REVERT tool

; version 1.05

C_MEM1 equ 0001h ; |
C_MEM2 equ 0002h ; |may be used simultaneously
C_MEM4 equ 0004h ; |
C_DATA1 equ 0100h ; |
C_DATA2 equ 0200h ; |may be used simultaneously
C_DATA4 equ 0400h ; |
C_67 equ 0010h ; used with C_PREFIX
C_MEM67 equ 0020h ; C_67 ? C_MEM2 : C_MEM4
C_66 equ 1000h ; used with C_PREFIX
C_DATA66 equ 2000h ; C_66 ? C_DATA2 : C_DATA4
C_PREFIX equ 0008h ; prefix. take opcode again
C_MODRM equ 4000h ; MODxxxR/M
C_DATAW0 equ 8000h ; opc&1 ? C_DATA66 : C_DATA1

p386
model flat
locals @@

.code

public disasm_main
public _disasm_main
public @disasm_main
public DISASM_MAIN

disasm_main:
_disasm_main:
@disasm_main:
DISASM_MAIN:


; __fastcall EAX
; __cdecl [ESP+4]

;這是我的第一處修改,它只是這個函數的聲明
get_instr_len:

mov ecx, [esp+4] ; ECX = opcode ptr

xor edx, edx ; 標誌
xor eax, eax

@@prefix: and dl, not C_PREFIX

mov al, [ecx]
inc ecx

or edx, table_1[eax*4]

test dl, C_PREFIX
jnz @@prefix

cmp al, 0F6h
je @@test
cmp al, 0F7h
je @@test

cmp al, 0CDh
je @@int

cmp al, 0Fh
je @@0F
@@cont:
test dh, C_DATAW0 shr 8
jnz @@dataw0
@@dataw0done:
test dh, C_MODRM shr 8
jnz @@modrm
@@exitmodrm:
test dl, C_MEM67
jnz @@mem67
@@mem67done:
test dh, C_DATA66 shr 8
jnz @@data66
@@data66done:
mov eax, ecx
sub eax, [esp+4]

and edx,C_MEM1+C_MEM2+C_MEM4+C_DATA1+C_DATA2+C_DATA4
add al, dl
add al, dh

;這裏是我的第二處修改,只有在原始版本這裏是retn
@@exit: ret 00004h

@@test: or dh, C_MODRM shr 8
test byte ptr [ecx], 00111000b ; F6/F7 -- test
jnz @@cont
or dh, C_DATAW0 shr 8
jmp @@cont

@@int: or dh, C_DATA1 shr 8
cmp byte ptr [ecx], 20h
jne @@cont
or dh, C_DATA4 shr 8
jmp @@cont

@@0F: mov al, [ecx]
inc ecx
or edx, table_0F[eax*4]

cmp edx, -1
jne @@cont

@@error: mov eax, edx
jmp @@exit

@@dataw0: xor dh, C_DATA66 shr 8
test al, 00000001b
jnz @@dataw0done
xor dh, (C_DATA66+C_DATA1) shr 8
jmp @@dataw0done

@@mem67: xor dl, C_MEM2
test dl, C_67
jnz @@mem67done
xor dl, C_MEM4+C_MEM2
jmp @@mem67done

@@data66: xor dh, C_DATA2 shr 8
test dh, C_66 shr 8
jnz @@data66done
xor dh, (C_DATA4+C_DATA2) shr 8
jmp @@data66done

@@modrm: mov al, [ecx]
inc ecx

mov ah, al ; ah=mod, al=rm

and ax, 0C007h
cmp ah, 0C0h
je @@exitmodrm

test dl, C_67
jnz @@modrm16

@@modrm32: cmp al, 04h
jne @@a

mov al, [ecx] ; sib
inc ecx
and al, 07h

@@a: cmp ah, 40h
je @@mem1
cmp ah, 80h
je @@mem4

cmp ax, 0005h
jne @@exitmodrm

@@mem4: or dl, C_MEM4
jmp @@exitmodrm

@@mem1: or dl, C_MEM1
jmp @@exitmodrm

@@modrm16: cmp ax, 0006h
je @@mem2
cmp ah, 40h
je @@mem1
cmp ah, 80h
jne @@exitmodrm

@@mem2: or dl, C_MEM2
jmp @@exitmodrm

endp

.data

;0F -- 在代碼中分析,不需要標誌(也就是標誌(flag)必須爲0)
;F6,F7 -- --//-- (ttt=000 -- 3 字節, 否則爲2字節)
;CD -- --//-- (如果爲 CD 20 爲6字節, 否則爲2字節)

table_1 label dword ; 一般的指令

dd C_MODRM ; 00
dd C_MODRM ; 01
dd C_MODRM ; 02
dd C_MODRM ; 03
dd C_DATAW0 ; 04
dd C_DATAW0 ; 05
dd 0 ; 06
dd 0 ; 07
dd C_MODRM ; 08
dd C_MODRM ; 09
dd C_MODRM ; 0A
dd C_MODRM ; 0B
dd C_DATAW0 ; 0C
dd C_DATAW0 ; 0D
dd 0 ; 0E
dd 0 ; 0F
dd C_MODRM ; 10
dd C_MODRM ; 11
dd C_MODRM ; 12
dd C_MODRM ; 13
dd C_DATAW0 ; 14
dd C_DATAW0 ; 15
dd 0 ; 16
dd 0 ; 17
dd C_MODRM ; 18
dd C_MODRM ; 19
dd C_MODRM ; 1A
dd C_MODRM ; 1B
dd C_DATAW0 ; 1C
dd C_DATAW0 ; 1D
dd 0 ; 1E
dd 0 ; 1F
dd C_MODRM ; 20
dd C_MODRM ; 21
dd C_MODRM ; 22
dd C_MODRM ; 23
dd C_DATAW0 ; 24
dd C_DATAW0 ; 25
dd C_PREFIX ; 26
dd 0 ; 27
dd C_MODRM ; 28
dd C_MODRM ; 29
dd C_MODRM ; 2A
dd C_MODRM ; 2B
dd C_DATAW0 ; 2C
dd C_DATAW0 ; 2D
dd C_PREFIX ; 2E
dd 0 ; 2F
dd C_MODRM ; 30
dd C_MODRM ; 31
dd C_MODRM ; 32
dd C_MODRM ; 33
dd C_DATAW0 ; 34
dd C_DATAW0 ; 35
dd C_PREFIX ; 36
dd 0 ; 37
dd C_MODRM ; 38
dd C_MODRM ; 39
dd C_MODRM ; 3A
dd C_MODRM ; 3B
dd C_DATAW0 ; 3C
dd C_DATAW0 ; 3D
dd C_PREFIX ; 3E
dd 0 ; 3F
dd 0 ; 40
dd 0 ; 41
dd 0 ; 42
dd 0 ; 43
dd 0 ; 44
dd 0 ; 45
dd 0 ; 46
dd 0 ; 47
dd 0 ; 48
dd 0 ; 49
dd 0 ; 4A
dd 0 ; 4B
dd 0 ; 4C
dd 0 ; 4D
dd 0 ; 4E
dd 0 ; 4F
dd 0 ; 50
dd 0 ; 51
dd 0 ; 52
dd 0 ; 53
dd 0 ; 54
dd 0 ; 55
dd 0 ; 56
dd 0 ; 57
dd 0 ; 58
dd 0 ; 59
dd 0 ; 5A
dd 0 ; 5B
dd 0 ; 5C
dd 0 ; 5D
dd 0 ; 5E
dd 0 ; 5F
dd 0 ; 60
dd 0 ; 61
dd C_MODRM ; 62
dd C_MODRM ; 63
dd C_PREFIX ; 64
dd C_PREFIX ; 65
dd C_PREFIX+C_66 ; 66
dd C_PREFIX+C_67 ; 67
dd C_DATA66 ; 68
dd C_MODRM+C_DATA66 ; 69
dd C_DATA1 ; 6A
dd C_MODRM+C_DATA1 ; 6B
dd 0 ; 6C
dd 0 ; 6D
dd 0 ; 6E
dd 0 ; 6F
dd C_DATA1 ; 70
dd C_DATA1 ; 71
dd C_DATA1 ; 72
dd C_DATA1 ; 73
dd C_DATA1 ; 74
dd C_DATA1 ; 75
dd C_DATA1 ; 76
dd C_DATA1 ; 77
dd C_DATA1 ; 78
dd C_DATA1 ; 79
dd C_DATA1 ; 7A
dd C_DATA1 ; 7B
dd C_DATA1 ; 7C
dd C_DATA1 ; 7D
dd C_DATA1 ; 7E
dd C_DATA1 ; 7F
dd C_MODRM+C_DATA1 ; 80
dd C_MODRM+C_DATA66 ; 81
dd C_MODRM+C_DATA1 ; 82
dd C_MODRM+C_DATA1 ; 83
dd C_MODRM ; 84
dd C_MODRM ; 85
dd C_MODRM ; 86
dd C_MODRM ; 87
dd C_MODRM ; 88
dd C_MODRM ; 89
dd C_MODRM ; 8A
dd C_MODRM ; 8B
dd C_MODRM ; 8C
dd C_MODRM ; 8D
dd C_MODRM ; 8E
dd C_MODRM ; 8F
dd 0 ; 90
dd 0 ; 91
dd 0 ; 92
dd 0 ; 93
dd 0 ; 94
dd 0 ; 95
dd 0 ; 96
dd 0 ; 97
dd 0 ; 98
dd 0 ; 99
dd C_DATA66+C_MEM2 ; 9A
dd 0 ; 9B
dd 0 ; 9C
dd 0 ; 9D
dd 0 ; 9E
dd 0 ; 9F
dd C_MEM67 ; A0
dd C_MEM67 ; A1
dd C_MEM67 ; A2
dd C_MEM67 ; A3
dd 0 ; A4
dd 0 ; A5
dd 0 ; A6
dd 0 ; A7
dd C_DATA1 ; A8
dd C_DATA66 ; A9
dd 0 ; AA
dd 0 ; AB
dd 0 ; AC
dd 0 ; AD
dd 0 ; AE
dd 0 ; AF
dd C_DATA1 ; B0
dd C_DATA1 ; B1
dd C_DATA1 ; B2
dd C_DATA1 ; B3
dd C_DATA1 ; B4
dd C_DATA1 ; B5
dd C_DATA1 ; B6
dd C_DATA1 ; B7
dd C_DATA66 ; B8
dd C_DATA66 ; B9
dd C_DATA66 ; BA
dd C_DATA66 ; BB
dd C_DATA66 ; BC
dd C_DATA66 ; BD
dd C_DATA66 ; BE
dd C_DATA66 ; BF
dd C_MODRM+C_DATA1 ; C0
dd C_MODRM+C_DATA1 ; C1
dd C_DATA2 ; C2
dd 0 ; C3
dd C_MODRM ; C4
dd C_MODRM ; C5
dd C_MODRM+C_DATA1 ; C6
dd C_MODRM+C_DATA66 ; C7
dd C_DATA2+C_DATA1 ; C8
dd 0 ; C9
dd C_DATA2 ; CA
dd 0 ; CB
dd 0 ; CC
dd 0 ; CD
dd 0 ; CE
dd 0 ; CF
dd C_MODRM ; D0
dd C_MODRM ; D1
dd C_MODRM ; D2
dd C_MODRM ; D3
dd C_DATA1 ; D4
dd C_DATA1 ; D5
dd 0 ; D6
dd 0 ; D7
dd C_MODRM ; D8
dd C_MODRM ; D9
dd C_MODRM ; DA
dd C_MODRM ; DB
dd C_MODRM ; DC
dd C_MODRM ; DD
dd C_MODRM ; DE
dd C_MODRM ; DF
dd C_DATA1 ; E0
dd C_DATA1 ; E1
dd C_DATA1 ; E2
dd C_DATA1 ; E3
dd C_DATA1 ; E4
dd C_DATA1 ; E5
dd C_DATA1 ; E6
dd C_DATA1 ; E7
dd C_DATA66 ; E8
dd C_DATA66 ; E9
dd C_DATA66+C_MEM2 ; EA
dd C_DATA1 ; EB
dd 0 ; EC
dd 0 ; ED
dd 0 ; EE
dd 0 ; EF
dd C_PREFIX ; F0
dd 0 ; F1
dd C_PREFIX ; F2
dd C_PREFIX ; F3
dd 0 ; F4
dd 0 ; F5
dd 0 ; F6
dd 0 ; F7
dd 0 ; F8
dd 0 ; F9
dd 0 ; FA
dd 0 ; FB
dd 0 ; FC
dd 0 ; FD
dd C_MODRM ; FE
dd C_MODRM ; FF

table_0F label dword ; 0F爲前綴的指令

dd C_MODRM ; 00
dd C_MODRM ; 01
dd C_MODRM ; 02
dd C_MODRM ; 03
dd -1 ; 04
dd -1 ; 05
dd 0 ; 06
dd -1 ; 07
dd 0 ; 08
dd 0 ; 09
dd 0 ; 0A
dd 0 ; 0B
dd -1 ; 0C
dd -1 ; 0D
dd -1 ; 0E
dd -1 ; 0F
dd -1 ; 10
dd -1 ; 11
dd -1 ; 12
dd -1 ; 13
dd -1 ; 14
dd -1 ; 15
dd -1 ; 16
dd -1 ; 17
dd -1 ; 18
dd -1 ; 19
dd -1 ; 1A
dd -1 ; 1B
dd -1 ; 1C
dd -1 ; 1D
dd -1 ; 1E
dd -1 ; 1F
dd -1 ; 20
dd -1 ; 21
dd -1 ; 22
dd -1 ; 23
dd -1 ; 24
dd -1 ; 25
dd -1 ; 26
dd -1 ; 27
dd -1 ; 28
dd -1 ; 29
dd -1 ; 2A
dd -1 ; 2B
dd -1 ; 2C
dd -1 ; 2D
dd -1 ; 2E
dd -1 ; 2F
dd -1 ; 30
dd -1 ; 31
dd -1 ; 32
dd -1 ; 33
dd -1 ; 34
dd -1 ; 35
dd -1 ; 36
dd -1 ; 37
dd -1 ; 38
dd -1 ; 39
dd -1 ; 3A
dd -1 ; 3B
dd -1 ; 3C
dd -1 ; 3D
dd -1 ; 3E
dd -1 ; 3F
dd -1 ; 40
dd -1 ; 41
dd -1 ; 42
dd -1 ; 43
dd -1 ; 44
dd -1 ; 45
dd -1 ; 46
dd -1 ; 47
dd -1 ; 48
dd -1 ; 49
dd -1 ; 4A
dd -1 ; 4B
dd -1 ; 4C
dd -1 ; 4D
dd -1 ; 4E
dd -1 ; 4F
dd -1 ; 50
dd -1 ; 51
dd -1 ; 52
dd -1 ; 53
dd -1 ; 54
dd -1 ; 55
dd -1 ; 56
dd -1 ; 57
dd -1 ; 58
dd -1 ; 59
dd -1 ; 5A
dd -1 ; 5B
dd -1 ; 5C
dd -1 ; 5D
dd -1 ; 5E
dd -1 ; 5F
dd -1 ; 60
dd -1 ; 61
dd -1 ; 62
dd -1 ; 63
dd -1 ; 64
dd -1 ; 65
dd -1 ; 66
dd -1 ; 67
dd -1 ; 68
dd -1 ; 69
dd -1 ; 6A
dd -1 ; 6B
dd -1 ; 6C
dd -1 ; 6D
dd -1 ; 6E
dd -1 ; 6F
dd -1 ; 70
dd -1 ; 71
dd -1 ; 72
dd -1 ; 73
dd -1 ; 74
dd -1 ; 75
dd -1 ; 76
dd -1 ; 77
dd -1 ; 78
dd -1 ; 79
dd -1 ; 7A
dd -1 ; 7B
dd -1 ; 7C
dd -1 ; 7D
dd -1 ; 7E
dd -1 ; 7F
dd C_DATA66 ; 80
dd C_DATA66 ; 81
dd C_DATA66 ; 82
dd C_DATA66 ; 83
dd C_DATA66 ; 84
dd C_DATA66 ; 85
dd C_DATA66 ; 86
dd C_DATA66 ; 87
dd C_DATA66 ; 88
dd C_DATA66 ; 89
dd C_DATA66 ; 8A
dd C_DATA66 ; 8B
dd C_DATA66 ; 8C
dd C_DATA66 ; 8D
dd C_DATA66 ; 8E
dd C_DATA66 ; 8F
dd C_MODRM ; 90
dd C_MODRM ; 91
dd C_MODRM ; 92
dd C_MODRM ; 93
dd C_MODRM ; 94
dd C_MODRM ; 95
dd C_MODRM ; 96
dd C_MODRM ; 97
dd C_MODRM ; 98
dd C_MODRM ; 99
dd C_MODRM ; 9A
dd C_MODRM ; 9B
dd C_MODRM ; 9C
dd C_MODRM ; 9D
dd C_MODRM ; 9E
dd C_MODRM ; 9F
dd 0 ; A0
dd 0 ; A1
dd 0 ; A2
dd C_MODRM ; A3
dd C_MODRM+C_DATA1 ; A4
dd C_MODRM ; A5
dd -1 ; A6
dd -1 ; A7
dd 0 ; A8
dd 0 ; A9
dd 0 ; AA
dd C_MODRM ; AB
dd C_MODRM+C_DATA1 ; AC
dd C_MODRM ; AD
dd -1 ; AE
dd C_MODRM ; AF
dd C_MODRM ; B0
dd C_MODRM ; B1
dd C_MODRM ; B2
dd C_MODRM ; B3
dd C_MODRM ; B4
dd C_MODRM ; B5
dd C_MODRM ; B6
dd C_MODRM ; B7
dd -1 ; B8
dd -1 ; B9
dd C_MODRM+C_DATA1 ; BA
dd C_MODRM ; BB
dd C_MODRM ; BC
dd C_MODRM ; BD
dd C_MODRM ; BE
dd C_MODRM ; BF
dd C_MODRM ; C0
dd C_MODRM ; C1
dd -1 ; C2
dd -1 ; C3
dd -1 ; C4
dd -1 ; C5
dd -1 ; C6
dd -1 ; C7
dd 0 ; C8
dd 0 ; C9
dd 0 ; CA
dd 0 ; CB
dd 0 ; CC
dd 0 ; CD
dd 0 ; CE
dd 0 ; CF
dd -1 ; D0
dd -1 ; D1
dd -1 ; D2
dd -1 ; D3
dd -1 ; D4
dd -1 ; D5
dd -1 ; D6
dd -1 ; D7
dd -1 ; D8
dd -1 ; D9
dd -1 ; DA
dd -1 ; DB
dd -1 ; DC
dd -1 ; DD
dd -1 ; DE
dd -1 ; DF
dd -1 ; E0
dd -1 ; E1
dd -1 ; E2
dd -1 ; E3
dd -1 ; E4
dd -1 ; E5
dd -1 ; E6
dd -1 ; E7
dd -1 ; E8
dd -1 ; E9
dd -1 ; EA
dd -1 ; EB
dd -1 ; EC
dd -1 ; ED
dd -1 ; EE
dd -1 ; EF
dd -1 ; F0
dd -1 ; F1
dd -1 ; F2
dd -1 ; F3
dd -1 ; F4
dd -1 ; F5
dd -1 ; F6
dd -1 ; F7
dd -1 ; F8
dd -1 ; F9
dd -1 ; FA
dd -1 ; FB
dd -1 ; FC
dd -1 ; FD
dd -1 ; FE
dd -1 ; FF

end


現在我們可以獲取任意地址的指令長度。我們重複調用這個函數直到讀取了5個字節。完成後把這些字節拷貝到old_hook。我們知道了開始這些指令的長度,所以我們可以在原始函數的下條指令填入跳轉地址。

.386p
.model flat, stdcall

...

.data

kernel_name db "kernel32.dll",0
sleep_name db "Sleep",0

...

MEM_RELEASE dd 000008000h

;16 nops + 一個跳轉指令
old_sleep db 090h,090h,090h,090h,090h,090h,090h,090h,
090h,090h,090h,090h,090h,090h,090h,090h,
0E9h,000h,000h,000h,000h


.code
start:
push 5000
call Sleep

do_hook:
push offset kernel_name
call GetModuleHandleA
push offset sleep_name
push eax
call GetProcAddress
push eax
mov esi,eax

xor ecx,ecx
mov ebx,esi
get_five_bytes:
push ecx
push ebx
call get_instr_len ;調用LDE32
pop ecx
add ecx,eax
add ebx,eax
cmp ecx,5
jb get_five_bytes
mov edi,offset old_sleep ;計算跳轉地址
mov [edi+011h],ebx
sub [edi+011h],edi
sub dword ptr [edi+011h],015h
rep movsb
pop edi

;下面的代碼都是前面有的,所以不需要註解了

push PAGE_READWRITE
push MEM_COMMIT
push MEMORY_BASIC_INFORMATION_SIZE
push 0
call VirtualAlloc
test eax,eax
jz do_sleep
mov esi,eax

push MEMORY_BASIC_INFORMATION_SIZE
push esi
push edi
call VirtualQuery
test eax,eax
jz free_mem

call GetCurrentProcess
push 5
push edi
push eax
call FlushInstructionCache

lea eax,[esi+014h]
push eax
push PAGE_EXECUTE_READWRITE
lea eax,[esi+00Ch]
push [eax]
push [esi]
call VirtualProtect
test eax,eax
jz free_mem

mov byte ptr [edi],0E9h
mov eax,offset new_sleep
sub eax,edi
sub eax,5
inc edi
stosd

push offset old_protect
lea eax,[esi+014h]
push [eax]
lea eax,[esi+00Ch]
push [eax]
push [esi]
call VirtualProtect

free_mem:
push MEM_RELEASE
push 0
push esi
call VirtualFree
do_sleep:
push 5000
call Sleep
push 0
call ExitProcess
new_sleep:
mov eax,dword ptr [esp+004h]
add eax,eax ;重複延時
push eax
mov eax,offset old_sleep ;調用原函數
call eax
ret 004h


掛鉤後看起來想這樣:

004010CC: 6888130000 push 000001388h
004010D1: E818090000 call Sleep


Sleep: ;跳轉到IAT裏的地址
004019EE: FF2514204000 jmp dword ptr [000402014h]

tabulka:
00402014: 79 67 E8 77 6C 7D E8 77

Kernel32.Sleep:
77E86779: E95FA95788 jmp 0004010DDh

new_sleep:
004010DD: 8B442404 mov eax,dword ptr [esp+4]
004010E1: 03C0 add eax,eax
004010E3: 50 push eax
004010E4: B827304000 mov eax,000403027h
004010E9: FFD0 call eax

old_sleep:
00403027: 6A00 push 0
00403029: FF742408 push dword ptr [esp+8]
0040302D: 90 nop
0040302E: 90 nop
0040302F: 90 nop
00403030: 90 nop
00403031: 90 nop
00403032: 90 nop
00403033: 90 nop
00403034: 90 nop
00403035: 90 nop
00403036: 90 nop
00403037: E94337A877 jmp Kernel32.77E8677F

;這個指令在Kernel32.Sleep(77E86779)後1個字節

Kernel32.77E8677F:
77E8677F: E803000000 call Kernel32.SleepEx
... ;後面的已不重要

爲了讓這些看起來更清楚,這是原始版本的Kernel32.Sleep:

Kernel32.Sleep:
77E86779: 6A00 push 0
77E8677B: FF742408 push dword ptr [esp+8]
77E8677F: E803000000 call Kernel32.SleepEx
77E86784: C20400 ret 00004h


就象你看到的,在我們已經拷貝了第1和第2個指令(這裏總共6個字節)和指向下一條指令的跳轉指令後應該是怎樣。這裏我們得假設跳轉指令並不象函數開始的指令那麼放置,否則我們就會遇到問題。下一個問題就是諸如ntdll.DbgBreakPoint這樣的API,它們太短了,所以不能用這種掛鉤的方法。並且它是由Kernel32.DebugBreak調用,所以也不能通過修改IAT來掛鉤。雖然說沒有誰會去掛鉤這個只有int 3的函數,但沒有什麼是做不到的,只要認真想想就能找到解決的方法。我的方法是掛鉤它之後的那個函數(它可能會因爲修改了前一個函數的開始5個字節而被破壞)。DbgBreakPoint函數長度爲2個字節,所以這裏我們可以設置一些標誌,然後試着在第二個函數的開始寫入條件跳轉指令...但這不是我們現在的問題。
保存原始函數的問題已經敘述完了,就到解除掛鉤(unhook)。解除掛鉤就是把被修改的字節恢復爲原始狀態。修改IAT的方法裏,如果你想解除掛鉤的話,你就需要在表裏恢復原始的地址。修改入口點的方法裏,你要做的就是把原始函數的開始指令拷貝回去。兩種做法都很簡單,所以不需要再講了。


=====[ 3.2.4 掛鉤其它進程 ]========================================

現在我們來實踐一下運行中掛鉤。試想,誰會想只掛鉤自己進程?這顯然是非常不實用的。
我來演示3種不同的掛鉤其它進程的方法。其中兩種都使用了CreateRemoteThread這個API,它只在使用了NT技術的Windows版本里有效。對我來說在較老的Windows裏掛鉤沒那麼有趣。忘了說我將介紹的這3個方法我都沒有實踐過,所以可能會出點問題。
先介紹CreateRemoteThread。就象幫助裏說的,這個函數可以在任意進程裏創建新線程並運行它的代碼。

HANDLE CreateRemoteThread(
HANDLE hProcess,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);

句柄hProcess可以通過OpenProcess獲得。這裏我們必須獲得足夠權限。lpStartAddress是指向目標進程地址空間裏存放新線程第一條指令地址的指針,因爲新線程是在目標進程裏創建,所以它存在於目標進程的地址空間裏。lpParameter是指向提交給新線程的參數的指針。


=====[ 3.2.4.1 DLL注入 ]==================================================

我們可以在目標進程地址空間裏任意地方運行我們的新線程。這看起來沒什麼用,除非在裏面有我們完整的代碼。第一種方法就是這麼實現。它調用GetProcAddress獲取LoadLibrary地址。然後把LoadLibrary賦值給參數lpStartAddress。LoadLibrary函數只有一個參數,就和目標進程裏新線程的函數一樣。

HINSTANCE LoadLibrary(
LPCTSTR lpLibFileName
);

我們可以使用這點相似性,把lpParameter參數賦爲我們的DLL庫的名字。在新線程運行後lpParameter的位置就是lpLibFileName的位置。這裏最重要的東西前面已經講過了。在加載了新的模塊到目標進程後就開始執行初始化部分。如果我們在這裏放置了能夠掛鉤其它函數的特殊函數就OK了。在執行了初始化部分後,這個線程就什麼都不做並被關閉,但我們的模塊仍然在地址空間中。這種方法很不錯而且很容易實現,它的名字叫DLL注入。但如果你和我一樣不喜歡還得多個DLL庫的話,請看下面的方法。但如果不介意多個DLL庫的話這確實是最快的方法(從程序員的角度來看)。


=====[ 3.2.4.2 獨立的代碼 ]===============================================

實現獨立的代碼比較困難,但也容易給人深刻印象。獨立的代碼是不需要任何靜態地址的代碼。它裏面所有東西都是互相聯繫地指向代碼裏面某些特定的地方。如果我們不知道這段代碼開始執行的地址它也能自己完成 。當然,也有可能先獲得地址然後重新鏈接我們的代碼這樣它可以完全正常地在新地址工作,但這比編寫獨立的代碼更困難。這類型代碼的例子比方說病毒的代碼。病毒通過這種方法感染可執行文件,它把它自己的代碼加入到可執行文件中的某個地方。在不同的可執行文件中放置病毒代碼的位置也不一樣,這取決於比方說文件結構的長度。
首先將我們的代碼插入目標進程,然後CreateRemoteThread函數就會負責運行我們的代碼。所以第一步我們要做的就是通過OpenProcess函數獲取目標進程的信息和句柄,接着調用VirtualAllocEx在目標進程地址空間裏分配一些內存給我們的代碼,最後調用WriteProcessMemory把我們的代碼寫入分配的內存裏並運行它。調用CreateRemoteThread的參數lpStartAddress設置爲分配的內存地址,lpParameter可以隨便設置。因爲我不喜歡附加什麼不必要的文件所以我使用了這種方法。


=====[ 3.2.4.3 原始修改 ]=====================================================

在非NT內核的老版本Windows裏是沒有CreateRemoteThread函數的,所以我們不能用以上的方法。可能會有比我現在介紹的這種方法好很多的方法,事實上我的這種方法還沒有經過實踐,但理論上來說是可行的。
我們其實根本不需要把我們代碼放到目標進程裏來掛鉤它的函數。有兩個函數WriteProcessMemory和OpenProcess,它們在所有版本的Windows中都有效。我們還需要的函數是VirtualProtectEx,用來修改進入目標進程的內存頁。我找不到任何不直接從我們的進程掛鉤目標進程的的理由...


=====[ 4. 結束語 ]================================================

我歡迎任何人提出更多的這裏沒有提到的掛鉤方法,我肯定那會有很多。同樣歡迎補充我介紹得不是很詳細的方法。也可以把我懶得寫的代碼部分完成,把源代碼發給我。這篇文檔的目的是演示掛鉤技術的細節,我希望我做到了。
特別感謝Z0MBiE的代碼,爲我節省了很多寶貴的時間。

============================[ End ]========================

發佈了31 篇原創文章 · 獲贊 0 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章