PE

详细的记录一波pe有关知识,便于以后复习

DOS头

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;

Dos头如上,比较重要的就是俩个一个是magic num MZ还有一个是e_lfanew为nt头开始的偏移

e_lfanew往下到pe开始处既是dos存根用来兼容dos的,在dos上运行时打出那一句话,因为基本不会用到通常会塞点邪恶的东西在这之间

Nt头

typedef struct _IMAGE_NT_HEADERS {

DWORD Signature;

IMAGE_FILE_HEADER FileHeader;

IMAGE_OPTIONAL_HEADER32 OptionalHeader;

} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

开头的签名就是PE

紧跟着俩结构体分别是文件头和可选文件头

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;

typedef struct _IMAGE_OPTIONAL_HEADER {

WORD Magic;

BYTE MajorLinkerVersion;

BYTE MinorLinkerVersion;

DWORD SizeOfCode;

DWORD SizeOfInitializedData;

DWORD SizeOfUninitializedData;

DWORD AddressOfEntryPoint; #重点,入口处

DWORD BaseOfCode;

DWORD BaseOfData;

DWORD ImageBase; #基地址,内存镜像

DWORD SectionAlignment; #内存对齐

DWORD FileAlignment; #文件对齐

WORD MajorOperatingSystemVersion;

WORD MinorOperatingSystemVersion;

WORD MajorImageVersion;

WORD MinorImageVersion;

WORD MajorSubsystemVersion;

WORD MinorSubsystemVersion;

DWORD Win32VersionValue;

DWORD SizeOfImage; #内存中整个pe文件映射尺寸

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;

节表

IMAGE_SECTION_HEADER STRUCT

BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8个字节的节区名称

union Misc

DWORD PhysicalAddress;

DWORD VirtualSize; //节区的尺寸

ends

DWORD VirtualAddress; // 节区的 RVA 地址

DWORD SizeOfRawData; // 在文件中对齐后的尺寸

DWORD PointerToRawData; // 在文件中的偏移量

DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移

DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地)

WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目

WORD NumberOfLinenumbers; // 行号表中行号的数目

DWORD Characteristics; // 节属性如可读,可写,可执行等

IMAGE_SECTION_HEADER ENDS

前面的name存放节名称如.text .data

下面来说一下pe文件的执行过程

\1. Filebuffer:吧文件读入内存

\2. 申请4Gb虚拟内存

\3. Imagebuffer:内存对齐后映射

\4. 重定位后改ip指向entry

\5. 页错误后加载到物理内存

\6. 开始执行

有个小坑就是VirtualSize,是否不大于SizeOfRawDatal?

因为SizeOfRawData是节区在文件中对齐后的大小,而VirtualSize只是节区的大小,所以想当然的就有结论VirtualSize<=SizeOfRawDatal

然而事实并非如此。。。一个典型的例子就是bss段数据,未初始化的全局变量在文件中并不会给他分配大小,但当加载入内存时会给其分配,而VirtualSize表示的就是在内存中的大小

代码注入实战

简单的代码注入

在.text末尾多余处加入自己的代码

我这边以cgctf第一道re为例子

1

Rodata处加上你的字符串

然后再text末尾加上机器码

2

改ep为你的代码偏移处然后执行,最后jmp跳回oep

算是复习一波pe文件结构

当然处理方式还有好多,直接在text节后增加的话代码量有限,可以通过新增节的方式,比如吧dos存根抹去,nt头转移到dos存根那,改dos头最后一个,就可以给新增节留出空间,32位一个节40字节,还需要40字节用0填充这样比较稳定。

或者用更稳的方法直接扩大节,扩大最后一个节并修改属性,也可以合并节然后有新的空间给自定节用。

接下去主要复习一波表

在可选头最后有个结构体数组,大小16,就是16个表,其中有一个是备用的。

#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory

#define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory

#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory

#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory

#define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory

#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table

#define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory

// IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage)

#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data

#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP

#define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory

#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory

#define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers

#define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table

#define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors

#define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor

如上图

每个结构体占8字节俩个dw

typedef struct _IMAGE_DATA_DIRECTORY {

DWORD VirtualAddress; 虚拟地址

DWORD Size; 大小

} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

如上

Va指向了内存对齐后的改表地址

导出表

typedef struct _IMAGE_EXPORT_DIRECTORY {

DWORD Characteristics;

DWORD TimeDateStamp; //时间戳

WORD MajorVersion;

WORD MinorVersion;

DWORD Name; //模块名

DWORD Base; //序号基数

DWORD NumberOfFunctions; //导出函数数目(不一定准确)

DWORD NumberOfNames; //导出函数名字数目

DWORD AddressOfFunctions; // 函数地址表

DWORD AddressOfNames; // 函数名称表数量由NumberOfNames决定

DWORD AddressOfNameOrdinals; // 函数序号表数量由NumberOfNames决定

} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

3

导出表结构如上

前面几个不需要多管,主要看下面的,name是指模块名比如xxx.dll,base是指序号基数后面会用到,NumberOfFunctions是不一定准确的,她是用最大序号减最小序号+1来求得(这边序号不是指AddressOfNameOrdinals序号,是指导出函数的序号

这边俩个表一个函数地址表还有一个函数名称表,函数地址表取决于导出函数数目,而函数名称表取决于导出名字数目,这边有个很神奇的东西,因为函数可以通过符号导出,比如说你编程往往函数名称就和他实现的功能一样,而你不想让别人知道你这个函数的功能就可以通过符号导出,这样的话就不会再函数名字表里出现了,但是在函数地址表里还会有,那么就会有个问题,是不是函数地址表总是比函数名字表来的大呢?

这是不一定的,举个例子你有俩个函数名字都指向了一个地址是完全可以实现的,这样就有可能名称表比地址表来得大。

函数名导出寻找

比如知道函数名为aaa

先会从AddressOfNames遍历寻找到对应地址下标

再去AddressOfNameOrdinals寻找对应下标的值

以其为下标在AddressOfFunctions寻找就是该函数地址

序号导出寻找

首先要有个概念就是AddressOfNameOrdinals里的不是函数序号

要加上base才是序号

那根据序号寻找就很轻松了,吧函数序号减去base为下标去AddressOfFunctions取出地址就是该函数地址

逆推

首先从函数地址开始AddressOfFunctions

遍历地址是否为0,不为0取出下标,去AddressOfNameOrdinals遍历寻找

如果找到值等于下标的,取出该值下标去AddressOfNames找到对应地址,取地址获取函数名

当然如果没有找到,直接吧该下标加上base就为函数的导出序号

重定位表

重定位表的作用很明显,一般在dll里用的比较多

以南邮ctf第一题签到题为例

4

重点关注机器码

Flag寻址的时候是绝对寻址而call printf函数时用的时相对寻址

后面机器码加上下一行指令地址即为函数地址

Flag寻址是更具pe的imagebase去算的,但是如果出现一个情况,特别是在dll里出现比较多,就是俩个dll映射基地址都是0x10000000,这样就会产生重叠,所以必须有1个dll映射到其他地址,一般都是下一个0x10000,这是模块对齐的大小,但不一定啊和内存对齐一样。

这就是重定位表需要的原因,来进行重定位

typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress;
DWORD SizeOfBlock;
} IMAGE_BASE_RELOCATION;
typedef IMAGE_BASE_RELOCATION ,* PIMAGE_BASE_RELOCATION;

结构如上

SizeOfBlock是指当前block大小

每个block大小就是一个内存页0x1000为单位

该页上有多少需要重定位的地方

5

通过判断最后一个结构的VirtualAddress与SizeOfBlock都为0来判断是否结束

内存页中的页大小是0x1000,也就是2的12次方就可以表示一个页内所有的偏移地址,具体项的宽度是16字节 高四位代表类型:值位3 代表的是需要修改的数据 值位0代表的是用于数据对齐的数据,可以不用修改,也就是说只需要关注高四位的值为三的就可以了

Block下面的高四位是个标志位后面是地址

Virtualaddress + 低十二位就是需要修改的地址

导入表

导入表涉及到iat表和int表

具体看时间戳

typedef struct _IMAGE_IMPORT_DESCRIPTOR {

union {

​ DWORD Characteristics; // 0 for terminating null import descriptor

​ DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)

} DUMMYUNIONNAME;

DWORD TimeDateStamp; // 0 if not bound,

​ // -1 if bound, and real date\time stamp

​ // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)

​ // O.W. date/time stamp of DLL bound to (Old BIND)

DWORD ForwarderChain; // -1 if no forwarders

DWORD Name;

DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)

} IMAGE_IMPORT_DESCRIPTOR;

typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;

Name为导入库的名称rva,第一项指向了int表

typedef struct _IMAGE_THUNK_DATA32 {

union {

​ DWORD ForwarderString; // PBYTE

​ DWORD Function; // PDWORD

​ DWORD Ordinal;

​ DWORD AddressOfData; //RVA 指向_IMAGE_IMPORT_BY_NAME

} u1;

} IMAGE_THUNK_DATA32;

typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

Int表的值如果最高位为0,就是name的rva

否则去除最高位剩下的就是导入序号

typedef struct _IMAGE_IMPORT_BY_NAME {

WORD Hint; //可能为0,编译器决定,如果不为0,是函数在导出表中的索引

BYTE Name[1]; //函数名称,以0结尾,由于不知道到底多长,所以干脆只给出第一个字符,找到0结束

} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

名称第一个是hint

然后是函数名

再看看iat表

当导入表时间戳为0时iat和int值都一样

为-1是iat是函数的真实地址,但很少有,因为当dll映射地址改变的时候需要重定位,一般notepad就是这样,好处就是打开快,不需要再根据int表去吧地址写入iat

//最后一个结构全0表示绑定导入表结束

typedef struct _IMAGE_BOUND_IMPORT_DESCRIPTOR {

DWORD TimeDateStamp; //表示绑定的时间戳,如果和PE头中的TimeDateStamp不同则可能被修改过

WORD OffsetModuleName; //dll名称地址

WORD NumberOfModuleForwarderRefs; //依赖dll个数

// Array of zero or more IMAGE_BOUND_FORWARDER_REF follows

} IMAGE_BOUND_IMPORT_DESCRIPTOR, *PIMAGE_BOUND_IMPORT_DESCRIPTOR;

下面看看绑定导入表

时间戳是pe时间戳,如果与绑定的时间戳不一样说明被更新了需要重定位

NumberOfModuleForwarderRefs表示下面紧接着跟了多少个如下结构

typedef struct _IMAGE_BOUND_FORWARDER_REF {

DWORD TimeDateStamp; //时间戳,同样的作用检查更新情况

WORD OffsetModuleName; //dll名称地址

WORD Reserved; //保留

} IMAGE_BOUND_FORWARDER_REF, *PIMAGE_BOUND_FORWARDER_REF;

OffsetModuleName加上绑定导入表首地址才是name的rva

6

自己编程写了下,可惜不小心被删除了。。。只剩下俩张截图

7

这个是导出表写法截图

到这pe算是告一段落了

顺带复习了波程序员的自我修养

文章目录
  1. 1. DOS头
  2. 2. Nt头
  3. 3. 节表
  4. 4. 代码注入实战
  5. 5. 导出表
    1. 5.1. 函数名导出寻找
    2. 5.2. 序号导出寻找
    3. 5.3. 逆推
  6. 6. 重定位表
  7. 7. 导入表
|