手工实现简易PE文件

0x01、写在之前

        PE文件作为可执行文件在Windows上的重要性不言而喻。为了更好的理解PE文件的结构,花了一定的时间手工实现了一个简易的PE文件。完成后做了一些总结,记录在此,以免后忘!如有错误之处,还请指正!

0x02、结构解析

        要想手工打造一个PE文件,那么我们就必须先要知晓PE文件各个主要结构体的大小以及结构体的各个字段,这样实现起来才会更得心应手。我们可以使用两种方式来达到手工PE的目的。一种是根据PE结构纯手动划分各个字段并处理,另一种是借助工具辅助我们实现。这两种方式接下来会一一介绍!

1、通过结构体字段

        编辑工具:Notepad++;

        字段介绍:IMAGE_DOS_HEADER、IMAGE_FILE_HEADER、IMAGE_OPTIONAL_HEADER;

1.1、IMAGE_DOS_HEADER

        这是为了兼容16位系统而保留的一个文件头,一个DOS头包含了:DOS MZ头和DOS Stub(指令字节码)。DOS Stub的指令直接码是用在DOS平台的,而此次构造的PE文件为了精简文件大小就并无考虑构造它(事实是它在32位系统下并无作用,没有它并不影响程序执行)。但是没有DOS MZ头是必须的,没有它程序将无法正常运行。接下来看IMAGE_DOS_HEADER结构体:

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // **Magic number 固定MZ(0x5A4D)
    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;                    // **PE头的文件偏移
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

在此结构体中,需要关注的就是代码中注释出来的两个字段:e_magic、e_lfanew。在构造时我们就按照结构体中每个字段所需大小进行构造,如下:

-----IMAGE_DOS_HEADER--------
4D 5A 		// Dos Magic
00 00 
00 00 
00 00 
00 00 
00 00 
00 00 
00 00
00 00 
00 00 
00 00 
00 00 
00 00 
00 00 
00 00 00 00 00 00 00 00 
00 00 
00 00 
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
40 00 00 00	//PE FOA
-----IMAGE_DOS_HEADER--------

1.2、IMAGE_NT_HEADER

        NT头中包含了标识PE的Signature字段以及IMAGE_FILE_HEADER(文件头)、IMAGE_OPTIONAL_HEADER(可选头)

1.2.1、IMAGE_FILE_HEADER

结构体如下:

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;                 // **指令运行平台
    WORD    NumberOfSections;        // **区段(节)数量
    DWORD   TimeDateStamp;           // 时间戳 
    DWORD   PointerToSymbolTable;    // COFF符号表文件偏移,此值对映像文件为 0(微软已不建议使用)
    DWORD   NumberOfSymbols;         // 符号表中元素数目,与上一个字段关联,为 0 
    WORD    SizeOfOptionalHeader;    // 可选头(扩展头)大小
    WORD    Characteristics;         // **文件属性标志
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

characteristics是一个很重要的字段。它的不同数据位定义了不同的文件属性,不同的定义将影响系统对文件的装入方式。对于普通PE文件这个值位:0x10F。

构造如下:

-----IMAGE_FILE_HEADER-------
4C 01 		// 指令运行平台 .386
03 00 		// 区段数量
00 00 00 00     // 时间戳
00 00 00 00	// 一般为0
00 00 00 00     // 一般为0
E0 00 		// 扩展头大小
0F 01 		// 文件属性标志 0000 0001 0000 1111
-----IMAGE_FILE_HEADER-------
1.2.2、IMAGE_OPTIONAL_HEADER

结构体如下:

typedef struct _IMAGE_OPTIONAL_HEADER {
    // Standard fields
    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    // NT additional fields.

    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;

构造如下:

0B 01        // magic PE32
00 00        // 链接器版本号,对程序运行无影响
00 02 00 00  // 区段文件对齐后的总大小
00 04 00 00  // 所有包含已经初始化数据的区段的总大小
00 00 00 00  // 所有包含未初始化数据的区段的总大小
00 10 00 00  // OEP
00 10 00 00  // 代码区段的起始RVA BaseOfCode 一般紧跟PE头后面,为.text段
00 20 00 00  // 数据区段的起始RVA BaseOfData 一般在文件末尾,为.data段
00 00 40 00  // PE文件默认加载基址
00 10 00 00  // 区段的内存对齐粒度
00 02 00 00  // 区段的文件对齐粒度
06 00 
00 00  	     // 操作系统版本号
00 00 
00 00  	     // 本PE文件映像版本号
06 00 
00 00  	     // 运行所需要的子系统版本号
00 00 00 00  // 子系统版本的值
00 00 00 00  // 加载到内存后的映像大小,可以比实际值大,但不能小
00 04 00 00  // SizeOfHeaders 所有头+区段表按照文件对齐粒度对齐后的大小
00 00 00 00  // 校验和,PE文件一般为0
03 00        // 指定使用界面的子系统 0000 0011 ==> 3
00 81        // PE文件属性,是否开启随机基址等
00 00 01 00  // 初始化时保留栈的大小,默认值为0x100000
00 10 00 00  // 初始化时实际提交的栈的大小,默认值为0x1000
00 00 01 00  // 初始化时保留堆大小,默认值0x100000
00 10 00 00  // 初始化时实际提交的堆大小,默认值为0x1000
00 00 00 00  // 加载标志 0
10 00 00 00  // 定义数据目录结构的数量,由SizeOfOptionalHeaders决定
00 00 00 00 00 00 00 00 //0 导出数据.edata,无
00 20 00 00 00 02 00 00 //1 导入数据.idata,需构造
00 00 00 00 00 00 00 00 //2 
00 00 00 00 00 00 00 00 //3 资源数据
00 00 00 00 00 00 00 00 //4 
00 00 00 00 00 00 00 00 //5 重定位数据,NULL
00 00 00 00 00 00 00 00 //6 
00 00 00 00 00 00 00 00 //7 必须为0
00 00 00 00 00 00 00 00 //8
00 00 00 00 00 00 00 00 //9
00 00 00 00 00 00 00 00 //10 
00 00 00 00 00 00 00 00 //11 
28 20 00 00 10 00 00 00 //12 IAT,是导入表的一部分
00 00 00 00 00 00 00 00 //13 延迟载入
00 00 00 00 00 00 00 00 //14 
00 00 00 00 00 00 00 00 //15 

可选头的构造与后面的数据相关联,并不是像DOS头和文件头一样那么简单的构造。可在与后面数据相关联的字段填上一些无关数据并做好注释以便后面修改。

1.3、IMAGE_SECTION_HEADER

        节(区段)是PE中非常重要的结构体之一,各个节分别表示着代码、数据、资源等数据的位置。

结构体如下:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

1.4、IMAGE_IMPORT_DESCRIPTOR(属于导入数据区段)

        导入表描述信息结构体,结构体中保存着双桥(IAT(桥2)、INT(桥1))的指向以及导入的DLL的名称。

        双桥中保存着的RVA都指向了一个结构体数组IMAGE_THUNK_DATA,而且它们指向的结构体数据相同。该结构体实际上是一个双字(DWORD),在不同的时刻拥有不同的解释。该字段有两种解释:

  1. 双字最高位为 0:表示导入符号是一个数值,该数值是一个RVA。
  2. 双字最高位为 1:表示导入符号是一个名称。

结构体如下:

typedef struct _IMAGE_THUNK_DATA32 {
    union {
        DWORD ForwarderString;      
        DWORD Function;             // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData;        // PIMAGE_IMPORT_BY_NAME是一个RVA,指向另一个结构体IMAGE_IMPORT_BY_NAME
    } u1;
} IMAGE_THUNK_DATA32;

1.5、IMAGE_IMPORT_BY_NAME(属于导入数据区段)

结构体如下:
typedef struct _IMAGE_IMPORT_BY_NAME {
    WORD    Hint;                   // 函数序号
    CHAR   Name[1];                 // 大小不确定,以 0 结尾
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

在内存中,通过桥1可以找到调用的函数名称或函数的索引编号,桥2可以帮助找到该函数指令代码在内存空间的地址。关系图如下:


当PE被加载进虚拟内存空间以后,IAT的内容会被操作系统填充为函数的VA。需要注意的一点是,IMAGE_IMPORT_DESCRIPTOR是以全0结构体结尾的,与之相关的导入名称表(INT)和导入地址表(IAT)也是以0结尾。如果不同的IMAGE_IMPORT_DESCRIPTOR中指向的INT、IAT不小心没有以0结尾,那么将造成某个IMAGE_IMPORT_BY_NAME明明不属于这个IMAGE_IMPORT_DESCRIPTOR但是010Editor却将它识别成了与它相关联的数据!

        关于函数的调用过程:汇编语句在执行到call的时候call的是IAT中已经被系统填充好的函数地址(有随机基址的情况下,这个地址是被系统修复重定位后的数据)。而这个填充的过程是在PE文件载入内存时且程序开始运行前完成的,完成这个步骤需要依赖于导入表的数据,即双桥结构的数据。

2、借助编辑工具

        使用010Editor工具。

        经过上一节的介绍,此处就不再赘述文件结构了。以下是一张我总结并制作好的图片。通过这张图会对PE文件有个更为清晰直观的认识!


0x03、结束语

        通过此次手工实现的一个可执行的PE文件,对PE的结构有了更为清晰的认识。逆向之路 路漫漫其修远......

        《PE权威指南》是学习PE的一本好书,如您看此文章因我概述不清想更为细致的了解可看此书!

        本文以及本文中的图片为个人原创内容,他人非本人授权转载盗用必究!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值