API拦截方法一:PE简介
自己的知识分享文章,整理到博客上 ---------hgy notes.
一.PE简介
PE全名Portable Executable File Format(可移植的执行体),是目前window主流可执行文件格式, 是一种通用于所有window平台和所有CPU上的文件格式
二.PE文件的定义
PE文件的类型定义集中在WinNT.h这个类中,打开WinNT.h,再搜索Image Format(搜到的在9560行),为什么叫Image Format,估计意思就是镜像文件,在这一节点后给出了DOS MZ格式和window3.1的NE格式文件,之后就是PE文件,那么在这个文件里我们几乎可以找到所有关于PE文件的每个数据结构定义.枚举类型.常量定义
EXE和DLL都是PE文件,唯一的区别是用了一个字段标识这个文件是EXE还是DLL,另外win64只对PE格式作了简单的变化,仅仅把32位字段扩展成了64位了,新格式叫PE32+,其实我们不需要关心这些,哥特别仔细看了下WinNT.h,搜下_WIN64,在9872行找到这样的定义:
#ifdef _WIN64 typedef IMAGE_OPTIONAL_HEADER64 IMAGE_OPTIONAL_HEADER; typedef PIMAGE_OPTIONAL_HEADER64 PIMAGE_OPTIONAL_HEADER; #define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR64_MAGIC #else typedef IMAGE_OPTIONAL_HEADER32 IMAGE_OPTIONAL_HEADER; typedef PIMAGE_OPTIONAL_HEADER32 PIMAGE_OPTIONAL_HEADER; #define IMAGE_NT_OPTIONAL_HDR_MAGIC IMAGE_NT_OPTIONAL_HDR32_MAGIC #endif
很明显,加了宏控制,我们只需调用IMAGE_OPTIONAL_HEADER这样的宏,就能在win32和win64下横行了.
三.PE文件图
PE文件图是从论坛上找的,很强大的一张图,大家可以边参考图片边理解PE、(找得辛苦,别浪费了)
四.重要的概念
1.我们要分析的PE文件有两种,一种是已运行的,就是已装到虚拟地址的,另一种是不运行,直接文件解析,这样可以直接给文件加壳,PE文件不是作为单一的内存映射文件被装入内存的,PE加载器遍历PE文件来决定文件哪部分被映射,但是PE使用的是一个平面地址,这就是说,偏移位置高的总是会被映射到相对较高的位置,这么说吧,PE有很多块(section),每一块都有一个RVA和RAW,根椐这两个值来得到相应的区域
为了便于大家理解,哥又摆渡了张图:
再来回味下,偏移位置高的总是会被映射到相对较高的位置,有人问为什么,这个主要是内存分块和文件分块的大小不同引起的吧,具体看核心编程去。还要注意前面三块在文件和内存映像中都一样.
2.上面提到了两个重要概念,RVA,RAW,这两个概念相当重要
RVA:Relative Virtual Address 相对虚拟地址,在运行时,API拦截等操作需要用它来确定导入表的内存地址,这个值会被给出,那么有如下关系:
虚拟地址(VA) = 基地址(ImageBase) + 相对虚拟地址(RVA)
RAW:相对文件偏移地址或相对物理地址,文件偏移地址是从PE文件的第一个字节开始计数,起始值为0
那么用UE之类的十六进制工具打开文件所显示的地址就是RAW,好理解吧
3. 基地址(ImageBase),WINNT将Module的基地址作为Module的实例句柄(Hinstance),可以通过GetModuleHandle得到,在写PE分析器时,经常用内存映射文件,那么MapViewOfFile返回文件的基地址(ImageBase)
五.学习PE的工具
推荐Stud_PE这个工具, 由于大家可能对这个工具不太熟悉,这里使用UE打开,示例文件来自网络
六.PE表头
所有PE都是以一个DOS stub的小程序开始的,有了它,一旦程序运行在DOS下,DOS就会运行DOS stub。所以第一个是DOS头IMAGE_DOS_HEADER
定义如下J (从WinNt.h中拷来的,重要的加个中文注释吧!)
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // DOS标记“MZ”(9568行有它的宏:IMAGE_DOS_SIGNATURE) 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; // DOS代码入口IP WORD e_cs; // DOS代码入口CS 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; // 指向PE文件头 “PE”,0,0 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
这个结构体两个参数最重要,e_magic:需要被设置为ME
#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ
e_lfanew定义了真正的PE文件头的相对偏移量RVA,其实是不是RVA都不重要,上面也提到了,( PE文件磁盘与内存映像结构图),前三块文件和内存都一样,IMAGE_DOS_HEADER大小为64,那么e_lfanew占四个字节,所以e_lfanew的起始RAW为64-4=60(3Ch),那么我们随便用UE打开一个文件,比如:
黑色标出的四位就是000000B0,也就是PE真正的头的RVA,同时,大家也应该注意到起始位的两个字节MZ,CS+IP确定了程序的入口点,大家可以用Ollydbg调试。
那么接下来当然是说PE真正的头IMAGE_NT_HEADERS:
定义如下(哥继续从WinNt.h中拷过来)
typedef struct _IMAGE_NT_HEADERS { DWORD Signature;// IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
第一个参数是PE00的定义:
#define IMAGE_NT_SIGNATURE 0x00004550 // PE00
继续看下去000000B0:
那么我们可以随手写个判断是否为PE文件的函数了:
假家我们载入一个文件:
ImageBase=MapViewOfFile(hMapping,FILE_MAP_READ,0,0,0);//得到文件在内存中的基地址
BOOL IsPEFile(LPVOID ImageBase) { PIMAGE_DOS_HEADER pDH=NULL; PIMAGE_NT_HEADERS pNtH=NULL; if(!ImageBase) return FALSE; pDH=(PIMAGE_DOS_HEADER)ImageBase; if(pDH->e_magic!=IMAGE_DOS_SIGNATURE) return FALSE; pNtH=(PIMAGE_NT_HEADERS32)((DWORD)pDH+pDH->e_lfanew); if (pNtH->Signature != IMAGE_NT_SIGNATURE ) return FALSE; return TRUE; }
第二个参数IMAGE_FILE_HEADER:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; //运行平台 WORD NumberOfSections; //文件的区块数目 DWORD TimeDateStamp; //文件创建的日期和时间 DWORD PointerToSymbolTable; //指向符号表(用于调试) DWORD NumberOfSymbols; //符号表中符号个数(用于调试) WORD SizeOfOptionalHeader; //IMAGE_OPTIONAL_HEADER32的大小 WORD Characteristics; //文件属性 } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
SizeOfOptionalHeader指出了IMAGE_OPTIONAL_HEADER的大小,32位这个值为00E0h,64位为00F0h(32的二倍),但有可能出现更大的值,所以这个大小被做为一个变量传递.刚提到的文件000000B0开始为PE头,加上20个字节为000000C4:
Characteristics也是一个比较有用的字段,我们前面提到了,用了一个字段标识这个文件是EXE还是DLL,那这就是那个字段.我在9715-9729行找到了它们的信息:(同样,哥把它们换成了中文翻译)
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // 文件中不存在重定位信息 #define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // 文件可执行,如果为0,一般是链接时出了问题 #define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // 行号信息被移除 #define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号信息被移除. #define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set #define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // 应用程序可以处理超过2G的地址,从NTSP3开始后,可以分配2-3G用户区 #define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // 处理器的低位字节是相反的 #define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32位机器 #define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // 在.DBG file 中的Debugging 信息被移除 #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 // 系统文件 #define IMAGE_FILE_DLL 0x2000 // 文件是DLL(这就是EXE和DLL的区别) #define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine #define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 //处理器的高位字节是相反的
第三个参数IMAGE_OPTIONAL_HEADER32:(同样从9776行拷来了:)
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic;// 标志字, ROM 映像(0107h),普通32可执行文件(010Bh),普通64可执行文件(020Bh)(DLL和EXE都是可执行文件) BYTE MajorLinkerVersion;// 链接程序的主版本号 BYTE MinorLinkerVersion;// 链接程序的次版本号 DWORD SizeOfCode;// 所有含代码的节的总大小 DWORD SizeOfInitializedData;// 所有含已初始化数据的节的总大小 DWORD SizeOfUninitializedData;// 所有含未初始化数据的节的大小 DWORD AddressOfEntryPoint;//程序执行入口RVA DWORD BaseOfCode;//代码节的起始RVA DWORD BaseOfData;//数据结的起始RVA DWORD ImageBase;//程序默认装入基地址 DWORD SectionAlignment;// 内存中的区块的对齐大小 DWORD FileAlignment;// 文件中的区块的对齐大小 WORD MajorOperatingSystemVersion;//操作系统主版本号 WORD MinorOperatingSystemVersion; //操作系统次版本号 WORD MajorImageVersion;//用户自定义主版本号 WORD MinorImageVersion;//用户自定义次版本号 WORD MajorSubsystemVersion; //所需子系统主版本号 WORD MinorSubsystemVersion; //所需子系统次版本号 DWORD Win32VersionValue;// 莫须有字段,不被病毒利用的话一般为0 DWORD SizeOfImage;// 映像装入内存后的总尺寸 DWORD SizeOfHeaders;//DOS头,PE头,区表总大小 DWORD CheckSum;//映象校验和 WORD Subsystem;//文件子系统 WORD DllCharacteristics;// DllMain()函数何时被调用,默认为 0 DWORD SizeOfStackReserve;// 初始化时的栈大小 DWORD SizeOfStackCommit;// 初始化时实际提交的栈大小 DWORD SizeOfHeapReserve;// 初始化时保留的堆大小 DWORD SizeOfHeapCommit;// 初始化时实际提交的堆大小 DWORD LoaderFlags;// 与调试有关,默认为 0 DWORD NumberOfRvaAndSizes;// 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];// // 数据目录表 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
介绍几个重要的,不然太多了!
AddressOfEntryPoint:
程序执行入口RVA,对于DLL,这个入口点是在进程初始化和关闭时以及线程创建/销毁时调用,在大多数的PE文件中,这个地址并不直接指向Main/WinMain或DllMain,而是指向运行时库代码并由它来调用上述的函数,如果在DLL中把这个值设为0,则前面提到的通知消息都不能收到,地址,这是一个RVA地址,如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了
ImageBase:
文件在内存中的首选装入地址,如果有可能(也就是说,目前如果没有其他占据这块地址,它是一个正确对齐且合法的地址),加载器试图在这个地址装入PE文件,如果可执行文件是在这个地址装入的,那么加载器会跳过应用基址重定位的步骤
对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被**模块占据,所以EXE总是能够按照这个地址装入,这也意味着EXE 文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被**的DLL使用,所以 DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1
链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。
SectionAlignment和FileAlignment:
SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位
Subsystem
一个标明可执行文件所期望的子系统的枚举值,这个值只对EXE是重要的:
链接时的/subsystem:**选项指定的就是这个字段的值,如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立
取值 |
Windows.inc中的预定义值 |
含义 |
0 |
IMAGE_SUBSYSTEM_UNKNOWN |
未知的子系统 |
1 |
IMAGE_SUBSYSTEM_NATIVE |
不需要子系统(如驱动程序) |
2 |
IMAGE_SUBSYSTEM_WINDOWS_GUI |
Windows图形界面 |
3 |
IMAGE_SUBSYSTEM_WINDOWS_CUI |
Windows控制台界面 |
5 |
IMAGE_SUBSYSTEM_OS2_CUI |
OS2控制台界面 |
7 |
IMAGE_SUBSYSTEM_POSIX_CUI |
POSIX控制台界面 |
8 |
IMAGE_SUBSYSTEM_NATIVE_WINDOWS |
不需要子系统 |
9 |
IMAGE_SUBSYSTEM_WINDOWS_CE_GUI |
Windows CE图形界面 |
DataDirectory[16]
这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成,虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个 IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress;//数据块的起始RVA DWORD Size;//数据块的长度 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
索引 |
索引值在Windows.inc中的预定义值 |
对应的数据块 |
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
导出表 |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
导入表 |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
资源 |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
异常(具体资料不详) |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
安全(具体资料不详) |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
重定位表 |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
调试信息 |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
版权信息 |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
具体资料不详 |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
Thread Local Storage |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
具体资料不详 |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
具体资料不详 |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
导入函数地址表 |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
具体资料不详 |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
具体资料不详 |
15 |
未使用 |
|
在 PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的,比如要存取资源,那么必须从第3个 IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小
(这里要特别提示下,运行时是VA = RVA+基地址,不运行时是RVA转换成RAW,VA=RAW+基地址)
有个叫ImageRvaToVa的函数,哥特别测试过,无论PE有没有运行,这个函数得到的值再减去基地址都是PE文件偏移量(RAW)。
再介绍下ImageDirectoryEntryToData:,哥也特别测试过,无论PE有没有运行,为FALSE,得到的值减去基地址为RAW,设为TRUE,得到的值减去基地址为RVA
当然我们也可以计算节的偏差来得到,而不使用window的API,下面我会讲到这种方法.
七.区块表
紧接着IMAGE_NT_HEADERS后的是区块表,它是一个IMAGE_SECTION_HEADER结构数组
数目由表头的NumberOfSections指出(自己找)
IMAGE_SECTION_HEADERS(在9623行找到):
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];//8个字节的块名
union {
DWORD PhysicalAddress;//物理地址
DWORD VirtualSize; //真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个
} Misc;
DWORD VirtualAddress;//RVA
DWORD SizeOfRawData;//在文件中对齐后的尺寸
DWORD PointerToRawData;//Raw
DWORD PointerToRelocations;//重定位的偏移量
DWORD PointerToLinenumbers;//行号的偏移量
WORD NumberOfRelocations;//重定位项的数目
WORD NumberOfLinenumbers;//行号表的行号数目
DWORD Characteristics;//节属性,如可读可写可执行,
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Name
区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$”
后边的字符的字母顺序进行合并的。
每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的,事实上你可以自定义节名,#pragam data_seg去查查干嘛用的(J)
因此,当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据,正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。
Virtual Size
对表对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小,
Virtual Address
该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个快的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。
SizeOfRawData
该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment
潜规则处理过的长度。
PointerToRawData
该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量哦。
PointerToRelocations
这哥们在EXE文件中没有意义,在OBJ
文件中,表示本区块重定位信息的偏移值。(在OBJ
文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)
PointerToLinenumbers
行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。
NumberOfRelocations
这哥们在EXE文件中也没有意义,在OBJ
文件中,是本区块在重定位表中的重定位数目来着。
NumberOfLinenumbers
该区块在行号表中的行号数目,鸡肋。
Characteristics
该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。
(9651行)
字段集 |
用途 |
IMAGE_SCN_CNT_CODE 0x00000020 |
包含代码,常与10000000h设置 |
IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 |
包含已初始化数据 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 |
包含未初始化数据 |
IMAGE_SCN_MEM_DISCARDABLE 0x02000000 |
该区块可被丢弃,因为当它一旦被装入后, |
IMAGE_SCN_MEM_EXECUTE 0x20000000 |
该区块可以执行。通常当0x00000020被设置 |
IMAGE_SCN_MEM_READ 0x40000000 |
该区块可读,可执行文件中的区块总是设置该 |
IMAGE_SCN_MEM_WRITE 0x80000000 |
该区块可写 |
J自己去看点击打开链接 |
|
八.换算RVA和文件偏移
当处理PE 文件时候,任何的 RVA 必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,事实上,可用的方法就是最土最笨的方法:(前面提到了一种懒人方法,自己去翻)
步骤一:循环扫描区块表得出每个区块在内存中的起始 RVA(根据IMAGE_SECTION_HEADER 中的VirtualAddress 字段),并根据区块的大小(根据IMAGE_SECTION_HEADER 中的SizeOfRawData 字段)算出区块的结束 RVA(两者相加即可),最后判断目标 RVA 是否落在该区块内。
步骤二:通过步骤一定位了目标 RVA 处于具体的某个区块中后,那么用目标 RVA 减去该区块的起始 RVA ,这样就能得到目标 RVA 相对于起始地址的偏移量 RVA2.
步骤三:在区块表中获取该区块在文件中所处的偏移地址(根据IMAGE_SECTION_HEADER 中的PointerToRawData 字段), 将这个偏移值加上步骤二得到的 RVA2 值,就得到了真正的文件偏移地址。
为了懒人的方便,提供个路径:win95有代码(我也懒,不想找了):
九.导入表
首先,我们知道PE 文件中的数据被载入内存后根据不同页面属性被划分成很多区块(节),并有区块表(节表)的数据来描述这些区块。这里我们需要注意的问题是:一个区块中的数据仅仅只是由于属性相同而放在一起,并不一定是同一种用途的内容。例如接着要讲的输入表、输出表等就有可能和只读常量一起被放在同一个区块中,因为他们的属性都是可读不可写的。
其次,由于不同用途的数据有可能被放入同一个区块中,因此仅仅依靠区块表是无法确定和定位的。那要怎么办?对了,PE 文件头中 IMAGE_OPTIONAL_DEADER32 结构的数据目录表来指出他们的位置,我们可以由数据目录表来定位的数据包括输入表、输出表、资源、重定位表和TLS等15 种数据。(数据目录表)
当PE 文件被执行的时候,Windows 加载器将文件装入内存并将导入表(Export Table) 登记的动态链接库(一般是DLL 格式)文件一并装入地址空间,再根据DLL 文件中的函数导出信息对被执行文件的IAT 进行修正。
注意,对于磁盘中的PE文件,它是无法得知这些输入函数在内存中的地址,只有PE被装入内存后.
在PE文件中,有一组数据结构,它们分别对应着每个被输入的DLL,每个这样的结构都给出了被输入的DLL的名称并指向一组函数指针,这组函数指针被称为IAT(Import Address Table)
每个被引入的API在IAT中都有它自己保留的位置,在那里它将被加载器写入输入函数的地址,一旦模块被装入,IAT中包含所要调用输入函数的地址。
输入表以一个IMAGE_IMPORT_DESCRIPTOR(IID)数线开始,每个隐式链接的DLL都有一个IID,隐式链接和动态加载的区别,建议大家查下window的编译过程,
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;
这个结构是比较难理解的!
OriginalFirstThunk
包括指向输入名称表(INT)的RVA,INT是一个IMAGE_THUNK_DATA数组,数组中每个IMAGE_THUNK_DATA又指向IMAGE_IMPORT_BY_NAME结构,数组最后一个以内容为NULL的IMAGE_THUNK_DATA结束
TimeDateStamp
该字段可以忽略
ForwarderChain
一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。
Name
DLL 名字的指针,以00结尾的ASCII字符的RVA地址,该字符包含输入的DLL名称
FirstThunk
指向IAT的RVA,它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。在Orignal
First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。
OriginalFirstThunk和FirstThunk非常像,都指向本质上相同的数组IMAGE_THUNK_DATA,
摆渡个表吧():
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE 指向一个转向者字符串的RVA DWORD Function; // PDWORD被输入函数的内存地址 DWORD Ordinal; // 被输入的API序数号 DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
当 IMAGE_THUNK_DATAR的值最高位为1时,表示函数以序号方式输入,这时低31位或底63位被看作为一个函数诹号,当
最高位为0时,表示函数是以字符串在型的函数名输入,这时它的值为一个指向IMAGE_IMPORT_BY_NAME结构,
为方便懒人们,我在10413行找到了如下宏定义
#define IMAGE_ORDINAL64(Ordinal) (Ordinal & 0xffff) #define IMAGE_ORDINAL32(Ordinal) (Ordinal & 0xffff) 随手写个函数: if (HIWORD(*pdwThunk)==0x8000) { sprintf("Ord:%08lX",IMAGE_ORDINAL32(*pdwThunk)); }
IMAGE_IMPORT_BY_NAME:
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0
Name 字段定义了导入函数的名称字符串,这是一个以 NULL 为结尾的字符串
在分析PE时,我们先判断OriginalFirstThunk是否为NULL,不为NULL,就用它来分析导入表,为NULL,则用FirstThunk来分析(PE未加载到内存时),给一个宏定义:
#define GETTHUNK(pImportDesc) ((DWORD) \ ((PIMAGE_IMPORT_DESCRIPTOR)pImportDesc->OriginalFirstThunk ? \ (PIMAGE_IMPORT_DESCRIPTOR)pImportDesc->OriginalFirstThunk:(PIMAGE_IMPORT_DESCRIPTOR)pImportDesc->FirstThunk \ ))
FirstThunk指向的IAT是由加载器填写的,在初始化时,系统根据FirstThunk的值找到指向函数名的地址串,由地址串找到函数名,再根据函数名得到入口地址,然后用入口地址取代FirstThunk指向的地址串中的原值.(那么加载后FirstThunk保存的是什么呢?给个window图形编程的代码测试下)
const unsigned * KPEFile::GetFunctionPtr( PIMAGE_IMPORT_DESCRIPTOR pImport, LPCSTR pProcName) { PIMAGE_THUNK_DATA pThunk; pThunk = (PIMAGE_THUNK_DATA) RVA2Ptr(pImport-> OriginalFirstThunk); for (int i=0; pThunk->u1.Function; i++) { bool match; if ( pThunk->u1.Ordinal & 0x80000000 ) // by ordinal match = (pThunk->u1.Ordinal & 0xFFFF) == ((DWORD) pProcName); else match = stricmp(pProcName, RVA2Ptr((unsigned) pThunk->u1.AddressOfData)+2) == 0; if ( match ) return (unsigned *) RVA2Ptr(pImport->FirstThunk)+i; pThunk ++; } return NULL; }
.后记
好像够多了,不写了,导出表和资源表都比较复杂,有空继续hgy413