1. 程式人生 > 實用技巧 >PE檔案動態載入執行過程

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("設定頁屬性失敗\n");
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.htmlhttps://www.cnblogs.com/Madridspark/p/WinPEFile.html

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

感謝以上資料作者帶來的啟發與借鑑,在這也是分享自己在做這個動態載入時的感受,如有不足之處也希望大家不吝賜教,指點出來。謝謝。

有什麼問題或看法歡迎評論