Quantcast
Channel: 看雪安全论坛
Viewing all articles
Browse latest Browse all 9556

【原创】[15Pb培训第三阶段课后小项目]PE解析软件

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

附件 84859

     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文件加载到一段虚拟内存中,当磁盘文件一旦被加载到内存中,磁盘上的数据结构布局和内存中的数据结构布局是一致的。只是数据之间的相对位置可能会有些变化,有些偏移地址可能区别与原始的偏移位置,但是文件在磁盘和在内存中的偏移都是可以计算转换的。

附件 84862

    使用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文件头的地址指针。

附件 84860
  • 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;
 
}

附件 84861

    我们需要的字段为 
    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

附件 84863
  • 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;

附件 84864
  • 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(输入地址表目录项)结构作为结束。

保存输入表信息结构体:
附件 84865

相应的导入表结构体:

代码:

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的分析有了更好的理解。但是目前软件尚未完成,通过以后的时间慢慢完善。:eek:

上传的图像
文件类型: jpg PE解析软件的大致框架.jpg (22.1 KB)
文件类型: jpg PE结构1.jpg (51.4 KB)
文件类型: jpg PE头.jpg (45.3 KB)
文件类型: jpg PE映射.jpg (48.5 KB)
文件类型: jpg 区块1.jpg (88.9 KB)
文件类型: jpg 输出表2.jpg (45.9 KB)
文件类型: jpg 输入表1.jpg (44.8 KB)

Viewing all articles
Browse latest Browse all 9556


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>