PE文件结构解析2

发布者:SecIN
发布于:2022-05-23 16:06

0x0导读

上一篇文章把Dos头,Nt头,可选头里的一些成员说过了(文章链接:[PE文件结构解析1-SecIN (sec-in.com)]),今天主要讲的内容是,Rva(内存偏移)转换Foa(文件偏移),数据目录表,节表,话不多说来看文章。

0x1环境

编译器:VirsualStudio2022

16进制查看工具:winhex

0x2Rva与Foa转换

rva到foa的意义:因为pe文件在文件中和在内存中的大小是不一样的,在内存中,pe文件会被拉伸,所以同一个地址在内存中和文件中所指向的值是不一样的,所以要把内存中的偏移转换成文件中的偏移,下面我们来看一下转换的函数。

函数定义

DWORD rtf(PBYTE buffer, DWORD rva)
{
PIMAGE_DOS_HEADER doshd = (PIMAGE_DOS_HEADER)buffer;
PIMAGE_NT_HEADERS nthd = (PIMAGE_NT_HEADERS)(buffer + doshd->e_lfanew);
PIMAGE_FILE_HEADER filehd = (PIMAGE_FILE_HEADER)(buffer + doshd->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 optionhd = (PIMAGE_OPTIONAL_HEADER32)(buffer + doshd->e_lfanew + 24);
PIMAGE_SECTION_HEADER sectionhd = IMAGE_FIRST_SECTION(nthd);
for (int i = 0; i < filehd->NumberOfSections; i++)
{
if (rva >= sectionhd[i].VirtualAddress && rva <= sectionhd[i].VirtualAddress + sectionhd[i].SizeOfRawData)
{
return rva - sectionhd[i].VirtualAddress + sectionhd[i].PointerToRawData;
}
}
}

转换函数代码解析:首先为这个函数定义了两个参数,一个参数是基址用于定位各种头,和节表,另一个参数是要转换的rva,PIMAGE开头的代码主要是定位头和节表,为下面循环判断做准备,rva是在节中的,所以只需要循环判断,如果rva>=当前节的VirtualAddress又小于VirtualAddress+SizeOfRawData就可以判断出这个rva在当前节,只需要减去VirtualAddress再加上PointerToRawData即可。

0x3数据目录解析

数据目录是可选头的一个成员(对可选头有疑问的可以看上一篇文章),这个成员是一个结构体类型的,像这个样的成员一共有16个,也就是说数据目录表其实就是16个这样的结构体,结构体定义如下。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;//一个rva,指向真正的数据表
    DWORD   Size;//数据表大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

这16个结构体对应的表分别是导出表,导入表,资源表,异常处理表,安全表,重定位表,调试表,版权表,指针目录,TLS表,载入配置表,绑定输入表,导入地址表,延迟载入表,COM信息,保留最后一个表是保留起来的用不到。

结构体中的VirtualAddress成员是一个rva,通过这个rva可以找到真正的表所在的地方,我们可以把VirtualAddress中的值看作一个"中间人",可以通过这个"中间人"来找到真正的数据表。这里要注意时16个这样的结构。

wKg0C2I9fU6AY6udAABThbXhqKw341.png

Size表示当前结构体的VirtualAddress所指向表的大小。

代码解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\blue\\Desktop\\Dll1.dll"

void main()
{
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(size);
memset(ptr, 0, size);
fread(ptr, size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);
PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;
for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);
}
getchar();
}

前三行代码主要是包含头文件,定义宏作为fopen函数的参数。

FILE* fp = fopen(path, "rb");定义一个文件指针来接受fopen函数返回值,fopen第一个参数是要打开文件的路径,第二个参数是以什么方式打开,这里是rb也就是以二进制方式打开一个文件,只能读不可以写。

fseek(fp, 0, SEEK_END);第一个参数是要设置文件的文件指针,第二个参数是一个相对于第三个参数是一个偏移量,第三个参数SEEK_END代表文件的末尾,代码大致意思文件流重定向到文件末尾。

int size = ftell(fp);定义一个变量用来接收ftell函数的返回值,ftell函数作用是计算文件的大小,第一个参数是要计算那个文件的文件指针。

rewind(fp);将文件流重定向到文件开头,为下面读取数据做准备。

PBYTE ptr = (PBYTE)malloc(size);定义一个PBYTE类型的指针指向malloc函数申请的内存,malloc函数第一个参数是申请多大的内存,PBYTE就是char*

memset(ptr, 0, size);用0填充刚才申请的内存块,第一个参数内存块的地址,第二个参数用什么填充,第三个参数填充多大。

fread(ptr, size, 1, fp);用于读取数据到内存,第一个参数是要读到哪里,第二个参数是读多少字节,第三个参数读多少次,第四个参数要读取文件的文件指针。

PIMAGE_DOS_HEADER结构体定义

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;                     // Magic number
    WORD   e_cblp;                      // Bytes on last page of file
    WORD   e_cp;                        // Pages in file
    WORD   e_crlc;                      // Relocations
    WORD   e_cparhdr;                   // Size of header in paragraphs
    WORD   e_minalloc;                  // Minimum extra paragraphs needed
    WORD   e_maxalloc;                  // Maximum extra paragraphs needed
    WORD   e_ss;                        // Initial (relative) SS value
    WORD   e_sp;                        // Initial SP value
    WORD   e_csum;                      // Checksum
    WORD   e_ip;                        // Initial IP value
    WORD   e_cs;                        // Initial (relative) CS value
    WORD   e_lfarlc;                    // File address of relocation table
    WORD   e_ovno;                      // Overlay number
    WORD   e_res[4];                    // Reserved words
    WORD   e_oemid;                     // OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;                   // OEM information; e_oemid specific
    WORD   e_res2[10];                  // Reserved words
    LONG   e_lfanew;                    // File address of new exe header
  } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;定义一个PIMAGE_DOS_HEADER类型的结构体指针来指向刚才申请的那块内存,也可以说用这块内存中的数据来填充这个结构体指针所指向的结构体,因为ptr是PBYTE类型和PIMAGE_DOS_HEADER类型不同所以要把ptr从PBYTE强转成PIMAGE_DOS_HEADER类型。

typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);定义一个PIMAGE_NT_HEADERS32类型的结构体指针在·通过基址+偏移的方式定位到Nt头,也就是ptr与Dos头的e_lfanew成员相加得到一个地址,这个地址就是Nt头开始的地方,也可理解为用这个地址中的数据填充这个结构体指针指向的结构体。

PIMAGE_FILE_HEADER结构体定义

typedef struct _IMAGE_FILE_HEADER {
    WORD    Machine;
    WORD    NumberOfSections;
    DWORD   TimeDateStamp;
    DWORD   PointerToSymbolTable;
    DWORD   NumberOfSymbols;
    WORD    SizeOfOptionalHeader;
    WORD    Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);定义一个结构体指针(PIMAGE_NT_HEADERS32类型),在用基址+Dos头的e_lfanew成员定位到nt头,跟据nt头的结构体定义可以知道Signature成员后面就是File头而Signature成员大小是四字节,所以加4定位到File头,所以是ptr + Dos->e_lfanew + 4运算结果就是File头开始的地址,再用这个结构体指针指向这个地址即可。

PIMAGE_OPTIONAL_HEADER32;结构体定义

typedef struct _IMAGE_OPTIONAL_HEADER {
    //
    // Standard fields.
    //

    WORD    Magic;
    BYTE    MajorLinkerVersion;
    BYTE    MinorLinkerVersion;
    DWORD   SizeOfCode;
    DWORD   SizeOfInitializedData;
    DWORD   SizeOfUninitializedData;
    DWORD   AddressOfEntryPoint;
    DWORD   BaseOfCode;
    DWORD   BaseOfData;

    //
    // NT additional fields.
    //

    DWORD   ImageBase;
    DWORD   SectionAlignment;
    DWORD   FileAlignment;
    WORD    MajorOperatingSystemVersion;
    WORD    MinorOperatingSystemVersion;
    WORD    MajorImageVersion;
    WORD    MinorImageVersion;
    WORD    MajorSubsystemVersion;
    WORD    MinorSubsystemVersion;
    DWORD   Win32VersionValue;
    DWORD   SizeOfImage;
    DWORD   SizeOfHeaders;
    DWORD   CheckSum;
    WORD    Subsystem;
    WORD    DllCharacteristics;
    DWORD   SizeOfStackReserve;
    DWORD   SizeOfStackCommit;
    DWORD   SizeOfHeapReserve;
    DWORD   SizeOfHeapCommit;
    DWORD   LoaderFlags;
    DWORD   NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);定义一个PIMAGE_OPTIONAL_HEADER32类型的结构体指针,然后通过基址+Dos头e_lfanew成员定位到nt头再加上nt头的Signature成员(4字节)得到File头地址,在加上File头的大小(20字节),它们相加结果是一个地址这个地址是可选头开始的地方,用结构体指针指向这个地址即可。

PIMAGE_DATA_DIRECTORY结构体定义

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;定义一个PIMAGE_DATA_DIRECTORY类型的结构体指针,通过可选头的DataDirectory成员定位到数据目录表。

for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);
}

这里使用了一个for循环来循环打印数据目录表的数据。每循环一次i就加1知道i小于17就停止循环。

程序执行结果

wKg0C2I9kluAWy72AAEagIiKrI119.png

个人对数据目录表的理解:就是16个结构体组成的表,通过结构体的VirtualAddress成员可以找到真正的表。

0x4节表解析

节表的大小是40个字节(注意是一个节表大小是40),节表的数量有File头的NumberOfSections成员决定,节表指向节,节是用来存储数据的,如.txt节存放代码,.data节存放数据,但是并不是一成不变的,也可以把存放数据的节名字改成.txt并不会影响程序运行。

节表的定义

typedef struct _IMAGE_SECTION_HEADER {
    BYTE    Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
            DWORD   PhysicalAddress;
            DWORD   VirtualSize;
    } Misc;
    DWORD   VirtualAddress;
    DWORD   SizeOfRawData;
    DWORD   PointerToRawData;
    DWORD   PointerToRelocations;
    DWORD   PointerToLinenumbers;
    WORD    NumberOfRelocations;
    WORD    NumberOfLinenumbers;
    DWORD   Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

Name[IMAGE_SIZEOF_SHORT_NAME]一个BYTE类型的数组,用来存放当前节的名字,大小是8字节。

Misc一个联合体,通常会使用VirtualSizez成员,VirtualSize当前节内存中的大小。

VirtualAddress内存中节开始的地方,也就是内存中的偏移。

SizeOfRawData节在文件中的大小,按照文件对齐。

PointerToRawData文件中节开始的地方,文件中的偏移。

Characteristics节的属性

节表示例

wKg0C2I9moWAT4rGAAAlXbjT2Ug017.png

前8字节是节的名字,可以看出名字是.textbss,根据上面节定义可知名字后面是VirtualSize(内存中节大小),我们从73也就是名字结束的地方往后查4个字节得到VirtualSize的值00010000去掉前面的0得到10000,再从VirtualSize结束的地方查4个字节得到VirtualAddress的值00001000同样去掉前面的0得到1000,剩下的成员怎么找就不赘述了,也是和上面这几个成员一样都是查出来的,我们直接看代码,用代码解析节表。

代码解析

#include <stdio.h>
#include <Windows.h>
#define path "C:\\Users\\blue\\Desktop\\Dll1.dll"

void main()
{
FILE* fp = fopen(path, "rb");
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
rewind(fp);
PBYTE ptr = (PBYTE)malloc(size);
memset(ptr, 0, size);
fread(ptr, size, 1, fp);
PIMAGE_DOS_HEADER Dos = (PIMAGE_DOS_HEADER)ptr;
PIMAGE_NT_HEADERS Nt = (PIMAGE_NT_HEADERS)(ptr + Dos->e_lfanew);
PIMAGE_FILE_HEADER File = (PIMAGE_FILE_HEADER)(ptr + Dos->e_lfanew + 4);
PIMAGE_OPTIONAL_HEADER32 Option = (PIMAGE_OPTIONAL_HEADER32)(ptr + Dos->e_lfanew + 20 + 4);
PIMAGE_DATA_DIRECTORY DataDir = Option->DataDirectory;

for (int i = 0; i < 17; i++)
{
printf("VirtualAddress:%x\n", DataDir[i].VirtualAddress);
printf("Size:%x\n", DataDir[i].Size);

}
PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(Nt);
printf("节表\n");
for (int i = 0; i < File->NumberOfSections; i++)
{

printf("Name:%s\n", Section[i].Name);
printf("VirtualSize:%x\n", Section[i].Misc.VirtualSize);
printf("VirtualAddress:%x\n", Section[i].VirtualAddress);
printf("SizeOfRawData:%x\n",Section[i].SizeOfRawData);
printf("PointerToRawData:%x\n", Section[i].PointerToRawData);
printf("Characteristics:%x\n", Section[i].Characteristics);
}
getchar();
}

上面说解析过的代码就不赘述了,直接看PIMAGE_SECTION_HEADER Section = IMAGE_FIRST_SECTION(Nt);这行代码还是定义一个PIMAGE_SECTION_HEADER类型的结构体指针,这里使用IMAGE_FIRST_SECTION宏来定位节表这样方便些,当然也可以通过,可选头的地址+可选头的大小来定位节表。

for (int i = 0; i < File->NumberOfSections; i++)
{

printf("Name:%s\n", Section[i].Name);
printf("VirtualSize:%x\n", Section[i].Misc.VirtualSize);
printf("VirtualAddress:%x\n", Section[i].VirtualAddress);
printf("SizeOfRawData:%x\n",Section[i].SizeOfRawData);
printf("PointerToRawData:%x\n", Section[i].PointerToRawData);
printf("Characteristics:%x\n", Section[i].Characteristics);
}

因为File头里NumberOfSections成员代表节表的数量,所以要用它作为判断条件,循环打印即可,这里打印名字是要用%s是打印字符串时用的,%d是10进制时用的,%x是打印16进制时用的

程序运行结果

wKg0C2I9nSyARq6JAAF9ACzYN78868.png

0x5结语

主要是介绍了Rva与Foa的转换和数据目录表,节表中一些比较重要的成员,和如何使用代码打印出它们,涉及到指针和结构体相关的知识。

由于作者水平有限,文章如有错误欢迎指出。


声明:该文观点仅代表作者本人,转载请注明来自看雪