iT邦幫忙

2025 iThome 鐵人賽

DAY 18
1
Security

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

Day18 - 幽影巡查,黑暗中的死亡筆記本:Windows 進程枚舉術(上)

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

Yo 歡迎回來,我是 CX330。昨天介紹了 Shellcode injection 的技術,其中我們的第一個步驟就是要枚舉所有的進程。而我們目前到現在,使用的方式都是用 CreateToolHelp32Snapshot,而現在,我們要另外介紹兩種不同的方式,會分成上下兩篇來介紹。

那我們開始囉!

完整程式碼可於此處找到:https://black-hat-zig.cx330.tw/Advanced-Malware-Techniques/Process-Enumeration/enum_processes/

疊甲

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

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

Zig 版本

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

使用 EnumProcess

歡迎,又有一個新的 Windows API 來和大家見面了。我們先來看一下微軟的文檔吧!

BOOL EnumProcesses(
  [out] DWORD   *lpidProcess,
  [in]  DWORD   cb,
  [out] LPDWORD lpcbNeeded
);

注意,這邊函數會回傳 PIDs 的陣列回來,但是它並不會包含與之對應的進程名稱。這對我們來說就會有個麻煩,就是比如我們選定了目標進程為 notepad.exe,但現在只有 PID,我們便無法確認哪個才是我們的目標。

要解決這樣的問題,我們需要再多用幾個不同的 Windows API,分別是 OpenProcessEnumProcessModulesGetModuleBaseNameW。這邊來介紹一下它們。

  • OpenProcess
    • 會開啟一個擁有 PROCESS_QUERY_INFORMATIONPROCESS_VM_READ 存取權限的 PID 句柄
  • EnumProcessModules
    • 用來枚舉開啟的進程中的所有模組(Module)
  • GetModuleBaseNameW
    • 用來確定進程的名稱

那使用 EnumProcess 來沒舉進程究竟有什麼優點呢?舉例而言,如果我們是使用 CreateToolhelp32Snapshot 的方法,會建立一個快照並進行字串比對,以確定進程名稱是否與目標進程匹配。但問題是,當有多個相同進程的實例(Instance)在不同的權限等級(Privilege levels)上運行時,我們無法透過字串的比較來區分它們。例如,有些 svchost.exe 進程以普通使用者權限運行,而其他則以較高的權限運行,但在字串比較中無法確定 svchost.exe 的權限等級。因此,唯一能確定該進程是否運行於高權限的方法是去判斷 OpenProcess 呼叫是否會失敗(如果我們的程式是跑在普通使用者權限的話)。如果失敗,則代表該進程是運行在高權限中的。

但如果我們是使用 EnumProcess,它在過程中就會提供我們 PID 和句柄,我們再透過 PID 去獲取進程名稱,這樣就不會有無法判斷進程的權限的問題了,因為如果打不開,就代表是高權限的。

程式碼範例

printProcesses 函數

在這個函數中,我們會把所有被列舉的進程的名稱和 PID 都打印出來。當然,只有和我們的程式相同或更低權限的進程的資訊才會被獲取,如果是更高權限的進程則無法獲取到資訊,這會導致 ERROR_ACCESS_DENIED 錯誤。

我們的判斷標準是去看 OpenProcess 是否可以正確被呼叫,可以開啟的則可以作為目標,反之則不然。

一開始,我們會先去把系統中所有進程的 PID 都寫到我們的變數裡。

    // Get the array of PIDs in the system
    if (EnumProcesses(&processes, @sizeOf(@TypeOf(processes)), &return_len) == 0) {
        std.debug.print("[!] EnumProcesses Failed With Error: {}\n", .{GetLastError()});
        return;
    }

注意,這邊的 processesvar processes: [1024 * 2]DWORD = undefined;。接著,我們可以去計算我們獲取到的進程數量。

    // Calculate the number of elements in the array returned
    const number_of_pids = return_len / @sizeOf(DWORD);
    std.debug.print("[i] Number Of Processes Detected: {}\n", .{number_of_pids});

最後,就是針對我們的進程數量做迭代,去把每個進程的 PID 都用 OpenProcess 來嘗試獲取句柄,如果成功,再使用 EnumProcessModules 去獲取該進程中的模組,然後再用 GetModuleBaseNameW 從模組獲取進程的名稱。

    for (0..number_of_pids) |i| {
        if (processes[i] != 0) {
            // Open a process handle with limited access
            if (OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, processes[i])) |h_process| {
                var h_module: HMODULE = undefined;

                // Get a handle of a module in the process
                if (EnumProcessModules(h_process, &h_module, @sizeOf(HMODULE), &return_len2) != 0) {
                    var proc_name_buffer: [MAX_PATH]u16 = undefined;

                    // Get the name of the process
                    if (GetModuleBaseNameW(h_process, h_module, &proc_name_buffer, proc_name_buffer.len) != 0) {
                        // Find the null terminator
                        var name_len: usize = 0;
                        for (proc_name_buffer) |char| {
                            if (char == 0) break;
                            name_len += 1;
                        }

                        // Convert UTF-16 to UTF-8 for printing
                        var utf8_name: [MAX_PATH * 2]u8 = undefined;
                        if (std.unicode.utf16LeToUtf8(&utf8_name, proc_name_buffer[0..name_len])) |utf8_len| {
                            std.debug.print("[{:0>3}] Process \"{s}\" - Of Pid: {}\n", .{ i, utf8_name[0..utf8_len], processes[i] });
                        } else |_| {
                            std.debug.print("[{:0>3}] Process [encoding error] - Of Pid: {}\n", .{ i, processes[i] });
                        }
                    } else {
                        std.debug.print("[!] GetModuleBaseName Failed [At Pid: {}] With Error: {}\n", .{ processes[i], GetLastError() });
                    }
                } else {
                    std.debug.print("[!] EnumProcessModules Failed [At Pid: {}] With Error: {}\n", .{ processes[i], GetLastError() });
                }

                _ = CloseHandle(h_process);
            }
        }
    }

getRemoteProcessHandle 函數

剛剛的 printProcesses 函數只是把所有的進程都打印出來,但這個函數則是剛剛的函數的再包裝,會回傳指定進程的句柄。它需要傳入一個目標的進程名稱,getRemoteProcessHandle 則會回傳該遠端進程的句柄。

它的大部分邏輯都和 printProcesses 差不多,不過在 OpenProcess 的權限設置中,我們會把它從 PROCESS_QUERY_INFORMATION | PROCESS_VM_READ 改為 PROCESS_ALL_ACCESS,這樣我們拿到的句柄就會有更多的權限,讓我們再後續操作的時候可以使用更為強大的進程句柄。

然後裡面會使用字串的比對,去比較是否為我們需要的目標進程。

// Compare process names
if (std.mem.eql(u16, proc_name, proc_name_buffer[0..name_len])) {
    return ProcessInfo{
        .pid = processes[i],
        .handle = h_process,
    };
}

執行結果

我們先來看一下使用 printProcesses 的執行結果會長怎樣吧。

printProcesses

測試完成後,我們會把 printProcesses 給註解掉,因為我們只需要獲取目標進程的句柄而已,我們來測試一下 getRemoteProcessHandle 吧。我們在這邊把目標進程設定為 svchost.exe 試試。

getRemoteProcessHandle

成功獲取到了我們目標的進程了!

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

結束囉!今天跟大家介紹了使用 EnumProcess 來進行進程枚舉的方法,明天會再來介紹另外一種方法~。那就先這樣囉!

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


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

1 則留言

1
tt27
iT邦新手 5 級 ‧ 2025-10-03 19:12:28

走在時代的前沿

我要留言

立即登入留言