65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (200投票s)

2005年12月27日

GPL3

33分钟阅读

viewsIcon

970929

downloadIcon

42777

本文演示了在不重新编译源代码的情况下,将代码注入可移植可执行(EXE、DLL、OCX等)文件的五个步骤。

下载次数

目录

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.1Win95Win98 都不是完美的 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_
HEADER
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
信息

IMAGE_
NT_HEADERS

签名 PE 签名 (PE)
000000F0  ASCII "PE"
IMAGE_
FILE_HEADER
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_
OPTIONAL_
HEADER32
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_
DATA_DIRECTORY[16]
导出表
导入表
资源表
异常表
证书文件
重定位表
调试数据
体系结构数据
全局指针
TLS 表
加载配置表
绑定导入表
导入地址表
延迟导入描述符
COM+ 运行时头
保留

信息
IMAGE_
SECTION_
HEADER[0]
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



IMAGE_
SECTION_
HEADER[n]
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
…



SECTION[n]
…
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 打包器开发人员来说,VirtualSizeVirtualAddressSizeOfRawDataPointerToRawDataCharacteristics 字段具有重要作用。在开发 EXE 打包器时,您应该足够聪明地利用它们。修改它们时需要注意一些事项;您需要注意将 VirtualSizeVirtualAddressOptionalHeader->SectionAlignment 对齐,以及 SizeOfRawDataPointerToRawDataOptionalHeader->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 只有对 DOSWindows 98Windows 2000 具有良好的功能。我发现这个调试器支持所有类型的 Windows 版本。因此,我开始快速学习它,现在它是我最喜欢的 Windows OS 调试器。它是一个可以用于跟踪除 Common Language Infrastructure (CLI) 文件格式之外的所有可移植可执行文件的调试器,在用户模式级别,通过使用 Windows 调试 APIOleh Yuschuk,作者,是我一生中见过的最值得尊敬的软件开发人员之一。他是一位乌克兰人,现居德国。我应该在这里提到,他的调试器是世界各地黑客和破解派对的最佳选择!它是免费软件!您可以从 OllyDbg 主页尝试。

图 2 - OllyDbg CPU 窗口

3.1.3 调试器界面中哪些部分很重要?

我介绍了两个调试器,但没有说明如何使用它们,也没有说明应该更多地关注哪些部分。关于调试器的使用,我将您引向其帮助文档中的说明。但是,我想简要解释一下调试器的重要部分;当然,我指的是低级别调试器,或者换句话说,x86 CPU 系列的机器语言调试器。

所有低级别调试器都包含以下子部分:

  1. 寄存器查看器。

    EAX
    ECX
    EDX
    EBX
    ESP
    EBP
    ESI
    EDI
    EIP

    od t s z a p c

  2. 反汇编器或代码查看器。

    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. 内存监视器。

    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..............

  4. 堆栈查看器。

    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 .... ....

  5. 用于跟踪调试过程的命令行、命令按钮或快捷键。

    命令 SoftICE OllyDbg
    Run F5 F9
    单步进入 F11 F7
    单步跳过 F10 F8
    设置断点 F8 F2

您可以比较 图 1图 2 来区分 SoftICE 和 OllyDbg 之间的区别。当您要跟踪 PE 文件时,您应该主要关注这五部分。此外,每个调试器都包含一些其他有用的部分;您应该自己去发现它们。

3.2 反汇编器

我们可以将 OllyDbg 和 SoftICE 视为出色的反汇编器,但我也想介绍另一个在逆向工程界很有名的反汇编器工具。

3.2.1 Proview 反汇编器

ProviewPVDasmReverse-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);    
    //-----------------------------------------
};

根据 表 1image_dos_headerpDosStubimage_nt_headersimage_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 对齐每个节的 VirtualAddressVirtualSize
    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 对齐每个节的 PointerToRawDataSizeOfRawData
    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
...
0x000002CB

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.dllLoadLibrary()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_
DESCRIPTOR[0]
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_
DESCRIPTOR[1]
...
IMAGE_IMPORT_
DESCRIPTOR[n]

表 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 链接器使用:DelphiC++ 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 的源代码混合来使您的打包器不可检测。我希望您喜欢这篇关于逆向工程领域一部分的简短讨论。下次再见!

© . All rights reserved.