zoukankan      html  css  js  c++  java
  • PE文件动态加载执行过程

    主要步骤:

    1.将要加载的文件读取到内存中(简称为文内),检查文件格式无误后,根据可选PE头(简称op头)的SizeOfImage,申请出一块空间用于存储该文件加载到内存后展开的数据(简称为内内)。记得先全部初始化为0,免去后续拷贝中对齐补0的步骤。

    2.将文件数据拷贝到申请出来内存空间中(模仿PE加载器将文件装载到虚拟内存中),先根据op头的SizeOfHeaders,将文件的各种头数据先拷贝过来(因为各种头数据是线性存储的,在静态动态都是相同的存放顺序),随后复制节表数据,遍历每个节表,如果当前节表在文内长度时为0,则说明该节在内存中仅做对齐用,并没有实际数据,遍历下一节,将数据从文起复制文长的数据到内起中,由于前面申请空间时已初始化,所以无需在填0对齐。

    3.进行重定位,如果当前加载到内存当中的基址与op的IB一样,即在理想基址中展开了,或重定位表data[5]的长度为0,则无需要重定位。否则获取到重定位表的块数据后,根据他的(块长度-8)/2得到该块的地址数量,前8字节存放着该块的偏移和大小,每个占4字节,一个重定位地址占2字节,通过块地址+8+(2*i)取出需要重定位的地址,与0x3000进行异或,如果首位为3,则后12位为地址偏移,则重定位地址=后12位(块中偏移)+块的起始位置+内存起始位置 重定位则为*重定位地址=重定位地址+(理想基址和实际基址的偏移) 即*重定位地址+=(实际基址-理想基址)。如果首位为0,则说明该偏移为对齐使用,遍历下一个(当前块基址+当前块长度)。将全部块遍历重定位完后,将op的IB也替换成当前加载到内存的基址。

    4.构建导入表:通过偏移+内存基址,获取导入表第一个dll的数据,按照导入的dll逐个遍历,直到当前导入表的OriginalFirstThunk为0,即遍历完毕。 先通过GetModuleHandleA函数获取当前DLL的句柄,如果返回为NULL,则当前进程还未加载该DLL,loadlibary进来,获得句柄。通过内存基址+OriginalFirstThunk获得INT(输入名字表),内存基址+FirstThunk获得IAT(输入地址表)。检查INT的Ordinal是否为0,为0则遍历完当前DLL,如果不为0,检查第32位是否为1(&0x80000000),为1则为序号导入,Ordinal的后16位为序号取出(&0xFFFF),GetProcAddress得到函数地址,并赋值给IAT表的Function。如果第32位为0,则说明按名称导入,此时先通过内存基址+AddressOfData获得函数名,再用GetProcAddress得到函数地址,并赋值给IAT表的Function,循环遍历各个DLL即可。

    导出表在PE文件加载到内存时并不会使用到

    5.通过VirtualProtect修改整个内存内容的保护属性,修改为PAGE_EXECUTE_READWRITE(执行读写)。

    6.定义一个指向DLL加载函数类型的函数指针,typedef BOOL(WINAPI *DllProcEntry)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);

    随后声明该函数指针的实例,并将(当前内存基址+op的AddressOfEntryPoint)的函数地址赋值给它,随后调用该入口函数。

    DllProcEntry ProcAddr = (DllProcEntry)(g_pFileBuffer + pNtHead->OptionalHeader.AddressOfEntryPoint);//定义函数入口地址

    bool bRetval = (*ProcAddr)((HINSTANCE)g_pFileBuffer, DLL_PROCESS_ATTACH, 0);//调用入口函数

    附上代码:

    #include<iostream>
    #include<Windows.h>
    #include <winnt.h>
    using namespace std;
    
    char *g_pFileSrc = NULL;//文件内容
    char *g_pFileBuffer = NULL;//虚拟内存空间
    int g_iFileBufferLen = 0;//虚拟内存空间大小
    
    //定义一个函数指针   指向DLL加载的入口函数类型的函数
    typedef BOOL(WINAPI *DllProcEntry)(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);
    
    DWORD RVAtoFA(DWORD dwRVA)   //rva转文件地址
    {
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileSrc;		//dos头
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);		//NT头
    	PIMAGE_FILE_HEADER pFileHead = (PIMAGE_FILE_HEADER)&pNtHead->FileHeader;		//PE头
    	PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHead);		//节表
    	int dwSectionCount = pFileHead->NumberOfSections;//获得节表数量
    	for (int iNum = 0; iNum < dwSectionCount; iNum++)
    	{
    		if (dwRVA >= pSection->VirtualAddress && dwRVA < (pSection->VirtualAddress + pSection->Misc.VirtualSize))//如果RVA的值落在当前节点的范围内
    		{
    			return (DWORD)g_pFileSrc + ((dwRVA - pSection->VirtualAddress) + pSection->PointerToRawData);
    			/*则文件地址=映射基址 + 文件偏移地址( RVA- VirtualAddress + RawAddress)
    			= 映射基址 + RVA - VirtualAddress + RawAddress*/
    		}
    		pSection++;//指向下一个节表
    	}
    	return 0;
    }
    bool LoadFile(char *pFileName)  //读取文件
    {
    	//读取文件内容
    	FILE* fp = fopen(pFileName, "rb");
    	if (!fp)
    	{
    		cout << "打开文件失败" << endl;
    		return false;
    	}
    	fseek(fp, 0, SEEK_END);
    	int iFileSize = ftell(fp);
    	g_pFileSrc = (char*)malloc(iFileSize);
    	//DWORD dwBufferSize = *(int*)((DWORD)g_pFileSrc - 16);//这种方法可以取出这段空间的长度
    	if (!g_pFileSrc)
    	{
    		cout << "分配内存失败" << endl;
    		return false;
    	}
    	memset(g_pFileSrc, 0, iFileSize);
    	rewind(fp);
    	fread(g_pFileSrc, 1, iFileSize, fp);
    
    	//检查文件格式
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileSrc;
    	if (pDosHead->e_magic != 0x5A4D)
    	{
    		cout << "该文件不是可执行文件" << endl;
    		return false;
    	}
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
    	if (pNtHead->Signature != 0x4550)
    	{
    		cout << "该文件不是PE文件" << endl;
    		return false;
    	}
    
    	PIMAGE_OPTIONAL_HEADER pOptionalHead = (PIMAGE_OPTIONAL_HEADER)&pNtHead->OptionalHeader;
    
    	g_pFileBuffer = (char*)malloc(pOptionalHead->SizeOfImage);
    	if (!g_pFileBuffer)
    	{
    		cout << "分配模虚拟内存失败" << endl;
    		return false;
    	}
    	memset(g_pFileBuffer, 0, pOptionalHead->SizeOfImage);
    	cout << "读取文件成功,by:阿怪   2020.7.9" << endl;
    	return true;
    }
    
    bool CopyContent()//拷贝数据
    {
    
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileSrc;		//dos头
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);		//NT头
    	PIMAGE_FILE_HEADER pFileHead = (PIMAGE_FILE_HEADER)&pNtHead->FileHeader;		//PE头
    	PIMAGE_OPTIONAL_HEADER pOptionalHead = (PIMAGE_OPTIONAL_HEADER)&pNtHead->OptionalHeader;		//可选PE头
    	PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHead);		//节表
    	int iSection = pFileHead->NumberOfSections;//节数量
    
    	memcpy(g_pFileBuffer, g_pFileSrc, pOptionalHead->SizeOfHeaders);//复制各种头数据
    	//	//pSection->PointerToRawData;//文件中节的起始地址			pSection->SizeOfRawData;//文件中节的长度
    	//	//pSection->VirtualAddress;//虚拟内存中节的起始地址		pSection->Misc.VirtualSize;//虚拟内存中节的长度
    	for (int num = 0; num < iSection; num++)
    	{
    		if (pSection->SizeOfRawData == 0) //如果在文件中这个节的长度为0,证明该节为未被初始化的静态内存区
    		{
    			pSection++;
    			continue;
    		}
    		memcpy(g_pFileBuffer + pSection->VirtualAddress, 
    			g_pFileSrc + pSection->PointerToRawData, 
    			pSection->SizeOfRawData
    			);
    		pSection++;
    	}
    	cout << "从文件拷贝数据到内存完毕,by:阿怪   2020.7.9" << endl;
    	return true;
    }
    
    bool Relocation()  //进行重定位并修改基址
    {
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileBuffer;		//dos头
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);		//NT头
    	PIMAGE_OPTIONAL_HEADER pOptional = (PIMAGE_OPTIONAL_HEADER)&(pNtHead->OptionalHeader);
    	DWORD dwRelocationRVA = pOptional->DataDirectory[5].VirtualAddress;//重定位表RVA
    	int iRelocationSize = pOptional->DataDirectory[5].Size;//重定位表长度
    	DWORD dwImageBaseGap = (DWORD)g_pFileBuffer - pOptional->ImageBase;  //计算加载后的基址与原先预想的基址的距离
    	if ((dwImageBaseGap == 0) || (iRelocationSize == 0))
    	{
    		cout << "该程序无需重定位" << endl;
    		return false;
    	}
    	PIMAGE_BASE_RELOCATION pBaseRelocation = (PIMAGE_BASE_RELOCATION)RVAtoFA(dwRelocationRVA);//重定位表当前块
    	while ((pBaseRelocation->VirtualAddress != 0) && (pBaseRelocation->SizeOfBlock != 0))  //遍历到重定位表末尾为止
    	{
    		for (int i = 0; i < ((pBaseRelocation->SizeOfBlock - 8) / 2); i++)   //块首地址-8(前4为块偏移,后4为块长度)/2=块中需重定位地址数量
    		{
    			WORD pRelocationAddr = *(WORD*)((DWORD)pBaseRelocation + 8 + (2 * i));//在块中,每个重定位地址占2字节(WORD类型)
    			if (pRelocationAddr != 0)  //为0时,说明该位置数据为对齐而填充
    			{
    				DWORD dwRVA = (pRelocationAddr ^ 0x3000) + pBaseRelocation->VirtualAddress;//需要重定位的偏移
    				PDWORD dwFileAddr = (DWORD*)(dwRVA + g_pFileBuffer);//重定位地址=当前程序基址+当前块基址+当前目标偏移
    				*dwFileAddr += dwImageBaseGap;
    			}
    		}
    		pBaseRelocation = (PIMAGE_BASE_RELOCATION)((DWORD)pBaseRelocation + pBaseRelocation->SizeOfBlock);  //下个块文件地址=当前块文件地址+当前块长度
    	}
    	pOptional->ImageBase = (DWORD)g_pFileBuffer;  //将当前文件的基址改为加载到内存后的基址
    
    	cout << "程序重定位并修改基址成功,by:阿怪   2020.7.9" << endl;
    	return true;
    
    }
    
    bool ImportList()  //导入表
    {
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileBuffer;		//dos头
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);		//NT头
    	PIMAGE_OPTIONAL_HEADER pOptional = (PIMAGE_OPTIONAL_HEADER)&(pNtHead->OptionalHeader);
    	DWORD dwImportListRVA = pOptional->DataDirectory[1].VirtualAddress;//导入表RVA
    	int dwImportListSize = pOptional->DataDirectory[1].Size;//导入表长度
    	PIMAGE_IMPORT_DESCRIPTOR pImpotrList = (PIMAGE_IMPORT_DESCRIPTOR)(dwImportListRVA + g_pFileBuffer); //获取导入表第一个导入的dll
    
    	while (pImpotrList->OriginalFirstThunk != 0)  //只有该结构中有一个成员内容为0,即遍历完了导入表了
    	{
    		char* szDllName = (char*)(g_pFileBuffer + pImpotrList->Name);//获取当前DLL的名字
    		HMODULE hDllImageBase = LoadLibrary(szDllName);//先将当前dll加载到程序中
    		if (!hDllImageBase)
    		{
    			int iError = GetLastError();
    			cout << "加载当前dll失败,错误代码:" << iError << endl;
    			return false;
    		}
    
    		PIMAGE_THUNK_DATA pDllINT = (PIMAGE_THUNK_DATA)(g_pFileBuffer + pImpotrList->OriginalFirstThunk);//获取当前DLL导入的函数的输入名称表(INT)
    		PIMAGE_THUNK_DATA pDllIAT = (PIMAGE_THUNK_DATA)(g_pFileBuffer + pImpotrList->FirstThunk);//获取当前DLL导入的函数的输入地址表(IAT) 当前IAT和INT指向同一内容
    
    		for (int i = 0; pDllINT->u1.Ordinal; i++)  //当当前DLL导入的函数Ordinal为0时,即遍历完当前DLL所有函数
    		{
    			if (pDllINT->u1.Ordinal & 0x80000000)  //如果当前结构信息的序数Ordinal的第32位为1  则当前dll的函数由序号导入
    			{
    				DWORD dwFuncNum = pDllINT->u1.AddressOfData & 0xFFFF;//后16位为导入序号
    				//dwDllIAT->u1.Function = (DWORD)hDllImageBase + dwFuncNum;
    				pDllIAT->u1.Function = (DWORD)GetProcAddress(hDllImageBase, (LPCSTR)dwFuncNum);
    			}
    			else   //不为1则由函数名字导入
    			{
    				PIMAGE_IMPORT_BY_NAME szFuncName = (PIMAGE_IMPORT_BY_NAME)(g_pFileBuffer +pDllINT->u1.AddressOfData);  //获得函数名
    				pDllIAT->u1.Function = (DWORD)GetProcAddress(hDllImageBase, szFuncName->Name);	//通过dll和函数名  获得当前函数在内存中的地址
    			}
    			pDllINT++;
    			pDllIAT++;
    		}
    		pImpotrList++;
    	}
    	cout << "构建IAT成功,by:阿怪   2020.7.9" << endl;
    	return true;
    	}
    
    
    bool DynamicLoad(char *pDllName)
    {
    	if (!LoadFile(pDllName)) //获取文件内容、检查文件格式、分配2块内存(存放文件数据和虚拟内存空间)
    	{
    		return false;
    	}
    	if (!CopyContent()) //将文件数据装载到虚拟内容中
    	{
    		return false;
    	}
    
    	Relocation(); //重定位并修改基址(并非所有PE结构都需要重定位)
    
    	if (!ImportList()) //通过导入表构建IAT
    	{
    		return false;
    	}
    
    
    	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)g_pFileBuffer;
    	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)pDosHead + pDosHead->e_lfanew);
    
    	DWORD dwOldProtect = 0;
    	if (FALSE == VirtualProtect(g_pFileBuffer, pNtHead->OptionalHeader.SizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    	{
    		printf("设置页属性失败
    ");
    		return NULL;
    	}
    	
    	DllProcEntry ProcAddr = (DllProcEntry)(g_pFileBuffer + pNtHead->OptionalHeader.AddressOfEntryPoint);//定义函数入口地址
    	MessageBoxA(0, 0, 0, 0);
    	bool bRetval = (*ProcAddr)((HINSTANCE)g_pFileBuffer, DLL_PROCESS_ATTACH, 0);//调用入口函数
    
    	cout << "加载完成"<<endl<<"by:阿怪   2020.7.9" << endl;
    
    	return true;
    }
    

    运行结果:

    在需要调试的位置调用messagebox 随后可在调试器(OD,MDbug)中定位到关键点,方便调试。

    踩坑点:

    1.节数据赋值时的对齐,如果不按照上面的步骤,也可通过对齐值,求得当前节在虚拟内存中的对齐后的大小,用该值-当前节在文件中的大小,可得用0补齐的长度。以及如何去复制各种头文件(各种头文件为线性的,可直接复制),当第一个节在文件大小为0时该怎么处理。

    2.重定位的重定位地址为当前内存基址+当前重定位块基址+当前重定位偏移,重定位后的值应该赋值给*重定位,即*重定位地址=重定位地址+(预先理想的基址与当前内存基址的距离)

    3.在静态文件到动态加载到内存时,导入表是通过INT表,把所导入的函数的地址装载到IAT表,全部加载完后,INT表就没用了,通过IAT表就可以找到相应的函数地址。且在取相应的地址时,应该是由导入表的成员的RVA+内存基址,而不是通过RVAtoFA去取值。

     

     导入表相关文章: https://www.sohu.com/a/278971010_653604

    导出表相关文章:https://www.write-bug.com/article/1926.html   https://www.cnblogs.com/Madridspark/p/WinPEFile.html

    模拟PE解析器工作原理:https://www.cnblogs.com/onetrainee/p/12938085.html 

     感谢以上资料作者带来的启发与借鉴,在这也是分享自己在做这个动态加载时的感受,如不足之处也希望大家不吝赐教,指点出来。谢谢。

    有什么问题或看法欢迎评论

  • 相关阅读:
    Docker安装MySQL&Redis
    使用VirtualBox+Vagrant快速搭建Linux虚拟机环境
    Java集合工具类的一些坑,Arrays.asList()、Collection.toArray()...
    1.docker常用命令
    4. 带有延迟时间的Queue(DelayQueue)
    3. 基于优先级的Queue(PriorityBlockingQueue)
    2. 常见的Queue
    1. 模拟Queue
    14. 线程调度
    13. 线程池
  • 原文地址:https://www.cnblogs.com/aaaguai/p/13277236.html
Copyright © 2011-2022 走看看