【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)

数据目录表 IMAGE_DATA_DIRECTORY

数据目录表:可选PE头最后一个成员,就是数据目录.一共有16个
分别是:导出表、导入表、资源表、异常信息表、安全证书表、重定位表、调试信息表、版权所以表、全局指针表
TLS表、加载配置表、绑定导入表、IAT表、延迟导入表、COM信息表 最后一个保留未使用,默认为0。
在这里插入图片描述
NumberOfRvaAndSize数据目录表的个数。
在这里插入图片描述
VirtualAddress RVA内存偏移
FOA硬盘中的偏移
Size 大小

导出表 IMAGE_EXPORT_DIRECTORY

在这里插入图片描述
Name 导出表文件名首地址
Base 导出函数起始序号
NumberOfFunctions是dll文件中导出函数的个数:最大的序号-最小序号+1,下图为6。
NumberOfNames以名称导出函数的个数:即在dll文件中函数后面不加noname的数量,下图为2.
在这里插入图片描述

在DLL文件中如何找到要用的函数呢?

AddressOfNames存的是函数名称起始位置的偏移。
AddressOfNameOrdinals存的是序号,加上Base等于dll文件中函数后面的序号。
AddressOfFunctions存的是真正函数存储位置的偏移。

下图从右向左
要找到MessageBoxW的函数地址,首先从AddressOfNames在AddressOfNameOrdinals中的索引找到MessageBoxW的序号,在AddressOfFunctions按序号找到地址。
在这里插入图片描述

数据FOA-区段FOA=数据RVA-区段RVA
区段FOA=数据FOA-数据RVA+区段RVA

导入表 IMAGE_IMPORT_DESCRIPTOR

问题
  • 1、调用dll文件函数原理?

程序在调用dl1文件函数时,并不是把d11文件函数的代码编译到当前文件中,而是把d11文件对应的函数地址保存到了当前文件中。(调用默认在内存)

  • 2、一个进程空间中的exe dll文件如何被加载到内存的?

exe被最先加载,后需要哪个dll文件再进行调用。

  • 3、Exe文件调用的动态链接库在内存中与在硬盘中有什么不同?

在硬盘中exe存储的是调用dll时用到的函数名称,在内存中是调用dll时将dll加载进内存后函数的地址。

这三个问题是非常重要的,主要是要能理解exe和dll文件之间的关系,看下图实例的讲解:
在这里插入图片描述
我们知道在一个进程中分配了4G内存空间,在我们exe文件中的该如何调用dll文件中的函数呢?
如上图右侧4G内存中,exe文件运行到要调用函数时找到函数调用的内存地址,对dll文件中函数进行调用。
但是这么多dll文件中的函数,不能保证这函数每次被加载到内存时一直在这个位置,该如何解决呢?
事实上,exe文件在硬盘中存储时在要调用dll文件(HMODULE)中函数位置存放的是函数的名称(func)当exe文件被加载进内存时发现要调用哪个dll文件,把dll文件加载进进程内存中,后利用GetProcessAddr(HMODULE,func)将在该HMODULE模块中要调用的function找到此时拿到了函数真正在内存的地址,然后才把类似上图中0x600000的地址填到exe文件中的。
也就是说exe对dll中函数的调用,dll的加载是一个动态的过程,用到哪个dll文件再加载,因为不能确定每个dll文件每次被加载进内存的位置,所以并不能一次写死,但是exe文件可以写死,因为exe是最先加载进4G内存的文件

在这里插入图片描述

导入表解析

一个文件只有一个导出表,有多个导入表
在这里插入图片描述
在这里插入图片描述

INT:导入名称表,无论在文件中还是在内存中都是指向函数的名称
IAT: 导入地址表,在文件中时,与INT是一样的指向函数名称,在内存中保存的是函数实际地址
在这里插入图片描述
OriginalFirstThunk指向一个结构体PIMAGE_THUNK_DATA,这结构体其中的联合体判断是按序号导入还是按名称导入,最高位如果为1就是按序号导入的,其他31位都为序号,如果不为1就是按名称导入的,其他都是函数名称。
OriginalFirstThunk(INT)中包含着IMAGE_IMPORT_BY_NAME结构体,存储的是导入函数的名称。
Name 指向被导入的dll的 RVA。
FirstThunk(IAT)中 需要根据判断TimeDataStamp是否为0,若为0则和IMAGE_IMPORT_BY_NAME一样,不为0则存储的是导入函数实际的位置。

获取导入表

文件结构

在这里插入图片描述

CPeUtil.cpp

#include "CPeUtil.h"

CPeUtil::CPeUtil()
{
	FileBuff=NULL;
	FileSize=0;
	pDosHeader = NULL;
	pNtHeaders = NULL;
	pFileHeader = NULL;
	pOptionHeader = NULL;
}

CPeUtil::~CPeUtil()
{
	if (FileBuff)
	{
		delete[]FileBuff;
		FileBuff = NULL;
	}
}
//载入文件
BOOL CPeUtil::loadFile(const char* patch)
{
	HANDLE hFile = CreateFileA(patch, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
	if (hFile==0) 
	{
		return FALSE;
	}
	//私有成员变量获取文件大小并初始化缓冲区
	FileSize = GetFileSize(hFile, 0);
	FileBuff = new char[FileSize]{0};
	DWORD realReadBytes = 0;
	//是否读取成功
	BOOL readSuccess =ReadFile(hFile,FileBuff,FileSize,&realReadBytes,0);
	if (readSuccess==0)
	{
		return FALSE;
	}
	if (InitPeInfo())
	{
		CloseHandle(hFile);
		return TRUE;
	}
	return FALSE;
}

//加载文件后初始化不同头位置
BOOL CPeUtil::InitPeInfo()
{
	//用以下两个判断该文件是否为PE文件
	pDosHeader = (PIMAGE_DOS_HEADER)FileBuff;
	if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
	{
		return FALSE;
	}
	pNtHeaders = (PIMAGE_NT_HEADERS)(pDosHeader->e_lfanew + FileBuff);
	if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE)
	{
		return FALSE;
	}
	pFileHeader = &pNtHeaders->FileHeader;
	pOptionHeader = &pNtHeaders->OptionalHeader;

	return TRUE;
}

//输出区段头
void CPeUtil::PrintSectionHeaders()
{
	PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
	//遍历不同区段
	for (int i = 0; i < pFileHeader->NumberOfSections; i++)
	{
		char name[9]{ 0 };
		memcpy_s(name, 9, pSectionHeaders->Name, 8);
		printf("区段名称:%s\n", name);
		pSectionHeaders++;
	}
}

//解析导出表
void CPeUtil::GetExportTable()
{
	IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[0];
	PIMAGE_EXPORT_DIRECTORY pexport = (PIMAGE_EXPORT_DIRECTORY)RvaToFoa(directory.VirtualAddress);
	char *dllName = RvaToFoa(pexport->Name)+FileBuff;
	printf("文件名称:%s\n", dllName);
	//遍历不同函数的地址
	DWORD* funaddr = (DWORD*)(RvaToFoa(pexport->AddressOfFunctions) + FileBuff);
	WORD* peot = (WORD*)(RvaToFoa(pexport->AddressOfNameOrdinals) + FileBuff);
	DWORD* pent = (DWORD*)(RvaToFoa(pexport->AddressOfNames) + FileBuff);
	for (int i = 0; i < pexport->NumberOfFunctions; i++)
	{
		printf("函数地址为:%x\n",*funaddr);
		for (int j = 0; j < pexport->NumberOfNames; j++)
		{
			if (peot[j]==i)
			{
				char* funName = RvaToFoa(pent[j])+FileBuff;
				printf("函数名称为:%s\n", funName);
				break;
			}
		}
		funaddr++;
	}
	
}

//获取导入表
void CPeUtil::GetImportTables()
{
	//导入表也是数据目录表的一部分,作为第二个
	IMAGE_DATA_DIRECTORY directory = pOptionHeader->DataDirectory[1];
	//获取真正导入表地址
	PIMAGE_IMPORT_DESCRIPTOR pImport = (PIMAGE_IMPORT_DESCRIPTOR)(RvaToFoa(directory.VirtualAddress) + FileBuff);
	//判断联合体中是否有数据
	while (pImport->OriginalFirstThunk)
	{
		char* dllName = RvaToFoa(pImport->Name) + FileBuff;
		printf("dll文件名称为:%s\n", dllName);
		PIMAGE_THUNK_DATA pThunkData = (PIMAGE_THUNK_DATA)(RvaToFoa(pImport->OriginalFirstThunk) + FileBuff);
		//判断联合体中是否有数据
		while (pThunkData->u1.Function)
		{
			//判断是按序号导入还是按名称导入
			if (pThunkData->u1.Ordinal & 0x80000000)
			{
				printf("按序号导入:%d\n", pThunkData->u1.Ordinal & 0x7FFFFFFF);
			}
			else
			{
				PIMAGE_IMPORT_BY_NAME importName = (PIMAGE_IMPORT_BY_NAME)(RvaToFoa(pThunkData->u1.AddressOfData) + FileBuff);
				printf("按名称导入:%s\n", importName->Name);
			}
			pThunkData++;
		}
		pImport++;
	}
}

//RVA转化FOA
DWORD CPeUtil::RvaToFoa(DWORD rva)
{
	
	PIMAGE_SECTION_HEADER pSectionHeaders = IMAGE_FIRST_SECTION(pNtHeaders);//获取第一个区段头地址
	//遍历不同区段
	for (int i = 0; i < pFileHeader->NumberOfSections; i++)
	{
		if (rva >= pSectionHeaders->VirtualAddress && rva < pSectionHeaders->VirtualAddress + pSectionHeaders->Misc.VirtualSize)
		{
			//数据的FOA=数据的RVA-区段的RVA+区段的FOA
			return rva - pSectionHeaders->VirtualAddress + pSectionHeaders->PointerToRawData;
		}
		pSectionHeaders++;
	}
	return 0;
}


CPeUtil.h

#pragma once
#include<Windows.h>
#include<iostream>
class CPeUtil {
public:
	CPeUtil();
	~CPeUtil();
	BOOL loadFile(const char* patch);
	BOOL InitPeInfo();
	void PrintSectionHeaders();
	void GetExportTable();
	void GetImportTables();
private:
	char* FileBuff;
	DWORD FileSize;
	PIMAGE_DOS_HEADER pDosHeader;
	PIMAGE_NT_HEADERS pNtHeaders;
	PIMAGE_FILE_HEADER pFileHeader;
	PIMAGE_OPTIONAL_HEADER pOptionHeader;
	DWORD RvaToFoa(DWORD rva);
};

main.cpp

#include<iostream>
#include"CPeUtil.h"

int main()
{
	CPeUtil peUtil;
	BOOL ifSuccess = peUtil.loadFile("D:\\code\\VisualStudio2022\\FirstDLL\\Debug\\FirstDLL.dll");
	if (ifSuccess)
	{
		peUtil.GetImportTables();
		//peUtil.GetExportTable();
		//peUtil.PrintSectionHeaders();
		return 0;
	}
	printf("加载PE文件失败!\n");
	return 0;
}

在这里插入图片描述

重定位表 IMAGE_BASE_RELOCATION

在代码中有很多像这种的全局变量,但是像我们之前说的,如果她是直接写死的,就无法确定在加载到内存后还在那个位置,该怎么办?
PE文件创建了多张表来存放这些写死地址的数据位置,用的时候到表中遍历找到相应数据即可。

在这里插入图片描述
在这里插入图片描述
1.VirtualAddress 是 Base Relocation Table 的位置它是一个 RVA 值;
2.SizeOfBlock 是 Base Relocation Table 的大小;
3.TypeOffset是一个数组,数组每项大小为两个字节(16位),它由高 4位和低 12位组成,高 4位代表重定位类型,低 12位是重定位地址,它与 VirtualAddress 相加即是指向PE 映像中需要修改的那个代码的地址。

下图就是重定位表的实例。假如有个0x4002/0x4018/0x4088被存入,则会存在第一个0x4000重定位表中,0x2/0x18/0x88,为什么要有多个重定位表,若为0x5000以上该怎么存?
放到下一个重定位表中,因为其为分页存储,每页偏移为0x1000,所以不同位置的相隔0x1000进行存储。
VirtualAddress + WORD格子中的偏移 = 要修复的地址RVA
但是像这些格子中,拿到的数据可能并不需要修复,如何判断呢?
在这个结构体中除了VirtualAddress和sizeofBlock是DWORD类型的,其他的是WORD类型,也就是16字节,在16字节中最高4位如果等于3,说明它是需要被修复的。也就是高4位是标识,后12位才是真正的偏移值。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

TLS

线程A去修改TLS变量时线程B是不会受影响的,因为每个线程都拥有一个TLS变量的副本。
什么是TLS?
TLS是 Thread Local Storage的缩写线程局部存储。主要是为了解决多线程中变量同步的问题。
TLS的意义?
进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交变得非常快捷。说它是缺点,多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相的 BUG。
如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为staticmemory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS
TLS变量只需要定义一次,类似全局变量,但定义完后每一个线程都能获取TLS变量的副本,解决了不能同步访问TLS的问题。节约了时间和成本。

在这里插入图片描述
在这里插入图片描述

#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
_declspec(thread) int g_number = 100;
HANDLE hEvent = NULL;

DWORD WINAPI threadProc1(LPVOID lparam)
{
	g_number = 200;
	printf("threadProc1 g_number=%d\n", g_number);
	SetEvent(hEvent);
	return 0;
}

DWORD WINAPI threadProc2(LPVOID lparam)
{
	WaitForSingleObject(hEvent, -1);
	printf("threadProc2 g_number=%d\n", g_number);
	return 0;
}

void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	printf("TLS函数执行了\n");
}

#pragma data_seg(".CRT$XLX")
//存储回调函数地址
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { t_TlsCallBack_A,0 };
#pragma data_seg()

int main()
{
	hEvent = CreateEventA(NULL, FALSE, FALSE, NULL);
	HANDLE hThread1 = CreateThread(NULL, NULL, threadProc1, NULL, NULL, NULL);
	HANDLE hThread2 = CreateThread(NULL, NULL, threadProc2, NULL, NULL, NULL);
	WaitForSingleObject(hThread1,-1);
	WaitForSingleObject(hThread2,-1);
	CloseHandle(hEvent);
	system("pause");
	return 0;
}

TLS函数我们可以看到执行了五次,他有点像PHP中的魔法函数,分别在进程创建/销毁,线程创建/销毁时调用TLS回调函数。
在这里插入图片描述
void NTAPI t_TlsCallBack_A(PVOID DllHandle, DWORD Reason, PVOID Reserved)中:
Reason是调用函数的时机,可以进行判断(即进程创建/销毁,线程创建/销毁时机判断)。
在这里插入图片描述
在这里插入图片描述
在晋城创建时调用TLS函数,这也说明了TLS是最先执行的。
OD加载一个程序,OEP我们之前认为是最先执行的,实际上TLS在OEP执行之前就已经执行了。

TLS反调试

TLS用途2:
在安全领域中,TLS常被用来处理诸如反调试、抢占执行等操作。

既然我们知道了TLS是最先执行的,那么我们在TLS回调函数中加上判断是否被调试的API,若被调试直接在OEP之前终止程序,即可做到反调试。

#include<Windows.h>
#include<iostream>
#pragma comment(linker,"/INCLUDE:__tls_used")
void NTAPI TLS_CALLBACK1(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
	if (Reason == DLL_PROCESS_ATTACH)
	{
		BOOL result = FALSE;
		HANDLE hNewHandle = 0;
		DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &hNewHandle, NULL, NULL, DUPLICATE_SAME_ACCESS);
		CheckRemoteDebuggerPresent(hNewHandle, &result);//微软提供的API 判断该文件有没有被调试
		if (result)
		{
			MessageBoxA(0, "程序被调试了!", "警告", MB_OK);
			ExitProcess(0);
		}
	}
	return;
}
#pragma data_seg(".CRT$XLX")
PIMAGE_TLS_CALLBACK pTLS_CALLBACKs[] = { TLS_CALLBACK1,0 }; 
#pragma data_seg()

int main()
{
	printf("main函数执行了");
	system("pause");
	return 0;
}

在这里插入图片描述

TLS表 IMAGE_TLS_DIRECTORY32

这其中的地址都是VA,而不是RVA。
在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webfker from 0 to 1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值