【原创&交流】PE文件格式的一些研究,该如何解决

【原创&交流】PE文件格式的一些研究
链接:PE文件格式的一些研究

  最近抽空对PE文件格式做了一些研究。众所周知,PE文件格式是Windows平台下可执行文件的格式。为什么要研究PE文件格式?可能有人认为,做这件事就是一件重复造轮子的事,因为之前已经有无数人做过这样的事。但是有些事不是简单地以是不是重复造轮子来衡量的。研究PE文件格式对加深程序本质的认识和理解程序的构成都有很大的好处。美籍匈牙利科学家冯•诺依曼最新提出程序存储的思想,具体到研究PE文件格式,或许可以是运行程序所需的指令和数据是以怎样的组织结构存贮在文件的,在运行时程序是怎样被加载的,数据是怎样初始化的。

  一个完整的PE文件结构一般由五大部分组成。如下图:



   
  最开头的是部分是DOS部首,DOS部首由两部分组成:DOS的MZ文件标志和DOS stub(DOS存根程序)。之所以设置DOS部首是微软为了兼容原有的DOS系统下的程序而设立的。
  紧接着的是真正的PE文件头。值得注意的是PE文件头中的IMAGE_OPTIONAL_HEADER32是一个非常重要的结构,PE文件中的导入表、导出表、资源、重定位表等数据的位置和长度都保存在这个结构里。

  PE文件结构中可研究的内容很多,暂时讲解这么多,有兴趣的朋友可以阅读《Windows图形编程》中的第一章和《程序员的自我修养》的5.6节。

  研究PE文件格式,最好的方法我觉得还是自己动手写一个PE文件解释类。看了《Windows图形编程》中的第一章,加深自己对PE文件结构的理解,我决定在袁峰大侠(《Windows图形编程》一书作者)编写PE文件解释类KPEFile的基础上增加一些接口。KPEFile类的构造函数是通过提供模块句柄来获取PE文件信息的,我发现这对运行中的程序是有用的,但对解析一个静态的exe文件并不有效。

  编程实现分析一个PE文件,网上一般有两种做法:一是打开exe文件,然后利用Dbghelp库的一个函数ImageRvaToVa来获取你要打开结构的指针;另一种做法也大同小异,也是打开exe文件,自己计算所有获取信息的结构的偏移地址。我采用的是第二种做法。

  下面是重要部分代码:

C/C++ code


/*! 
* @brief tString类主要是为了兼容unicode字符集和多字节字符集
*
*   
*/

#ifdef UNICODE

#define tString  std::wstring

#else

#define tString  std::string 

#endif

/*! @struct stImportDll
* @brief 导入的DLL的信息结构体
*
*   
*/
struct stImportDll
{
    /* @brief 导入的DLL名
    *
    *   
    */
    tString m_strDllName; 
    /* @brief 导入的DLL中的函数
    *
    *   
    */
    std::vector<tString> m_vecStrFunname; //
};

/*!
*  @brief KPEFile类构造函数
*
*  @param [in]strBinPath exe文件全路径,主要是获取DOS部首和PE文件头的位置
*  \return 
*/
KPEFile::KPEFile( tString strBinPath )
{
    DWORD dwRead = 0;
    m_pModule = NULL;

    // 打开exe文件
    HANDLE hFile = CreateFile(
        strBinPath.c_str(), //PE文件名
        GENERIC_READ, 
        FILE_SHARE_READ,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);

    if(hFile == INVALID_HANDLE_VALUE)
    {
        return;
    }

    // 获取文件大小
    DWORD dwSize = GetFileSize(hFile,NULL);
    if (dwSize == 0xFFFFFFFF){
        return ;
    }

    // 开辟读取缓冲区
    m_pModule = (LPBYTE)VirtualAlloc( NULL,dwSize,MEM_COMMIT,PAGE_READWRITE);

    // 读取文件
    ReadFile(hFile,m_pModule,dwSize,&dwRead,NULL);
    if( dwRead != dwSize)
        return ;
    // 关闭文件
    CloseHandle(hFile);

    // 获取DOS部首和PE文件头的位置
    m_pDosHeader = (PIMAGE_DOS_HEADER)m_pModule;
    m_pNTHeader = (PIMAGE_NT_HEADERS)(m_pModule + m_pDosHeader->e_lfanew);
}

/*!
*  @brief 由虚拟地址获取偏移距离
*
*  @param [in]VirtualAddr 虚拟地址
*  \return 
*/
DWORD KPEFile::VirtualToRaw(DWORD VirtualAddr)
{
    int i;
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((LPBYTE)m_pNTHeader + sizeof(IMAGE_NT_HEADERS));
    for( i = 0; i < m_pNTHeader->FileHeader.NumberOfSections; i++)
    {
        if (VirtualAddr >= pSectionHeader->VirtualAddress 
            && VirtualAddr < pSectionHeader->VirtualAddress + pSectionHeader->Misc.VirtualSize)
            return pSectionHeader->PointerToRawData + (DWORD)VirtualAddr - (DWORD)pSectionHeader->VirtualAddress;
        pSectionHeader ++;
    }
    return 0;
}

/*!
*  @brief 获取所有的导入表
*
*  @param [in]vecImportDll 导入的DLL的信息结构体
*  \return 
*/
BOOL KPEFile::GetAllImportSymbol(std::vector<stImportDll> &vecImportDll)
{
    if((m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress==0)
        ||(m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Size==0))
        return FALSE;

    DWORD ITableRAoffset = VirtualToRaw(m_pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(m_pModule+ITableRAoffset);

    if (pImport==NULL)
        return FALSE;

    DWORD Nameoffset ;    
    DWORD OFToffset ;
    DWORD Funcoffset;
    while((Nameoffset = VirtualToRaw(pImport->Name))!= 0)
    {
        stImportDll ImportDll;
        LPCSTR szDllName = reinterpret_cast<LPCSTR>(m_pModule + Nameoffset);

#ifdef UNICODE
        TCHAR szwszDllName[MAX_PATH];
        ::MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,szDllName,-1,szwszDllName,MAX_PATH);

        ImportDll.m_strDllName = szwszDllName;
#else

        ImportDll.m_strDllName = szDllName;

#endif

        PIMAGE_THUNK_DATA32 pThunk;
        OFToffset = VirtualToRaw(pImport->OriginalFirstThunk);
        pThunk = (PIMAGE_THUNK_DATA)(m_pModule + OFToffset);

        for (int i = 0;pThunk->u1.Function;i++)
        {
            if (pThunk->u1.Ordinal&IMAGE_ORDINAL_FLAG32)
         {
             // 按序号导入不予处理
             break;
         }
            else
         {

             PIMAGE_IMPORT_BY_NAME pFuncName;
             Funcoffset = VirtualToRaw(pThunk->u1.AddressOfData);
             pFuncName = (PIMAGE_IMPORT_BY_NAME)(m_pModule + Funcoffset);

#ifdef UNICODE
             TCHAR szwFunctionName[MAX_PATH];
             ::MultiByteToWideChar(CP_ACP,MB_PRECOMPOSED,reinterpret_cast<LPCSTR>(pFuncName->Name),-1,szwFunctionName,MAX_PATH);
             tString strFuncname = szwFunctionName;
             ImportDll.m_vecStrFunname.push_back(strFuncname);

#else
             tString strFuncname = reinterpret_cast<LPCSTR>(pFuncName->Name); 
             ImportDll.m_vecStrFunname.push_back(strFuncname);
#endif
         }
            pThunk++;
        }


        vecImportDll.push_back(ImportDll);
        pImport++;
    }
    return TRUE;
}