欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]PE解析软件

[原创]PE解析软件

本软件只是对LordPE进行简单的仿写,功能并不齐全,只是通过本软件来加深对PE文件格式的理解,对PE文件进行分析能更好的了解PE的结构,了解了PE才能对安全方向的一些知识有更好的理解。所以PE文件的解析对新人是很重要的知识,PE文件网上有大量的资料供新人们学习。

     1.需求分析:   
    此软件功能需要,对PE文件进行分析,将PE文件的一些基本信息解析出来,文件的PE头基本信息,文件的区段个数和信息。PE文件的目录信息,期中重要的信息,文件导出表,导入表,资源。
    对文件的导出表进行解析,获取到该文件导出表的起始地址,导出函数的数量函数名称数量,导出函数名称、序号、RVA、偏移等信息。对文件的导入表进行解析,获取到导入表中导入的DLL和DLL中相关的函数信息。资源,解析出位图,图标,图标ID等文件中用到的资源。可以对这些信息进行修改,并且保存文件。
    编写PE解析软件需要对PE文件有一定了解,知道PE结构,可以将PE文件中的数据很好的提取并解析出来。
   2.开发过程以及函数:
    软件功能可以像lordpe可以对PE文件进行分析,并且有修改保存PE文件功能。写这个软件目前实现功能,将解析文件的功能写了一个简单的类PeHear,其中实现了PE文件解析,解析文件头的基本信息函数,解析PE区段信息函数,解析PE导入表信息函数,解析PE文件导出表信息函数。资源解析函数未完成。
    该软件开发使用了类的继承,PeHear类是父类,其他函数通过继承PeHear读取文件中的信息,调用类中的函数将其中信息存到PeHear类中变量和自定义结构体中,创建出相应窗口将PE信息显示出来。
函数信息:

//计算值将Rva转换成Foa

DWORD RVA2Offset(DWORD dwRVA);      

//加载PE 

BOOL  LoadPe(); 

//判断是否为PE文件

BOOL  IsPE_File();                                      

//获取PE映像文件头

void  GetFileHeader();

//获取PE映像扩展头

void  GetOptionalHeader();

//获取PE区段信息

void  GetSectionHeader();

//导出表

void  GetExportDirectory();

//导入表

void  GetImportDirectory();

    资源函数未完成
    void  cller();此函数为外界接口,调用此函数,会将文件加载分析,保存到相关变量和自定义结构体中。
    首先,编写一个解析PE的类,这个类为基类,其他界面上的功能都继承于此类中。
    解析PE文件信息,我们需要将PE文件加载到一段虚拟内存中,当磁盘文件一旦被加载到内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。只是数据之间的相对位置可能会有些变化,有些偏移地址可能区别与原始的偏移位置,但是文件在磁盘和在内存中的偏移都是可以计算转换的。

    使用CreateFile() 打开所要解析的PE文件,GetFileSize() 获取到PE文件的大小,通过大小
    使用VirtualAlloc()开辟一块虚拟内存,使用ReadFile() 将PE文件读取到这块内存中。
    加载到内存中的首地址就是PE的DOS头的位置,通过DOS头的结构体找到PE所在的位置。到此我们的准备工作已经做好了,后面就要通过windows自带的结构体对PE文件进行解析了。
    DOS头指向PE文件。PE文件包含PE映像文件头和PE映像的扩展头。
    将PE加载完成之后,对其加载首地址强制类型转换为PIMAGE_DOS_HEADER 类型的指针,获取到DOS头的结构信息。我们需要注意两个字段,e_mageic DOS可执行文件的表示为,表示这是一个MS-DOS下的可执行文件,e_lfanew新的EXE文件头偏移地址。
    PE文件头 
    PE文件头的结构体类型为IMAGE_NT_HEADERS ,通过DOS头的内存中加载位置与DOS头中的e_lfanew字段相加计算出PE文件头的地址指针。

●
  • Signature是PE文件头的标识,其值始终为0x00004550,ASCII码为“PE/0/0”,win32SDK使用#define IMAGE_NT_SIGNATURE定义了这个值,我们可以通过DOS和PE这两个标识来判断该文件是否为PE文件。
  • FileHeader是标准PE头
  • OptionalHeader是扩展PE头,重要的信息都存放在IMAGE_OPTIONAL_HEADER这个结构体中。

  •      使用GetFileHeader()获取PE映像文件头
    //解析PE映像文件头
    
    void  GetFileHeader()
    
    {
    
        m_PeHead = m_pNT->FileHeader;
    
        m_pImageHeaderNum = m_PeHead.NumberOfSections;
    
     
    
    }


        我们需要的字段为 
        NUmberOfSections 区块的数据量,用来获取到程序区段的数量。
        SizeOfOptionalHeader 扩展头大小,在IMAGE_FILE_HEADER结构后面的扩展头大小。一般情况下,这个结构的大小在32为的系统中是0x00E0,而在64位系统中是0x00F0。        
        使用GetOptionalHeader()获取PE映像的扩展头
        IMAGE_OPTIONAL_HEADER32
        此结构比较大,就不粘此结构体了,只列出需要用到的字段。
    ●
  • SizeOfCode 所有代码区段的总大小。
  • AddressOfEntryPoint 程序执行入口RVA。也就是我们所说的OEP。
  • ImageBase 文件在内存中的首选装入地址,默认加载基址,为PE文件的优先装载地址。比如,如果该值是400000h,PE装载器将尝试把文件装到虚拟地址空间的400000h处。字眼"优先"表示若该地址区域已被其他模块占用,那PE装载器会选用其他空闲地址。
  • SectionAlignment:映像文件在内存中的区段对齐大小。
  • FileAlignment:映像文件在磁盘中的区段对齐大小。
  • SizeOfImage:内存中整个PE映像体的尺寸。它是所有头和节经过节对齐处理后的大小。
  • SizeOfHeaders:为MS-DOS头、PE头、区块表的尺寸之和。
  • NumberOfRvaAndSizes:数据目录表的成员数量,一般为16个。
  • IMAGE_DATA_DIRECTORY DAtaDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]为数据目录数组,这些目录中的每一个目录都描述了一个特定的、位于目录项后面的某一节中的信息的位置。

  • //获取PE映像扩展头
    
    void  GetOptionalHeader()
    
    {
    
        m_Optinal = m_pNT->OptionalHeader;
    
        m_pDataDir =(PIMAGE_DATA_DIRECTORY)m_pNT->OptionalHeader.DataDirectory;
    
    }

        在获取区段或者其他的一些PE信息时,需要将RVA地址转换为Foa,自定义函数DWORD RVA2Offset(DWORD dwRVA); dwRVA参数是要转换的RVA的值。
        实现函数如下:
    DWORD CPeHear::RVA2Offset( DWORD dwRVA )
    
    {
    
     
    
        for ( DWORD i=0; i<m_pNT->FileHeader.NumberOfSections; i++ )
    
        {
    
            if ( dwRVA>=m_pSection[i].VirtualAddress && dwRVA<m_pSection[i].VirtualAddress+m_pSection[i].Misc.VirtualSize )
    
            {
    
                DWORD dwR_Offset = dwRVA - m_pSection[i].VirtualAddress;
    
                DWORD dwOffset   = (DWORD)m_lpFileImage + dwR_Offset + m_pSection[i].PointerToRawData;
    
                return dwOffset;
    
            }
    
        }
    
        return 0;  
    
    }

    区段信息:
        用来保存PE区段信息的结构体:
    typedef  struct _SECTIONHEADER
    
    {
    
        CString NAME;
    
        DWORD PointerToRawData;                 //区段在文件中的偏移 
    
        DWORD SizeOfRawData;                    //文件中区段对齐大小
    
        DWORD MiscVirtualSize;                  //偏移大小
    
        DWORD VirtualAddress;                   //区段的RVA地址
    
     
    
    }SECTION,*PSECTION


    ●
  • VirtualSize:实际使用的区段大小。
  • VirtualAddress:此区段载入内存后的RVA。
  • PointerToRawData:此区段在文件中的偏移。
  • SizeOfRawData:此区段在磁盘中的体积,这个地址是按照文件页对齐的。
  • Characteristics:区段属性,用以描述此区段的读写情况、状态等属性。

  •     通过自定义GetSectionHeader();函数获取到PE区段信息,使用IMAGE_FIRST_SECTION  获取到区段表中第一个区段,通过区段个数遍历循环获取出区段表中的信息。将区段名称,实际呗使用的区段大小,此区段载入内存后的RVA,和此区段在文件中大小等信息。保存到结构体类型的向量中。
    输出表信息:
       保存输出表信息自定义结构体:
    typedef struct  _EXPORT
    
    {
    
        DWORD      ID;      //导出函数的序号
    
        DWORD      EAT;     //导出函数的地址RVA
    
        DWORD      EX_RVA;  //导出函数的地址偏移
    
        CString    NAME;    //导出的函数名称
    
    }EXPORTA,*PEXPORT;


    ●
  • Name指向模块名的ASCII字符的RVA
  • Base 导出表用于输出API索引值的基数
  • NumberOfName:ENT中的条目数量,ENT的条目数量小于等于EAT中条目的数量。
  • NumberOfFunctions:EAT中的条目数量。
  • AddressOfNameOrdinal:EAT的RVA(导出函数地址的相对虚拟地址),EAT中的每一个非0的项都对应一个被导出的函数名称或序号。
  • AddressOfNames:ENT的RVA(导出函数名的相对虚拟地址),ENT和EAT的每一个非0的项都对应一个被导出的函数地址或序号。
  • AddressOfNameOrdinals:导出序号表的RVA。这个表是一个WORD类型的数组。它将ENT中的索引映射到导出地址表中相应的元素上。

  •     GetExportDirectory() 获取输出表信息,一般只是DLL文件中有输出表信息,所以需要判断该程序是否有输出表,如果没有则将设置弹出框标志位,弹出提示没有导出表结构,返回。
        如果有输出表,则获取到PE输出表的信息输出表偏移函数数量,函数名数量、函数地址等信息,基址,名称,函数名称地址,名称字符串,函数名序号地址。
        然后使用EXPORTA 类型的vector来保存导出函数序号,导出函数的RVA,偏移地址和函数名称。
       通过以下函数和上面的结构体获取到导出表相应的信息。
    void GetExportDirectory()
    
    {
    
         
    
     
    
        PIMAGE_DATA_DIRECTORY pExportDir = &m_pDataDir[IMAGE_DIRECTORY_ENTRY_EXPORT];
    
        if (pExportDir->Size == 0)
    
        return;
    
        DWORD                 dwExportOfffset = RVA2Offset(pExportDir->VirtualAddress);
    
        m_pExport    = (PIMAGE_EXPORT_DIRECTORY)dwExportOfffset;
    
        m_ExportOffset = RVA2Offset(m_Optinal.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress) - (DWORD)m_lpFileImage;
    
        PDWORD pEAT = (PDWORD)RVA2Offset(m_pExport->AddressOfFunctions);
    
        PDWORD pENT = (PDWORD)RVA2Offset(m_pExport->AddressOfNames);
    
        PWORD  pEIT = (PWORD) RVA2Offset(m_pExport->AddressOfNameOrdinals);
    
        //m_ExportOffset = 
    
     
    
        for ( DWORD dwOrdinal=0; dwOrdinal<m_pExport->NumberOfFunctions; dwOrdinal++ )
    
        {
    
            if ( !pEAT[dwOrdinal] )
    
                continue;
    
     
    
            for ( DWORD dwIndex=0; dwIndex<m_pExport->NumberOfFunctions; dwIndex++ )
    
            {
    
                    EXPORTA saveImport;
    
                if ( pEIT[dwIndex] == dwOrdinal )
    
                {
    
                    PCHAR pszFunName = (PCHAR)RVA2Offset(pENT[dwIndex]);
    
                    saveImport.NAME.Format(_T("%S"),pszFunName);
    
                    saveImport.EX_RVA = m_pExport->AddressOfFunctions;
    
                    saveImport.ID   = m_pExport->Base+dwOrdinal;
    
                    saveImport.EAT  = pEAT[dwOrdinal];
    
                    m_vExport.push_back(saveImport);
    
                    break;
    
                }
    
                else if ( dwIndex == m_pExport->NumberOfFunctions-1 )
    
                {
    
                    saveImport.NAME.Format(_T("(Null)"));
    
                    saveImport.ID   = m_pExport->Base+dwOrdinal;
    
                    saveImport.EX_RVA = m_pExport->AddressOfFunctions;
    
                    saveImport.EAT  = pEAT[dwOrdinal];
    
                    m_vExport.push_back(saveImport);
    
                    break;
    
                }
    
            }
    
        }
    
     
    
    }

    输入表信息:
        输入目录是一个多IMAGE_IMPORT_DESCRIPTOR(输入描述结构)的数组,每个被使用的DLL文件都有一个。(它们的)列表由一个全部用0填充的IMAGE_IMPORT_DESCRIPTOR(输入地址表目录项)结构作为结束。
    保存输入表信息结构体:

    相应的导入表结构体:
    typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    
        union {
    
            DWORD   Characteristics;            
    
            DWORD   OriginalFirstThunk;         // 指向输入名称表(IAT)的RVA
    
        } DUMMYUNIONNAME;
    
        DWORD   TimeDateStamp;                  //时间标识
    
        DWORD   ForwarderChain;                 // 转发链如果不转发此值为0
    
        DWORD   Name;                           //指向导入映像文件的名字
    
        DWORD   FirstThunk;                     // 指向导入地址表(IAT)的RVA
    
    } IMAGE_IMPORT_DESCRIPTOR;
    
     
    
     
    
    typedef struct _IMAGE_THUNK_DATA32 {
    
        union {
    
            DWORD ForwarderString;      // 转发字符的RVA
    
            DWORD Function;             // 被导入函数的地址
    
            DWORD Ordinal;              //被导入函数的序号
    
            DWORD AddressOfData;        // 指向输入名称
    
        } u1;
    
    } IMAGE_THUNK_DATA32;
    
     
    
     
    
    typedef struct _IMAGE_IMPORT_BY_NAME {
    
        WORD    Hint;                   //需导入函数序号
    
        CHAR   Name[1];                 //导入函数名称
    
    } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

    获取导入表信息的函数:
    void CPeHear::GetImportDirectory()
    
    {
    
     
    
        PIMAGE_DATA_DIRECTORY pDir = (PIMAGE_DATA_DIRECTORY)m_pNT->OptionalHeader.DataDirectory;
    
        PIMAGE_DATA_DIRECTORY pDataDir = pDir+IMAGE_DIRECTORY_ENTRY_IMPORT;
    
        PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)RVA2Offset(pDataDir->VirtualAddress);
    
     
    
     
    
        while ( pImport->Name )
    
        {
    
            IMPORT saveImport;
    
     
    
            saveImport.pszDllName = (PCHAR)RVA2Offset(pImport->Name);
    
            PIMAGE_THUNK_DATA32 pINT = (PIMAGE_THUNK_DATA32)RVA2Offset(pImport->OriginalFirstThunk);
    
             
    
            while ( pINT->u1.Ordinal )
    
            {
    
                if ( !IMAGE_SNAP_BY_ORDINAL32(pINT->u1.Ordinal) )
    
                {
    
                    PIMAGE_IMPORT_BY_NAME pByName = (PIMAGE_IMPORT_BY_NAME)RVA2Offset(pINT->u1.AddressOfData);
    
                    saveImport.NAME.Format(_T("%s"),pByName->Name);
    
                    saveImport.ID = pByName->Hint;   
    
                    saveImport.IM_RVA = pImport->FirstThunk;
    
                    saveImport.Offset = RVA2Offset(pImport->FirstThunk);
    
                    m_vImport.push_back(saveImport);
    
                    pINT++;
    
                    continue;
    
                } 
    
                // 2.4.2 如果最高位为1,则直接打印Ordinal部分
    
                saveImport.NAME.Format(_T("Null"));
    
                saveImport.ID = pINT->u1.Ordinal&0x0000FFFF;
    
                saveImport.IM_RVA = pImport->FirstThunk;
    
                saveImport.Offset = RVA2Offset(pImport->FirstThunk);
    
                m_vImport.push_back(saveImport);
    
                pINT++;
    
            }
    
            m_vpImport.push_back(pImport);
    
             
    
            pImport++;
    
        }
    
    }

    程序中使用到的变量:
        由于本程序是通过继承实现的,要使不同的模块访问同一个基类的时候,基类中保存的信息不会改变,所以要使用静态变量来保存PE结构中的需要用到的信息。
        变量信息:
    
        static PVOID                    m_lpFileImage;           //文件内存中起始加载位置
    
        static CString                  m_strInPath;                 //文件名
    
        static IMAGE_FILE_HEADER        m_PeHead;                //PE映像文件头
    
        static IMAGE_OPTIONAL_HEADER    m_Optinal;               //PE映像扩展头
    
        static PIMAGE_SECTION_HEADER    m_pSection;              //获取PE区段信息
    
        static PIMAGE_DOS_HEADER        m_pDos;                  //DOS头地址
    
        static PIMAGE_NT_HEADERS        m_pNT;                   //PE头地址
    
        static PIMAGE_DATA_DIRECTORY    m_pDataDir;              //PE数据目录
    
        static PIMAGE_EXPORT_DIRECTORY  m_pExport;               //输出表
    
        //static PIMAGE_IMPORT_DESCRIPTOR m_pImport;                 //输入表
    
     
    
        static CString                 m_pszDllName;            
    
        static DWORD                   m_pImageHeaderNum;       //区段数量
    
        static DWORD                   m_NumberOfRvaAndSizes;   //
    
        static DWORD                   m_ExportOffset;          //导出表的偏移
    
     
    
        static vector<SECTION>  m_vSection;                       //保存区段中的信息
    
        static vector<EXPORTA>  m_vExport;                        //保存导出表的信息
    
        static vector<IMPORT>   m_vImport;                        //保存导入表的信息
    
        static vector<PIMAGE_IMPORT_DESCRIPTOR> m_vpImport;       
    
    
     
    总结:
        此类的编写实现了PE文件的解析,可以将PE文件中内容显示到界面,但是对于修改文件的PE信息,没有用指针容易实现,没有用到指针不会出现内存泄露等指针问题,但是没有使用指针的节省空间和指针的操作速度,指针对文件修改的方便。有些函数完成的不是很好,需要进一步改善,通过这个项目更好的了解PE结构,对PE的分析有了更好的理解。但是目前软件尚未完成,通过以后的时间慢慢完善。

    上传的附件 PE解析软件的大致框架.jpg
    PE结构1.jpg
    PE头.jpg
    PE映射.jpg
    区块1.jpg
    输出表2.jpg
    输入表1.jpg

    注意:上传附件及图片大小不得大于30M。

    ⚠️ 版权声明:
    本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
    未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

    0 0 0 举报
    复制成功