Skip to content

PE文件加壳(一)--PE文件结构分析

Published: at 11:43

本来想一次性把加壳给写完,发现还是得分成2部分。

对PE文件的格式有一个简单的了解后,在进行加壳时会更容易理解,上手也会更快。

由于网上有太多的佬(比如:看雪1、伟牛牛2、吾爱3)对PE文件结构进行了详细说明,这里我就不多赘述,只简单说一下(作个记录),后面我会在参考链接中给出相关帖子。

1. PE文件结构图

先给出整个PE文件结构图: PE结构图

下面再对上图的部分结构进行一个大致的说明。

2. DOS头

2.1 IMAGE_DOS_HEADER

先来看DOS-header字段,下面给出该字段的结构信息:

typedef struct _IMAE_DOS_HEADER {       
    WORD e_magic;        **重要成员 相对该结构的偏移0x00**
    WORD e_cblp;
    WORD e_cp;
    WORD e_crlc;
    WORD e_cparhdr;
    WORD e_minalloc;
    WORD e_maxalloc;
    WORD e_ss;
    WORD e_sp;
    WORD e_csum;
    WORD e_ip;
    WORD e_cs;
    WORD e_lfarlc;
    WORD e_ovno;
    WORD e_res[4];
    WORD e_oemid;
    WORD e_oeminfo;
    WORD e_res2[10];
    LONG e_lfanew;        **重要成员 相对该结构的偏移0x3C**
} IMAGE_DOS-HEADER, *PIMAGE_DOS_HEADER;

其中有两个需要注意的字段,

2.2 IMAGE_DOS_STUB

IMAGE_DOS_STUB不用太过注意,这段值基本上是固定的,就是一句话:

This program cannot be run in DOS mode

3. NT头

NT头是整个PE文件中的核心,它的结构如下所示:

typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;         **重要成员 PE签名 相对该结构的偏移0x00**
  IMAGE_FILE_HEADER       FileHeader;        **重要成员 结构体 相对该结构的偏移0x04**
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;    **重要成员 结构体 相对该结构的偏移0x18**
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

这三个字段都值得注意!

3.1 Signature

这个字段也被称作PE签名,这个成员和DOS头中的MZ标记一样都是PE文件的标准特征。

3.2 FileHeader

FileHeader是一个IMAGE_FILE_HEADER类型的结构体,具体大小要看内部数据类型,它的具体结构如下:

typedef struct _IMAGE_FILE_HEADER {
  WORD  Machine;                    **        机器号     相对该结构的偏移0x00**
  WORD  NumberOfSections;           **重要成员 节区数量   相对该结构的偏移0x02**
  DWORD TimeDateStamp;              **        时间戳     相对该结构的偏移0x04**
  DWORD PointerToSymbolTable;       **        符号表偏移  相对该结构的偏移0x08**
  DWORD NumberOfSymbols;            **        符号表数量  相对该结构的偏移0x0C**
  WORD  SizeOfOptionalHeader;       **重要成员 可选头大小  相对该结构的偏移0x10**
  WORD  Characteristics;            **重要成员 PE文件属性  相对该结构的偏移0x12**
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

在这个结构下有三个需要注意的字段:

3.3 OptionalHeader

OptionalHeader是一个IMAGE_OPTIONAL_HEADER32类型的结构体,它的结构如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;                        **魔术字                     偏移0x00
  BYTE                 MajorLinkerVersion;           **链接器主版本                偏移0x02
  BYTE                 MinorLinkerVersion;           **链接器副版本                偏移0x03
  DWORD                SizeOfCode;                   **所有含代码的节的总大小       偏移0x04
  DWORD                SizeOfInitializedData;        **所有含初始数据的节的总大小    偏移0x08
  DWORD                SizeOfUninitializedData;      **所有含未初始数据的节的总大小  偏移0x0C    
  DWORD                AddressOfEntryPoint;          **程序执行入口地址             偏移0x10   重要
  DWORD                BaseOfCode;                   **代码节的起始地址             偏移0x14
  DWORD                BaseOfData;                   **数据节的起始地址             偏移0x18
  DWORD                ImageBase;                    **程序首选装载地址             偏移0x1C   重要
  DWORD                SectionAlignment;             **内存中节区对齐大小           偏移0x20   重要
  DWORD                FileAlignment;                **文件中节区对齐大小           偏移0x24   重要
  WORD                 MajorOperatingSystemVersion;  **操作系统的主版本号           偏移0x28
  WORD                 MinorOperatingSystemVersion;  **操作系统的副版本号           偏移0x2A
  WORD                 MajorImageVersion;            **镜像的主版本号               偏移0x2C
  WORD                 MinorImageVersion;            **镜像的副版本号               偏移0x2E
  WORD                 MajorSubsystemVersion;        **子系统的主版本号             偏移0x30
  WORD                 MinorSubsystemVersion;        **子系统的副版本号             偏移0x32
  DWORD                Win32VersionValue;            **保留,必须为0               偏移0x34
  DWORD                SizeOfImage;                  **镜像大小                    偏移0x38   重要
  DWORD                SizeOfHeaders;                **PE头大小                    偏移0x3C   重要
  DWORD                CheckSum;                     **校验和                      偏移0x40
  WORD                 Subsystem;                    **子系统类型                   偏移0x44
  WORD                 DllCharacteristics;           **DLL文件特征                  偏移0x46
  DWORD                SizeOfStackReserve;           **栈的保留大小                 偏移0x48
  DWORD                SizeOfStackCommit;            **栈的提交大小                 偏移0x4C
  DWORD                SizeOfHeapReserve;            **堆的保留大小                 偏移0x50
  DWORD                SizeOfHeapCommit;             **堆的提交大小                 偏移0x54
  DWORD                LoaderFlags;                  **保留,必须为0                偏移0x58
  DWORD                NumberOfRvaAndSizes;          **数据目录的项数               偏移0x5C
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

需要注意的内容主要是下面几个:

SizeOfHeaders = (e_lfanew/*DOS头部*/ + 4/*PE签名*/ +
               sizeof(IMAGE_FILE_HEADER) +
               SizeOfOptionalHeader + /*NT头*/
               sizeof(IMAGE_SECTION_HEADER) * NumberOfSections) / /*节表*/
               FileAlignment  *
               FileAlignment +
               FileAlignment;    /*向上舍入 一般该结果不可能是FileAlignment的整数倍,所以直接加上FileAlignment还是没问题的 */

由于本文章内容主要是实现加壳操作,所以只探究导入表导入地址表,这里以导入表为例子。

导入表的每一项也是一个结构体,用于描述一个DLL文件及其相关的导入信息,它的结构如下:

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    DWORD OriginalFirstThunk;   // 指向IMAGE_THUNK_DATA结构的数组,保存导入函数的原始信息
    DWORD TimeDateStamp;        // 时间戳(调试用)
    DWORD ForwarderChain;       // 转发链(调试用)
    DWORD Name;                 // 指向 DLL 名称的 RVA(相对于文件基址的偏移)
    DWORD FirstThunk;           // 同样指向IMAGE_THUNK_DATA结构的数组
} IMAGE_IMPORT_DESCRIPTOR;

这里主要关注三个字段:

上面的OriginalFirstThunk与FirstThunk在PE文件还没有执行时都指向相同的结构。区别在于当PE文件执行后,OriginalFirstThunk所指向的数组元素不会改变,而FirstThunk所指向的数组,其中的元素会被重写为所导入函数的真实地址(加载到内存后的地址),从而变成导入函数地址表IAT)。

IMAGE_THUNK_DATA结构如下:

typedef struct _IMAGE_THUNK_DATA {
    union {
        DWORD Function;            // 实际函数地址
        DWORD Ordinal;             // 按序号导入时的序号
        DWORD AddressOfData;       // 按名称导入时的指针
    } u1;
} IMAGE_THUNK_DATA;

该结构在不同情况下的成员不同,但这里着重关注AddressofData字段。

IMAGE_IMPORT_BY_NAME结构如下:

typedef struct _IMAGE_IMPORT_BY_NAME {
	Hint WORD //忽略设置为0
	Name BYTE //导入函数名称
};IMAGE_IMPORT_BY_NAME

4. SECTION头

一个PE文件中回包含多个section头,具体的数量需要参考FileHeader中的NumberOfSections字段。

secition头的结构如下:

typedef struct _IMAGE_SECTION_HEADER {
  BYTE  Name[IMAGE_SIZEOF_SHORT_NAME];         **节区名                 偏移0x00
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;                         **节区的虚拟大小          偏移0x08      重要
  } Misc;                                     
  DWORD VirtualAddress;                        **节区的虚拟地址          偏移0x0C      重要  
  DWORD SizeOfRawData;                         **节区在硬盘上的大小       偏移0x10      重要
  DWORD PointerToRawData;                      **节区在硬盘上的地址       偏移0x14      重要
  DWORD PointerToRelocations;                  **指向重定位项开头的地址   偏移0x18
  DWORD PointerToLinenumbers;                  **指向行号项开头的地址     偏移0x1C
  WORD  NumberOfRelocations;                   **节区的重定位项数         偏移0x20
  WORD  NumberOfLinenumbers;                   **节区的行号数            偏移0x22
  DWORD Characteristics;                       **节区的属性              偏移0x24       重要
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

其中需要注意的字段有:

参考链接

Footnotes

  1. https://bbs.kanxue.com/thread-252795.htm#msg_header_h2_2

  2. https://jev0n.com/2020/02/02/start.html

  3. https://www.52pojie.cn/thread-1520945-1-1.html


Previous Post
PE文件加壳(二)--32位PE文件加壳
Next Post
2024网鼎杯青龙组REVERSE