主要步驟:
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.html https://www.cnblogs.com/Madridspark/p/WinPEFile.html
模擬PE解析器工作原理:https://www.cnblogs.com/onetrainee/p/12938085.html
感謝以上資料作者帶來的啟發與借鑒,在這也是分享自己在做這個動態加載時的感受,如有不足之處也希望大家不吝賜教,指點出來。謝謝。
有什么問題或看法歡迎評論