windows原理01

什么是文件

存储数据的实体

不同的文件是给不同的软件去使用的。不同的文件主要是格式不同。

格式就是数组的排列组织方式

什么是PE文件

(Portable Executable) 可执行文件的缩写。这种类型的文件是供windows系统解析,解析完了之后能够创建出进程去运行的文件。

img

PE文件的头部信息(DOS头,NT头,区段表)

学习PE文件就是学习PE的格式。格式就是一堆结构体。需要记忆。为了便于记忆,需要一些辅助性的工具

  1. PE文件格式图

    PEimg

  2. 010-Editor

    010img

  3. PE文件的十六进制与结构体的对照图

  4. LordPE是一个非常好用的PE解析器以及编辑器

DOS头:

可执行文件再设计的时候,考虑到了兼容性问题。在正常的可执行文件开始的部分,嵌入了一个DOS可执行文件。作用就是再MS-DOS系统下能够输出一行 这个不是运行在此系统下的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

两个重要字段:

e_magic永远都是 0x5A4D (小端存储)

e_lfanew 是真正的可执行文件的起始部分

NT头:

1
2
3
4
5
struct _IMAGE_NT_HEADERS {
DWORD Signature; //此字段被设置为0x00004550,'PE00'的ascii码
IMAGE_FILE_HEADER FileHeader; //一个IMAGE_FILE_HEADER结构
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //一个IMAGE_OPTIONAL_HEADER结构
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

Signature 永远都是 0x50 0x45 0x00 0x00 (0x00004550)

IMAGE_FILE_HEADER

1
2
3
4
5
6
7
8
9
struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections; //区段的数量
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;//限在此结构体后面的数据的大小,即IMAGE_OPTIONAL_HEADER的大小
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

重要字段:

NumberOfSections 区段的数量

SizeOfOptionalHeader 扩展头的大小,因为扩展头中数据目录表的个数是不确定的。所以这里需要一个大小。

有用字段:

Machine 运行平台

TimeDateStamp 时间戳 表明是再什么时候编译的

IMAGE_OPTIONAL_HEADER 扩展头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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;

非常重要的字段:

ImageBase 程序的默认加载基址

AddressOfEntryPoint 程序的入口点(EP)。

比较重要的字段:

SectionAlignment 内存对齐 0x1000 (因为一页内存是4KB)

FileAlignment 文件对齐 0x200

SizeOfImage 映像大小(这个PE文件被加载到内存 占用空间应该是多大)

SizeOfHeader 头部大小 DOS头+NT头+区块表的大小。

NumberOfRvaAndSizes 数据目录表的元素个数

DllCharacteristics PE的一组属性。

极为重要的字段:

DataDirectory 数据目录表 描述了PE文件中16个非常重要的数据块的大小和位置。

1
2
3
4
struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据的RVA
DWORD Size; //数据的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
  1. 导入表
  2. 导出表
  3. 重定位表
  4. 资源表
  5. TLS表
  6. ……

区段表:结构体数组,数组的元素个数由文件头中的NumberOfSections 绝定。

区段表中的一个元素描述的就是一个区段的信息:

  1. Name 区段名
  2. PointerToRawData 在文件中的位置 FOA
  3. SizeOfRawData 再文件中的大小
  4. VirtualAddress 再内存中的位置 RVA
  5. Misc.VirtualSize 再内存中的大小
  6. Characteristics 区段的属性:可读 可写 可执行 ……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

FOA VA RVA

文件偏移(FOA或者Offset):某一个数据距离文件开头的偏移

虚拟地址(VA):程序在运行的时候,是将PE文件加载到进程的内存空间中。进程的这块内存空间就称之为 虚拟内存空间。32位的程序虚拟内存空间是以字节为单位的,每一个字节都有一个编号从0x0000 0000 到 0xFFFF FFFF。这些编号就是虚拟地址

相对虚拟地址(RVA):PE文件不会占满整个虚拟内存空间,而是会占用一部分。那么就会有一个起始位置,这个起始位置也称为加载基址。PE文件中的数据相对于加载基址的偏移就是相对虚拟地址

如果系统加载PE文件的时候,是将PE文件原封不动的复制到了内存中,那么某一个数据的FOA和RVA就是相等的。

关闭ASLR

1
2
PIMAGE_NT_HEADERS.OptionalHeader.DllCharacteristics;
开启是0x8140 关闭是0x8100
1
2
PIMAGE_NT_HEADERS.FileHeader.Characteristics;
开启是0x0102 关闭是0x0103

查看PE头示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <stdio.h>
#include <Windows.h>

char* ReadFileToMemory(const char* pFilePath)
{
HANDLE hFile = CreateFileA(
pFilePath,
GENERIC_READ | GENERIC_WRITE,
FALSE,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("文件打开失败\n");
return 0;
}
DWORD dwFileSize = GetFileSize(hFile, NULL);
char* pBuf = new char[dwFileSize] {};
if (!pBuf)
{
CloseHandle(hFile);
printf("内存申请失败\n");
return 0;
}
DWORD dwRead;
ReadFile(hFile, pBuf, dwFileSize, &dwRead, NULL);
return pBuf;
}

bool IsPeFile(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
if (pNt->Signature != IMAGE_NT_SIGNATURE)
{
printf("不是PE文件\n");
return false;
}
return true;
}

void ShowImportantHeaders(char* pBuf)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBuf;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + pBuf);
printf("默认加载基址:%#x\n", pNt->OptionalHeader.ImageBase);
printf("文件入口点:%#x\n", pNt->OptionalHeader.AddressOfEntryPoint);
printf("文件区段个数:%d\n", pNt->FileHeader.NumberOfSections);
}

int main()
{
char* pBuf = ReadFileToMemory("123.exe");
if (IsPeFile(pBuf))
{
ShowImportantHeaders(pBuf);
}
delete pBuf;
return 0;
}