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文件的步骤

  1. 观察其前2字节是否为MZ
  2. 找到e_lfanew
  3. 根据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

方法一

  1. 通过VA减装载地址转化出RVA:403006 - 40000 = 3006
  2. 找到RVA所在的节:.data(.data是3000开始)
  3. 计算.data节起始RVA和起始FOA的差值:3000 - 800 = 2800(Hex)
  4. 通过RVA减去差值:3006 - 2800 = 806

用c32asm验证一下,直接跳转到806

很正确

方法二

前两步是一样的,后面不一样

  1. 通过VA减装载地址转化出RVA:403006 - 40000 = 3006
  2. 找到RVA所在的节:.data(.data是3000开始)
  3. 计算RVA在节内的偏移:3006 - 3000 = 6
  4. 节内偏移加上该节的起始FOA:6 + 800 = 806

用LordPE算一下,很正确

(注意用RVA来算,VA算.exe大概率正确,但是其他的很可能计算错误)

这个的编程解析和文件头的也是一样的,懒得写了呜呜呜

添加节

添加节的一般步骤

  1. 增加节表项
  2. 修正文件的映像长度
  3. 修正一个节的数量
  4. 增加文件的节数据
    即: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在内存中的地址。在文件中是通过名字找到的,内存中就不是指向字符串了。

posted @ 2022-01-10 15:23  iPlayForSG  阅读(774)  评论(0编辑  收藏  举报