将代码注入可移植可执行文件






4.97/5 (200投票s)
本文演示了在不重新编译源代码的情况下,将代码注入可移植可执行(EXE、DLL、OCX等)文件的五个步骤。
下载次数
- PE 查看器
- PE Maker - 步骤 1 - 添加新节。
- PE Maker - 步骤 2 - 跳转到 OEP。
- PE Maker - 步骤 3 - 支持导入表。
- PE Maker - 步骤 4 - 支持 DLL 和 OCX。
- PE Maker - 步骤 5 - 收尾工作。
- CALC.EXE - 测试文件
目录
- 0. 前言
- 1. 先决条件
- 2. 可移植可执行文件格式
- 3. 调试器、反汇编器和一些有用工具
- 4. 添加新节并更改 OEP
- 5. 存储重要数据并到达原始 OEP
- 6. 构建导入表并重构原始导入表
- 7. 支持 DLL 和 OCX
- 8. 保留线程局部存储
- 9. 注入您的代码
- 10. 结论
0 前言
您可能想了解病毒程序如何将其过程注入到可移植可执行文件的内部并进行破坏,或者您对实现一个打包器或保护程序感兴趣,以便加密您的可移植可执行(PE)文件的数据。本文致力于提供对 EXE 工具或某些恶意软件所完成操作的简要了解。
您可以使用本文的源代码来创建自定义的 EXE 构建器。它可以被用来正确地创建 EXE 保护程序,或者带有不良意图地传播病毒。但是,我写这篇文章的目的是为了关注第一种应用,因此我不会对这些方法的滥用负责。
1 先决条件
没有特定的强制性先决条件来遵循本文的主题。如果您熟悉调试器和可移植文件格式,我建议您跳过第 2 节和第 3 节,这两节完全是为了那些对 EXE 文件格式和调试器一无所知的人而设计的。
2 可移植可执行文件格式
可移植可执行文件格式的定义是为了让 Windows 操作系统以最佳方式执行代码并存储运行程序所需的重要数据,例如常量数据、变量数据、导入库链接和资源数据。它包含 MS-DOS 文件信息、Windows NT 文件信息、节头和节图像,表 1。
2.1 MS-DOS 数据
这些数据让您回想起 Windows 操作系统开发早期的时光。我们正走在实现像 Windows NT 3.51 这样完整的操作系统的道路上(我的意思是,Win3.1、Win95、Win98 都不是完美的 OS)。MS-DOS 数据使得您的可执行文件在 MS-DOS 中调用一个函数,而 MS-DOS Stub 程序会显示:“此程序无法在 MS-DOS 模式下运行”或“此程序只能在 Windows 模式下运行”,或者类似这样的评论,当您尝试在 MS-DOS 6.0 中运行 Windows EXE 文件时,此时没有任何 Windows 的痕迹。因此,这些数据是保留给代码用于指示在 MS-DOS 中显示这些评论的 操作系统。MS-DOS 数据中最有趣的部分是“MZ”!您相信吗,它指的是一位早期的微软程序员“Mark Zbikowski”的名字?
对我来说,只有 PE 签名在 MS-DOS 数据中的偏移量是重要的,这样我就可以用它来找到 Windows NT 数据的位置。我只是建议您看一下 表 1,然后观察 <Microsoft Visual Studio .net path>\VC7\PlatformSDK\include\ 文件夹或 <Microsoft Visual Studio 6.0 path>\VC98\include\ 文件夹中 <winnt.h> 头文件中的 IMAGE_DOS_HEADER
结构。我不知道微软团队为什么在 MSDN 库中忘记提供关于此结构的任何注释!
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header "MZ"
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of the new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
e_lfanew
是指向 Windows NT 数据位置的偏移量。我提供了一个程序来从 EXE 文件获取头信息并显示给您。要使用该程序,只需尝试
PE 查看器
此示例对本文的整个内容都有用。
表 1 - 可移植可执行文件格式结构
MS-DOS 信息 |
IMAGE_DOS_ |
DOS EXE 签名 | 00000000 ASCII "MZ"
00000002 DW 0090
00000004 DW 0003
00000006 DW 0000
00000008 DW 0004
0000000A DW 0000
0000000C DW FFFF
0000000E DW 0000
00000010 DW 00B8
00000012 DW 0000
00000014 DW 0000
00000016 DW 0000
00000018 DW 0040
0000001A DW 0000
0000001C DB 00
…
…
0000003B DB 00
0000003C DD 000000F0
|
DOS_PartPag | |||
DOS_PageCnt | |||
DOS_ReloCnt | |||
DOS_HdrSize | |||
DOS_MinMem | |||
DOS_MaxMem | |||
DOS_ReloSS | |||
DOS_ExeSP | |||
DOS_ChkSum | |||
DOS_ExeIPP | |||
DOS_ReloCS | |||
DOS_TablOff | |||
DOS_Overlay | |||
… 保留字
| |||
指向 PE 签名的偏移量 | |||
MS-DOS 存根 程序 |
00000040 º.´.Í!¸\LÍ!This program canno
00000060 t be run in DOS mode....$.......
| ||
Windows NT 信息
|
签名 | PE 签名 (PE) | 000000F0 ASCII "PE"
|
IMAGE_ |
Machine |
000000F4 DW 014C
000000F6 DW 0003
000000F8 DD 3B7D8410
000000FC DD 00000000
00000100 DD 00000000
00000104 DW 00E0
00000106 DW 010F
| |
NumberOfSections | |||
TimeDateStamp | |||
PointerToSymbolTable | |||
NumberOfSymbols | |||
SizeOfOptionalHeader | |||
Characteristics | |||
IMAGE_ |
MagicNumber |
00000108 DW 010B
0000010A DB 07
0000010B DB 00
0000010C DD 00012800
00000110 DD 00009C00
00000114 DD 00000000
00000118 DD 00012475
0000011C DD 00001000
00000120 DD 00014000
00000124 DD 01000000
00000128 DD 00001000
0000012C DD 00000200
00000130 DW 0005
00000132 DW 0001
00000134 DW 0005
00000136 DW 0001
00000138 DW 0004
0000013A DW 0000
0000013C DD 00000000
00000140 DD 0001F000
00000144 DD 00000400
00000148 DD 0001D7FC
0000014C DW 0002
0000014E DW 8000
00000150 DD 00040000
00000154 DD 00001000
00000158 DD 00100000
0000015C DD 00001000
00000160 DD 00000000
00000164 DD 00000010
| |
MajorLinkerVersion | |||
MinorLinkerVersion | |||
SizeOfCode | |||
SizeOfInitializedData | |||
SizeOfUninitializedData | |||
AddressOfEntryPoint | |||
BaseOfCode | |||
BaseOfData | |||
ImageBase | |||
SectionAlignment | |||
FileAlignment | |||
MajorOSVersion | |||
MinorOSVersion | |||
MajorImageVersion | |||
MinorImageVersion | |||
MajorSubsystemVersion | |||
MinorSubsystemVersion | |||
保留 | |||
SizeOfImage | |||
SizeOfHeaders | |||
CheckSum | |||
Subsystem | |||
DLLCharacteristics | |||
SizeOfStackReserve | |||
SizeOfStackCommit | |||
SizeOfHeapReserve | |||
SizeOfHeapCommit | |||
LoaderFlags | |||
NumberOfRvaAndSizes | |||
IMAGE_ |
导出表 | ||
导入表 | |||
资源表 | |||
异常表 | |||
证书文件 | |||
重定位表 | |||
调试数据 | |||
体系结构数据 | |||
全局指针 | |||
TLS 表 | |||
加载配置表 | |||
绑定导入表 | |||
导入地址表 | |||
延迟导入描述符 | |||
COM+ 运行时头 | |||
保留 | |||
节 信息 |
IMAGE_ |
Name[8] |
000001E8 ASCII".text"
000001F0 DD 000126B0
000001F4 DD 00001000
000001F8 DD 00012800
000001FC DD 00000400
00000200 DD 00000000
00000204 DD 00000000
00000208 DW 0000
0000020A DW 0000
0000020C DD 60000020
CODE|EXECUTE|READ |
VirtualSize | |||
VirtualAddress | |||
SizeOfRawData | |||
PointerToRawData | |||
PointerToRelocations | |||
PointerToLineNumbers | |||
NumberOfRelocations | |||
NumberOfLineNumbers | |||
Characteristics | |||
… |
00000210 ASCII".data"; SECTION
00000218 DD 0000101C ; VirtualSize = 0x101C
0000021C DD 00014000 ; VirtualAddress = 0x14000
00000220 DD 00000A00 ; SizeOfRawData = 0xA00
00000224 DD 00012C00 ; PointerToRawData = 0x12C00
00000228 DD 00000000 ; PointerToRelocations = 0x0
0000022C DD 00000000 ; PointerToLineNumbers = 0x0
00000230 DW 0000 ; NumberOfRelocations = 0x0
00000232 DW 0000 ; NumberOfLineNumbers = 0x0
00000234 DD C0000040 ; Characteristics =
INITIALIZED_DATA|READ|WRITE
00000238 ASCII".rsrc"; SECTION
00000240 DD 00008960 ; VirtualSize = 0x8960
00000244 DD 00016000 ; VirtualAddress = 0x16000
00000248 DD 00008A00 ; SizeOfRawData = 0x8A00
0000024C DD 00013600 ; PointerToRawData = 0x13600
00000250 DD 00000000 ; PointerToRelocations = 0x0
00000254 DD 00000000 ; PointerToLineNumbers = 0x0
00000258 DW 0000 ; NumberOfRelocations = 0x0
0000025A DW 0000 ; NumberOfLineNumbers = 0x0
0000025C DD 40000040 ; Characteristics =
INITIALIZED_DATA|READ
| ||
SECTION[0] |
00000400 EA 22 DD 77 D7 23 DD 77 ê"Ýw×#Ýw
00000408 9A 18 DD 77 00 00 00 00 šÝw....
00000410 2E 1E C7 77 83 1D C7 77 .ÇwƒÇw
00000418 FF 1E C7 77 00 00 00 00 ÿÇw....
00000420 93 9F E7 77 D8 05 E8 77 “ŸçwØèw
00000428 FD A5 E7 77 AD A9 E9 77 ý¥çw©éw
00000430 A3 36 E7 77 03 38 E7 77 £6çw8çw
00000438 41 E3 E6 77 60 8D E7 77 Aãæw`çw
00000440 E6 1B E6 77 2B 2A E7 77 ææw+*çw
00000448 7A 17 E6 77 79 C8 E6 77 zæwyÈæw
00000450 14 1B E7 77 C1 30 E7 77 çwÁ0çw
…
| ||
… |
…
0001BF00 63 00 2E 00 63 00 68 00 c...c.h.
0001BF08 6D 00 0A 00 43 00 61 00 m...C.a.
0001BF10 6C 00 63 00 75 00 6C 00 l.c.u.l.
0001BF18 61 00 74 00 6F 00 72 00 a.t.o.r.
0001BF20 11 00 4E 00 6F 00 74 00 .N.o.t.
0001BF28 20 00 45 00 6E 00 6F 00 .E.n.o.
0001BF30 75 00 67 00 68 00 20 00 u.g.h. .
0001BF38 4D 00 65 00 6D 00 6F 00 M.e.m.o.
0001BF40 72 00 79 00 00 00 00 00 r.y.....
0001BF48 00 00 00 00 00 00 00 00 ........
0001BF50 00 00 00 00 00 00 00 00 ........
0001BF58 00 00 00 00 00 00 00 00 ........
0001BF60 00 00 00 00 00 00 00 00 ........
0001BF68 00 00 00 00 00 00 00 00 ........
0001BF70 00 00 00 00 00 00 00 00 ........
0001BF78 00 00 00 00 00 00 00 00 ........
|
2.2 Windows NT 数据
如前一节所述,MS-DOS 数据结构中的 e_lfanew
存储指向 Windows NT 信息的位置。因此,如果假设 pMem
指针指向所选可移植可执行文件的内存空间的起始点,您可以通过以下几行检索 MS-DOS 头和 Windows NT 头,您也可以在 PE 查看器示例(pelib.cpp, PEStructure::OpenFileName()
)中看到这些行。
IMAGE_DOS_HEADER image_dos_header;
IMAGE_NT_HEADERS image_nt_headers;
PCHAR pMem;
…
memcpy(&image_dos_header, pMem,
sizeof(IMAGE_DOS_HEADER));
memcpy(&image_nt_headers,
pMem+image_dos_header.e_lfanew,
sizeof(IMAGE_NT_HEADERS));
检索头信息似乎非常简单。我建议您查阅 MSDN 库中关于 IMAGE_NT_HEADERS
结构定义的说明。它使您能够理解图像 NT 头如何处理在 Windows NT OS 中执行代码。现在,您已经了解了 Windows NT 结构,它包含"PE"签名,文件头,以及可选头。请勿忘记在 MSDN 库和 表 1 中查看它们的注释。
总的来说,我认为在大多数情况下,我们只需要关注 IMAGE_NT_HEADERS
结构中的以下单元。
FileHeader->NumberOfSections
OptionalHeader->AddressOfEntryPoint
OptionalHeader->ImageBase
OptionalHeader->SectionAlignment
OptionalHeader->FileAlignment
OptionalHeader->SizeOfImage
OptionalHeader->
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]->VirtualAddress
OptionalHeader->DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]->Size
如果您注意它们在 MSDN 库中的解释,您就能清楚地观察到这些值的用途及其作用,当 Windows OS 为 EXE 文件分配的内部虚拟内存空间完全分配时。因此,我将不再重复 MSDN 的注释。
我应该简要评论一下 PE 数据目录,或者 OptionalHeader
->DataDirectory[]
,因为我认为它们有一些有趣的方面。当您通过 Windows NT 信息检查 Optional Header 时,您会发现 Optional Header 的末尾有 16 个目录,您可以在其中找到连续的目录,包括它们的相对虚拟地址和大小。我在此仅引用 <winnt.h> 中的注释来阐明这些信息。
#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_EXCEPTION 3 // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
最后一个(15)是为将来保留的;即使在 PE64 中,我也从未见过它的用途。
例如,如果您想知道资源数据的相对虚拟地址 (RVA) 和大小,只需通过以下方式检索即可:
DWORD dwRVA = image_nt_headers.OptionalHeader->
DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]->VirtualAddress;
DWORD dwSize = image_nt_headers.OptionalHeader->
DataDirectory[IMAGE_DIRECTORY_ENTRY_RESOURCE]->Size;
为了更深入地理解数据目录的重要性,我将您引向第 3.4.3 节,Microsoft Portable Executable and the Common Object File Format Specification 文档,以及本文的第 6 节,在那里您将了解各种节的类型及其应用。稍后我们将讨论节的优势。
2.3 节头和节
我们目前正在观察可移植可执行文件如何通过 IMAGE_NT_HEADERS
->OptionalHeader
->SizeOfImage
在磁盘存储文件和分配给程序的虚拟内存空间中声明节的位置和大小,就像 Windows 任务管理器一样,以及用于演示节类型的特性。为了更好地理解节头,正如我之前的声明,我建议您简要看一下 MSDN 库中 IMAGE_SECTION_HEADER
结构定义的说明。对于 EXE 打包器开发人员来说,VirtualSize
、VirtualAddress
、SizeOfRawData
、PointerToRawData
和 Characteristics
字段具有重要作用。在开发 EXE 打包器时,您应该足够聪明地利用它们。修改它们时需要注意一些事项;您需要注意将 VirtualSize
和 VirtualAddress
与 OptionalHeader
->SectionAlignment
对齐,以及 SizeOfRawData
和 PointerToRawData
与 OptionalHeader
->FileAlignment
对齐。否则,您将损坏目标 EXE 文件,它将无法运行。关于 Characteristics
,我主要关注通过 IMAGE_SCN_MEM_READ
| IMAGE_SCN_MEM_WRITE
| IMAGE_SCN_CNT_INITIALIZED_DATA
来创建一个节,我更倾向于让我的新节能够在运行时初始化数据;例如导入表;此外,我需要它能够被加载器根据我在节特性中的设置进行读写修改。
此外,您还应该注意节的名称,您可以通过其名称了解每个节的用途。我将您引向第 6 节:Microsoft Portable Executable and the Common Object File Format Specification 文档。我相信它通过名称描述了节的整体情况,表 2。
表 2 - 节名称
".text" | 代码节 |
"CODE" | 由 Borland Delphi 或 Borland Pascal 链接的文件的代码节 |
".data" | 数据节 |
"DATA" | 由 Borland Delphi 或 Borland Pascal 链接的文件的。数据节 |
".rdata" | 常量数据节 |
".idata" | 导入表 |
".edata" | 导出表 |
".tls" | TLS 表 |
".reloc" | 重定位信息 |
".rsrc" | 资源信息 |
要理解节头和节,您可以运行示例 PE 查看器。通过这个 PE 查看器,您只能了解节头在文件映像中的应用,因此要观察在虚拟内存中的主要意义,您应该尝试使用调试器加载 PE 文件,下一节将通过使用调试器来介绍使用虚拟地址和大小的主要思路。最后的说明是关于 IMAGE_NT_HEADERS
->FileHeader
->NumberOfSections
,它提供了 PE 文件中的节数,当您删除或添加节时,请勿忘记调整它,我指的是节注入!
3 调试器、反汇编器和一些有用工具
在本部分,您将熟悉开发 PE 工具所需的必要设备。
3.1 调试器
成为 PE 工具开发人员的第一个重要先决条件是拥有足够的调试器使用经验。此外,您应该了解大多数汇编指令。对我来说,Intel 的文档是最好的参考。您可以从 Intel 网站获取 IA-32 的文档,以及 IA-64;未来属于 IA-64 CPU、Windows XP 64 位和 PE64!
要跟踪 PE 文件,Compuware Corporation 的 SoftICE,我还在高中时知道它也叫 NuMega,是世界上最好的 调试器。它通过使用 内核模式方法调试来实现进程跟踪,而不使用 Windows 调试 应用程序编程接口 (API) 函数。此外,我将介绍一个在 用户模式级别非常出色的调试器。它利用 Windows 调试 API 来跟踪 PE 文件,并将其附加到活动的 进程。这些 API 函数由微软团队提供,位于 Windows Kernel32 库中,用于跟踪特定进程,通过使用微软工具,或者也许,用于创建您自己的调试器!其中一些 API 函数包括:CreateThread()
、CreateProcess()
、OpenProcess()
、DebugActiveProcess()
、GetThreadContext()
、SetThreadContext()
、ContinueDebugEvent()
、DebugBreak()
、ReadProcessMemory()
、WriteProcessMemory()
、SuspendThread()
和 ResumeThread()
。
3.1.1 SoftICE
1987 年,Frank Grossman 和 Jim Moskun 决定成立一家名为 NuMega Technologies 的公司,位于 Nashua, NH,目的是开发用于跟踪和测试 Microsoft Windows 软件程序可靠性的设备。如今,它是 Compuware Corporation 的一部分,其产品有助于提高 Windows 软件的可靠性,以及 Windows 驱动程序的开发。目前,大家都知道 Compuware DriverStudio,它用于建立一个实现内核驱动程序或系统文件开发的环境,并借助 Windows Driver Development Kit (DDK)。它绕过了 DDK 的参与,为 Windows 系统软件开发人员实现了内核级别的可移植可执行文件。对我们来说,DriverStudio 中只有一个工具很重要,那就是 SoftICE,这个调试器可以用来跟踪所有可移植可执行文件,无论是用户模式级别的 PE 文件还是内核模式级别的 PE 文件。
图 1 - SoftICE 窗口
EAX=00000000EBX=7FFDD000ECX=0007FFB0 EDX=7C90EB94ESI=FFFFFFFF EDI=7C919738EBP=0007FFF0 ESP=0007FFC4 EIP=010119E0o d i szapc CS=0008 DS=0023 SS=0010 ES=0023 FS=0030 GS=0000 SS:0007FFC4=87C816D4F |
0023:01013000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................ 0023:01013010 01 00 00 00 20 00 00 00-0A 00 00 00 0A 00 00 00 ................ 0023:01013020 20 00 00 00 00 00 00 00-53 63 69 43 61 6C 63 00 ........SciCalc. 0023:01013030 00 00 00 00 00 00 00 00-62 61 63 6B 67 72 6F 75 ........backgrou 0023:01013040 6E 64 00 00 00 00 00 00-2E 00 00 00 00 00 00 00 nd.............. |
0010:0007FFC4 4F 6D 81 7C 38 07 91 7C-FF FF FF FF 00 90 FD 7F Om |8 ‘| . 0010:0007FFD4 ED A6 54 80 C8 FF 07 00-E8 B4 F5 81 FF FF FF FF T . 0010:0007FFE4 F3 99 83 7C 58 6D 81 7C-00 00 00 00 00 00 00 00 Xm |........ 0010:0007FFF4 00 00 00 00 E0 19 01 01-00 00 00 00 00 00 00 00 .... .... |
010119E0 PUSH EBP 010119E1 MOV EBP,ESP 010119E3 PUSH -1 010119E5 PUSH 01001570 010119EA PUSH 01011D60 010119EF MOV EAX,DWORD PTR FS:[0] 010119F5 PUSH EAX 010119F6 MOV DWORD PTR FS:[0],ESP 010119FD ADD ESP,-68 01011A00 PUSH EBX 01011A01 PUSH ESI 01011A02 PUSH EDI 01011A03 MOV DWORD PTR SS:[EBP-18],ESP 01011A06 MOV DWORD PTR SS:[EBP-4],0 |
:_ |
3.1.2 OllyDbg
大约 4 年前,我偶然第一次看到了这个调试器。对我来说,这是最好的选择,我当时没有足够的财力购买 SoftICE,而且当时 SoftICE 只有对 DOS、Windows 98 和 Windows 2000 具有良好的功能。我发现这个调试器支持所有类型的 Windows 版本。因此,我开始快速学习它,现在它是我最喜欢的 Windows OS 调试器。它是一个可以用于跟踪除 Common Language Infrastructure (CLI) 文件格式之外的所有可移植可执行文件的调试器,在用户模式级别,通过使用 Windows 调试 API。Oleh Yuschuk,作者,是我一生中见过的最值得尊敬的软件开发人员之一。他是一位乌克兰人,现居德国。我应该在这里提到,他的调试器是世界各地黑客和破解派对的最佳选择!它是免费软件!您可以从 OllyDbg 主页尝试。
图 2 - OllyDbg CPU 窗口
3.1.3 调试器界面中哪些部分很重要?
我介绍了两个调试器,但没有说明如何使用它们,也没有说明应该更多地关注哪些部分。关于调试器的使用,我将您引向其帮助文档中的说明。但是,我想简要解释一下调试器的重要部分;当然,我指的是低级别调试器,或者换句话说,x86 CPU 系列的机器语言调试器。
所有低级别调试器都包含以下子部分:
- 寄存器查看器。
EAX ECX EDX EBX ESP EBP ESI EDI EIP od t s z a p c
- 反汇编器或代码查看器。
010119E0 PUSH EBP 010119E1 MOV EBP,ESP 010119E3 PUSH -1 010119E5 PUSH 01001570 010119EA PUSH 01011D60 010119EF MOV EAX,DWORD PTR FS:[0] 010119F5 PUSH EAX 010119F6 MOV DWORD PTR FS:[0],ESP 010119FD ADD ESP,-68 01011A00 PUSH EBX 01011A01 PUSH ESI 01011A02 PUSH EDI 01011A03 MOV DWORD PTR SS:[EBP-18],ESP 01011A06 MOV DWORD PTR SS:[EBP-4],0
- 内存监视器。
0023:01013000 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
0023:01013010 01 00 00 00 20 00 00 00-0A 00 00 00 0A 00 00 00 ................
0023:01013020 20 00 00 00 00 00 00 00-53 63 69 43 61 6C 63 00 ........SciCalc.
0023:01013030 00 00 00 00 00 00 00 00-62 61 63 6B 67 72 6F 75 ........backgrou
0023:01013040 6E 64 00 00 00 00 00 00-2E 00 00 00 00 00 00 00 nd.............. - 堆栈查看器。
0010:0007FFC4 4F 6D 81 7C 38 07 91 7C-FF FF FF FF 00 90 FD 7F Om |8 ‘| .
0010:0007FFD4 ED A6 54 80 C8 FF 07 00-E8 B4 F5 81 FF FF FF FF T .
0010:0007FFE4 F3 99 83 7C 58 6D 81 7C-00 00 00 00 00 00 00 00 Xm |........
0010:0007FFF4 00 00 00 00 E0 19 01 01-00 00 00 00 00 00 00 00 .... .... - 用于跟踪调试过程的命令行、命令按钮或快捷键。
命令 SoftICE OllyDbg Run F5 F9 单步进入 F11 F7 单步跳过 F10 F8 设置断点 F8 F2
您可以比较 图 1 和 图 2 来区分 SoftICE 和 OllyDbg 之间的区别。当您要跟踪 PE 文件时,您应该主要关注这五部分。此外,每个调试器都包含一些其他有用的部分;您应该自己去发现它们。
3.2 反汇编器
我们可以将 OllyDbg 和 SoftICE 视为出色的反汇编器,但我也想介绍另一个在逆向工程界很有名的反汇编器工具。
3.2.1 Proview 反汇编器
Proview 或 PVDasm 是 Reverse-Engineering-Community 开发的一款令人赞叹的反汇编器;它仍在开发和修复 bug 中。您可以找到其反汇编器引擎并使用它来创建自己的反汇编器。
3.2.2 W32Dasm
W32DASM 可以反汇编 16 位和 32 位可执行文件格式。除了反汇编能力外,您还可以使用它来分析导入、导出和资源数据目录数据。
3.2.3 IDA Pro
所有逆向工程专家都知道 IDA Pro 不仅可以用于检查 x86 指令,还可以检查各种 CPU 类型,如 AVR、PIC 等。它可以使用彩色图形和表格来说明可移植可执行文件的汇编源,对于该领域的新手来说非常有帮助。此外,它还能够像 OllyDbg 一样跟踪用户模式级别的可执行文件。
3.3 一些有用工具
一个好的 PE 工具开发者应该熟悉那些能够节省他时间的工具,因此我建议选择一些合适的工具来研究可移植可执行文件下的基本信息。
3.3.1 LordPE
y0da 的 LordPE 仍然是检索 PE 文件信息并对其进行修改的首选。
3.3.2 PEiD
PE iDentifier 对于识别 PE 文件的编译器、打包器和加密器类型很有价值。截至目前,它可以检测超过 500 种不同的 PE 文件签名类型。
3.3.3 Resource Hacker
Resource Hacker 可用于修改资源目录信息;图标、菜单、版本信息、字符串表等。
3.3.4 WinHex
WinHex,您可以清楚地知道这个工具能做什么。
3.3.5 CFF Explorer
最后,Ntoskrnl 的 CFF Explorer 是您梦想中的 PE 实用工具;它支持 PE32/64,包括 PE 重建 Common Language Infrastructure (CLI) 文件,换句话说,就是 .NET 文件,一个资源修改器,以及许多其他工具中找不到的功能,只需尝试手动发现每一个您想象不到的选项。
4 添加新节并更改 OEP
我们已经准备好开始我们项目的第一步。因此,我提供了一个库来添加新节并重建可移植可执行文件。开始之前,我希望您能熟悉 PE 文件的头信息,通过使用 OllyDbg。您应该首先打开一个 PE 文件,然后会弹出一个菜单,选择 **View->Executable file**,再次弹出菜单 **Special->PE header**。您将看到一个类似于 图 3 的场景。现在,转到主菜单 **View->Memory**,尝试区分 **Memory map** 窗口中的节。
图 3
00000000
00000002
00000004
00000006
00000008
0000000A
0000000C
0000000E
00000010
00000012
00000014
00000016
00000018
0000001A
0000001C
0000001D
0000001E
0000001F
00000020
00000021
00000022
00000023
00000024
00000025
00000026
00000027
00000028
00000029
0000002A
0000002B
0000002C
0000002D
0000002E
0000002F
00000030
00000031
00000032
00000033
00000034
00000035
00000036
00000037
00000038
00000039
0000003A
0000003B
0000003C
|
4D 5A
9000
0300
0000
0400
0000
FFFF
0000
B800
0000
0000
0000
4000
0000
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
00
F0000000
|
ASCII "MZ"
DW 0090
DW 0003
DW 0000
DW 0004
DW 0000
DW FFFF
DW 0000
DW 00B8
DW 0000
DW 0000
DW 0000
DW 0040
DW 0000
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DB 00
DD 000000F0
|
DOS EXE Signature
DOS_PartPag = 90 (144.)
DOS_PageCnt = 3
DOS_ReloCnt = 0
DOS_HdrSize = 4
DOS_MinMem = 0
DOS_MaxMem = FFFF (65535.)
DOS_ReloSS = 0
DOS_ExeSP = B8
DOS_ChkSum = 0
DOS_ExeIP = 0
DOS_ReloCS = 0
DOS_TablOff = 40
DOS_Overlay = 0
Offset to PE signature |
我想解释一下如何简单地更改我们 示例文件(Windows XP 的 CALC.EXE)的入口点偏移量 (OEP)。首先,使用 PE 工具和我们的 PE 查看器,我们找到 OEP,0x00012475
,以及映像基址,0x01000000
。此 OEP 值是相对虚拟地址,因此映像基址用于将其转换为虚拟地址。
Virtual_Address = Image_Base + Relative_Virtual_Address |
DWORD OEP_RVA = image_nt_headers->OptionalHeader.AddressOfEntryPoint ;
// OEP_RVA = 0x00012475
DWORD OEP_VA = image_nt_headers->OptionalHeader.ImageBase + OEP_RVA ;
// OEP_VA = 0x01000000 + 0x00012475 = 0x01012475
PE Maker - 步骤 1
CALC.EXE - 测试文件
loader.cpp 中的 DynLoader()
用于新节的数据,换句话说,就是 **Loader**。
DynLoader 步骤 1
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
MOV EAX,01012475h // << Original OEP
JMP EAX
//----------------------------------
DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
不幸的是,此源文件只能用于 示例测试文件。我们需要通过在新节中保存原始 OEP 的值来完成它,并使用它来到达真实 OEP。我已在步骤 2(第 5 节)中完成了此操作。
4.1 恢复和重建 PE 文件
我创建了一个简单的类库来恢复 PE 信息并将其用于新的 PE 文件。
CPELibrary 类 步骤 1
//----------------------------------------------------------------
class CPELibrary
{
private:
//-----------------------------------------
PCHAR pMem;
DWORD dwFileSize;
//-----------------------------------------
protected:
//-----------------------------------------
PIMAGE_DOS_HEADER image_dos_header;
PCHAR pDosStub;
DWORD dwDosStubSize, dwDosStubOffset;
PIMAGE_NT_HEADERS image_nt_headers;
PIMAGE_SECTION_HEADER image_section_header[MAX_SECTION_NUM];
PCHAR image_section[MAX_SECTION_NUM];
//-----------------------------------------
protected:
//-----------------------------------------
DWORD PEAlign(DWORD dwTarNum,DWORD dwAlignTo);
void AlignmentSections();
//-----------------------------------------
DWORD Offset2RVA(DWORD dwRO);
DWORD RVA2Offset(DWORD dwRVA);
//-----------------------------------------
PIMAGE_SECTION_HEADER ImageRVA2Section(DWORD dwRVA);
PIMAGE_SECTION_HEADER ImageOffset2Section(DWORD dwRO);
//-----------------------------------------
DWORD ImageOffset2SectionNum(DWORD dwRVA);
PIMAGE_SECTION_HEADER AddNewSection(char* szName,DWORD dwSize);
//-----------------------------------------
public:
//-----------------------------------------
CPELibrary();
~CPELibrary();
//-----------------------------------------
void OpenFile(char* FileName);
void SaveFile(char* FileName);
//-----------------------------------------
};
根据 表 1,image_dos_header
、pDosStub
、image_nt_headers
、image_section_header
[MAX_SECTION_NUM
] 和 image_section
[MAX_SECTION_NUM
] 的使用是清晰的。我们使用 OpenFile()
和 SaveFile()
来检索和重建 PE 文件。此外,AddNewSection()
用于创建新节,这是重要的一步。
4.2 创建新节的数据
在 pecrypt.cpp 中,我用另一个类 CPECryptor
来包含新节的数据。但是,新节的数据是由 loader.cpp 中的 DynLoader()
创建的,DynLoader 步骤 1。我们使用 CPECryptor
类将这些数据以及其他一些东西输入到新节中。
CPECryptor 类 步骤 1
//----------------------------------------------------------------
class CPECryptor: public CPELibrary
{
private:
//----------------------------------------
PCHAR pNewSection;
//----------------------------------------
DWORD GetFunctionVA(void* FuncName);
void* ReturnToBytePtr(void* FuncName, DWORD findstr);
//----------------------------------------
protected:
//----------------------------------------
public:
//----------------------------------------
void CryptFile(int(__cdecl *callback) (unsigned int, unsigned int));
//----------------------------------------
};
//----------------------------------------------------------------
4.3 关于创建新 PE 文件的一些说明
- 使用
SectionAlignment
对齐每个节的VirtualAddress
和VirtualSize
。image_section_header[i]->VirtualAddress= PEAlign(image_section_header[i]->VirtualAddress, image_nt_headers->OptionalHeader.SectionAlignment); image_section_header[i]->Misc.VirtualSize= PEAlign(image_section_header[i]->Misc.VirtualSize, image_nt_headers->OptionalHeader.SectionAlignment);
- 使用
FileAlignment
对齐每个节的PointerToRawData
和SizeOfRawData
。image_section_header[i]->PointerToRawData = PEAlign(image_section_header[i]->PointerToRawData, image_nt_headers->OptionalHeader.FileAlignment); image_section_header[i]->SizeOfRawData = PEAlign(image_section_header[i]->SizeOfRawData, image_nt_headers->OptionalHeader.FileAlignment);
- 通过最后一个节的虚拟大小和虚拟地址更正
SizeofImage
。image_nt_headers->OptionalHeader.SizeOfImage = image_section_header[LastSection]->VirtualAddress + image_section_header[LastSection]->Misc.VirtualSize;
- 将绑定导入目录头设置为零,因为该目录对于执行 PE 文件来说并不非常重要。
image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT]. VirtualAddress = 0; image_nt_headers-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0;
4.4 关于链接此 VC 项目的一些说明
- 将 *链接器->常规->启用增量链接* 设置为 **否 (/INCREMENTAL:NO)**。
您可以通过查看以下图片来理解增量链接和非增量链接的区别。
为了获取
DynLoader()
的虚拟地址,在增量链接中,我们获取JMP pemaker.DynLoader
的虚拟地址,但在非增量链接中,真实虚拟地址是通过以下代码获得的:DWORD dwVA= (DWORD) DynLoader;
此设置在增量链接中更为关键,当您尝试通过
CPECryptor::ReturnToBytePtr()
查找 **Loader**,即DynLoader()
的开始和结束时。void* CPECryptor::ReturnToBytePtr(void* FuncName, DWORD findstr) { void* tmpd; __asm { mov eax, FuncName jmp df hjg: inc eax df: mov ebx, [eax] cmp ebx, findstr jnz hjg mov tmpd, eax } return tmpd; }
5 存储重要数据并到达原始 OEP
现在,我们保存原始 OEP 和映像基址,以便到达 OEP 的虚拟地址。我已经在 DynLoader()
的末尾保留了一个免费空间来存储它们,DynLoader 步骤 2。
PE Maker - 步骤 2
DynLoader 步骤 2
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
PUSHAD
// get base ebp
CALL Main_1
Main_1:
POP EBP
SUB EBP,OFFSET Main_1
MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
PUSH EAX
RETN // >> JMP to Original OEP
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
新函数 CPECryptor::CopyData1()
将实现将映像基址值和入口点偏移量复制到加载器中的 8 字节免费空间。
5.1 恢复第一个寄存器上下文
恢复线程的原始上下文非常重要。我们尚未在 DynLoader 步骤 2 的源代码中完成此操作。我们可以修改 DynLoader()
的源以重新获取第一个上下文。
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
PUSHAD// Save the registers context in stack
CALL Main_1
Main_1:
POP EBP// Get Base EBP
SUB EBP,OFFSET Main_1
MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
MOV DWORD PTR [ESP+1Ch],EAX // pStack.Eax <- EAX
POPAD // Restore the first registers context from stack
PUSH EAX
XOR EAX, EAX
RETN // >> JMP to Original OEP
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
5.2 恢复原始堆栈
我们也可以通过将起始堆栈 + 0x34
的值设置为原始 OEP 来恢复原始堆栈,但这并不非常重要。不过,在以下代码中,我通过一个简单的技巧实现了加载器代码,除了重新装饰堆栈之外,还可以到达 OEP。您可以通过使用 OllyDbg 或 SoftICE 进行跟踪来观察实现。
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
PUSHAD // Save the registers context in stack
CALL Main_1
Main_1:
POP EBP
SUB EBP,OFFSET Main_1
MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
MOV DWORD PTR [ESP+54h],EAX // pStack.Eip <- EAX
POPAD // Restore the first registers context from stack
CALL _OEP_Jump
DWORD_TYPE(0xCCCCCCCC)
_OEP_Jump:
PUSH EBP
MOV EBP,ESP
MOV EAX,DWORD PTR [ESP+3Ch] // EAX <- pStack.Eip
MOV DWORD PTR [ESP+4h],EAX // _OEP_Jump RETURN pointer <- EAX
XOR EAX,EAX
LEAVE
RETN
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
5.3 通过结构化异常处理到达 OEP
当程序发生故障代码执行并出现错误时,会生成一个异常,因此在这种特殊情况下,程序会立即跳转到 线程信息块 的异常处理程序列表中名为异常处理程序的函数。
下一个 C++ 中的 try
-except
语句 示例阐明了 结构化异常处理 的操作。除了此代码的汇编代码外,它还阐明了结构化异常处理程序的安装、异常的引发以及异常处理程序函数。
#include "stdafx.h"
#include "windows.h"
void RAISE_AN_EXCEPTION()
{
_asm
{
INT 3
INT 3
INT 3
INT 3
}
}
int _tmain(int argc, _TCHAR* argv[])
{
__try
{
__try{
printf("1: Raise an Exception\n");
RAISE_AN_EXCEPTION();
}
__finally
{
printf("2: In Finally\n");
}
}
__except( printf("3: In Filter\n"), EXCEPTION_EXECUTE_HANDLER )
{
printf("4: In Exception Handler\n");
}
return 0;
}
; main()
00401000: PUSH EBP
00401001: MOV EBP,ESP
00401003: PUSH -1
00401005: PUSH 00407160
; __try {
; the structured exception handler (SEH) installation
0040100A: PUSH _except_handler3
0040100F: MOV EAX,DWORD PTR FS:[0]
00401015: PUSH EAX
00401016: MOV DWORD PTR FS:[0],ESP
0040101D: SUB ESP,8
00401020: PUSH EBX
00401021: PUSH ESI
00401022: PUSH EDI
00401023: MOV DWORD PTR SS:[EBP-18],ESP
; __try {
00401026: XOR ESI,ESI
00401028: MOV DWORD PTR SS:[EBP-4],ESI
0040102B: MOV DWORD PTR SS:[EBP-4],1
00401032: PUSH OFFSET "1: Raise an Exception"
00401037: CALL printf
0040103C: ADD ESP,4
; the raise a exception, INT 3 exception
; RAISE_AN_EXCEPTION()
0040103F: INT3
00401040: INT3
00401041: INT3
00401042: INT3
; } __finally {
00401043: MOV DWORD PTR SS:[EBP-4],ESI
00401046: CALL 0040104D
0040104B: JMP 00401080
0040104D: PUSH OFFSET "2: In Finally"
00401052: CALL printf
00401057: ADD ESP,4
0040105A: RETN
; }
; }
; __except(
0040105B: JMP 00401080
0040105D: PUSH OFFSET "3: In Filter"
00401062: CALL printf
00401067: ADD ESP,4
0040106A: MOV EAX,1 ; EXCEPTION_EXECUTE_HANDLER = 1
0040106F: RETN
; , EXCEPTION_EXECUTE_HANDLER )
; {
; the exception handler funtion
00401070: MOV ESP,DWORD PTR SS:[EBP-18]
00401073: PUSH OFFSET "4: In Exception Handler"
00401078: CALL printf
0040107D: ADD ESP,4
; }
00401080: MOV DWORD PTR SS:[EBP-4],-1
0040108C: XOR EAX,EAX
; restore previous SEH
0040108E: MOV ECX,DWORD PTR SS:[EBP-10]
00401091: MOV DWORD PTR FS:[0],ECX
00401098: POP EDI
00401099: POP ESI
0040109A: POP EBX
0040109B: MOV ESP,EBP
0040109D: POP EBP
0040109E: RETN
创建一个 Win32 控制台项目,并链接和运行前面的 C++ 代码,以观察结果。
1:引发异常 3:在 Filter 中 2:在 Finally 中 4:在 Exception Handler 中 _ |
此程序在发生异常时运行异常表达式 printf("3: In Filter\n");
,在本例中是 INT 3
异常。您也可以使用其他类型的异常。在 OllyDbg 中,**Debugging options->Exceptions**,您可以看到不同类型异常的简短列表。
5.3.1 实现异常处理程序
我们希望构造一个结构化异常处理程序来到达 OEP。现在,我认为您已经通过前面的汇编代码区分了 SEH 的安装、异常的引发和异常表达式过滤器。为了建立我们的异常处理程序方法,我们需要包含以下代码:
- SEH 安装:
LEA EAX,[EBP+_except_handler1_OEP_Jump] PUSH EAX PUSH DWORD PTR FS:[0] MOV DWORD PTR FS:[0],ESP
- 异常引发:
INT 3
- 异常处理程序表达式过滤器:
_except_handler1_OEP_Jump: PUSH EBP MOV EBP,ESP ... MOV EAX, EXCEPTION_CONTINUE_SEARCH // EXCEPTION_CONTINUE_SEARCH = 0 LEAVE RETN
因此,我们希望通过汇编语言编写以下 C++ 代码来启动我们的引擎,通过 SEH 来到达入口点偏移量。
__try // SEH installation
{
__asm
{
INT 3 // An Exception Raise
}
}
__except( ..., EXCEPTION_CONTINUE_SEARCH ){}
// Exception handler expression filter
在汇编代码中...
; ----------------------------------------------------
; the structured exception handler (SEH) installation
; __try {
LEA EAX,[EBP+_except_handler1_OEP_Jump]
PUSH EAX
PUSH DWORD PTR FS:[0]
MOV DWORD PTR FS:[0],ESP
; ----------------------------------------------------
; the raise a INT 3 exception
INT 3
INT 3
INT 3
INT 3
; }
; __except( ...
; ----------------------------------------------------
; exception handler expression filter
_except_handler1_OEP_Jump:
PUSH EBP
MOV EBP,ESP
...
MOV EAX, EXCEPTION_CONTINUE_SEARCH ; EXCEPTION_CONTINUE_SEARCH = 0
LEAVE
RETN
; , EXCEPTION_CONTINUE_SEARCH ) { }
异常值 __except(..., Value)
决定了异常如何处理,它可以有三个值:1、0、-1。要理解它们,请参考 MSDN 库中 try
-except
语句 的说明。我们将其设置为 EXCEPTION_CONTINUE_SEARCH (0)
,不运行异常处理程序函数,因此通过此值,异常未被识别,被简单忽略,线程继续其代码执行。
SEH 安装的实现方式
从示例代码中可以看出,SEH 的安装是通过 FS 段寄存器完成的。Microsoft Windows 32 位使用 FS 段寄存器作为指向主线程数据块的指针。前0x1C几个字节包含 线程信息块 (TIB) 的信息。因此,FS:[00h]
指向主线程的 ExceptionList
,表 3。在我们的代码中,我们将指向 _except_handler1_OEP_Jump
的指针推送到堆栈上,并将 ExceptionList
的值(FS:[00h]
)更改为堆栈的开始(ESP
)。
线程信息块 (TIB)
typedef struct _NT_TIB32 {
DWORD ExceptionList;
DWORD StackBase;
DWORD StackLimit;
DWORD SubSystemTib;
union {
DWORD FiberData;
DWORD Version;
};
DWORD ArbitraryUserPointer;
DWORD Self;
} NT_TIB32, *PNT_TIB32;
表 3 - FS 段寄存器和线程信息块
DWORD PTR FS:[00h] | ExceptionList |
DWORD PTR FS:[04h] | StackBase |
DWORD PTR FS:[08h] | StackLimit |
DWORD PTR FS:[0Ch] | SubSystemTib |
DWORD PTR FS:[10h] | FiberData / Version |
DWORD PTR FS:[14h] | ArbitraryUserPointer |
DWORD PTR FS:[18h] | Self |
5.3.2 通过调整线程上下文到达 OEP
在本部分,我们通过完成 OEP 方法来执行我们的性能。我们更改线程的上下文并忽略所有简单的异常处理,让线程继续执行,但到达原始 OEP!
当发生异常时,异常发生时的处理器上下文会被保存在堆栈中。通过 EXCEPTION_POINTERS
,我们可以访问 ContextRecord
的指针。ContextRecord
包含 CONTEXT
数据结构,表 4,这是异常发生时的线程上下文。当我们通过 EXCEPTION_CONTINUE_SEARCH (0)
忽略异常时,指令指针和上下文将被设置为 ContextRecord
以返回到先前状态。因此,如果我们更改 Win32 线程上下文 中的 Eip
为原始入口点偏移量,它将清晰地进入 OEP。
MOV EAX, ContextRecord
MOV EDI, dwOEP ; EAX <- dwOEP
MOV DWORD PTR DS:[EAX+0B8h], EDI ; pContext.Eip <- EAX
Win32 线程上下文结构
#define MAXIMUM_SUPPORTED_EXTENSION 512
typedef struct _CONTEXT {
//-----------------------------------------
DWORD ContextFlags;
//-----------------------------------------
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//-----------------------------------------
FLOATING_SAVE_AREA FloatSave;
//-----------------------------------------
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//-----------------------------------------
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//-----------------------------------------
DWORD Ebp;
DWORD Eip;
DWORD SegCs;
DWORD EFlags;
DWORD Esp;
DWORD SegSs;
//-----------------------------------------
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
//----------------------------------------
} CONTEXT,
*LPCONTEXT;
表 4 - CONTEXT
Context Flags | 0x00000000 | ContextFlags | |
Context Debug Registers |
0x00000004 | Dr0 | |
0x00000008 | Dr1 | ||
0x0000000C | Dr2 | ||
0x00000010 | Dr3 | ||
0x00000014 | Dr6 | ||
0x00000018 | Dr7 | ||
Context Floating Point |
0x0000001C | FloatSave |
StatusWord |
0x00000020 | StatusWord | ||
0x00000024 | TagWord | ||
0x00000028 | ErrorOffset | ||
0x0000002C | ErrorSelector | ||
0x00000030 | DataOffset | ||
0x00000034 | DataSelector | ||
0x00000038 ... 0x00000087 |
RegisterArea [0x50] | ||
0x00000088 | Cr0NpxState | ||
Context Segments | 0x0000008C | SegGs | |
0x00000090 | SegFs | ||
0x00000094 | SegEs | ||
0x00000098 | SegDs | ||
Context Integer | 0x0000009C | Edi | |
0x000000A0 | Esi | ||
0x000000A4 | Ebx | ||
0x000000A8 | Edx | ||
0x000000AC | Ecx | ||
0x000000B0 | Eax | ||
Context Control | 0x000000B4 | Ebp | |
0x000000B8 | Eip | ||
0x000000BC | SegCs | ||
0x000000C0 | EFlags | ||
0x000000C4 | Esp | ||
0x000000C8 | SegSs | ||
Context Extended Registers |
0x000000CC |
ExtendedRegisters [0x200] |
通过以下代码,我们实现了通过结构化异常处理程序到达 OEP 的主要目标。
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
PUSHAD // Save the registers context in stack
CALL Main_1
Main_1:
POP EBP
SUB EBP,OFFSET Main_1 // Get Base EBP
MOV EAX,DWORD PTR [EBP+_RO_dwImageBase]
ADD EAX,DWORD PTR [EBP+_RO_dwOrgEntryPoint]
MOV DWORD PTR [ESP+10h],EAX // pStack.Ebx <- EAX
LEA EAX,[EBP+_except_handler1_OEP_Jump]
MOV DWORD PTR [ESP+1Ch],EAX // pStack.Eax <- EAX
POPAD // Restore the first registers context from stack
//----------------------------------------------------
// the structured exception handler (SEH) installation
PUSH EAX
XOR EAX, EAX
PUSH DWORD PTR FS:[0] // NT_TIB32.ExceptionList
MOV DWORD PTR FS:[0],ESP // NT_TIB32.ExceptionList <-ESP
//----------------------------------------------------
// the raise a INT 3 exception
DWORD_TYPE(0xCCCCCCCC)
//--------------------------------------------------------
// -------- exception handler expression filter ----------
_except_handler1_OEP_Jump:
PUSH EBP
MOV EBP,ESP
//------------------------------
MOV EAX,DWORD PTR SS:[EBP+010h] // PCONTEXT: pContext <- EAX
//==============================
PUSH EDI
// restore original SEH
MOV EDI,DWORD PTR DS:[EAX+0C4h] // pContext.Esp
PUSH DWORD PTR DS:[EDI]
POP DWORD PTR FS:[0]
ADD DWORD PTR DS:[EAX+0C4h],8 // pContext.Esp
//------------------------------
// set the Eip to the OEP
MOV EDI,DWORD PTR DS:[EAX+0A4h] // EAX <- pContext.Ebx
MOV DWORD PTR DS:[EAX+0B8h],EDI // pContext.Eip <- EAX
//------------------------------
POP EDI
//==============================
MOV EAX, EXCEPTION_CONTINUE_SEARCH
LEAVE
RETN
//----------------------------------
DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase: DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint: DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
6 构建导入表并重构原始导入表
要使用 Windows 动态链接库 (DLL) 在 Windows 应用程序编程中,有两种方式:
- 使用具有附加依赖项的 Windows 库:
- 在运行时使用 Windows 动态链接库:
// DLL function signature typedef HGLOBAL (*importFunction_GlobalAlloc)(UINT, SIZE_T); ... importFunction_GlobalAlloc __GlobalAlloc; // Load DLL file HINSTANCE hinstLib = LoadLibrary("Kernel32.dll"); if (hinstLib == NULL) { // Error - unable to load DLL } // Get function pointer __GlobalAlloc = (importFunction_GlobalAlloc)GetProcAddress(hinstLib, "GlobalAlloc"); if (addNumbers == NULL) { // Error - unable to find DLL function } FreeLibrary(hinstLib);
当您创建一个 Windows 应用程序项目时,链接器会在您的项目的基本依赖项中包含至少 kernel32.dll。没有 Kernel32.dll 的 LoadLibrary()
和 GetProcAddress()
,我们就无法在运行时加载 DLL。依赖项信息存储在导入表节中。使用 Dependency Walker,可以很容易地查看 DLL 模块以及导入到 PE 文件中的函数。
我们尝试构建自定义导入表来完成我们的项目。此外,我们必须在最后修复原始导入表,以便运行程序的真实代码。
PE Maker - 步骤 3
6.1 构建客户端导入表
我强烈建议您阅读 Microsoft Portable Executable and the Common Object File Format Specification 文档的第 6.4 节。本节包含理解导入表性能的主要信息。
导入表数据可以通过可选头从 PE 头中的第二个数据目录访问,因此您可以使用以下代码访问它:
DWORD dwVirtualAddress = image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress;
DWORD dwSize = image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size;
VirtualAddress
指向 IMAGE_IMPORT_DESCRIPTOR
结构。此结构包含指向导入的 DLL 名称和第一个 Thunk 的相对虚拟地址的指针。
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name; // the imported DLL name
DWORD FirstThunk; // the relative virtual address of the first thunk
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
当程序运行时,Windows 任务管理器会用函数虚拟地址设置 Thunk。通过函数名称找到虚拟地址。最初,Thunk 保存函数名称的相对虚拟地址,表 5;在执行期间,它们会被函数的虚拟地址修复,表 6。
表 5 - 文件映像中的导入表
IMAGE_IMPORT_ |
OriginalFirstThunk |
||||
TimeDateStamp | |||||
ForwarderChain | |||||
Name_RVA |
------> | "kernel32.dll",0 | |||
FirstThunk_RVA |
------> | proc_1_name_RVA |
------> | 0,0,"LoadLibraryA",0 | |
proc_2_name_RVA |
------> | 0,0,"GetProcAddress",0 | |||
proc_3_name_RVA |
------> | 0,0,"GetModuleHandleA",0 | |||
... | |||||
IMAGE_IMPORT_ |
|||||
... |
|||||
IMAGE_IMPORT_ |
表 6 - 虚拟内存中的导入表
IMAGE_IMPORT_DESCRIPTOR[0] |
OriginalFirstThunk |
||
TimeDateStamp | |||
ForwarderChain | |||
Name_RVA |
------> |
"kernel32.dll",0 | |
FirstThunk_RVA |
------> |
proc_1_VA | |
proc_2_VA | |||
proc_3_VA | |||
... | |||
IMAGE_IMPORT_DESCRIPTOR[1] |
|||
... |
|||
IMAGE_IMPORT_DESCRIPTOR[n] |
我们想创建一个简单的导入表,从 Kernel32.dll 中导入 LoadLibrary()
和 GetProcAddress()
。我们需要这两个必需的 API 函数来覆盖运行时中的其他 API 函数。下面的汇编代码显示了我们如何轻松地实现我们的解决方案。
0101F000:
00000000 ; OriginalFirstThunk
0101F004: 00000000 ; TimeDateStamp
0101F008: 00000000 ; ForwarderChain
0101F00C: 0001F034 ; Name; ImageBase + 0001F034 -> 0101F034 -> "Kernel32.dll",0
0101F010: 0001F028 ; FirstThunk; ImageBase + 0001F028 -> 0101F028
0101F014: 00000000
0101F018: 00000000
0101F01C: 00000000
0101F020: 00000000
0101F024: 00000000
0101F028: 0001F041 ; ImageBase + 0001F041 -> 0101F041 -> 0,0,"LoadLibraryA",0
0101F02C: 0001F050 ; ImageBase + 0001F050 -> 0101F050 -> 0,0,"GetProcAddress",0
0101F030: 00000000
0101F034: 'K' 'e' 'r' 'n' 'e' 'l' '3' '2' '.' 'd' 'l' 'l' 00
0001F041: 00 00 'L' 'o' 'a' 'd' 'L' 'i' 'b' 'r' 'a' 'r' 'y' 'A'
00
0001F050: 00 00 'G' 'e' 't' 'P' 'r' 'o' 'c' 'A' 'd' 'd' 'r' 'e' 's' 's'
00
运行后...
0101F000:
00000000 ; OriginalFirstThunk
0101F004: 00000000 ; TimeDateStamp
0101F008: 00000000 ; ForwarderChain
0101F00C: 0001F034 ; Name; ImageBase + 0001F034 -> 0101F034 -> "Kernel32.dll",0
0101F010: 0001F028 ; FirstThunk; ImageBase + 0001F028 -> 0101F028
0101F014: 00000000
0101F018: 00000000
0101F01C: 00000000
0101F020: 00000000
0101F024: 00000000
0101F028: 7C801D77 ; -> Kernel32.LoadLibrary()
0101F02C: 7C80AC28 ; -> Kernel32.GetProcAddress()
0101F030: 00000000
0101F034: 'K' 'e' 'r' 'n' 'e' 'l' '3' '2' '.' 'd' 'l' 'l'
00
0001F041: 00 00 'L' 'o' 'a' 'd' 'L' 'i' 'b' 'r' 'a' 'r' 'y' 'A'
00
0001F050: 00 00 'G' 'e' 't' 'P' 'r' 'o' 'c' 'A' 'd' 'd' 'r' 'e' 's' 's'
00
我准备了一个类库,通过客户端字符串表来制作每个导入表。itmaker.h 中的 CITMaker
类库将通过 sz_IT_EXE_strings
和导入表的相对虚拟地址来构建导入表。
static const char *sz_IT_EXE_strings[]=
{
"Kernel32.dll",
"LoadLibraryA",
"GetProcAddress",
0,,
0,
};
我们随后使用这个类库来构建一个导入表来支持 DLL 和 OCX,因此这是一个通用的库,可以轻松地呈现所有可能的导入表。下一步在以下代码中阐明。
CITMaker *ImportTableMaker = new CITMaker( IMPORT_TABLE_EXE );
...
pimage_section_header=AddNewSection( ".xxx", dwNewSectionSize );
// build import table by the current virtual address
ImportTableMaker->Build
( pimage_section_header->VirtualAddress );
memcpy( pNewSection, ImportTableMaker->pMem,
ImportTableMaker->dwSize );
...
memcpy( image_section[image_nt_headers->FileHeader.NumberOfSections-1],
pNewSection,
dwNewSectionSize );
...
image_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress
= pimage_section_header->VirtualAddress;
image_nt_headers->OptionalHeader.
DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size
= ImportTableMaker->dwSize;
...
delete ImportTableMaker;
导入表被复制到新节的开头,相关的目录被调整到新节的相对虚拟地址和新导入表的大小。
6.2 在运行时使用其他 API 函数
此时,我们可以使用 LoadLibrary()
和 GetProcAddress()
加载其他 DLL 并查找其他函数的进程地址。
lea edi, @"Kernel32.dll"
//-------------------
push edi
mov eax,offset _p_LoadLibrary
call [ebp+eax] //LoadLibrary(lpLibFileName);
//-------------------
mov esi,eax // esi -> hModule
lea edi, @"GetModuleHandleA"
//-------------------
push edi
push esi
mov eax,offset _p_GetProcAddress
call [ebp+eax] //GetModuleHandle=GetProcAddress(hModule, lpProcName);
//--------------------
我想有一个完整的导入函数表,其性能类似于实际 EXE 文件中的。如果您查看 PE 文件内部,您会发现 API 调用是通过 API 函数的虚拟地址的间接跳转完成的。
JMP DWORD PTR [XXXXXXXX]
...
0101F028: 7C801D77 ; Virtual Address of kernel32.LoadLibrary()
...
0101F120: JMP DWORD PTR [0101F028]
...
0101F230: CALL 0101F120 ; JMP to kernel32.LoadLibrary
...
这使得通过此性能扩展我们项目的其他部分变得容易,因此我们构建了两个数据表:第一个用于 API 虚拟地址,第二个用于 JMP [XXXXXXXX]
。
#define __jmp_api byte_type(0xFF) byte_type(0x25)
__asm
{
...
//----------------------------------------------------------------
_p_GetModuleHandle: dword_type(0xCCCCCCCC)
_p_VirtualProtect: dword_type(0xCCCCCCCC)
_p_GetModuleFileName: dword_type(0xCCCCCCCC)
_p_CreateFile: dword_type(0xCCCCCCCC)
_p_GlobalAlloc: dword_type(0xCCCCCCCC)
//----------------------------------------------------------------
_jmp_GetModuleHandle: __jmp_api dword_type(0xCCCCCCCC)
_jmp_VirtualProtect: __jmp_api dword_type(0xCCCCCCCC)
_jmp_GetModuleFileName: __jmp_api dword_type(0xCCCCCCCC)
_jmp_CreateFile: __jmp_api dword_type(0xCCCCCCCC)
_jmp_GlobalAlloc: __jmp_api dword_type(0xCCCCCCCC)
//----------------------------------------------------------------
...
}
在下面的代码中,我们完成了安装自定义内部导入表的雄心!(我们不能称之为导入表。)
...
lea edi,[ebp+_p_szKernel32]
lea ebx,[ebp+_p_GetModuleHandle]
lea ecx,[ebp+_jmp_GetModuleHandle]
add ecx,02h
_api_get_lib_address_loop:
push ecx
push edi
mov eax,offset _p_LoadLibrary
call [ebp+eax] //LoadLibrary(lpLibFileName);
pop ecx
mov esi,eax // esi -> hModule
push edi
call __strlen
add esp,04h
add edi,eax
_api_get_proc_address_loop:
push ecx
push edi
push esi
mov eax,offset _p_GetProcAddress
call [ebp+eax]//GetModuleHandle=GetProcAddress(hModule, lpProcName);
pop ecx
mov [ebx],eax
mov [ecx],ebx // JMP DWORD PTR [XXXXXXXX]
add ebx,04h
add ecx,06h
push edi
call __strlen
add esp,04h
add edi,eax
mov al,byte ptr [edi]
test al,al
jnz _api_get_proc_address_loop
inc edi
mov al,byte ptr [edi]
test al,al
jnz _api_get_lib_address_loop
...
6.3 修复原始导入表
为了再次运行程序,我们应该修复实际导入表的 Thunk,否则我们将得到一个损坏的目标 PE 文件。我们的代码必须更正所有 Thunk,就像 表 5 到 表 6 一样。再次,LoadLibrary()
和 GetProcAddress()
协助我们实现目标。
...
mov ebx,[ebp+_p_dwImportVirtualAddress]
test ebx,ebx
jz _it_fixup_end
mov esi,[ebp+_p_dwImageBase]
add ebx,esi // dwImageBase + dwImportVirtualAddress
_it_fixup_get_lib_address_loop:
mov eax,[ebx+00Ch] // image_import_descriptor.Name
test eax,eax
jz _it_fixup_end
mov ecx,[ebx+010h] // image_import_descriptor.FirstThunk
add ecx,esi
mov [ebp+_p_dwThunk],ecx // dwThunk
mov ecx,[ebx] // image_import_descriptor.Characteristics
test ecx,ecx
jnz _it_fixup_table
mov ecx,[ebx+010h]
_it_fixup_table:
add ecx,esi
mov [ebp+_p_dwHintName],ecx // dwHintName
add eax,esi // image_import_descriptor.Name + dwImageBase = ModuleName
push eax // lpLibFileName
mov eax,offset _p_LoadLibrary
call [ebp+eax] // LoadLibrary(lpLibFileName);
test eax,eax
jz _it_fixup_end
mov edi,eax
_it_fixup_get_proc_address_loop:
mov ecx,[ebp+_p_dwHintName] // dwHintName
mov edx,[ecx] // image_thunk_data.Ordinal
test edx,edx
jz _it_fixup_next_module
test edx,080000000h // .IF( import by ordinal )
jz _it_fixup_by_name
and edx,07FFFFFFFh // get ordinal
jmp _it_fixup_get_addr
_it_fixup_by_name:
add edx,esi // image_thunk_data.Ordinal + dwImageBase = OrdinalName
inc edx
inc edx // OrdinalName.Name
_it_fixup_get_addr:
push edx //lpProcName
push edi // hModule
mov eax,offset _p_GetProcAddress
call [ebp+eax] // GetProcAddress(hModule, lpProcName);
mov ecx,[ebp+_p_dwThunk] // dwThunk
mov [ecx],eax // correction the thunk
// dwThunk => next dwThunk
add dword ptr [ebp+_p_dwThunk], 004h
// dwHintName => next dwHintName
add dword ptr [ebp+_p_dwHintName],004h
jmp _it_fixup_get_proc_address_loop
_it_fixup_next_module:
add ebx,014h // sizeof(IMAGE_IMPORT_DESCRIPTOR)
jmp _it_fixup_get_lib_address_loop
_it_fixup_end:
...
7 支持 DLL 和 OCX
现在,我们打算将 动态链接库 (DLL) 和 OLE-ActiveX 控件包含到我们的 PE 构建器项目中。支持它们非常容易,如果我们注意入口点偏移量的两次到达、重定位表实现和客户端导入表。
PE Maker - 步骤 4
7.1 双重 OEP 方法
DLL 文件或 OCX 文件的入口点偏移量至少被主程序触及两次。
- 构造函数:
当 DLL 被
LoadLibrary()
加载,或者 OCX 通过调用DllRegisterServer()
使用LoadLibrary()
和GetProcAddress()
注册时,第一次 OEP 到达发生。hinstDLL = LoadLibrary( "test1.dll" );
hinstOCX = LoadLibrary( "test1.ocx" ); _DllRegisterServer = GetProcAddress( hinstOCX, "DllRegisterServer" ); _DllRegisterServer(); // ocx register
- 析构函数:
当主程序通过
FreeLibrary()
释放库使用时,第二次 OEP 到达发生。FreeLibrary( hinstDLL );
FreeLibrary( hinstOCX );
为了实现这一点,我使用了一个技巧,导致在第二次时,指令指针 (EIP) 通过结构化异常处理程序再次指向原始 OEP。
_main_0:
pushad // save the registers context in stack
call _main_1
_main_1:
pop ebp
sub ebp,offset _main_1 // get base ebp
//---------------- support dll, ocx -----------------
_support_dll_0:
jmp _support_dll_1 // nop; nop; // << trick // in the second time OEP
jmp _support_dll_2
_support_dll_1:
//----------------------------------------------------
...
//---------------- support dll, ocx 1 ---------------
mov edi,[ebp+_p_dwImageBase]
add edi,[edi+03Ch]// edi -> IMAGE_NT_HEADERS
mov ax,word ptr [edi+016h]// edi -> image_nt_headers->FileHeader.Characteristics
test ax,IMAGE_FILE_DLL
jz _support_dll_2
mov ax, 9090h // << trick
mov word ptr [ebp+_support_dll_0],ax
_support_dll_2:
//----------------------------------------------------
...
into OEP by SEH ...
我希望您已经理解了前面代码中的技巧,但这还不是全部,当我们使用的映像基址与主程序加载库时使用的映像基址不同时,我们有一个问题。我们应该编写一些代码来查找实际的映像基址并存储它以备将来使用。
mov eax,[esp+24h] // the real imagebase
mov ebx,[esp+30h] // oep
cmp eax,ebx
ja _no_dll_pe_file_0
cmp word ptr [eax],IMAGE_DOS_SIGNATURE
jne _no_dll_pe_file_0
mov [ebp+_p_dwImageBase],eax
_no_dll_pe_file_0:
这段代码通过检查堆栈信息来查找实际的映像基址。通过使用实际映像基址和正式映像基址,我们应该修正映像程序中的所有内存调用!别担心,这可以通过重定位表信息简单地完成。
7.2 实现重定位表
要更好地理解重定位表,您可以参考 Microsoft Portable Executable and Common Object File Format Specification 文档的第 6.6 节。重定位表包含许多包,用于重定位与虚拟内存映像中的虚拟地址相关的信息。每个包包含一个 8 字节的头,用于显示基址虚拟地址和数据数量,由 IMAGE_BASE_RELOCATION
数据结构表示。
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
表 7 - 重定位表
Block[1] | VirtualAddress | |||
SizeOfBlock | ||||
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
... | ... | ... | ... | |
type:4 | offset:12 | 00 | 00 | |
Block[2] | VirtualAddress | |||
SizeOfBlock | ||||
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
... | ... | ... | ... | |
type:4 | offset:12 | 00 | 00 | |
... |
... | |||
Block[n] | VirtualAddress | |||
SizeOfBlock | ||||
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
type:4 | offset:12 | type:4 | offset:12 | |
... | ... | ... | ... | |
type:4 | offset:12 | 00 | 00 |
表 7 说明了重定位表的主要思想。此外,您可以将 DLL 或 OCX 文件上传到 OllyDbg 中,通过 *Memory map window* 观察重定位表,即 ".reloc" 节。顺便说一下,我们在项目中使用以下代码找到重定位表的位置:
DWORD dwVirtualAddress = image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].
VirtualAddress;
DWORD dwSize = image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].Size;
通过 OllyDbg,使用长十六进制查看器模式,我们得到与 ".reloc" 节类似的结果。在此示例中,基址虚拟地址为 **0x1000**,块大小为 **0x184**。
008E1000 : 00001000 00000184 30163000 30403028
008E1010 : 30683054 308C3080 30AC309C 30D830CC
008E1020 : 30E030DC 30E830E4 30F030EC 310030F4
008E1030 : 3120310D 315F3150 31A431A0 31C031A8
008E1040 : 31D031CC 31F431EC 31FC31F8 32043200
008E1050 : 320C3208 32143210 324C322C 32583254
008E1060 : 3260325C 32683264 3270326C 32B03274
它重定位后续虚拟地址中的数据。
0x1000 + 0x0000 = 0x1000
0x1000 + 0x0016 = 0x1016
0x1000 + 0x0028 = 0x1028
0x1000 + 0x0040 = 0x1040
0x1000 + 0x0054 = 0x1054
...
每个包通过使用其内部信息的连续 4 字节来执行重定位。第一个字节表示重定位类型,接下来的三个字节是偏移量,必须与基址虚拟地址和映像基址一起使用以修正映像信息。
type | offset | ||
03 | 00 | 00 | 00 |
类型是什么
类型可以是以下值之一:
IMAGE_REL_BASED_ABSOLUTE (0)
:无效果。IMAGE_REL_BASED_HIGH (1)
:通过基址虚拟地址和偏移量的高 16 位进行重定位。IMAGE_REL_BASED_LOW (2)
:通过基址虚拟地址和偏移量的低 16 位进行重定位。IMAGE_REL_BASED_HIGHLOW (3)
:通过基址虚拟地址和偏移量进行重定位。
重定位中做了什么?
通过重定位,虚拟内存中的某些值根据当前映像基址由 ".reloc" 节包进行修正。
delta_ImageBase = current_ImageBase - image_nt_headers->OptionalHeader.ImageBase |
mem[ current_ImageBase + 0x1000 ] =
mem[ current_ImageBase + 0x1000 ] + delta_ImageBase ;
mem[ current_ImageBase + 0x1016 ] =
mem[ current_ImageBase + 0x1016 ] + delta_ImageBase ;
mem[ current_ImageBase + 0x1028 ] =
mem[ current_ImageBase + 0x1028 ] + delta_ImageBase ;
mem[ current_ImageBase + 0x1040 ] =
mem[ current_ImageBase + 0x1040 ] + delta_ImageBase ;
mem[ current_ImageBase + 0x1054 ] =
mem[ current_ImageBase + 0x1054 ] + delta_ImageBase ;
...
我使用了 Morphine 打包器的以下代码来实现重定位。
...
_reloc_fixup:
mov eax,[ebp+_p_dwImageBase]
mov edx,eax
mov ebx,eax
add ebx,[ebx+3Ch] // edi -> IMAGE_NT_HEADERS
mov ebx,[ebx+034h]// edx ->image_nt_headers->OptionalHeader.ImageBase
sub edx,ebx // edx -> reloc_correction // delta_ImageBase
je _reloc_fixup_end
mov ebx,[ebp+_p_dwRelocationVirtualAddress]
test ebx,ebx
jz _reloc_fixup_end
add ebx,eax
_reloc_fixup_block:
mov eax,[ebx+004h] //ImageBaseRelocation.SizeOfBlock
test eax,eax
jz _reloc_fixup_end
lea ecx,[eax-008h]
shr ecx,001h
lea edi,[ebx+008h]
_reloc_fixup_do_entry:
movzx eax,word ptr [edi]//Entry
push edx
mov edx,eax
shr eax,00Ch //Type = Entry >> 12
mov esi,[ebp+_p_dwImageBase]//ImageBase
and dx,00FFFh
add esi,[ebx]
add esi,edx
pop edx
_reloc_fixup_HIGH: // IMAGE_REL_BASED_HIGH
dec eax
jnz _reloc_fixup_LOW
mov eax,edx
shr eax,010h //HIWORD(Delta)
jmp _reloc_fixup_LOW_fixup
_reloc_fixup_LOW: // IMAGE_REL_BASED_LOW
dec eax
jnz _reloc_fixup_HIGHLOW
movzx eax,dx //LOWORD(Delta)
_reloc_fixup_LOW_fixup:
add word ptr [esi],ax// mem[x] = mem[x] + delta_ImageBase
jmp _reloc_fixup_next_entry
_reloc_fixup_HIGHLOW: // IMAGE_REL_BASED_HIGHLOW
dec eax
jnz _reloc_fixup_next_entry
add [esi],edx // mem[x] = mem[x] + delta_ImageBase
_reloc_fixup_next_entry:
inc edi
inc edi //Entry++
loop _reloc_fixup_do_entry
_reloc_fixup_next_base:
add ebx,[ebx+004h]
jmp _reloc_fixup_block
_reloc_fixup_end:
...
7.3 构建特殊导入表
为了支持 OLE-ActiveX 控件注册,我们应该为目标 OCX 和 DLL 文件提供合适的导入表。
因此,我使用以下字符串创建了一个导入表。
const char *sz_IT_OCX_strings[]=
{
"Kernel32.dll",
"LoadLibraryA",
"GetProcAddress",
"GetModuleHandleA",
0,
"User32.dll",
"GetKeyboardType",
"WindowFromPoint",
0,
"AdvApi32.dll",
"RegQueryValueExA",
"RegSetValueExA",
"StartServiceA",
0,
"Oleaut32.dll",
"SysFreeString",
"CreateErrorInfo",
"SafeArrayPtrOfIndex",
0,
"Gdi32.dll",
"UnrealizeObject",
0,
"Ole32.dll",
"CreateStreamOnHGlobal",
"IsEqualGUID",
0,
"ComCtl32.dll",
"ImageList_SetIconSize",
0,
0,
};
没有这些 API 函数,库就无法加载,而且 DllregisterServer()
和 DllUregisterServer()
将无法运行。在 CPECryptor::CryptFile
中,在创建新的导入表对象初始化时,我区分了 EXE 文件和 DLL 文件。
if(( image_nt_headers->FileHeader.Characteristics
& IMAGE_FILE_DLL ) == IMAGE_FILE_DLL )
{
ImportTableMaker = new CITMaker( IMPORT_TABLE_OCX );
}
else
{
ImportTableMaker = new CITMaker( IMPORT_TABLE_EXE );
}
8 保留线程局部存储
通过使用线程局部存储 (TLS),程序能够执行多线程进程,这种功能通常由 Borland 链接器使用:Delphi 和 C++ Builder。当您打包 PE 文件时,您应该注意保持 TLS 的清洁,否则,您的打包器将不支持 Borland Delphi 和 C++ Builder 链接的 EXE 文件。要理解 TLS,我将您引向 Microsoft Portable Executable and Common Object File Format Specification 文档的第 6.7 节,您可以通过 winnt.h 中的 IMAGE_TLS_DIRECTORY32
来观察 TLS 结构。
typedef struct _IMAGE_TLS_DIRECTORY32 {
DWORD StartAddressOfRawData;
DWORD EndAddressOfRawData;
DWORD AddressOfIndex;
DWORD AddressOfCallBacks;
DWORD SizeOfZeroFill;
DWORD Characteristics;
} IMAGE_TLS_DIRECTORY32, * PIMAGE_TLS_DIRECTORY32;
为了安全地保存 TLS 目录,我将其复制到加载器中的一个特殊位置。
...
_tls_dwStartAddressOfRawData: dword_type(0xCCCCCCCC)
_tls_dwEndAddressOfRawData: dword_type(0xCCCCCCCC)
_tls_dwAddressOfIndex: dword_type(0xCCCCCCCC)
_tls_dwAddressOfCallBacks: dword_type(0xCCCCCCCC)
_tls_dwSizeOfZeroFill: dword_type(0xCCCCCCCC)
_tls_dwCharacteristics: dword_type(0xCCCCCCCC)
...
有必要修正可选头中的 TLS 目录条目。
if(image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].
VirtualAddress!=0)
{
memcpy(&pDataTable->image_tls_directory,
image_tls_directory,
sizeof(IMAGE_TLS_DIRECTORY32));
dwOffset=DWORD(pData1)-DWORD(pNewSection);
dwOffset+=sizeof(t_DATA_1)-sizeof(IMAGE_TLS_DIRECTORY32);
image_nt_headers->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS].
VirtualAddress=dwVirtualAddress + dwOffset;
}
9 注入您的代码
我们已准备好将代码放置在新节中。我们的代码是通过 user32.dll 中的 MessageBox()
显示的“Hello World!”消息。
...
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) ;
...
PE Maker - 步骤 5
10 结论
通过本文,您已经了解了将代码注入可移植可执行文件是多么容易。您可以通过使用其他打包器的源代码来完成代码,以类似 Yoda's Protector 的方式创建打包器,并通过与 Morphine 的源代码混合来使您的打包器不可检测。我希望您喜欢这篇关于逆向工程领域一部分的简短讨论。下次再见!