导入表内注入代码(一)
作者: Ashkbiz Danehkar
翻译:小刀人
下载源代码(包括: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 | 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
导入表内注入代码(二)