PE文件格式学习
感觉这博客写起来和抄书差不多。。。
PE文件结构概述
PE文件,即Portable Executable File Format,可移植的执行体,Windows下的所有可执行文件都是PE文件格式,比如.exe,.dll,.sys等
PE文件格式是一种对文件组织管理的方式
用RadASM编写一个简单的可执行程序做为分析的对象
(工程类型win32(nores))
这玩意好像没法写注释语句,那我就按c的语法写注释了
.386 // 用到的汇编指令的指令集是.386
.model flat, stdcall // flat表示使用的是内存的平坦模式,stdcall是函数调用的一种方式
option casemap:none // casemap:none就是不区分大小写
// 调用头文件和链接库
include windows.inc
include kernel32.inc
include user32.inc
includelib kernel32.lib
includelib user32.lib
// 定义数据
.data
szCaption db 'hello', 0 // db是字节的意思,定义了一个hello的字符串,汇编中win32用, 0进行结尾
szText db 'hello world!', 0
// 写代码
.code
start: // 代码从标号开始执行,下面的end start也就是说标号是start
push 0
lea eax, szCaption
push eax
lea eax, szText
push eax
push 0
call MessageBox
push 0
call ExitProcess
end start
编译,连接,然后运行.exe
这就是这段代码的含义
用WinHex来对比可执行文件在文件和内存中的差异
打开WinHex并打开刚刚编译的pe.exe,并且不关闭对话框,然后在winhex里打开ram
找到PE
下面的dll文件就是该exe所依赖的dll文件,不管他们,我们直接点PE.exe点确定
左边这个是在磁盘打开的,右边这个是从内存打开的
第一个区别,左边的文件Offset(偏移)是从0000000开始的,而右边的文件Offset是从00400000开始的
磁盘内的文件是根据一些规范映射到内存中的,所以这个偏移量是不同的
第二个区别,从400220开始两个文件都是00,但是左边的文件到400就有数据了,而右边的要到1000才有数据
并且这两坨数据是一样的
在左边的600,右边的2000处,可以看到调用的dll是一样的,但是数据不同了
还有左边的800,右边的3000是我们定义的字符串
剩下的全是00
用PEView查看可执行文件的结构
用PEView打开PE.exe
pFile是文件中的偏移,Raw Data是原始数据,Value是字符串形式显示,不能显示的用'.'代替
在左边打开IMAGE_DOS_HEADER,这东西对该文件进行了解析
注意到
而原来我们看到第一行前2个数字是4D 5A,他倒过来了,这种玩意叫“字节序”
在IMAGE_NT_HEADERS里面点Signature
我们跟着找一下这个偏移
这个数据也是倒着的,也是字节序导致的
我们再看看左边这串英文
- IMAGE_DOS_HEADER:dos头
- MS-DOS Stub Program:DOS存根
- Signature:PE文件的标识
- IMAGE_FILE_HEADER:文件头
- IMAGE_OPTIONAL_HEADER:可选头(但不是可以不选的那种,只是其中某些东西只需要占位,不需要有具体数据)
- IMAGE_SECTION_HEADER:节区,给出了三种数据在文件和在内存中的位置
- .text:代码
- .rdata: 只读数据
- .data:数据
- SECTION:真正的数据
文件中的数据不会变化,但是在映射到内存中后一些相对位置就变了
DOS头
DOS头是PE文件结构的第一个头,用来保持对DOS系统的兼容,并且用于定位真正的PE头
DOS头在winnt.h头文件中的定义如下(该文件头大小为40h,64d)
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic; // 0x00 EXE标志MZ
WORD e_cblp; // 0x02 最后(部分)页中的字节数
WORD e_cp; // 0x04 文件中的全部和部分页数
WORD e_crlc; // 0x06 重定位表中的指针数
WORD e_cparhdr; // 0x08 头部尺寸,以段落为单位
WORD e_minalloc; // 0x0A 所需的最小附加段
WORD e_maxalloc; // 0x0C 所需的最大附加段
WORD e_ss; // 0x0E 初始的SS值(相对偏移量)
WORD e_sp; // 0x10 初始的SP值
WORD e_csum; // 0x12 校验和
WORD e_ip; // 0x14 初始的IP值
WORD e_cs; // 0x16 初始的CS值
WORD e_lfarlc; // 0x18 重定位表的字节偏移量
WORD e_ovno; // 0x1A 覆盖号
WORD e_res[4]; // 0x1C 保留字
WORD e_oemid; // 0x24 EM标识符(相对e_oeminfo )
WORD e_oeminfo; // 0x26 OEM信息; e_oemid specific
WORD e_res2[10]; // 0x28 保留字
LONG e_lfanew; // 0x3C PE头相对于文件的偏移地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
#define IMAGE_DOS_SIGNATURE 0x4D5A // MZ
其中我们最关心的是e_magic和e_lfanew(MZ其实是一个开发人员的名字的缩写,被保留了下来)
如何判断文件是否为PE结构的文件
用C32ASM打开上次编写的那个PE.exe
这几行其实就是DOS头
WORD e_magic; // 0x00 EXE标志MZ
WORD在windows下是2个字节
前2个字节4D 5A就是e_magic,win下所有可执行文件前2个字节都是他们,其ASCII码是MZ
LONG e_lfanew;
LONG在windows下是4个字节
最后4个字节是B0 00 00 00,它们指向了我们PE头的偏移
但是,此处存储方式是小端序存储,也就是低地址保存低位数据,高地址保存高位数据,实际上他指向的位置是00 00 00 B0
intel架构的cpu存储数据都是小端序,大端序存储一般在其他cpu架构或者网络传输数据时使用
B0行的开头是50 45 00 00,前2个字节翻译成字符串是PE,这就是PE文件头
总结一下,判断一个文件是否为PE文件的步骤
- 观察其前2字节是否为MZ
- 找到e_lfanew
- 根据e_lfanew找到地址,观察其前2字节是否为PE
找到了PE的话一般都是PE文件了
计算IMAGE_DOS_HEADER结构体大小
#include <stdio.h>
#include <windows.h>
using namespace std;
int main()
{
printf("%d %x\r\n", sizeof(IMAGE_DOS_HEADER), sizeof(IMAGE_DOS_HEADER));
return 0;
}
10进制是64,16进制是40
一个小实验
在刚刚的PE.exe中,在B0 00 00 00 到 50 45 00 00中间的数据实际上是完全没用的
实际上这些是DOS的代码
将其全部填充为00,保存,然后打开PE.exe
他还是可以运行的
我们最关心的是e_magic和e_lfanew
那我们尝试把DOS头其他的数据全部填充为00
再次运行
还是可以运行的,也就是说我们改的数据其实是完全不需要的,那他们有些啥用呢
在c32asm中新建一个文件,把00-A0的代码复制下来,保存为dos.bin
扔进IDA打开
这一块代码实际上是在编译-连接的时候自动添加进来的一个程序,被称为DOS存根
读一下汇编,它的作用就是输出"This program cannot be run in DOS mode.",然后关闭程序。
文件头及编程解析
文件头定义与分析
真正的PE头,即IMAGE_NT_HEADERS,其定义如下
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64 PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32 IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32 PIMAGE_NT_HEADERS;
#endif
typedef struct _IMAGE_NT_HEADERS64 {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
- Signature:PE标识符
- FileHeader:文件头
- OptionalHeader:可选头
其中FileHeader的定义如下
// 该结构体可以用于判断文件是exe文件还是dll文件
// 14h 20d
struct _IMAGE_FILE_HEADER {
WORD Machine; // 0x04 运行平台
WORD NumberOfSections; // 0x06 PE中节的数量,最大96个节
DWORD TimeDateStamp; // 0x08 文件创建日期和时间,编译器创建此文件时的时间戳
DWORD PointerToSymbolTable; // 0x0C 指向符号表(用于调试)
DWORD NumberOfSymbols; // 0x10 符号表中符号个数(用于调试)
WORD SizeOfOptionalHeader; // 0x14 可选头IMAGE_OPTIONAL_HEADER结构体的长度 32位是E0 64位是F0
WORD Characteristics; // 0x16 文件的属性 exe是010f dll是210e
}IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
#define IMAGE_SIZEOF_FILE_HEADER 20
IMAGE_FILE_HEADER.MACHINE的常用取值:
#define lMAGE_FILE_MACHINE_1386 0x014c // Intel 386
#define lMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
IMAGE_FILE_HEADER.Characteristics的常用属性:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
用c32asm打开PE.exe
选中的部分就是FILE_HEADER
我们一个一个来看
MACHINE对应的是4C 01,014C表示是386平台的(32位的)
#define lMAGE_FILE_MACHINE_1386 0x014c // Intel 386
NumberOfSections对应的是03 00,0003就是有3个节
扔进LordPE可以发现是.text,.rdata,.data
TimeDateStamp对应的是F8 22 DD 61,61DD22F8代表文件编译的时间
源代码编译完后生成的是obj文件,再经过连接才生成了exe文件。这个时间戳就是给obj文件使用的
之后的PointerToSymbolTable和NumberOfSymbols都是调试用,这里也全是00,不管他
SizeOfOptionalHeader对应的是E0 00,00E0说明他是32位文件
WORD SizeOfOptionalHeader; // 0x14 可选头IMAGE_OPTIONAL_HEADER结构体的长度 32位是E0 64位是F0
Characteristics对应的是0F 01,010F说明他是exe文件
WORD Characteristics; // 0x16 文件的属性 exe是010f dll是210e
另外,010F = 1 + 2 + 4 + 8 + 0100,根据Characteristics的常用属性可以知道
- 它没有重定位的数
- 它是一个可执行文件
- 没有行号
- 没有本地符号
- 在32位机器上运行
编程实现文件头解析
#include <stdio.h>
#include <windows.h>
using namespace std;
#define FILENAME L"C:\\Users\\iPlayForSG\\Desktop\\PE\\PE.exe"
void PrintDosHdr(PIMAGE_DOS_HEADER pImgDosHdr)
{
printf("IMAGE_DOS_HEADER:\r\n");
/*
typedef struct _IMAGE_DOS_HEADER {
WORD e_magic;
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew;
} IMAGE_DOS_HEADER,*PIMAGE_DOS_HEADER;
*/
// 没写的部分都差不多
printf("e_magic:%04X(%c%c)\r\n", pImgDosHdr -> e_magic, *(char*)pImgDosHdr, *((char*)pImgDosHdr + 1));
printf("e_res[4]:");
for (int i = 0; i < 4; ++i)
{
printf("%02X ", pImgDosHdr -> e_res[i]);
}
printf("\r\n");
printf("e_lfanew:%08X\r\n", pImgDosHdr -> e_lfanew);
}
void PrintNtHdr(PIMAGE_NT_HEADERS pImgNtHdrs)
{
printf("IMAGE_NT_HEADERS:\r\n");
printf("Signature:%08X(%s)\r\n", pImgNtHdrs -> Signature, pImgNtHdrs);
}
int main()
{
// 打开文件
HANDLE hFile = CreateFile(FILENAME, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// 创建文件映射内核对象
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
// 将文件映射入内存
LPVOID lpBase = MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
PIMAGE_DOS_HEADER pImgDosHdr = (PIMAGE_DOS_HEADER) lpBase;
PIMAGE_NT_HEADERS32 pImgNtHdr = (PIMAGE_NT_HEADERS) ((DWORD)lpBase + (DWORD)pImgDosHdr -> e_lfanew);
PrintDosHdr(pImgDosHdr);
PrintNtHdr(pImgNtHdr);
// 释放文件映射
UnmapViewOfFile(lpBase);
// 关闭文件映射内核对象
CloseHandle(hMap);
// 关闭文件
CloseHandle(hFile);
return 0;
}
输出大概是这么个样子
可选头
定义
其定义如下
// 32位头的大小是e0h, 224d
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields. 标准字段
//
WORD Magic; // 0x18 魔术字 107h = ROM Image 10Bh = EXE(32位) Image 20Bh = PE32+(64位)
BYTE MajorLinkerVersion; // 0x1A 连接器主版本号(对执行没有任何影响)
BYTE MinorLinkerVersion; // 0x1B 连接器次版本号(对执行没有任何影响)
DWORD SizeOfCode; // 0x1C 所有含代码的节的大小(按照文件对齐,判断某节是否含代码,使用节属性是否包含TNA
// (GE_scu_cwr_coE属性判断,而不是通过IMAGE_sCN_CNT_EXECUTE)
DWORD SizeOfInitializedData; // 0x20 所有含有初始化数据的节的大小
DWORD SizeOfUninitializedData; // 0x24 所有含未初始化数据的节的大小(被定义为未初始化,不占用文件空间,加载入内存后为其分配空间)
DWORD AddressOfEntryPoint; // 0x28 程序执行入口RVA(距离PE加载后地址的距离,对于病毒和加密程序,都会修改该值,从而获得程序的控制权,对于DLL如果没有入口函数,那么是0,对于驱动该值是初始化的函数的地址)
DWORD BaseOfCode; // 0x2C 代码的节的起始RVA(一般情况下跟在PE头部的后面)
DWORD BaseOfData; // 0x30 数据的节的起始RVA
//
// NT additional fields. NT系统增加的字段
//
DWORD ImageBase; // 0x34 程序的建议装载地址
DWORD SectionAlignment; // 0x38 内存中的节的对齐值 32位0x1000 64位0x2000
DWORD FileAlignment; // 0x3C 文件中的节的对齐值 0x1000或者0x200
WORD MajorOperatingSystemVersion; // 0x40 操作系统主版本号
WORD MinorOperatingSystemVersion; // 0x42 操作系统次版本号
WORD MajorImageVersion; // 0x44 该PE的主版本号
WORD MinorImageVersion; // 0x46 该PE的次版本号
WORD MajorSubsystemVersion; // 0x48 所需子系统的主版本号
WORD MinorSubsystemVersion; // 0x4A 所需子系统的次版本号
DWORD Win32VersionValue; //0x4C 未使用,必须为0
DWORD SizeOfImage; // 0x50 内存中的整个PE文件映像大小(按照内存对齐)
DWORD SizeOfHeaders; // 0x54 所有头+节表的大小
DWORD CheckSum; // 0x58 校验和(一般exe文件为0,而dll和sys文件则必须是正确的值)
WORD Subsystem; // 0x5C 文件子系统
WORD DllCharacteristics; // 0x5E DLL文件特性
DWORD SizeOfStackReserve; // 0x60 初始化时保留的栈大小(默认1M)
DWORD SizeOfStackCommit; // 0x64 初始化时实际提交的钱大小(默认4k)
DWORD SizeOfHeapReserve; // 0x68 初始化时保留的堆大小(默认1M)
DWORD SizeOfHeapCommit; // 0x6C 初始化时实际提交的堆大小(默认4K)
DWORD LoaderFlags; // 0x70 加载标志一般为0
DWORD NumberOfRvaAndSizes; // 0x74 数据目录的数效量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 0x78 数据目录数组
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
- ImageBase:只是建议使用的装载地址。若该地址已被使用,则系统会为其重定向一个地址
- 对齐:"A班有50人,B班只有5人,但是两个班都分别坐在一样大的教室里"
一些重要的属性
// 这玩意不在PE头里,而是在整个PE体里
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 虚拟地址
DWORD Size;
} IMAGE_DATA_DIRECTORY,*PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
用C32ASM看数据
还是打开PE.exe
这些是可选头
- Magic是010B,也就是exe文件
- 主版本好05,次版本号0C
- 代码大小是00002000
- 包含的初始化数据大小是00004000
- 包含的未初始化数据大小是0
- 程序入口地址是00001000
- 代码起始地址是00001000
- 数据的起始地址是00001000
- 建议装载地址是00004000
- ......
剩下的也是一个意思,就不多写了
编程解析和前面那个差不多就懒得写了
节表以及地址转换
在PE文件中经常会用到三种地址,分别是
- VA (Virtual Address): 虚拟地址
- RVA (Relatvie Virtual Address)∶ 相对虚拟地址
- FOA (File Offset Address): 文件偏移地址
// section header format
// 此处的偏移是按照每个IMAGE_SECTION_HEADER开始的(28h, 40d)
#define IMAGE_SIZEOF_SHORT_NAME 8
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 0x00 节名称
union {
DWORD PhysicalAddress;
DWORD VirtualSize; // 0x08 节区的尺寸
} Misc;
DWORD VirtualAddress; // 0x0C 节区的起始RVA地址
DWORD SizeOfRawData; // 0x10 在文件中对齐后的尺寸
DWORD PointerToRawData; // 0x14 该节在文件中的起始偏移
DWORD PointerToRelocations; // 0x18 在OBJ文件中使用
DWORD PointerToLinenumbers; // 0x1C 行号表的位置(调试用)
WORD NumberOfRelocations; // 0x20 在OBJ文件中使用
WORD NumberOfLinenumbers; // 0x24 行号表中行号的数量
DWORD Characteristics; // 0x28 节的属性
} IMAGE_SECTION_HEADER,*PIMAGE_SECTION_HEADER;
#define IMAGE_SIZEOF_SECTION_HEADER 40
用LordPE进行解析
用LordPE打开PE.exe,对照着进行理解
.text这一列就是节名称,这只是一个标识,并不影响内部数据的使用,有些保护措施就会将这些节名称给擦除或者重命名,以此达到软件保护的效果
VSize就是结构体中的VirtualSize,它并没有对齐,有多长就是多长
VOffset就是起始RVA地址
注意到文件中的对齐值(FileAlignment)是200,内存中的对齐值是(SectionAlignment,就是那个块对齐)1000,可以看到RSize就是200 200加上去的
ROffset就是PointerToRawData,.rdata的起始位置就是.text的起始位置加上他的大小RSize
右键点编辑区段再点flag右边的点点
右下角的当前值就是Characteristics,这个数字会随着节的属性的更改而更改,比如这个可读可写可执行,它们各有一个值,加起来就是60000020,这个数字就可以表示有且仅有这3个属性为真
用OD来看内存布局
扔进OD,点上面的M,进入内存布局
已经可以在00400000PE文件头和下面的三个节了
并且我们发现,每个节在内存中的地址(VA)是起始地址(RVA)加上装载地址
地址转换
就以这个为例吧,我们想找到下面这一串字符串在文件中的偏移地址
其起始地址是400036
方法一
- 通过VA减装载地址转化出RVA:403006 - 40000 = 3006
- 找到RVA所在的节:.data(.data是3000开始)
- 计算.data节起始RVA和起始FOA的差值:3000 - 800 = 2800(Hex)
- 通过RVA减去差值:3006 - 2800 = 806
用c32asm验证一下,直接跳转到806
很正确
方法二
前两步是一样的,后面不一样
- 通过VA减装载地址转化出RVA:403006 - 40000 = 3006
- 找到RVA所在的节:.data(.data是3000开始)
- 计算RVA在节内的偏移:3006 - 3000 = 6
- 节内偏移加上该节的起始FOA:6 + 800 = 806
用LordPE算一下,很正确
(注意用RVA来算,VA算.exe大概率正确,但是其他的很可能计算错误)
这个的编程解析和文件头的也是一样的,懒得写了呜呜呜
添加节
添加节的一般步骤
- 增加节表项
- 修正文件的映像长度
- 修正一个节的数量
- 增加文件的节数据
即:IMAGE_OPTIONAL_HEADER.SizeOfImage;
IMAGE_FILE_HEADER.NumberOfSections;
用c32asm打开PE.exe,找到节的位置
增加节表项
我们添加的节应该在下一行开始,一共2.5行
对照着来吧,首先是节的名字,写个www吧
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
然后在第8个字节处添加节长度,直接按照内存对齐写个1000
DWORD PhysicalAddress;
DWORD VirtualSize;
上面的.data节的长度是13,起始位置是3000
按照对齐,新增节的起始位置是4000
DWORD VirtualAddress;
节内长度给个200
DWORD SizeOfRawData;
上面的.data节节内长度200,起始偏移800,新增节的起始偏移就是0A00
DWORD PointerToRawData;
后面8个字节(2个DWORD)对我们来说没用
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
再后面4个字节(2个WORD)也没用
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
最后节的属性给E0000060,意思是包含可执行代码,可读可写,包含初始化代码(可以在LordPE里面把flag改成这个去看看)
DWORD Characteristics;
修正节的数量
比较简单
修正文件的映像长度
可以通过查看 -> PE信息来辅助查找
找到SizeOfImage,双击就找到了,给他改成5000
然后在末尾插入512个00(新增节的长度是200),保存然后运行
没有问题
在LordPE看一眼
导入表分析
PE文件不只有头部,还有一些PE体作为可执行文件运行的支撑部分
分析MessageBox函数的调用过程
OD打开PE.exe
F7单步执行,在401012的call处执行后会跳转到401024处,下一步他将会跳转到MessageBox函数内。
我们在此时按回车进入该函数
可以发现,入口地址是75BB3670,并且模块是user32
把左边的地址拖长一点,可以发现
那么这一坨就是真正的MessageBoxA的代码
回到jmp那里,这个FF25就是jmp的机器码,08204000(实际上是00402008)是一个内存地址,我们跳转去看看
和MessageBoxA的入口地址是一样的,也就是jmp进行了一次内存寻址,它保存的值就是跳转的地址,也就是API函数真正的虚拟地址
打开内存模块
可以看到user32的虚拟地址是从75B3开始的,这些地址就是通过导入表装载进来的
导入表分析
导入表的定位:通过IMAGE_OPTIONAL_HEADER.DataDirectory的第二项获取
LordPE的目录可以看到导入表(就是那个输入表)
点旁边的...,可以看到装载了kernel32.dll和user32.dll
并且kernel32导入了ExitProcess这个API函数,而user32导入了MessageBoxA这个API函数
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 of DLL bound to (Old BIND)
// 实际上可忽略
DWORD ForwarderChain; // -1 if no forwards, 可忽略
DWORD Name; // RVA, dll名, 0指示结束, 不再继续遍历了
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses), 0也能指示结束, 不再继续遍历了
} IMAGE_IMPORT_DESCRIPTOR;
其中一些重要的如下
内存中:
- Name: 保存的是一个RVA,这个RVA指向的内容是DLL的文件名
- OriginalFirstThunk: RVA,指向的是一个INT表(Import Name Table),这个表中保存的是所有导入函数名称的RVA
- FirstThunk: RVA,指向的是一个IAT表(Import Address Table),这个表中保存的是所有导入函数的地址(VA)
文件中:
- Name: 保存的是一个RVA,这个RVA指向的内容是DLL的文件名
- OriginalFirstThunk: RVA,指向的是一个INT表(Import Name Table),这个表中保存的是所有导入函数名称的RVA
- FirstThunk: RVA,指向的是一个INT表(Import Name Table),这个表中保存的是所有导入函数名称的RVA
另外,FirstThunk指向的是
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString; // PBYTE
DWORD Function; // PDWORD
DWORD Ordinal;
DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
这个联合体(union) 中有4个字段,但是他所占的空间是其中最大的类型的空间(DWORD, 4字节)而不是空间之和
如果他的值的最高位是1的话,那么他的低16位是导入的序号
而如果他的值最高位不是1的话,那么这个值指向的值是导入函数的名称
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME,*PIMAGE_IMPORT_BY_NAME;
我们看看什么是序号导入
随便扔一个比较复杂的软件进LordPe
注意,对于2进制最高位是1,那么对于16进制最高位是8
那么这里这个东西导入的序号就是2A
这些东西就是通过名称导入的
文件解析
用c32asm看PE.exe
在PE信息找DataDirectory -> IMAGE_DIRECTORY_ENTRY_IMPORT,可以找到他的RVA是2010
计算出他的FOA是610
导入表长度是3C,这一串就是导入表
4C 20 00 00
4C 20 00 00 是OriginalFirstThunk,是一个RVA值,指向的是一个INT(IMAGE_THUNK_DATA)
204C转换后是64C, 也就是这里
IMAGE_THUNK_DATA的值就是 5C 20 00 00 00 00 00 00
205C转换后是65C,
FOA是65C,转化出来是ExitProcess,这里就已经解析出了第一个API函数
6A 20 00 00
接下来6A 20 00 00是Name,206A转化出来FOA是66A
那么他是kernel32.dll
00 20 00 00
下一个00 20 00 00是FitstThunk,指向的也是IMAGE_THUNK_DATA,2000转化出来是600
和上面的OriginalFirstThunk是一样的
OriginalFirstThunk和FitstThunk在文件中指向的是一个东西,在内存中不一样
86 20 00 00
2086转化出来是686,
剩下的导入表全是00,那么解析完成
内存解析
在OD里面看,最开始的RVA是2010,转化成VA是204010
前面是一样的,我们直接看00 20 00 00,转化成虚拟地址直接加400000就行
这里的值是77 32 4E 10,和以前不一样了。
直接跳转过去
是ExitProcess在内存中的地址。在文件中是通过名字找到的,内存中就不是指向字符串了。