内容纲要

PE(Portable Executable)

分类

可执行系列:exe、scr

库系列:dll(动态)、ocx、cpl、drv、lib(静态)

驱动程序系列:sys、vxd

对象文件系列:obj

文件格式

exe运行所需要的所有信息都存储在PE头中。

image-20210523012528192

从DOS头到节区头是PE头部分,其下的节区合称为PE体。文件中使用偏移(offset),内存中使用VA(Virtual Address,虚拟地址)来表示位置。文件加载到内存时,情况就会发生变化(节区大小、位置等)。文件的内容一般可分为代码(.text)、数据(.data)、资源(.rsrc)节,分别保存。PE头与各节区的尾部存在一个区域,成为NULL填充。文件/内存中节区的起始位置应该在各文件/内存最小单位的倍数上,空白区域使用NULL进行填充(如上图所示)。

VA指进程虚拟内存的绝对地址,RVA(Relative Virtual Address,相对虚拟地址)指从某个基准未知(ImageBase)开始的相对地址。VA与RVA的换算满足如下公式:

RVA + IamgeBase = VA

PE头内部信息主要以RVA的形式进行存储,主要原因是PE文件(主要是DLL)加载到进程虚拟内存的特定位置时, 该位置可能已经加载了其他PE文件(DLL)。此时需要进行重定位将其加载到其他的空白位置,保证程序的正常运行。

PE头

DOS头、DOS存根、NT头(PE签名、PE文件头、PE可选头)、节区头

DOS头

DOS头的作用是兼容MS-DOS操作系统中的可执行文件,对于32位PE文件来说,DOS所起的作用就是显示一行文字,提示用户:我需要在32位windows上才可以运行。

DOS头定义:

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;

一共64字节。

重要的两个域:

e_magic:WORD类型,值是一个常数0x4D5A,用文本编辑器查看该值位‘MZ’,可执行文件必须都是'MZ'开头。(MZ是MS-DOS的开发者之一的Mark Zbikowski的缩写)

e_lfanew:为32位可执行文件扩展的域,用来表示DOS头之后的NT头相对文件起始地址的偏移

image-20210523012502335

NT头(PE签名、PE文件头、PE可选头)

由DOS头的e_lfanew(相对文件的偏移)所指,是32位PE文件中最有用的头。

248字节

定义:

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;//签名    4字节
    IMAGE_FILE_HEADER FileHeader;//PE文件物理分布的信息  20字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;//PE文件逻辑分布的信息    224字节
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

形如:

image-20210523012610637

image-20210522215955287

PE签名

Signature:类似于DOS头中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。

PE文件头

FileHeader:IMAGE_FILE_HEADER是PE文件头

定义:

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:该文件的运行平台,是x86、x64还是I64等等。可以是下面值里的某一个。

#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_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
#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
#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)
#define IMAGE_FILE_MACHINE_M32R              0x9041  // M32R little-endian
#define IMAGE_FILE_MACHINE_CEE               0xC0EE

②NumberOfSections:该PE文件中有多少个节,也就是节表中的项数。

③TimeDateStamp:PE文件的创建时间,一般由链接器填写。

④PointerToSymbolTable:COFF文件符号表在文件中的偏移,主要用于调试。

⑤NumberOfSymbols:符号表的数量。

⑥SizeOfOptionalHeader:紧随其后的可选头(IMAGE_OPTIONAL_DEADER32)的大小。

⑦Characteristics:可执行文件的属性,可以是下面这些值按位相或。

#define IMAGE_FILE_RELOCS_STRIPPED   0x0001 // 重定位信息被移除 
#define IMAGE_FILE_EXECUTABLE_IMAGE   0x0002 // 文件可执行 
#define IMAGE_FILE_LINE_NUMS_STRIPPED  0x0004 // 行号被移除 
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // 符号被移除 
……..
#define IMAGE_FILE_32BIT_MACHINE  0x0100 // 32位机器 
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // .dbg文件的调试信息被移除 
………………….
#define IMAGE_FILE_SYSTEM       0x1000 // 系统文件 
#define IMAGE_FILE_DLL         0x2000 // 文件是一个dll 
#define IMAGE_FILE_UP_SYSTEM_ONLY    0x4000 // 文件只能运行在单处理器上

PE文件头定义了PE文件的一些基本信息和属性,这些属性会在PE加载器加载时用到,如果加载器发现PE文件头中定义的一些属性不满足当前的运行环境,将会终止加载该PE。

image-20210522220605137

PE可选头

在不同的平台下是不一样的,例如32位下是IMAGE_OPTIONAL_HEADER32,而在64位下是IMAGE_OPTIONAL_HEADER64。

224字节

以下列举32位:

typedef struct _IMAGE_OPTIONAL_HEADER {
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    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;
    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;

image-20210522223857192

①Magic:可选头的类型。

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b  // 32位PE可选头
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b  // 64位PE可选头
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107 

②MajorLinkerVersion和MinorLinkerVersion:链接器的版本号。

③SizeOfCode:代码段的长度,如果有多个代码段,则是代码段长度的总和。

image-20210522224425276

image-20210522224643495

④SizeOfInitializedData:初始化的数据长度。

image-20210522224704363

image-20210522224713040

⑤SizeOfUninitializedData:未初始化的数据长度。

⑥AddressOfEntryPoint:程序入口的RVA,对于exe这个地址可以理解为WinMain的RVA。对于DLL,这个地址可以理解为DllMain的RVA,如果是驱动程序,可以理解为DriverEntry的RVA。

[OEP = ImageBase + (AddressOfEntryPoint)RVA]

image-20210522225121739

image-20210522225143457

⑦BaseOfCode:代码段起始地址的RVA。

⑧BaseOfData:数据段起始地址的RVA。

⑨ImageBase:映象(加载到内存中的PE文件)的基地址,这个基地址是建议,对于DLL来说,如果无法加载到这个地址,系统会自动为其选择地址。

image-20210522224745335

⑩SectionAlignment:节对齐,PE中的节被加载到内存时会按照这个域指定的值来对齐,比如这个值是0x1000,那么每个节的起始地址的低12位都为0。(一般为0x00001000)

image-20210522225200475

⑪FileAlignment:节在文件中按此值对齐,SectionAlignment必须大于或等于FileAlignment。

image-20210522225238078

⑫MajorOperatingSystemVersion、MinorOperatingSystemVersion:所需操作系统的版本号,随着操作系统版本越来越多,这个好像不是那么重要了。

⑬MajorImageVersion、MinorImageVersion:映象的版本号,这个是开发者自己指定的,由链接器填写。

⑭MajorSubsystemVersion、MinorSubsystemVersion:所需子系统版本号。

⑮Win32VersionValue:保留,必须为0。

⑯SizeOfImage:映象的大小,PE文件加载到内存中空间是连续的,这个值指定占用虚拟空间的大小。

image-20210522225318861

image-20210522225417778

⑰SizeOfHeaders:所有文件头(包括节表)的大小,这个值是以FileAlignment对齐的。

⑱CheckSum:映象文件的校验和。

⑲Subsystem:运行该PE文件所需的子系统,可以是下面定义中的某一个:

#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.
#define IMAGE_SUBSYSTEM_EFI_APPLICATION      10  //
#define IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER  11   //
#define IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER   12  //
#define IMAGE_SUBSYSTEM_EFI_ROM              13
#define IMAGE_SUBSYSTEM_XBOX                 14
#define IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION 16

⑳DllCharacteristics:DLL的文件属性,只对DLL文件有效,可以是下面定义中某些的组合:

#define IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE               0x0040     // DLL can move.
#define IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY            0x0080     // Code Integrity Image
#define IMAGE_DLLCHARACTERISTICS_NX_COMPAT                  0x0100     // Image is NX compatible
#define IMAGE_DLLCHARACTERISTICS_NO_ISOLATION               0x0200     // Image understands isolation and doesn't want it
#define IMAGE_DLLCHARACTERISTICS_NO_SEH                     0x0400     // Image does not use SEH.  No SE handler may reside in this image
#define IMAGE_DLLCHARACTERISTICS_NO_BIND                    0x0800     // Do not bind this image.
//                                                          0x1000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_WDM_DRIVER                 0x2000     // Driver uses WDM model
//                                                          0x4000     // Reserved.
#define IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE      0x8000

㉑SizeOfStackReserve:运行时为每个线程栈保留内存的大小。
㉒SizeOfStackCommit:运行时每个线程栈初始占用内存大小。

㉓SizeOfHeapReserve:运行时为进程堆保留内存大小。

㉔SizeOfHeapCommit:运行时进程堆初始占用内存大小。

㉕LoaderFlags:保留,必须为0。

㉖NumberOfRvaAndSizes:数据目录的项数,即下面这个数组的项数。

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]

DataDirectory:数据目录,这是一个数组,数组的项定义如下:

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
//VirtualAddress:是一个RVA。
//Size:是一个大小。
//Size = sizeof(_IMAGE_DATA_DIRECTORY) * 16
//sizeof(_IMAGE_DATA_DIRECTORY) = 8 bytes

VirtualAddress和Size的作用:

DataDirectory是个数组,数组中的每一项对应一个特定的数据结构,包括导入表,导出表等等,根据不同的索引取出来的是不同的结构,头文件里定义各个项表示哪个结构,如下面的代码所示:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE        2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION       3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY        4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC       5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG           6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT       7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS             9   // TLS Directory
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   // Load Configuration Directory
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14   // COM Runtime descriptor

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]每一项是一个特定的结构,通过函数获取数组中的项可以用RtlImageDirectoryEntryToData函数。

DataDirectory中的每一项都可以用这个函数获取,函数原型如下:

PVOID NTAPI RtlImageDirectoryEntryToData(PVOID Base, BOOLEAN MappedAsImage, USHORT Directory, PULONG Size);
//Base:模块基地址。
//MappedAsImage:是否映射为映象。
//Directory:数据目录项的索引。如上段代码所示。
//Size:对应数据目录项的大小,比如Directory为0,则表示导出表的大小。
//返回值表示数据目录项的起始地址。

导入表的IMAGE_DATA_DIRECTORY:

image-20210522225701159

PE导出表

导出表是用来描述模块中的导出函数的结构,如果一个模块导出了函数,那么这个函数会被记录在导出表中,这样通过GetProcAddress函数就能动态获取到函数的地址。函数导出的方式有两种,一种是按名字导出,一种是按序号导出。这两种导出方式在导出表中的描述方式也不相同。

如:

image-20210522012402875

红框位置显示的就是模块的导出函数,有时候显示的导出函数名字中有一些符号,像 ??0CP2PDownloadUIInterface@@QAE@ABV0@@Z,这种是导出了C++的函数名,编译器将名字进行了修饰。

导出表定义:

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;

①Characteristics:现在没有用到,一般为0。

②TimeDateStamp:导出表生成的时间戳,由链接器生成。

③MajorVersion,MinorVersion:看名字是版本,实际貌似没有用,都是0。

④Name:模块的名字。

⑤Base:序号的基数,按序号导出函数的序号值从Base开始递增。

⑥NumberOfFunctions:所有导出函数的数量。

⑦NumberOfNames:按名字导出函数的数量。

AddressOfFunctions(函数的偏移地址):一个RVA,指向一个DWORD数组,数组中的每一项是一个导出函数的RVA,顺序与导出序号相同。

AddressOfNames(函数名字的偏移地址):一个RVA,依然指向一个DWORD数组,数组中的每一项仍然是一个RVA,指向一个表示函数名字。

AddressOfNameOrdinals(函数名字序号的偏移地址):一个RVA,还是指向一个WORD数组,数组中的每一项与AddressOfNames中的每一项对应,表示该名字的函数在AddressOfFunctions中的序号。

AddressOfFunctionsAddressOfNamesAddressOfNameOrdinals的解释:

image-20210523012717815

AddressOfNames指向一个数组,数组里保存着一组RVA,每个RVA指向一个字符串,这个字符串即导出的函数名,与这个函数名对应的是AddressOfNameOrdinals中的对应项。

①获取导出函数地址时,先在AddressOfNames中找到对应的名字,比如Func2,他在AddressOfNames中是第二项,

②然后从AddressOfNameOrdinals中取出第二项的值,这里是2,

③表示函数入口保存在AddressOfFunctions这个数组中下标为2的项里,即第三项,取出其中的值,加上模块基地址便是导出函数的地址。

④如果函数是以序号导出的,那么查找的时候直接用序号减去Base,得到的值就是函数在AddressOfFunctions中的下标。

大概的流程:

image-20210522012918158

代码:

DWORD* CEAT::SearchEAT( const char* szName)
{
    if (IS_VALID_PTR(m_pTable))
    {
        bool bByOrdinal = HIWORD(szName) == 0;
        DWORD* pProcs = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfFunctions));
        if (bByOrdinal)     //HIWORD(szName) == 0   define数字
        {
            DWORD dwOrdinal = (DWORD)szName;    //②
            if (dwOrdinal < m_pTable->NumberOfFunctions && dwOrdinal >= m_pTable->Base)
                //≥序号的基数且<所有导出函数的数量
            {
                return &pProcs[dwOrdinal-m_pTable->Base];        //③④
            }
        }
        else        //HIWORD(szName) != 0   名字
        {
            WORD* pOrdinals = (WORD*)((char*)RVA2VA(m_pTable->AddressOfNameOrdinals));
            DWORD* pNames = (DWORD*)((char*)RVA2VA(m_pTable->AddressOfNames));
            for (unsigned int i=0; i<m_pTable->NumberOfNames; ++i)
            {
                char* pNameVA = (char*)RVA2VA(pNames[i]);
                if (strcmp(szName, pNameVA) != 0)       //比较字符串
                {
                    continue;
                }
                return &pProcs[pOrdinals[i]];       //返回该名字的函数
            }
        }
    }
    return NULL;
}
PE导入表

IMAGE_DIRECTORY_ENTRY_IMPORT,即导入表。

20字节。

IMAGE_DATA_DERECTORY:

#define IMAGE_DIRECTORY_ENTRY_EXPORT          0  
#define IMAGE_DIRECTORY_ENTRY_IMPORT          1   // Import Directory
#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_ARCHITECTURE    7   
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR       8   
#define IMAGE_DIRECTORY_ENTRY_TLS             9   
#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG    10   
#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT   11   // Bound Import Directory in headers
#define IMAGE_DIRECTORY_ENTRY_IAT            12   // Import Address Table
#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT   13   // Delay Load Import Descriptors
#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14  

IMAGE_DIRECTORY_ENTRY_IMPORT:导入表。

在PE文件加载时,会根据这个表里的内容加载依赖的DLL,并填充所需函数的地址。

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT:绑定导入表。

在第一种导入表导入地址的修正是在PE加载时完成,如果一个PE文件导入的DLL或者函数多那么加载起来就会略显的慢一些,所以出现了绑定导入,在加载以前就修正了导入表,这样就会快一些。

IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT:延迟导入表。

一个PE文件也许提供了很多功能,也导入了很多其他DLL,但是并非每次加载都会用到它提供的所有功能,也不一定会用到它需要导入的所有DLL,因此延迟导入就出现了,只有在一个PE文件真正用到需要的DLL,这个DLL才会被加载,甚至于只有真正使用某个导入函数,这个函数地址才会被修正。

IMAGE_DIRECTORY_ENTRY_IAT:导入地址表。

前面的三个表其实是导入函数的描述,真正的函数地址是被填充在导入地址表中的

举例:

image-20210522141948436

调用了一个RegOpenKeyW的导入函数,我们看到其opcode是FF 15 00 00 19 30

FF 15表示这是一个间接调用,即call dword ptr [30190000] ;

这表示要调用的地址存放在30190000这个地址中,而30190000这个地址在导入地址表的范围内,当模块加载时,PE 加载器会根据导入表中描述的信息修正30190000这个内存中的内容。

导入表的定义:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    union {
        DWORD   Characteristics;            // 0 for terminating null import descriptor
        DWORD   OriginalFirstThunk;         // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
    } DUMMYUNIONNAME;
    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;                     // 指向IAT(引入函数真实地址单元处的RVA)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

每个导入的DLL都会成为该数组中的一项,也就是说,一个这样的结构对应一个导入的DLL。

CharacteristicsOriginalFirstThunk:一个联合体,如果是数组的最后一项Characteristics为0,否则OriginalFirstThunk保存一个RVA,指向一个IMAGE_THUNK_DATA的数组,这个数组中的每一项表示一个导入函数。

TimeDateStamp:映象绑定前,这个值是0,绑定后是导入模块的时间戳。

ForwarderChain:转发链,如果没有转发器,这个值是-1。

Name:一个RVA,指向导入模块的名字,所以一个IMAGE_IMPORT_DESCRIPTOR描述一个导入的DLL。

FirstThunk:也是一个RVA,也指向一个IMAGE_THUNK_DATA数组。

IMAGE_THUNK_DATA定义:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      // PBYTE 
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

ForwarderString是转发用的,暂时不用考虑。

Function表示函数地址,如果是按序号导入Ordinal就有用了,若是按名字导入AddressOfData便指向名字信息

区分是序号还是名字可以通过Ordinal判断,如果Ordinal的最高位是1,就是按序号导入的,这时候,低16位就是导入序号,如果最高位是0,则AddressOfData是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,用来保存名字信息。(由于Ordinal和AddressOfData实际上是同一个内存空间,所以AddressOfData其实只有低31位可以表示RVA,但是一个PE文件不可能超过2G,所以最高位永远为0

微软提供两个宏定义处理序号导入:

IMAGE_SNAP_BY_ORDINAL:判断是否按序号导入

IMAGE_ORDINAL:用来获取导入序号

OriginalFirstThunk指向的IMAGE_THUNK_DATA数组包含导入信息,在这个数组中只有Ordinal和AddressOfData是有用的,因此可以通过OriginalFirstThunk查找到函数的地址。

FirstThunk则略有不同,在PE文件加载以前或者说在导入表未处理以前,他所指向的数组与OriginalFirstThunk中的数组虽不是同一个,但是内容却是相同的,都包含了导入信息,而在加载之后,FirstThunk中的Function开始生效,他指向实际的函数地址,因为FirstThunk实际上指向IAT中的一个位置,IAT就充当了IMAGE_THUNK_DATA数组,加载完成后,这些IAT项就变成了实际的函数地址,即Function的意义。

加载前:

image-20210522145932575

加载后:

image-20210522145943411

image-20210522232133123

总结:

①导入表其实是一个IMAGE_IMPORT_DESCRIPTOR的数组,每个导入的DLL对应一个IMAGE_IMPORT_DESCRIPTOR。

②IMAGE_IMPORT_DESCRIPTOR包含两个IMAGE_THUNK_DATA数组,数组中的每一项对应一个导入函数。

③加载前OriginalFirstThunk与FirstThunk的数组都指向名字信息,加载后FirstThunk数组指向实际的函数地址。

实例:

①获取导入表的RVA

data directory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress 所指的值

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_DIRECTORY_ENTRY_IMPORT             1   // Import Directory

查看 IMAGE_DIRECTORY_ENTRY_IMPORT的值

image-20210522232428271

&data directory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress = 0x0002D51C

&data directory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualSize= 0xA0

②查找导入表所在的节

image-20210522232858393

③确定导入表在文件中的偏移量:RVA-△k(节大小)

VirtualAddress = 0x00026000

PointerToRawData = 0x00025000

△k = VirtualAddress – PointerToRawData = 0x1000H

Address = 0x0002D51C - △k = 0x0002C51C

定位:

image-20210522233405409

阴影部分是IID的内容, IID的大小为20h, 阴影部分存在链各个IID,最后一个为0000000, 说明此PE文件只有7个IID, 对应7个dll。

0xA0 = 160 bytes = 7 * 20bytes +20bytes(空白)

image-20210522233456212

第四个变量的地址RVA:0002DB14, 需要转换成对应的文件偏移

(0x0002DB14 –△k) = 0x0002cb14,定位到文件偏移为0x0002cb14的地方

查看内容,里面记录的是IMAGE_IMPORT_DESCRIPTOR的第四个成员变量多对应的dll的名字,mfc90u.dll,到此,我们已经找到了这个输入的dll

image-20210522233703766

延迟导入表

有些导入函数可能使用的频率比较低,或者在某些特定的场合才会用到,而有些函数可能要在程序运行一段时间后才会用到,这些函数可以等到他实际使用的时候再去加载对应的DLL,而没必要再程序一装载就初始化好。

延迟导入表:IMAGE_DATA_DERECTORYIMAGE_DIRECTORY_DELAY_IMPORT

延迟导入表的定义:

typedef struct ImgDelayDescr {  
    DWORD           grAttrs;        // attributes  
    RVA             rvaDLLName;     // RVA to dll name  
    RVA             rvaHmod;        // RVA of module handle  
    RVA             rvaIAT;         // RVA of the IAT  
    RVA             rvaINT;         // RVA of the INT  
    RVA             rvaBoundIAT;    // RVA of the optional bound IAT  
    RVA             rvaUnloadIAT;   // RVA of optional copy of original IAT  
    DWORD           dwTimeStamp;    // 0 if not bound,  
                                    // O.W. date/time stamp of DLL bound to (Old BIND)  
} ImgDelayDescr, * PImgDelayDescr;  
typedef const ImgDelayDescr *   PCImgDelayDescr;

数组,每一项都是一个ImgDelayDescr结构体,和导入表一样,每一项都代表一个导入的DLL.

①grAttrs:用来区分版本,1是新版本,0是旧版本,旧版本中后续的rvaxxxxxx域使用的都是指针,而新版本中都用RVA,我们只讨论新版本。

②rvaDLLName:一个RVA,指向导入DLL的名字

③rvaHmod:一个RVA,指向导入DLL的模块基地址,这个基地址在DLL真正被导入前是NULL,导入后才是实际的基地址

④rvaIAT:一个RVA,表示导入函数表,实际上指向IAT,在DLL加载前,IAT里存放的是一小段代码的地址加载后才是真正的导入函数地址

⑤rvaINT:一个RVA,指向导入函数的名字表

⑥rvaUnloadIAT:延迟导入函数卸载表。

⑦dwTimeStamp:延迟导入DLL的时间戳。

实现行为:

在延迟导入函数指向的IAT里,默认保存的是一段代码的地址,当程序第一次调用到这个延迟导入函数时,流程会走到那段代码

.text:75C7A363 __imp_load__InternetConnectA@32:        ;InternetConnectA(x,x,x,x,x,x,x,x)  
.text:75C7A363                 mov     eax, offset __imp__InternetConnectA@32 
;把导入函数IAT项的地址放到eax中 
.text:75C7A368                 jmp     __tailMerge_WININET 
;jmp↓
__tailMerge_WININET proc near             
.text:75C6BEF0                 push    ecx  
.text:75C6BEF1                 push    edx  
.text:75C6BEF2                 push    eax  
.text:75C6BEF3                 push    offset __DELAY_IMPORT_DESCRIPTOR_WININET  
.text:75C6BEF8                 call    __delayLoadHelper  
.text:75C6BEFD                 pop     edx  
.text:75C6BEFE                 pop     ecx  
.text:75C6BEFF                 jmp     eax  
.text:75C6BEFF __tailMerge_WININET endp

push了一个__DELAY_IMPORT_DESCRIPTOR_WININET,这个就是ImgDelayDescr结构,他的DLL名字是wininet.dll。之后,CALL了一个__delayLoadHelper,在这个函数里,执行了加载DLL,查找导出函数,填充导入表等一系列操作,函数结束时IAT中已经是真正的导入函数的地址,这个函数同时返回了导入函数的地址,因此之后的eax里保存的就是函数地址,最后的jmp eax就跳转到了真实的导入函数中。

__DELAY_IMPORT_DESCRIPTOR_WININET中有一项是rvaIAT,这里实际上就是指向了IAT,而且是该模块第一个导入函数的IAT的偏移,现在我们有两个偏移,即将导入的函数IAT项的偏移(记作RVA1)和要导入模块第一个函数IAT项的偏移(记作RVA0),(RVA1-RVA0)/4 = 导入函数IAT项在rvaIAT中的下标,rvaINT中的名字顺序与rvaIAT中的顺序是相同的,所以下标也相同,这样就能获取到导入函数的名字了。有了模块名和函数名,用GetProcAddress就可以获取到导入函数的地址了。

注意:

延迟导入的加载只发生在函数第一次被调用的时候,之后IAT就填充为正确函数地址,不会再走__delayLoadHelper了。

延迟导入一次只会导入一个函数,而不是一次导入整个模块的所有函数。

总结:

image-20210523012756166

重定位

Windows使用重定位机制保证代码无论模块加载到哪个基址都能正确被调用:

1.编译的时候由编译器识别出哪些项使用了模块内的直接VA,比如push一个全局变量、函数地址,这些指令的操作数在模块加载的时候就需要被重定位。

2.链接器生成PE文件的时候将编译器识别的重定位的项纪录在一张表里,这张表就是重定位表,保存在DataDirectory中,序号是 IMAGE_DIRECTORY_ENTRY_BASERELOC。

3.PE文件加载时,PE 加载器分析重定位表,将其中每一项按照现在的模块基址进行重定位。
存储方式:

typedef struct _IMAGE_BASE_RELOCATION {
    DWORD   VirtualAddress;
    DWORD   SizeOfBlock;
//  WORD    TypeOffset[1];
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;

①VirtualAddress:页起始地址RVA。

②SizeOfBlock:表示该分组保存了几项重定位项。

③TypeOffset:这个域有两个含义,大家都知道,页内偏移用12位就可以表示,剩下的高4位用来表示重定位的类型。而事实上,Windows只用了一种类型IMAGE_REL_BASED_HIGHLOW,数值是 3。

需要被重定位:

1.代码中使用全局变量的指令,因为全局变量一定是模块内的地址,而且使用全局变量的语句在编译后会产生一条引用全局变量基地址的指令。

2.将模块函数指针赋值给变量或作为参数传递,因为赋值或传递参数是会产生mov和push指令,这些指令需要直接地址。

3.C++中的构造函数和析构函数赋值虚函数表指针,虚函数表中的每一项本身就是重定位项。

节区头

节区头数量:NT头(PE文件头)_IMAGE_FILE_HEADER中NumberOfSections的值。

节区头定位:紧跟在IMAGE_NT_HEADERS(NT头)后面。

大小:40字节。

定义:

#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;    //PE文件中对其之后的大小
    DWORD   PointerToRawData;//为PE块区在PE文件中偏移
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;    //块区的属性:可读、可写..
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER          40

①Name[IMAGE_SIZEOF_SHORT_NAME]:8字节大小的NAME, 如果节区名称小于8个字节,则多余的用0填充,否则全部填充节名,末尾不保证有1个0,同样会被名字填充。

②PointerToRawData:节区在PE文件中的偏移。

③Characteristics: 为节区的属性,如可读、可写、可执行等。

#define IMAGE_SCN_CNT_CODE       0x00000020  // Section contains code.
#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.

image-20210522230759541

部分总结

img

一些问题

VirtualSize和SizeOfRawData大小的问题

image-20210620233357896

image-20210620233424934

image-20210620233445234

参考:https://www.zhihu.com/question/64435651

ELF(Executable and Linkable Format)

分类

共享库共享文件(库文件):.so

可重定位的目标文件(目标文件):.o

可执行文件

gcc生成的二进制格式文件:.out

Linux中查看ELF文件的详细信息:

readelf -a xxxx

elf文件转化为BIN文件:

objcopy -O binary xxxx xxxxx

文件格式

image-20210523005705162

常见的ELF格式如上图所示,左边为链接视图,右边为执行视图。
链接视图:
静态链接器(即编译后参与生成最终ELF过程的链接器,如ld )会以链接视图解析ELF。编译时生成的 .o(目标文件)以及链接后的 .so (共享库)均可通过链接视图解析,链接视图可以没有段表(如目标文件不会有段表)。
执行视图:
动态链接器(即加载器,如x86架构 linux下的 /lib/ld-linux.so.2或者安卓系统下的 /system/linker均为动态链接器)会以执行视图解析ELF并动态链接,执行视图可以没有节表。

  • ELF header保存在文件最顶端它记录了整个文件的基本信息;
  • Sections包含了链接视角中每个节里的信息:指令、数据、符号表、重定位信息等等;
  • section header table包含描述文件节的信息,每个节对应表中的一项,包含节名、节的大小信息等等;
  • segment包含了执行视角中的每个段(就是程序数据和指令)文本段、数据段等等;
  • program header table描述了一个段的信息或者系统准备程序运行环境时所需要的其他信息;

ELF header

定义:

typedef struct elf32_hdr{
    unsigned char e_ident[EI_NIDENT];     /* 魔数和相关信息 */
    Elf32_Half    e_type;                 /* 目标文件类型 */
    Elf32_Half    e_machine;              /* 硬件体系 */
    Elf32_Word    e_version;              /* 目标文件版本 */
    Elf32_Addr    e_entry;                /* 程序进入点 */
    Elf32_Off     e_phoff;                /* 程序头部偏移量 */
    Elf32_Off     e_shoff;                /* 节头部偏移量 */
    Elf32_Word    e_flags;                /* 处理器特定标志 */
    Elf32_Half    e_ehsize;               /* ELF头部长度 */
    Elf32_Half    e_phentsize;            /* 程序头部中一个条目的长度 */
    Elf32_Half    e_phnum;                /* 程序头部条目个数  */
    Elf32_Half    e_shentsize;            /* 节头部中一个条目的长度 */
    Elf32_Half    e_shnum;                /* 节头部条目个数 */
    Elf32_Half    e_shstrndx;             /* 节头部字符表索引 */
} Elf32_Ehdr;
  • e_ident[0~3]包含了ELF文件的魔数,依次是0x7f、’E’、’L’、’F’
  • e_ident[4]表示硬件系统的位数,1代表32位,2代表64位
  • e_ident[5]表示数据编码方式,1代表小端格式,2代表大端格式
  • e_ident[6]指定ELF头部的版本,当前必须为1
  • e_ident[7~15]是填充符,通常是0
  • e_ident[16]e_ident[]数组的大小

image-20210523011023175

section header table

定义:

typedef struct elf32_shdr {
    Elf32_Wordsh_name;   
    Elf32_Wordsh_type;
    Elf32_Wordsh_flags;
    Elf32_Addrsh_addr;
    Elf32_Off sh_offset;
    Elf32_Wordsh_size;
    Elf32_Wordsh_link;
    Elf32_Wordsh_info;
    Elf32_Wordsh_addralign;
    Elf32_Wordsh_entsize;
} Elf32_Shdr;

image-20210523011408684

  1. .bss该节保存着未初始化的数据,这些数据存在于程序内存映象中。通过定义,当程序开始运行,系统初始化那些数据为0。该节不占文件空间,正如它的节类型SHT_NOBITS指示的一样。
  2. .comment该节保存着版本控制信息。
  3. .data.data1这些节保存着初始化了的数据,那些数据存在于程序内存映象中。
  4. .debug该节保存着为符号调试的信息。这些内容不明确。
  5. .dynamic该节保存着动态连接的信息。SHF_ALLOC位指定该节的属性。设置SHF_WRITE位时表示是处理器特定的。
  6. .dynstr该节保存着动态连接时需要的字符串,一般情况下表示和一个符号表项相对应的名字
  7. .dynsym该节保存着动态链接符号表,如Symbol Table的描述。
  8. .fini该节保存着可执行指令,它包含了进程的终止代码。当一个程序正常退出时,系统安排执行这个节的中的代码。
  9. .got该节保存着全局的偏移量表。
  10. .hash该节保存着符号哈希表。
  11. .init该节保存着可执行指令,它构成了进程的初始化代码。当一个程序开始运行时,在main函数被调用之前(c语言中称为main),系统安排执行这个节的中的代码。
  12. .interp该节保存了程序解释器(program interpreter)的路径。假如在这个节中有一个可装载的节,那么该节属性的SHF_ALLOC位将被设置;否则,该位不会被设置。
  13. .line该节包含源文件中的行数信息用于符号调试,它描述源程序与机器代码之间的对应关系。该节内容不明确的。
  14. .note该节保存Part 2讨论的Note Section节的格式信息。
  15. .plt该节保存过程链接表(Procedure Linkage Table)。
  16. .rel<name>.rela<name>这些节保存着重定位的信息。如果文件包含了一个可装载的节需要重定位,那么该节属性中的SHF_ALLOC位会被设置;否则该位被关闭。通常,由需要重定的那个节来提供。比如一个需要重定位的节.text,那么该节名字为.rel.text或者.rela.text
  17. .rodata.rodata1这些节保存着只读数据,放在进程映象中的只读节。
  18. .shstrtab该节保存着节名称。
  19. .strtab该节保存着字符串,一般表示和一个符号表项相对应的名字。假如文件有一个可装载的节,并且该节包括了符号字符表,那么该节属性的SHF_ALLOC位将被设置;否则不设置。
  20. .symtab该节保存着一个符号表。假如文件有一个可装载的节,并且该节包含了符号表,那么该节属性的SHF_ALLOC位将被设置;否则不设置。
  21. .text该节保存着程序的text或者说是程序的可执行指令。

Symbol Table

符号表保存着程序定义和引用的符号(全局变量和函数)信息。一个符号表的索引是数组的下标。第0项既指定这个表的起始项也作为未定义符号的索引。

定义:

typedef struct elf32_sym{
    Elf32_Wordst_name;
    Elf32_Addrst_value;
    Elf32_Wordst_size;
    unsigned char st_info;
    unsigned char st_other;
    Elf32_Halfst_shndx;
} Elf32_Sym;

image-20210523011702573

Program Header table

定义:

typedef struct elf32_phdr{
  Elf32_Word  p_type;        /* 段类型 */
  Elf32_Off   p_offset;      /* 段位置相对于文件开始处的偏移量 */
  Elf32_Addr  p_vaddr;       /* 段在内存中的地址 */
  Elf32_Addr  p_paddr;       /* 段的物理地址 */
  Elf32_Word  p_filesz;      /* 段在文件中的长度 */
  Elf32_Word  p_memsz;       /* 段在内存中的长度 */
  Elf32_Word  p_flags;       /* 段的标记 */
  Elf32_Word  p_align;       /* 段在内存中对齐标记 */
} Elf32_Phdr;

image-20210523011802568

参考资料

PE文件1

PE文件2

PE文件3

PE文件4

ELF文件1