嗨大家我又來了,我 CX330。昨天我們已經初探了 Windows 的架構和記憶體分配等等的,今天要來跟大家聊聊 Windows API!
Windows API 是微軟提供給開發者讓其跟 Windows 作業系統互動的方式,比如如果應用程式需要在螢幕上顯示一個視窗、要修改檔案或是查詢註冊表(Registry),那就會需要用到 Windows API。
好啦那廢話不多說,蜥蜴軍團們,我們開始囉!
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
Windows API 有很多除了 C 語言的基本資料型態(如 int
、flaot
等)外的資料型別,這些都有被文檔紀錄,可以在這邊看到。以下會列出一些常見的資料型別:
DWORD
SIZE_T
VOID
PVOID
HANDLE
HMODULE
LPCSTR/PCSTR
const char*
等效LPSTR/PSTR
LPCSTR/PCSTR
唯一的區別在於 LPSTR/PSTR
不是指向常數字串,而是指向可讀寫的字串,與 char*
等效LPCWSTR/PCWSTR
const wchar*
等效LPWSTR/PWSTR
LPCWSTR/PCWSTR
唯一的區別在於 LPWSTR/PWSTR
不是指向常數字串,而是智想可讀寫的字串,與 wchar*
等效wchar_t
wchar
,用來表示寬字符ULONG_PTR
Windows API 允許開發人員可以直接宣告資料型別或是資料型別的指針,這會在資料型態的名稱中有所體現。以 "P" 開頭的資料型別代表著該資料型別的指針,而沒有 "P" 的代表該資料型別本身,例如:
PHANDLE
等於 HANDLE*
PSIZE_T
等於 SIZE_T*
PDWORD
等於 DWORD*
在 Windows API 的很多函數中都會看到兩個不同的版本,分別會以 "A" 結尾或是 "W" 結尾。舉例來說,我們有 CreateFileA
和 CreateFileW
。結尾為 "A" 的表示 ANSI,而結尾為 "W" 的表示 Unicode 或是 Wide。
身為開發人員(惡意程式開發人員也是開發人員),最重要的事情是要記住,ANSI 函數會接受 ANSI 資料型別作為參數,而 Unicode 函數則要接收 Unicode 型別。像是 CreateFileA
的第一個參數是 LPCSTR
而 CreateFileW
的第一個參數則為 LPCWSTR
。
我們可以看一下以下的比較,來更清楚的知道兩者的區別。
char str1[] = "CX330"; // 6 bytes (5 + null byte)
wchar str2[] = L"CX330"; // 12 bytes, each char is 2 bytes
在 Windows API 中有兩種參數,入參和出參。IN
參數是一個傳遞給函數的參數,用於作爲輸入;而 OUT
參數則用於將值回傳給函數的呼叫者(Caller)。出參通常會通過指針去傳遞引用(Reference)。
這邊舉個例子,有個叫作 ShareThisArticle
的函數(我亂取的,快按讚訂閱分享!),會接收一個整數的真並將其值設置為 123。這會被當作是一個出參,因為參數返回了一個新的值。
BOOL ShareThisArticle(OUT int* num) {
*num = 123;
return True;
}
int main() {
int a = 0;
ShareThisArticle(&a);
}
值得注意的是,這個 OUT
或是 IN
的關鍵字目的是為了讓開發者更好理解函數的期望參數和對參數的處理過程,加與不加並不會影響函數的運行和編譯,也不會影響該參數為入參或出參。
或許大家會覺得今天講的內容很無趣,不過理解這些命名慣例和資料型別等等,都對於日後在閱讀 Microsoft 提供的 Windows API 文檔很有幫助。
在本系列中,我們會使用兩種方式來調用 Windows API,分別是:
extern
該函數我們一個一個來看看,首先我們看一下第一種方式。在 Zig 語言的標準函式庫裡面有一個已經封裝好的一些常見的 Windows API 和 Windows 資料型別,在 std.os.windows
模組之中。比如說我們要獲取一個 CreateProcessW
函數,只需要透過以下的程式碼就可以了。
const std = @import("std");
const win = std.os.windows;
const CreateProcessW = win.CreateProcessW;
這樣一來,我們就可以調用 CreateProcessW
了!
那接著來看一下第二個方法,就是使用 extern
關鍵字來直接宣告目標 DLL 的 Windows API 函數原形。比如說我們要使用 CreateFileW
這個函數,就可以像這樣:
const std = @import("std");
const win = std.os.windows;
extern fn CreateFileW(
lpFileName: [*:0]const u16,
dwDesiredAccess: win.DWORD,
dwShareMode: win.DWORD,
lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES,
dwCreationDisposition: win.DWORD,
dwFlagsAndAttributes: win.DWORD,
hTemplateFile: ?win.HANDLE,
) callconv(win.WINAPI) win.HANDLE;
在下一個章節中,我們會寫一個範例,用 Zig 來呼叫 CreateFileW
並建立文件。
我們來示範一下 CreateFileW
函數的使用。我們在不確定一個函數的功能的時候,請務必要去閱讀一下參考文件或是官方的文檔,以確保該函數的行為和我們的預期相同。CreateFileW
的文檔在這邊,我幫大家把文檔中的函數原形放過來。
HANDLE CreateFileW(
[in] LPCWSTR lpFileName,
[in] DWORD dwDesiredAccess,
[in] DWORD dwShareMode,
[in, optional] LPSECURITY_ATTRIBUTES lpSecurityAttributes,
[in] DWORD dwCreationDisposition,
[in] DWORD dwFlagsAndAttributes,
[in, optional] HANDLE hTemplateFile
);
閱讀後會發現,在文檔中提到如果函數成功執行,回傳值將會是對指定檔案、設備、命名管道或 Mail slot 的開啟的句柄(Handle),因此 CreateFileW
將會回傳 我們建立的物件的 HANDLE
資料型別。
此外,這個函數的所有參數都是 in
參數,代表函數不會從參數回傳任何資料。在這邊想提一下,Windows API 官方文檔中的方括號中的字,包括 in
、out
、optional
等都只是為了讓開發者參考,並沒有什麼實際的影響。
接著我們就用 Zig 語言來示範一下這個函數的用法,用於在我的當前目錄建立一個 Hi.txt
。
const std = @import("std");
const win = std.os.windows;
extern fn CreateFileW(
lpFileName: [*:0]const u16,
dwDesiredAccess: win.DWORD,
dwShareMode: win.DWORD,
lpSecurityAttributes: ?*win.SECURITY_ATTRIBUTES,
dwCreationDisposition: win.DWORD,
dwFlagsAndAttributes: win.DWORD,
hTemplateFile: ?win.HANDLE,
) callconv(win.WINAPI) win.HANDLE;
extern fn CloseHandle(
hObject: win.HANDLE,
) callconv(win.WINAPI) win.BOOL;
pub fn main() !void {
const path: [:0]const u16 = &[_:0]u16{
'H', 'i', '.', 't', 'x', 't', 0,
};
const h = CreateFileW(
path,
win.GENERIC_WRITE,
0,
null,
win.CREATE_ALWAYS,
win.FILE_ATTRIBUTE_NORMAL,
null,
);
if (h == win.INVALID_HANDLE_VALUE) return error.CreateFailed;
_ = CloseHandle(h);
}
這邊的 main
函數中會先定義一個 path
,是個空終止的 UTF-16 寬字串,每個字元為 16 位元。然後傳入給 CreateFileW
的參數如下:
path
GENERIC_WRITE
0
null
CREATE_ALWAYS
FILE_ATTRIBUTE_NORMAL
null
之後這個 CreateFileW
會回傳一個文件句柄給 h
,並會去檢查是否錯誤。最後再用 CloseHandle(h)
關掉句柄並把回傳值丟掉給 _
。
當函數執行失敗的時候,它們通常都會回傳很詳細的錯誤。像是如果 CreateFileW
執行失敗,它會回傳 INVALID_HANDLE_VALUE
,表示無法建立檔案。要了解檔案無法建立的原因,我們可以使用 GetLastError
函數來取得錯誤碼。
取得錯誤碼後,需要查詢 Windows 的系統錯誤碼清單,以下為一些常見的錯誤碼。
5
2
87
大家如果還記得昨天的內容,我們說過 NTAPIs 主要是從 ntdll.dll
匯出的。和普通的 Windows API 不同的是,它們不能透過 GetLastError
取得錯誤碼,相反,它們直接回傳錯誤,該錯誤碼會以 NTSTATUS
資料型別表示。
NTSTATUS
用來表示系統調用(System call)或是函數的狀態,並定義為 32 位無符號整數。成功的系統調用將返回 STATUS_SUCCESS
,其值為 0。相反的,若是調用失敗,它將反為非 0 的值。這時候我們為了進一步知道錯誤的原因,我們可以把該 NTAPI 函數的回傳值打印出來,並檢查 Microsoft 對 NTSTATUS
的文檔。
另一種檢查回傳值的方式是透過這裡提到的 NT_SUCCESS
這個 Macro。這個 Macro 會在這個函數成功的時候回傳 TRUE
,反之回傳 FALSE
。
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
這邊舉個 C 程式碼的例子,看一下如何使用這個 Macro。
NTSTATUS STATUS = ExampleNativeSyscall(...);
if (!NT_SUCCESS(STATUS)) {
printf("ExampleNativeSyscall failed with status: 0x%08x\n", STATUS);
}
結束囉!我要去吃晚餐了。今天帶大家了解了 Windows API 的各種資料型別和一些小知識點,也跟大家介紹了 Zig 如何使用 Windows API。明天應該會來詳細了解一下 PE 文件格式(這主題稍大,但是我明天超級無敵忙,救命,希望有時間可以寫完)。
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!