pe结构详解

admin 2022年10月18日07:58:52评论40 views字数 25332阅读84分26秒阅读模式

声明

由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,雷神众测以及文章作者不为此承担任何责任。

雷神众测拥有对此文章的修改和解释权。如欲转载或传播此文章,必须保证此文章的完整性,包括版权声明等全部内容。未经雷神众测允许,不得任意修改或者增减此文章内容,不得以任何方式将其用于商业目的。

No.1

结构图

pe结构可以总结为以下这个略缩图,DOS头和NT头就是PE文件中两个重要的文件头,不过里面nt头会比较复杂,下面详细介绍

pe结构详解

No.2


Dos头

Dos头数据格式定义如下,大小为64字节:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header    WORD   e_magic;                     // Magic number (固定标志,不会变的标志)4D5A MZ,用来判断是否是DOS程序的    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;                    // DOS代码的执行位置,16位DOS下告知程序无法运行    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 (PE文件头偏移,文件偏移值FA)  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

在32位程序下,有用的就是2个字段,第一个是e_magic标志和最后一个字段e_lfanew 是指向PE文件头格式的偏移值

打印dos头的部代码:

LPVOID pFileBuffer = NULL;          PIMAGE_DOS_HEADER pDosHeader = NULL;          
pFileBuffer = ReadPEFile(FILEPATH); if(!pFileBuffer) { printf("文件读取失败n"); return ; } //判断是否是有效的MZ标志 if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) { printf("不是有效的MZ标志n"); free(pFileBuffer); return ; } pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; //打印DOC头 printf("********************DOC头********************n"); printf("MZ标志:%xn",pDosHeader->e_magic); printf("PE偏移:%xn",pDosHeader->e_lfanew);

pe文件的e_magic固定是4D5A 对应的字母也就是MZ,这里的e_lfanew为0130(由于是小端模式,这里的e_lfanew的地址是从右往左读,所以是0130),所以nt头的开始地址是0000+0130=0130

pe结构详解

学习pe结构建议手工先不依赖工具写下每个字段的值,然后用工具解析pe文件,再看看自己手工写下的值跟工具显示的一不一样,这里使用petools

pe结构详解

而DOS stub 则为dos头和nt头中间那部分 是由代码与数据混合而成,这中间的代码不会被运行,可视为垃圾代码,但作为攻击者,这块是个可以藏数据的地方,dos头也是ctf常考的一部分。

No.3


NT头

由结构图可见,nt头可分为三部分,分别为开头4个字节的pe文件标志,然后到pe文件头,接下来是可选pe头,由于pe文件头跟可选pe头都是结构体,他们又会有自己的各项成员

typedef struct _IMAGE_NT_HEADERS {    DWORD Signature;                           //固定值 PE文件标志4个字节    IMAGE_FILE_HEADER FileHeader;              //pe文件头结构体    IMAGE_OPTIONAL_HEADER32 OptionalHeader;    //可选pe头结构体} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 

pe文件头结构:

typedef struct _IMAGE_FILE_HEADER {    WORD    Machine;                         //CPU类型        不可更改    WORD    NumberOfSections;                //节数量         可以更改    DWORD   TimeDateStamp;                   //文件创建时间   可以更改    DWORD   PointerToSymbolTable;            //符号表偏移     可以更改    DWORD   NumberOfSymbols;                 //符号数量       可以更改    WORD    SizeOfOptionalHeader;            //可选pe头的大小 可以更改    WORD    Characteristics;                 //文件属性       不可更改} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

可以看到,一个word是2个字节,dword就是4个字节,所以pe文件头的大小是固定的,也就是20个字节

OptionalHeader只是翻译过来叫可选pe头,但他是pe结构必不可少的一部分,并不是可选不可选的,嘻嘻-0-,虽然可选pe头的大小是不确定的,但在pe文件头可选pe头的结构:

//YES:表示可以改  NO:表示不能改  RES:表示能改但是有限制
typedef struct _IMAGE_OPTIONAL_HEADER { WORD Magic;               //NO 机器型号,判断是 PE是 32位还是 64位 BYTE MajorLinkerVersion;        //YES 链接器版本号高版本 BYTE MinorLinkerVersion;        //YES 链接器版本号低版本,组合起来就是 5.12 其中 5是高版本,C是低版本 DWORD SizeOfCode;            //YES 代码节的总大小(512为一个磁盘扇区) DWORD SizeOfInitializedData;      //YES 初始化数据的节的总大小,也就是.data DWORD SizeOfUninitializedData;     //YES 未初始化数据的节的大小,也就是.data? DWORD AddressOfEntryPoint;       //NO 程序执行入口地址(OEP) RVA(相对虚拟偏移地址) DWORD BaseOfCode;            //YES 代码的节的起始 RVA(相对偏移)也就是代码区的偏移,偏移+模块首地址定位代码区 DWORD BaseOfData;            //YES 数据结的起始偏移(RVA),同上 DWORD ImageBase;            //YES 程序的建议模块基址(意思就是说作参考用的,模块建议基址如果被使用了就会使用别的地址) DWORD SectionAlignment;        //RES 内存中的节对齐 一般是0x1000 DWORD FileAlignment;          //RES 文件中的节对齐 一般是0x200 WORD MajorOperatingSystemVersion;  //YES 操作系统版本号高位 WORD MinorOperatingSystemVersion;  //YES 操作系统版本号低位 WORD MajorImageVersion;        //YES PE版本号高位 WORD MinorImageVersion;        //YES PE版本号低位 WORD MajorSubsystemVersion;      //NO 子系统版本号高位 WORD MinorSubsystemVersion;      //YES 子系统版本号低位 DWORD Win32VersionValue;        //YES 32位系统版本号值,注意只能修改为4 5 6表示操作系统支持nt4.0 以上,5的话依次类推 DWORD SizeOfImage;           //RES 整个程序也就是整PE文件在内存中占用的空间(包含PE映射尺寸) DWORD SizeOfHeaders;          //RES 所有头大小(头的结构体大小)+节表结构体大小,记得值一定是文件对齐值的倍数,也就是到第一节区实际位置的偏移 DWORD CheckSum;            //YES 校验和,对于驱动程序,可能会使用 WORD Subsystem;            //NO 文件的子系统 :0x02表示窗口程序 WORD DllCharacteristics;       //NO DLL文件属性,也可以成为特性,可能DLL文件可以当做驱动程序使用 DWORD SizeOfStackReserve;       //RES 预留的栈的大小 DWORD SizeOfStackCommit;       //RES 立即申请的栈的大小(分页为单位) DWORD SizeOfHeapReserve;       //RES 预留的堆空间大小 DWORD SizeOfHeapCommit;        //RES 立即申请的堆的空间的大小 DWORD LoaderFlags;          //YES 与调试有关 DWORD NumberOfRvaAndSizes;      //RES 下面数据目录的数量:0x10表示有16个 IMAGE_DATA_DIRECTORY DataDirectory[16]; //RES 数据目录,默认16个,可以查看宏*/} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

可以看看这里16进制编辑器的显示

pe结构详解
pe结构详解

打印nt头的代码:


VOID PrintNTHeaders()                          {                            LPVOID pFileBuffer = NULL;                          PIMAGE_DOS_HEADER pDosHeader = NULL;                          PIMAGE_NT_HEADERS pNTHeader = NULL;                          PIMAGE_FILE_HEADER pPEHeader = NULL;                          PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;                          PIMAGE_SECTION_HEADER pSectionHeader = NULL;                                                    pFileBuffer = ReadPEFile(FILEPATH);                          if(!pFileBuffer)                          {                            printf("文件读取失败n");                          return ;                         }                                                    //判断是否是有效的MZ标志                          if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE)                          {                            printf("不是有效的MZ标志n");                          free(pFileBuffer);                          return ;                         }                          pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;                          //打印DOC头                          printf("********************DOC头********************n");                          printf("MZ标志:%xn",pDosHeader->e_magic);                          printf("PE偏移:%xn",pDosHeader->e_lfanew);                          //判断是否是有效的PE标志                          if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)                          {                            printf("不是有效的PE标志n");                          free(pFileBuffer);                          return ;                        }                          pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer+pDosHeader->e_lfanew);                          //打印NT头                          printf("********************NT头********************n");                          printf("NT:%xn",pNTHeader->Signature);                          pPEHeader = (PIMAGE_FILE_HEADER)(((DWORD)pNTHeader) + 4);                          printf("********************PE头********************n");                          printf("PE:%xn",pPEHeader->Machine);                          printf("节的数量:%xn",pPEHeader->NumberOfSections);                          printf("SizeOfOptionalHeader:%xn",pPEHeader->SizeOfOptionalHeader);                          //可选PE头                          pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader+IMAGE_SIZEOF_FILE_HEADER);                          printf("********************OPTIOIN_PE头********************n");                          printf("OPTION_PE:%xn",pOptionHeader->Magic);                          //释放内存                          free(pFileBuffer);                        }                          

No.4


节表

节表结构体如下,一般一个pe文件不止一个节表,每个节表的大小为40字节,pe文件节表的数量可在pe文件头里面的NumberOfSections 字段可见

typedef struct _IMAGE_SECTION_HEADER{    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];  //YES 节区的名字 8个字节    union    {            DWORD   PhysicalAddress;                      DWORD   VirtualSize;        //YES 节区在内存的大小,实际值是按内存对齐算    } Misc;    DWORD   VirtualAddress;          //虚拟地址 节区的 RVA地址(拷到内存中哪个位置)    DWORD   SizeOfRawData;           //在文件中对齐的尺寸(拷多大)    DWORD   PointerToRawData;         //在文件中的偏移FA(从文件哪里开始拷)    DWORD   PointerToRelocations;       //在 OBJ文件中使用    DWORD   PointerToLinenumbers;      //行号表位置,调试使用    WORD    NumberOfRelocations;       //在 OBJ文件中使用    WORD    NumberOfLinenumbers;        //行号表的数量    DWORD   Characteristics;         //节的属性} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

可选pe头结束紧跟着就是节表,如图有6个节表

pe结构详解

打印全部节表的部分代码:

pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader+pPEHeader->SizeOfOptionalHeader);//nt头开始位置+pe标志+pe文件头+可选pe头大小=  pSectionHeader开始位置printf("NumberOfSections: %dn",pPEHeader->NumberOfSections);  
for(int i=0;i<pPEHeader->NumberOfSections;i++) //遍历节表各字段{ //printf("%dn",pPEHeader->NumberOfSections); //pSectionHeader=pSectionHeader+i*IMAGE_SIZEOF_SECTION_HEADER; printf("pSectionHeader->Name:%sn",pSectionHeader->Name); printf("pSectionHeader->VirtualSize: %xn",pSectionHeader->Misc.VirtualSize); printf("pSectionHeader->VirtualAddress:%xn",pSectionHeader->VirtualAddress); printf("pSectionHeader->SizeOfRawData:%xn",pSectionHeader->SizeOfRawData); printf("pSectionHeader->PointerToRawData:%xn",pSectionHeader->PointerToRawData); printf("pSectionHeader->PointerToRelocations:%xn",pSectionHeader->PointerToRelocations); printf("pSectionHeader->PointerToLinenumbers:%xn",pSectionHeader->PointerToLinenumbers); printf("pSectionHeader->NumberOfRelocations:%xn",pSectionHeader->NumberOfRelocations); printf("pSectionHeader->NumberOfLinenumbers: %xn",pSectionHeader->NumberOfLinenumbers); printf("pSectionHeader->NumberOfLinenumbers: %xn",pSectionHeader->NumberOfLinenumbers); printf("pSectionHeader->Characteristics: %xn",pSectionHeader->Characteristics); printf("n"); printf("%dn",sizeof(pSectionHeader));  pSectionHeader++;}

petools 节表显示:

pe结构详解

这里用*号标记出比较重要的字段,如果要编写程序解析pe文件的pe头,这里星号字段是肯定会用到,而且要理解其作用的各个头中比较重要的字段:

1、DOC头:                                        WORD   e_magic                *        "MZ标记" 用于判断是否为可执行文件.            DWORD  e_lfanew;              *        PE头相对于文件的偏移,用于定位PE文件                                                    2、标准PE头:                                        WORD    Machine;              *        程序运行的CPU型号:0x0 任何处理器/0x14C 386及后续处理器            WORD    NumberOfSections;     *        文件中存在的节的总数,如果要新增节或者合并节 就要修改这个值.            DWORD   TimeDateStamp;        *        时间戳:文件的创建时间(和操作系统的创建时间无关),编译器填写的.            DWORD   PointerToSymbolTable;                     DWORD   NumberOfSymbols;                     WORD    SizeOfOptionalHeader; *        可选PE头的大小,32位PE文件默认E0h 64位PE文件默认为F0h  大小可以自定义.            WORD    Characteristics;      *        每个位有不同的含义,可执行文件值为10F 即0 1 2 3 8位置1                                 3、可选PE头:                                        WORD    Magic;        *        说明文件类型:10B 32位下的PE文件     20B 64位下的PE文件            BYTE    MajorLinkerVersion;                    BYTE    MinorLinkerVersion;                    DWORD   SizeOfCode;*        所有代码节的和,必须是FileAlignment的整数倍 编译器填的  没用            DWORD   SizeOfInitializedData;*        已初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的  没用            DWORD   SizeOfUninitializedData;*        未初始化数据大小的和,必须是FileAlignment的整数倍 编译器填的  没用            DWORD   AddressOfEntryPoint;*        程序入口            DWORD   BaseOfCode;*        代码开始的基址,编译器填的   没用            DWORD   BaseOfData;*        数据开始的基址,编译器填的   没用            DWORD   ImageBase;*        内存镜像基址            DWORD   SectionAlignment;*        内存对齐            DWORD   FileAlignment;*        文件对齐            WORD    MajorOperatingSystemVersion;                    WORD    MinorOperatingSystemVersion;                    WORD    MajorImageVersion;                    WORD    MinorImageVersion;                    WORD    MajorSubsystemVersion;                    WORD    MinorSubsystemVersion;                    DWORD   Win32VersionValue;                    DWORD   SizeOfImage;*        内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍            DWORD   SizeOfHeaders;*        所有头+节表按照文件对齐后的大小,否则加载会出错            DWORD   CheckSum;*        校验和,一些系统文件有要求.用来判断文件是否被修改.            WORD    Subsystem;                    WORD    DllCharacteristics;                    DWORD   SizeOfStackReserve;*        初始化时保留的堆栈大小             DWORD   SizeOfStackCommit;*        初始化时实际提交的大小           DOS  DWORD   SizeOfHeapReserve;*        初始化时保留的堆大小           ...  DWORD   SizeOfHeapCommit;*        初始化时实践提交的大小           PE标记  DWORD   LoaderFlags;                  标准PE  DWORD   NumberOfRvaAndSizes;*        目录项数目          可选PE头  

No.5


RVA与FOA

在介绍数据目录前说这个之前补充以下几点知识:相对虚拟地址(Relative Virtual Address,RVA) 又叫内存偏移 Memory Offset

文件偏移地址(FO_A_)  又叫 文件偏移 File OffsetVA: 全名virtualAddress 虚拟地址. 就是内存中虚拟地址. 例如 0x00401000

由于文件读取到内存中会进行拉伸,如下图:

pe结构详解
pe结构详解

因为读到内存中文件被拉伸过,所以文件中的地址偏移跟内存中的偏移不一样设x 为节数据的任意一位置内存偏移转文件偏移总结:

1.计算RVA 公式: x - ImageBase == RVA

2.计算差值偏移: RVA - 节.VirtualAddress == 差值偏移.    

3.计算FOA: 差值偏移 + 节.PointerToRawData == FOA

rva转foa代码:

DWORD RvaToFileOffset(IN LPVOID pFileBuffer,IN DWORD dwRva){    PIMAGE_DOS_HEADER pDosHeader = NULL;    PIMAGE_NT_HEADERS pNTHeader = NULL;    PIMAGE_FILE_HEADER pPEHeader = NULL;    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader = NULL;        if (!pFileBuffer)  {    printf("(RvaToFileOffset)Can't open file!n");    return 0;  }  //判断是否是有效的MZ标志    if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //typedef DWORD near          *PDWORD;    {      printf("不是有效的MZ标志n");    free(pFileBuffer);    return 0;   }    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;  //判断是否是有效的PE标志    if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)    {      printf("不是有效的PE标志n");    free(pFileBuffer);    return 0;  }        
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew); pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader); PIMAGE_SECTION_HEADER pSectionTemp = pSectionHeader;
if(dwRva<pOptionHeader->SizeOfHeaders) { return dwRva; } else { for(int i=0;i<pPEHeader->NumberOfSections;i++,pSectionTemp++) { if(dwRva>=pSectionTemp->VirtualAddress && dwRva<pSectionTemp->VirtualAddress+pSectionTemp->Misc.VirtualSize) { return dwRva-pSectionTemp->VirtualAddress+pSectionTemp->PointerToRawData; } } }
printf("RvaToFileOffset faildn"); return 0;

}

foa转rva代码:

DWORD FoatoRva(IN LPVOID pFileBuffer,IN DWORD dwFoa){    PIMAGE_DOS_HEADER pDosHeader = NULL;    PIMAGE_NT_HEADERS pNTHeader = NULL;    PIMAGE_FILE_HEADER pPEHeader = NULL;    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader = NULL;        if (!pFileBuffer)  {    printf("(RvaToFileOffset)Can't open file!n");    return 0;  }  //判断是否是有效的MZ标志    if(*((PWORD)pFileBuffer) != IMAGE_DOS_SIGNATURE) //typedef DWORD near          *PDWORD;    {      printf("不是有效的MZ标志n");    free(pFileBuffer);    return 0;   }    pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer;  //判断是否是有效的PE标志    if(*((PDWORD)((DWORD)pFileBuffer+pDosHeader->e_lfanew)) != IMAGE_NT_SIGNATURE)    {      printf("不是有效的PE标志n");    free(pFileBuffer);    return 0;  }        
pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew); pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader); PIMAGE_SECTION_HEADER pSectionTemp = pSectionHeader;
if(dwFoa<pOptionHeader->SizeOfHeaders) { return dwFoa; } else { for(int i=0;i<pPEHeader->NumberOfSections;i++,pSectionTemp++) { if(dwFoa>=pSectionTemp->PointerToRawData && dwFoa<pSectionTemp->PointerToRawData+pSectionTemp->SizeOfRawData) { return dwFoa-pSectionTemp->PointerToRawData+pSectionTemp->VirtualAddress; } } }
printf("FoatoRva faildn");  return 0;}

总结就是在文件中计算偏移要用foa,内存中计算偏移要用rva,如果需要互相转换就调用相关转换函数

No.6


数据目录

接下来重点介绍下位于可选pe头的最后的IMAGE_DATA_DIRECTORY DataDirectory[16];  //数据目录,默认16个

# define IMAGE_DIRECTORY_ENTRY_EXPORT     0 导出表# define IMAGE_DIRECTORY_ENTRY_IMPORT     1 导入表 # define IMAGE_DIRECTORY_ENTRY_RESOURCE     2 资源目录# define IMAGE_DIRECTORY_ENTRY_EXCEPTION     3 异常目录# define IMAGE_DIRECTORY_ENTRY_SECURITY     4 安全目录# define IMAGE_DIRECTORY_ENTRY_BASERELOC           5 重定位基本表# define IMAGE_DIRECTORY_ENTRY_DEBUG     6 调试目录# define IMAGE_DIRECTORY_ENTRY_COPYRIGHT     7 描术字串# define IMAGE_DIRECTORY_ENTRY_GLOBALPTR     8 机器值# define IMAGE_DIRECTORY_ENTRY_TLS     9 TLS目录# define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG   10 载入配值目录# define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT       11 绑定导入表# define IMAGE_DIRECTORY_ENTRY_IAT     12 导入地址表# define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13 延迟载入描述# define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR     14 COM信息
pe结构详解

这里有几个数据目录比较重要,这里逐一介绍,分别是导出表、导入表、重定位基本表、绑定导入表和导入地址表,至少得懂得输出、修改和移动这几个表,才算掌握pe结构的大概

     IMAGE_DIRECTORY_ENTRY_EXPORT          0   // 导出表     IMAGE_DIRECTORY_ENTRY_IMPORT          1   // 导入表     IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // 资源表     IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table     IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory     IMAGE_DIRECTORY_ENTRY_IAT            12   // 导入表地址

No.7


导出表

一般的简单pe文件并不存在导出表,一般的dll都有导出表,因为写有导出函数可以给到别的pe文件调用功能函数

typedef struct _IMAGE_EXPORT_DIRECTORY {                      DWORD   Characteristics;        // 未使用              DWORD   TimeDateStamp;        // 时间戳              WORD    MajorVersion;        // 未使用          11    WORD    MinorVersion;        // 未使用          12    DWORD   Name;        // 指向该导出表文件名字符串          13    DWORD   Base;        // 导出函数起始序号          15    DWORD   NumberOfFunctions;        // 所有导出函数的个数          17    DWORD   NumberOfNames;        // 以函数名字导出的函数个数              DWORD   AddressOfFunctions;     // 导出函数地址表RVA                      DWORD   AddressOfNames;         // 导出函数名称表RVA                      DWORD   AddressOfNameOrdinals;  // 导出函数序号表RVA                  } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;        

从下图可以看到,name是dll的名称字符串地址的RVA,而AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals则分别对应导出函数地址表RVA、导出函数名称表RVA和导出函数序号表RVA,他们的数量也是对应NumberOfFunctions和NumberOfNames,分别提供按照名字导出和序号导出两种。

pe结构详解

petools显示的导出表:

pe结构详解

输入导出表代码:

VOID PrintExportDrectory(){  LPVOID pFileBuffer = NULL;  PIMAGE_DOS_HEADER pDosHeader = NULL;    PIMAGE_NT_HEADERS pNTHeader = NULL;    PIMAGE_FILE_HEADER pPEHeader = NULL;    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader = NULL;      ReadPEFile(DLLPATH, &pFileBuffer);    if(!pFileBuffer){        printf("PrintExportDrectory读取文件失败n");        return;    }

pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew); pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader);


IMAGE_DATA_DIRECTORY* pFileBufferr_Data = NULL; pFileBufferr_Data=(IMAGE_DATA_DIRECTORY*)((DWORD)(&(pOptionHeader->NumberOfRvaAndSizes)) + 4);//数据目录起始位置
IMAGE_EXPORT_DIRECTORY* pFileBuffer_Drectory=(IMAGE_EXPORT_DIRECTORY*)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBufferr_Data->VirtualAddress));
printf("---------------------------------------打印导出表信息--------------------------------------n"); printf("ExportDrectory的VirtualAddress:%xn",pOptionHeader->DataDirectory[0].VirtualAddress); printf("ExportDrectory的Size:%xn",pOptionHeader->DataDirectory[0].Size); if(pOptionHeader->DataDirectory[0].Size==0 && pOptionHeader->DataDirectory[0].VirtualAddress==0) { printf("ExportDrectory is emptyn"); return ; } printf("Characteristics:%xn",pFileBuffer_Drectory->Characteristics); printf("TimeDateStamp:%xn",pFileBuffer_Drectory->TimeDateStamp); printf("MajorVersion:%xn",pFileBuffer_Drectory->MajorVersion); printf("Name:%xn",pFileBuffer_Drectory->Name); printf("Base:%xn",pFileBuffer_Drectory->Base); printf("NumberOfFunctions:%xn",pFileBuffer_Drectory->NumberOfFunctions); printf("NumberOfNames:%xn",pFileBuffer_Drectory->NumberOfNames); printf("AddressOfFunctions:%xn",pFileBuffer_Drectory->AddressOfFunctions); printf("AddressOfNames:%xn",pFileBuffer_Drectory->AddressOfNames); printf("AddressOfNameOrdinals:%xn",pFileBuffer_Drectory->AddressOfNameOrdinals);
/*打印地址表*/ printf("*************** AddressOfFunctions ***************n"); LPDWORD AddressOfFunctions = (LPDWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfFunctions));
for(int i=0;i < pFileBuffer_Drectory->NumberOfFunctions;i++) { printf("第%d个pFileBuffer_Drectory->NumberOfFunctions :%xn",i,*AddressOfFunctions); AddressOfFunctions++; }
/*打印序号表*/ printf("*************** AddressOfNameOrdinals ***************n"); LPWORD AddressOfNameOrdinals=(LPWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfNameOrdinals)); for(int k=0;k < pFileBuffer_Drectory->NumberOfNames;k++) { printf("第%d个pFileBuffer_Drectory->AddressOfNameOrdinals :%xn",k,*AddressOfNameOrdinals); AddressOfNameOrdinals++; }

/*打印名称表*/ printf("*************** AddressOfNames ***************n"); LPDWORD AddressOfNames=(LPDWORD)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer,pFileBuffer_Drectory->AddressOfNames)); for(int n=0;n < pFileBuffer_Drectory->NumberOfNames;n++) { printf("第%d个pFileBuffer_Drectory->AddressOfNames :%sn",n,LPSTR((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *AddressOfNames))); printf("第%d个pFileBuffer_Drectory->AddressOfNames 长度为 :%dn",n,strlen(LPSTR((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *AddressOfNames)))); AddressOfNames++; }
free(pFileBuffer);
}

No.8


导入表和导入名称表、导入地址表

位于数据目录的第二个即是导入表,大小为20字节,结构体如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {                        union {                            DWORD   Characteristics;                                       DWORD   OriginalFirstThunk;                     //RVA 指向IMAGE_THUNK_DATA结构数组            };                        DWORD   TimeDateStamp;                           //时间戳            DWORD   ForwarderChain;                                      DWORD   Name;            //RVA,指向dll名字,该名字以 两个字节的0结尾            DWORD   FirstThunk;                             //RVA,指向IMAGE_THUNK_DATA结构数组        } IMAGE_IMPORT_DESCRIPTOR;                    typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;  

其中OriginalFirstThunk和FirstThunk又指向了一个IMAGE_THUNK_DATA结构体,分别是INT和IAT,IMAGE_THUNK_DATA结构体如下:

typedef struct _IMAGE_IMPORT_BY_NAME {                              WORD    Hint;            //可能为空,编译器决定 如果不为空 是函数在导出表中的索引                  BYTE    Name[1];            //函数名称,这里只有一个字节,采用越界的方式读取              } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;  

pe文件加载前:

pe结构详解

pe文件加载后:

输出导入表的步骤

pe结构详解

一、找到第二个数据目录,把rva转成foa,找到导入表位置,然后遍历每个导入表,直到有sizeof(IMAGE_IMPORT_DESCRIPTOR)个0出现,即20个0结束

二、每个导入表遍历OriginalFirstThunk和FirstThunk,直到出现4字节的0,遍历OriginalFirstThunk和FirstThunk的时候,如果最高位为1,即为按序号导出,应该去掉最高位的1,然后按序号导出,如果最高位不是1,就是直接对应IMAGE_IMPORT_BY_NAME的rva步骤如下图:

pe结构详解

sizeOf(IMAGE_IMPORT_DESCRIPTOR)个0代表导入表结束

pe结构详解
pe结构详解
pe结构详解

petools显示的导入表:

pe结构详解

打印导入表代码:

VOID PrintImport(){  LPVOID pFileBuffer = NULL;  PIMAGE_DOS_HEADER pDosHeader = NULL;    PIMAGE_NT_HEADERS pNTHeader = NULL;    PIMAGE_FILE_HEADER pPEHeader = NULL;    PIMAGE_OPTIONAL_HEADER32 pOptionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader = NULL;    PIMAGE_SECTION_HEADER pSectionHeader_last = NULL;  BOOL isOK = FALSE;  size_t FileBufferSize=ReadPEFile(NOTEPAD_FILEPATH,&pFileBuffer);  DWORD addr;
if (!pFileBuffer) { printf("(MoveExport)Can't open file!n"); return ; }
pDosHeader = (PIMAGE_DOS_HEADER)pFileBuffer; pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pFileBuffer + pDosHeader->e_lfanew); pPEHeader = (PIMAGE_FILE_HEADER)((DWORD)pNTHeader + 4); // 这里必须强制类型转换 pOptionHeader = (PIMAGE_OPTIONAL_HEADER32)((DWORD)pPEHeader + IMAGE_SIZEOF_FILE_HEADER); pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pOptionHeader + pPEHeader->SizeOfOptionalHeader); PIMAGE_IMPORT_DESCRIPTOR pFileBuffer_Import = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer+RvaToFileOffset(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress)); DWORD OriginalFirstThunk_temp=NULL;
PDWORD OriginalFirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->OriginalFirstThunk)); PDWORD FirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->FirstThunk)); printf("pFileBuffer_Import->FirstThunk:%xn",pFileBuffer_Import->FirstThunk); printf("pFileBuffer_Import->OriginalFirstThunk:%xn",pFileBuffer_Import->OriginalFirstThunk);
int n=1; int m=1; while (pFileBuffer_Import->OriginalFirstThunk) { LPSTR name = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name)); //1 输出导出表dll的名字 printf("第%d个dll的名称为:%sn", n, name); LPSTR pName = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name)); printf("================%s================n", name); //解析INT表 PDWORD OriginalFirstThunk = (PDWORD)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, (DWORD)pFileBuffer_Import->OriginalFirstThunk)); while (*OriginalFirstThunk) { if ((*OriginalFirstThunk & 0x80000000) >> 31 == 1) { //如果第一位为1,表示为序号导入 DWORD ord = *OriginalFirstThunk & 0x7fffffff; printf("第%d个函数按序号导入:%dn",m, ord); } else { PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *OriginalFirstThunk)); printf("第%d个按函数名导入:%sn",m, pName->Name); } //下一个INT表项 OriginalFirstThunk++; m++; } //下一张导入表 printf("按enter键显示下一个dll导入表信息"); getchar(); pFileBuffer_Import++; n++; m = 1; printf("n");
} printf("************************FirstThunk阶段************************n"); pFileBuffer_Import = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pOptionHeader->DataDirectory[1].VirtualAddress)); n = 1; m = 1;
//PDWORD OriginalFirstThunk=PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->OriginalFirstThunk));
while (pFileBuffer_Import->FirstThunk) {
LPSTR name = (LPSTR)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->Name)); //1 输出导出表dll的名字 printf("第%d个dll的名称为:%sn", n, name); PDWORD FirstThunk = PDWORD((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, pFileBuffer_Import->FirstThunk)); while (*FirstThunk) { if (pFileBuffer_Import->TimeDateStamp == 0) { if ((*FirstThunk & 0x80000000) >> 31 == 1) { OriginalFirstThunk_temp = *FirstThunk & 0x7FFFFFFF; printf("第%d个函数的序号为:%dn", m, OriginalFirstThunk_temp); } else { //PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *OriginalFirstThunk)); PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pFileBuffer + RvaToFileOffset(pFileBuffer, *FirstThunk)); printf("第%d个的函数名导入:%sn", m, pName->Name); } } else { printf("TimeDateStamp 不为0 已添加绑定导入表 直接输出函数地址n"); printf("第%d个函数的地址为:%xn", m, *FirstThunk); }
FirstThunk++; m++; }
//2 遍历OriginalFirstThunk 输出每个函数的序号或者名字 //3 遍历FirstThunk 输出每个函数的序号或者名字
printf("按enter键显示下一个dll导入表信息:n"); getchar();
pFileBuffer_Import++; n++; }


printf("pe加载前的导入表输出完成!n"); }

No.9


绑定导入表

绑定导入表位于数据目录的第12项结构体如下:

 typedef struct _IMAGE_BOUND_FORWARDER_REF {             DWORD   TimeDateStamp;             WORD    OffsetModuleName;             WORD    Reserved;        } IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;   

PE加载EXE相关的DLL时,首先会根据IMAGE_IMPORT_DESCRIPTOR结构中的TimeDateStamp来判断是否要重新          
计算IAT表中的地址。
TimeDateStamp == 0  未绑定 
TimeDateStamp == -1 已绑定 真正的绑定时间为IMAGE_BOUND_IMPORT_DESCRIPTOR的TimeDateStamp

No.10


重定位表

数据目录项的第6个结构,就是重定位表

 typedef struct _IMAGE_BASE_RELOCATION {             DWORD   VirtualAddress;             DWORD   SizeOfBlock;        } IMAGE_BASE_RELOCATION;        typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;        

重定位表是这样连续一块一块的,直到遇到8个字节的0为结束

pe结构详解

一般exe用不到重定位表,用的比较多的是dll,一般分配内存时候占不到原有的位置,则依靠重定位表修复地址,以免调用时候发生错误。

修复步骤:

1、通过IMAGE_DATA_DIRECTORY结构的VirtualAddress        属性 找到第一个IMAGE_BASE_RELOCATION                2、判断一共有几块数据:                最后一个结构的VirtualAddress与SizeOfBlock都为0                3、具体项 宽度:2字节                 也就是这个数据                 内存中的页大小是1000H 也就是说2的12次方 就可以表示         一个页内所有的偏移地址 具体项的宽度是16字节 高四位         代表类型:值为3 代表的是需要修改的数据 值为0代表的是         用于数据对齐的数据,可以不用修改.也就是说 我们只关注         高4位的值为3的就可以了.                4、VirtualAddress 宽度:4字节                当前这一个块的数据,每一个低12位的值+VirtualAddress 才是        真正需要修复的数据的RVA                真正的RVA = VirtualAddress + 具体项的低12位                5、SizeOfBlock 宽度:4字节                当前块的总大小                具体项的数量 = (SizeOfBlock - 8)/2         

重定位表:

pe结构详解




招聘启事

安恒雷神众测SRC运营(实习生)
————————
【职责描述】
1.  负责SRC的微博、微信公众号等线上新媒体的运营工作,保持用户活跃度,提高站点访问量;
2.  负责白帽子提交漏洞的漏洞审核、Rank评级、漏洞修复处理等相关沟通工作,促进审核人员与白帽子之间友好协作沟通;
3.  参与策划、组织和落实针对白帽子的线下活动,如沙龙、发布会、技术交流论坛等;
4.  积极参与雷神众测的品牌推广工作,协助技术人员输出优质的技术文章;
5.  积极参与公司媒体、行业内相关媒体及其他市场资源的工作沟通工作。

【任职要求】 
 1.  责任心强,性格活泼,具备良好的人际交往能力;
 2.  对网络安全感兴趣,对行业有基本了解;
 3.  良好的文案写作能力和活动组织协调能力。


简历投递至 

[email protected]

设计师(实习生)

————————

【职位描述】
负责设计公司日常宣传图片、软文等与设计相关工作,负责产品品牌设计。

【职位要求】
1、从事平面设计相关工作1年以上,熟悉印刷工艺;具有敏锐的观察力及审美能力,及优异的创意设计能力;有 VI 设计、广告设计、画册设计等专长;
2、有良好的美术功底,审美能力和创意,色彩感强;精通photoshop/illustrator/coreldrew/等设计制作软件;
3、有品牌传播、产品设计或新媒体视觉工作经历;

【关于岗位的其他信息】
企业名称:杭州安恒信息技术股份有限公司
办公地点:杭州市滨江区安恒大厦19楼
学历要求:本科及以上
工作年限:1年及以上,条件优秀者可放宽


简历投递至 

[email protected]

安全招聘
————————

公司:安恒信息
岗位:Web安全 安全研究员
部门:战略支援部
薪资:13-30K
工作年限:1年+
工作地点:杭州(总部)、广州、成都、上海、北京

工作环境:一座大厦,健身场所,医师,帅哥,美女,高级食堂…

【岗位职责】
1.定期面向部门、全公司技术分享;
2.前沿攻防技术研究、跟踪国内外安全领域的安全动态、漏洞披露并落地沉淀;
3.负责完成部门渗透测试、红蓝对抗业务;
4.负责自动化平台建设
5.负责针对常见WAF产品规则进行测试并落地bypass方案

【岗位要求】
1.至少1年安全领域工作经验;
2.熟悉HTTP协议相关技术
3.拥有大型产品、CMS、厂商漏洞挖掘案例;
4.熟练掌握php、java、asp.net代码审计基础(一种或多种)
5.精通Web Fuzz模糊测试漏洞挖掘技术
6.精通OWASP TOP 10安全漏洞原理并熟悉漏洞利用方法
7.有过独立分析漏洞的经验,熟悉各种Web调试技巧
8.熟悉常见编程语言中的至少一种(Asp.net、Python、php、java)

【加分项】
1.具备良好的英语文档阅读能力;
2.曾参加过技术沙龙担任嘉宾进行技术分享;
3.具有CISSP、CISA、CSSLP、ISO27001、ITIL、PMP、COBIT、Security+、CISP、OSCP等安全相关资质者;
4.具有大型SRC漏洞提交经验、获得年度表彰、大型CTF夺得名次者;
5.开发过安全相关的开源项目;
6.具备良好的人际沟通、协调能力、分析和解决问题的能力者优先;
7.个人技术博客;
8.在优质社区投稿过文章;


岗位:安全红队武器自动化工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.熟练使用Python、java、c/c++等至少一门语言作为主要开发语言;
2.熟练使用Django、flask 等常用web开发框架、以及熟练使用mysql、mongoDB、redis等数据存储方案;
3:熟悉域安全以及内网横向渗透、常见web等漏洞原理;
4.对安全技术有浓厚的兴趣及热情,有主观研究和学习的动力;
5.具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 

[email protected]

岗位:红队武器化Golang开发工程师
薪资:13-30K
工作年限:2年+
工作地点:杭州(总部)

【岗位职责】
1.负责红蓝对抗中的武器化落地与研究;
2.平台化建设;
3.安全研究落地。

【岗位要求】
1.掌握C/C++/Java/Go/Python/JavaScript等至少一门语言作为主要开发语言;
2.熟练使用Gin、Beego、Echo等常用web开发框架、熟悉MySQL、Redis、MongoDB等主流数据库结构的设计,有独立部署调优经验;
3.了解docker,能进行简单的项目部署;
3.熟悉常见web漏洞原理,并能写出对应的利用工具;
4.熟悉TCP/IP协议的基本运作原理;
5.对安全技术与开发技术有浓厚的兴趣及热情,有主观研究和学习的动力,具备正向价值观、良好的团队协作能力和较强的问题解决能力,善于沟通、乐于分享。

【加分项】
1.有高并发tcp服务、分布式、消息队列等相关经验者优先;
2.在github上有开源安全产品优先;
3:有过安全开发经验、独自分析过相关开源安全工具、以及参与开发过相关后渗透框架等优先;
4.在freebuf、安全客、先知等安全平台分享过相关技术文章优先;
5.具备良好的英语文档阅读能力。


简历投递至 

[email protected]

pe结构详解

专注渗透测试技术

全球最新网络攻击技术

END

pe结构详解

原文始发于微信公众号(白帽子):pe结构详解

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年10月18日07:58:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   pe结构详解http://cn-sec.com/archives/1356178.html

发表评论

匿名网友 填写信息