windows PE 指南(基础部分)(二)

windows PE 指南(基础部分)(二)

    • PE文件头
      • IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint
      • SectionAlignment
      • FileAlignment
    • PE文件布局和装入后内存布局
      • 节表内容
      • 你想在PE文件(PE内存映像)里面找一个数据该怎么找?
    • PE重定位
      • Load Config Table
      • 如何在内存或者文件里面找着数据目录元素所对应的内容
      • loader重定位
        • Base Relocation Table
      • 我们来看看重定位表在文件里面长啥样
      • 我们变量的位置需不需要进行修正呢?
        • .text节为什么没有重定位信息
        • linker重定位
    • 导出表
      • 导出表的目的
      • DLL的导出表
        • DLL文件的PE格式

PE文件头

windows PE 指南(基础部分)(二)_第1张图片

这个PE文件的内容是怎么装入内存并执行的呢?

windows PE 指南(基础部分)(二)_第2张图片

windows PE 指南(基础部分)(二)_第3张图片

windows PE 指南(基础部分)(二)_第4张图片

windows PE 指南(基础部分)(二)_第5张图片

#include
#include
#include

IMAGE_LOAD_CONFIG_DIRECTORY;
IMAGE_IA64_RUNTIME_FUNCTION_ENTRY;

extern "C"
{
	__declspec(dllexport) int Add(int x, int y)
	{
		return x + y;
	}

	__declspec(dllexport) int Sub(int x, int y)
	{
		return x + y;
	}
}

IMAGE_DOS_HEADER* pDos_Head;

IMAGE_NT_HEADERS* pNt_Head;

IMAGE_SECTION_HEADER* pSection_Head;

IMAGE_BASE_RELOCATION* pBase_Relocation;

IMAGE_RESOURCE_DIRECTORY* pResource_Directory;
IMAGE_RESOURCE_DIRECTORY_ENTRY* pResource_Entry;
IMAGE_RESOURCE_DIR_STRING_U* pString;
IMAGE_RESOURCE_DATA_ENTRY* pData;

IMAGE_EXPORT_DIRECTORY* pExport_Directory;

IMAGE_IMPORT_DESCRIPTOR* pImport_Descriptor;
IMAGE_THUNK_DATA* pImport_ThunkData;
IMAGE_IMPORT_BY_NAME* pImport_ByName;

IMAGE_LOAD_CONFIG_DIRECTORY *pConfigDirectory;
int main()
{
	int i = 0;
	printf("%llX\n", GetModuleHandle(NULL));
	//HANDLE hFile = CreateFile(L"..\\x64\\Debug\\PE - 副本.exe", GENERIC_ALL, 1, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	HANDLE hFile = CreateFile(L"F:\\课程\\前课程\\编译和连接\\PE文件格式详解\\PE\\x64\\Debug\\PE - 副本.exe", GENERIC_ALL, 1, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	//HANDLE hFile = CreateFile(L"C:\\Users\\Administrator\\Desktop\\Project1\\x64\\Debug\\Project3.exe", GENERIC_ALL, 1, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	
	DWORD er = GetLastError();

	DWORD length = GetFileSize(hFile, NULL);

	er = GetLastError();

	char* p = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, length);

	BOOL re = ReadFile(hFile, p, length, &length, NULL);
	
	pDos_Head = (IMAGE_DOS_HEADER*)p;

	pNt_Head = (IMAGE_NT_HEADERS*)(p + pDos_Head->e_lfanew);

	pSection_Head = (IMAGE_SECTION_HEADER*)((char*)(&pNt_Head->OptionalHeader) + pNt_Head->FileHeader.SizeOfOptionalHeader);

	//显示所有的段的信息:
	printf("\n-------------------所有的段名----------------------------------\n");
	for (int i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		printf("%s\tvirtualAddress = %X\t Size = %X\n", pSection_Head[i].Name,pSection_Head[i].VirtualAddress,pSection_Head[i].SizeOfRawData);
	}
	
	printf("\n-------------------LoadConfigurationDirectory----------------------------------\n");
	for (i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{

		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress)
		{
			break;
		}
	}
	pConfigDirectory = (IMAGE_LOAD_CONFIG_DIRECTORY*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress - pSection_Head[i].VirtualAddress));

	//pBase_Relocation = pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress;

	/*-----------------------------------重定位段--------------------------------------*/

	printf("\n-------------------重定位段----------------------------------\n");
	//找出.relocation信息(重定位段的DataDirectory数组的索引是5)。
	
	for (; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress)
		{
			break;
		}
	}

	pBase_Relocation = (IMAGE_BASE_RELOCATION*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress-pSection_Head[i].VirtualAddress));

	//显示当前块的重定位信息。
	while (1)
	{
		if (!pBase_Relocation->VirtualAddress)
			break;
		printf("Page VirtualAddress = %X Size = %X\n", pBase_Relocation->VirtualAddress,pBase_Relocation->SizeOfBlock);
		WORD* pWord = (WORD*)(pBase_Relocation + 1);
		for (int i = 0; i < (pBase_Relocation->SizeOfBlock - 8) / 2; i++)
		{
			printf("%X\n", pWord[i]);
		}
		//另外一个页面的重定位信息
		pBase_Relocation = (IMAGE_BASE_RELOCATION*)((char*)pBase_Relocation + pBase_Relocation->SizeOfBlock);
	}
	/*-------------------------------------资源段-------------------------------------------*/

	printf("\n------------------资源段----------------------------------\n");
	//开始搞资源段,资源段的
	//资源段的DataDirectory数组的索引是2
	
	for (i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[2].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[2].VirtualAddress)
		{
			break;
		}
	}

	pResource_Directory = (IMAGE_RESOURCE_DIRECTORY*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[2].VirtualAddress - pSection_Head[i].VirtualAddress));
	pResource_Entry = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)((char*)pResource_Directory + sizeof(IMAGE_RESOURCE_DIRECTORY));
	//第一层目录
	for (int i = 0; i < pResource_Directory->NumberOfNamedEntries+pResource_Directory->NumberOfIdEntries; i++)
	{
		if (pResource_Entry[i].NameIsString)
		{
			pString = (IMAGE_RESOURCE_DIR_STRING_U*)((char*)pResource_Directory + pResource_Entry[i].NameOffset);
			_tprintf(L"type:%s\n", pString->NameString);
		}
		else
		{
			printf("type:%d\n", pResource_Entry[i].Id);
		}
		//如果是目录的话,第二层目录
		if (pResource_Entry[i].DataIsDirectory)
		{
			IMAGE_RESOURCE_DIRECTORY* pDirectory = (IMAGE_RESOURCE_DIRECTORY*)((char*)pResource_Directory + pResource_Entry[i].OffsetToDirectory);
			IMAGE_RESOURCE_DIRECTORY_ENTRY* pEntry = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)((char*)pDirectory + sizeof(IMAGE_RESOURCE_DIRECTORY));
			for (int j = 0; j < pDirectory->NumberOfNamedEntries + pDirectory->NumberOfIdEntries; j++)
			{
				if (pEntry[j].NameIsString)
				{
					pString = (IMAGE_RESOURCE_DIR_STRING_U*)((char*)pResource_Directory + pEntry[j].NameOffset);
					_tprintf(L"\tID:%s\n", pString->NameString);
				}
				else
				{
					printf("\tID:%d\n", pEntry[j].Id);
				}

				//如果是目录的话,第三层目录
				if (pEntry[j].DataIsDirectory)
				{
					IMAGE_RESOURCE_DIRECTORY* pDirectory1 = (IMAGE_RESOURCE_DIRECTORY*)((char*)pResource_Directory + pEntry[j].OffsetToDirectory);
					IMAGE_RESOURCE_DIRECTORY_ENTRY* pEntry1 = (IMAGE_RESOURCE_DIRECTORY_ENTRY*)((char*)pDirectory1 + sizeof(IMAGE_RESOURCE_DIRECTORY));
					for (int k = 0; k < pDirectory1->NumberOfNamedEntries + pDirectory1->NumberOfIdEntries; k++)
					{
						if (pEntry1[k].NameIsString)
						{
							pString = (IMAGE_RESOURCE_DIR_STRING_U*)((char*)pResource_Directory + pEntry1[k].NameOffset);
							_tprintf(L"PAGE:\t\tPAGE:%s\n", pString->NameString);
						}
						else
						{
							printf("\t\tPAGE:%d\n", pEntry1[k].Id);
						}
						if (!pEntry1[k].DataIsDirectory)
						{
							//pData中的OffsetToData是一个RVA,通过换算,可以得到资源数据。
							pData = (IMAGE_RESOURCE_DATA_ENTRY*)((char*)pResource_Directory + pEntry1[k].OffsetToData);
							printf("\t\t\tRVA %X Size %X\n", pData->OffsetToData, pData->Size);
						}
					}
				}
			}
		}
	}

	/*-----------------------------导出段----------------------------------*/
	//开始搞导出段
	//导出段的DataDirectory数组的索引是0
	printf("\n-------------------导出段----------------------------------\n");
	for (i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{

		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[0].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[0].VirtualAddress)
		{
			break;
		}
	}

	pExport_Directory = (IMAGE_EXPORT_DIRECTORY*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[0].VirtualAddress - pSection_Head[i].VirtualAddress));

	char* FileName = (char*)(p + pSection_Head[i].PointerToRawData + (pExport_Directory->Name - pSection_Head[i].VirtualAddress));
	printf("the name of export file : %s\n", FileName);
	DWORD* AddressOfNames = (DWORD*)(p + pSection_Head[i].PointerToRawData + (pExport_Directory->AddressOfNames - pSection_Head[i].VirtualAddress));
	
	WORD* AddressOfOrdinals = (WORD*)(p+pSection_Head[i].PointerToRawData+(pExport_Directory->AddressOfNameOrdinals-pSection_Head[i].VirtualAddress));
	
	DWORD* AddressOfFuns = (DWORD*)(p + pSection_Head[i].PointerToRawData + (pExport_Directory->AddressOfFunctions - pSection_Head[i].VirtualAddress));

	for (int j = 0; j < pExport_Directory->NumberOfNames; j++)
	{
		char* Names = (char*)(p + pSection_Head[i].PointerToRawData + (AddressOfNames[j] - pSection_Head[i].VirtualAddress));
		printf("%s\t%d\t%X\n", Names, AddressOfOrdinals[j] + pExport_Directory->Base, AddressOfFuns[j]);
	}

	/*-----------------------------导入段----------------------------------*/
	//开始搞导入段
	//带入段的索引是1
	printf("\n-------------------导出段----------------------------------\n");

	for (i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{

		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[1].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[1].VirtualAddress)
		{
			break;
		}
	}

	pImport_Descriptor = (IMAGE_IMPORT_DESCRIPTOR*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[1].VirtualAddress - pSection_Head[i].VirtualAddress));

	int Import_Index = 0;
	while (1)
	{
		if (pImport_Descriptor[Import_Index].Name == NULL)
		{
			break;
		}
		char* Name = (p + pSection_Head[i].PointerToRawData + (pImport_Descriptor[Import_Index].Name - pSection_Head[i].VirtualAddress));
		printf("%s\n", Name);
		pImport_ThunkData = (IMAGE_THUNK_DATA*)(p + pSection_Head[i].PointerToRawData + (pImport_Descriptor[Import_Index].OriginalFirstThunk-pSection_Head[i].VirtualAddress));
		int Thunk_Index = 0;
		while (1)
		{
			if (pImport_ThunkData[Thunk_Index].u1.AddressOfData == NULL)
			{
				break;
			}

			pImport_ByName = (IMAGE_IMPORT_BY_NAME*)(p + pSection_Head[i].PointerToRawData +
				(pImport_ThunkData[Thunk_Index].u1.AddressOfData - pSection_Head[i].VirtualAddress));

			printf("\t%d\t%s\n", pImport_ByName->Hint, pImport_ByName->Name);

			Thunk_Index++;
		}
		
		Import_Index++;

	}

	/*-----------------------------导入段(装入内存后的导入段)----------------------------------*/
	//开始搞导入段
	//带入段的索引是1

	printf("\n-------------------导出段(装入内存中)----------------------------------\n");

	HMODULE h = GetModuleHandle(NULL);
	char* p1 = (char*)h;

	pDos_Head = (IMAGE_DOS_HEADER*)p1;
	pNt_Head = (IMAGE_NT_HEADERS*)(p1 + pDos_Head->e_lfanew);

	pImport_Descriptor = (IMAGE_IMPORT_DESCRIPTOR*)(p1 + pNt_Head->OptionalHeader.DataDirectory[1].VirtualAddress);
	Import_Index = 0;
	while (1)
	{
		if (pImport_Descriptor[Import_Index].Name == NULL)
		{
			break;
		}
		char* Name = (p1 + pImport_Descriptor[Import_Index].Name);
		printf("%s\n", Name);
		pImport_ThunkData = (IMAGE_THUNK_DATA*)(p1 + pImport_Descriptor[Import_Index].OriginalFirstThunk);
		LONGLONG* pIAT = (LONGLONG*)(p1 + pImport_Descriptor[Import_Index].FirstThunk);
		int Thunk_Index = 0;
		while (1)
		{
			if (pImport_ThunkData[Thunk_Index].u1.AddressOfData == NULL)
			{
				break;
			}

			pImport_ByName = (IMAGE_IMPORT_BY_NAME*)(p1 + pImport_ThunkData[Thunk_Index].u1.AddressOfData);

			//_tprintf(L"%16X\n", pIAT[Thunk_Index]);
			LONG* pLONG = (LONG*)(&pIAT[Thunk_Index]);
			printf("\t%d\t%X%X\t%s\n", pImport_ByName->Hint,*(pLONG+1),*pLONG, pImport_ByName->Name);

			Thunk_Index++;
		}

		Import_Index++;
	}

	printf("\n-------------------LoadConfigurationDirectory----------------------------------\n");
	for (i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress)
		{
			break;
		}
	}
	pConfigDirectory = (IMAGE_LOAD_CONFIG_DIRECTORY*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress - pSection_Head[i].VirtualAddress));

	HeapFree(GetProcessHeap(), 0, p);

	_gettchar();
	return 0;
}

IMAGE_FILE_HEADER.Machine=0x8664,是指我们CPU类型是64位:

windows PE 指南(基础部分)(二)_第6张图片

IMAGE_OPTIONAL_HEADER.Maigc=0x20b,是指我们这个PE文件是64位。

windows PE 指南(基础部分)(二)_第7张图片

这个可选头的魔术数字用来确定映像是 PE32还是 PE32 + 可执行文件。
PE32+映像允许64位地址空间,同时将映像大小限制为2 gb。其他PE32+修改将在各自的部分中讨论。

IMAGE_OPTIONAL_HEADER.AddressOfEntryPoint

这是程序的入口。
当可执行文件加载到内存中时,入口点相对于映像基地址的偏移。对于程序映像,这是起始地址。对于设备驱动程序,这是初始化函数的地址。Dll的入口点是可选的。当不存在入口点时,此字段必须为零。

windows PE 指南(基础部分)(二)_第8张图片

我们来看看mainCRTStartup是哪个模块的函数:

windows PE 指南(基础部分)(二)_第9张图片

mainCRTStartup这个函数是在我们PE模块里面,没在其他模块里面,它应该是C语言的静态库链接过来的一个函数,这个才是我们真正的入口函数。

我们可以找着main函数:

windows PE 指南(基础部分)(二)_第10张图片

windows PE 指南(基础部分)(二)_第11张图片

windows PE 指南(基础部分)(二)_第12张图片

我们可以从main函数的调用栈中看到这是kernel32里面的代码调用pe模块里面的mainCRTStartup,然后就到我们main函数里面。

所以说,我们写程序的时候那个main函数不是我们程序真正的入口,我们程序真正的入口是C语言库里面的一个函数mainCRTStartup,因为C语言要做自己的一个初始化。

SectionAlignment

这是节对齐,当我们把节装入内存的时候,它有要求的,这个节的起始内存地址必须要能够被0x1000整数,因为我们操作系统里面管理内存是一个页面一个页面管理的,而一个页面一般是4KB大小,4KB的十六进制是0x1000,它会把这个节按照页面为单位,装入到页面开头,而不会从页面的半拉位置装入节的,所以说这是节的内存对齐要求。
表示内存中节的对齐粒度,即每个节被载入的内存地址必须是该字段指定数值的整数倍;此值必须大于或等于FileAlignment字段的值,默认值为系统的页面大小即4KB(0x1000字节),在PE32+中该字段的值默认为8KB(0x2000字节)。因为内存中的数据存取以页面为单位,内存对齐是为了提高程序内存访问速度。

FileAlignment

我们这个PE开始是在一个文件里面,它的每个节也不是想放哪里就放哪里的,它必须放到一个扇区上,我们磁盘的一个扇区一般是512KB,十六进制是0x200,所以说在这个PE文件里面我们每个节的开头都是从扇区的开头来开始的,这是为了方便。我们要从磁盘里面读文件内容的话,即使你读一个字节的内容,你也必须要把一个扇区全部读进来,所以它以扇区的大小为对齐粒度。
表示文件中的节的对齐粒度,即每个节在文件中的偏移地址必须是该字段指定数值的整数倍。该值可以是512字节到64KB的任意值,默认值为一个扇区的大小即512字节(0x2000字节)。扇区是硬盘物理存取的最小单位,每个扇区通常可以存放512字节的数据(以后可能发展为4096字节),文件对齐是为了提高文件从磁盘加载的效率。如果SectionAlignment字段的值小于系统页面大小,那么该字段的值必须与SectionAlignment相同。


PE文件布局和装入后内存布局

PE文件=DOS头+PE头(Signature+标准PE头+可选PE头)+节头/节表+节(数据/内容)

DOS头+PE头(Signature+标准PE头+可选PE头)+节头/节表,这一大块PE数据可以叫做windows头(自定义的称呼)。

PE文件在磁盘上的样子和被装入器oader装入到进程的虚拟内存地址空间(64位进程地址空间:从0到2的48次方-1)里面的样子:

windows PE 指南(基础部分)(二)_第13张图片

从上图左边“装入后”那块,我们看到这是假定loader是从0这个内存地址空间开始装入PE文件的,但是windows实际上肯定是不会往这个地址上装的,这是我们为了更容易理解PE文件装入内存后的布局变化而做的假设(相当于是PE文件各部分在内存地址空间中相对于模块首地址/模块装入地址的偏移量)。

文件指针通俗的来说,就是说某一个字节在文件里的位置,我们叫它文件指针,比方说文件里的第一个字节它的文件指针就是0x0,第二个字节它的文件指针就是0x1,以此类推。

节表内容

我们看到上图PE的文件布局中并没有第0个节,这是因为第0个节的PointerToRawData的值是0,这个就是一个文件指针,该文件指针的值就是你这个节在文件里的位置,而且我们也看到这个节在文件里的大小SizeOfRawData的值也是0,说明这个节是一个“虚节”:

windows PE 指南(基础部分)(二)_第14张图片

也就是说这个“虚节”在我们PE文件中并不存在(在节数据SectionData里面不存在)(在节头/节表中有这个“虚节”的描述pSection_Head[0]),但是呢,在我们PE文件里不存在,并不代表我们PE文件装入内存后这个节不存在(在该“虚节”描述中的VirtualAddress的值为0x1000,它在内存里的大小VirtualSize是0x10000)。

第一个节pSection_Head[1]的内容为:

windows PE 指南(基础部分)(二)_第15张图片

我们发现第1个节在文件里面的大小SizeOfRawData比在内存里的大小VirtualSize(0x973f)要大一些,而且该节在内存里的大小加上它在内存里的地址(0x11000),并不等于第2个节在内存里的地址0x1B000,而是要小于第2个节在内存里的地址(0x11000+0x973f<0x1B000),那该怎么办呢?
加载器会把第2个节的地址0x1A73F“圆整了”(通过在第1个节尾部填充0的方式扩展为IMAGE_OPTIONAL_HEADER32.SectionAlignment字段指定的值0x1000的整数倍),会把该节搞到能够被0x1000整除的一个位置上,这里就是0x1B000地址了。

windows PE 指南(基础部分)(二)_第16张图片

第1个节或第2个节在文件里面的大小SizeOfRawData比在内存里的大小VirtualSize要大一些,这是怎么回事呢?
这是因为VirtualSize表示没有进行文件对齐的实际节区大小;
SizeOfRawData表示基于文件对齐后的节区大小,该字段的值等于VirtualSize字段的值按照IMAGE_OPTIONAL_HEADER32.FileAlignment字段的值对齐以后的大小(例如上图所示的VirtualSize的值0x328a按照0x200对齐后应该是0x3400,正好是SizeOfRawData的值)。

windows PE 指南(基础部分)(二)_第17张图片

我们在SectionHeader里面记录的VirtualAddress是一个相对偏移量,这个相对偏移量是装入到内存以后的偏移量,我们把这个相对偏移量叫相对内存地址,用RVA来表示(Related Virtual Address),也就是说我们记录的内存地址VirtualAddress都是相对内存地址。

你想在PE文件(PE内存映像)里面找一个数据该怎么找?

你想在PE文件(PE内存映像)里面找一个数据该怎么找?你想在PE文件被加载器loader装入到内存以后,要找PE文件里的一个数据,你应该怎么找?

比方说在Section1里面有一个数据,一般我们描述Section1节里面一个数据的话,用的是“这个数据相对于这个节的开始的偏移量”,也就是说我们这个节的数据或代码相对于自己这个节的开头的一个偏移量,来描述这个数据所在的位置。
这样描述有什么好处呢?
这样描述以后,因为我们这个节是整个拷贝过来的,这个节里面某个数据在这个节内部的一个相对位置它是不会改变,但是它会改变的是什么呢?你比方说我们这个数据相对于PE文件布局中的起始位置,但是呢当放到内存里面的时候,我们把在内存里面的起始位置和这个节里面该数据之间的偏移量一算,就会发现这两个数据到文件开头(起始位置)的偏移量是不相等的。
但是不管是在文件布局中还是装入到内存以后,这个数据到它所在节的开始位置的偏移量是不会变得,所以说我们记录一个数据或代码的偏移量,记录的是它在节里面的一个偏移量,这个偏移量是不会变得,只要我们把这个偏移量抓住,然后再把节的到模块首地址之间的偏移量抓住,那么我们就能够推算这个数据在内存里面的位置,因为该数据所在节的首地址我们能算出来(根据我们节头中的信息),再加上不变的节数据偏移,就能把节内部的数据在该节整个装入到内存以后相对于模块首地址的位置,就能够算出来了。
不管你这个模块在内存里面怎么挪,我们这个数据相对于模块首地址的偏移量是不会变得,因为我们模块在内存里面挪的话是整体挪的,它内部数据的相对位置不会发生变化。
这个就是我们PE文件放到内存以后它的一个变化。

基本上来说,在磁盘上的PE文件被加载器loader装入到进程内存地址空间以后,它的windows头里面的内容它不会发生变化,但是节里面的内容(SectionData节数据)有可能发生变化,我们的加载器loader在把节里面的内容拷贝到进程内存地址空间以后,有可能会把节里面的内容进行修改,修改主要是两方面的原因,一方面是重定位,有可能会对里面的一些字节进行修改,还有一方面,就是一些函数的地址它需要进行一些修改;一般就是这两部分需要进行修改,其他99%的内容都是直接拷贝过来了。


PE重定位

Special Sections数据目录

windows PE 指南(基础部分)(二)_第18张图片

我们要认识PE文件,关键是要通过数据目录数组这个表,通过这个表里面的元素认识PE文件,这个表里面的每个元素指向一段内容,这个内容就是我们要学习的东西,那么我们怎么样通过这个表来找着这个表里面元素所对应的内容呢,这个是我们首先应该知道的。

Load Config Table

我们首先拿一个非常简单的元素来练一下手,我们找第10个元素Load Config Table,这个在32位里面跟结构化异常处理(SEH)有关,但是在64位里它基本上没啥用的,所以我们现在以这个简单的为例子来给大家说一下。
我们怎么找Load Config Table这个表里面元素所对应的内容呢?

windows PE 指南(基础部分)(二)_第19张图片

我们看上图这个表里面每个元素是这样的:一个VirtualAddress,这是一个相对虚拟内存地址RVA;一个Size,这个Size是EXE或DLL文件装入到内存以后,这个元素所对应的这个内容的字节大小,是在内存里面的字节大小。
大家注意在文件里面的大小和在内存里面的大小,它们是不一样的。

我们先把Load Config Table这个元素所对应的内容找到,我们来看怎么找。

如何在内存或者文件里面找着数据目录元素所对应的内容

首先打印我们所有的节的名字,和虚拟内存地址RVA,Size也是在内存里面的大小:

//显示所有的段的信息:
	printf("\n-------------------所有的段名----------------------------------\n");
	for (int i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		printf("%s\tvirtualAddress = %X\t Size = %X\n", pSection_Head[i].Name,pSection_Head[i].VirtualAddress,pSection_Head[i].SizeOfRawData);
	}
	

在加载器把本程序EXE文件装入到内存里面以后,查找数据目录元素LoadConfigurationDirectory在内存里面的位置,并且还要查找该数据目录元素在EXE文件中的位置:

printf("\n-------------------LoadConfigurationDirectory----------------------------------\n");
	//我们先在内存中找着DataDirectory[10]在内存中的位置(g_pModule是加载器把本程序EXE文件装入到内存里面以后的该模块的首地址)
	pLoadConfig = (IMAGE_LOAD_CONFIG_DIRECTORY*)(g_pModule + pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress);
	//内存中的位置(这个找到的是该数据目录元素对应内容在内存里面的内容)

	//我们为了找着DataDirectory[10]在文件中的位置,首先要确定该数据目录元素在哪个节里面。
	//我们知道这个元素对应的内容在内存里面的相对虚拟内存地址RVA,我们还知道节在内存里面的相对虚拟内存地址RVA和大小,然后我们就对照,如果这个元素的内容正好在这些节中的某一个节里面,那么我们就用该元素对应内容的相对虚拟内存地址VirtualAddress减去所在节的相对虚拟内存地址VirtualAddress,我们就能找着这个元素内容在这个节里面的相对位置(偏移量),而我们能够找着文件中这个节的起始位置PointerToRawData,再加上这个元素内容在该节中的偏移量,我们就能够找着这个元素对应的内容在文件里面的具体位置。
	for (int i = 0; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress)
		//还有我们要知道,数据目录元素对应的内容一定在一个节内,而不会跨过(横跨)一个节,它不可能位于两个节里,所以不需要再判断数据目录元素对应内容的结尾地址是否在该节内
		{
			break;
		}
	}
	//这个p是我们EXE文件完整的、模样不变的拷贝到内存里的起始地址
	//pSection_Head[i].PointerToRawData这个是节在文件里的偏移量
	//pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress - pSection_Head[i].VirtualAddress这个是我们这个数据目录元素对应内容在所在节内的偏移量
	pConfigDirectory = (IMAGE_LOAD_CONFIG_DIRECTORY*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[10].VirtualAddress - pSection_Head[i].VirtualAddress));
	//文件中的位置(这个找到的是该数据目录元素对应内容在文件里面的内容)

The Load Configuration Structure (Image Only)

该结构体在官方在线文档中的和VS编译器中的结构体有些不一致,所以我们应该以VS编译器中的结构体为准。
在64位系统下,该结构体里面的内容大部分不用了,就留了3个安全检查方面的函数指针。

windows PE 指南(基础部分)(二)_第20张图片

windows PE 指南(基础部分)(二)_第21张图片
下图是EXE文件里面的Load Config Table所对应的内容:

windows PE 指南(基础部分)(二)_第22张图片

windows PE 指南(基础部分)(二)_第23张图片

上图这两个GuardCF开头的检查函数,分别是CF检查函数的一个指针,和Dispatch检查函数的一个指针,并且该结构体前面还有一个安全的函数指针SecurityCookie,这些是在文件里面的内容。

windows PE 指南(基础部分)(二)_第24张图片

(上图最左边的两个数字,前面的是32位的偏移,后面的是64位的偏移)

下图是EXE文件由加载器loader装入到内存里面的Load Config Table所对应的内容:

windows PE 指南(基础部分)(二)_第25张图片

windows PE 指南(基础部分)(二)_第26张图片

大家注意,上图这3个安全相关的函数指针的值变了,loader把我们exe装入到内存的时候,它把第10个元素所对应的内容从exe文件“放到”内存里面的时候,这三个函数指针的值被修改了,我们可以看看它是怎么修改的,我们以第一个函数指针SecurityCookie的值7ff6a052f008为例,我们再看文件里面该指针的值14001f008,我们得看看它是按照什么规则来改这个东西的。

windows PE 指南(基础部分)(二)_第27张图片

OptionalHeader.ImageBase的值是140000000,然后我们再看这个exe被loader装入到内存里面的地址是7FF6A0510000:

windows PE 指南(基础部分)(二)_第28张图片

我们把pConfigDirecotry叫做“文件内容”(这个该元素内容在exe文件里面的样子,这个叫“文件内容”),pLoadConfig叫做“内存内容”(这个是exe被loader装入器装入到内存里面,准备执行的以后变成的样子,所以我们就把它叫做“内存内容”)。

我们内存内容里面有一个函数指针SecurityCookie的值是7ff6a052f008,现在由loader把我们这个exe文件装入到内存里面以后的首地址是7FF6A0510000,我们用这个函数指针的值减去这个exe模块的首地址,差值是1f008。

我们再看文件内容,我们说exe文件被loader装入到内存里面,它应该被装入到哪去呢,应该是OptionalHeader.ImageBase所指向的位置140000000,我们再用文件内容中的函数指针SecurityCookie的值14001f008减去,差值也是1f008,都减出来一个1f008,也就是说这个1f008是固定的。

在我们PE格式的exe文件里面,要求我们这个exe文件装入到140000000这个内存位置,但是实际上loader把exe装入到内存里面,它装入到的是7FF6A0510000这个内存位置,本来exe文件想把自己装入到140000000这个内存位置,但是它实际装入的确是7FF6A0510000这个内存位置,显然我们这个SecurityCookie的位置发生了变化,不是exe文件所要求的位置,所以说我们要对这个函数的指针进行修正,这就是一个重定位!

怎么重定位的呢,就是7FF6A0510000和140000000这俩首地址有一个差值(将这两地址相减),用这个差量加上SecurityCookie在exe文件里面原始的函数指针14001f008相加,进行这么一个修正就正确了,这其实就是一个重定位;
也就是说,7ff6a052f008-7FF6A0510000=14001f008-140000000,
那么可以推导出:7ff6a052f008=7FF6A0510000-140000000+14001f008

当然,上面所举的例子并不是我们讲的重点,这只是让大家看的一个例子,我们现在讲的内容应该是把什么理解:
这16个数据目录元素是我们的重点,每一个元素对应一段内容,那么我们怎么样通过这个元素IMAGE_DATA_DIRECORY[i]里面的VirtualAddress和Size这两个信息,在内存和文件里面找着元素所对应的内容(在内存里面比较好找,在文件里面找就比较复杂了)。

loader重定位

Base Relocation Table

windows PE 指南(基础部分)(二)_第29张图片
在这里插入图片描述

这16个数据目录元素里面有1个元素是重定位表Base Relocation Table(第5个),这个元素所指向内容其实就指向了PE里面的.reloc这个节,它的格式是什么呢?

首先数据目录表第5个元素所指向的内容就是基址重定位表,包含映像中所有基本重定位的条目(是一个重定位块结构IMAGE_BASE_RELOCATION数组)。可选头数据目录中的 Base Relocation Table 字段给出了基址重定位表中的字节数。基址重定位表被划分为块(IMAGE_BASE_RELOCATION)。每个块表示4K 页面的基址重定位。每个块必须从32位边界开始(32位边界是指占4字节的地址?)。

每个块的开头就是下图这么一个占8字节的结构体Base Relocation Block:

windows PE 指南(基础部分)(二)_第30张图片

第一个成员Page RVA是一个相对虚拟内存地址。
在32位或64位系统下,一个exe可执行文件最大不能超过2G,这是一个规定,也是相对虚拟地址RVA的一个限制,如果你看到一个可执行文件超过2G的话那肯定就不对了。

Page RVA:将映像基址加上Page RVA 添加到每个偏移量,以创建必须应用基址重定位的 VA。
Page RVA加上ModuleHandle模块句柄(exe装入到内存里面的首地址),就是一个页面的首地址,能够被0x1000这个数整除(一个页面是4K),也就是说这个地址的后三位是0;
这个页面的首地址就指向我们进程里面的一个内存块(基址重定位表被划分为块,每个块表示4K 页面的基址重定位,每个块必须从32位边界开始),这块内存里面的一些地址需要修正(例如刚才看到的函数指针的值需要修正),修正的办法刚才也说过了;
也就是说这个相对内存地址Page RVA决定了我们这个EXE被loader装入到内存以后,哪个位置的内存地址需要被修正,那个内存地址所在的内存页面的一个首地址,它并不是某一个具体的位置,它只是一个重定位内存页的首地址(起始RVA),具体的位置在后面还得说。

Block Size:基址重定位块中的总字节数,包括 Page RVA 和 Block Size 字段以及后面的一堆WORD数组(每个元素是 Type/Offset 字段)。
再说一下,数据目录表第5个元素所对应的内容是基址重定位表(重定位块结构数组),这个表被划分为块,每个块表示4K 页面的基址重定位,每个块的最开头是一个占8字节的Base Relocation Block结构体(IMAGE_BASE_RELOCATION)(我们可以认为这是一个头),在这个IMAGE_BASE_RELOCATION结构体后面跟着一堆WORD元素(来表示每个重定位项),这一堆元素所占的总字节数就是这个Block Size(包括IMAGE_BASE_RELOCATION结构体的大小),每个元素占1个word,即2个字节,每个元素就是一个重定位信息。

也就是说,Block Size“块大小”字段后跟任意数量的Type/Offset 字段条目(重定位项数组),每个条目是一个 WORD (2字节) ,具有以下结构:

windows PE 指南(基础部分)(二)_第31张图片

Type:重定位类型。
存储在 WORD 的高4位中,这是一个指示要应用的基址重定位类型的值。

Offset:重定位项的位置,一个偏移量(相对于页起始地址)。是一个需要修正的操作数绝对地址的地址
需要修正的操作数绝对地址的地址(偏移量Offset)。假设以页为单位(4096字节)在一个页面中寻址,由于绝对地址相邻的重定位项的高位地址是相同的,所以只需要12位的内存地址。
存储在 WORD 的剩余12位中,是相对于该块的 PageRVA 字段指定的页面起始地址的偏移量。此偏移量指定应用基址重定位的内存地址的具体位置。
也就是说,这个Offset是相对于它所在内存页面首地址的偏移量,而该Offset偏移量指向的内容是某个需要重定位(修正)的内存地址(操作数的绝对地址)。
即,操作数绝对地址的地址的RVA等于该重定位内存页的VirtualAddress字段的值 + Offset偏移量。

我们刚才说过,Page RVA决定了一个页面的首地址,Offset是相对于这个页面首地址的一个偏移量,也就是说从Offset开始是一个内存地址(这个Offset所指向的内存地址),需要对这个内存地址进行修正,什么时候需要对它修正呢,就是说我们exe文件想让loader把它装入到A这个模块建议装载位置(0x140000000),但是loader呢“不愿意”,非得把这个exe装入到B这个模块实际载入位置(7FF6A0510000),如果A和B不一样的话,我们就用B减去A,然后把这个差量和要进行修正的内存地址(14001f008)相加,我们就把这个内存地址修正好了,即重定位算法为:操作数的绝对地址 + (模块实际载入地址 - 模块建议装载地址)

前面我们讲过,我们内存内容里面有一个函数指针SecurityCookie的值是7ff6a052f008,在exe文件里面这个原始的函数指针SecurityCookie的值是14001f008(需要修正的操作数绝对地址),而7ff6a052f008-7FF6A0510000=14001f008-140000000=1f008,
那么可以推导出:7ff6a052f008=7FF6A0510000-140000000+14001f008
假设A地址是140000000(PE头中的模块建议装载地址),14001f008是Offset所指向的内容(这是需要修正的操作数绝对地址),而B地址(映像实际加载的地址)是7FF6A0510000,那么修正后的地址:
7FF6A0510000 - 140000000+14001f008 = 7ff6a052f008

Offset这是一个偏移量,占12个位,2的12次方就是4K,占一个页面(也就是说该偏移量可以是一个页面中的任意一个地址)。

也就是说,Base Relocation Block的头IMAGE_BASE_RELOCATION结构体带了一堆重定位项元素(WORD数组),对一个页面里面哪个内存地址需要进行修正的话,那么就有一个WORD元素跟它对应。
如果有很多页面中的内存地址需要修正的话,那么重定位表的内存布局就是:
Base Relocation Block这一个头带的一堆元素,用来修正一个页面,后面又跟着一个头带着一堆元素对应另一个页面,接着又跟着一个头带的一堆元素对应又一个页面,有多少个页面需要修正,就有多少个Base Relocation Block头带的一堆元素。

我们来看看重定位表在文件里面长啥样

//代码中我们找的是重定位表在exe文件里面的位置和内容
//我们来看看重定位表在文件里面长啥样(只需要看在文件里面的样子,因为重定位表在文件里面和在内存里面长得是一样的)

	/*-----------------------------------重定位段--------------------------------------*/

	printf("\n-------------------重定位段----------------------------------\n");
	//找出.relocation信息(重定位段的DataDirectory数组的索引是5)。
	
	for (; i < pNt_Head->FileHeader.NumberOfSections; i++)
	{
		if (pSection_Head[i].VirtualAddress <= pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress &&
			pSection_Head[i].VirtualAddress + pSection_Head[i].SizeOfRawData > pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress)
		{
			//首先我们找着数据目录第5个元素所对应的重定位表所在的节
			break;
		}
	}

	//找着重定位表在文件里面的位置
	pBase_Relocation = (IMAGE_BASE_RELOCATION*)(p + pSection_Head[i].PointerToRawData + (pNt_Head->OptionalHeader.DataDirectory[5].VirtualAddress-pSection_Head[i].VirtualAddress));

	//显示当前块的重定位信息。
	while (1)
	{
		//如果pBase_Relocation->VirtualAddress是0的话,就说明这个重定位元素所指向的内容已经到头了(结束了),所以我们先判断到头了没有
		//重定位表(重定位块结构数组)的最后以一个VirtualAddress字段为0x00000000的IMAGE_BASE_RELOCATION结构(或者说结构全为0)作为结束
		if (!pBase_Relocation->VirtualAddress)
			break;
		//先把Base Relocation Block的头部里面的内容显示出来
		printf("Page VirtualAddress = %X Size = %X\n", pBase_Relocation->VirtualAddress,pBase_Relocation->SizeOfBlock);
		WORD* pWord = (WORD*)(pBase_Relocation + 1);
		//再把头部后面跟着的这个块里面每个元素的值打印出来
		//因为SizeOfBlock的大小包括了头部,而头部占8个字节,所以这里要减8
		for (int i = 0; i < (pBase_Relocation->SizeOfBlock - 8) / 2; i++)
		{
			printf("%X\n", pWord[i]);
		}
		//另外一个页面的重定位信息
		pBase_Relocation = (IMAGE_BASE_RELOCATION*)((char*)pBase_Relocation + pBase_Relocation->SizeOfBlock);

	}

windows PE 指南(基础部分)(二)_第32张图片

windows PE 指南(基础部分)(二)_第33张图片

我们可以看到第一个基址重定位块是1B000页面,要对这个页面进行重定位,它的大小是0x1C,0x1C-8=0x14(减去的数字8是重定位块的头部两个成员Page RVA和Block Size所占的字节总数),0x14/2=10,这个重定位项数组的个数10包括了最后面的一个0是用来方便对齐的(为了对齐),我们不用管这个0,如果元素个数是单数的话,它用一个0来补齐,双数的话就不用补齐了。
PE和PE+32的重定位表数组结构是相同的,不同的只是重定位块结构IMAGE_BASE_RELOCATION数组的定位不同(这里的定位是什么意思?)。

我们来看第一个元素A110,这个A就是它的重定位类型Type,110是重定位的位置Offset,那么这个重定位的具体位置(需要进行修正的绝对地址)在哪呢?
大家注意,1B000这也是一个相对地址RVA,相对于我们这个exe装入到内存里面模块首地址的一个相对地址(模块实际载入地址 + 1B000就是该重定位页面的VA),我们先看看类型Type:

在这里插入图片描述

这个类型是0xA,即十进制的10,基本上我们在程序输出中所看到需要重定位的Offset都是0xA这种类型,没有其他类型了。
The base relocation applies the difference to the 64-bit field at offset.
基址重定位将差值(模块实际载入地址-模块建议装载地址)应用于位于Offset处的64位地址(操作数绝对地址)。

windows PE 指南(基础部分)(二)_第34张图片

我们看到1B000是对.rdata节进行重定位,而我们前面说过.rdata节是跟结构化异常处理、函数的调用等等有关的节,由于.rdata这个节比较大(3400),所以下面的重定位页1C000和重定位页1D000也是对.rdata这个节进行重定位。

还有一个25000是.00cfg节进行的重定位,这个节就不知道是干啥的了,网上也没找到相关说明,它并不是前面讲过的数据目录表第10个元素Load Config Table,Load Config Table(RVA=1cbb0)是在.rdata节(RVA=1B000)中,这个Load Config Table元素所对应的内容并没有在.00cfg这个节里面(RVA=25000),而.rdata节后面的节是.data节(RVA=1F000)。

1B000(重定位内存页的起始RVA)是一个相对内存地址,该重定位内存页中的第一个元素(重定位项)是A110,所以第一个元素的相对内存地址1B000+110=1B110所指向的内容(操作数绝对地址)需要重定位,现在我们算1B110在内存里是哪个位置(VA),也就是VA=7FF6A0510000+1B110=7FF6A052B110,我们在VS的内存窗口中看看这个内存位置里面是什么内容,我们看看这个内容为什么需要重定位。

windows PE 指南(基础部分)(二)_第35张图片

这个内容就是7ff6a05239b0,很显然这个内存地址已经被修正过了,这是一个什么内存地址呢,我们并不知道,我们可以在VS的反汇编窗口中看看这个内存地址:

windows PE 指南(基础部分)(二)_第36张图片

我们一看就是一个函数,这就是对一个函数代码的地址进行了一个修正。
为什么要对这个函数地址进行修正呢?

首先这一段函数代码在呢呢?在.rdata这个节里面,我们.rdata这个节里面它有代码(函数),原来这个代码它应该装在140000000+.rdata节的RVA,它应该装在这个内存地址,但是它现在装在了7ff6a0510000这个地方去了,所以说这个函数代码的位置发生了一个变化,而在我们exe文件里面的Page RVA+Offset这个位置保存了这个函数的地址(例如14001f008),在文件里面这个函数的地址是按140000000这个基地址来算的,但是你现在exe文件装入内存以后该函数的地址应该要按照7ff6a0510000这个模块基地址(模块实际载入地址)来算的,所以exe文件里面的Page RVA+Offset这个地方的函数指针的地址(例如14001f008)你必须要进行修改,所以这就是我们看到的一个重定位项的解析。
其实其他的Offset都是需要修正的函数指针,因为函数的位置发生了变化,所以函数指针的地址(机器指令中的操作数绝对地址)需要进行修正。

我们变量的位置需不需要进行修正呢?

现在我们再想一个事,既然函数的指针需要进行修正,那么我们变量的位置需不需要进行修正呢?
比方说pDos_Head这个变量(全局变量),这个变量是放在.data节里面,我们看看它到底在不在.data节里面,怎么看呢:

windows PE 指南(基础部分)(二)_第37张图片

&pDos_Head=7ff6a052f180,用这个地址减去我们exe模块实际载入的首地址,即7ff6a052f180-7ff6a0510000=1f180,然后我们看RVA=1f180这个值的是哪个节:

windows PE 指南(基础部分)(二)_第38张图片

我们可以看到1f180确实是在.data节里面,这样我们就能断定pDos_Head这个变量在我们.data节里面,那么我们这个.data节是不是也要装入到内存里面,装入到内存里面我们.data节的起始位置也变了。
如果某个地方有一个指令,反汇编代码显示该指令如下:

windows PE 指南(基础部分)(二)_第39张图片

上图选中的这个地方就是我们变量pDos_Head的地址了,在exe文件里面这个变量的地址和把我们exe装入内存以后的这个变量的内存地址,显然就要进行变化,那这是不是也需要进行重定位呢,显然也是需要进行重定位的。

windows PE 指南(基础部分)(二)_第40张图片

所以说我们要对这段代码里面的上图选中的这条指令里面的内存地址进行修正,那我们这一句机器指令里面是不是使用了pDos_Head这个变量的内存地址,是不是在这一句指令里面的这个内存地址需要进行修正,因为这个变量位置已经发生变化了(所在的.data数据节的位置发生了变化),所以说它的那个内存地址也要发生变化,这个机器指令里面含有这个变量的内存地址,是不是就要对这个指令里面的这个内存地址进行修正了。

前面我们在讲COFF格式文件的时候,链接器在链接的时候由于某一个节的位置发生了变化(例如.data数据节的位置发生了变化),那么在链接成exe文件的时候linker需要对包含在指令里面的内存地址进行修正。
但是现在要不要进行修正,现在显然从我们目前来看,也是需要进行修正的,因为位置发生变化了,它也是需要进行修正的。

windows PE 指南(基础部分)(二)_第41张图片

我们看到反汇编代码中这一句里面的内存地址的重定位需要在.text节里面进行重定位,我们先来看看内存地址为00007FF6A0521B6B的这一句机器指令位于哪个节里面。
7FF6A0521B6B - 7FF6A0510000 = 11B6B,所以它是位于.text节里面(.text节后面的.rdata节的RVA=1B000);
那么我们看.text节里面有没有重定位信息),从下面输出的重定位信息来看,我们这个.text节里面没有需要重定位的地址(重定位内存页面的首地址对不上.text节),但是从我们刚才讲的常识来看,这一句机器指令显然是需要重定位的,但是现在这个.text节里面找不着重定位信息,我们这一句机器指令位于.text节里面,但是重定位信息里面没有RVA=11000开始的页面信息。
这就很奇怪了,为什么没有重定位信息呢?

.text节为什么没有重定位信息

我们再来看反汇编代码,这一句代码就是把rax寄存器里面的值放到我们这个变量pDos_Head里面,那么我们怎么得到这个变量真正的内存地址呢?

windows PE 指南(基础部分)(二)_第42张图片

它内存地址是这么得到的,上图选中的后面这4个字节是相对地址RVA=D60E,那么我们看下一条指令的内存地址是7FF6A0521B72,然后7FF6A0521B72 + D60E = 7FF6A052F180,而pDos_Head这个变量的内存地址&pDos_Head正好就是7FF6A052F180,现在我们所看到的反汇编代码中的内存地址7FF6A052F180是已经修正以后的,用下一条指令的内存地址7FF6A0521B72加上机器指令中的偏移量D60E就能得到pDos_Head这个变量真正的内存地址7FF6A052F180,也就是说我们在这个机器指令里面用了一个相对内存地址D60E,那么上图选中的4个字节**这个地方到底有没有经过修正呢?**因为我们现在看到的是通过反汇编代码看到的,我们看到的这个相对内存地址D60E可能是已经修正过的值了,只是可能而已,具体是不是还要看一下文件中该位置的值。
所以我们要看一下文件里面这个位置的值,首先把反汇编窗口中的这句机器码记到纸上(48 89 05 0E D6 00 00),因为机器指令代码在exe文件中和在内存中是不会变得,把编译好的PE - 副本.exe文件拷贝出来,再用VS调试运行程序输出重定位信息,并打开VS的反汇编窗口用来对照,那么现在我们要找到这句机器指令在exe文件里面的位置。

我们先看看这句机器指令在.text节里面的偏移量是多少呢?
我们知道这个.text节的RVA=0x11000,所以用这句机器指令所在的内存地址7FF6A0521B6B减去模块实际载入地址7FF6A0510000,就能得到这句机器指令的RVA=11B6B,然后用11B6B减去.text节的RVA就得到这句机器指令在.text节里面的偏移量B6B,B6B就是这句机器指令在.text节里面的一个偏移量。

我们看一下.text这个节在文件里面的位置:

windows PE 指南(基础部分)(二)_第43张图片

第一个节.text在exe文件里面的偏移量PointerToRawData是0x400,那么400+B6B=F6B是不是就是我们这句机器指令在文件里面的偏移量了,现在我们就可以用WinHex看一下这句机器指令(48 89 05 0E D6 00 00)在文件里面的位置:

windows PE 指南(基础部分)(二)_第44张图片

我们可以看到在文件中和在内存中这两个地方的机器指令是一模一样的,也就是说这个地方确实没有进行重定位。
那么为什么这里没有进行重定位呢?
我们把一个exe文件由loader装入到内存里面开始执行的时候,不管你装到哪个内存位置(例如A位置或B位置,我们现在对装入到不同位置的这个形式进行对比),装入完以后A位置和B位置的内容是一样的,只要相对位置不变,装入以后exe里面的内容是不变的,既然这个内容是不变的,不管我们装入的是A位置还是B位置,pDos_Head的内存地址和使用该变量的机器指令之间的偏移量是固定不变的,这个偏移量跟loader把你这个exe装入的内存位置是无关的,所以该机器指令里面采用了一个相对内存地址(偏移量),就把重定位给规避掉了,避免了一个重定位,没有必要进行重定位了,因为机器码里使用的是相对位置,这样就方便多了。

64位里面的代码基本上用的都是相对内存地址,这样就规避了大量的重定位操作。

windows PE 指南(基础部分)(二)_第45张图片

我们说代码基本上都在这个.text节里面,我们可以看到这个节最大(0x9800),代码都在这个节里面,但是这个节里面的内容根本就不需要重定位,这么多的代码都不需要重定位,即使你写再多的代码,这个重定位表里面的条目数量也不会有太大的变化,重定位项还是这么点,所以说使用这种相对地址的方式就大量规避了这种重定位。

所以说,程序中涉及绝对地址的操作数(例如函数、全局变量)都需要进行重定位。

linker重定位

那为什么COFF格式的文件通过linker链接到我们EXE文件的时候,我们看见这句代码pDos_Head = (IMAGE_DOS_HEADER*)p的机器码里面也有相对地址,但是COFF文件链接到EXE文件的时候,该相对地址会被改变(重定位修正了?),这种情况下重定位是无法规避的,为什么无法进行规避呢?

现在我们回到COFF被linker链接为EXE文件这方面,比方说有两个obj文件(1.obj和2.obj),这两个.obj里面都有.data节和.text节,链接器合并这两个obj文件的时候,它是怎么合并的呢?
链接器会把1.obj里面的.data节和2.obj里面的.data节这俩捏到一起(如下图所示),.text同理,形成1.obj.data+2.obj.data+1.obj.text+2.obj.text这种格式:

windows PE 指南(基础部分)(二)_第46张图片

然后我们看节里面的相对位置,比方说1.obj.text节里面有一条指令code1引用了1.obj.data节里面的一个数据data1,指令code1和该数据data1之间的相对偏移量是offset1,但是到了exe文件里面,exe.1.obj.text节里面的指令code1和exe.1.obj.data节里面的数据data1之间的偏移量就成了offset3了,不再是offset1,偏移量变了(因为exe中1.obj.data节和1.obj.text节之间的距离变了,1.obj.data节和1.obj.text节之间多了一个节2.obj.data),这个时候你就必须要把偏移量offset3给修复好。
所以我们在obj里面链接到exe的时候,这种修正是避免不了的,但是这种修正是一次性修正,即只需要修正一次就行,因为你这个程序只需要链接一次,后面在执行exe的时候就不需要重新进行链接了,我们执行的都是exe,所以这种修正的代价也只有一次,这种代价大点也无所谓了。
但是我们要把一个exe装入到内存里面运行的时候,这个时候运行的次数可就不一定了,次数可能就很多了,所以说我们把多次的重定位给规避掉了,而linker的这种一次重定位就没必要规避了(也无法规避),这样的话效率就很高了。

这节课我们主要就是把loader的重定位进行了讲解。


导出表

在这里插入图片描述

在这里插入图片描述

windows PE 指南(基础部分)(二)_第47张图片

windows PE 指南(基础部分)(二)_第48张图片

windows PE 指南(基础部分)(二)_第49张图片

windows PE 指南(基础部分)(二)_第50张图片

windows PE 指南(基础部分)(二)_第51张图片

windows PE 指南(基础部分)(二)_第52张图片

windows PE 指南(基础部分)(二)_第53张图片

windows PE 指南(基础部分)(二)_第54张图片

导出表的目的

比方说当我们pe.exe这个模块知道我们CreateFileW这个函数在kernel32.dll这个dll里面位置的时候,那我们能不能计算出CreateFileW函数在我们整个pe进程地址空间中的位置(也就是地址),当然能计算出来了,当一个dll被装入到内存里面的时候,那我们dll的模块句柄(模块的首地址)肯定能知道,CreateFileW函数在kernel32.dll这个模块里面的位置我们知道了以后,这时候我们就能知道CreateFileW函数在pe整个进程地址空间里面的地址,这时候pe模块里面的代码才能调用CreateFileW函数;否则你没法调用它,你不知道它在哪。
从上述过程来看,我们dll文件必须有一个机制,要让别人能够知道自己所想使用的dll里面的函数或者数据的位置,也就是说我们能够知道dll里面导出的每一个函数的函数地址和函数名的话,我们exe里面就可以用它们了,所以得有这么一个机制,而且是大家公认的机制,通过这种机制,别人能够知道,通过函数名能够知道函数地址(相对地址)。

DLL的导出表

我们把这个导出表位于数据目录表第0个元素所对应的内容找出来,然后知道这个内容的格式,我们就能够根据函数名找着这个函数的地址了。

windows PE 指南(基础部分)(二)_第55张图片

DLL文件的PE格式

数据目录DataDirectory数组索引为0的元素所对应的内容就是导出表,我们看到64位程序输出,导出表所在的节是索引为1的.rdata节,如下图所示:

windows PE 指南(基础部分)(二)_第56张图片

在这里插入图片描述

我们从文件里面找导出表:

windows PE 指南(基础部分)(二)_第57张图片

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

我们看到导出表的RVA是0x1b750,而导出表所在的节.rdata的RVA是0x19000,导出表所在位置还不是.rdata节的开始。

这是用程序对导出表进行分析的方法,还有一种分析方法,用dumpin命令:

windows PE 指南(基础部分)(二)_第58张图片


你可能感兴趣的:(windows系统编程,windows,PE)