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 |
Signature |
PE 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=00, 07FFB0 EDX=7C90EB94 ESI=FFFFFFFF EDI=7C919738 EBP=0007FFF0 ESP=0007FFC4 EIP=010119E0 o d i s z a p c CS=0008 DS=0023 SS=0010 ES=0023 FS=0030 GS=0000SS: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/处下载试用。
图2:OllyDbg的CPU窗口 调试器中哪一部分最重要 前面已经介绍过两款调试器了,而没有说到怎样使用它们或该多留意哪一部分,关于怎样使用调试器,还是建议参考它们的帮助文档。不过在此,还是要讲一些调试器中的重要部分,当然,我们说的是系统低级调试器,换句话来说,我们在此谈得是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 dfhjg:
inc eaxdf:
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_1Main_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_1Main_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_1Main_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 <- EAXPOPAD //从堆栈中恢复第一个寄存器上下文
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异常