导入表中的注入代码






4.95/5 (114投票s)
介绍将代码注入可移植可执行文件格式的导入表,这被称为API重定向技术。
- 导入表查看器 - 87.1 KB
- PE Maker 用于重定向API(JMP) - 96.6 KB
- PE Maker 用于重定向 ShellAbout() - 193 KB
- 导入表运行时重定向器 - 130 KB
目录
让我们设想一下,通过操纵导入表的调接器(thunks),我们可以将导入函数的入口重定向到我们特殊的例程,这样就可以通过我们的例程来过滤导入的请求。此外,我们还可以通过这种方式来设置我们合适的例程,专业的可移植可执行文件(PE)保护器就是这样做的,此外,某些类型的Rootkit也利用这种方法通过特洛伊木马将恶意代码嵌入受害者系统。
在逆向工程领域,我们称之为“API重定向技术”。尽管我不会提供该领域所有观点的源代码,但本文仅通过一个简单的代码片段来介绍这项技术的简要方面。我将在没有源代码的情况下描述其他问题;我无法发布与商业项目相关或有意图恶意动机的代码,但我认为本文可以作为该主题的入门介绍。
1. 注入导入表
可移植可执行文件结构由MS-DOS头、NT头、节头和节镜像组成,您可以在图1中看到。MS-DOS头是Microsoft从DOS时代到Windows时代所有可执行文件格式的通用部分。NT头的思想是从UNIX系统的可执行与链接格式(ELF)中抽象出来的,事实上,可移植可执行文件(PE)格式与Linux的可执行与链接格式(ELF)是姊妹格式。PE格式头由“PE”签名、通用对象文件格式(COFF)头、可移植可执行文件可选头和节头组成。
图1 - 可移植可执行文件格式结构
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;
在可移植可执行文件可选头中,有一些数据目录,它们描述了当前进程虚拟内存中主要信息表的相对位置和大小。这些表可以包含资源、导入、导出、重定位、调试、线程局部存储和COM运行时的信息。没有导入表是找不到PE可执行文件的;这个表包含DLL名称和函数名称,当程序需要通过其虚拟地址请求它们时,这些信息非常重要。资源表在控制台可执行文件中找不到;然而,它对于带有图形用户界面(GUI)的Windows可执行文件来说是至关重要的部分。当动态链接库倾向于向外导出其函数时,以及在OLE Active-X容器中,导出表是必需的。 .NET虚拟机无法在没有COM+运行时头文件的陪同下执行。正如您所见的,每个表在PE格式中都有其特殊的作用,图2。
图2 - 数据目录
Data |
0 导出表 |
1 导入表 | |
2 资源表 | |
3 异常表 | |
4 证书文件 | |
5 重定位表 | |
6 调试数据 | |
7 架构数据 | |
8 全局指针 | |
9 线程局部存储表 | |
10 加载配置表 | |
11 绑定导入表 | |
12 导入地址表 | |
13 延迟导入描述符 | |
14 COM+ 运行时头 | |
15 保留 |
// <winnt.h>
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
// 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_EXPORT 0 // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
我们只需两三行代码就可以获得导入表的位置和大小。知道了导入表的位置,我们就可以进入下一步,获取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都有一个容器,即导入描述符,它包含第一个调接器的地址和第一个原始调接器的地址,以及指向DLL名称的指针。First Thunk 指向第一个调接器的位置;调接器将在程序运行时由Windows的PE加载器初始化,图5。Original First Thunk 指向调接器的第一个存储位置,该位置提供了每个函数的Hint数据和函数名称数据的地址,图4。如果Original First Thunk不存在,First Thunks将指向Hint数据和函数名称数据所在的位置,图3。
导入描述符由IMAGE_IMPORT_DESCRIPTOR
结构表示,定义如下:
ypedef struct _IMAGE_IMPORT_DESCRIPTOR {
DWORD OriginalFirstThunk;
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
成员
- OriginalFirstThunk
它指向第一个调接器IMAGE_THUNK_DATA
,调接器包含Hint和函数名称的地址。 - TimeDateStamp
如果存在绑定,它包含时间/日期戳。如果为0,则未发生绑定的导入DLL。在新版本中,它被设置为0xFFFFFFFF来表示发生了绑定。 - ForwarderChain
在旧版本的绑定中,它充当API第一个转发链的引用。它可以设置为0xFFFFFFFF来表示没有转发。 - 名称
它显示DLL名称的相对虚拟地址。 - FirstThunk
它包含由IMAGE_THUNK_DATA
定义的第一个调接器数组的虚拟地址,调接器由加载器用函数虚拟地址初始化。在Original First Thunk不存在的情况下,它指向第一个调接器,即Hint和函数名称的调接器。
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;
图3 - 导入表视图
图4 - 带有Original First Thunk的导入表视图
这两个导入表(图3和图4)展示了带有和不带Original First Thunk的导入表之间的区别。
图5 - PE加载器覆盖后的导入表
我们可以使用Dependency Walker(图6)来查看导入表的全部信息。顺便说一句,我提供了一个名为“导入表查看器”的工具(图7),操作简单且功能相似。我相信它的源代码将帮助您更好地理解这类设备的主要表示方式。
图6 - Dependency Walker, Steve P. Miller
在这里,我们观察到一个简单的源代码,可以用于通过控制台程序显示导入的DLL和导入的函数。然而,我认为我的“导入表查看器”(图7)由于其图形用户界面,对理解这个主题更有启发性。
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++;
}
图7 - 导入表查看器
3. API重定向技术
我们已经掌握了关于导入表的所有必要知识,现在是时候建立我们的重定向方法了。算法非常简单,在当前进程的虚拟内存中创建一个额外的虚拟空间,并生成指令以JMP
重定向到原始函数位置。我们可以通过绝对跳转或相对跳转来实现。您应该注意绝对跳转的情况,您不能像图8那样简单地执行它,您需要先将虚拟地址移动到EAX
,然后通过JMP EAX
跳转。在pemaker6.zip中,我通过相对跳转实现了一个重定向。
图8 - 通过绝对跳转指令进行简单API重定向的概述
这个PE Maker 是在我之前的文章[1]的基础上创建的,如果您有兴趣了解它是如何工作的,我建议您阅读它。在这个版本中,我修改了导入表的修复例程,如以下几行所示,我编写了一些行来生成跳转到函数真实位置的相对JMP
指令。重要的是要知道,您不能对所有DLL模块执行API重定向。例如,在CALC.EXE中,MSVCRT.DLL的一些调接器将在运行时初始化期间从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 // .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
//================================================================
// Redirection Engine
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 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 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
不要以为API重定向在专业的EXE保护器中只是这种简单的方法,它们拥有一个x86指令生成引擎,用于创建重定向目的的代码。有时这个引擎还会伴随一个多态引擎,这使得它们分析起来极其复杂。
它是如何工作的?
前面的代码按照以下算法工作:
-
使用
VirtualAlloc()
创建一个单独的空间来存储生成的指令。 -
使用
LoadLibrary()
和GerProcAddress()
查找函数的虚拟地址。 -
检查DLL名称是否与有效DLL列表匹配。在此示例中,我们将KERNEL32.DLL、USER32.DLL、GDI32.DLL、ADVAPI32.DLL和SHELL32.DLL识别为可重定向的有效DLL名称。
-
如果DLL名称有效,则转到重定向例程;否则,用原始函数虚拟地址初始化调接器。
-
要重定向API,生成
JMP
(0xE9)指令,计算函数位置的相对位置,以便建立相对跳转。 -
将生成的指令存储在单独的空间中,并将调接器指向这些指令的第一个位置。
-
对其他函数和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
作为家庭作业,您可以使用此代码通过绝对跳转指令修改PE Maker源代码。
008E0000 - B8 EBF8A57C MOV EAX,7CA5F8EBh // address of SHELL32.ShellAboutW
008E0005 FFE0 JMP EAX
你称之为?
这次,我想用这种技术改变API的功能。我不确定是否还可以称之为“API重定向”。在这个例子中,我将CALC.EXE的ShellAbout()
对话框重定向到我的“Hello World!”消息框,在pemaker7.zip中。您将看到通过对以下代码进行一些更改,实现起来多么容易。
...
//==============================================================
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 // move address of new function to the thunk
_it_fixup_1_do_normal_it_1:
pop ebx
pop esi
pop edi
//==============================================================
...
我将此例程逐步总结如下:
-
检查DLL名称是否为
"Shell32.DLL"
。 -
检查函数名称是否为
"ShellAboutW"
。 -
如果条件1和2为真,则将
ShellAbout()
的调接器重定向到新函数。
这个新函数是一个简单的消息框。
_ShellAbout_NewCode:
_local_0:
pushad // save the registers context in stack
call _local_1
_local_1:
pop ebp
sub ebp,offset _local_1 // get base 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 // restore the first registers context from stack
ret 10h
当您计划用新函数替换API时,您应该考虑一些重要提示:
- 不要因为丢失堆栈指针而破坏堆栈内存。因此,最终有必要通过
ADD ESP,xxx
或RET xxx
恢复原始堆栈指针。 - 尝试保护大多数线程寄存器(除了
EAX
),方法是使用PUSHAD
和POPAD
捕获并恢复它们。
正如您所见,我使用了PUSHAD
和POPAD
来恢复线程寄存器。对于ShellAbout()
这种情况,它有4个DWORD
成员,因此返回时堆栈指针会增加0x10。
重定向ShellAbout()
后,您可以尝试“帮助”菜单中的“关于计算器”菜单项,您将看到它对目标CALC.EXE做了什么。
图9 - 将“关于计算器”重定向到对话框消息框
EXE保护器以这种方式操作目标,它们将重定向建立到它们额外的内存空间,下一节将讨论。
4. 防止逆向
使用复杂的API重定向技术重建导入表非常困难。有时像Import REConstructor(图10)这样的工具会混淆,难以重建导入表,尤其是在重定向是通过多态代码镜像完成的情况下。Import REConstructor是逆向领域的一个著名工具;它会挂起目标进程以捕获导入信息。如果您进行了类似于JMP
的重定向,它肯定会被该工具重建。然而,如果我们加密函数名称并将其与内存中的多态代码捆绑在一起,那么检索正确的导入表将会变得模糊。我们按照这种技术展示我们的EXE保护器,“Native Security Engine”[6]是一个遵循此方法的打包器。它拥有一个x86代码生成器和一个元变形引擎,两者都有助于建立复杂的重定向结构。
图10 - Import REConstructor, MackT/uCF2000
图11说明了EXE保护器中导入保护的主要策略。其中一些利用重定向到虚拟Win32库。例如,它们拥有Kernel32、User32和AdvApi32的虚拟库。它们使用自己的库来防止黑客攻击或安装其虚拟机。
图11 - 导入表保护
通过这种技术可以切断对外部的访问。正如您所见,MoleBox也是如此,它过滤FindFirstFile()
和FindNextFile()
,以便将文本文件和JPEG文件合并到打包文件中。当程序试图从硬盘查找文件时,它将被重定向到内存。
5. 运行时导入表注入
现在我想再多说一点。这个主题肯定对那些想了解Windows系统上用户级别(ring-3)Rootkit[7]操作的人感兴趣。第一个也是最终的问题:“如何能够注入到运行时进程的导入表中?”本节将回答这个问题。
我们想注入到运行时进程并对其进行修改。如果您还记得,在我以前的一篇文章[2]中,我建立了一个Windows Spy来捕获窗口类属性并实时修改它们。这次,我将更接近于重写内存并从外部重定向导入表。
-
通过使用
WindowFromPoint()
,我们可以获得某个点的窗口句柄;GetWindowThreadProcessId()
帮助我们知道这个窗口句柄的进程ID和线程ID。POINT point; HWND hWindowUnderTheMouse = WindowFromPoint(point); DWORD dwProcessId; DWORD dwThreadId; dwThreadId=GetWindowThreadProcessId(hSeekedWindow, &dwProcessId);
-
进程句柄和线程是通过
OpenProcess()
和OpenThread()
获得的。但是Windows 98中没有OpenThread()
!别担心,尝试查找EliCZ的RT库,一个用于在Windows 98中模拟OpenThread()
、CreateRemoteThread()
、VirtualAllocEX()
和VirtualFreeEx()
的库。HANDLE hProcess = OpenProcess( PROCESS_ALL_ACCESS, FALSE, dwProcessId ); HANDLE hThread = OpenThread( THREAD_ALL_ACCESS, FALSE, dwThreadId);
-
要开始操作进程内存,我们首先需要通过挂起主线程来冻结进程。
SuspendThread(hThread);
-
线程环境块(TEB)的位置可以通过
FS:[18]
获得,但我们无法访问它!因此GetThreadContext()
和GetThreadSelectorEntry()
帮助我们知道FS段的基址。CONTEXT Context; LDT_ENTRY SelEntry; Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread,&Context); // Calculate the base address of FS GetThreadSelectorEntry(hThread, Context.SegFs, &SelEntry); DWORD dwFSBase = ( SelEntry.HighWord.Bits.BaseHi << 24) | (SelEntry.HighWord.Bits.BaseMid << 16) | SelEntry.BaseLow;
-
通过读取目标进程虚拟内存中的TEB位置来获得线程环境块(TEB)。线程和进程环境块(图12)在“Undocumented Windows 2000 secrets”[4]中有充分的解释。此外,NTInternals团队[5]提供了TEB和FEB的完整定义。正如我猜测的那样,Microsoft团队似乎忘记提供有关它们的信息,或者不打算公开它们!这就是为什么我喜欢Linux团队。
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);
图12 - 线程环境块和进程环境块
-
当前进程内存中可移植可执行文件镜像的镜像基址可以从进程环境块信息中找到。
DWORD dwImageBase = (DWORD)ppeb->ImageBaseAddress;
-
ReadProcessMemory()
帮助我们读取可移植可执行文件的整个镜像。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);
-
我们查找DLL名称和调接器值,找到目标并重定向它。在此示例中,DLL名称为Shell32.dll,调接器是
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; }
-
通过
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);
-
加载器被写入额外的内存中。它包含显示示例消息框的代码。
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 // save the registers context in stack call _local_1 _local_1: pop ebp sub ebp,offset _local_1// get base 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 // restore the first registers context from stack ret 10h ...
-
修改后的可执行镜像被写入内存。在写入之前,不要忘记为内存设置完全访问权限。
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_READWRITE
访问权限,在可执行页面情况下需要PAGE_EXECUTE
。 -
现在进程已准备好解冻,生活将重新开始,但会发生什么?尝试about菜单项,您将看到(图13),这是注入生命的第一个方面!
ResumeThread(hThread);
图13 - 运行时注入ShellAbout()调接器
我正在考虑注入其他API调接器,我们还可以将其他动态链接库上传到目标进程中以重定向受害者调接器,但这已在另一篇文章[3]中完全解释。下一节将讨论这种行为可能导致的一种灾难。您可以自己想象其他可能的“海啸”。
6. 特洛伊木马
始终阻止您的网页浏览器上的弹出窗口,并关闭Internet Explorer上的Active-X控件和插件的自动安装。它们将通过OLE组件或小型DLL插件进入您的计算机,并在进程中启动。有时,这种生命存在于特定进程(例如Yahoo Messenger或MSN Messenger)的导入表中。它可以挂钩所有Windows控件并过滤API(天哪!)我的电子邮件密码去哪儿了?这是用户级别Rootkit[7]的一种可能性。它可以让您的计算机生根并窃取您的重要信息。防病毒软件只能扫描文件镜像;它们在运行时进程注入方面失去了控制。因此,在网上冲浪时要小心,并始终使用强大的防火墙过滤。
Yahoo Messenger Hooker 如何工作?
我解释了编写Yahoo Messenger Hooker 的可行步骤。
-
使用
FindWindow()
获取Yahoo Messenger句柄及其类名。HWND hWnd = FindWindow("YahooBuddyMain", NULL);
- 像上一节一样,将注入实现到其进程中。
- 对
GetDlgItemText()
的导入调接器执行此注入,以过滤其成员。UINT GetDlgItemText( HWND hDlg, int nIDDlgItem, LPTSTR lpString, int nMaxCount);
-
将对话框控件ID
nIDDlgItem
与特定ID进行比较,以检测当前正在使用哪个控件。如果找到ID,则使用原始GetDlgItemText()
挂钩字符串。CHAR pYahooID[127]; CHAR pPassword[127]; switch(nIDDlgItem) { case 211: // Yahoo ID GetDlgItemText(hDlg, nIDDlgItem, pYahooID, 127); // for stealing // ... GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate //the original break; case 212: // Password GetDlgItemText(hDlg, nIDDlgItem, pPassword, 127); // for stealing // ... GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate //the original break; default: GetDlgItemText(hDlg, nIDDlgItem, lpString, nMaxCount);// Emulate //the original }
图14 - 挂钩Yahoo Messenger
现在我相信没有安全可言了。有人可以通过几行代码窃取我的Yahoo ID和密码。我们生活在一个不安全的世界里!
7. 后果
导入表本质上是Windows可执行文件的一部分。了解导入表的性能有助于我们理解API在运行时是如何被请求的。您可以将导入表重定向到当前进程内存中的另一个可执行内存空间,以防止逆向活动,并使用自己的PE加载器以及挂钩API函数。可以通过从外部冻结和解冻进程来实时修改进程的导入表;这种灾难迫使我们更多地考虑安全设备(如防病毒软件、防火墙等)。然而,对于每天出现的新方法,它们并没有持久的好处。此外,这种概念有助于我们建立虚拟机监视器,在Windows或Linux内部在一个独立的环境中运行Windows可执行文件。因此,我不再需要Windows系统来运行我的Windows EXE文件了!
- 将您的代码注入可移植可执行文件, The Code Project, 2005年12月。
- 捕获窗口控件并修改其属性, The Code Project, 2005年2月。
- 注入代码到另一个进程的三种方法, Robert Kuster, The Code Project, 2003年7月。
Documents
- 未公开的Windows® 2000秘密:程序员的食谱,Sven B. Schreiber,Addison-Wesley,2001年7月,ISBN 0-201-72187-2。
- Microsoft® Windows® NT®/ 2000的未公开函数,Tomasz Nowak及其他人,NTInternals团队,1999-2005。
链接
- NTCore,系统与安全团队。
- Rootkit,在线Rootkit杂志。