PE文件格式分析
_____pedump程序分析
作者 : WY.lslrt
E-mail : wy_lslrt@yahoo.com.cn
QQ : 8968569
1. 开发工具VC++6.0
2. 开发环境windows xp
实例程序下载:attachments/pedump.rar
PE文件就是一种可执行文件的结构,它里面包含有可执行的代码,资源和一些其它的信息。了解PE文件格式对于很多方面都很重要,程序加壳脱壳,加密解密,可执行文件的压缩,资源的读取与修改。废话少说,下面开始分析一下PE文件。
首先,得让大家系统地了解一下整个PE文件的结构,下面我画了一个PE文件的结构简图:
- ┌─────────────┐
- │ MS-DOS │
- │ MZ Header │
- │ │
- ├─────────────┤
- │ MS-DOS Real-Mode │
- │ Stub Program │
- │ │
- ├─────────────┤
- │ PE File Signature │
- ├─────────────┤
- │ PE File Header │
- │ │
- ├─────────────┤
- │ PE File │
- │ Optional Header │
- │ │
- ├─────────────┤
- │ .text Section header │
- ├─────────────┤
- │ .rdata Section header │
- ├─────────────┤
- │ . │
- │ . │
- │ . │
- ├─────────────┤
- │ │
- │ .text Section │
- │ │
- ├─────────────┤
- │ │
- │ .rdata Section │
- │ │
- └─────────────┘
从这个图中大家可以大概了解了PE文件的整个构成,其中PE File Signature,PE File Header,PE File Optional Header组成了一个结构称为IMAGE_NT_HEADERS。
PE文件中的第一块部分是MS-DOS Header,它和DOS系统下的可执行文件的第一个结构是一样的,下面是这个结构具体组成:
winnt.h中的定义
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
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 new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一个域e_magic,它是用来标示MS-DOS可执行文件的一个标志,所有的MS-DOS可执行文件都将此值设为0x5A4D,也就是所说得字符"MZ",这也是为什么叫做MZ Header的原因。在这个结构中,还有一个与对我们有用,e_ifanew,它指向了IAMGE_NT_HEADERS的文件偏移。
接下来这一个区域是MS-DOS Stub 程序,这是由编译器生成的一个简单的16位的程序,仅仅在DOS下显示字符串"This Program cannot be run in DOS mode",现在来讲已经没有多大的意义。
紧接下来就是NT Headers了,这一个大的结构中包含了很多PE文件的非常有用的信息,如code和data的定位和大小,该文件运行在什么系列的系统下,堆栈的初始化大小。下面是NT Headers的具体结构
winnt.h中的定义
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif
我们只需要看IMAGE_NT_HEADERS32,毕竟64位的系统还不是很普及。
首先第一块就是PE File Signature,这是个DWORD的域,它其实是一个四字节的字符串,PE文件中,这个值为"PE/0/0",16位的NE文件则为"NE",LE文件则为"LE",下面说一下如何从文件中获得这个值:
HANDLE WY_hPEFile;
IMAGE_DOS_HEADER WY_MZDosHeader;
IMAGE_NT_HEADERS WY_NTHeader;
//打开一个文件
WY_hPEFile = CreateFile(WY_strFile,GENERIC_READ,FILE_SHARE_READ|FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
//读取第一个结构
ReadFile(WY_hPEFile,(LPVOID*)&WY_MZDosHeader,sizeof(IMAGE_DOS_HEADER),&WY_dwRetValue,NULL);
//判断是否为可执行文件
if(WY_MZDosHeader.e_magic != 0x5a4d)
{
MessageBox((LPCTSTR)"这不是可执行文件");
return;
}
//是可执行文件
//读取IMAGE_NT_HEADERS
//设置文件指针,NT File Headers的位置可以通过e_lfanew加上文件指针得到
WY_dwRetValue = SetFilePointer(WY_hPEFile,(LONG)(WY_MZDosHeader.e_lfanew),NULL,FILE_BEGIN);
//读取IMAGE_NT_HEADER
ReadFile(WY_hPEFile,(LPVOID*)&WY_NTHeader,sizeof(IMAGE_NT_HEADERS),&WY_dwRetValue,NULL);
//读取后该结构的第一个域就是PE File Signature了
//输出Signature
CString WY_strOpMsg //负责初始化输出字符串
CListBox WY_ctrPEInfo //ListBox的控制对象
//输出Signature
WY_strOpMsg.Format("PE文件标志:Signature : 0x%x",WY_NTHeader.Signature);
WY_ctrPEInfo.AddString(WY_strOpMsg);
紧接着PE File Signature的就是PE File Header了,这个结构包含了整个文件最基本的信息,下面是具体结构:
winnt.h中的定义
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Machine : 这个域定义的是程序工作的cpu类型,下面是微软的定义,英文注释应该能看懂吧。
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
//输出工作的处理器类型machine
switch(WY_NTHeader.FileHeader.Machine)
{
case 0x14d:
WY_strOpMsg.Format("工作于处理器类型: Inter i860");
break;
case 0x14c:
WY_strOpMsg.Format("工作于处理器类型: Inter i386");
break;
case 0x162:
WY_strOpMsg.Format("工作于处理器类型: MIPS R3000");
break;
case 0x166:
WY_strOpMsg.Format("工作于处理器类型: MIPS R4000");
break;
case 0x183:
WY_strOpMsg.Format("工作于处理器类型: DEC Alpha AXP");
break;
default:
WY_strOpMsg.Format("工作于处理器类型: Unkown");
break;
}
WY_ctrPEInfo.AddString(WY_strOpMsg);
NumberOfSections : 这个很重要,这个是该文件中的Section的个数,待会这个要用到很多。
//输出section数目
WY_strOpMsg.Format("Section 数目:NumberOfSections : %d个",WY_NTHeader.FileHeader.NumberOfSections);
WY_ctrPEInfo.AddString(WY_strOpMsg);
TimeDateStamp : 这是由连接器生成的时间,是距December 31st,1969 at 4:00 P.M.的秒数。
PointerToSymbolTable : 这是COFF 符号表的文件偏移,这个域只用于OBJ文件和有COFF调试信息的PE文件。在IMAGE_DIRECTORY_ENTRY_DEBUG结构中可以获得更多的调试信息
NumberOfSymbols : 这是SymbolTable的个数
WY_strOpMsg.Format("COFF 符号表 offset:PointerToSymbolTable : 0x%x",WY_NTHeader.FileHeader.PointerToSymbolTable);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_strOpMsg.Format("符号表个数:NumberOfSymbols : 0x%x",WY_NTHeader.FileHeader.NumberOfSymbols);
WY_ctrPEInfo.AddString(WY_strOpMsg);
SizeOfOptionalHeader : 这个值是和 IMAGE_OPTIONAL_HEADER的大小是一样的
Characteristics : 这个是PE文件的属性
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
这里有很多,我们只需要了解几个比较常用的,IMAGE_FILE_RELOCS_STRIPPED这个是文件需要重定位,IMAGE_FILE_EXECUTABLE_IMAGE这是可执行文件的标志,不是lib或obj,
IMAGE_FILE_SYSTEM 这是个系统文件标志,IMAGE_FILE_DLL这个就表明这个文件是个动态链接库dll了。
其实一个文件的属性并不是单一的,它有可能是上面的几种组合,但是上面的定义很巧妙WORD是个16位的数据,它上面15种属性每种只是让其中1位置1,这样15种叠加起来不管哪几样相加,都不会重复,所以我们去分析时,只需对特定位置上的1bit进行比较,就可以得到它是哪种属性,下面就给出具体方法
WY_strOpMsg.Format("PE文件属性:Characteristics : 0x%x",WY_NTHeader.FileHeader.Characteristics);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_wPECharacter = WY_NTHeader.FileHeader.Characteristics & 0x1000; //获得第13位
//判断文件属性
if(WY_wPECharacter == 0x1000) //如果第13位为1,则是系统文件
{
WY_ctrPEInfo.AddString((LPCTSTR)" 系统文件");
}
else
{
WY_wPECharacter = WY_NTHeader.FileHeader.Characteristics & 0x2000;//获得第14位
if(WY_wPECharacter == 0x2000) //如果第14位为1,则是DLL文件
{
WY_ctrPEInfo.AddString((LPCTSTR)" DLL 文件");
}
else
{
WY_wPECharacter = WY_NTHeader.FileHeader.Characteristics & 0x0002; //获得第2位
if(WY_wPECharacter == 0x0002) //如果第2位为1的,则是可执行文件
{
WY_ctrPEInfo.AddString((LPCTSTR)" 可执行文件");
}
else
{
WY_ctrPEInfo.AddString(" Unkown");
}
}
}
为什么要从高位到低位呢,因为DLL作为动态链接库,它也是可执行的,所以要实现判断低位,那么就要误判了。
紧接着FILE Header的是Optional Header 其实这个结构并不像它名字那样,而且是必须有的。下面是具体结构
winnt.h中的定义
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
这里我们只看32位的就行了。
Magic : 通常是0x10B
MajorLinkerVersion MinorLinkerVersion是链接器的version。
SizeOfCode,SizeofInitializedData分别是代码段的大小,数据段的大小,因为代码段经常只有一个,通常这个值和.text的大小是一样的
SizeOfUninitializedData : 这个大小是加载器在虚拟内存中为这些UninitializedData节划分的大小,并不代表示在文件中的大小。
AddressOfEntryPoint : 这个域指出了程序的进入点,加载器通过它可以知道,整个程序加载完成之后从哪里开始运行。这是一个RVA。在这里,要说一下RVA,它是Relative Virtual Address的缩写,也就是说它只在虚拟地址中才起作用,它是相对于进程的基地址的,如果进程被加载到基址0x00400000,如果一个RVA等于0x1000,那么这个实际的虚拟地址就应该是0x00401000,就好像MS-DOS Header中的e_ifanew是文件的偏移地址,那么它实际的地址是文件基质+e_ifanew。
BaseOfCode : 这也是个RVA,告诉加载器,代码段要加载到什么地方
BaseOfData : 这个同上。
ImageBase : 该域告诉加载器这个PE文件需要映射到内存的何处。也是基地址。可执行程序通常是0x10000,DLL通常为0x400000.
SectionAlignment : 内存对齐密度,通常为0x1000,就是4kB对齐,这和windows的内存管理有关。若第一节从401000h开始且大小是10个字节,则下一节必定从402000h开始,即使401000h和402000h之间还有很多空间没被使用。
FileAlignment : 这个上面那个类似,不过是在文件中的对齐密度,通常是0x200就是512,应该适合扇区大小有关系吧。
中间几个major/minor...version没有多大的用处,就是版本号,从命名规则上应该都能知道是什么
Win32VersionValue这个域没有用
SizeOfImage : 内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
SizeOfHeaders : 是所有头+节表的大小,也就等于文件尺寸减去文件中所有节的尺寸。
SubSystem : 这个是指明这个可执行文件是是什么样的可执行文件,是控制台,还是GUI的。
#define IMAGE_SUBSYSTEM_UNKNOWN 0 // Unknown subsystem.
#define IMAGE_SUBSYSTEM_NATIVE 1 // Image doesn't require a subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_GUI 2 // Image runs in the Windows GUI subsystem.
#define IMAGE_SUBSYSTEM_WINDOWS_CUI 3 // Image runs in the Windows character subsystem.(就是控制台程序)
#define IMAGE_SUBSYSTEM_OS2_CUI 5 // image runs in the OS/2 character subsystem.
#define IMAGE_SUBSYSTEM_POSIX_CUI 7 // image runs in the Posix character subsystem.
#define IMAGE_SUBSYSTEM_NATIVE_WINDOWS 8 // image is a native Win9x driver.
#define IMAGE_SUBSYSTEM_WINDOWS_CE_GUI 9 // Image runs in the Windows CE subsystem.
//判断程序类型
switch(WY_NTHeader.OptionalHeader.Subsystem)
{
case 0:
WY_ctrPEInfo.AddString("Unknown subsystem.");
break;
case 1:
WY_ctrPEInfo.AddString("Image doesn't require a subsystem.");
break;
case 2:
WY_ctrPEInfo.AddString((LPCTSTR)"Windows GUI 程序.");
break;
case 3:
WY_ctrPEInfo.AddString((LPCTSTR)" Windows 控制台程序.");
break;
case 5:
WY_ctrPEInfo.AddString((LPCTSTR)"OS/2 控制台程序.");
break;
case 7:
WY_ctrPEInfo.AddString((LPCTSTR)"Posix 控制台程序.");
break;
case 8:
WY_ctrPEInfo.AddString((LPCTSTR)"Win9x driver.");
break;
case 9:
WY_ctrPEInfo.AddString((LPCTSTR)"Windows CE 程序.");
break;
default:
WY_ctrPEInfo.AddString("Unknown subsystem.");
break;
}
SizeOfStackReserve : 为初始化线程堆栈(<<windows核心编程>>中有讲)保留的虚拟内存空间,默认大小是0x100000(1MB).
SizeOfStackCommit : 初始化线程堆栈的大小,默认是0x1000(1个内存分页大小)。
SizeOfHeapReserve : 为初始化进程堆的保留虚拟内存空间。
SizeOfHeapCommit : 初识化进程堆的大小,默认也是一个分页大小。
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES] : 是16个IMAGE_DATA_DIRECTORY 结构数组。每个结构数组给出一个重要数据结构的RVA,比如引入地址表等。
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
有用的是VirtualAddress这是一个RVA但是它可以帮助我们确定该Data Directory在哪一个Section内。
紧接着的Optional Header得是一组Section Headers它的个数是由File Header中的NumberOfSecions确定的,下面是Section Header的具体结构
winnt.h中的定义
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name : 指出该节的名称。
VirtualSize : 指出了该节在未作对齐操作前的大小。
VirtualAddress : 字段是本节的起始RVA(相对虚拟地址)。PE装载器将节映射至内存时会读取本值,因此如果域值是1000h,而PE文件装在地址400000h处,那么本节就被载到401000h。而且这个地址是按照内存页对齐的,它的值总是SectionAlignment的值的整数倍。
SizeOfRawData : 是经过文件对齐处理后节尺寸,PE装载器提取本域值了解需映射入内存的节字节数。(注: 假设一个文件的文件对齐尺寸是0x200,如果前面的 VirtualSize域指示本节长度是0x388字节,则本域值为0x400,表示本节是0x400字节长)。该值等于VirtualSize字段的值按照FileAlignment字段的值对齐后的大小。
PointerToRawData : 这是节基于文件的起始偏移量,是从文件头(文件的第一个字节)开始算起的值。PE装载器通过本域值找到节数据在文件中的位置。
Characteristics : 这个是这个Section的属性,定义如下,分析方法和PE文件的属性分析方法一样。
#define IMAGE_SCN_TYPE_NO_PAD 0x00000008 // Reserved.
#define IMAGE_SCN_CNT_CODE 0x00000020 // Section contains code.
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 // Section contains initialized data.
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 // Section contains uninitialized data.
#define IMAGE_SCN_LNK_OTHER 0x00000100 // Reserved.
#define IMAGE_SCN_LNK_INFO 0x00000200 // Section contains comments or some other type of information.
#define IMAGE_SCN_LNK_REMOVE 0x00000800 // Section contents will not become part of image.
#define IMAGE_SCN_LNK_COMDAT 0x00001000 // Section contents comdat.
#define IMAGE_SCN_NO_DEFER_SPEC_EXC 0x00004000 // Reset speculative exceptions handling bits in the TLB entries for this section.
#define IMAGE_SCN_GPREL 0x00008000 // Section content can be accessed relative to GP
#define IMAGE_SCN_MEM_FARDATA 0x00008000
#define IMAGE_SCN_MEM_PURGEABLE 0x00020000
#define IMAGE_SCN_MEM_16BIT 0x00020000
#define IMAGE_SCN_MEM_LOCKED 0x00040000
#define IMAGE_SCN_MEM_PRELOAD 0x00080000
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 // Default alignment if no others are specified.
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 // Section contains extended relocations.
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 // Section can be discarded.
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 // Section is not cachable.
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 // Section is not pageable.
#define IMAGE_SCN_MEM_SHARED 0x10000000 // Section is shareable.
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 // Section is executable.
#define IMAGE_SCN_MEM_READ 0x40000000 // Section is readable.
#define IMAGE_SCN_MEM_WRITE 0x80000000 // Section is writeable.
下面给出的是获得所有Section Header的代码
IMAGE_SECTION_HEADER WY_SectionHeader[26];
WORD WY_wSecNum;
WY_wSecNum = WY_NTHeader.FileHeader.NumberOfSections;
for(WY_i = 0;WY_i < WY_wSecNum;WY_i++) //循环获得Section Headers
{
ReadFile(WY_hPEFile,(LPVOID)(WY_SectionHeader+WY_i),sizeof(IMAGE_SECTION_HEADER),&WY_dwRetValue,NULL);
}
for(WY_i = 0;WY_i < WY_wSecNum;WY_i++) //输出每个Section的信息
{
WY_strOpMsg.Format("Section名称:Name : %s",WY_SectionHeader[WY_i].Name);
WY_ctrPEInfo.AddString(WY_strOpMsg);
if(WY_strOpMsg.Find("UPX") != -1) //如果Section的Name中含有UPX,那么很有可能被UPX加过壳了。
{
WY_ctrPEInfo.AddString((LPCTSTR)" UPX加壳");
}
WY_strOpMsg.Format("Section大小:SizeOfRawData : %dB",WY_SectionHeader[WY_i].SizeOfRawData);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_strOpMsg.Format("Section属性(具体含义请查看msdn):Characteristics : 0x%x",WY_SectionHeader[WY_i].Characteristics);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_strOpMsg.Format("在文件中的偏移量:PointerToRawData : 0x%x",WY_SectionHeader[WY_i].PointerToRawData);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_strOpMsg.Format("VirtualAddress: 0x%x",WY_SectionHeader[WY_i].VirtualAddress);
WY_ctrPEInfo.AddString(WY_strOpMsg);
WY_ctrPEInfo.AddString("_______________________________________________________________");
}
前面说过Data Directory的结构,通过其中的Virtual Address可以确定在哪一个Section里,这两个Virtual Address都是RVA怎么可以确定呢,因为加载器既然要将这些结构加载到内存中,肯定结构直接肯定有些偏移,既然Data Directory是在某一个Section中的,在内存中的偏移肯定和在文件中的是一样的,所以两个Virtual Address相减便得到了再Section中的,加上PointerToRawData就得到了在文件中的偏移。下面便是计算每一个Data Directory的文件偏移
DWORD WY_dwImgDirOffset[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //IMAGE_DATA_DIRECTORY在文件中的偏移量
int WY_nSecDirin[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //每个Data Directory所在的Section
//开始计算各个DATA_DIRECTORY的文件偏移量
for(WY_i = 0;WY_i < IMAGE_NUMBEROF_DIRECTORY_ENTRIES;WY_i++)
{
WY_dwTmp = WY_NTHeader.OptionalHeader.DataDirectory[WY_i].VirtualAddress;
for(WY_j = 0;WY_j < WY_wSecNum;WY_j++)
{
//判断在哪个section里
if(WY_dwTmp >= WY_SectionHeader[WY_j].VirtualAddress && WY_dwTmp < (WY_SectionHeader[WY_j].VirtualAddress +
WY_SectionHeader[WY_j].SizeOfRawData))
{
WY_nSecDirin[WY_i] = WY_j; //在哪一个Section
break;
}
}
//计算得出文件偏移
WY_dwImgDirOffset[WY_i] = (WY_dwTmp - WY_SectionHeader[WY_j].VirtualAddress) + WY_SectionHeader[WY_j].PointerToRawData;
if(WY_dwImgDirOffset[WY_i]){
WY_strOpMsg.Format("%s Offset : 0x%x in %s",WY_pchDirName[WY_i],WY_dwImgDirOffset[WY_i],WY_SectionHeader[WY_j].Name);
WY_ctrPEInfo.AddString(WY_strOpMsg);
}
}
说完了前面所有的重要结构之后,我们就该探讨探讨一下剩下的最主要部分Section里面都有什么些东西,首先说一下资源吧。
资源,说白了就是一个PE文件所需要展示给用户看得的东西,如图标,位图,对话框,菜单,字串,版本啦一类的东西。资源通常包含在.rsrc Section中,由三种结构IMAGE_RESOURCE_DIRECTORY,IMAGE_RESOURCE_DIRECTORY_ENTRY,IMAGE_RESOURCE_DATA_ENTRY和数据组成的一个树型的逻辑结构
IMAGE_RESOURCE_DIRECTORY
- ┌──────┐
- │ │
- │ │
- │ ID:0(ROOT) │
- │ │
- │ │
- │Entries Num │
- ├──────┤ ┌──────┐
- │ Id │---------------------> │ │
- ├──────┤ │ │
- │ Named │-------- │ ID:2 │
- ├──────┤ | │ (Bitmap) │
- │ Id │--- | │ │
- ├──────┤ | | │ Entries Num│
- │ ...... │ | | ├──────┤
- └──────┘ | | │ Named │--------------
- | | ├──────┤ |
- | | │ ...... │ |
- | | └──────┘ | ┌──────┐
- | | | │ │
- | | |------> │ ID:22 │
- | | │ │
- | ----> │(My Bitmap) │
- | │ │
- ----> │Entries Num │
- ├──────┤
- │ Data Dir │---------------> ┌──────┐
- └──────┘ │ │ │ Data Dir │---------->Data
- │ │
- └──────┘
这是我画的一个简图,大家从上图可以看出,资源是以一个IMAGE_RESOURCE_DIRECTORY结构为根,紧接着下面跟着的是几个IMAGE_RESOURCE_DIRECTORY_ENTRY结构,指向了下一层DIRECTORY,总共是三层,第三层指向了IMAGE_RESOURCE_DATA_ENTRY结构,这就是树的叶子了,这个结构包含了数据的位置,大小。下面是这几个结构
winnt.h中定义
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
WORD NumberOfNamedEntries;
WORD NumberOfIdEntries;
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
};
DWORD Name;
WORD Id;
};
union {
DWORD OffsetToData;
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
};
};
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData;
DWORD Size;
DWORD CodePage;
DWORD Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
先来看一下IMAGE_RESOURCE_DIRECTORY,这个结构只有最后两个有用,这两个数目决定了紧跟其后的entry的数目,也就是说决定了下一层Directory的数目。
IMAGE_RESOURCE_DIRECTORY_ENTRY这个结构看似复杂其实很简单,第一个域,Name,其实它并不是资源的名字,它是名字的文件偏移或者是资源ID,如何判断这就要看它的最高位是否置位,如果置位了那么它的低31位就是指向名字结构的文件偏移,这个偏移可不是对文件头的偏移,也不是RVA,而是相对于这个IMAGE_RESOURCE_DIRECTORY结构起始地址的偏移,如果没有置位,那么它就是这个资源以ID的形式命名,这个域就是这个资源的ID。下一个OffsetToData的偏移也是如此。
说了上面那么多,如果Name指向的是名字,怎么样去获得这个名字呢?先给出这个名字的结构
winnt.h中的定义
typedef struct _IMAGE_RESOURCE_DIRECTORY_STRING {
WORD Length;
CHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIRECTORY_STRING, *PIMAGE_RESOURCE_DIRECTORY_STRING;
typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
WORD Length;
WCHAR NameString[ 1 ];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
其实使用的都是unicode字符,所以我们就用第二个结构。结构第一个域给出的是这个名字字符串的长度,注意,unicode可是两个字节一个字符不管是不是字母还是汉字。这个Length只是有多少个字符,并没有说多少个字节。NameString[1]只是这个名字的第一个字符,其实就是说明了,长度是在字符串开始的前两个字节。我们用(Name & 0x80000000) == 0x80000000(掩去低31位,只比较最高位)是不是指向名字,指向后在(Name & 0x7FFFFFFF) + DirStarAddress(& 0x7FFFFFFF只留下第31位)就得到了在文件中的偏移,然后根据Length,在Length域后面读取Length*2字节,就是名字了。
再来看看OffsetToData,它指向两种不同的结构,一个是上面所说的IMAGE_RESOURCE_DIRECTORY,另外一个就是IMAGE_RESOURCE_DATA_ENTRY了,当OffsetToData的最高位置位时,它就指向IMAGE_RESOURCE_DIRECTORY,否则指向IMAGE_RESOURCE_DATA_ENTRY了。
因此要想完整的获得整个资源所有信息,就得将这个资源树遍历一边。方法有两种,一种是递归,这个方法很简单,只不过是当资源过大时,这样就显得有些慢了,我们可以根据这个资源树的一些特性,用非递归实现了,根目录只有一个IMAGE_RESOURCE_DIRECTORY,就不需要循环了,第二层就必须要循环了,要读取紧跟根目录后面Entry然后获得下一层的Directory,循环多少次呢?该根DIRECTORY有两个域,刚才说过了就是两个Number,这是我们循环的总数,我们还需要判断entry里的Name域是否指向名字,其实Named Entry指向的肯定需要名字,Id Entry指向的肯定不需要名字,因此两个循环可以分开写,这样就可以去掉了判断,但是增加了代码长度,看刚才的简图,第二层的Directory一般都是资源的类型,资源的类型总数并不多,所以可以将两个循环写在一起,加上判断。读取第三层的时候,这个时候就到了用户自定的了,这个时候用户资源可能会很多,每一种都可能几十或者上百,这个时候如果将两个循环写开,则就不需要判断了,虽然增加了代码长度,但是只是不多的长度,却省掉了上百次的比较,增加了速度。下面给出具体的代码(给大家提个醒,在CString中Formatunicode字符串的时候,格式符需要用%S,而不是%s)
DWORD WY_dwTmp; //作为临时保存文件偏移量
DWORD WY_dwTmp1; //同上
DWORD WY_dwTmp2; //同上
IMAGE_RESOURCE_DIRECTORY WY_tmpRescDir; //临时保存IMAGE_RESOURCE_DIRECTORY
IMAGE_RESOURCE_DIRECTORY WY_tmpRescDir1; //同上
IMAGE_RESOURCE_DIRECTORY_ENTRY WY_tmpRDEntry; //临时保存Entry
IMAGE_RESOURCE_DATA_ENTRY WY_tmpRescDEntry; //临时保存Data Entry
IMAGE_RESOURCE_DIR_STRING_U WY_DirStrU; //获得资源名称。
WCHAR *WY_RescName = NULL;
//获得资源信息
SetFilePointer(WY_hPEFile,(LONG)WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE],NULL,FILE_BEGIN);
if(!ReadFile(WY_hPEFile,(LPVOID)&WY_RescImg,sizeof(IMAGE_RESOURCE_DIRECTORY),&WY_dwRetValue,NULL))
{
return;
}
WY_strOpMsg.Format("ROOT: %d个Named Entries %d个ID Entries",WY_RescImg.NumberOfNamedEntries,WY_RescImg.NumberOfIdEntries);
WY_ctrPEInfo.AddString(WY_strOpMsg);
//开始循环获得资源,因为是嵌套循环,非递归方法,所以每个for循环里大概都是类似的语句.这里的偏移量都是相对第一个resource dir来说的
if(WY_RescImg.NumberOfNamedEntries || WY_RescImg.NumberOfIdEntries) //读取第一层的entry结构
{
for(WY_i = 0;WY_i < (WY_RescImg.NumberOfNamedEntries + WY_RescImg.NumberOfIdEntries);WY_i++)//第一层循环
{
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRDEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&WY_dwRetValue,NULL);
if((WY_tmpRDEntry.Name & 0x80000000) == 0x80000000) //判断最高为是否为1,如果为1则去获得名称
{
WY_dwTmp = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT);//保存文件指针
SetFilePointer(WY_hPEFile,(LONG)((WY_tmpRDEntry.Name & 0x7fffffff) + WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_DirStrU,sizeof(IMAGE_RESOURCE_DIR_STRING_U),&WY_dwRetValue,NULL); //读取DirStrU结构
WY_RescName = (WCHAR*)malloc(sizeof(WCHAR)*(WY_DirStrU.Length));
WY_RescName[0] = WY_DirStrU.NameString[0]; //这句和下边一句获取名字
ReadFile(WY_hPEFile,(LPVOID)(WY_RescName+1),(WY_DirStrU.Length-1)*2,&WY_dwRetValue,NULL);
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp,NULL,FILE_BEGIN);//恢复本层指针
}
if((WY_tmpRDEntry.OffsetToData & 0x80000000) == 0x80000000) //这指向另外一个IMAGE_RESOURCE_DIRECTORY结构的偏移
{
WY_dwTmp = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT); //保存指针,因为指针要跳转,而下个循环还要从这个地址开始读
SetFilePointer(WY_hPEFile,(LONG)((WY_tmpRDEntry.OffsetToData & 0x7fffffff)+ WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRescDir,sizeof(IMAGE_RESOURCE_DIRECTORY),&WY_dwRetValue,NULL); //读取子Directory结构
if(WY_RescName)
{
WY_strOpMsg.Format(" Name : %S,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个",WY_RescName, (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
free(WY_RescName);
WY_RescName = NULL;
}
else
{
switch(WY_tmpRDEntry.Name)
{
case 1:
WY_strOpMsg.Format(" 类型:CURSOR,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 2:
WY_strOpMsg.Format(" 类型:BITMAP,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 3:
WY_strOpMsg.Format(" 类型:ICON, Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 4:
WY_strOpMsg.Format(" 类型:MENU, Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 5:
WY_strOpMsg.Format(" 类型:DIALOG,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 6:
WY_strOpMsg.Format(" 类型:STRING,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 7:
WY_strOpMsg.Format(" 类型:FONTDIR,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 8:
WY_strOpMsg.Format(" 类型:FONT, Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 9:
WY_strOpMsg.Format(" 类型:ACCELERATOR,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.Offset ToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 10:
WY_strOpMsg.Format(" 类型:RCDATA,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 11:
WY_strOpMsg.Format(" 类型:MESSAGETABLE,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 12:
WY_strOpMsg.Format(" 类型:GROUP_CURSOR,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 13:
WY_strOpMsg.Format(" 类型:GROUP_ICON,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 16:
WY_strOpMsg.Format(" 类型:VERSION,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 17:
WY_strOpMsg.Format(" 类型:DLGINCLUDE,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 19:
WY_strOpMsg.Format(" 类型:PLUGPLAY,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 20:
WY_strOpMsg.Format(" 类型:VXD,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个",
(WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 21:
WY_strOpMsg.Format(" 类型:ANICURSOR,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 22:
WY_strOpMsg.Format(" 类型:ANIICON,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
case 23:
WY_strOpMsg.Format(" 类型:RT_HTML,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
break;
default:
WY_strOpMsg.Format(" 类型:UNKNOWN,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", (WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir.NumberOfNamedEntries,WY_tmpRescDir.NumberOfIdEntries);
}
}
WY_ctrPEInfo.AddString(WY_strOpMsg);
if(WY_tmpRescDir.NumberOfNamedEntries) //读取Name Entry
{
for(WY_j = 0;WY_j < WY_tmpRescDir.NumberOfNamedEntries;WY_j++) //第二层循环
{
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRDEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&WY_dwRetValue,NULL);
if((WY_tmpRDEntry.Name & 0x80000000) == 0x80000000) //判断是否含有名字
{
WY_dwTmp1 = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT);
SetFilePointer(WY_hPEFile,(LONG)((WY_tmpRDEntry.Name & 0x7fffffff) +
WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_DirStrU,sizeof(IMAGE_RESOURCE_DIR_STRING_U),&WY_dwRetValue,NULL);
WY_RescName = (WCHAR*)malloc(sizeof(WCHAR)*(WY_DirStrU.Length));
WY_RescName[0] = WY_DirStrU.NameString[0];
ReadFile(WY_hPEFile,(LPVOID)(WY_RescName+1),(WY_DirStrU.Length-1)*2,&WY_dwRetValue,NULL);
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp1,NULL,FILE_BEGIN);
}
if((WY_tmpRDEntry.OffsetToData & 0x80000000) == 0x80000000) //
{
WY_dwTmp1 = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT); //保存本层指针
SetFilePointer(WY_hPEFile,(LONG)((WY_tmpRDEntry.OffsetToData & 0x7fffffff)+
WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);//获得子结构的文件偏移地址
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRescDir1,sizeof(IMAGE_RESOURCE_DIRECTORY),&WY_dwRetValue,NULL);
if(WY_RescName)
{
WY_strOpMsg.Format(" Resc3 Name : %S,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", WY_RescName+1,(WY_tmpRDEntry.OffsetToData & 0x7fffffff),
WY_tmpRescDir1.NumberOfNamedEntries,WY_tmpRescDir1.NumberOfIdEntries);
free(WY_RescName);
WY_RescName = NULL;
}
else
{
WY_strOpMsg.Format(" Resc3 ID : %d,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", WY_tmpRDEntry.Name,(WY_tmpRDEntry.OffsetToData & 0x7fffffff),
WY_tmpRescDir1.NumberOfNamedEntries,WY_tmpRescDir1.NumberOfIdEntries);
}
WY_ctrPEInfo.AddString(WY_strOpMsg);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRDEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),
&WY_dwRetValue,NULL);//读取最后一个entry信息 if(WY_tmpRescDir1.NumberOfIdEntries) //到最后一层了,entry结构中指向的必为data entry结构,所以直接 //处理不必判断
{
WY_dwTmp2 = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT);
SetFilePointer(WY_hPEFile,(LONG)(WY_tmpRDEntry.OffsetToData +
WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRescDEntry,sizeof(IMAGE_RESOURCE_DATA_ENTRY), &WY_dwRetValue,NULL);
WY_strOpMsg.Format(" ID : %d,Size : %dB,Offset : 0x%x, WY_tmpRDEntry.Name,WY_tmpRescDEntry.Size,WY_tmpRescDEntry.OffsetToData);
WY_ctrPEInfo.AddString(WY_strOpMsg);
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp2,NULL,FILE_BEGIN);
}
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp1,NULL,FILE_BEGIN);
}
}
}
if(WY_tmpRescDir.NumberOfIdEntries) //读取Id Entry
{
for(WY_j = 0;WY_j < WY_tmpRescDir.NumberOfIdEntries;WY_j++) //第二层循环
{
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRDEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY),&WY_dwRetValue,NULL);
if((WY_tmpRDEntry.OffsetToData & 0x80000000) == 0x80000000) //
{
WY_dwTmp1 = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT); //保存本层指针
SetFilePointer(WY_hPEFile,(LONG)((WY_tmpRDEntry.OffsetToData & 0x7fffffff)+
WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRescDir1,sizeof(IMAGE_RESOURCE_DIRECTORY),&WY_dwRetValue,NULL);
WY_strOpMsg.Format(" Resc3 ID : %d,Offset : 0x%x,Named Entry : %d个 ID Entry : %d个", WY_tmpRDEntry.Name,(WY_tmpRDEntry.OffsetToData & 0x7fffffff),WY_tmpRescDir1.NumberOfNamedEntries,WY_tmpRescDir1.NumberOfIdEntries);
WY_ctrPEInfo.AddString(WY_strOpMsg);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRDEntry,sizeof(IMAGE_RESOURCE_DIRECTORY_ENTRY), &WY_dwRetValue,NULL); //读取最后一个entry信息
if(WY_tmpRescDir1.NumberOfIdEntries)
{
WY_dwTmp2 = SetFilePointer(WY_hPEFile,0,NULL,FILE_CURRENT);
SetFilePointer(WY_hPEFile,(LONG)(WY_tmpRDEntry.OffsetToData +
WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_RESOURCE]),NULL,FILE_BEGIN);
ReadFile(WY_hPEFile,(LPVOID)&WY_tmpRescDEntry,sizeof(IMAGE_RESOURCE_DATA_ENTRY), &WY_dwRetValue,NULL);
WY_strOpMsg.Format(" ID : %d,Size : %dB,Offset : 0x%x", WY_tmpRDEntry.Name,WY_tmpRescDEntry.Size,(WY_tmpRescDEntry.OffsetToData & 0x7fffffff));
WY_ctrPEInfo.AddString(WY_strOpMsg);
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp2,NULL,FILE_BEGIN);
}
SetFilePointer(WY_hPEFile,(LONG)WY_dwTmp1,NULL,FILE_BEGIN);
}
}
}
SetFilePointer(WY_hPEFile,WY_dwTmp,NULL,FILE_BEGIN); //恢复本层文件指针
}
}
}
这一段代码比较长,循环也很多,大家把整个资源的分布搞懂,再来理解这段代码就很轻松了。
接下来,就该是函数导出表了,大家如果写过dll,那么就很明白什么是函数导出表了。它通常包含在.edata中,下面我简单的说下导出表(如果像更进一步的了解,请参看<<windows核心编程>>)
当PE装载器执行一个程序,它将相关DLLs都装入该进程的地址空间。然后根据主程序的引入函数信息,查找相关DLLs中的真实函数地址来修正主程序。PE装载器搜寻的是DLLs中的引出函数。
DLL/EXE要引出一个函数给其他DLL/EXE使用,有两种实现方法: 通过函数名引出或者仅仅通过序数引出。比如某个DLL要引出名为"GetSysConfig"的函数,如果它以函数名引出,那么其他DLLs/EXEs若要调用这个函数,必须通过函数名,就是GetSysConfig。另外一个办法就是通过序数引出。什么是序数呢? 序数是唯一指定DLL中某个函数的16位数字,在所指向的DLL里是独一无二的。例如在上例中,DLL可以选择通过序数引出,假设是16,那么其他DLLs/EXEs若要调用这个函数必须以该值作为GetProcAddress调用参数。这就是所谓的仅仅靠序数引出。不提倡仅仅通过序数引出函数这种方法,这会带来DLL维护上的问题。一旦DLL升级/修改,程序员无法改变函数的序数,否则调用该DLL的其他程序都将无法工作。
现在我们看一下导出结构。象导出表一样,可以通过数据目录找到导出表的位置。这儿,导出表是数据目录的第一个成员,又可称为IMAGE_EXPORT_DIRECTORY。该结构中共有11个成员,定义如下
winnt.h中的定义
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
我们来说几个重要的域
Name : 模块的真实名称的RVA。本域是必须的,因为文件名可能会改变。这种情况下,PE装载器将使用这个内部名字。
Base : 基数,加上序数就是函数地址数组的索引值了。
NumberOfFunctions : 模块引出的函数/符号总数。
NumberOfNames : 通过名字引出的函数/符号数目。该值不是模块引出的函数/符号总数,这是由上面的NumberOfFunctions给出。本域可以为0,表示模块可能仅仅通过序数引出。如果模块根本不引出任何函数/符号,那么数据目录中导出表的RVA为0。
AddressOfFunctions : 模块中有一个指向所有函数/符号的RVAs数组,本域就是指向该RVAs数组的RVA。模块中所有函数的RVAs都保存在一个数组里,其实这个RVA只是指像一个数组的起始地址,在数组中,每一个元素的值才是函数的RVA
AddressOfNames : 类似上个域,模块中有一个指向所有函数名的RVAs数组,本域就是指向该RVAs数组的RVA。
AddressOfNameOrdinals : RVA,指向包含上述 AddressOfNames数组中相关函数之序数的16位数组。
导出表的设计是为了方便PE装载器工作。首先,模块必须保存所有引出函数的地址以供PE装载器查询。模块将这些信息保存在AddressOfFunctions域指向的数组中,而数组元素数目存放在NumberOfFunctions域中。 因此,如果模块引出40个函数,则AddressOfFunctions指向的数组必定有40个元素,而NumberOfFunctions值为40。现在如果有一些函数是通过名字引出的,那么模块必定也在文件中保留了这些信息。这些 名字的RVAs存放在一数组中以供PE装载器查询。该数组由AddressOfNames指向,NumberOfNames包含名字数目。考虑一下PE装载器的工作机制,它知道函数名,并想以此获取这些函数的地址。至今为止,模块已有两个模块: 名字数组和地址数组,但两者之间还没有联系的纽带。因此我们还需要一些联系函数名及其地址的东东。PE参考指出使用到地址数组的索引作为联接,因此PE装载器在名字数组中找到匹配名字的同时,它也获取了 指向地址表中对应元素的索引。 而这些索引保存在由AddressOfNameOrdinals域指向的另一个数组(最后一个)中。由于该数组是起了联系名字和地址的作用,所以其元素数目必定和名字数组相同,比如,每个名字有且仅有一个相关地址,反过来则不一定: 每个地址可以有好几个名字来对应。因此我们给同一个地址取"别名"。为了起到连接作用,名字数组和索引数组必须并行地成对使用,譬如,索引数组的第一个元素必定含有第一个名字的索引,以此类推。
AddressOfNames AddressOfNameOrdinals
| |
RVA of Name 1 <--> Index of Name 1
RVA of Name 2 <--> Index of Name 2
RVA of Name 3 <--> Index of Name 3
RVA of Name 4 <--> Index of Name 4
... ... ...
RVA of Name N <--> Index of Name N
下面举一两个例子说明问题。如果我们有了导出函数名并想以此获取地址,可以这么做:
1.定位到PE header。
2.从数据目录读取导出表的虚拟地址。
3.定位导出表获取名字数目(NumberOfNames)。
4.并行遍历AddressOfNames和AddressOfNameOrdinals指向的数组匹配名字。如果在AddressOfNames 指向的数组中找到匹配名字,从AddressOfNameOrdinals 指向的数组中提取索引值。例如,若发现匹配名字的RVA存放在AddressOfNames 数组的第77个元素,那就提取AddressOfNameOrdinals数组的第77个元素作为索引值。如果遍历完NumberOfNames 个元素,说明当前模块没有所要的名字。
5.从AddressOfNameOrdinals 数组提取的数值作为AddressOfFunctions 数组的索引。也就是说,如果值是5,就必须读取AddressOfFunctions 数组的第5个元素,此值就是所要函数的RVA。
现在我们在把注意力转向IMAGE_EXPORT_DIRECTORY 结构的Base成员。您已经知道AddressOfFunctions 数组包含了模块中所有引出符号的地址。当PE装载器索引该数组查询函数地址时,让我们设想这样一种情况,如果程序员在.def文件中设定起始序数号为200,这意味着AddressOfFunctions 数组至少有200个元素,甚至这前面200个元素并没使用,但它们必须存在,因为PE装载器这样才能索引到正确的地址。这种方法很不好,所以又设计了Base 域解决这个问题。如果程序员指定起始序数号为200,Base 值也就是200。当PE装载器读取Base域时,它知道开始200个元素并不存在,这样减掉一个Base值后就可以正确地索引AddressOfFunctions 数组了。有了Base,就节约了200个空元素。
注意Base并不影响AddressOfNameOrdinals数组的值。尽管取名"AddressOfNameOrdinals",该数组实际包含的是指向AddressOfFunctions 数组的索引,而不是什么序数啦。
讨论完Base的作用,我们继续下一个例子。假设我们只有函数的序数,那么怎样获取函数地址呢,可以这么做:
1.定位到PE header。
2.从数据目录读取导出表的虚拟地址。
3.定位导出表获取Base值。
4.减掉Base值得到指向AddressOfFunctions 数组的索引。 将该值与NumberOfFunctions作比较,大于等于后者则序数无效。 通过上面的索引就可以获取AddressOfFunctions 数组中的RVA了。
可以看出,从序数获取函数地址比函数名快捷容易。不需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。然而,综合性能必须与模块维护的简易程度作一平衡。总之,如果想通过名字获取函数地址,需要遍历AddressOfNames 和 AddressOfNameOrdinals 这两个数组。如果使用函数序数,减掉Base值后就可直接索引AddressOfFunctions 数组。
如果一函数通过名字引出,那在GetProcAddress中可以使用名字或序数。但函数仅由序数引出情况又怎样呢? 现在就来看看。
"一个函数仅由序数引出"意味着函数在AddressOfNames 和 AddressOfNameOrdinals 数组中不存在相关项。记住两个域,NumberOfFunctions 和 NumberOfNames。这两个域可以清楚地显示有时某些函数没有名字的。函数数目至少等同于名字数目,没有名字的函数通过序数引出。比如,如果存在70个函数但AddressOfNames数组中只有40项,这就意味着模块中有30个函数是仅通过序数引出的。现在我们怎样找出那些仅通过序数引出的函数呢?这不容易,必须通过排除法,比如,AddressOfFunctions 的数组项在AddressOfNameOrdinals 数组中不存在相关指向,这就说明该函数RVA只通过序数引出。
下面给出读取Export Table的例子,这个时候已经用到了文件映射,因为要读取函数的名字,但是名字长度不确定,所以不能靠文件I/O来读取了,必须将整个文件映射到内存中靠判断结束符去读取。
LPVOID WY_lpPEFile;
HANDLE WY_hFileMap;
PIMAGE_EXPORT_DIRECTORY WY_lpExpDir;
DWORD *WY_dwExpNamesArr;
WORD *WY_wExpOrdinArr;
DWORD *WY_dwExpFunRVAArr;
char *WY_pcName;
WY_hFileMap = CreateFileMapping(WY_hPEFile,NULL,PAGE_READONLY,0,0,NULL);
WY_lpPEFile = MapViewOfFile(WY_hFileMap,FILE_MAP_READ,0,0,0);
//开始获得exports table的信息,这里因为有许多东西需要提取,所以用到了文件内存映射
if(WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_EXPORT])
{
WY_ctrPEInfo.AddString("Exports Infomation");
WY_lpExpDir = (PIMAGE_EXPORT_DIRECTORY)((DWORD)WY_lpPEFile + WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_EXPORT]);
//通过RVA计算获得模块名文件指针
WY_pcName = (char*)(WY_lpExpDir->Name-WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].VirtualAddress+
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].PointerToRawData+(DWORD)WY_lpPEFile);
WY_strOpMsg.Format("导出模块名称 : %s,BASE : %d,函数个数 : %d,函数名称个数 : %d",WY_pcName,WY_lpExpDir->Base,
WY_lpExpDir->NumberOfFunctions,WY_lpExpDir->NumberOfNames);
WY_ctrPEInfo.AddString(WY_strOpMsg);
//通过RVA计算获得函数名数字起始文件指针
WY_dwExpNamesArr = (DWORD*)(WY_lpExpDir->AddressOfNames-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
//通过RVA计算获得序号表数组起始文件指针
WY_wExpOrdinArr = (WORD*)(WY_lpExpDir->AddressOfNameOrdinals-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
//通过RVA计算获得函数RVAs数组起始文件指针
WY_dwExpFunRVAArr = (DWORD*)(WY_lpExpDir->AddressOfFunctions-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
WY_ctrPEInfo.AddString((LPCTSTR)"ordin索引值 函数RVA地址 函数名");
for(WY_i = 0;WY_i < (int)WY_lpExpDir->NumberOfNames;WY_i++)
{
//根据函数名称数组中的名称RVA计算名称的起始文件指针
WY_pcName = (char*)(WY_dwExpNamesArr[WY_i]-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_EXPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
WY_strOpMsg.Format("%4d %8xh %s",WY_wExpOrdinArr[WY_i],
WY_dwExpFunRVAArr[WY_wExpOrdinArr[WY_i]-WY_lpExpDir->Base+1],WY_pcName);
WY_ctrPEInfo.AddString(WY_strOpMsg);
}
}
现在我们开始讨论引入表了。引入表实际上是一个 IMAGE_IMPORT_DESCRIPTOR 结构数组。每个结构包含PE文件引入函数的一个相关DLL的信息。比如,如果该PE文件从10个不同的DLL中引入函数,那么这个数组就有10个成员。该数组以一个全0的成员结尾。下面是具体结构
winnt.h中的定义
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0 for terminating null import descriptor
DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD TimeDateStamp; // 0 if not bound,
// -1 if bound, and real date/time stamp
// in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
结构第一项是一个union子结构。事实上,这个union子结构只是给 OriginalFirstThunk 增添了个别名,也可以称其为"Characteristics"。 该成员项含有指向一个IMAGE_THUNK_DATA 结构数组的RVA。
什么是 IMAGE_THUNK_DATA? 这是一个dword类型的集合。通常为指向一个 IMAGE_IMPORT_BY_NAME 结构的指针。注意 IMAGE_THUNK_DATA 包含了指向一个IMAGE_IMPORT_BY_NAME 结构的指针。有几个 IMAGE_IMPORT_BY_NAME 结构,我们收集起这些结构的RVA (IMAGE_THUNK_DATAs)组成一个数组,并以0结尾,然后再将数组的RVA放入 OriginalFirstThunk。此IMAGE_IMPORT_BY_NAME 结构存有一个引入函数的相关信息。看一下IMAGE_IMPORT_BY_NAME 和IMAGE_THUNK_DATA的具体结构。
winnt.h中的定义
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
typedef struct _IMAGE_THUNK_DATA32 {
union {
PBYTE ForwarderString;
PDWORD Function;
DWORD Ordinal;
PIMAGE_IMPORT_BY_NAME AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
可以看出来THUNK结构其实就是一个DWORD数据。
Hint : 指示本函数在其所驻留DLL的引出表中的索引号。该域被PE装载器用来在DLL的引出表里快速查询函数。该值不是必须的,一些连接器将此值设为0。
Name[1] : 含有引入函数的函数名。函数名是一个ASCIIZ字符串。注意这里虽然将Name的大小定义成字节,其实它是可变尺寸域,只不过我们没有更好方法来表示结构中的可变尺寸域
我们接着来说Descriptor其它的成员
TimeDateStamp 和 ForwarderChain 只想另外的结构,TimeDateStamp如果最高为为1的话,则指向一个新的包括新的模块名,和时间辍。不过不是很重要,就暂且不说了
Name : 含有指向DLL名字的RVA,即指向DLL名字的指针,也是一个ASCIIZ字符串。
FirstThunk 与 OriginalFirstThunk 非常相似,它也包含指向一个 IMAGE_THUNK_DATA 结构数组的RVA(当然这是另外一个IMAGE_THUNK_DATA 结构数组)。
为什么我们需要两个完全相同的数组? 为了回答该问题,我们需要了解当PE文件被装载到内存时,PE装载器将查找IMAGE_THUNK_DATA 和 IMAGE_IMPORT_BY_NAME 这些结构数组,以此决定引入函数的地址。然后用引入函数真实地址来替代由FirstThunk指向的 IMAGE_THUNK_DATA 数组里的元素值。由OriginalFirstThunk 指向的RVA数组始终不会改变
假设我们要列出某个PE文件的所有引入函数,可以照着下面步骤走:
1.定位第一个 IMAGE_IMPORT_DESCRIPTOR 结构。
2.检查 OriginalFirstThunk值。若不为0,顺着 OriginalFirstThunk 里的RVA值转入那个RVA数组。
3.对于每个数组元素,我们比对元素值是否等于IMAGE_ORDINAL_FLAG32。如果该元素值的最高二进位为1, 那么函数是由序数引入的,可以从该值的低字节提取序数。如果元素值的最高二进位为0,就可将该值作为RVA转入 IMAGE_IMPORT_BY_NAME 数组,跳过 Hint 就是函数名字了。再跳至下一个数组元素提取函数名一直到数组底部(它以null结尾)。现在我们已遍历完一个DLL的引入函数,接下去处理下一个DLL。
4.即跳转到下一个 IMAGE_IMPORT_DESCRIPTOR 并处理之,如此这般循环直到数组见底。(IMAGE_IMPORT_DESCRIPTOR 数组以一个全0域元素结尾)。
下面给出获得引入表信息的代码:
//开始获得imports table的信息
PIMAGE_IMPORT_DESCRIPTOR WY_lpImpDescript;
PIMAGE_THUNK_DATA WY_pThunkData;
if(WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_IMPORT])
{
WY_ctrPEInfo.AddString("Imports Information");
WY_lpImpDescript = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)WY_lpPEFile + WY_dwImgDirOffset[IMAGE_DIRECTORY_ENTRY_IMPORT]);
while(WY_lpImpDescript->OriginalFirstThunk || WY_lpImpDescript->TimeDateStamp
|| WY_lpImpDescript->ForwarderChain || WY_lpImpDescript->Name || WY_lpImpDescript->FirstThunk)
{
//获得该引入模块的名称
WY_pcName = (char*)(WY_lpImpDescript->Name-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
WY_strOpMsg.Format(" 模块名称: %s",WY_pcName);
WY_ctrPEInfo.AddString(WY_strOpMsg);
//获得该模块中的函数名与序号
//获得THUNK数组的起始指针
WY_pThunkData = (PIMAGE_THUNK_DATA)(WY_lpImpDescript->OriginalFirstThunk-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
while(WY_pThunkData->u1.AddressOfData)
{
if((WY_pThunkData->u1.Ordinal & 0x80000000) == 0x80000000) //并不指向地址而是函数序号
{
WY_strOpMsg.Format(" 序号: %xh",(WY_pThunkData->u1.Ordinal & 0x7fffffff));
WY_ctrPEInfo.AddString(WY_strOpMsg);
}
else
{ //指向函数名数组
//此处借用这个指针变量只是为了获得序号和名字结构的起始指针,不要被变量的名字所迷惑
WY_wExpOrdinArr = (WORD*)((DWORD)WY_pThunkData->u1.AddressOfData-
WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].VirtualAddress+WY_SectionHeader[WY_nSecDirin[IMAGE_DIRECTORY_ENTRY_IMPORT]].PointerToRawData+ (DWORD)WY_lpPEFile);
WY_pcName = (char*)((DWORD)WY_wExpOrdinArr + (DWORD)sizeof(WORD));
WY_strOpMsg.Format(" 序号: %8xh 函数名: %s",*WY_wExpOrdinArr,WY_pcName);
WY_ctrPEInfo.AddString(WY_strOpMsg);
}
WY_pThunkData++;
}
WY_lpImpDescript++;
}
}
至此就已经将PE文件中最重要的几个部分说完了,如果大家想对PE做更加深的了解,请查阅MSDN。