美文网首页
PE文件解析 基础篇

PE文件解析 基础篇

作者: 看雪学院 | 来源:发表于2018-10-19 18:02 被阅读161次


    前言

    之前学习了PE格式,为了更好的理解,决定写一个类似LoadPE的小工具。

    编译器是VS2015,采用MFC框架。

    此系列文章采用边介绍知识点,边写代码的形式,以免变的无聊丧失兴趣。

    PE知识请参照《加密与解密》第10章。

    PE文件格式

    1. PE文件基本概念

    PE文件是windows系统中遵循PE结构的文件,比如以.exe .dll为后缀名的文件以及系统驱动文件。(PE结构框架看下图)

    PE文件大体分为两部分,头(包括下图中的DOS头,PE文件头,块表)与主体(块)。

    PE文件从磁盘当中像内存中的映射,不是简单的“1对1”的关系,而是“拉长”了。具体的位置表现在块。但是磁盘上的数据结构与在内存中的结构是一致的。

    无论PE文件在磁盘中还是在内存中,都少不了地址的概念,理解一下几个概念至关重要。

     虚拟地址(VA): 在一个程序运行起来的时候,会被加载到内存中,并且每个进程都有自己的4GB,这个4GB当中的某个位置叫做**虚拟地址**,由物理地址映射过来的,4GB的空间,并没有全部被用到。

    基地址(Imagebase):磁盘中的文件加载到内存当中的时候可以加载到任意位置,而这个位置就是程序的基址。EXE默认的加载基址是400000h,DLL文件默认基址是10000000h。需要注意的是基地址不是程序的入口点。

    相对虚拟地址(RVA):为了避免PE文件中有确定的内存地址,引入了相对虚拟地址的概念。RVA是在内存中相对与载入地址(基地址)的偏移量,所以你可以发现前三个概念的关系:虚拟地址(VA)= 基地址+相对虚拟地址(RVA)

    文件偏移地址(FOA):当PE文件储存在某个磁盘当中的时候,某个数据的位置相对于文件头的偏移量。

    入口点(OEP):首先明确一个概念就是OEP是一个RVA,然后使用OEP +Imagebase ==入口点的VA,通常情况下,OEP指向的不是main函数。

    存了张图比较好的解释了各部分的关系:

    接下来依次介绍PE结构框图的每个部分。

    2. DOS头部

    每个PE文件都是以DOS头开始的,IMAGE_DOS_HEADER 结构如下所示:

    (最左边是文件头的偏移量。)

    IMAGE_DOS_HEADER STRUCT

    {

    +0h WORD    e_magic//   MZ(4Dh 5Ah)     DOS可执行文件标记

    +2h     WORD    e_cblp

    +4h WORD    e_cp

    +6h WORD    e_crlc

    +8h WORD    e_cparhdr

    +0ah    WORD    e_minalloc

    +0ch    WORD    e_maxalloc

    +0eh    WORD    e_ss

    +10h    WORD    e_sp

    +12h    WORD    e_csum

    +14h    WORD    e_ip

    +16h    WORD    e_cs

    +18h    WORD    e_lfarlc

    +1ah    WORD    e_ovno

    +1ch    WORD    e_res[4]

    +24h    WORD    e_oemid

    +26h    WORD    e_oeminfo

    +29h    WORD    e_res2[10]

    +3ch    DWORD   e_lfanew//  RVA     指向PE文件头

    } IMAGE_DOS_HEADER ENDS

    需要关注的点是结构体的第一个和第二个元素。

    e_magic:DOS头的标记位,值为4D5Ah。ASCII为”MZ“,判断一个文件是否为PE文件是会用。

    e_lfanew:这是一个RVA,代表了PE文件头到基址的偏移量,我们可以用它来找到PE文件头的位置。

    我们用010editor打开一个exe文件:


    3. PE文件头

    IMAGE_NT_HEADERS STRUCT  结构体

    IMAGE_NT_HEADERS STRUCT

    {

    +0h       DWORD    Signature

    +4h       IMAGE_FILE_HEADER    FileHeader

    +18h      IMAGE_OPTIONAL_HEADER32   OptionalHeader

    } IMAGE_NT_HEADERS ENDS

    Signature  字段

    在一个PE文件中Signature字段被设置为4550h,ASCII码为”PE00“。如上图所示。 

    IMAGE_FILE_HEADER  结构体

    structIMAGE_FILE_HEADER

    {

    WORD Machine;//运行平台

    WORD NumberOfSections;//区块表的个数

    DWORD TimeDataStamp;//文件创建时间,是从1970年至今的秒数

    DWORD PointerToSymbolicTable;//指向符号表的指针

    DWORD NumberOfSymbols;//符号表的数目

    WORD SizeOfOptionalHeader;//IMAGE_NT_HEADERS结构中OptionHeader成员的大小,对于win32平台这个值通常是0x00e0

    WORD Characteristics;//文件的属性值

    }

    在010 Editor上查看一下:

    IMAGE_OPTIONAL_HEADER 结构体

    typedefstruct_IMAGE_OPTIONAL_HEADER

    {

    //

    // Standard fields.  

    //

    +18h    WORD    Magic;// 标志字, ROM 映像(0107h),普通可执行文件(010Bh)

    +1Ah    BYTE    MajorLinkerVersion;// 链接程序的主版本号

    +1Bh    BYTE    MinorLinkerVersion;// 链接程序的次版本号

    +1Ch    DWORD   SizeOfCode;// 所有含代码的节的总大小

    +20h    DWORD   SizeOfInitializedData;// 所有含已初始化数据的节的总大小

    +24h    DWORD   SizeOfUninitializedData;// 所有含未初始化数据的节的大小

    +28h    DWORD   AddressOfEntryPoint;// 程序执行入口RVA

    +2Ch    DWORD   BaseOfCode;// 代码的区块的起始RVA

    +30h    DWORD   BaseOfData;// 数据的区块的起始RVA

    //

    // NT additional fields.    以下是属于NT结构增加的领域。

    //

    +34h    DWORD   ImageBase;// 程序的首选装载地址

    +38h    DWORD   SectionAlignment;// 内存中的区块的对齐大小

    +3Ch    DWORD   FileAlignment;// 文件中的区块的对齐大小

    +40h    WORD    MajorOperatingSystemVersion;// 要求操作系统最低版本号的主版本号

    +42h    WORD    MinorOperatingSystemVersion;// 要求操作系统最低版本号的副版本号

    +44h    WORD    MajorImageVersion;// 可运行于操作系统的主版本号

    +46h    WORD    MinorImageVersion;// 可运行于操作系统的次版本号

    +48h    WORD    MajorSubsystemVersion;// 要求最低子系统版本的主版本号

    +4Ah    WORD    MinorSubsystemVersion;// 要求最低子系统版本的次版本号

    +4Ch    DWORD   Win32VersionValue;// 莫须有字段,不被病毒利用的话一般为0

    +50h    DWORD   SizeOfImage;// 映像装入内存后的总尺寸

    +54h    DWORD   SizeOfHeaders;// 所有头 + 区块表的尺寸大小

    +58h    DWORD   CheckSum;// 映像的校检和

    +5Ch    WORD    Subsystem;// 可执行文件期望的子系统

    +5Eh    WORD    DllCharacteristics;// DllMain()函数何时被调用,默认为 0

    +60h    DWORD   SizeOfStackReserve;// 初始化时的栈大小

    +64h    DWORD   SizeOfStackCommit;// 初始化时实际提交的栈大小

    +68h    DWORD   SizeOfHeapReserve;// 初始化时保留的堆大小

    +6Ch    DWORD   SizeOfHeapCommit;// 初始化时实际提交的堆大小

    +70h    DWORD   LoaderFlags;// 与调试有关,默认为 0

    +74h    DWORD   NumberOfRvaAndSizes;// 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16

    +78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

    // 数据目录表

    } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

    重要的有:

    AddressOfEntryPoint: 也就是上文提到的OEP,程序源入口点。

    ImageBase: 默认加载基址。

    SectionAlignment:  内存当中的块对齐数,一般为0x1000。

    FileAlignment:磁盘当中块对齐数,一般为0x200。

    SizeOfHeaders:所有头部大小 也就是DOS头 文件头 以及区块头的总大小,文件主体相对文件其实的偏移。

    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]:数据目录表,保存了各种表的RVA及大小。

    来看一下数据目录的定义:

    IMAGE_DATA_DIRECTORY STRUCT

    VirtualAddress    DWORD       ?   ; 数据的起始RVA

    Size             DWORD       ?   ; 数据块的长度

    IMAGE_DATA_DIRECTORY ENDS

    在010 Editor上查看一下:


    4. 写代码操作一下

    主要解析了DOS头与PE文件头比较重要的字段,直接放代码。

    //打开文件

    m_hFile = CreateFile(

    m_DeleFileName,GENERIC_READ,NULL,NULL,OPEN_EXISTING,

    FILE_ATTRIBUTE_NORMAL,NULL);

    DWORD dwSize = GetFileSize(m_hFile,NULL);

    PBYTE pBuf =newBYTE[dwSize]{};

    //读取

    ReadFile(m_hFile,pBuf,dwSize,&dwSize,NULL);

    //判断是否为PE文件

    m_pDos = PIMAGE_DOS_HEADER(pBuf);

    if(m_pDos->e_magic!=IMAGE_DOS_SIGNATURE)

    {

    MessageBox(L"不是有效的PE文件 \n");

    CloseHandle(m_hFile);

    m_hFile =NULL;

    return;

    }

    m_pNTHeader = PIMAGE_NT_HEADERS(pBuf+m_pDos->e_lfanew);

    if(m_pNTHeader->Signature!= IMAGE_NT_SIGNATURE)

    {

    MessageBox(L"不是有效的PE文件 \n");

    CloseHandle(m_hFile);

    m_hFile =NULL;

    return;

    }

    //读取文件头信息

    m_pFileHeader = &(m_pNTHeader->FileHeader);

    m_NumberOfSections.Format(L"%X",m_pFileHeader->NumberOfSections);

    m_TimeDateStamp.Format(L"%p", m_pFileHeader->TimeDateStamp);

    m_SizeOfOptionalHeader.Format(L"%X", m_pFileHeader->SizeOfOptionalHeader);

    //拓展头信息

    m_pOptionalHeader = &(m_pNTHeader->OptionalHeader);

    m_AddressOfEntryPoint.Format(L"%X",m_pOptionalHeader->AddressOfEntryPoint);

    m_SizeOfHeaders.Format(L"%X", m_pOptionalHeader->SizeOfHeaders);

    m_ImageBase.Format(L"%X", m_pOptionalHeader->ImageBase);

    m_SizeOfImage.Format(L"%X", m_pOptionalHeader->ImageBase);

    m_BaseOfCode.Format(L"%X", m_pOptionalHeader->BaseOfCode);

    m_DllCharacteristics.Format(L"%X", m_pOptionalHeader->DllCharacteristics);

    m_BaseOfData.Format(L"%X", m_pOptionalHeader->BaseOfData);

    m_NumberOfRvaAndSizes.Format(L"%X", m_pOptionalHeader->NumberOfRvaAndSizes);

    m_SectionAlignment.Format(L"%X", m_pOptionalHeader->SectionAlignment);

    m_FileAlignment.Format(L"%X", m_pOptionalHeader->FileAlignment);

    m_CheckSum.Format(L"%X", m_pOptionalHeader->CheckSum);

    m_Magic.Format(L"%X", m_pOptionalHeader->CheckSum);

    m_Subsystem.Format(L"%X", m_pOptionalHeader->Subsystem);

    实现的效果如下:

    第一部分比较简单,完整代码放到附件。(点击原文链接,即可获取附件)

    - End -

    原文作者:Jabez

    原文链接:https://bbs.pediy.com/thread-247114.htm

    转载请注明:转自看雪学院

    更多阅读:

    1、Ollydbg插件的编写流程

    2、利用TGP接口查数据

    3、C语言文件利用读写操作,寻找输入文件中的两个最短字符串

    4、新增节移动重定向表……

    相关文章

      网友评论

          本文标题:PE文件解析 基础篇

          本文链接:https://www.haomeiwen.com/subject/ifxszftx.html