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

系统底层 [15Pb培训第三阶段课后小项目]PE解析软件

$
0
0
PE解析软件项目报告

写在前面
对于PE有很多前人有很多很好的文章,本文很多思路并非原创只是本着共享的原则对于一些资源的整理加上一些自己的认识,能帮助某些正巧需要的人了解到一些东西就好。:eek:
目标人群 熟悉简单C++的MFC开发并对PE初步的大体了解想用代码来解析PE文件
注意:看本文章请下载相关的代码(有些东西不易表达但是一结合到代码环境内很容易意会了)和PE结构图或者介绍PE文档书籍对照阅读,本文章注重编程解析而不是对于单纯PE结构的理解。
本文假设读者有基本的英文基础,代码尽量望名生意。
表达中可能用到一些简化的英文表达方式例如NumbersofFunctions就直接写成NumofFuncs


1. 项目信息
2. 关键技术分析
3. 主要成果
4. 经验与不足


1. 项目信息
    项目作为一个学习项目主要的目的是通过对于PE文件各种信息的解析显示来对PE结构加深认识。PE结构一个主要的特点就是数据结构比较繁杂且关系紧密。在这里通过程序把静态的文件这种把“线性的字节流”转换成PE文件格式定义所采用的“结构化的描述”,把二者结合在一起,能给人形成一个非常直观的认识,对于理解PE文件相关内容比较有帮助。
项目主要是依托于MFC各种对话框和自写的一个PE相关的工具类进行一系列的交互。
界面方面一般一个对话框对应一个具体的功能。在这里有
IDD_DISASM         反汇编
IDD_IMPORT        导入表相关
IDD_EXPORT        导出表相关
IDD_PEINFO        PE信息窗口
IDD_MAIN          主窗口
IDD_PEVIEW        PEView主界面
IDD_SECTION       区段相关信息
其逻辑关系如下图
附件 84745

逻辑方面主要是封装了一个PE文件操作类class CPE.
其函数包括
代码:

 //反汇编
    static ULONG DisASM(TCHAR* pSrc,ULONG ulSrcLen,ULONG ulImageBase,t_disasm* pResult,int iDisModule=DISASM_CODE);
    //校验是否是PE文件
    BOOL IsValidPE();
    //映射文件
    BOOL MapFile();
    //卸载映射
    void UnMapFile();
    //获取DosHeader
    IMAGE_DOS_HEADER* GetDosHeader(){return m_pDosHeader;}
    //获取NTHeader
    IMAGE_NT_HEADERS* GetPEHeader(){return m_pPEHeader;}
    //获取第一个Section
    IMAGE_SECTION_HEADER* GetFirstSecHeader(){return m_pSectionHeader;}
    //获取下一个Section
    IMAGE_SECTION_HEADER* GetNextSecHeader(){return m_pSectionHeader+1;}
    //获取数据目录表基址
    IMAGE_DATA_DIRECTORY* GetDataDirectory(){return m_pDataDirectory;}
    //获取输出表
    IMAGE_EXPORT_DIRECTORY* GetExportTable(){return m_pExportTable;}
    //获取输入表
    IMAGE_IMPORT_DESCRIPTOR* GetImportTable(){return m_pImportTable;}
    //获取导出表来源的名称
    LPCTSTR GetImportFrom(){return m_sExportFrom;}
    //DWORD转换为字符串
    static LPCTSTR LongToStr(LONG lLong);
    //根据文件偏移定位所在区段
    IMAGE_SECTION_HEADER* Raw2Section(DWORD dwRaw);
    //根据虚拟偏移定位所在区段
    IMAGE_SECTION_HEADER* Rav2Section(DWORD dwRav);
    //RVA转文件偏移
    DWORD Rva2Raw(DWORD dwRva);
    //RVA转数据指针
    BYTE* Rva2Ptr(DWORD dwRva);
    //文件偏移转RVA
    DWORD Raw2Rva(DWORD dwRaw);
    //文件偏移转内存指针
    BYTE* Raw2Ptr(DWORD dwRaw);
    //是否存在输出表
    BOOL ExistExportTable(){return m_bExistExportTable;}
    //是否存在输入表
    BOOL ExistImportTable(){return m_bExistImportTable;}
    //获取模块所在路径
    void GetModulePath(TCHAR* lpszModulePath);
    //获取模块名称
    void GetModuleName(TCHAR* lpszModuleName);
...

通过这些函数操作相关的成员来实现功能。
代码:

IMAGE_DOS_HEADER* m_pDosHeader;
IMAGE_NT_HEADERS* m_pPEHeader;
IMAGE_SECTION_HEADER* m_pSectionHeader;
IMAGE_DATA_DIRECTORY* m_pDataDirectory;
IMAGE_EXPORT_DIRECTORY* m_pExportTable;
IMAGE_IMPORT_DESCRIPTOR* m_pImportTable;
vector<IMPORTINFO> m_ImportVector;
...

2. 关键技术分析
    在PE结构相关概念理解之后进行操作并没有什么难度。但是有一些需要注意的方面。对于PE信息的解析都是些老生长谈的东西了,无非就是利用一些winnt.h定义好的一系列数据结构然后将文件正确的解析好处理好地址对应结构指向好然后根据不同结构的特点便能够正常的解析出来想要的信息。
对于PE文件解析一个很重要的点是各种地址的转换,在进行PE文件解析时必须对于地址有一个明确的概念。核心思想是关键数据的真实大小在两种地址中都是一样的,主要是由于对其的不同导致了真实大小的起点不同,所以这里的核心是抓住这一不变量来计算出相对应的偏移。而且由于这里的文件操作方式需要地址转相应数据指针的函数。如下所示
代码:

 //RVA转文件偏移
DWORD CPE::Rva2Raw(DWORD dwRva)
{
    int iSecNum=CPE::GetPEHeader()->FileHeader.NumberOfSections;
    for (int i=0;i<iSecNum;i++)
    {
        IMAGE_SECTION_HEADER* pSecHeader=GetFirstSecHeader()+i;
        if (dwRva>=pSecHeader->VirtualAddress && dwRva<(pSecHeader->VirtualAddress+pSecHeader->Misc.VirtualSize))
        {
            return (dwRva-pSecHeader->VirtualAddress+pSecHeader->PointerToRawData);
        }

    
        return 0;
}

//文件偏移转RVA

DWORD CPE::Raw2Rva(DWORD dwRaw)
{
    int iSecNum=GetPEHeader()->FileHeader.NumberOfSections;
    for (int i=0;i<iSecNum;i++)
    {
        IMAGE_SECTION_HEADER* pSecHeader=GetFirstSecHeader()+i;
        if (dwRaw>=pSecHeader->PointerToRawData && dwRaw<(pSecHeader->PointerToRawData+pSecHeader->SizeOfRawData))
        {
            return dwRaw-pSecHeader->PointerToRawData+pSecHeader->VirtualAddress;
        }
        
    }
    
        return 0;
}
    //RVA转数据指针
    BYTE* Rva2Ptr(DWORD dwRva);
{
  return (BYTE*)(m_pPEHeader->OptionalHeader.ImageBase+dwRva);
}
    //文件偏移转内存指针
    BYTE* Raw2Ptr(DWORD dwRaw);
{
  return (BYTE*)(m_pData+dwRaw);
}

程序主要流程是依据打开文件的路径传入成员函数的PE操作类对象然后将整个文件映射到内存中,注意不是程序映射是通过CreateFileMapping和MapViewOfFile来完成的映射可以理解为将整个PE文件的二进制文件整体放倒某个内存位置,因而这里对于地址的一些操作要进行一些注意,这里说一些地址相关的概念,开始由于这个不清晰犯了一些不该犯的错误。
简单来说对于PE文件的操作基本有三种方式
1磁盘文件操作方式,这种方式也就是PE文件概念中的Raw
2内存方式PE文件被映射到内存(MapViewOfFile),简单理解就是将上一种方式的文件平移到内存中的BaseAddress的地址。
3内存方式PE文件被装载到内存,这种就是正常执行文件了,相关概念就是PE文件里的Rva。
在自己进行解析操作时要分好这些概念。这里由于方式二的便利性(例如比较好的利用ImageHlp的微软操作PE的库)选择二这种方式来进行文件的操作。
私称为老三套的相关代码是
代码:

m_hFile=CreateFile(m_strDestExePath,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL|FILE_ATTRIBUTE_ARCHIVE,NULL);
    if (INVALID_HANDLE_VALUE==m_hFile)
    {
        return FALSE;
    }
    m_hMap=CreateFileMapping(m_hFile,NULL,PAGE_READONLY,0,0,NULL);
    if (NULL==m_hMap)
    {
        return FALSE;
    }
    m_pData=(BYTE*)MapViewOfFile(m_hMap,FILE_MAP_READ,0,0,0);
    if (NULL==m_pData)
    {
        return FALSE;
    }
    if (!IsValidPE())
    {
        UnMapFile();
        return FALSE;
    }

然后经过以上初始化得到的数据可以来解析一些关键的信息,在这里就是
m_pDosHeader m_pPEHeader m_pSectionHeader m_pDataDirectory这些是根据格式直接得到的信息或者偏移来的得到的。 这里还想要获得的信息还有导入导出表,它们同属于_IMAGE_OPTIONAL_HEADER中的_IMAGE_DATA_DIRECTORY 数组中的东西。也就是解析这个结构来获取内容
获取这两个数据还是比较直观的。这里注意的是保存的指针是在上面调用MapViewOfFile返回的m_pData加上获得方式为Rva2Raw(dwRvaofExport)的相应地址的Raw
由此便可以获得
指向IMAGE_EXPORT_DIRECTORY结构体和指向IMAGE_IMPORT_DESCRIPTOR结构体
然后再由这些信息解析出相应表的信息
解析导出表的信息是相对简单的这里拿个图示来理解。这里核心基本上就是理通最后三个成员的地址关系
附件 84746
通俗点的介绍就是由于有些某些导出表项并不一定有相应的Name所以便不能单纯的地址指针和函数名指针一一对应因而需要一个纽带来联系他们,这里多了一个索引值数组也就是NameOrdinals,它与名称数组个数相同。可以理解成为地址数组根据这个索引来找到相应的名称。如果不存在对应的索引这个函数便没有名称。
这里的代码思路是以NumofFuncs作为最外层条件作为总的导出数量然后内层条件是NumofNames用于区分是否存在导出函数名并解析出相应信息最后统一存到一个vec中备用.
代码:

void CPE::ParseExportFunc()
{
    if (!ExistExportTable())
        return ;
    BYTE* psFromName=m_pData+Rva2Raw(m_pExportTable->Name);
    ZeroMemory(m_sExportFrom,MAX_PATH);
    _tcscpy_s(m_sExportFrom,MAX_PATH,(TCHAR*)psFromName);  //获取导出函数模块的名称
    DWORD dwFuncNum=m_pExportTable->NumberOfFunctions; //函数数目
    DWORD dwNameNum=m_pExportTable->NumberOfNames;
    DWORD* pFuncNameArray=(DWORD*)(m_pData+Rva2Raw(m_pExportTable->AddressOfNames));   //函数名称数组
    DWORD* pFuncAddArray=(DWORD*)(m_pData+Rva2Raw(m_pExportTable->AddressOfFunctions));//函数地址数组
    WORD* pFuncOrdinalArray=(WORD*)(m_pData+Rva2Raw(m_pExportTable->AddressOfNameOrdinals));//函数序号数组
    m_FuncVector.clear();
    if (0==*pFuncAddArray)
    {
        return ;
    }
    for (DWORD i=0;i<dwFuncNum;i++)
    {
        EXPORTINFO exportinfo;
        ZeroMemory(&exportinfo,sizeof(EXPORTINFO));
        BOOL ExportByName=FALSE;
        for (DWORD j=0;j<dwNameNum;j++)
        {
            if (pFuncOrdinalArray[j]==i)
            {
                ExportByName=TRUE;
                exportinfo.dwFuncNameRva=pFuncNameArray[j];
                exportinfo.dwFuncNameRaw=Rva2Raw(pFuncNameArray[j]);
                TCHAR* pFuncName=(TCHAR*)(m_pData+Rva2Raw(pFuncNameArray[j]));
                _tcscpy_s(exportinfo.sFuncName,MAX_PATH,pFuncName);
                break;
            }
        }
        if (!ExportByName)
        {
            _tcscpy_s(exportinfo.sFuncName,MAX_PATH,_T("nothing"));
        }
       exportinfo.dwFuncAddRva=pFuncAddArray;
        exportinfo.dwFuncAddRaw=Rva2Raw(pFuncAddArray);
       exportinfo.dwFuncOrdinal=i+m_pExportTable->Base;
        if(exportinfo.dwFuncAddRaw==NULL)
        continue;
        m_FuncVector.push_back(exportinfo);
    }
}

解导入表这里需要和导出表进行区别的是可能存在多个导入的Dll然后每一套对应的Dll会对应相应的一套导出函数表
还有一点就是具体一个DLL的函数表的解析。这里可以看到相关的比较核心的成员是类型同为IMAGE_THUNK_DATA的两个
OriginalFirstThunk输入名称表(INT)的RAV和FirstThunk指向输入地址表(IAT)的RAV
附件 84747

其实这两个成员不但类型相同,而且在静态PE中指向的内容页相同,这里只能说FirstThunk也就是指向输入地址表(IAT)的RAV是在操作系统进行PE装载后会显现作用,这文章重点在解析静态的PE文件故不在此处延展有兴趣可自行搜索相关文章。
下面来看指向IMAGE_THUNK_DATA的细节问题它是一个4字节联合体。
当这个双字的最高位为1时,表示函数是以序号的方式导入的;当最高位为0时,表示函数是以名称方式导入的,
这是这个双字是一个RVA,指向一个IMAGE_IMPORT_BY_NAME结构,这个结构用来指定导入函数。代码要考虑这两种情况进行适当的转换名称。
关键代码如下
代码:

void CPE::ParseImportFunc()
{
    if (ExistImportTable())
    {
       m_ImportVector.clear();
        DWORD dwImportTableNum=0;
        while (m_pImportTable->OriginalFirstThunk)
        {
            IMPORTINFO imfo;
            ZeroMemory(&imfo,sizeof(IMPORTINFO));
            imfo.FirstThunk=m_pImportTable->FirstThunk;
            imfo.OriginalFirstThunk=m_pImportTable->OriginalFirstThunk;
            imfo.ForwarderChain=m_pImportTable->ForwarderChain;
            imfo.TimeDateStamp=m_pImportTable->TimeDateStamp;
            memcpy_s(imfo.DLLName,MAX_PATH,Raw2Ptr(Rva2Raw(m_pImportTable->Name)),MAX_PATH);
            IMAGE_THUNK_DATA* pOrignThunkData=(IMAGE_THUNK_DATA*)Raw2Ptr(Rva2Raw(imfo.OriginalFirstThunk));
            IMAGE_THUNK_DATA* pFirstThunkData=(IMAGE_THUNK_DATA*)imfo.FirstThunk;
            while (*(DWORD*)pOrignThunkData)
            {
                IMPORT im;
                im.dwThunkValue=*(DWORD*)pOrignThunkData;
                im.dwThunkRva=*(DWORD*)&pFirstThunkData;
                im.dwThunkRaw=Rva2Raw(im.dwThunkRva);
                if (HIWORD(im.dwThunkValue)==0x8000)
                {
                    im.dwThunkHint = NULL;
                    int tempaddr=im.dwThunkValue&0x0000FFFF;
                    char tempstring[10];
                    itoa(tempaddr,tempstring,16);
                    _tcscpy_s(im.sName,MAX_PATH,tempstring);
                }
                else
                {
                    IMAGE_IMPORT_BY_NAME* pName=(IMAGE_IMPORT_BY_NAME*)Raw2Ptr(Rva2Raw(im.dwThunkValue));
                    im.dwThunkHint=pName->Hint;
                    _tcscpy_s(im.sName,MAX_PATH,(TCHAR*)pName->Name);

                }
                imfo.pImportInfo.push_back(im);
                pOrignThunkData++;
                pFirstThunkData++;
            }
            m_ImportVector.push_back(imfo);
            m_pImportTable++;
        }
    }
}


3. 主要成果
    
一个PE解析的软件。实现了作为一般PE文件内容解析的功能,主要有导入表导出表还有反汇编和16进制显示的功能。
4.经验与不足
    在开发的开始的过程中没有注重设计。导致了PE操作相关全放在了对话框类里实现,限制了程序的扩展性,通过分离抽象出一个PE操作类,很好的解决了多个这个问题。类似的软件可以借鉴这个思路。
在开发的过程中查看各种PE文件解析器时候发现有16进制显示功能而大部分软件都是调用的一个16edit的模块来显示相关信息。通过网上找到了DLL和相关的操作接口信息成功的运用到项目之中。还有反汇编引擎源作者也是OD的。实现一些功能的时候看看前人走过的路会很好的节省时间。:)
这里还有一些没有实现的功能例如资源和重定向相关的展示。一方面资源如果不能解析还有重定向一般没有展示的必要。另一方面自己对相关理解不够深入。做了一些显示达不到预想的目标。如果更新版本会对其进行完善。

上传的图像
文件类型: png 1.png (33.4 KB)
文件类型: png 2.png (158.9 KB)
文件类型: png 3.png (155.3 KB)
上传的附件
文件类型: rar PE_D_1201.rar (437.1 KB)

Viewing all articles
Browse latest Browse all 9556

Trending Articles



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