# PE Loader # I. Introduction - Sau bài viết về [PE Parser](/cf_Jjw1NTiueW9igAUxP8Q) thì mình thấy Loader là một chủ đề khá dễ chịu. Về cơ bản, mục tiêu của chúng ta ở đây là có thể by-hand thực thi một file PE bằng cách tự load nó trên mem. # II. Content ## Loaded PE - Trước hết mình sẽ tự viết ra một chương trình để Loader của mình có thể load nó vào mem, đơn giản như sau: ```c= #include <windows.h> int main(){ MessageBoxA(NULL, "Hello, World!", "Greeting", MB_OK | MB_ICONINFORMATION); return 0; } ``` - Compile với `gcc`: ```bash= gcc .\Loaded.c -o Loaded.exe ``` - Dưới đây là một vài thông tin của con PE mình sẽ load vào: - File Header ![image](https://hackmd.io/_uploads/r1H8mVl_xg.png) - Optional Header ![image](https://hackmd.io/_uploads/B1sjfEx_le.png) - Data Directory ![image](https://hackmd.io/_uploads/B1iyQNe_ge.png) - Mapping from File (Raw) to Mem (Virtual) ![image](https://hackmd.io/_uploads/BkSzm4x_ge.png) - Imports ![image](https://hackmd.io/_uploads/H1weENgOxl.png) - Relocation ![image](https://hackmd.io/_uploads/ByK-VVgOee.png) - Do chương trình mình code khá đơn giản nên từng này thông tin là có thể load được thành công con PE ở trên. ## General Process - Để load được một con PE lên mem, trước hết cần đặt ra một vài câu hỏi: :::info - Đây có thực sự là một file PE chuẩn không? - Thực thi PE đó chiếm bao nhiêu size trên mem? - Load vào đâu? ::: - Tương ứng với bộ câu hỏi trên là giải pháp của mình: :::success - Kiểm tra `Magic` trong `Dos Header` và `Signature` trong `File Header` - Lấy size của PE trên mem bằng trường `SizeOfImage` trong `Optional Header` - Tạo không gian bằng `VirtualAlloc`, sử dụng trên process của Loader ::: - Theo ý kiến chủ quan của mình, việc file thuộc kiến trúc 32bit hay 64bit cũng đóng vai trò khá quan trọng do chúng có sự khác biệt đáng kể trong việc gọi hàm, tuy nhiên mình sẽ không đề cập trong blog này để đơn giản hóa toàn bộ chương trình. Bên cạnh đó, thay vì sử dụng process của Loader, chúng ta có thể inject PE vào một process khác bằng API, cơ chế khá giống `Reflective PE Injection` - Quá trình load PE lên mem sẽ tuân theo các bước sau: :::warning - Validate file PE - Allocate một vùng nhớ để thực thi PE - Mapping data từ file sang mem, lần lượt như sau: - Copy Headers - Copy Sections - Relocate theo bảng BaseReloc của PE - Fix bảng Imports - Invoke Entry Point của PE ::: ## Detailed Process ### Parsing, Validating and Allocating - Tất nhiên điều đầu tiên khi load PE file là phải đọc file :v. Sau khi đọc file thì mình sẽ có được `fileData` và `fileData` sẽ được sử dụng xuyên suốt quá trình. - Do trước đó đã có bài về [PE Parser](/cf_Jjw1NTiueW9igAUxP8Q) nên phần này mình sẽ lướt qua khá nhanh, đây là phần kiểm tra một file PE hợp lệ: ```c= // Global Var BOOL is64Bit = FALSE; IMAGE_DOS_HEADER* dos = NULL; IMAGE_NT_HEADERS* nt = NULL; DWORD parsing(BYTE* buf) { dos = (IMAGE_DOS_HEADER*)buf; if (dos->e_magic != IMAGE_DOS_SIGNATURE) { return -1; } nt = (IMAGE_NT_HEADERS*)(buf + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) { return -1; } WORD magic = nt->OptionalHeader.Magic; if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) is64Bit = FALSE; else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) is64Bit = TRUE; else return -1; return nt->OptionalHeader.SizeOfImage; } ``` - Ở đây mình đã return thẳng về kích thước của PE file trên mem để thuận tiện cho bước allocate ở dưới: ```c= // Read File DWORD fileSize = GetFileSize(hFile, NULL); BYTE* fileData = (BYTE*)malloc(fileSize); if (!fileData) { MessageBoxA(NULL, "Failed to Malloc", "Error", MB_OK); return EXIT_FAILURE; } DWORD bytesRead = 0; if (!ReadFile(hFile, fileData, fileSize, &bytesRead, NULL)) { MessageBoxA(NULL, "Failed to Read", "Error", MB_OK); return EXIT_FAILURE; } // Parsing and Validate DWORD memSize = parsing(fileData); if (-1 == memSize) { MessageBoxA(NULL, "Invalid PE File", "Error", MB_OK); return EXIT_FAILURE; } // Allocation BYTE* memData = (BYTE*)VirtualAlloc(NULL, memSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (NULL == memData) { MessageBoxA(NULL, "Failed to Allocate", "Error", MB_OK); return EXIT_FAILURE; } ``` ### Mapping: Copy Headers, Sections - Trước hết cần load đủ headers của PE file lên mem: ```c= BOOL copying(BYTE* dst, size_t dstSize, BYTE* src, size_t srcSize) { DWORD headerSize = nt->OptionalHeader.SizeOfHeaders; memcpy(dst, src, headerSize); // Next step: Copy Sections ``` - Sau đó, chúng ta sẽ dựa vào `Sections Header` và `NumberOfSections` để lấy đủ thông tin về các `Sections` sẽ load vào, về cơ bản code sẽ như sau: ```c= //IMAGE_FILE_HEADER* file = &nt->FileHeader; //IMAGE_OPTIONAL_HEADER* optional = &nt->OptionalHeader; //IMAGE_SECTION_HEADER* secStart = (IMAGE_SECTION_HEADER*)((BYTE*)optional + file->SizeOfOptionalHeader); IMAGE_SECTION_HEADER* sec = IMAGE_FIRST_SECTION(nt); if (NULL == sec) { return FALSE; } DWORD secCount = nt->FileHeader.NumberOfSections; for (int i = 0; i < secCount; i++) { BYTE* dstVA = dst + sec[i].VirtualAddress; // Where to write section into BYTE* srcVA = src + sec[i].PointerToRawData; // Data of section size_t size = sec[i].SizeOfRawData; // Size of section if (sec[i].PointerToRawData + sec[i].SizeOfRawData > srcSize || sec[i].VirtualAddress + sec[i].SizeOfRawData > dstSize) { MessageBoxA(NULL, "Invalid Section Bounds", "Error", MB_OK); return FALSE; } memcpy(dstVA, srcVA, size); } ``` - Ta có hai cách để trỏ tới `section` đầu tiên trong `Sections Header`, mình có ghi rõ trong code rồi. Quy trình sẽ là duyệt lần lượt các `section` trong `Sections Header`, và trích xuất đầy đủ data của `sec` đó, lưu data vào `srcVA`, viết vào `dstVA`. - Đôi khi, kích thước data viết vào `dstVA` nhỏ hơn `Virtual Size`, nhưng do sử dụng `VirtualAlloc` để cấp phát, các byte trong page đã mặc định là 0 nên chúng ta không cần phải fill zero nữa. - Sau bước mapping này, chúng ta sẽ có chính xác map đã thấy ở phần đầu tiên: ![image](https://hackmd.io/_uploads/BkSzm4x_ge.png) ### Relocating - PE file có một trường trong Optional Header là ImageBase: ![image](https://hackmd.io/_uploads/rJK3C4x_xe.png) - Đây là vị trí trên mem mà PE file có thể được load vào. Nếu nó được load tại đúng ImageBase, chúng ta sẽ không cần phải relocate nữa. Nhưng trên thực tế, vị trí PE được load trên mem thường khác ImageBase, khi đó cần relocate. - Có thể đưa ra một ví dụ đơn giản như sau: :::success - Giả sử: - Image Base: `0x140000` - Mem Base: `0x240000` - PE file có một biến con trỏ tại fileOffset `0x100` (RVA) tức `0x140100` (VA) - Nếu tiếp tục giữ biến con trỏ ở `0x140100`, chương trình sẽ không sử dụng được, cần phải relocate nó, công thức như sau: ``` MemAddr = (MemBase - ImageBase) + VarAddr MemAddr = (0x240000 - 0x140000) + 0x100 = 0x240100 ``` ::: - Đối với công đoạn này, chúng ta cần lấy thông tin từ section `.reloc`, được trỏ tới bởi trường `IMAGE_DIRECTORY_ENTRY_BASERELOC` trong Data Directory. - Các relocation data được quản lý bởi các block liên tiếp nhau, các block là các struct `IMAGE_BASE_RELOCATION` và theo sau bởi một mảng các `IMAGE_BASE_RELOCATION_ENTRY`: ![image](https://hackmd.io/_uploads/SJPMvTx_ll.png) ![image](https://hackmd.io/_uploads/H1nk8agdeg.png) - Có thể nhìn hình này để hiểu hơn về cách mà data trong relocation table được sắp xếp: ![image](https://hackmd.io/_uploads/HkRGPpl_ee.png) - Do `Entries` được sắp xếp theo mảng, chúng ta cần lấy số `Entries` được khai báo, tính toán theo công thức: ```c= DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); // Count number of entries ``` - Trong mỗi `Entry`, giả sử lấy `0xA060` trong hình, `Entry` lại gồm 4 bit cuối (`0xA`) là `type`, 12 bit còn lại (`0x060`) là `offset`, tính toán như sau: ```c= WORD type = entry >> 12; // last 4 bits WORD offset = entry & 0x0FFF; // remaining bits ``` - Có rất nhiều `type` được định nghĩa sẵn, tuy nhiên chỉ thường gặp các loại sau đây: ``` IMAGE_REL_BASED_HIGH - patch HIWORD IMAGE_REL_BASED_LOW - patch LOWORD IMAGE_REL_BASED_HIGHLOW - patch DWORD IMAGE_REL_BASED_DIR64 - patch 64bit ``` - Code mà mình dùng để xử lý phần này như sau: ```c= BOOL relocating(BYTE* imageBase, ULONGLONG oldBase) { // Information about base relocation table IMAGE_DATA_DIRECTORY* relocDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; // Section .reloc IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)(imageBase + relocDir->VirtualAddress); IMAGE_BASE_RELOCATION* end = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + relocDir->Size); ULONGLONG delta = (ULONGLONG)imageBase - oldBase; while (reloc < end && reloc->SizeOfBlock > 0) { char err[64]; DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); // Count number of entries WORD* entries = (WORD*)(reloc + 1); for (int i = 0; i < count; i++) { WORD entry = entries[i]; WORD type = entry >> 12; // last 4 bits WORD offset = entry & 0x0FFF; // remaining bits BYTE* patchAddress = imageBase + reloc->VirtualAddress + offset; // Go to .reloc then to offset switch (type) { case IMAGE_REL_BASED_LOW: *(PWORD)patchAddress += LOWORD(delta); break; case IMAGE_REL_BASED_HIGH: *(PWORD)patchAddress += HIWORD(delta); break; case IMAGE_REL_BASED_HIGHLOW: *(PDWORD)patchAddress += (DWORD)delta; break; case IMAGE_REL_BASED_DIR64: *(PDWORD64)patchAddress += delta; break; case IMAGE_REL_BASED_ABSOLUTE: break; default: sprintf_s(err, sizeof(err), "Unsupported relocation type: %d", type); MessageBoxA(NULL, err, "Reloc Error", MB_OK | MB_ICONERROR); return FALSE; } } reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock); } return TRUE; } ``` ### Imports - Một phần không thể thiếu trong việc load PE là fix bảng IAT. ![image](https://skr1x.github.io/assets/reflective-loading-memory/iat_patching.png) - Chúng ta cần phải điền đầy đủ các địa chỉ hàm được gọi vào bảng IAT. Trước hết cần access vào `Import Directory` được trỏ tới bằng `IMAGE_DIRECTORY_ENTRY_IMPORT` trong Data Directory. Khi có được vị trí của nó, ta sẽ sử dụng struct `IMAGE_IMPORT_DESCRIPTOR` để phân tích và fix bảng IAT. - Các bước để làm sẽ tương tự như công việc của `Loader` chính hiệu, mình đã ghi chi tiết trong blog [này](https://hackmd.io/@Zupp/RE_Tech_101#41-How-the-program-calls-a-function). - Code xử lí sẽ như sau: ```c= BOOL importing(BYTE* imageBase) { // Information about import directory IMAGE_DATA_DIRECTORY* importDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; IMAGE_IMPORT_DESCRIPTOR* import = (IMAGE_IMPORT_DESCRIPTOR*)(imageBase + importDir->VirtualAddress); DWORD importSize = importDir->Size; while (import->Characteristics || import->FirstThunk) { BYTE* name = (BYTE*)(imageBase + import->Name); HMODULE module = NULL; IMAGE_THUNK_DATA* origFirstThunk = NULL; // INT, used to get function name IMAGE_THUNK_DATA* firstThunk = NULL; // IAT, used to assign function address if (NULL == (module = LoadLibraryA((LPCSTR)name))) return FALSE; // If OriginalFirstThunk = 0, use FirstThunk instead if (import->OriginalFirstThunk) origFirstThunk = (PIMAGE_THUNK_DATA)(imageBase + import->OriginalFirstThunk); else origFirstThunk = (PIMAGE_THUNK_DATA)(imageBase + import->FirstThunk); firstThunk = (PIMAGE_THUNK_DATA)(imageBase + import->FirstThunk); while (origFirstThunk->u1.AddressOfData) { FARPROC funcAddress = NULL; if (origFirstThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) { WORD ordinal = (WORD)(origFirstThunk->u1.Ordinal & 0xFFFF); funcAddress = GetProcAddress(module, (LPCSTR)(uintptr_t)ordinal); if (!funcAddress) { char err[256]; sprintf_s(err, sizeof(err), "Failed to resolve ordinal %u from DLL: %s", ordinal, name); MessageBoxA(NULL, err, "Import Error", MB_OK | MB_ICONERROR); FreeLibrary(module); return FALSE; } } else { IMAGE_IMPORT_BY_NAME* importByName = (IMAGE_IMPORT_BY_NAME*)(imageBase + origFirstThunk->u1.AddressOfData); funcAddress = GetProcAddress(module, (LPCSTR)importByName->Name); } if (!funcAddress) { FreeLibrary(module); MessageBoxA(NULL, "Failed to resolve import", "Import Error", MB_OK | MB_ICONERROR); return FALSE; } firstThunk->u1.Function = (ULONG_PTR)funcAddress; origFirstThunk++; firstThunk++; } import++; } return TRUE; } ``` - Các hàm hỗ trợ trong phần này sẽ là `LoadLibraryA` và `GetProcAddress`, gần tương tự với phương pháp resolve API dynamically trong [Task 3: Dynamic API Resolution](/pxnLm8AlTBy8pV-19WKG5w) của mình. Bên cạnh đó, ta có hai cách để lấy địa chỉ hàm, thông qua `Ordinal` hoặc thông qua `Name`, cả hai cách đều tốt và hiệu quả. ### Invoking - Cuối cùng, để run được file PE đã load, chúng ta sẽ cần trỏ tới `Entry Point` và thực thi, có thể bằng gọi trực tiếp hoặc `CreateThread`: ```c= BOOL invoking(BYTE* imageBase) { union { void* entryPoint; BOOL(WINAPI* dllEntry)(HINSTANCE, DWORD, LPVOID); BOOL(WINAPI* exeEntry)(void); } info; info.entryPoint = imageBase + nt->OptionalHeader.AddressOfEntryPoint; if (nt->FileHeader.Characteristics & IMAGE_FILE_DLL) { if (!info.dllEntry((HINSTANCE)imageBase, DLL_PROCESS_ATTACH, NULL)) return FALSE; if (!info.dllEntry((HINSTANCE)imageBase, DLL_PROCESS_DETACH, NULL)) return FALSE; } else { //info.exeEntry(); DWORD threadId; HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)info.exeEntry, NULL, 0, &threadId); if (hThread == NULL) { DWORD err = GetLastError(); char msg[128]; sprintf_s(msg, sizeof(msg), "Failed to create thread. Error code: %lu", err); MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONERROR); return FALSE; } else WaitForSingleObject(hThread, INFINITE); } return TRUE; } ``` ## Full code - Code của mình tại đây: :::spoiler Manual PE Loader ```c= #include <stdio.h> #include <windows.h> BOOL is64Bit = FALSE; IMAGE_DOS_HEADER* dos = NULL; IMAGE_NT_HEADERS* nt = NULL; DWORD parsing(BYTE* buf) { dos = (IMAGE_DOS_HEADER*)buf; if (dos->e_magic != IMAGE_DOS_SIGNATURE) { return -1; } nt = (IMAGE_NT_HEADERS*)(buf + dos->e_lfanew); if (nt->Signature != IMAGE_NT_SIGNATURE) { return -1; } WORD magic = nt->OptionalHeader.Magic; if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) is64Bit = FALSE; else if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) is64Bit = TRUE; else return -1; return nt->OptionalHeader.SizeOfImage; } BOOL copying(BYTE* dst, size_t dstSize, BYTE* src, size_t srcSize) { DWORD headerSize = nt->OptionalHeader.SizeOfHeaders; memcpy(dst, src, headerSize); //IMAGE_FILE_HEADER* file = &nt->FileHeader; //IMAGE_OPTIONAL_HEADER* optional = &nt->OptionalHeader; //IMAGE_SECTION_HEADER* secStart = (IMAGE_SECTION_HEADER*)((BYTE*)optional + file->SizeOfOptionalHeader); IMAGE_SECTION_HEADER* sec = IMAGE_FIRST_SECTION(nt); if (NULL == sec) { return FALSE; } DWORD secCount = nt->FileHeader.NumberOfSections; for (int i = 0; i < secCount; i++) { BYTE* dstVA = dst + sec[i].VirtualAddress; // Where to write section into BYTE* srcVA = src + sec[i].PointerToRawData; // Data of section size_t size = sec[i].SizeOfRawData; // Size of section if (sec[i].PointerToRawData + sec[i].SizeOfRawData > srcSize || sec[i].VirtualAddress + sec[i].SizeOfRawData > dstSize) { MessageBoxA(NULL, "Invalid Section Bounds", "Error", MB_OK); return FALSE; } memcpy(dstVA, srcVA, size); } return TRUE; } BOOL relocating(BYTE* imageBase, ULONGLONG oldBase) { IMAGE_DATA_DIRECTORY* relocDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; if (relocDir->VirtualAddress == 0 || relocDir->Size == 0) { MessageBoxA(NULL, "No relocation table found.", "Reloc Info", MB_OK | MB_ICONINFORMATION); return TRUE; } IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)(imageBase + relocDir->VirtualAddress); IMAGE_BASE_RELOCATION* end = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + relocDir->Size); ULONGLONG delta = (ULONGLONG)imageBase - oldBase; while (reloc < end && reloc->SizeOfBlock > 0) { DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); WORD* entries = (WORD*)(reloc + 1); for (DWORD i = 0; i < count; i++) { WORD entry = entries[i]; WORD type = entry >> 12; WORD offset = entry & 0x0FFF; BYTE* patchAddress = imageBase + reloc->VirtualAddress + offset; __try { switch (type) { case IMAGE_REL_BASED_ABSOLUTE: // No relocation needed break; case IMAGE_REL_BASED_LOW: *(PWORD)patchAddress += LOWORD(delta); break; case IMAGE_REL_BASED_HIGH: *(PWORD)patchAddress += HIWORD(delta); break; case IMAGE_REL_BASED_HIGHLOW: *(PDWORD)patchAddress += (DWORD)delta; break; case IMAGE_REL_BASED_DIR64: *(PDWORD64)patchAddress += delta; break; default: { char err[128]; sprintf_s(err, sizeof(err), "Unsupported relocation type: %d at RVA 0x%X", type, reloc->VirtualAddress + offset); MessageBoxA(NULL, err, "Relocation Error", MB_OK | MB_ICONERROR); return FALSE; } } } __except (EXCEPTION_EXECUTE_HANDLER) { char err[128]; sprintf_s(err, sizeof(err), "Access violation at address: 0x%p (type: %d)", patchAddress, type); MessageBoxA(NULL, err, "Relocation Error", MB_OK | MB_ICONERROR); return FALSE; } } reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock); } return TRUE; } BOOL importing(BYTE* imageBase) { IMAGE_DATA_DIRECTORY* importDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; if (importDir->VirtualAddress == 0 || importDir->Size == 0) { MessageBoxA(NULL, "No import table found.", "Import Info", MB_OK | MB_ICONINFORMATION); return TRUE; } IMAGE_IMPORT_DESCRIPTOR* import = (IMAGE_IMPORT_DESCRIPTOR*)(imageBase + importDir->VirtualAddress); while (import->Characteristics || import->FirstThunk) { if (import->Name == 0) { MessageBoxA(NULL, "Import descriptor has null DLL name RVA.", "Import Error", MB_OK | MB_ICONERROR); return FALSE; } BYTE* name = imageBase + import->Name; char msg[512] = { 0 }; HMODULE module = LoadLibraryA((LPCSTR)name); if (!module) { sprintf_s(msg, sizeof(msg), "Failed to load DLL: %s\nError code: %lu", name, GetLastError()); MessageBoxA(NULL, msg, "Import Error", MB_OK | MB_ICONERROR); return FALSE; } IMAGE_THUNK_DATA* origFirstThunk = import->OriginalFirstThunk ? (PIMAGE_THUNK_DATA)(imageBase + import->OriginalFirstThunk) : (PIMAGE_THUNK_DATA)(imageBase + import->FirstThunk); IMAGE_THUNK_DATA* firstThunk = (PIMAGE_THUNK_DATA)(imageBase + import->FirstThunk); while (origFirstThunk->u1.AddressOfData) { FARPROC funcAddress = NULL; if (origFirstThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) { // Import by ordinal WORD ordinal = (WORD)(origFirstThunk->u1.Ordinal & 0xFFFF); funcAddress = GetProcAddress(module, (LPCSTR)(uintptr_t)ordinal); if (!funcAddress) { sprintf_s(msg, sizeof(msg), "Failed to resolve ordinal %u from DLL: %s", ordinal, name); MessageBoxA(NULL, msg, "Import Error", MB_OK | MB_ICONERROR); FreeLibrary(module); return FALSE; } } else { // Import by name IMAGE_IMPORT_BY_NAME* importByName = (IMAGE_IMPORT_BY_NAME*)(imageBase + origFirstThunk->u1.AddressOfData); if (!importByName || !importByName->Name) { sprintf_s(msg, sizeof(msg), "Invalid import name pointer in DLL: %s", name); MessageBoxA(NULL, msg, "Import Error", MB_OK | MB_ICONERROR); FreeLibrary(module); return FALSE; } funcAddress = GetProcAddress(module, (LPCSTR)importByName->Name); if (!funcAddress) { sprintf_s(msg, sizeof(msg), "Failed to resolve function: %s from DLL: %s", importByName->Name, name); MessageBoxA(NULL, msg, "Import Error", MB_OK | MB_ICONERROR); FreeLibrary(module); return FALSE; } } // Write resolved address into the IAT firstThunk->u1.Function = (ULONG_PTR)funcAddress; origFirstThunk++; firstThunk++; } import++; } return TRUE; } BOOL invoking(BYTE* imageBase) { union { void* entryPoint; BOOL(WINAPI* dllEntry)(HINSTANCE, DWORD, LPVOID); BOOL(WINAPI* exeEntry)(void); } info; info.entryPoint = imageBase + nt->OptionalHeader.AddressOfEntryPoint; if (nt->FileHeader.Characteristics & IMAGE_FILE_DLL) { if (!info.dllEntry((HINSTANCE)imageBase, DLL_PROCESS_ATTACH, NULL)) return FALSE; if (!info.dllEntry((HINSTANCE)imageBase, DLL_PROCESS_DETACH, NULL)) return FALSE; } else { //info.exeEntry(); DWORD threadId; HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)info.exeEntry, NULL, 0, &threadId); if (hThread == NULL) { DWORD err = GetLastError(); char msg[128]; sprintf_s(msg, sizeof(msg), "Failed to create thread. Error code: %lu", err); MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONERROR); return FALSE; } else WaitForSingleObject(hThread, INFINITE); } return TRUE; } int main(int argc, char* argv[]) { if (argc < 2) { MessageBoxA(NULL, "Usage: ManualPELoader <path_to_pe_file>", "Error", MB_OK); return EXIT_FAILURE; } const char* pe = argv[1]; HANDLE hFile = CreateFileA(pe, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { DWORD err = GetLastError(); char msg[128]; sprintf_s(msg, sizeof(msg), "Failed to open file. Error code: %lu", err); MessageBoxA(NULL, msg, "Error", MB_OK | MB_ICONERROR); return EXIT_FAILURE; } // Validation DWORD fileSize = GetFileSize(hFile, NULL); BYTE* fileData = (BYTE*)malloc(fileSize); if (!fileData) { MessageBoxA(NULL, "Failed to Malloc", "Error", MB_OK); return EXIT_FAILURE; } DWORD bytesRead = 0; if (!ReadFile(hFile, fileData, fileSize, &bytesRead, NULL)) { MessageBoxA(NULL, "Failed to Read", "Error", MB_OK); return EXIT_FAILURE; } DWORD memSize = parsing(fileData); if (-1 == memSize) { MessageBoxA(NULL, "Invalid PE File", "Error", MB_OK); return EXIT_FAILURE; } // Allocation BYTE* memData = (BYTE*)VirtualAlloc(NULL, memSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (NULL == memData) { MessageBoxA(NULL, "Failed to Allocate", "Error", MB_OK); return EXIT_FAILURE; } // Copying if (!copying(memData, memSize, fileData, fileSize)) { MessageBoxA(NULL, "Copying sections failed", "Error", MB_OK); return EXIT_FAILURE; } // Relocation ULONGLONG oldBase = (&nt->OptionalHeader)->ImageBase; if (!relocating(memData, oldBase)) { MessageBoxA(NULL, "Relocation failed", "Error", MB_OK); return EXIT_FAILURE; } // Imports if (!importing(memData)) { MessageBoxA(NULL, "Import resolution failed", "Error", MB_OK); return EXIT_FAILURE; } // Invoke DWORD status = invoking(memData); if (FALSE == status) { MessageBoxA(NULL, "Failed to Invoking", "Error", MB_OK); return EXIT_FAILURE; } else { MessageBoxA(NULL, "Success to Load PE Manually", "Success", MB_OK); // Cleanup CloseHandle(hFile); free(fileData); } return EXIT_SUCCESS; } ``` ::: - Trong code, mình vẫn khai báo đầy đủ việc check xem PE file là 32 bit hay 64 bit để tiện debug và cải thiện code về sau. ## Test - Run `Loaded.exe`: ![image](https://hackmd.io/_uploads/rkNAp6lugl.png) - Manual Load `Loaded.exe`: ![image](https://hackmd.io/_uploads/rJQZR6lOgl.png) - Manual Load `calc.exe`: ![image](https://hackmd.io/_uploads/SJkNCpxOeg.png) - Manual Load `notepad.exe`: ![image](https://hackmd.io/_uploads/BkN8C6edgl.png) > Riêng với `notepad.exe` thì mình bị lỗi thư viện nên load không thành công. Một vài thông tin cho rằng `COMCTL32.dll` không còn ở trên Wins 11 nên không thể load được bình thường nhưng mình cũng không fix được :v > Bên cạnh đó, thông qua việc test này, mình cũng nhận ra Loader của mình đang chỉ ở dạng đơn giản, chưa tùy biến được để phù hợp với hầu hết các file PE, đặc biệt là `.NET` do cách vận hành của nó khác hoàn toàn so với chương trình viết bằng C/C++. # III. Wrapping up - Loader trong blog của mình thuộc dạng khá đơn giản, nhưng mình nghĩ nó đã khá đầy đủ để chia sẻ kiến thức về PE Loader của mình cho cộng đồng bởi đây là một mảng kiến thức khá hay và quan trọng. Hi vọng bài viết này hữu ích với các bạn. Dear!!! # IV. References - [Ambray](https://www.ambray.dev/writing-a-windows-loader/) - [Skr1x](https://skr1x.github.io/reflective-loading-portable-executable-memory/) - [Captain-Woof](https://captain-woof.medium.com/how-to-write-a-local-pe-loader-from-scratch-for-educational-purposes-30e10cd88abc) ![image](https://skr1x.github.io/assets/reflective-loading-memory/pe_inmem.png)