Day29 - 地獄之門,直達內核:Hell's Gate 的直接系統呼叫術(上)

走在時代前沿的前言

嗨嗨大家好,倒數第二天!昨天跟大家介紹了 Heaven’s Gate 的技術,今天要來介紹一個名稱跟它很像,但是八竿子打不著的 Hell’s Gate 技術啦!

本來是打算一篇寫完的,但寫著寫著發現篇幅有點過長,就把它拆成上下兩篇吧!今天的內容主要會是 Hell’s Gate 的一些前情提要,然後稍微先看一下 Hell’s Gate 中會使用到的結構體。至於 Hell’s Gate 的詳細實作方式還有 Zig 的完整程式碼會於明天的文章中

話說今天已經是倒數第二天了,沒想到自己真的會完賽,感謝陪我到這邊的各位,希望大家都有收穫!

疊甲

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

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

Zig 版本

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

Windows 的系統呼叫

系統呼叫,又稱為系統調用(system call,常簡稱 syscall)。

系統呼叫是使用者空間(user space)的程式向作業系統核心請求服務的唯一正式入口,例如開檔、建立線程、建立進程等等,都是需要透過系統呼叫完成的任務。

在 Windows 中使用系統呼叫最最最常見的方法,就是透過呼叫常規的 Windows API。Windows API 通常會包含系統呼叫的實作,例如,當程式設計師呼叫 CreateFile 的時候,程式實際上會呼叫內部實作的 NtCreateFile

所有的系統調用都會回傳一個 NTSTATUS 的值,代表著錯誤碼。如果我們去查看官方文檔,會發現它記錄著不同的系統調用的回傳值。例如如果我們呼叫成功,系統呼叫就會回傳 STATUS_SUCCESS

大部分的系統呼叫都沒有被微軟的文檔給紀錄,因此有很多逆向工程師會去對那些 Binary 做逆向工程,並把它們發現的結果記錄在第三方的非官方文檔中。以下舉一些例子,這些都會是未來很好用的資源:

大部分的系統呼叫都是從 ntdll.dll 所匯出的。使用系統呼叫可以提供比標準的 API 更多的選項,同時,它還可以繞過一些基於主機的安全解決方案。

Zw/Nt 系統呼叫

在 Windows 中,有兩種系統呼叫,分別是以 ZwNt 開頭的。

Nt 系統呼叫是使用者模式程式的主要介面,大部分 Windows 應用程式都是透過它來進行系統呼叫。另一方面,Zw 系統呼叫屬於更底層、核心模式的介面,通常用於驅動程式和其他需要直接訪問作業系統的核心程式碼。

雖然在使用者模式中,兩者都能被呼叫並完成相同的功能,但是 NtZw 的系統呼叫實際上是使用相同的記憶體地址,這代表著它們在內部其實是同一套實作。我們可以用以下的 C 語言程式碼來證明並驗證這件事。

#include <stdio.h>
#include <windows.h>

typedef NTSTATUS (NTAPI *NtAllocateVirtualMemory_t)(
    HANDLE    ProcessHandle,
    PVOID     *BaseAddress,
    ULONG_PTR ZeroBits,
    PSIZE_T   RegionSize,
    ULONG     AllocationType,
    ULONG     Protect
);

int main() {
    HMODULE ntdll = GetModuleHandleA("ntdll.dll");
    if (!ntdll) {
        printf("Failed to load ntdll.dll\n");
        return 1;
    }

    FARPROC pNt = GetProcAddress(ntdll, "NtAllocateVirtualMemory");
    FARPROC pZw = GetProcAddress(ntdll, "ZwAllocateVirtualMemory");

    if (!pNt || !pZw) {
        printf("Failed to get function addresses.\n");
        return 1;
    }

    printf("NtAllocateVirtualMemory address: %p\n", pNt);
    printf("ZwAllocateVirtualMemory address: %p\n", pZw);

    if (pNt == pZw) {
        printf("[+] They point to the same function!\n");
    } else {
        printf("[-] They are different!\n");
    }

    return 0;
}

// Output:
// NtAllocateVirtualMemory address: 00007ffc24b8d7e0
// ZwAllocateVirtualMemory address: 00007ffc24b8d7e0
// [+] They point to the same function!

系統服務編號(SSN)

每一個系統呼叫都會有一個特殊的編號,稱為系統服務編號(System Service Number,SSN),它們就像 Linux 中的 syscall 編號一樣。這是一個用於系統內部的識別符,用來表示不同的系統服務,例如記憶體分配、讀寫文件等等。它不是給開發者使用的,而是給作業系統核心用來分派的。當我們在使用者模式嘗試執行系統呼叫時,會依照當前系統是 32 位或 64 位平台,分別把 SSN 設在 eaxrax 暫存器中,讓核心知道我們要使用的是哪一項系統服務。

不過值得注意的一點是,隨著 Windows 的版本和構建不同,SSN 也會隨之改變。和 Linux 上的 syscall 編號不同,它並不是一個固定的數字,不同的 Windows 版本會重新排列系統服務的位置,所以我們並不能在程式碼中寫死 SSN 的值

但在同一台機器上,SSN 編號並不會是完全隨機的,它們之間會存在一定的關係。記憶體中的每個系統呼叫的編號都會等於前一個 SSN + 1。下圖會更好地說明這件事。

SSN

如圖所示,所有系統呼叫的結構都相同;我們可以透過下方的程式碼片段來觀察其系統呼叫模式。

mov r10, rcx
mov eax, <SSN>
syscall
ret

在 64 位元的 Windows 呼叫慣例(calling convention)中,第一個參數存放在 rcx,但是 syscall 指令會預期參數存放在 r10 裡面,所以我們用 mov r10, rcx 來滿足這個要求。之後,SSN 會被移動到 eax 暫存器中,並通過 syscall 來觸發我們需要的系統呼叫功能。

對於 64 位元的系統,我們會用 syscall 來觸發系統呼叫;而在 32 位元的系統上,則是使用 sysenter。執行 syscall 指令將會導致進程從使用者模式轉換到核心模式。系統核心將會去執行我們指定的操作,並在完成後把控制權返還給用戶模式。

不過如果我們再次查看圖中的指令,我們可以看到其中還有兩條剛剛沒說到的指令,也就是 testjne。它們的存在是因為 WoW64 子系統,會讓 32 位元的進程可以在 64 位元的機器上運行。不過當今天的進程是 64 位元的時候,這兩個指令並不會影響正常的控制流程。

Hell’s Gate

Hell’s Gate 是一種**直接系統呼叫(direct system call)**的技巧。前面提到,SSN 不是在程式碼裡寫死,就是在執行時透過「按地址排序系統呼叫」的方法取得;但 Hell’s Gate 採用不同的做法:在執行期間從 ntdll 動態取得 SSN。要達成這件事可以有多種路徑與實作方式:

  • 使用 GetModuleHandleAGetProcAddress
  • PEB 遍歷(PEB walk)結合 EAT 解析(EAT parsing)

在這邊,最簡單的方式是第一種方法,我們可以先使用 GetModuleHandleA 獲取 ntdll.dll 的基址,然後再使用 GetProcAddressntdll.dll 裡面獲取原生函數的記憶體位址。但如果我們以 OPSEC 的角度來講,這並不會是最佳的實作方式,因為如果安全解決方案 Hook 了 GetModuleHandleA 或是 GetProcAddress 的其中一個,那我們就慘了。這就是為什麼我們會需要 Hell’s Gate,它使用的便是第二種方法。

Hell’s Gate 中的結構體

在這邊,我們會去看一下 Hell’s Gate 論文中的原始實作方式。這篇論文裡面的 Hell’s Gate 是由 @am0nsec@RtlMateusz 用 C 語言撰寫的。

接下來我們先來看一下 Hell’s Gate 的實作吧!在 Hell’s Gate 的組合語言裡面,它定義了兩個結構,分別為 _VX_TABLE_ENTRY_VX_TABLE。我們先來看一下第一個結構。

系統呼叫結構體

typedef struct _VX_TABLE_ENTRY {
 PVOID   pAddress;             // The memory address of a syscall function
 DWORD64 dwHash;               // The hash value of the syscall name
 WORD    wSystemCall;          // The SSN of the syscall
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;

_VX_TABLE_ENTRY 代表的是一個系統呼叫,例如 NtAllocateVirtualMemory 會被表示為 VX_TABLE_ENTRY NtAllocateVirtualMemory。在這個結構裡面會包含系統呼叫函數的記憶體地址、該系統呼叫的 hash 值以及它的 SSN。

系統呼叫表

typedef struct _VX_TABLE {
 VX_TABLE_ENTRY NtAllocateVirtualMemory;
 VX_TABLE_ENTRY NtProtectVirtualMemory;
 VX_TABLE_ENTRY NtCreateThreadEx;
 VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;

_VX_TABLE 的結構裡面會有若干個 _VX_TABLE_ENTRY 結構。如同剛才所說,裡面的每個 _VX_TABLE_ENTRY 都代表著一個 Hell’s Gate 會用到的系統呼叫。我們可以把 _VX_TABLE 想像成是一個函數表,包含所有我們的 Hell’s Gate 會使用到的系統呼叫的所有資料。

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

OKOK!那就先這樣吧。

明天的文章內容會緊接著這篇所聊到的觀念和提到的這兩個結構體開始,深入去看一下 Hell’s Gate 的實作方式,然後也會用 Zig 來實作一次給大家參考。

完整的 Zig 程式碼一樣可以在明天文章中的前言裡找到

好!那就這樣吧!明天一起來慶祝一下我們完賽了吧!

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