PE
文件导入表的代码注入
试想一下,如果通过修改导入表,能把PE格式文件中的函数入口点,重定向到自己的程序中来,是不是很酷!这样,在自己在程序中,可以过滤掉对某些函数的调用,或者,设置自己的处理程序,Professional Portable Executable (PE) Protector也就是这样做的。另外,某些rootkit也使用了此方法把恶意代码嵌入到正常程序中。在逆向工程的概念里,均称为API重定向技术,让我们一起进入这个神奇的世界吧。
1
、导入表简介
PE文件由MS-DOS头、NT头、节头、节映像组成,如图1所示。MS-DOS头在从DOS至现今Windows的所有微软可执行文件中都存在;NT头的概念抽象自Unix系统的可执行与链接文件格式(ELF)。实际上,PE格式可以说是Linux可执行与链接格式(ELF)的兄弟,PE格式头由PE签名、通用对象文件格式(COFF)头、PE最优化头、节头组成。
图1:PE文件格式结构
NT头的定义可在Visual C++的<winnt.h>头文件中找到,也可使用DbgHelp.dll中的ImageNtHeader()函数得到相关信息,另外,还可以使用DOS头来获取NT头,因为DOS头的最后位置e_lfanew,代表了NT头的偏移量。
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
在PE最优化头中,有一些数据目录指明了当前进程虚拟内存中,主信息表的相对位置及大小,这些表可保存资源、导入、导出、重定位、调试、线程本地存储及COM运行时的有关信息。没有导入表的PE可执行文件是不可能存在的,这张表包含了DLL名及函数名,这些都是程序通过虚拟地址调用它们时所必不可少的;控制台可执行文件中没有资源表,然而,对图形用户界面的Windows可执行文件来说,资源表却是至关重要的部分;当某个动态链接库导出了它的函数,此时就需要导出表了,在OLE Active-X容器中也同样;而.NET虚拟机缺少了COM+运行时头则不能被执行。PE格式的详细说明见表1:
数据目录
|
0 Export Table(导出表)
|
1 Import 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(COM+运行时头)
|
15 Reserved(保留)
|
表1:数据目录
// <winnt.h>
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES
16
//可选头格式
typedef struct _IMAGE_OPTIONAL_HEADER {
...
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
//目录项
#define IMAGE_DIRECTORY_ENTRY_EXPORT
0 //导出目录
#define IMAGE_DIRECTORY_ENTRY_IMPORT
1 //导入目录
#define IMAGE_DIRECTORY_ENTRY_RESOURCE
2 //资源目录
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 //基重定位表
#define IMAGE_DIRECTORY_ENTRY_DEBUG
6 //调试目录
#define IMAGE_DIRECTORY_ENTRY_TLS
9 //TLS目录
可通过简单的几行代码获得导入表的位置及大小,知道了导入表的位置后,就可知道DLL名及函数名了,这将在后面进行讨论。
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
、导入描述符概览
通过导入表的导入目录项,可获知文件映像内部导入表的位置,且对每个导入的DLL、导入描述符,都有一个相应的容器,其包含了首个thunk(转换程序)地址、原始首个thunk的地址,还有指向DLL名的指针。FirstThunk指向首个thunk的位置,这些thunk在程序运行时,由Windows的PE加载器初始化,如图5所示。OriginalFirstThunk指向这些thunk的第一个存储位置,对每个函数而言,这也是提供Hint(提示)数据地址及函数名数据之处,见图4。在本例中,OriginalFirstThunk不存在,而FirstThunk则指向了提示数据及函数名数据位置之处,见图3。
IMAGE_IMPORT_DESCRIPTOR结构代表了导入描述符,以下是其定义:
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD
OriginalFirstThunk;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
OriginalFirstThunk指向第一个thunk(IMAGE_THUNK_DATA),thunk中保存了提示数据地址及函数名。
TimeDateStamp包含了绑定的时间日期戳,如果它为0,表示在导入的DLL没有任何绑定。在将来,会设为0xFFFFFFFF以表明有绑定。
ForwarderChain指向API的第一个转发链,设为0xFFFFFFFF表示没有转发。
Name指明了DLL名的相对虚拟地址。
FirstThunk包含了由IMAGE_THUNK_DATA定义的首个thunk数组的虚拟地址,而thunk由加载器用函数虚拟地址初始化。如果OrignalFirstThunk不存在,它指向了第一个thunk、提示(Hint)thunk及函数名。
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA {
union {
PDWORD Function;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;
图2:导入表一览
图3:带有Orignal First Thunk的导入表视图
这两张导入表(图2、3)清楚地说明了带有及不带Orignal First Thunk时的区别。
图4:被PE加载器覆写后的导入表
可用Dependency Walker,见图5,来查看导入表的所有信息,另外,还有一个小工具Import Table viewer,见图6,也可用来查看此类信息。
图5:Dependency Walker——Visual Studio自带的工具
图6:Import Table viewer
另外,也可利用下面这段代码,在自己的程序中显示导入DLL及导入函数(只适用于控制台模式的程序)。
PCHAR
pThunk;
PCHAR
pHintName;
DWORD
dwAPIaddress;
PCHAR
pDllName;
PCHAR
pAPIName;
//----------------------------------------
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++;
}
3
、API
重定向技术
在了解了导入表的所有基础知识后,现在就要来看看重定向方法了,其算法非常简单,在当前进程的虚拟内存内部创建一个额外的虚拟空间,并生成相应指令通过JMP重定向到原始函数位置。在这里,可使用绝对跳转或相对跳转,在使用绝对跳转时要非常小心,不能像图7中那么简单地进行,应首先把虚拟地址存到EAX中,接着由JMP EAX跳转。
图7:绝对跳转指令的API重定向
当然了,也可以使用相对跳转,下面的代码就是这样做的,但要清楚,不能对所有DLL模块都进行API重定向,比如,拿“计算器”CALC.EXE来说,MSVCRT.DLL的某些thunk在运行时初始化期间,是从CALC.EXE代码节内部访问的,因此,重定向的话则不能正常工作。
_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 //是否按顺序导入
jz _it_fixup_1_by_name
and edx,07FFFFFFFh //取得顺序
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
//=========================================================
// 重定向引擎
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
add esp, 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指令
mov eax,[ebp-14h]
sub eax, edi
sub eax, 05h
mov [edi+1],eax //相对JMP值
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 dword ptr [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
说点题外话,千万不要认为这样就可以绕过Professional EXE Protector了,这个软件中有一套自己的x86指令生成引擎用于创建重定向代码,有时,这个引擎还带有一个扰乱变形引擎,以使它复杂到难以跟踪分析。
在工作原理上,上述代码依照了下列算法:
1、 创建一个单独的空间以存储由VirtualAlloc()生成的指令。
2、 通过LoadLibrary()和GerProcAddress()找到函数的虚拟地址。
3、 检查DLL名是否匹配有效DLL列表。在本例中,识别出KERNEL32.DLL、USER32.DLL、GDI32.DLL、ADVAPI32.DLL、SHELL32.DLL为可重定向的有效DLL名。
4、 如果DLL名有效,转到重定向部分;否则,用原始函数虚拟地址初始化thunk。
5、 为重定向API,生成JMP(0xE9)指令并计算函数的相对地址以确定相对跳转。
6、 把生成的指令存储在单独的空间中,并把thunk引用为这些指令的首地址。
7、 对其他函数及DLL重复以上步骤。
如果对CALC.EXE执行了以上步骤,并以OllyDbg进行跟踪,将会看到类似以下的代码:
008E0000 - E9 E6F8177C
JMP SHELL32.ShellAboutW
008E0005
8BC0 MOV EAX,EAX
008E0007 - E9 0F764F77
JMP ADVAPI32.RegOpenKeyExA
008E000C
8BC0 MOV EAX,EAX
008E000E - E9 70784F77
JMP ADVAPI32.RegQueryValueExA
008E0013
8BC0 MOV EAX,EAX
008E0015 - E9 D66B4F77
JMP ADVAPI32.RegCloseKey
008E001A
8BC0 MOV EAX,EAX
008E001C - E9 08B5F27B
JMP kernel32.GetModuleHandleA
008E0021
8BC0 MOV EAX,EAX
008E0023 - E9 4F1DF27B
JMP kernel32.LoadLibraryA
008E0028
8BC0 MOV EAX,EAX
008E002A - E9 F9ABF27B
JMP kernel32.GetProcAddress
008E002F
8BC0 MOV EAX,EAX
008E0031 - E9 1AE4F77B
JMP kernel32.LocalCompact
008E0036
8BC0 MOV EAX,EAX
008E0038 - E9 F0FEF27B
JMP kernel32.GlobalAlloc
008E003D
8BC0 MOV EAX,EAX
008E003F - E9 EBFDF27B
JMP kernel32.GlobalFree
008E0044
8BC0 MOV EAX,EAX
008E0046 - E9 7E25F37B
JMP kernel32.GlobalReAlloc
008E004B
8BC0 MOV EAX,EAX
008E004D - E9 07A8F27B
JMP kernel32.lstrcmpW
008E0052
8BC0 MOV EAX,EAX
而绝对跳转时的代码如下:
008E0000 - B8 EBF8A57C
MOV EAX,7CA5F8EBh
//SHELL32.ShellAboutW的地址
008E0005
FFE0 JMP EAX
下面,就要用重定向技术来改变某个API的功能了,在本例中,将把CALC.EXE的ShellAbout()对话框重定向到“Hello World!”消息框,且只需对前述代码作稍许改动:
...
//==============================================================
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] // [ebp+_p_szShell32]
_it_fixup_1_check_dll_redirected:
push edi
call __strlen
add esp, 4
mov ebx,eax
mov ecx,eax
push edi
push esi
repe cmps // byte ptr [edi], byte ptr [esi]
jz _it_fixup_1_check_func_name
jmp _it_fixup_1_no_check_func_name
_it_fixup_1_check_func_name:
mov edi,[ebp+014h] // [ebp+_p_szShellAbout]
push edi
call __strlen
add esp, 4
mov ecx,eax
mov esi,[ebp-18h]
mov edi,[ebp+014h] // [ebp+_p_szShellAbout]
repe cmps //byte ptr [edi], byte ptr [esi]
jz _it_fixup_1_do_normal_it_0
_it_fixup_1_no_check_func_name:
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 ecx,[ebp-08h]
mov edi,[ebp+18h]
mov [ecx],edi //把新函数的地址存到thunk中
_it_fixup_1_do_normal_it_1:
pop ebx
pop esi
pop edi
//==============================================================
...
先检查DLL是否为“Shell32.dll”,再检查函数名是否为“ShellAboutW”,如果这两个条件为true,把ShellAbout()的thunk重定向到新的函数。
新的函数只是一个简单的消息框:
_ShellAbout_NewCode:
_local_0:
pushad //把寄存器上下文保存在堆栈中
call _local_1
_local_1:
pop ebp
sub ebp,offset _local_1 //得到基ebp
push MB_OK | MB_ICONINFORMATION
lea eax,[ebp+_p_szCaption]
push eax
lea eax,[ebp+_p_szText]
push eax
push NULL
call _jmp_MessageBox
// MessageBox(NULL, szText, szCaption, MB_OK | MB_ICONINFORMATION) ;
popad //从堆栈中恢复第一个寄存器上下文
ret 10h
当想要用一个新的函数取代某个API时,应考虑以下两个重点部分:
一是不要因丢失堆栈点而破坏了堆栈内存(Stack memory),因此,最终必需以ADD ESP,xxx或RET xxx恢复原始堆栈点。
二是通过PUSHAD、POPAD,尽量保证除了EAX之外的大多数线程寄存器的安全。
大家可以看到,代码中也使用了PUSHAD及POPAD回收线程寄存器,对本例来说,ShellAbout()有4个DWORD成员,因此当返回时,堆栈点增长了0x10。
重定向ShellAbout()之后,点击计算器(CALC.EXE)“帮助”菜单中的“关于计算器”,就可看到结果了。
图8:“关于计算器”被重定向到一个消息框
4
、保护防止逆向工程
要使用复杂的API重定向技术来重新构造一个导入表简直是难上加难,有时,像Import REConstructor这样的工具(图10)在重建导入表时也会被搞糊涂,尤其是当重定向是通过多态代码映像来实现时。在逆向工程界,Import REConstructor是一个非常有名的工具,它会挂起目标进程以捕捉导入表信息。如果是像简单的JMP这样实现的重定向,使用此工具当然可以被重建,不过,如果加密了函数名并在内存中与多态变形代码进行绑定,它就不能得到正确的导入表了。Native Security Engine 6就是这样的一个打包器,它有一个x86代码生成器外加一个扰乱变形引擎,两者都有助于实现一个复杂的重定向结构。
图9:Import REConstructor
图10中列明了导入表保护的主要策略,它们中有些重定向至虚拟的Win32库,例如,分别有Kernel32、User32、AdvApi32的虚拟库,使用自己的库可防止被他人破解或安装自己的虚拟机。
图10:导入表保护
利用此方法,也可切断对外界的访问,如MoleBox就是这样,它过滤了FindFirstFile()及FindNextFile()以把TEXT及JPEG文件合并在压缩文件内部,当程序找到硬盘上的一个文件时,它将会被重定向到内存中。
5
、导入表的运行时注入
在这一节,最重要的问题是,怎样注入到运行时进程的导入表中呢,其实可通过重写内存数据及从外部重定向导入表来实现。
(1)、WindowFromPoint()可获取特定点的窗口句柄,GetWindowThreadProcessId()可有助于了解此窗口句柄的进程ID及线程ID。
POINT point;
HWND hWindowUnderTheMouse = WindowFromPoint(point);
DWORD
dwProcessId;
DWORD
dwThreadId;
dwThreadId=GetWindowThreadProcessId(hSeekedWindow, &dwProcessId);
(2)、进程及线程句柄可由OpenProcess()及OpenThread()获得。
HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE,
dwProcessId );
HANDLE hThread = OpenThread( THREAD_ALL_ACCESS, FALSE,
dwThreadId);
(3)、要操纵进程内存,先要挂起主线程来冻结进程。
SuspendThread(hThread);
(4)、线程环境块(Thread Environment Block TEB)的位置可由FS:[18]获得,而使用GetThreadContext()与GetThreadSelectorEntry()则可以知道FS段的基址值。
CONTEXT
Context;
LDT_ENTRY
SelEntry;
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hThread,&Context);
//计算FS的基址
GetThreadSelectorEntry(hThread, Context.SegFs, &SelEntry);
DWORD dwFSBase = ( SelEntry.HighWord.Bits.BaseHi << 24) |
(SelEntry.HighWord.Bits.BaseMid << 16) |
SelEntry.BaseLow;
(5)、线程环境块可在目标进程虚拟内存内部读取获得,线程与进程环境块的具体形式,请见图11。
PTEB pteb = new TEB;
PPEB ppeb = new PEB;
DWORD
dwBytes;
ReadProcessMemory( hProcess, (LPCVOID)dwFSBase, pteb,
sizeof(TEB), &dwBytes);
ReadProcessMemory( hProcess, (LPCVOID)pteb->Peb, ppeb,
sizeof(PEB), &dwBytes);
图11:线程环境块(Thread Environment Block)及进程环境块(Process Environment Block)
(6)、当前进程内存的PE映像的映像基址可由进程环境块中信息获得。
DWORD dwImageBase = (DWORD)ppeb->ImageBaseAddress;
(7)、ReadProcessMemory()可用于读取PE文件的整个映像。
PIMAGE_DOS_HEADER pimage_dos_header = new IMAGE_DOS_HEADER;
PIMAGE_NT_HEADERS pimage_nt_headers = new IMAGE_NT_HEADERS;
ReadProcessMemory( hProcess,
(LPCVOID)dwImageBase,
pimage_dos_header,
sizeof(IMAGE_DOS_HEADER),
&dwBytes);
ReadProcessMemory( hProcess,
(LPCVOID)(dwImageBase+pimage_dos_header->
e_lfanew),
pimage_nt_headers, sizeof(IMAGE_NT_HEADERS),
&dwBytes);
PCHAR pMem = (PCHAR)GlobalAlloc(
GMEM_FIXED | GMEM_ZEROINIT,
pimage_nt_headers->
OptionalHeader.SizeOfImage);
ReadProcessMemory( hProcess,
(LPCVOID)(dwImageBase),
pMem,
pimage_nt_headers->
OptionalHeader.SizeOfImage,
&dwBytes);
(8)、查找DLL名及thunk值确定目标以便对它进行重定向,在本例中,DLL名是Shell32.dll,而thunk是ShellAbout()的虚拟地址。
HMODULE hModule = LoadLibrary("Shell32.dll");
DWORD dwShellAbout= (DWORD)GetProcAddress(hModule, "ShellAboutW");
DWORD dwRedirectMem = (DWORD)VirtualAllocEx(
hProcess,
NULL,
0x01D000,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
RedirectAPI(pMem, dwShellAbout, dwRedirectMem);
...
int RedirectAPI(PCHAR pMem, DWORD API_voffset,
DWORD NEW_voffset)
{
PCHAR pThunk;
PCHAR pHintName;
DWORD dwAPIaddress;
PCHAR pDllName;
DWORD dwImportDirectory;
DWORD dwAPI;
PCHAR pImageBase = pMem;
//----------------------------------------
PIMAGE_IMPORT_DESCRIPTOR pimage_import_descriptor;
PIMAGE_THUNK_DATA pimage_thunk_data;
//----------------------------------------
PIMAGE_DOS_HEADER pimage_dos_header;
PIMAGE_NT_HEADERS pimage_nt_headers;
pimage_dos_header = PIMAGE_DOS_HEADER(pImageBase);
pimage_nt_headers = (PIMAGE_NT_HEADERS)
(pImageBase+pimage_dos_header-> e_lfanew);
//----------------------------------------
dwImportDirectory=pimage_nt_headers->OptionalHeader
.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
.VirtualAddress;
if(dwImportDirectory==0)
{
return -1;
}
//----------------------------------------
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+=pimage_import_descriptor->
OriginalFirstThunk;
}
else
{
pHintName+=pimage_import_descriptor->FirstThunk;
}
pDllName=pImageBase+pimage_import_descriptor->Name;
StrUpper(pDllName);
if(strcmp(pDllName,"SHELL32.DLL")==0)
{
pimage_thunk_data=PIMAGE_THUNK_DATA(pHintName);
while(pimage_thunk_data->u1.AddressOfData!=0)
{
//----------------------------------------
memcpy(&dwAPI, pThunk, 4);
if(dwAPI==API_voffset)
{
memcpy(pThunk, &NEW_voffset, 4);
return 0;
}
//----------------------------------------
pThunk+=4;
pHintName+=4;
pimage_thunk_data++;
}
}
pimage_import_descriptor++;
}
//----------------------------------------
return -1;
}
(9)、重定向中的额外内存由VirtualProtectEx()分配,在此只是生成代码,并把它写到新的空间中。
DWORD dwRedirectMem = (DWORD)VirtualAllocEx(
hProcess,
NULL,
0x01D000,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
...
PCHAR pLdr;
DWORD Ldr_rsize;
GetLdrCode(pLdr, Ldr_rsize);
WriteProcessMemory( hProcess,
(LPVOID)(dwRedirectMem),
pLdr,
Ldr_rsize,
&dwBytes);
(10)、额外的内存中是加载代码,它显示一个消息框。
void GetLdrCode(PCHAR &pLdr, DWORD &rsize)
{
HMODULE hModule;
DWORD dwMessageBox;
PCHAR ch_temp;
DWORD dwCodeSize;
ch_temp=(PCHAR)DWORD(ReturnToBytePtr(DynLoader,
DYN_LOADER_START_MAGIC))+4;
dwCodeSize=DWORD(ReturnToBytePtr(DynLoader,
DYN_LOADER_END_MAGIC))-DWORD(ch_temp);
rsize= dwCodeSize;
pLdr = (PCHAR)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT, dwCodeSize);
memcpy(pLdr, ch_temp, dwCodeSize);
ch_temp=(PCHAR)ReturnToBytePtr(pLdr,
DYN_LOADER_START_DATA1);
hModule = LoadLibrary("User32.dll");
dwMessageBox= (DWORD)GetProcAddress(hModule, "MessageBoxA");
memcpy(ch_temp+4, &dwMessageBox, 4);
}
...
_ShellAbout_NewCode:
_local_0:
pushad //在堆栈中保存寄存器上下文
call _local_1
_local_1:
pop ebp
sub ebp,offset _local_1 //取得ebp基址
push MB_OK | MB_ICONINFORMATION
lea eax,[ebp+_p_szCaption]
push eax
lea eax,[ebp+_p_szText]
push eax
push NULL
mov eax, [ebp+_p_MessageBox]
call eax
// MessageBox(NULL, szText, szCaption,
/ MB_OK | MB_ICONINFORMATION) ;
popad //从堆栈中恢复第一个寄存器上下文
ret 10h
...
(11)、在修改之后,可执行映像就写到内存中了,记得在写之前,不要忘了把内存设为完全访问。
VirtualProtectEx( hProcess,
(LPVOID)(dwImageBase),
pimage_nt_headers->
OptionalHeader.SizeOfImage,
PAGE_EXECUTE_READWRITE,
&OldProtect);
WriteProcessMemory( hProcess,
(LPVOID)(dwImageBase),
pMem,
pimage_nt_headers->
OptionalHeader.SizeOfImage,
&dwBytes);
VirtualProtectEx()设置页访问为PAGE_EXECUTE_READWRITE保护类型,当使用WriteProcessMemory()且可执行页中有PAGE_EXECUTE时,必须有PAGE_READWRITE访问权限。
(12)、现在,可以释放前面挂起的进程了,点击“关于”菜单,将会看到如图12所示的界面。
ResumeThread(hThread);
图12:ShellAbout() thunk的运行时注入
6
、对程序的挂钩
利用此方法,可挂钩所有Windows控件并过滤API,也就是通常所说的用户级rootkit,下面就来看一下对Yahoo Messenger的挂钩是怎样实现的。
(1)、使用FindWindow()通过类名取得Yahoo Messenger的句柄。
HWND hWnd = FindWindow("YahooBuddyMain", NULL);
(2)、用前面描述的方法实现对进程的注入。
(3)、在GetDlgItemText()的导入thunk上进行注入,以过滤它的参数。
UINT GetDlgItemText( HWND hDlg,
int nIDDlgItem,
LPTSTR lpString,
int nMaxCount);
(4)、比较对话框ID、nIDDlgItem,以确定当前哪个项目在使用,找到之后,对原始GetDlgItemText()进行挂钩。
CHAR pYahooID[127];
CHAR pPassword[127];
switch(nIDDlgItem)
{
case 211:
// Yahoo ID
//
GetDlgItemText(hDlg, nIDDlgItem, pYahooID, 127);
// ...
//
GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
break;
case 212:
//密码
//
GetDlgItemText(hDlg, nIDDlgItem, pPassword, 127);
// ...
//
GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
break;
default:
//
GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);
}
图13:对Yahoo Messenger的挂钩
7
、结论
导入表是Windows可执行文件中非常重要的部分,相关知识可助于理解程序在运行时,是怎样请求某个API的。你也可重定向导入表到当前进程的另一个可执行内存中,通过自己的PE加载器防止别人对程序进行逆向工程,或者仅是挂钩API函数;也可从外部在运行时挂起某个进程从而修改它的导入表。更进一步来说,在理解了这个概念之后,还可以创建自己的虚拟机,在Windows或Liunx内部的一个单独环境中,运行Windows可执行文件,也就是说,甚至可以不需要Windows就能运行Windows EXE文件了。