设为首页 收藏本站
查看: 952|回复: 0

[经验分享] 【学习】Windows PE文件学习(一:导出表)

[复制链接]
发表于 2017-6-28 12:38:03 | 显示全部楼层 |阅读模式
  今天做了一个读取PE文件导出表的小程序,用来学习。
  参考了《Windows PE权威指南》一书。
  首先, PE文件的全称是Portable Executable,可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件。
    我们知道,一个Windows程序,它所实现的所有功能最终几乎都是调用系统DLL提供的API函数。要使用任何一个DLL所提供的函数,我们需要将它导入,也就是用到了导入表。然而对于那些提供了被导出的函数的DLL程序来说,他们必须使用导出表将函数导出,之后别的程序才可以使用。无论是系统提供的标准DLL还是个人编写的DLL,只要想提供自己的函数给别人使用就必须建立导出表。一般使用任何开发环境编写具有导出功能的程序,导出表都是由链接器自动建立的。程序员只需指定被导出的函数名称或序号即可。
    导出表通常出现在DLL文件的.edata节中。
    知道了导出表的位置,我们可以得到导出函数的地址,进而对这些函数进行Hook。而我们现在的目的是为了学习PE文件中导出表的构成,所以有必要了解PE文件的结构。
    
  1 基本概念
    注:以下引用部分均来自网络

  下表描述了贯穿于本文中的一些概念:





名称描述
地址是“虚拟地址”而不是“物理地址”。为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支、避开错误的内存位置等的优势。同时用户并不需要知道具体的“真实地址”,因为系统自己会为程序准备好内存空间的(只要内存足够大)
镜像文件包含以EXE文件为代表的“可执行文件”、以DLL文件为代表的“动态链接库”。为什么用“镜像”?这是因为他们常常被直接“复制”到内存,有“镜像”的某种意思。看来西方人挺有想象力的哦^0^
RVA英文全称Relatively Virtual Address。偏移(又称“相对虚拟地址”)。相对镜像基址的偏移。(有时候不一定是相对镜像的基址,还可能以某个结构的首地址为基址)
节是PE文件中代码或数据的基本单元。原则上讲,节只分为“代码节”和“数据节”。(文件中节大小通常以磁盘的一个物理扇区也就是512B对齐,若是镜像文件加载到内存中,以一个内存页大小对齐,32位为4K,64位为8K)
VA英文全称Virtual Address。虚拟地址(虚拟内存中的正常地址,不需要进行转换)

      有特殊的节无论是在文件中还是在内存中,对齐粒度与其他的节都不同,如:资源字节码以双字对齐

  2 PE文件的结构

    PE文件的总体结构:如果形象地说,即是3个头和身子。3个头是Dos头、Nt头和节表(节头),身子就是一个一个地节(存放数据和代码的地方)以上的各个头部都是数据结构,可以在winnt.h头文件中找到它们对应的struct定义(Nt头分为32位和64位)。
    由于PE文件是兼容Windows NT以前的Dos系统的,所以现在的任何一个PE文件拿到Dos系统上都是可以运行的,不过大多数可能也只能打出一句话:“This program cannot be run in DOS mode”。这是由PE文件的结构中的Dos头决定的。

  用记事本打开任何一个镜像文件,其头2个字节必为字符串“MZ”,这是Mark Zbikowski的姓名缩写,他是最初的MS-DOS设计者之一。然后是一些在MS-DOS下的一些参数,这些参数是在MS-DOS下运行该程序时要用到的。在这些参数的末尾也就是文件的偏移0x3C(第60字节)处是是一个4字节的PE文件签名的偏移地址。该地址有一个专用名称叫做“E_lfanew”。这个签名是“PE00”(字母“P”和“E”后跟着两个空字节)。紧跟着E_lfanew的是一个MS-DOS程序。那是一个运行于MS-DOS下的合法应用程序。当可执行文件(一般指exe、com文件)运行于MS-DOS下时,这个程序显示“This program cannot be run in DOS mode(此程序不能在DOS模式下运行)”这条消息。用户也可以自己更改该程序,有些还原软件就是这么干的。同时,有些程序既能运行于DOS又能运行于Windows下就是这个原因。Notepad.exe整个DOS头大小为224个字节,大部分不能在DOS下运行的Win32文件都是这个值。MS-DOS程序是可有可无的,如果你想使文件大小尽可能的小可以省掉MS-DOS程序,同时把前面的参数都清0。

  3 Nt头部 IMAGE_NT_HEADERS
  PE文件中较为复杂的部分就是这里了。
  在 2 中说到的DosHeader->E_lfanew所指向的签名“PE\0\0”就是Nt头的第一个成员了,我们在编程中得到Nt头的方法也是这样做的,因为Dos头的第二部分MS-DOS程序部分的大小是可以改变的,连带着整个Dos就是不定长的了,只有其中的E_lfanew指向它自己的末尾。
    Nt头同样分为两部分(除去签名4个字节):
    给出winnt.h中的定义



1 typedef struct _IMAGE_NT_HEADERS {
2     DWORD Signature;                        //4 bytes PE文件头标志:(e_lfanew)->‘PE\0\0’
3     IMAGE_FILE_HEADER FileHeader;           //20 bytes PE文件物理分布的信息
4     IMAGE_OPTIONAL_HEADER32 OptionalHeader; //224bytes PE文件逻辑分布的信息
5 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  其中的IMAGE_FILE_HEADER我们称作文件头,IMAGE_OPTIONAL_HEADER32称作可选映像头(我习惯称之为选项头)。有点滑稽的是,选项头可以说是PE文件中最重要、最复杂的部分了,却是可选的。。
  同时我们看到,选项头在32位和64位PE文件中结构是有所不同的,注意,只是有所不同而已,大致上还是没什么区别的。但是在编程中我们必须将其考虑进去,由于选项头是不同的,所以Nt头也会是不同的。



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;
  但是文件头还是很清晰明了的,其中比较常用的成员就是Machine和Characteristics了。都是用来判断的,其中Machine标志了PE文件需要运行的目标平台,也就是期望在哪种指令集的CPU的平台上被加载,一般可以用来判断PE文件是64位还是32位的;Characteristics是采用标志位的方式来判断许多关于PE文件的信息,其中最重要的是判断其是不是dll,使用的时候与(&)上就行了。



#define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved external references).这是标志其能不能独立运行,像dll就必须让别的模块来加载自己,但是exe和sys是自己加载运行的
#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  // Aggressively 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.
#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386              0x014c  // Intel 386.32位
#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_SH3               0x01a2  // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3DSP            0x01a3
#define IMAGE_FILE_MACHINE_SH3E              0x01a4  // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4               0x01a6  // SH4 little-endian
#define IMAGE_FILE_MACHINE_SH5               0x01a8  // SH5
#define IMAGE_FILE_MACHINE_ARM               0x01c0  // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB             0x01c2  // ARM Thumb/Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_ARMNT             0x01c4  // ARM Thumb-2 Little-Endian
#define IMAGE_FILE_MACHINE_AM33              0x01d3
#define IMAGE_FILE_MACHINE_POWERPC           0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_POWERPCFP         0x01f1
#define IMAGE_FILE_MACHINE_IA64              0x0200  // Intel 64 64位
#define IMAGE_FILE_MACHINE_MIPS16            0x0266  // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64           0x0284  // ALPHA64
#define IMAGE_FILE_MACHINE_MIPSFPU           0x0366  // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16         0x0466  // MIPS
#define IMAGE_FILE_MACHINE_AXP64             IMAGE_FILE_MACHINE_ALPHA64
#define IMAGE_FILE_MACHINE_TRICORE           0x0520  // Infineon
#define IMAGE_FILE_MACHINE_CEF               0x0CEF
#define IMAGE_FILE_MACHINE_EBC               0x0EBC  // EFI Byte Code
#define IMAGE_FILE_MACHINE_AMD64             0x8664  // AMD64 (K8) 64位
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE
  接下来重点介绍选项头 IMAGE_OPTIONAL_HEADER。

  

偏移(32/64)大小英文名中文名描述
02Magic魔数这个无符号整数指出了镜像文件的状态。
0x10B表明这是一个32位镜像文件。
0x107表明这是一个ROM镜像。
0x20B表明这是一个64位镜像文件。
21MajorLinkerVersion链接器的主版本号链接器的主版本号。
31MinorLinkerVersion链接器的次版本号链接器的次版本号。
44SizeOfCode代码节大小一般放在“.text”节里。如果有多个代码节的话,它是所有代码节的和。必须是FileAlignment的整数倍,是在文件里的大小。
84SizeOfInitializedData已初始化数大小一般放在“.data”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。
124SizeOfUninitializedData未初始化数大小一般放在“.bss”节里。如果有多个这样的节话,它是所有这些节的和。必须是FileAlignment的整数倍,是在文件里的大小。
164AddressOfEntryPoint入口点当可执行文件被加载进内存时其入口点RVA。对于一般程序镜像来说,它就是启动地址。为0则从ImageBase开始执行。对于dll文件是可选的。
204BaseOfCode代码基址当镜像被加载进内存时代码节的开头RVA。必须是SectionAlignment的整数倍。
244BaseOfData数据基址当镜像被加载进内存时数据节的开头RVA。(在64位文件中此处被并入紧随其后的ImageBase中。)必须是SectionAlignment的整数倍。
28/244/8ImageBase镜像基址当加载进内存时镜像的第1个字节的首选地址。它必须是64K的倍数。DLL默认是10000000H。Windows CE 的EXE默认是00010000H。Windows 系列的EXE默认是00400000H。
324SectionAlignment内存对齐当加载进内存时节的对齐值(以字节计)。它必须≥FileAlignment。默认是相应系统的页面大小。
364FileAlignment文件对齐用来对齐镜像文件的节中的原始数据的对齐因子(以字节计)。它应该是界于512和64K之间的2的幂(包括这两个边界值)。默认是512。如果SectionAlignment小于相应系统的页面大小,那么FileAlignment必须与SectionAlignment相等。
402MajorOperatingSystemVersion主系统的主版本号操作系统的版本号可以从“我的电脑”→“帮助”里面看到,Windows XP是5.1。5是主版本号,1是次版本号
422MinorOperatingSystemVersion主系统的次版本号
442MajorImageVersion镜像的主版本号
462MinorImageVersion镜像的次版本号
482MajorSubsystemVersion子系统的主版本号
502MinorSubsystemVersion子系统的次版本号
522Win32VersionValue保留,必须为0
564SizeOfImage镜像大小当镜像被加载进内存时的大小,包括所有的文件头。向上舍入为SectionAlignment的倍数。
604SizeOfHeaders头大小所有头的总大小,向上舍入为FileAlignment的倍数。可以以此值作为PE文件第一节的文件偏移量。
644CheckSum校验和镜像文件的校验和。计算校验和的算法被合并到了Imagehlp.DLL 中。以下程序在加载时被校验以确定其是否合法:所有的驱动程序、任何在引导时被加载的DLL以及加载进关键Windows进程中的DLL。
682Subsystem子系统类型运行此镜像所需的子系统。参考后面的“Windows子系统”部分。
702DllCharacteristicsDLL标识参考后面的“DLL特征”部分。
724/8SizeOfStackReserve堆栈保留大小最大大小。CPU的堆栈。默认是1MB。
76/804/8SizeOfStackCommit堆栈提交大小初始提交的堆栈大小。默认是4KB。
80/884/8SizeOfHeapReserve堆保留大小最大大小。编译器分配的。默认是1MB。
84/964/8SizeOfHeapCommit堆栈交大小初始提交的局部堆空间大小。默认是4KB。
88/1044LoaderFlags保留,必须为0
92/1084NumberOfRvaAndSizes目录项数目  数据目录项的个数。由于以前发行的Windows NT的原因,它只能为16。

96/1128*16DataDirectory数据目录  目录项数组,包含16个目录项


  这是完整的选项头的结构,其中只提Magic和DataDirectory,至于镜像加载时的基址与重定向问题,本文不做介绍,因为PE文件解析并不需要把镜像给加载到我们自己的程序中,只需要映射到内存中,对其内容进行解析即可。
  对Magic域进行判断,可以区分文件是64位还是32位,所以到现在我们有两种方法来区分。
  本文的主角——导出表就是由DataDirectory[0]中的目录项指出的,具体如下:



typedef struct _IMAGE_DATA_DIRECTORY {
DWORD   VirtualAddress;
DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  由此我们可以知道,DataDirectory并不是直接指向导出表的,真相是这样的:DataDirectory是一个数组,每个项都是一样的,IMAGE_DATA_DIRECTORY,每一项都由一个地址和大小,这就告诉我们导出表的基地址和其大小(别小看这个大小,我们会用到的)。
  得到了导出表的地址和大小,那么我们就可以搞些事情了(23333~)。



typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD   Characteristics;
DWORD   TimeDateStamp;
WORD    MajorVersion;
WORD    MinorVersion;
DWORD   Name;            // 这是这个PE文件的模块名
DWORD   Base;           
DWORD   NumberOfFunctions;     // 这两个域按字面意思理解,这个为总的导出函数的个数
DWORD   NumberOfNames;      // 这个是有名称的函数的个数,因为有的导出函数是没有名字的,只有序号
DWORD   AddressOfFunctions;     // RVA from base of image 这三个就是所谓的EAT,导出地址表
DWORD   AddressOfNames;         // RVA from base of image Nt头基址加上这个偏移得到的数组中存放所有的名称字符串
DWORD   AddressOfNameOrdinals;  // RVA from base of image Nt头基址加上这个偏移得到的数组中存放所有的函数序号,并不一定是连续的,但一般和导出地址表是一一对应的
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
  这是导出表的结构,其中重要的域我用红色的字标注了出来。
  我在网上查的资料说的比较清晰:


导出地址表(Export Address Table,EAT)
  导出地址表的格式为下表所述的两种格式之一。如果指定的地址不是位于导出节(其地址和长度由NT头给出)中,那么这个域就是一个Export RVA;否则这个域是一个Forwarder RVA,它给出了一个位于其它DLL中的符号的名称。





偏移大小描述
04Export RVA当加载进内存时,导出函数RVA。
04Forwarder RVA这是指向导出节中一个以NULL结尾的ASCII码字符串的指针。这个字符串必须位于Export Table(导出表)数据目录项给出的范围之内。这个字符串给出了导出函数所在DLL的名称以及导出函数的名称(例如“MYDLL.expfunc”),或者DLL的名称以及导出函数的序数值(例如“MYDLL.#27”)。
  Forwarder RVA导出了其它镜像中定义的函数,使它看起来好像是当前镜像导出的一样。因此对于当前镜像来说,这个符号同时既是导入函数又是导出函数。
  例如对于Windows XP系统中的Kernel32.dll文件来说,它导出的“HeapAlloc”被转发到“NTDLL.RtlAllocateHeap”。这样就允许应用程序使用Windows XP系统中的Ntdll.dll模块而不需要实际包含任何相关的导入信息。应用程序的导入表只与Kernel32.dll有关。
  导出地址表的的值有时为0,此时表明这里没有导出函数。这是为了能与以前版本兼容,省去修改的麻烦。

导出名称指针表
  导出名称指针表是由导出名称表中的字符串的地址(RVA)组成的数组。二进制进行排序的,以便于搜索。
  只有当导出名称指针表中包含指向某个导出名称的指针时,这个导出名称才算被定义。换句话说,导出名称指针表的值有可能为0,这是为了能与前面版本兼容。

导出序数表
  导出序数表是由导出地址表的索引组成的一个数组,每个序数长16位。必须从序数值中减去Ordinal Base域的值得到的才是导出地址表真正的索引。注意,导出地址表真正的索引真正的索引是从0开始的。由此可见,微软弄出Ordinal Base是找麻烦的。导出序数表的值和导出地址表的索引的值都是无符号数。
  导出名称指针表和导出名称序数表是两个并列的数组,将它们分开是为了使它们可以分别按照各自的边界(前者是4个字节,后者是2个字节)对齐。在进行操作时,由导出名称指针这一列给出导出函数的名称,而由导出序数这一列给出这个导出函数对应的序数。导出名称指针表的成员和导出序数表的成员通过同一个索引相关联。

导出名称表(Export Name Table,ENT)
  导出名称表的结构就是长度可变的一系列以NULL结尾的ASCII码字符串。 导出名称表包含的是导出名称指针表实际指向的字符串。这个表的RVA是由导出名称指针表的第1个值来确定的。这个表中的字符串都是函数名称,其它文件可以通过它们调用函。

  这里需要特别注意的是,有时候你在遍历导出地址表的时候,有可能得到的并不是一个地址(或者说并不是目标函数的地址),而是一个字符串。那么这就是遇到了函数转发的情况。判断方法就是上面所说的判断这个指针是不是在导出表的范围内。
    学习PE文件可能比较难想象其中的数据结构组织,因为比较复杂,所以我建议可以上网找关于PE文件各个结构的示意图看看。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-388990-1-1.html 上篇帖子: UEFI+GPT模式下的Windows系统中分区结构和默认分区大小及硬盘整数分区研究 下篇帖子: WINDOWS API 函数(超长,值得学习)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表