首页
论坛
专栏
课程

[调试逆向] [原创]PE格式学习之导出表

2019-3-20 15:37 1383

[调试逆向] [原创]PE格式学习之导出表

2019-3-20 15:37
1383

最近几天在温习PE格式,特此发帖纪录一下自己的学习过程,感谢论坛提供的学习环境和氛围。

 

PE即Portable Executable:32位或64位Windows操作系统使用的可执行程序或者动态链接库的文件格式。所以我首先贴出微软官方关于PE格式(导出表)的在线说明文档的链接:https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#the-edata-section-image-only,英文水平好的可以直接点击链接略过本帖。

导出表的结构如下:
// Export Format
//
typedef struct _IMAGE_EXPORT_DIRECTORY 
{
    DWORD   Characteristics;        // 保留,必须为0
    DWORD   TimeDateStamp;          // 时间戳
    WORD    MajorVersion;           // 主要版本号,主要和次要版本号可由用户设置
    WORD    MinorVersion;           // 次要版本号
    DWORD   Name;                    // RVA,包含导出文件名称的ASCII字符串的地址
    DWORD   Base;                    // 此映像中导出的起始序号,指定导出地址表(AddressOfFunctions)的起始序号,通常设置为1
    DWORD   NumberOfFunctions;        // 导出函数的个数,导出地址表(AddressOfFunctions)中的条目数
    DWORD   NumberOfNames;            // 按名称导出的函数的个数,名称表(AddressOfNames)中的条目数。这也是序数表(AddressOfNameOrdinals)中的条目数
    DWORD   AddressOfFunctions;     // RVA from base of image
    DWORD   AddressOfNames;         // RVA from base of image
    DWORD   AddressOfNameOrdinals;  // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

关于前4个字段在这里就不再赘述,我们主要看一下后面的7个字段,在这7个字段中其中有4个是RVA,我们在解析时需要先将RVA转换为FOA,我的转换思路如下:
根据PE文件加载时的特性:PE文件头直接放入内存空间,不需要进行拉伸;节则根据节表的VirtualAddress进行拉伸,所以在进行转换时,只需要分别判断即可。

DWORD RvaToFoa(BYTE* pFileBuff,DWORD dwRva)
{
    DWORD dwSizeOfHeaders = 0;        //PE文件头的大小
    //定位DOS头
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pFileBuff;
    //定位NT头
    PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(pFileBuff+pDosHeader->e_lfanew);
    //定位可选PE头(PE32与PE32+结构成员不同,需按照格式解析)
    if (pNtHeader->OptionalHeader.Magic == 0x20B)
    {
        PIMAGE_OPTIONAL_HEADER64 pOptionalHeader64 = (PIMAGE_OPTIONAL_HEADER64)(&pNtHeader->OptionalHeader);
        //PE头大小赋值
        dwSizeOfHeaders = pOptionalHeader64->SizeOfHeaders;
    }
    else
    {
        PIMAGE_OPTIONAL_HEADER32 pOptionalHeader32 = (PIMAGE_OPTIONAL_HEADER32)(&pNtHeader->OptionalHeader);
        //PE头大小赋值
        dwSizeOfHeaders = pOptionalHeader32->SizeOfHeaders;
    }

    //定位节表
    PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((BYTE*)&pNtHeader->FileHeader+IMAGE_SIZEOF_FILE_HEADER+pNtHeader->FileHeader.SizeOfOptionalHeader);    
    //如果RVA在PE文件头范围内(PE头不需要拉伸),此时的RVA即为FOA
    if (dwRva < dwSizeOfHeaders)
    {
        return dwRva;
    }
    else
    {
        //循环判断RVA在哪个节表内
        for (DWORD i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++)
        {
            if ( (pSectionHeader[i].VirtualAddress <= dwRva) && (dwRva < (pSectionHeader[i].VirtualAddress + pSectionHeader[i].Misc.VirtualSize) ))
            {
                //在某个节中,终止循环
                break;
            }
        }
        //找到RVA相对该节表的偏移,偏移+该节表在文件内的起始地址则为FOA
        return dwRva - pSectionHeader[i].VirtualAddress + pSectionHeader[i].PointerToRawData;
    }
}

导出表并不是DLL文件的专属,一个EXE文件也可以拥有导出表,可以看一下我们最常用的OD,我下面的测试例子是用的DLL。现在我们来着重说明以下几个字段:

  1. NumberOfFunctions :顾名思义,是所有导出函数的个数,但是这个值并不一定准确。我们都知道一个函数可以按照函数名的方式导出,也可以按照序号的方式导出,如果DLL的导出函数是按函数名的方式或按连续的序号的方式导出的,那么这个值就是准确的。如果DLL的导出函数按照序号的方式导出且导出序号不连续时,那么这个值就是不准确的,它的计算方式很暴力:最大的导出序号-最小的导出序号+1,我们可以测试一下:
.def文件内容
EXPORTS    

Plus       @12
Sub     @15 NONAME
Mul        @13
Div        @20


2.AddressOfFunctions:导出函数的RVA数组。这些是可执行代码和数据部分中导出的函数和数据的实际地址。通过NumberOfFunctions的说明我们知道函数地址表中的数据并不一定都是有效的,如果函数地址表的某一项内容为0x00000000,即为无效项
解析未按连续的序号导出的DLL文件,测试函数地址表的内容
解析未按连续的序号导出的DLL文件,调试查看函数地址表的内容
3.AddressOfNames
指向导出名称的指针数组(RVA),数组是一系列以null结尾的ASCII字符串,按升序排序。
4.AddressOfNameOrdinals:与名称指针表的成员(即导出名称的指针数组)相对应的序号数组。它们的关系是一一对应的;因此,名称指针表和序号表必须具有相同数量的成员。导出序号表是导出地址表中的16位索引数组(即每个序号都是导出地址表的索引),必须从序号中减去序数基数(即Base),以获得导出地址表中的真实索引。

 

相信讲到这里大家在控制台解析并打印出一个文件的导出表的内容应该不会有什么难度了,但是你会发现你的解析和其他工具解析的显示内容不一样,那是因为解析的过程并没有按照函数地址表和函数名称表及函数序号表的工作方式,这三张表才是整个导出表的难点和精髓,想了解导出表的工作原理,我们应该想到的是:为什么微软要设计3张表,我们先来看一下比较常用的函数GetProcAddress,函数原型如下

FARPROC GetProcAddress(
  HMODULE hModule,    // handle to DLL module
  LPCSTR lpProcName   // function name
);

我们都知道想要获取一个模块中的函数的地址,参数2——lpProcName可以是名称,也可以是函数的序号。设计三张表的原因归根结底就是函数导出是可以按照函数名的方式导出,也可以按照序号的方式导出。

 

它们的工作原理如下:
当另一个映像文件按名称导入函数时,Win32加载程序在导入名称表(AddressOfNames)中搜索匹配的字符串。如果找到匹配的字符串,则通过在序号表(AddressOfNameOrdinals)中查找相应的成员来识别关联的序号。查找到的序号是导出地址表(AddressOfFunctions)的索引,它提供了所需函数的实际位置。

 

当另一个映像文件按序号导入函数时,不必在名称指针表中搜索匹配的字符串。直接在序号表(AddressOfNameOrdinals)中查找相应的成员来识别关联的序号,查找到的序号是导出地址表(AddressOfFunctions)的索引,它提供了所需函数的实际位置。

 

附上我的部分解析代码:

//定位导出表的位置
PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)(m_pFileBuff + RvaToFoa(m_pFileBuff,m_dwOffsetToExport));
//函数序号表 FOA
DWORD dwAddressOfNameOrdinals = RvaToFoa(m_pFileBuff,pExportTable->AddressOfNameOrdinals);
//函数名称表 FOA
DWORD dwAddressOfNames = RvaToFoa(m_pFileBuff,pExportTable->AddressOfNames);
//函数地址表 FOA
DWORD dwAddressOfFunctions = RvaToFoa(m_pFileBuff,pExportTable->AddressOfFunctions);
//函数序号表 
PWORD pAddressOfNameOrdinals = (PWORD)(m_pFileBuff + dwAddressOfNameOrdinals);
//函数名称表(仍为RVA)
PDWORD pAddressOfNames = (PDWORD)(m_pFileBuff + dwAddressOfNames);
//函数地址表
PDWORD pAddressOfFunctions = (PDWORD)(m_pFileBuff + dwAddressOfFunctions);
/****************************************************************************************************/
DWORD dwOrdinalsIndex = 0;        //函数名称序号表的索引
DWORD dwListItemIndex = 0;        //ListCtrl控件的Item的索引
DWORD dwNameStringFOA = 0;        //函数名称的FOA
//根据导出函数的数量,分析出有效的导出函数并显示(包括以序号导出的函数和以名称导出的函数)
for (WORD wFunctionIndex = 0; wFunctionIndex < pExportTable->NumberOfFunctions; wFunctionIndex++)
{
    if (pAddressOfFunctions[wFunctionIndex])    //判断如果函数地址表中的地址不为空,则为有效的导出函数
    {
        //函数序号:Base + 索引值
        strTemp.Format(_T("%04X"), pExportTable->Base + wFunctionIndex); 
        m_ExportTable_ListCtrl.InsertItem(dwListItemIndex, strTemp);
        for (dwOrdinalsIndex = 0; dwOrdinalsIndex < pExportTable->NumberOfNames; dwOrdinalsIndex++)
        {
            //在函数名称序号表中查找与函数地址表的索引值相等的项,能找到则说明函数是以函数名的方式导出的
            if (pAddressOfNameOrdinals[dwOrdinalsIndex] == wFunctionIndex)
            {
                //函数名称序号表
                strTemp.Format(_T("%04X"),pAddressOfNameOrdinals[dwOrdinalsIndex]); 
                m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 1, strTemp);
                //函数地址表
                strTemp.Format(_T("%08X"),pAddressOfFunctions[wFunctionIndex]); 
                m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 2, strTemp);
                //函数名称表
                strTemp.Format(_T("%08X"),pAddressOfNames[dwOrdinalsIndex]); 
                m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 3, strTemp);
                //函数名称
                dwNameStringFOA = RvaToFoa(m_pFileBuff,pAddressOfNames[dwOrdinalsIndex]);
                strTemp = m_pFileBuff+dwNameStringFOA;
                m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 4, strTemp);
                //成功找到,则终止循环
                break;;
            }
        }
        //如果该表达式的值为真,说明该函数是以序号的方式导出的
        if (dwOrdinalsIndex == pExportTable->NumberOfNames)
        {    
            //无函数名称序号表
            strTemp = _T("-"); 
            m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 1, strTemp);
            //函数地址表
            strTemp.Format(_T("%08X"),pAddressOfFunctions[wFunctionIndex]); 
            m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 2, strTemp);
            //无函数名称表和函数名
            strTemp = _T("-"); 
            m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 3, strTemp);
            m_ExportTable_ListCtrl.SetItemText(dwListItemIndex, 4, strTemp);
        }
        dwListItemIndex++;
    }
}

解析效果如下(界面模仿的LordPE):

 

映像文件(image file):可执行文件是指.EXE文件或DLL。 映像文件可以被认为是“内存映像”。通常使用术语“映像文件”而不是“可执行文件”,因为后者有时被认为仅表示.EXE文件。

 

如有错误,请大家指正!



[招生]科锐逆向工程师培训(3月6日远程教学班首开特惠, 第37期) !

最后于 2019-3-20 15:42 被Sanie编辑 ,原因:
上传的附件:
最新回复 (0)
游客
登录 | 注册 方可回帖
返回