«

PE文件详解 - 导出表

ljierui 发布于 阅读:72 技术杂谈


1、导出表

1.1、IMAGE_EXPORT_DIRECTORY结构

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics; // 保留,恒为0x0000000
    DWORD   TimeDateStamp; // 时间戳
    WORD    MajorVersion; // 主版本号,一般不赋值
    WORD    MinorVersion; // 子版本号, 一般不赋值
    DWORD   Name; // 模块名称
    DWORD   Base; // 索引基数
    DWORD   NumberOfFunctions; // 导出地址表中的成员个数
    DWORD   NumberOfNames; // 导出名称表中的成员个数
    DWORD   AddressOfFunctions;     // 导出地址表(EAT)
    DWORD   AddressOfNames;         // 导出名称表(ENT)
    DWORD   AddressOfNameOrdinals;  // 指向导出序列号数组
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

1.2、通过代码获取导出表

#include<stdio.h>
#include<Windows.h>
#include "Entry.h"

int main()
{
    FILE* pFile = NULL;
    char* buffer;
    int nFileLength = 0;
    errno_t err = fopen_s(&pFile, "E:\\C_code\\MyDemo\\Debug\\MyDemo.exe", "rb");
    if (err != 0)
    {
        // 处理文件打开错误
        printf("Failed to open file.\n");
        return 1;
    }
    // fseek()设置文件指针的位置。这里将文件指针移动到文件末尾
    // SEEK_END 指定偏移量为0
    fseek(pFile, 0, SEEK_END);
    // ftell() 获取文件指针的当前位置,也就是文件的长度
    nFileLength = ftell(pFile);
    // rewind() 将文件指针重置到文件开头
    rewind(pFile);
    // 存储缓冲区大小
    // 计算方式:文件长度乘以sizeof(char)(sizeof(char)表示char类型的大小,通常是1字节),再加1字节用于存储字符串结束符\0
    int imageLength = nFileLength * sizeof(char) + 1;
    // 动态分配内存 : malloc函数动态分配了一块内存,大小为imageLength字节
    buffer = (char*)malloc(imageLength);
    // memset() :将分配的内存初始化为0。
    memset(buffer, 0, nFileLength * sizeof(char) + 1);
    // fread函数用于从文件中读取数据。这里将文件中的内容读取到之前分配的缓冲区buffer中,每次读取1字节,总共读取imageLength个字节。
    fread(buffer, 1, imageLength, pFile);

    // 读取DOS头
    // 定义指向 PIMAGE_DOS_HEADER的指针 ReadDosHeader
    PIMAGE_DOS_HEADER ReadDosHeader;
    // 因为buffer是char类型,所以要转成指针类型
    // 目的是将缓冲区中的数据解释为IMAGE_DOS_HEADER结构
    ReadDosHeader = (PIMAGE_DOS_HEADER)buffer;

    // 打印指针中的数据
    printf("MS-DOS Info:\n");
    printf("MZ标志位:%x\n", ReadDosHeader->e_magic);
    printf("PE头位置:%x\n", ReadDosHeader->e_lfanew);

    // PE头
    // 要注意你读取的EXE是32位还是64位的,64位要使用 PIMAGE_NT_HEADERS64
    printf("PE Header Info:\n");
    PIMAGE_NT_HEADERS ReadNTHeaders;
    // 因为要跳过DOS头,读取到PE开头,所以要加e_lfanew
    ReadNTHeaders = (PIMAGE_NT_HEADERS)(buffer + ReadDosHeader->e_lfanew);
    printf("PE标志位:%x\n", ReadNTHeaders->Signature);
    // 打印标准PE头里的东西
    printf("运行平台:%x\n", ReadNTHeaders->FileHeader.Machine);
    // 打印拓展PE头里的东西
    printf("ImageBase: %x\n",ReadNTHeaders->OptionalHeader.ImageBase);

    // 区段解析遍历
    printf("Section Header Info:\n");
    // Windows定义了宏来解析区段,使用IMAGE_FIRST_SECTION 
    // 定位到区段表
    PIMAGE_SECTION_HEADER ReadSectionHeader = IMAGE_FIRST_SECTION(ReadNTHeaders);
    // 在标准PE头中有个字段可以遍历区段的数量,在 PIMAGE_FILE_HEADER中的NumberOfSections字段
    PIMAGE_FILE_HEADER pFileHeader = &ReadNTHeaders->FileHeader;
    for (int i = 0; i < pFileHeader->NumberOfSections; i++)
    {
        printf("Name(区段名称):%s\n", ReadSectionHeader[i].Name);
        printf("VOffset(起始相对虚拟地址):%08X\n", ReadSectionHeader[i].VirtualAddress);
        printf("VSize(区段大小):%08X\n", ReadSectionHeader[i].SizeOfRawData);
        printf("ROffset(文件偏移):%08X\n", ReadSectionHeader[i].PointerToRawData);
        printf("RSize(文件中区段大小):%08X\n", ReadSectionHeader[i].Misc.VirtualSize);
        printf("标记(区段的属性):%08X\n\n", ReadSectionHeader[i].Characteristics);
    }

    printf("=================================================\n");
    // 获取导出表
    ImportTable(buffer);
    printf("=================================================\n");
    // 获取导入表
    ExportTable(buffer);

    // 释放动态内存
    free(buffer);
    // 关闭文件
    fclose(pFile);
    return 0;

}

// dwRva 是某个数据目录表的起始位置
// buffer 读取到的PE文件缓冲区
DWORD RvaToOffset(DWORD dwRva, char* buffer)
{
    // 从Dos头解析
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
    // PE头
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
    // 区段表
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
    //判断是否落在头部当中
    if (dwRva < pSection[0].VirtualAddress)
    {
        return dwRva;
    }
    // VirtualAddress 起始地址
    // Size 长度
    // VirtualAddress + Size = 结束地址
    // 判断是否落在某个区段内
    for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
    {
        if (dwRva >= pSection[i].VirtualAddress && dwRva <= pSection[i].VirtualAddress+pSection[i].Misc.VirtualSize)
        {
            // dwRva - pSection[i].VirtualAddress 是数据目录表起始地址到区段起始地址的偏移(OFFSET)
            // pSection[i].PointerToRawData 区段到文件头的偏移
            // 这一串返回的是数据目录表起始地址到文件头的偏移
            return dwRva - pSection[i].VirtualAddress + pSection[i].PointerToRawData;
        }
    }
}

// 解析导入表
void ImportTable(char* buffer)
{
    // 从DOS头开始解析
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
    // PE头
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
    // 定位导入表
    PIMAGE_DATA_DIRECTORY pImportDir = (PIMAGE_DATA_DIRECTORY)(pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_IMPORT);
    // 填充结构
    PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToOffset(pImportDir->VirtualAddress,buffer)+buffer);
    // 判断导入表的动态链接库是否遍历完
    while (pImport->Name != NULL)
    {
        // 获取名字,因为NAME是一个相对虚拟地址,所以要计算
        char *szDllName = (char *)(RvaToOffset(pImport->Name, buffer) + buffer);
        printf("DLL名称:%s\n",szDllName);
        printf("日期时间标志:%08X\n",pImport->TimeDateStamp);
        printf("ForwarderChain:%08X\n",pImport->ForwarderChain);
        printf("名称OFFSET:%08X\n",pImport->Name);
        printf("FirstThunk:%08X\n",pImport->FirstThunk);
        printf("OriginalFirstThunk:%08X\n\n",pImport->OriginalFirstThunk);

        // 解析里面的函数
        // 指向导入地址表的RVA 通过OriginalFirstThunk字段来计算偏移
        PIMAGE_THUNK_DATA pIat = (PIMAGE_THUNK_DATA)(RvaToOffset(pImport->OriginalFirstThunk, buffer) + buffer);
        DWORD index = 0;
        DWORD ImportOffset = 0;
        // 被导入函数的序号
        while (pIat->u1.Ordinal != 0)
        {
            printf("ThunkRva:%08X\n",pImport->OriginalFirstThunk+index);
            ImportOffset = RvaToOffset(pImport->OriginalFirstThunk,buffer);
            printf("ThunkOffset:%08X\n", ImportOffset + index);
            index += 4;
            if ((pIat->u1.Ordinal & 0x80000000) != 1)
            {
                PIMAGE_IMPORT_BY_NAME pName = (PIMAGE_IMPORT_BY_NAME)(RvaToOffset(pIat->u1.AddressOfData,buffer)+buffer);
                printf("API名称:%s\n",pName->Name);
                // 序号
                printf("Hint:%04X\n",pName->Hint);
                // 被导入函数地址 pIat->u1.Function
                printf("ThunkValue:%08X\n\n",pIat->u1.Function);
            }
            // 指向下一个
            pIat++;
        }
        // 判断完后,跳到下个结构
        pImport++;
    }
}

void ExportTable(char* buffer)
{
    // 从DOS头开始解析
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)buffer;
    // PE头
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + buffer);
    // 定位数据库目录中的导出表
    PIMAGE_DATA_DIRECTORY pExportDir = pNt->OptionalHeader.DataDirectory + IMAGE_DIRECTORY_ENTRY_EXPORT;
    // 填充导出表 返回的是文件头到导出表的偏移
    PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(RvaToOffset(pExportDir->VirtualAddress, buffer) + buffer);
    // 计算DLL名字
    char* szName = (char*)(RvaToOffset(pExport->Name,buffer),buffer);
    // 判断
    if (pExport->AddressOfFunctions == 0)
    {
        printf("当前没有导出表\n");
        return;
    }
    printf("导出表OFFSET:%08X\n",RvaToOffset(pExportDir->VirtualAddress,buffer));
    printf("特征值:%08X\n",pExport->Characteristics);
    printf("基数:%08X\n",pExport->Base);
    printf("名称OFFSET:%08X\n",pExport->Name);
    printf("名称:%s\n",szName);
    printf("函数数量:%08X\n",pExport->NumberOfFunctions);
    printf("函数名数量:%08X\n",pExport->NumberOfNames);
    printf("函数地址:%08X\n",pExport->AddressOfFunctions);
    printf("函数名称地址:%08X\n",pExport->AddressOfNames);
    printf("函数名称序号地址:%08X\n",pExport->AddressOfNameOrdinals);

    // 解析里面的值
    // 函数地址数量
    DWORD dwNumOfFun = pExport->NumberOfFunctions;
    // 函数名数量
    DWORD dwNumOfNames = pExport->NumberOfNames;
    // 基数
    DWORD dwBase = pExport->Base;
    // 导出地址表
    PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfFunctions,buffer)+buffer);
    // 导出名称表
    PDWORD pEat32 = (PDWORD)(RvaToOffset(pExport->AddressOfNames, buffer) + buffer);
    // 导出序号表
    PWORD pId = (PWORD)(RvaToOffset(pExport->AddressOfNameOrdinals, buffer) + buffer);

    for (DWORD i = 0; i < dwNumOfFun; i++)
    {
        if (pEat32[i] == 0)
        {
            continue;
        }
        // 查找函数名是否存在
        DWORD Id = 0;
        for (; Id < dwNumOfNames; Id++)
        {
            if (pId[Id] == i)
            {
                break;
            }

        }
        if (Id == dwNumOfNames)
        {
            printf("Id:%x Address:0x%08X Name[NULL]\n",i+dwBase,pEat32[i]);
        }
        else
        {
            char* szFunName = (char*)(RvaToOffset(pEat32[Id],buffer)+buffer);
            printf("Id:%x Address:0x%08X Name[%s]\n", i + dwBase, pEat32[i],szFunName);
        }
    }
}

PE文件

推荐阅读: