2.1 PE结构:文件映射进内存

pe,结构,文件,映射,内存 · 浏览次数 : 53

小编点评

#include <iostream>#include <Windows.h>#include <ImageHlp.h>#pragma comment(lib,\"Imagehlp.lib\")// --------------------------------------------------// 定义全局变量,来存储 DOS头部/NT头部/Section头部// --------------------------------------------------PIMAGE_DOS_HEADER DosHeader = nullptr;PIMAGE_NT_HEADERS NtHeader = nullptr;PIMAGE_FILE_HEADER FileHead = nullptr;PIMAGE_SECTION_HEADER pSection = nullptr;// --------------------------------------------------// 读取并设置文件基址以及文件大小// --------------------------------------------------CHAR GlobalFilePath[2048] = { 0 }; // 保存文件路径DWORD GlobalFileSize = 0; // 定义文件大小DWORD GlobalFileBase = 0; // 保存文件的基地址DWORD IsOpen = 0; // 设置文件是否已经打开// --------------------------------------------------// 打开文件操作// --------------------------------------------------HANDLE OpenPeFile(LPCSTR FileName){ HANDLE hFile, hMapFile, lpMapAddress = NULL; hFile = CreateFileA(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf(\"[-] 打开文件失败 \\"); exit(0); } GlobalFileSize = GetFileSize(hFile, NULL); if (GlobalFileSize != 0) { printf(\"[+] 已读入文件 \\"); } hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, GlobalFileSize, NULL); if (hMapFile == NULL) { printf(\"[-] 创建映射对象失败\\"); exit(0); } lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, GlobalFileSize); if (lpMapAddress != NULL) { // 设置读入文件基地址 GlobalFileBase = (DWORD)lpMapAddress; // 获取DOS头并判断是不是一个有效的DOS文件 DosHeader = (PIMAGE_DOS_HEADER)GlobalFileBase; if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE) { printf(\"[-] 文件不属于DOS结构 \\"); exit(0); } // 获取 NT 头并判断是不是一个有效的PE文件 NtHeader = (PIMAGE_NT_HEADERS)(GlobalFileBase + DosHeader->e_lfanew); if (NtHeader->Signature != IMAGE_NT_SIGNATURE) { printf(\"[-] 文件不属于PE结构 \\"); exit(0); } // 判断是不是32位程序 if (NtHeader->OptionalHeader.Magic != 0x010B) { printf(\"[-] 无法调试非32位PE文件\\"); exit(0); } // 获取到文件头指针 FileHead = &NtHeader->FileHeader; // 获取到节表头 pSection = IMAGE_FIRST_SECTION(NtHeader); } return lpMapAddress;}int main(int argc, char * argv[]){ HANDLE BaseAddr = OpenPeFile(\"c://pe/x86.exe\"); printf(\"[+] 入口地址 = %x \\", BaseAddr); system(\"pause\"); return 0;}

正文

PE结构是Windows系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制约了软件的发展。

为了应对这种局面,微软的工程师们就发明了新的文件格式(EXE文件),该文件格式在代码段前面增加了文件头结构,文件头中包括各种说明数据,如程序的入口地址,堆栈的位置,重定位表等,显然可执行文件的格式是操作系统工作方式的真实写照,不同的系统之间文件格式千差万别,从而导致不同系统中的可执行文件无法跨平台运行。

PE结构包含了各类结构体,DOS头,PE标识,文件头,可选头,目录结构,节表,导入表,导出表,重定位表,资源表等等,要想掌握PE结构首相要对这些表有一个整体上的认识,Windows NT 系统中可执行文件使用微软设计的新的文件格式,也就是至今还在使用的PE格式,PE文件的基本结构如下图所示:

在PE文件中,代码,已初始化的数据,资源和重定位信息等数据被按照属性分类放到不同的Section(节区/或简称为节)中,而每个节区的属性和位置等信息用一个IMAGE_SECTION_HEADER结构来描述,所有的IMAGE_SECTION_HEADER结构组成了一个节表(Section Table),节表数据在PE文件中被放在所有节数据的前面。

在PE文件中将同样属性的数据分类放在一起是为了统一描述这些数据装入内存后的页面属性,由于数据是按照属性在节中放置的,不同用途但是属性相同的数据可能被放在同一个节中,PE文件头被放置在节和节表的前面,上面介绍的是真正的PE文件,为了兼容以前的DOS系统,所以保留了DOS的文件格式,接下来笔者将带大家从最基本的读入文件开始依次实现对PE文件的解析,并使用C语言实现一个PeView结构解析器。

在解析PE文件之前,我们首先要做的则是将PE文件从磁盘中读入到内存,有两种方式可以实现,一种是通过ReadFile函数将完整的数据读入内存,该方法会消耗更多的内存资源这里并不推荐使用,第二种方式则是采用映射的模式,所谓的映射则是将一个磁盘中的部分数据读入内存,当需要使用该片区域时由操作系统动态的装载一部分,该方式也是笔者推荐的一种实现模式;

一般来说映射文件的流程是,使用CreateFile()打开一个磁盘文件,接着使用CreateFileMapping()函数创建文件的内存映像,最后使用MapViewOfFile()读取映射中的内存并返回一个句柄,后面的程序就可以通过该句柄操作打开后的文件。

CreateFile

用来创建或打开文件的API函数,它可以接受一个文件名作为输入参数,并返回一个文件句柄。文件句柄是用来标识打开的文件的唯一标识符,后续对该文件的操作需要使用这个句柄。下面是CreateFile函数的原型:

HANDLE CreateFile(
    LPCTSTR lpFileName,          // 文件名或路径
    DWORD dwDesiredAccess,       // 访问权限
    DWORD dwShareMode,           // 共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全属性
    DWORD dwCreationDisposition, // 创建选项
    DWORD dwFlagsAndAttributes,  // 文件属性
    HANDLE hTemplateFile         // 模板文件句柄
);

其中,各个参数的含义如下:

  • lpFileName:指向null结尾字符串的指针,该字符串是文件名或文件的路径。
  • dwDesiredAccess:一个32位的AccessMask值,用来表示对文件的访问权限。
  • dwShareMode: 一个32位的ShareMode值,它表示其他进程可以如何访问文件。
  • lpSecurityAttributes:指向SECURITY_ATTRIBUTES结构体的指针,表示安全属性。
  • dwCreationDisposition:一个32位的值,它表示对文件的创建选项如何操作。
  • dwFlagsAndAttributes:一个32位的值,用来指定文件的属性和标志。
  • hTemplateFile:可选的模板文件句柄,用来将文件属性/属性设置为其它文件的属性/属性。

函数返回值为一个文件对象的句柄,如果函数执行失败,则返回INVALID_HANDLE_VALUE(即-1)。

CreateFileMapping

用来创建文件的内存映像的API函数。它可以将一个文件映射到内存中,这样我们就可以像访问内存一样访问文件。这个函数需要传入一个文件句柄以及一个映像的大小。它返回一个句柄,表示创建的内存映像。下面是CreateFileMapping函数的原型:

HANDLE CreateFileMapping(
    HANDLE hFile,                     // 文件句柄
    LPSECURITY_ATTRIBUTES lpAttributes, // 安全属性
    DWORD flProtect,                    // 内存保护属性
    DWORD dwMaximumSizeHigh,           // 文件映像的高32位字节大小
    DWORD dwMaximumSizeLow,            // 文件映像的低32位字节大小
    LPCTSTR lpName                     // 映像名
);

其中,各个参数的含义如下:

  • hFile:要映射到内存中的文件的句柄
  • lpAttributes:指向SECURITY_ATTRIBUTES结构体的指针,它描述内存映射对象的安全性,如果为NULL,则内存映射对象不可继承。
  • flProtect:一组标志位,它们指定内存映射区域的内存保护属性;
  • dwMaximumSizeHigh:文件映像的高32位字节大小
  • dwMaximumSizeLow:文件映像的低32位字节大小
  • lpName:映像名,可以为NULL;而且,如果该参数不为空,映像对象就成为本地系统对象,可以通过名字查找映像。

函数返回值为一个文件映射对象的句柄,如果函数执行失败,返回值为NULL。

MapViewOfFile

用来读取映射中的内存的API函数。它需要传入一个映像的句柄以及一个偏移量,用来指定从哪个位置开始读取内存。该函数返回一个指向映射内存的指针,我们可以使用它来读取或修改映射内存中的数据。下面是MapViewOfFile函数的原型:

LPVOID MapViewOfFile(
    HANDLE hFileMappingObject,  // 文件映射对象的句柄
    DWORD dwDesiredAccess,      // 访问权限
    DWORD dwFileOffsetHigh,     // 文件偏移的高32位字节个数
    DWORD dwFileOffsetLow,      // 文件偏移的低32位字节个数
    SIZE_T dwNumberOfBytesToMap // 要映射到内存中的字节数
);

其中,各个参数的含义如下:

  • hFileMappingObject:文件映射对象的句柄,可以使用CreateFileMapping函数创建,表示要映射到内存中的文件或共享内存的句柄。
  • dwDesiredAccess:一个32位的AccessMask值,用来表示对内存的访问权限。可以设置为FILE_MAP_READ、FILE_MAP_WRITE、FILE_MAP_ALL_ACCESS等。
  • dwFileOffsetHigh:文件偏移的高32位字节个数。
  • dwFileOffsetLow:文件偏移的低32位字节个数。
  • dwNumberOfBytesToMap:要映射到内存中的字节数。

函数返回值为指向映射内存的指针,如果函数执行失败,则返回NULL。在使用完内存映像后,读者记得使用UnmapViewOfFile()函数来释放映像内存,使用CloseHandle()函数来关闭文件句柄和映像句柄,以便操作系统可以回收资源。

有了上述几个关键API函数那么实现内存映射功能将会变得很容易实现,直接来看一下如下代码,当程序运行后会自动将c://pe/x86.exe目录下的文件读入内存,并返回一个lpMapAddress文件句柄;

#include <iostream>
#include <Windows.h>
#include <ImageHlp.h>

#pragma comment(lib,"Imagehlp.lib")

// --------------------------------------------------
// 定义全局变量,来存储 DOS头部/NT头部/Section头部
// --------------------------------------------------
PIMAGE_DOS_HEADER DosHeader = nullptr;
PIMAGE_NT_HEADERS NtHeader = nullptr;
PIMAGE_FILE_HEADER FileHead = nullptr;
PIMAGE_SECTION_HEADER pSection = nullptr;

// --------------------------------------------------
// 读取并设置文件基址以及文件大小
// --------------------------------------------------
CHAR GlobalFilePath[2048] = { 0 }; // 保存文件路径
DWORD GlobalFileSize = 0;          // 定义文件大小
DWORD GlobalFileBase = 0;          // 保存文件的基地址
DWORD IsOpen = 0;                  // 设置文件是否已经打开

// --------------------------------------------------
// 打开文件操作
// --------------------------------------------------
HANDLE OpenPeFile(LPCSTR FileName)
{
    HANDLE hFile, hMapFile, lpMapAddress = NULL;

    hFile = CreateFileA(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("[-] 打开文件失败 \n");
        exit(0);
    }
    GlobalFileSize = GetFileSize(hFile, NULL);
    if (GlobalFileSize != 0)
    {
        printf("[+] 已读入文件 \n");
    }

    hMapFile = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, GlobalFileSize, NULL);
    if (hMapFile == NULL)
    {
        printf("[-] 创建映射对象失败\n");
        exit(0);
    }

    lpMapAddress = MapViewOfFile(hMapFile, FILE_MAP_READ, 0, 0, GlobalFileSize);
    if (lpMapAddress != NULL)
    {
        // 设置读入文件基地址
        GlobalFileBase = (DWORD)lpMapAddress;

        // 获取DOS头并判断是不是一个有效的DOS文件
        DosHeader = (PIMAGE_DOS_HEADER)GlobalFileBase;
        if (DosHeader->e_magic != IMAGE_DOS_SIGNATURE)
        {
            printf("[-] 文件不属于DOS结构 \n");
            exit(0);
        }

        // 获取 NT 头并判断是不是一个有效的PE文件
        NtHeader = (PIMAGE_NT_HEADERS)(GlobalFileBase + DosHeader->e_lfanew);
        if (NtHeader->Signature != IMAGE_NT_SIGNATURE)
        {
            printf("[-] 文件不属于PE结构 \n");
            exit(0);
        }

        // 判断是不是32位程序
        if (NtHeader->OptionalHeader.Magic != 0x010B)
        {
            printf("[-] 无法调试非32位PE文件\n");
            exit(0);
        }

        // 获取到文件头指针
        FileHead = &NtHeader->FileHeader;

        // 获取到节表头
        pSection = IMAGE_FIRST_SECTION(NtHeader);
    }

    return lpMapAddress;
}

int main(int argc, char * argv[])
{
    HANDLE BaseAddr = OpenPeFile("c://pe/x86.exe");
    printf("[+] 入口地址 = %x \n", BaseAddr);

    system("pause");
    return 0;
}

与2.1 PE结构:文件映射进内存相似的内容:

2.1 PE结构:文件映射进内存

PE结构是`Windows`系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,在任何一款操作系统中,可执行程序在被装入内存之前都是以文件的形式存放在磁盘中的,在早期DOS操作系统中,是以COM文件的格式存储的,该文件格式限制了只能使用代码段,堆栈寻址也被限制在了64KB的段中,由于PC芯片的快速发展这种文件格式极大的制

2.4 PE结构:节表详细解析

节表(Section Table)是Windows PE/COFF格式的可执行文件中一个非常重要的数据结构,它记录了各个代码段、数据段、资源段、重定向表等在文件中的位置和大小信息,是操作系统加载文件时根据节表来进行各个段的映射和初始化的重要依据。节表中的每个记录则被称为`IMAGE_SECTION_HEADER`,它记录了一个段的各种属性信息和在文件中的位置和大小等信息,一个文件可以由多个`IMA

2.2 PE结构:文件头详细解析

PE结构是`Windows`系统下最常用的可执行文件格式,理解PE文件格式不仅可以理解操作系统的加载流程,还可以更好的理解操作系统对进程和内存相关的管理知识,DOS头是PE文件开头的一个固定长度的结构体,这个结构体的大小为64字节(0x40)。DOS头包含了很多有用的信息,该信息可以让Windows操作系统使用正确的方式加载可执行文件。从DOS文件头`IMAGE_DOS_HEADER`的`e_lf

2.14 PE结构:地址之间的转换

在可执行文件PE文件结构中,通常我们需要用到地址转换相关知识,PE文件针对地址的规范有三种,其中就包括了`VA`,`RVA`,`FOA`三种,这三种该地址之间的灵活转换也是非常有用的,本节将介绍这些地址范围如何通过编程的方式实现转换。VA(Virtual Address,虚拟地址):它是在进程的虚拟地址空间中的地址,用于在运行时访问内存中的数据和代码。VA是相对于进程基址的偏移量。在不同的进程中,

2.11 PE结构:添加新的节区

在可执行PE文件中,节(section)是文件的组成部分之一,用于存储特定类型的数据。每个节都具有特定的作用和属性,通常来说一个正常的程序在被编译器创建后会生成一些固定的节,通过将数据组织在不同的节中,可执行文件可以更好地管理和区分不同类型的数据,并为运行时提供必要的信息和功能。节的作用是对可执行文件进行有效的分段和管理,以便操作系统和加载器可以正确加载和执行程序。

2.12 PE结构:实现PE字节注入

本章笔者将介绍一种通过Metasploit生成ShellCode并将其注入到特定PE文件内的Shell植入技术。该技术能够劫持原始PE文件的入口地址,在PE程序运行之前执行ShellCode反弹,执行后挂入后台并继续运行原始程序,实现了一种隐蔽的Shell访问。而我把这种技术叫做字节注入反弹。字节注入功能调用`WritePEShellCode`函数,该函数的主要作用是接受用户传入的一个文件位置,并

2.6 PE结构:导出表详细解析

导出表(Export Table)是Windows可执行文件中的一个结构,记录了可执行文件中某些函数或变量的名称和地址,这些名称和地址可以供其他程序调用或使用。当PE文件执行时Windows装载器将文件装入内存并将导入表中登记的DLL文件一并装入,再根据DLL文件中函数的导出信息对可执行文件的导入表(IAT)进行修正。

2.8 PE结构:资源表详细解析

在Windows PE中,资源是指可执行文件中存放的一些固定不变的数据集合,例如图标、对话框、字符串、位图、版本信息等。PE文件中每个资源都会被分配对应的唯一资源ID,以便在运行时能够方便地查找和调用它们。PE文件中的资源都被组织成一个树形结构,其中最顶层为根节点(Root),下一级为资源类型(Type),再下一级为资源名称(Name),最终是实际的资源内容。PIMAGE_RESOURCE_DIR

21.1 使用PEfile分析PE文件

PeFile模块是`Python`中一个强大的便携式第三方`PE`格式分析工具,用于解析和处理`Windows`可执行文件。该模块提供了一系列的API接口,使得用户可以通过`Python`脚本来读取和分析PE文件的结构,包括文件头、节表、导入表、导出表、资源表、重定位表等等。此外,PEfile模块还可以帮助用户进行一些恶意代码分析,比如提取样本中的字符串、获取函数列表、重构导入表、反混淆等等。PE

2.5 PE结构:导入表详细解析

导入表(Import Table)是Windows可执行文件中的一部分,它记录了程序所需调用的外部函数(或API)的名称,以及这些函数在哪些动态链接库(DLL)中可以找到。在Win32编程中我们会经常用到导入函数,导入函数就是程序调用其执行代码又不在程序中的函数,这些函数通常是系统提供给我们的API,在调用者程序中只保留一些函数信息,包括函数名机器所在DLL路径。当程序需要调用某个函数时,它必须知