In questo articolo non si descriverà in dettaglio il formato PE nel suo complesso (si veda [2] per questo) ma si prenderà in considerazione e si spiegherà in dettaglio quella parte del formato che serve allo scopo di questo articolo. Prima di cominciare la lettura l'ideale sarebbe avere almeno una conoscenza di base sul formato PE. In futuro spero di scrivere un breve articolo introduttivo sull'argomento ma, per il momento, consiglio di leggere [1] (paragrafo 9.2 e sezione relativa a export table di paragrafo 10.1) dove in una decina di pagine si riesce a descrivere tutta la parte rilevante del formato; sicuramente una delle migliori introduzioni all'argomento. Inoltre consiglio di leggere il codice di questo articolo partendo dall'entry point (wmain), seguendo il flusso del codice, istruzione dopo istruzione. Si è scelto, infatti, di inserire tutte le informazioni nei commenti al codice (quasi ogni istruzioni è commentata). Si è optato per questo stile in quanto è un articolo rivolto ai programmatori/reverser più che ai curiosi del formato PE.
Una rappresentazione semplificata del formato PE è illustrata nell'immagine sotto, dove le frecce indicano campi di una componente del formato che contengono RVA ad un'altra componente.
#include <iostream> #include "MyLoadDll.h" int wmain(int argc, wchar_t * argv[]) { wchar_t szFileName[MAX_PATH] = L"MyDLL.dll"; // Ottiene handle a file MyDLL.dll HANDLE hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { ShowError(L"CreateFile"); return 1; } // Calcola dimensione MyDLL.dll su disco DWORD dwFileSize = GetFileSize(hFile, NULL); // Array che conterra copia di immagine su disco di MyDLL.dll. // Da non confondere con immagine della stessa DLL una volta caricata in memoria // che avrà indirizzo di base e allineamento delle sezioni diverso rispetto alla sua controparte su disco. BYTE *lpData = new BYTE[dwFileSize]; if (lpData == NULL) { ShowError(L"new"); return 2; } // Copia immagine su disco di MyDLL.dll in memoria. // Ancora una volta, da non confondere con l'immagine della DLL una volta caricata in memoria. // Per essere chiari, lpData specifica uno spazio di memoria che conterrà una copia dell'immagine su disco. DWORD dwRet = 0; ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL); // Alloca memoria nel processo corrente e carica MyDLL.dll in tale spazio secondo le // indicazioni fornite dal formato PE di MyDLL.dll. // In questo caso si tratta proprio dell'immagine di MyDLL.dll una volta caricata in memoria. LPVOID lpBaseAddress = MyLoadLibrary(lpData, dwFileSize); if (lpBaseAddress == NULL) { ShowError(L"MyLoadLibrary"); return 3; } std::cout << "DLL load OK!" << std::endl; // Ottiene indirizzo del metodo ShowMessage esportato da MyDLL.dll typedef BOOL(*typedef_ShowMessage)(const wchar_t *lpszText, const wchar_t *lpszCaption); typedef_ShowMessage ShowMessage = (typedef_ShowMessage)MyGetProcAddress(lpBaseAddress, "ShowMessage"); if (ShowMessage == NULL) { ShowError(L"MyGetProcAddress"); return 4; } ShowMessage(L"ShowMessage called!", L"MyDLL"); // Libera la memoria allocata in precedenza BOOL bRet = MyFreeLibrary(lpBaseAddress); if (bRet == FALSE) { ShowError(L"MyFreeLirbary"); } delete[] lpData; lpData = NULL; CloseHandle(hFile); std::cout << "Press any key to Exit!" << std::endl; std::getchar(); return 0; }
void ShowError(const wchar_t *lpszText) { wchar_t szErr[MAX_PATH] = { 0 }; wsprintf(szErr, L"%s Error!\nError Code Is:%d\n", lpszText, GetLastError()); MessageBox(NULL, szErr, L"ERROR", MB_OK | MB_ICONERROR); }
LPVOID MyLoadLibrary(LPVOID lpData, DWORD dwSize) { LPVOID lpBaseAddress = NULL; // Calcola dimensione della DLL una volta caricata in memoria DWORD dwSizeOfImage = GetSizeOfImage(lpData); // Alloca in questo processo tanto spazio quanto richiesto per caricare l'immagine della DLL in memoria lpBaseAddress = VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (lpBaseAddress == NULL) { ShowError(L"VirtualAlloc"); return NULL; } RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // Copia header e sezioni (senza fixarne il contenuto per il momento) da "disco" // (tra parentesi perchè si tratta di una copia, in memoria, dell'immagine su disco) // a memoria (quella allocata al passo precedente che dovrà contenere l'immagine in memoria del modulo DLL) if (MyMapFile(lpData, lpBaseAddress) == FALSE) { ShowError(L"MyMapFile"); return NULL; } // Fixa la tabella delle rilocazioni if(DoRelocationTable(lpBaseAddress) == FALSE) { ShowError(L"DoRelocationTable"); return NULL; } // Fixa la tabella delle importazioni if (DoImportTable(lpBaseAddress) == FALSE) { ShowError(L"DoImportTable"); return NULL; } // E' necessario impostare i permessi per eseguire del codice dalla memoria allocata che contiene l'immagine della DLL DWORD dwOldProtect = 0; if (VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect) == FALSE) { ShowError(L"VirtualProtect"); return NULL; } // Salva nuovo indirizzo di base della DLL in campo ImageBase di IMAGE_OPTIONAL_HEADER64 if (SetImageBase(lpBaseAddress) == FALSE) { ShowError(L"SetImageBase"); return NULL; } // Avendo caricato la DLL manualmente, se necessario, bisogna chiamare il suo entry point esplicitamente. if (CallDllMain(lpBaseAddress) == FALSE) { ShowError(L"CallDllMain"); return NULL; } return lpBaseAddress; }
DWORD GetSizeOfImage(LPVOID lpData) { // e_lfanew è offset (rispetto a indirizzo di base della DLL) a IMAGE_NT_HEADERS. // Da notare che fino a che non si arriva alle sezioni vere e proprie gli offset tra // i vari header non cambiano tra l'immagine di un modulo su disco e la sua controparte caricata in memoria. // SizeOfImage indica la dimesione del modulo una volta caricato in memoria. DWORD dwSizeOfImage = 0; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); dwSizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage; return dwSizeOfImage; }
BOOL MyMapFile(LPVOID lpData, LPVOID lpBaseAddress) { // SizeOfHeaders indica la dimensione (arrotondata a FileAlignment, che indica l'allineamento delle sezioni su disco) // complessiva di tutti gli header dal DOS header fino a tutti i section header (compresi). // Da notare che fino a che non si arriva alle sezioni vere e proprie le dimensioni dei // vari header non cambiano tra l'immagine di un modulo su disco e la sua controparte caricata in memoria. // NumberOfSections indica il numero di sezioni presenti nel modulo. // Dopo IMAGE_NT_HEADERS ci sono vari section header e per arrivarci // basta sommare l'indirizzo di partenza di IMAGE_NT_HEADERS con la sua dimensione. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); DWORD dwSizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders; WORD wNumberOfSections = pNtHeaders->FileHeader.NumberOfSections; PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)pNtHeaders + sizeof(IMAGE_NT_HEADERS)); // controlla DOS ed NT signature if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE || pNtHeaders->Signature != IMAGE_NT_SIGNATURE) { return FALSE; } // copia gli header da "disco" a memoria. // Da notare che fino a che non si arriva alle sezioni vere e proprie il contenuto dei // vari header non cambiano tra l'immagine di un modulo su disco e la sua controparte caricata in memoria. RtlCopyMemory(lpBaseAddress, lpData, dwSizeOfHeaders); WORD i = 0; LPVOID lpSrcMem = NULL; LPVOID lpDestMem = NULL; DWORD dwSizeOfRawData = 0; // itera finchè ci sono sezioni for (i = 0; i < wNumberOfSections; i++) { if ((pSectionHeader->VirtualAddress == 0) || (pSectionHeader->SizeOfRawData == 0)) { pSectionHeader++; continue; } // PointerToRawData indica l'indirizzo (arrotondato a FileAlignment, che indica l'allineamento delle sezioni su disco) // della sezione corrente. // VirtualAddress in realtà è un Relative Virtual Address (RVA), cioè un offset a partire dall'indirizzo // di base dell'immagine in memoria del modulo, che indica da dove inizia la sezione corrente. // SizeOfRawData indica la dimensione della sezione corrente su disco (arrotondata a FileAlignment) lpSrcMem = (LPVOID)((ULONG_PTR)lpData + pSectionHeader->PointerToRawData); lpDestMem = (LPVOID)((ULONG_PTR)lpBaseAddress + pSectionHeader->VirtualAddress); dwSizeOfRawData = pSectionHeader->SizeOfRawData; // copia sezione da "disco" a memoria RtlCopyMemory(lpDestMem, lpSrcMem, dwSizeOfRawData); pSectionHeader++; } return TRUE; }
BOOL DoRelocationTable(LPVOID lpBaseAddress) { // DataDirectory è un array di IMAGE_DATA_DIRECTORY che descrivono una specifica sezione (.idata, .edata, .reloc, ecc.). // Nel caso specifico di IMAGE_DIRECTORY_ENTRY_BASERELOC, VirtualAddress è un RVA ad un array di blocchi di rilocazione. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); // Itera finchè ci sono blocchi di rilocazione (ogni blocco descrive // i fix da effettuare in una specifica pagina di codice da 4 KB). // L'ultimo blocco ha tutti i campi nulli. // Ogni blocco di rilocazione parte con un IMAGE_BASE_RELOCATION, che specifica RVA della pagina da 4 KB // e dimensione del blocco di rilocazione comprese una serie di WORD che seguono IMAGE_BASE_RELOCATION ed // indicano tipo ed offset (rispetto alla pagina di 4 KB) del fix. while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) { WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION)); int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int i = 0; i < nNumberOfReloc; i++) { // Ogni WORD è divisa con i primi 12 bit che indicano l'offset e i restanti 4 bit che indicano il tipo di fix. // Nei sistemi a 32 bit le rilocazioni sono praticamente solo del tipo IMAGE_REL_BASED_HIGHLOW (il cui valore // esadecimale è 0x3), nei sistemi a 64-bit sono del tipo IMAGE_REL_BASED_DIR64 (0xA) if ((pLocData[i] & 0xF000) == 0xA000) { // indirizzo da fixare = image base address + offset pagina 4 KB + offset rilocazione PULONG_PTR pAddress = (PULONG_PTR)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF)); // fix = image base address reale - image base address come indicato da formato PE della DLL LONGLONG dwDelta = (ULONG_PTR)pDosHeader - pNtHeaders->OptionalHeader.ImageBase; // Da notare che viene fixato il valore a cui punta il puntatore, non l'indirizzo a cui punta. // Per essere chiari, pAddress punta ad un indirizzo che contiene, a sua volta, un indirizzo da fixare. *pAddress += dwDelta; } } pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock); } return TRUE; }
BOOL DoImportTable(LPVOID lpBaseAddress) { // Nel caso specifico di IMAGE_DIRECTORY_ENTRY_IMPORT, VirtualAddress è un RVA ad un array di IMAGE_IMPORT_DESCRIPTOR. // Ogni IMAGE_IMPORT_DESCRIPTOR descrive un modulo da cui dipende il modulo corrente. PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG_PTR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); char *lpDllName = NULL; HMODULE hDll = NULL; PIMAGE_THUNK_DATA lpImportNameArray = NULL; PIMAGE_IMPORT_BY_NAME lpImportByName = NULL; PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL; FARPROC lpFuncAddress = NULL; DWORD i = 0; // Itera finchè ci sono IMAGE_IMPORT_DESCRIPTOR. L'ultimo blocco ha tutti i campi nulli. while (TRUE) { if (pImportTable->OriginalFirstThunk == 0) { break; } // Name è un RVA ad una stringa ASCII che indica il nome del modulo da cui dipende il modulo corrente lpDllName = (char *)((ULONG_PTR)pDosHeader + pImportTable->Name); hDll = GetModuleHandleA(lpDllName); if (hDll == NULL) { // usa LoadLibraryA per caricare tale modulo se non è già stato caricato hDll = LoadLibraryA(lpDllName); if (hDll == NULL) { pImportTable++; continue; } } i = 0; // OriginalFirstThunk e FirstThunk sono due RVA che puntano a due copie differenti e distinte di // array di IMAGE_THUNK_DATA che all'inizio sono uguali. // Un IMAGE_THUNK_DATA è una DWORD in OS a 32-bit e una QWORD in OS a 64-bit dato che è un unione // che contiene, fra i vari campi, dei puntatori come campi più grandi) . // Ogni IMAGE_THUNK_DATA descrive una funzione da importare dalla DLL da cui dipende il modulo corrente. // AddressOfData è un RVA ad un IMAGE_IMPORT_BY_NAME che contiene il campo Name che punta ad una // stringa ASCII contenente il nome della funzione da importare da tale modulo. // Una volta che la DLL da cui dipende il modulo corrente viene caricato in memoria le // IMAGE_THUNK_DATA puntate dal campo FirstThunk vengono fixate nel loro campo Function per // diventare così VA alle relative funzioni importate dalla DLL di dipendenza. lpImportNameArray = (PIMAGE_THUNK_DATA)((ULONG_PTR)pDosHeader + pImportTable->OriginalFirstThunk); lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((ULONG_PTR)pDosHeader + pImportTable->FirstThunk); // itera finchè ci sono IMAGE_THUNK_DATA. L'ultima ha tutti i campi nulli. while (TRUE) { if (lpImportNameArray[i].u1.AddressOfData == 0) { break; } lpImportByName = (PIMAGE_IMPORT_BY_NAME)((ULONG_PTR)pDosHeader + lpImportNameArray[i].u1.AddressOfData); // Se il bit più significativo di IMAGE_THUNK_DATA è settato la funzione è importata con ordinale e non con nome. // I restanti bit indicano l'ordinale. if (lpImportNameArray[i].u1.Ordinal & IMAGE_ORDINAL_FLAG) { lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal - IMAGE_ORDINAL_FLAG)); } else { // prende l'indirizzo della funzione importata lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name); } // fixa campo Function di elementi IMAGE_THUNK_DATA di FirstThunk lpImportFuncAddrArray[i].u1.Function = (ULONG_PTR)lpFuncAddress; i++; } pImportTable++; } return TRUE; }
BOOL SetImageBase(LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); pNtHeaders->OptionalHeader.ImageBase = (ULONG_PTR)lpBaseAddress; return TRUE; }
BOOL CallDllMain(LPVOID lpBaseAddress) { // AddressOfEntryPoint è RVA di entry point del modulo typedef_DllMain DllMain = NULL; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); DllMain = (typedef_DllMain)((LPBYTE)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint); BOOL bRet = DllMain((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL); if (bRet == FALSE) { ShowError(L"DllMain"); } return bRet; }
LPVOID MyGetProcAddress(LPVOID lpBaseAddress, PCCH lpszFuncName) { // Nel caso specifico di IMAGE_DIRECTORY_ENTRY_EXPORT, VirtualAddress è un RVA ad un IMAGE_EXPORT_DIRECTORY. // Un IMAGE_EXPORT_DIRECTORY descrive le funzioni esportate dal modulo. // AddressOfNames è un RVA ad un array di RVA a stringhe ASCII con i nomi delle funzioni esportate con nome. // AddressOfNameOrdinals è un RVA ad un array di ordinali per le funzioni esportate con nome // AddressOfFunctions è un RVA ad array di RVA alle funzioni esportate. // NumberOfNames indica il numero di funzioni esportate con nome. LPVOID lpFunc = NULL; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((ULONG_PTR)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); PDWORD lpAddressOfNamesArray = (PDWORD)((ULONG_PTR)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((ULONG_PTR)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((ULONG_PTR)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; for (i = 0; i < dwNumberOfNames; i++) { // prende nome della funzione lpFuncName = (PCHAR)((ULONG_PTR)pDosHeader + lpAddressOfNamesArray[i]); if (lstrcmpiA(lpFuncName, lpszFuncName) == 0) { // se è quella cercata ne prende il rispettivo ordinale wHint = lpAddressOfNameOrdinalsArray[i]; // usa tale ordinale per indicizare l'array di indirizzi delle funzioni lpFunc = (LPVOID)((ULONG_PTR)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc; }
BOOL MyFreeLibrary(LPVOID lpBaseAddress) { BOOL bRet = FALSE; if (NULL == lpBaseAddress) { return bRet; } bRet = VirtualFree(lpBaseAddress, 0, MEM_RELEASE); lpBaseAddress = NULL; return bRet; }
Come si può notare la DLL caricata manualmente, non avendo il relativo descriptor nel file eseguibile, non viene elencata nei moduli di dipendenza dell'applicazione ma è comunque possibile chiamare uno dei suoi metodi esportati.
Codice sorgente:
Manual_DLL_Loading.zip
Riferimenti:
[1] The Rootkit Arsenal - Blunden
[2] https://ntcore.com/files/pe.htm
[3] https://github.com/DemonGan/Windows-Hack-Programming
Nessun commento:
Posta un commento