PE注入代码


PE格式文件的代码注入
 
 
 
本文演示了在不需要重新编译源代码的情况下,怎样向Windows PE(Portable Executable)格式的文件(包括EXE、DLL、OCX)中注入自己的代码。
 
程序如图:
 
 
       前言
       或许,你想了解一个病毒程序是怎样把自身注入到一个正常的PE文件中的,又或者是,你为了保护某种数据而加密自己的PE文件,从而想实现一个打包或保护程序;而本文的目的,就是为了向大家展示,通常的EXE工具或某种恶意程序,是怎样实现上述目的的。
       可以基于本文中的代码创建一个你自己的EXE修改器,如果运用得当,它可以是一个EXE保护程序,相反的话,也可能发展成某种病毒。不管怎样,本文写作的意图是前者,对于任何不适当的用法,本文作者概不负责。
 
 
       首要条件
       对于阅读本文,不存在任何特定强制性的先决条件——即基础知识,如果你对调试器或者PE文件格式非常熟悉,可以跳过下面的两节,而这两节是为不具备调试器或PE文件格式知识的读者准备的。
 
 
       PE文件格式
       PE文件格式被定义用于在Windows操作系统上执行代码或存储运行程序所需的基本数据,例如:常量、变量、引入库的链接、资源数据等等,其由MS-DOS文件头信息、Windows NT文件信息、节头部及节映像等组成,见表1。
 
 
       MS-DOS头部数据
       MS-DOS头部数据让人回想起最初在Windows操作系统上部署程序的那些日子,那些在如Windows NT 3.51这类系统上部署程序的日子——这里是说,Win3.1、Win95、Win98不是开发所需的理想操作系统。这些MS-DOS数据的功能就是在你运行一个可执行文件时,调用一个函数显示出“This program can not be run in MS-DOS mode”或者“This program can be run only in Windows mode”,或者当你想在MS-DOS 6.0中运行一个Windows的EXE文件时,显示出一些类似的信息,所以这些数据是被保留在代码中以作提示之用,但其中最让人感兴趣的还是数据中的“MZ”,不管你相不相信,它是微软的一个程序员“Mark Zbikowski”的首字母缩写。
       对本文来说,MS-DOS头部数据中,只有PE标志的偏移量最重要,可据此来找出Windows NT信息数据的实际位置。在此,建议读者先看一下表1,再查看一下<Microsoft Visual Studio .NET>/VC7/PlatformSDK/include/目录中,或<Microsoft Visual Studio 6.0>/VC98/include/目录中的<winnt.h>头文件中的IMAGE_DOS_HEADER结构。
 
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE头"MZ"
    WORD   e_magic;                //神奇数字
    WORD   e_cblp;                 //文件尾页的字节数
    WORD   e_cp;                  //文件页数
    WORD   e_crlc;                 //重定位信息
    WORD   e_cparhdr;              //段落中头部信息的大小
    WORD   e_minalloc;             //所需的最小额外段
    WORD   e_maxalloc;             //所需的最大额外段
    WORD   e_ss;                   //初始(相对)SS值
    WORD   e_sp;                   //初始SP 值
    WORD   e_csum;                //校验和
    WORD   e_ip;                   //初始IP值
    WORD   e_cs;                   //初始(相对)CS值
    WORD   e_lfarlc;                //重定位表的文件地址
    WORD   e_ovno;                //覆盖数
    WORD   e_res[4];               //保留字数
    WORD   e_oemid;               // OEM标识符(为e_oeminfo准备)
    WORD   e_oeminfo;             // OEM信息;e_oemid详细说明
    WORD   e_res2[10];             //保留字数
    LONG   e_lfanew;               //新exe头中文件的地址
 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
 
       结构中e_lfanew代表了Windows NT数据位置的偏移量。以下程序演示了如何从一个EXE文件中获取相关的头部信息。
 
 
 
 
 
 
 
       请留心观察,这些例子对全文来说,都是很重要的。
 
 
MS-DOS头信息IMAGE_DOS_
HEADER
DOS EXE Signature
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

Reserved words
Offset to PE signature
MS-DOS Stub 
程序
00000040 
º
.´.Í!¸/LÍ!This program canno
00000060 t be run in DOS mode....$.......
Windows NT信息
IMAGE_
NT_HEADERS
SignaturePE signature (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
Reserved
SizeOfImage
SizeOfHeaders
CheckSum
Subsystem
DLLCharacteristics
SizeOfStackReserve
SizeOfStackCommit
SizeOfHeapReserve
SizeOfHeapCommit
LoaderFlags
NumberOfRvaAndSizes
IMAGE_
DATA_DIRECTORY[16]
Export Table
Import Table
Resource Table
Exception Table
Certificate File
Relocation Table
Debug Data
Architecture Data
Global Ptr
TLS Table
Load Config Table
Bound Import Table
Import Address Table
Delay Import Descriptor
COM+ Runtime Header
Reserved
节信息
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çw 8ç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 ........
表1:PE文件格式结构
 
 
       Windows NT信息数据
       正如前一部分所提到的,MS-DOS头数据结构中,e_lfanew代表了Windows NT头数据位置的偏移量。因此,如果假定pMem是指向特定PE文件内存空间起始点的指针,那么以下代码就可以获得MS-DOS头与Windows NT头。
 
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));
 
 
       取得PE文件的头部信息,看起来似乎很简单,不过还是推荐查阅一下MSDN中IMAGE_NT_HEADERS的结构定义,以领会NT头部映像中究竟包含了什么,以用于支持在Windows系列操作系统中运行代码。现在,你应该对Windows NT头部结构非常熟悉了吧,它由PE标志、文件头信息、可选头信息组成;另外别忘了,要看一下它们在MSDN中的注释。以下是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
 
 
       由上,可清楚地看到,当Windows操作系统分配虚拟内存空间时,这些值和它们所扮演的角色的主要作用,在此不便赘述,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(Windows x64可执行文件所用格式),还不得而知。
       举例来说,下列代码,就可以获取相对虚拟地址(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;
 
       要理解这些数据索引的重要性,建议参考微软的Microsoft Portable Executable and the Common Object File Format Specification,此外,在本文后叙部分,也会讨论到这些索引的作用。
 
 
       节头部信息
       我们刚才也看到了,一个PE格式文件,是怎样在磁盘上申请位置和区域大小的,是怎样通过IMAGE_NT_HEADERS->OptionalHeader->SizeOfImage指示Windows任务管理器为程序分配虚拟内存的,同样,为了更好地理解,建议还是查阅一下MSDN中IMAGE_SECTION_HEADER的结构定义。如果要开发一个EXE包装器,那么VirtualSize、VirtualAddress、SizeOfRawData、PointerToRawData和Characteristics这些部分无疑具有非常重要意义;并且在开发期间,你必须熟练运用它们,不但要根据OptionalHeader->SectionAlignment排列VirtualSize和VirtualAddress,还要OptionalHeader->FileAlignment排列SizeOfRawData和PointerToRawData,否则,有可能损坏目标EXE文件,导致其不能运行。关于Characteristics,我把大部分的精力都放在了通过IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE | IMAGE_SCN_CNT_INITIALIZED_DATA来确定一个节上,并且更倾向于在处理目标EXE文件时,新的节可以得到初始化,如导入表(import table);此外,如果也需要它可以通过加载器修改其自身,那此时Characteristics需设置为可写。
 
       另外,也需要多留意节名,因为可以通过名字知道其含义,此外还是建议参考Microsoft Portable Executable and the Common Object File Format Specification文档,表2中列出了一些重要的节名及其含义:
 
".text"
代码节
"CODE"
通过 Borland Delphi Borland Pascal 链接的代码节
".data"
数据节
"DATA"
通过 Borland Delphi Borland Pascal 链接的数据节
".rdata"
常量节
".idata"
导入表
".edata"
导出表
".tls"
TLS
".reloc"
重定位信息
".rsrc"
资源信息
表2:节名
 
     最后要提醒一点的是,IMAGE_NT_HEADERS->FileHeader-><CODE>NumberOfSections代表了一个PE文件中节的数目,如果你添加或删除了一个PE文件中的节,请不要忘记对它作相应的调整,这就是所谓的“节注入”!
 
 
       调试器
       要开发一个PE文件工具,首要条件是有足够的跟踪调试工具的使用经验,此外,还必须熟悉汇编指令,一般来说,Intel的官方文档是最好的参考资料。
       要跟踪一个PE文件,SoftICE可能是大家所知道的最好的工具了,它可在不使用Windows API方式的情况下,使用内核模式来进行跟踪。另外,在此也准备向大家介绍一款工作在用户模式下的堪称完美的调试器——“OllyDbg”,它利用了Windows的调试API来跟踪一个PE文件,并且可把自身附加到一个活动的进程;这些API在Windows Kernel32库中,由Microsoft Team提供,你可以使用它们来跟踪特定的进程,或者,也许能够做出自己的调试器,以下是其中一些API:CreateThread()、CreateProcess()、OpenProcess()、DebugActiveProcess()、GetThreadContext()、SetThreadContext()、ContinueDebugEvent()、DebugBreak()、ReadProcessMemory()、WriteProcessMemory()、SuspendThread()及ResumeThread()。
 
 
       SofeICE
       在1987年,Frank Grossman和Jim Moskun决定在新罕布什尔州的纳舒厄创办一家名为NuMega Technologies的公司,以开发一些工具,用于跟踪Microsoft Windows软件程序及测试其的可靠性。现在,这家公司已成为了Compuware Corporation的一部分,而且,它的产品加速了Windows软件产品的可靠性进程;另外,在Windows驱动程序开发方面,Compuware DriverStudio可谓是人人皆知,它通过Windows Driver Development Kit (DDK),实现了一个带有着内核驱动或系统文件详尽细节的环境,Windows系统软件开发人员由此不必再涉及到DDK,就可以直接开发内核模式级别的PE文件了。而在本文中,只用到了DriverStudio中的一个产品——SoftICE,这个调试器可用于跟踪每一个PE文件,而不管它是处在内核模式或用户模式下。
 
 
EAX=00000000  EBX=7FFDD000  ECX=0007FFB0 EDX=7C90EB94  ESI=FFFFFFFF
EDI=7C919738 
EBP=0007FFF0 ESP=0007FFC4 EIP=010119E0  o d i s  p  c
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
图1:SoftICE窗口
 
 
       OllyDbg
       大概是在4年前,一个很偶然的机会发现了这个调试器,从此就对它爱不释手,一方面因为SoftICE实在是价格太贵,另一方面,它只对DOS、Windows 98、Windows 2000支持得比较好,但OllyDbg几乎支持所有的Windows版本,它是一个可用于跟踪调试在用户模式下所有类型PE文件的调试器,但除了通用语言基础架构(CLI)程序。OllyDbg的作者Oleh Yuschuk是乌克兰人,现居住在德国,还要多提一点的是,他的调试器简直就是黑客和破解者的最佳选择,并且是免费软件,可以从 http://www.ollydbg.de/处下载试用。
 
 
 
 
       调试器中哪一部分最重要
       前面已经介绍过两款调试器了,而没有说到怎样使用它们或该多留意哪一部分,关于怎样使用调试器,还是建议参考它们的帮助文档。不过在此,还是要讲一些调试器中的重要部分,当然,我们说的是系统低级调试器,换句话来说,我们在此谈得是x86 CPU机器语言调试器。
 
所有的系统低级调试器都由以下部分组成:
 
1、 寄存器部分
EAX
ECX
EDX
EBX
ESP
EBP
ESI
EDI
EIP
o d 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、 用于调试过程的命令行、按钮、快捷键
Command
SoftICE
OllyDbg
Run
F5
F9
Step Into
F11
F7
Step Over
F10
F8
Set Break Point
F8
F2
 
 
       可以对比图1与图2,看一下SoftICE与OllyDbg有哪些不同之处。当开始跟踪一个PE文件时,以上五部分是最需要多留意的地方,此外,每一个调试器都有一些其他的有用工具,这就需要靠自己去摸索了。
 
 
       反汇编工具
       尽管SoftICE和OllyDbg都具有极其优秀的反汇编功能,但还是要介绍一下在逆向工程领域一些其他知名的反汇编工具。
 
² Proview或PVDasm:它们都是逆向工程社区中极受称赞的反汇编工具,你甚至可利用它的反汇编引擎,创建自己的反汇编工具。
² W32Dasm:可对16位及32位的可执行文件进行反汇编操作,另外,利用它的反汇编功能,还可以分析文件的导入表、导出表、资源数据目录等等。
² IDA Pro:所有逆向工程方面的高手都知道IDA Pro不但能用于x86指令集的CPU,而且能用于其他如AVR、PIC指令集的CPU;并且以图形和表格的形式,表示出PE格式文件的汇编资源,这对新手来说,是非常有用的。此外,它和OllyDbg一样,也可在用户模式级别对可执行文件进行跟踪。
 
 
       其他可用工具
       在开发PE文件工具软件之前,首先需要了解一些有关PE文件格式的信息,以下是一些可用到的软件,用于找出隐藏在PE文件下的基本信息。
 
       LordPE
       LordPE是查看PE格式文件信息的首选工具,并且可以修改相关信息。
 
 
 
       PEiD
       PE iDentifier在确定PE文件的编译器类型、打包器、加密器方面,有极高的实用价值;到目前为止,它可以探测出超出500种不同的PE文件标志。
 
 
 
       Resource Hacker
       Resource Hacker可用于修改资源索引信息、图标、菜单、版本号、字符串表等等。
 
 
 
       WinHex
       WinHex,只要你能想到的,它都能做到。
 
 
 
       CFF Explorer
       最后,隆重出场的是CFF Explorer,它简直是梦想中的PE工具软件,它同时支持PE32/64、及包括CLI文件在内的PE格式文件——换句话说,可用于修改 .NET文件的资源,它具有其他工具所不具有的多种功能,只有亲身使用,才能体会到它的种种不可思议的功能。
 
 
 
 
       添加新节并修改OEP
       现在,我们要开始第一步了,在开始之前,必须要非常熟悉PE文件的头部信息,在此可使用OllyDbg,先打开一个PE文件,然后点击菜单“View->Executable file”,再在弹出菜单中选择“Special->PE header”,将会看到类似图3的界面;再回到主菜单中“View->Memory”,尝试在“Memory map”窗口中找出节信息。
 
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
3
 
       在此拿Windows XP的的计算器“calc.exe”作示例程序,讲解一下怎样修改OEP(Offset of Entry Point)。首先,可使用PE Viewer,查找OEP,在0x00012475,再找到映像基地址,在0x01000000,此OEP值是相对虚地址,因此,映像基地址被用于转换为虚地址。
 
虚地址 = 映像基地址 + 相对虚地址
 
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 程序中, DynLoader() 保留作新节的数据。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
    MOV EAX,01012475h //原始OEP
    JMP EAX
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       获取并再生成PE文件
       此处,有一个简单的类,可用于恢复 PE 信息,并可把它用在一个新的 PE 文件中。
 
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() 用于创建新节。
 
 
       为新节创建数据
       下面的类中,包含了新节的数据,然而,新节却是由DynLoader()创建的,我们利用CPECryptor类为新节引入数据和一些其他的东西。
 
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));
    //----------------------------------------
};
 
 
       创建新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;
 
 
       链接此VC工程时的注意事项
       设置Linker->General->Enable Incremental Linking为No (/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;
}
 
 
       存储重要数据并取得原始OEP
       现在,我们必须保存原始OEP与映像基地址,以便取得OEP的虚地址,已经在DynLoader()的末尾保留了一块空间用于存储它。
 
__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 // >>跳到原始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()将会实现复制映像基地址值与进入点偏移量的值到loader末尾的8字节空间中。
 
 
       恢复第一个寄存器上下文
       在线程中恢复原始上下文非常重要,在前面的DynLoader()中还未完成此功能,现在修改源代码以恢复第一个上下文。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD//把寄存器上下文保存在堆栈中
    CALL Main_1
Main_1:   
    POP EBP//取得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 //从堆栈中恢复第一个寄存器上下文
    PUSH EAX
    XOR EAX, EAX
    RETN // >>跳到原始OEP
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_DATA1)
//----------------------------------
_RO_dwImageBase:                DWORD_TYPE(0xCCCCCCCC)
_RO_dwOrgEntryPoint:            DWORD_TYPE(0xCCCCCCCC)
//----------------------------------
    DWORD_TYPE(DYN_LOADER_END_MAGIC)
//----------------------------------
}
}
 
 
       恢复原始堆栈
       把堆栈开始值加上0x34所得到的位置值设置为原始OEP值,可恢复原始堆栈,但此步骤不是非常重要的。然而,在下例代码中,通过一点小技巧完成了加载器代码,并取得OEP,另外对堆栈也作了一点修饰,可在OllyDbg或SoftICE中跟踪观察此代码的动作。
 
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD //把寄存器上下文保存在堆栈中
    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 //从堆栈中恢复第一个寄存器上下文
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)
//----------------------------------
}
}
 
 
       通过结构化异常处理(SEH)取得OEP
       当程序执行了有缺陷的代码或有错误发生时,此时就会产生异常,在特殊条件下,程序会直接跳到线程信息块(TIB)列出的异常处理中。下例的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控制台项目,并链接运行,以下是输出:
1: Raise an Exception
3: In Filter
2: In Finally
4: In Exception Handler
 
       示例中引发了INT 3异常,当此异常发生时,程序会运行到代表异常发生的表达式中:printf("3: In Filter/n");,也可尝试其他种类的异常,在OllyDbg中,Debugging options->Exceptions,在此可看到各种不同类型异常的一个简短列表。
 
 
 
       实现异常处理程序
       我们期望构造一个结构化异常处理来取得 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取得进入点的偏移量。
 
C++代码:
__try //启用SEH
{
    __asm
    {
        INT 3 //抛出异常
    }
}
__except( ..., EXCEPTION_CONTINUE_SEARCH ){}
//异常处理程序
 
 
汇编代码:
; ----------------------------------------------------
    ;开始启用SEH
    ; __try {
    LEA EAX,[EBP+_except_handler1_OEP_Jump]
    PUSH EAX
    PUSH DWORD PTR FS:[0]
    MOV DWORD PTR FS:[0],ESP
    ; ----------------------------------------------------
; 抛出一个INT 3异常
INT 3
    INT 3
    INT 3
    INT 3
    ; }
    ; __except( ...
    ; ----------------------------------------------------
    ;异常处理程序
_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异常处理是通过分段的操作符号(try-except)实现的。Microsoft Windows把此分段的操作符当成一个指向主线程数据块的指针,而数据块的前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;
 
 
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
表3:分段操作符号与线程信息块
 
 
       通过调整线程上下文获得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;
 
 
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]
表4:上下文
 
 
       下例代码演示了,如何通过结构化异常处理来获得OEP:
__stdcall void DynLoader()
{
_asm
{
//----------------------------------
    DWORD_TYPE(DYN_LOADER_START_MAGIC)
//----------------------------------
Main_0:
    PUSHAD //把寄存器上下文保存在堆栈中
    CALL Main_1
Main_1:   
    POP EBP
    SUB EBP,OFFSET Main_1 //取得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 //从堆栈中恢复第一个寄存器上下文
    //----------------------------------------------------
    //启用SEH
    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)
    //--------------------------------------------------------
// --------异常处理程序----------
_except_handler1_OEP_Jump:
    PUSH EBP
    MOV EBP,ESP
    //------------------------------
    MOV EAX,DWORD PTR SS:[EBP+010h]    // PCONTEXT: pContext <- EAX
    //==============================
    PUSH EDI
    //恢复原始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
    //------------------------------
    //把Eip指向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)
//----------------------------------
}
}
 
 
       生成导入表并重建原始导入表
       要在Windows程序中使用动态链接库(DLL),一般来说,有两种方法:
 
² 在工程属性中设定:
 
² 在运行时动态加载:
 
typedef HGLOBAL (*importFunction_GlobalAlloc)(UINT, SIZE_T);
...
importFunction_GlobalAlloc __GlobalAlloc;
 
//加载DLL文件
HINSTANCE hinstLib = LoadLibrary("Kernel32.dll");
if (hinstLib == NULL)
{
    //错误——不能加载DLL
}
 
//取得函数指针
__GlobalAlloc = (importFunction_GlobalAlloc)GetProcAddress(hinstLib,                                                "GlobalAlloc");
if (addNumbers == NULL)
{
     //错误——不能定位DLL函数
}
 
FreeLibrary(hinstLib);
 
 
       当建立一个基于Windows环境的工程时,链接器至少会在工程中包含kernel32.dll,如果没有kernel32.dll中的LoadLibrary()和GetProcAddress()函数,就不可能在运行时加载DLL。程序的依赖项信息存储在导入表中,通过Dependency Walker,不难看出引入到一个PE文件中的DLL模块与函数。
 
 
 
       在此,我们必须自定义工程文件导入表,而且,最后还要恢复原始导入表,以便运行程序的真实代码。
 
 
       构建客户端导入表
       从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;         //DLL引入名
    DWORD   FirstThunk;   //第一个转换程序(thunk)的相对虚地址
} IMAGE_IMPORT_DESCRIPTOR, *PIMAGE_IMPORT_DESCRIPTOR;
 
 
       当一个程序运行时,Windows任务管理器通过函数的虚地址设置好转换程序(thunk),而虚地址是通过函数名查找所得。首先,转换程序(thunk)保存了函数名的相对虚地址,见表5;而在程序运行时,可根据函数的虚地址调整它们,见表6。
 
 
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]
 
表5:文件映像中的导入表
 
 
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]
 
表6:虚拟内存中的导入表
 
 
       在此,需生成一个简单的导入表,以引入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
 
 
       在此,已经准备了一个类库,可使用一个客户端字符串表,生成第一个导入表。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;
 
 
       导入表在新节的开始处被复制,而相应的数据索引被调整为新节的相对虚地址,同样,也会调整新导入表的大小。
 
 
       在运行时使用其他API函数
       这一次,我们可以加载其他DLL,并通过LoadLibrary()和GetProcAddress()函数查找其他函数的进程地址。
 
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      ;kernel32.LoadLibrary()的虚地址
...
0101F120: JMP DWORD PTR [0101F028]
...
0101F230: CALL 0101F120   ;跳至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
...
 
 
调整原始导入表
为了再次运行程序,我们必须要调整好实际导入表的转换程序(thunk),否则,会损坏目标PE文件,我们的代码必须要调整好表5与表6中所有的转换程序(thunk),在这里,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:
...
 
 
       支持DLL与OCX
       现在,我们打算在工程中支持DLL与OCX了,支持它们很容易,只须多留意进入点偏移量、重定位表的实现及客户端导入表。
       DLL文件与OCX文件的进入点偏移量会被主程序至少访问两次:
 
第一次:在构造函数中
当一个DLL通过LoadLibrary()加载时,或者一个OCX通过LoadLibrary()及GetProcAddress()所取得的DllRegisterServer()函数注册时,这是第一次。
 
hinstDLL = LoadLibrary( "test1.dll" );
hinstOCX = LoadLibrary( "test1.ocx" );
_DllRegisterServer = GetProcAddress( hinstOCX, "DllRegisterServer" );
_DllRegisterServer(); // ocx注册函数
 
第二次:在析构函数中
       当主程序通过FreeLibrary()释放库时,这是第二次。
       FreeLibrary( hinstDLL );
       FreeLibrary( hinstOCX );
 
 
       为达到此目的,这里使用了一点小技巧,在第二次时,使指令指针通过结构异常处理指向原始OEP。
 
_main_0:
    pushad //把寄存器上下文保存在堆栈中
    call _main_1
_main_1:   
    pop ebp
    sub ebp,offset _main_1 //取得EBP基地址
    //---------------- 支持dll,ocx -----------------
_support_dll_0:
    jmp _support_dll_1 // nop; nop; //小技巧——在第二次时
    jmp _support_dll_2
_support_dll_1:
    //----------------------------------------------------
   
    ...
   
    //---------------- 支持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 //小技巧
        mov word ptr [ebp+_support_dll_0],ax
_support_dll_2:
    //----------------------------------------------------
    ...
通过SEH进入OEP ...
 
 
       前述代码中的小技巧并不是全部内容,当库被主程序加载到不同的映像基地址时,可能会在ImageBase上遇到一点小麻烦,我们必须自己编写一些代码查找到真实的映像基地址,把它存储起来以备用。
 
mov eax,[esp+24h] //真实的映像基地址
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:
 
 
       上面的代码通过检查堆栈信息而查找真实的映像基地址,利用真实的映像基地址和正式的基地址,只需简单地重定位表信息,我们就能纠正映像程序内部的所有内存调用。
 
 
       重定位表的实现
       重定位表中包含了一些“数据包”,这些“数据包”用于重定位与虚拟内存映像中的虚地址有关的信息,第一个“数据包”由8字节的头组成,用于表示虚拟基地址与数据数目,如下面的IMAGE_BASE_RELOCATION结构所示:
 
 
typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
} IMAGE_BASE_RELOCATION, *PIMAGE_BASE_RELOCATION;
 
 
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:重定位表
 
 
       表7中列出了重定位表的主要概念,此外,还可以在OllyDbg中加载一个DLL或OCX文件,以观察其重定位表,注意在“Memory map”窗口中的“.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中,使用“Long Hex Viewer”模式,可找到如下所示相同的“.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个字节来进行重定位;第一个字节表示重定位的类型,余下的三个字节是偏移量,需和虚拟基地址与映像基地址同时使用,才能调整相关的映像信息。
 
类型偏移量
03000000
 
 
       此处的类型可取得为下述四种:
 
² 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 ;
...
 
 
       下面的代码,实现了重定位:
 
    ...
_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:
...
 
 
       生成特殊的导入表
       为支持OCX注册,必须为目标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 );
}
 
 
       保留线程局部存储
       通过线程局部存储(TLS),程序可并发出多线程,而这通常被Borland的链接器所使用——Delphi和C++ Builder。当打包生成一个PE文件时,必须小心地清理干净TLS,否则,可能导致不支持Borland Delphi与C++ Builder链接生成的EXE文件。另外,可通过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)
...
 
 
       另外,也必须在OptionalHeader中调整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;
}
 
 
       注入自己的代码
       现在,可以把我们自己的代码放到新节中了,代码使用了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文件中的全过程,在最后,建议大家查阅《微软可移植执行文件与通用对象文件格式说明》——http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx,以达到更好理解的目的。
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值