数据目录表 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 线程局部静态变量),就需要新的机制来实现。这就是TLSTLS变量只需要定义一次,类似全局变量,但定义完后每一个线程都能获取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;
}
文章来源:https://www.toymoban.com/news/detail-792011.html
TLS表 IMAGE_TLS_DIRECTORY32
这其中的地址都是VA,而不是RVA。
文章来源地址https://www.toymoban.com/news/detail-792011.html
到了这里,关于【免杀前置课——PE文件结构】十八、数据目录表及其内容详解——数据目录表(导出表、导入表、IAT表、TLS表)详解;如何在程序在被调试之前反击?TLS反调试(附代码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!