程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 導入表內注入代碼(一)

導入表內注入代碼(一)

編輯:關於VC++

(包括:itview.zip (87.1 KB) pemaker6.zip (96.6 KB) pemaker7.zip (193 KB)zimport.zip (130 KB)。譯注:本文代碼可在VS2003及WINDOWSXP+sp2下正常運行,Windows2000下ITview功能有異常。)

本文介紹注入代碼到PE(Portable Executable可移植的執行體)文件格式的Import Table(導入表,也有譯為“引入表”)技術,其也被稱為API重定向技術(API redirection technique)。

讓我們想像一下:如果我們可以通過操作導入表thunks將導入函數的入口點(thoroughfare)重定向到我們的指定的例程,用我們的例程過濾導入(消息)就成為可能。此外,我們可以通過這個功能實現安排給我們適當的例程,專業的Portable Executable (PE) Protectors正是這麼來做的,另外一些種類的rootkits使用這個方法通過一個特洛伊木馬嵌入其惡意代碼到受害者。在反向工程世界裡,我們稱之為:API重定向技術,然而我不准備通過源代碼描述這個領域的所有觀點,本文只是通過一個簡單代碼介紹一下這個技術的概況。我將描述這個源代碼中沒有的其他一些問題;我不能公開這些代碼,原因是其關系到一些商業項目或可能會被懷有惡意者利用,然而我想本文可以被用來作為一個關於該主題的入門。

1.進入導入表

PE文件格式包括:MS-DOS header、NT headers、Sections headers和Section images(譯注:正如很多技術讀物上一樣,其實header可以譯為“頭”,image可譯為“映像”,但本文正文在不影響理解的前提下盡量保留原文術語,以免誤解。)正如你在圖 1中所看到的。MS-DOS header是自DOS時代到Windows時代在所有微軟可執行文件格式(executable file format)公有的。NT headers的思想來源於UNIX系統的Executable and Linkable Format (ELF),當然Portable Executable (PE)格式是Linux Executable and Linkable Format (ELF)的姐妹。PE 格式包括"PE" Signature、Common Object File Format (COFF) header、Portable Executable Optimal header和Section headers。

圖1 - Portable Executable 文件格式結構

NT headers的定義可以在Virtual C++ included 目錄下 <winnt.h>頭文件中找到。該信息可以非常容易地通過使用DbgHelp.dll的ImageNtHeader()函數獲得。你也可以使用DOS header來獲取NT headers,因為DOS header的末尾位置:e_lfanew,代表NT headers的偏移。(譯注:將這個偏移加到內存映射文件的基址上就得到了PE header 的地址:pNTHeader=dosHeader+ dosHeader->e_lfanew;)

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;

在Portable Executable Optional header,有一些數據目錄(data directories)描述了當前進程在虛擬內存中主信息表(the principal information tables)的相對位置和大小。這些表可以有關於資源的信息、import(導入)、export(導出)、relocation(重定位)、Debug(調試)、thread local storage(線程本地存儲)和COM運行時。沒有導入表想要找到一個PE可執行文件是不可能的;該表包含DLL的名稱和Functions(函數)名稱,這些是當程序意圖通過它們的虛地址來請求(調用)它們時所必需的。在Console executable files(控制台可執行文件)中沒有發現資源表(resource table);然而它是擁有Graphic User Interface (GUI)的Windows可執行文件的至關重要的部分。導出表(export table)在一個動態鏈接庫想要導出它的函數到外界時是必需的,並且它也在OLE Active-X容器中。Dot NET虛擬機在沒有COM+ runtime header下時不能被執行。正如你看到的,在PE格式中每個表都有特定的委派任務,圖 2。

圖2 - Data Directories(數據目錄)

Data

Directories

0 Export Table

1Import Table

2 Resource Table

3 Exception Table

4 Certificate File

5 Relocation Table

6 Debug Data

7 Architecture Data

8 Global Ptr

9 Thread Local Storage Table

10 Load Config Table

11 Bound Import Table

12 Import Address Table

13 Delay Import Descriptor

14 COM+ Runtime Header

15 Reserved

// <winnt.h>
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES16
// Optional header format.
typedef struct _IMAGE_OPTIONAL_HEADER {
...
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_TLS9 // TLS Directory

我們只用三兩行代碼就可以獲得導入表的位置和大小。通過知道導入表的位置,我們轉入下一步獲得DLL名稱和Function名稱,它將在下一節裡討論。

PIMAGE_NT_HEADERS pimage_nt_headers = ImageNtHeader(pImageBase);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
PIMAGE_DOS_HEADER pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
PIMAGE_NT_HEADERS pimage_nt_headers = (PIMAGE_NT_HEADERS)
(pImageBase + pimage_dos_header->e_lfanew);
DWORD it_voffset = pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;

2. 導入描述符(Import Descriptor)一瞥

導入表的導入目錄入口(import directory entry)帶我們來到文件image內部的導入表位置。

對於每個被導入的DLL,導入描述符是個容器,它包含了first thunk的地址和original first thunk的地址,DLL名稱的指針。First Thunk引用first thunk的位置,thunks在運行該程序時將會被Windows的PE loader(裝載器)初始化,圖 5。Original First Thunk指向thunks第一個存儲處,該存儲處提供Hint 數據的地址和每個函數的Function Name數據,圖 4。在此情況下,First Original Thunk沒有出現;First Thunks引用Hint 數據和Function Name 數據被定位的位置,圖 3。用IMAGE_IMPORT_DESCRIPTOR來表示導入描述符結構如下定義:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORDOriginalFirstThunk;
DWORDTimeDateStamp;
DWORDForwarderChain;
DWORDName;
DWORDFirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;

成員:

OriginalFirstThunk

它指向first thunk,IMAGE_THUNK_DATA,該thunk擁有Hint和Function name的地址。

TimeDateStamp

如果那裡有綁定的話它包含時間/數據戳(time/data stamp)。如果它是0,就沒有綁定在被導入的DLL中發生。在最近,它被設置為0xFFFFFFFF以表示綁定發生。

ForwarderChain

在老版的綁定中,它引用API的第一個forwarder chain(傳遞器鏈表)。它可被設置為0xFFFFFFFF以代表沒有forwarder。

Name

它表示DLL 名稱的相對虛地址(譯注:相對一個用null作為結束符的ASCII字符串的一個RVA,該字符串是該導入DLL文件的名稱,如:KERNEL32.DLL)。

FirstThunk

它包含由IMAGE_THUNK_DATA定義的 first thunk數組的虛地址,通過loader用函數虛地址初始化thunk。在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。

typedef struct _IMAGE_IMPORT_BY_NAME {
WORDHint;
BYTEName[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA {
union {
PDWORDFunction;
PIMAGE_IMPORT_BY_NAMEAddressOfData;
} u1;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;

圖3 - Import Table View

圖4 - Import Table View (有 Orignal First Thunk)

這裡有兩個導入表(圖3和圖4)表示有和沒有original first thunk下導入表的不同。

圖5-用PE loader重寫後的導入表

我們可以用Dependency Walker,圖 6,來察看導入表的所有信息。順便說一下,我已提供另一個工具,Import Table viewer,圖 7,它簡單且與前面工具的操作相似。通過這類工具我肯定它的資源將幫助你更好地理解它做的主要(功能)表現。

Figure 6 - Dependency Walker, Steve P. Miller

這裡我們看到一個簡單的可以用一個控制台模式程序來顯示導入DLLs和導入Functions的資源。然而,我想我的Import Table viewer,圖 7,因為它的圖形用戶界面更適於來理解主題。

PCHARpThunk;
PCHARpHintName;
DWORDdwAPIaddress;
PCHARpDllName;
PCHARpAPIName;
//----------------------------------------
DWORD dwImportDirectory= RVA2Offset(pImageBase, pimage_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);
//----------------------------------------
PIMAGE_IMPORT_DESCRIPTOR pimage_import_descriptor= (PIMAGE_IMPORT_DESCRIPTOR)
(pImageBase+dwImportDirectory);
//----------------------------------------
while(pimage_import_descriptor->Name!=0)
{
pThunk= pImageBase+pimage_import_descriptor->FirstThunk;
pHintName= pImageBase;
if(pimage_import_descriptor->OriginalFirstThunk!=0)
{
pHintName+= RVA2Offset(pImageBase, pimage_import_descriptor->OriginalFirstThunk);
}
else
{
pHintName+= RVA2Offset(pImageBase, pimage_import_descriptor->FirstThunk);
}
pDllName= pImageBase + RVA2Offset(pImageBase, pimage_import_descriptor->Name);
printf(" DLL Name: %s First Thunk: 0x%x", pDllName,
pimage_import_descriptor->FirstThunk);
PIMAGE_THUNK_DATA pimage_thunk_data= (PIMAGE_THUNK_DATA) pHintName;
while(pimage_thunk_data->u1.AddressOfData!=0)
{
dwAPIaddress= pimage_thunk_data->u1.AddressOfData;
if((dwAPIaddress&0x80000000)==0x80000000)
{
dwAPIaddress&= 0x7FFFFFFF;
printf("Proccess: 0x%x", dwAPIaddress);
}
else
{
pAPIName= pImageBase+RVA2Offset(pImageBase, dwAPIaddress)+2;
printf("Proccess: %s", pAPIName);
}
pThunk+= 4;
pHintName+= 4;
pimage_thunk_data++;
}
pimage_import_descriptor++;
}

圖 7 - Import Table viewer

3. API重定向技術

我們了解了所有關於導入表的基本知識,是時候來建立我們重定向方法的時候了。算法是很簡單的,在當前進程的虛擬內存中創建一個額外虛擬空間,並生成指令用JMP到原始函數位置來重定向。我們可以用絕對jump(跳)或相對jump來實現之。你應該注意絕對jump這種情況,你不能像圖8那樣簡單地實現,你應該首先移動虛地址到EAX然後用JMP EAX做一個jump。在pemaker6.zip中,我已經用相對jump做了一個重定向。

圖 8 -一個用絕對jump指令的簡單API重定向概覽

這個PE maker是在我先前的文章 [1]的成果上創建的,如果你有興趣知道它是如何工作的話,我建議你去看這篇文章。在這個版本中,我已經改進了導入表修改例程,正如你下面看到的這些代碼,我寫了幾行的代碼來生成相對JMP指令到函數的實際位置。你一定要知道,你不能實現所有DLL模塊的API重定向。比如在CALC.EXE中,在運行時初始化時MSVCRT.DLL的一些thunks將會被CALC.EXE內部的一些code 節訪問,因此在重定向的情況下它將不會起作用。

_it_fixup_1:
push ebp
mov ebp,esp
add esp,-14h
push PAGE_READWRITE
push MEM_COMMIT
push 01D000h
push 0
call _jmp_VirtualAlloc
//NewITaddress=VirtualAlloc(NULL, 0x01D000, MEM_COMMIT, PAGE_READWRITE);
mov [ebp-04h],eax
mov ebx,[ebp+0ch]
test ebx,ebx
jz _it_fixup_1_end
mov esi,[ebp+08h]
add ebx,esi// dwImageBase + dwImportVirtualAddress
_it_fixup_1_get_lib_address_loop:
mov eax,[ebx+0ch]// image_import_descriptor.Name
test eax,eax
jz _it_fixup_1_end
mov ecx,[ebx+10h]// image_import_descriptor.FirstThunk
add ecx,esi
mov [ebp-08h],ecx// dwThunk
mov ecx,[ebx]// image_import_descriptor.Characteristics
test ecx,ecx
jnz _it_fixup_1_table
mov ecx,[ebx+10h]
_it_fixup_1_table:
add ecx,esi
mov [ebp-0ch],ecx// dwHintName
add eax,esi// image_import_descriptor.Name + dwImageBase = ModuleName
push eax// lpLibFileName
mov [ebp-10h],eax
call _jmp_LoadLibrary// LoadLibrary(lpLibFileName);
test eax,eax
jz _it_fixup_1_end
mov edi,eax
_it_fixup_1_get_proc_address_loop:
mov ecx,[ebp-0ch]// dwHintName
mov edx,[ecx]// image_thunk_data.Ordinal
test edx,edx
jz _it_fixup_1_next_module
test edx,080000000h// .IF( import by ordinal )
jz _it_fixup_1_by_name
and edx,07FFFFFFFh// get ordinal
jmp _it_fixup_1_get_addr
_it_fixup_1_by_name:
add edx,esi// image_thunk_data.Ordinal + dwImageBase = OrdinalName
inc edx
inc edx// OrdinalName.Name
_it_fixup_1_get_addr:
push edx// lpProcName
push edi// hModule
call _jmp_GetProcAddress// GetProcAddress(hModule, lpProcName);
mov [ebp-14h],eax//_p_dwAPIaddress
//================================================================
//RedirectionEngine
push edi
push esi
push ebx
mov ebx,[ebp-10h]
push ebx
push ebx
call _char_upper
mov esi,[ebp-10h]
mov edi,[ebp+010h]
_it_fixup_1_check_dll_redirected:
push edi
call __strlen
addesp, 4
mov ebx,eax
mov ecx,eax
push edi
push esi
repe cmps
jz_it_fixup_1_do_normal_it_0
pop esi
pop edi
add edi,ebx
cmp byte ptr [edi],0
jnz _it_fixup_1_check_dll_redirected
mov ecx,[ebp-08h]
mov eax,[ebp-014h]
mov [ecx],eax
jmp _it_fixup_1_do_normal_it_1
_it_fixup_1_do_normal_it_0:
pop esi
pop edi
mov edi,[ebp-04h]
mov byte ptr [edi], 0e9h// JMP Instruction
mov eax,[ebp-14h]
sub eax, edi
sub eax, 05h
mov [edi+1],eax// Relative JMP value
mov word ptr [edi+05], 0c08bh
mov ecx,[ebp-08h]
mov [ecx],edi// -> Thunk
add dword ptr [ebp-04h],07h
_it_fixup_1_do_normal_it_1:
pop ebx
pop esi
pop edi
//================================================================
add dword ptr [ebp-08h],004h// dwThunk => next dwThunk
add dwordptr [ebp-0ch],004h// dwHintName => next dwHintName
jmp _it_fixup_1_get_proc_address_loop
_it_fixup_1_next_module:
add ebx,014h// sizeof(IMAGE_IMPORT_DESCRIPTOR)
jmp _it_fixup_1_get_lib_address_loop
_it_fixup_1_end:
mov esp,ebp
pop ebp
ret 0ch

不要認為API重定向是用這個簡單方法在專業EXE protectors(保護器)中被搞定的;他們有一個x86指令生成器引擎,它被用來生成重定向意圖的代碼。有時這個引擎是和metamorphism(混淆或擾亂)引擎一同使用的,這可以使得他們極為復雜難於分析。

它是如何工作的?

前面的代碼是依照下面的算法工作的:

1.用VirtualAlloc()創建一塊獨立空間來存放生成的指令。

2.用LoadLibrary()和GerProcAddress()找到函數虛地址

3.檢查DLL名稱是否在有效DLL表單中。在這個例子中,我們認為KERNEL32.DLL、USER32.DLL、GDI32.DLL、ADVAPI32.DLL和SHELL32.DLL是可以重定向的有效DLL名稱。

4.如果DLL名稱有效,轉入重定向例程,另外用original function虛地址初始化這個thunk。

5.為了重定向API,生成JMP (0xE9)指令,計算function position的相對位置來建立一個相對jump。

6.存儲生成指令到獨立的空間,並引用thunk到這些指令的首位置。

7.繼續對其他Functions 和DLLs運行這個例程。

如果你在CALC.EXE上實現這個技術,並用OllyDbg或一個相似的用戶模式調試器跟蹤它,你會覺得這個代碼生成了和下面視圖相似的一個視圖:

008E0000- E9 E6F8177CJMP SHELL32.ShellAboutW
008E00058BC0MOV EAX,EAX
008E0007- E9 0F764F77JMP ADVAPI32.RegOpenKeyExA
008E000C8BC0MOV EAX,EAX
008E000E- E9 70784F77JMP ADVAPI32.RegQueryValueExA
008E00138BC0MOV EAX,EAX
008E0015- E9 D66B4F77JMP ADVAPI32.RegCloseKey
008E001A8BC0MOV EAX,EAX
008E001C- E9 08B5F27BJMP kernel32.GetModuleHandleA
008E00218BC0MOV EAX,EAX
008E0023- E9 4F1DF27BJMP kernel32.LoadLibraryA
008E00288BC0MOV EAX,EAX
008E002A- E9 F9ABF27BJMP kernel32.GetProcAddress
008E002F8BC0MOV EAX,EAX
008E0031- E9 1AE4F77BJMP kernel32.LocalCompact
008E00368BC0MOV EAX,EAX
008E0038- E9 F0FEF27BJMP kernel32.GlobalAlloc
008E003D8BC0MOV EAX,EAX
008E003F- E9 EBFDF27BJMP kernel32.GlobalFree
008E00448BC0MOV EAX,EAX
008E0046- E9 7E25F37BJMP kernel32.GlobalReAlloc
008E004B8BC0MOV EAX,EAX
008E004D - E9 07A8F27BJMP kernel32.lstrcmpW
008E00528BC0MOV EAX,EAX

留給你一個家庭作業:你可以用下面這個代碼實踐一下以絕對jump指令改變PE Maker source。

008E0000- B8 EBF8A57CMOV EAX,7CA5F8EBh // address of SHELL32.ShellAboutW
008E0005FFE0JMP EAX

本文配套源碼

  1. 上一頁:
  2. 下一頁:
欄目導航
Copyright © 程式師世界 All Rights Reserved