iT邦幫忙

2025 iThome 鐵人賽

DAY 6
1
Security

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

Day06 - 初探窗內的世界:Windows 架構與記憶體管理簡介

  • 分享至 

  • xImage
  •  

走在時代前沿的前言

嗨大家,我 CX330。今天事情有點多,不知道寫不寫得完 XD。

昨天聊了 Shellcode 和 Payload 的放置等等,今天想要來把窗戶打開,揭開它的神秘面紗,會和大家一起初步認識 Windows 的架構和 Windows 如何做記憶體管理等等的。

好啦那就開始囉!

疊甲

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

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

Zig 版本

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

Windows 架構

Windows 作業系統中的處理器可以跑在兩種不同的模式上:User mode 和 Kernel mode。一般的應用程式會是在 User mode 之下運行,而操作系統的元件們則會在 Kernel mode 運行。如果今天一般的應用程式想要完成某些系統任務,例如建立檔案,那它其實是不能獨立做完這件事的。它需要去遵守某個函數呼叫的流程,才能夠請 Kernel 幫忙做這件事。我們可以看一下以下的圖。

Windows Architecture - https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/overview-of-windows-components

  • User application
    • 由使用者執行的應用程式,比如:小算盤、Google Chrome、Microsoft Word 等等
  • Subsystem DLLs
    • 包含使用者進程(使用者所開啟的應用程式)所調用的 API 的 DLL。在這邊的範例是 kernel32.dll 匯出 CreateFile Windows API 函數。還有一些其他常見的 Subsystem DLL 包括 ntdll.dlladvapi32.dlluser32.dll
  • NTDLL.DLL
    • 導出許多原生 API(Native API 或是 NTAPI)
    • 系統範圍的 DLL,是可用於 User mode 的最底層。這是一個特殊的 DLL,用於從 User mode 到 Kernel mode 的轉換
  • Kernel
    • Windows 核心
    • 他會調用核心模式中可用的其他驅動程式和模組等
    • Windows 核心有一部份存儲在 C:\Windows\System32 下的 ntoskrnl.exe 的檔案之中

函數呼叫流程

下圖顯示了一個建立檔案的應用程式的呼叫流程的範例。它會從使用者進程調用 kernel32.dll 中的 CreateFile 這個 Windows API 開始。kernel32.dll 是一個很重要的 DLL,它會將 Windows API 導出給一般應用程式使用,所以可以看到大部分應用程式都會 Import 這個 DLL。接下來,CreateFile 會去調用 ntdll.dll 裡面相對應的 NTAPI 函數,NtCreateFile。然後 ntdll.dll 會執行一個組合語言指令 sysenter 或是 syscall (取決於機器為 x86 或 x64)並轉移到 Kernel mode。最後,使用 NtCreateFile 調用 Kernel 的驅動和模組來完成這個任務。

Function Call Flow - https://medium.com/@int2Eh/understanding-the-windows-system-call-mechanism-7d27db219478

函數呼叫流程範例

我們先寫一個呼叫 CreateFileW 這個 Windows API 來創建檔案的程式。在這邊,我們為了方便且因為只是用於範例,所以我們先寫 C 程式碼,之後會再跟大家說如何使用 Zig 去調用 Windows API 並寫 Zig 程式。

#include <windows.h>

int main(void) {
    HANDLE h = CreateFileW(
        L"test.txt",
        GENERIC_WRITE,
        0,
        NULL,
        CREATE_ALWAYS,
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );

    if (h == INVALID_HANDLE_VALUE)
        return (int)GetLastError();

    CloseHandle(h);
    return 0;
}

呼叫 CreateFileW

CreateFileW

接著會在 NtCreateFile 裡面使用 syscall 這個組合語言指令從 User mode 切換到 Kernel mode,再由 Kernel 創建檔案。

NtCreateFile

syscall

直接呼叫 Native API

很重要的一個點是,應用程式其實可以直接呼叫系統調用(NTDLL 函數),而不需要透過 Windows API,因為 Windows API 其實只是 NTAPI 的包裝。換句話說,NTAPI 使用起來較為複雜,因為它沒有被 Microsoft 官方正式寫入公開文檔給開發者使用,此外 Microsoft 也建議不要使用 NTAPI 函數,因為他們可能會被無預警的修改。

不過直接呼叫 Native API 在惡意程式開發中卻有它的好處,不過我們不會在今天講這個,以後的文章會在來聊聊這件事。(我是老高嗎?)

虛擬記憶體與分頁機制(Paging)

現代的作業系統的記憶體並不會直接映射到物理的記憶體(RAM),而是會讓進程使用虛擬的記憶體地址(Virtual memory),這些虛擬地址會映射到物理的記憶體地址。這樣做最重要的目的是盡可能地節省物理的記憶體。

虛擬記憶體可以映射到物理記憶體,但也可能會存放在硬碟(Disk)上。利用虛擬記憶體地址,多個進程可以共用相同的物理地址,同時擁有獨立的虛擬記憶體地址。虛擬記憶體依賴記憶體分頁的概念,會把記憶體分成一個一個 4KB 的塊,並稱為「頁」,不過我從現在開始要叫它 Page。

下圖為虛擬記憶體和實體記憶體的映射關係,圖片來源於《Windows Internals 第七版》。

Virtual Memory - Windows Internals 7th Edition

Page 狀態

根據 Microsoft 文檔,在進程的虛擬地址空間(Virtual Address Space, VAS)中的頁面會處於以下三種不同的狀態的其中一種:

  • Free
    • Page 沒有被提交(Commit)也沒有被保留(Reserve),因此進程不可存取
    • 它可以被保留、提交或是同時保留和提交
    • 任何嘗試對狀態為 Free 的 Page 進行讀取或寫入的操作都會導致 Access violation exception
  • Reserved
    • 這個 Page 已經被保留供未來使用
    • 這塊地址不能被再次被一些 Allocation 的函數給分配
    • 不能被存取且沒有和其相關的實體記憶體位址
    • 可以被提交
  • Committed
    • 已經從 RAM 和 Disk 中分配了記憶體
    • 可以被進程存取,存取會被一個記憶體保護常數控制
    • 系統只會在第一次嘗試讀取或寫入該 Page 的時候把提交的每個 Page 初始化並加載到實體記憶體中
    • 進程終止時,系統會釋放這個 Page

Page 保護選項

當 Page 被提交,就需要給它設置保護選項。完整的記憶體保護常數(Memory protection constants)的列表可以在官方文檔中找到,這邊僅列出一些常見、常用的範例:

  • PAGE_NOACCESS
    • 禁止所有對已提交的 Page 的存取,包括讀取、寫入、執行都將導致 Access violation exception
  • PAGE_EXECUTE_READWRITE
    • 允許讀取、寫入和執行
    • 不建議在惡意程式開發時使用,極容易被當成入侵指標(Indicators of Compromise, IoC),因為同時可讀可寫可執行的記憶體十分罕見
  • PAGE_READONLY
    • 允許讀取,寫入和執行操作會引發 Access violation exception

現代記憶體保護

現代的作業系統通常都會內建記憶體保護功能,防止惡意攻擊,包括但不限於:

  • Data Execution Prevention (DEP)
    • 這是 Windows XP SP2 和 WIndows Server 2003 SP1 開始引入的系統及記憶體保護
    • 啟用時,程式碼不能從未明確標記為可執行的記憶體區域中執行,例如使用 PAGE_READONLY 的記憶體
  • Address Space Layout Randomization (ASLR)
    • 每次執行會隨機排列進程中關鍵資料的記憶體位址,包括 Base、Stack、Libraries 等

分配記憶體

我們來寫一個 C 程式碼來示範如何透過 C 函數或是 Windows API 來和記憶體互動。我們先來看一下如何分配記憶體,以下的程式碼片段展示了一些分配記憶體的方式,會在進程中保留該記憶體。

// 1. Using malloc()
PVOID pAddress = malloc(100);

// 2. Using HeapAlloc()
PVOID pAddress = HeapAlloc(GetProccessHeap(), 0, 100);

// 3. Using LocalAlloc()
PVOID pAddress = LocalAlloc(LPTR, 100);

記憶體分配函數會返回基址(Base address),也就是指向已分配的記憶體的開始的地址的指針。上面的程式碼中,pAddress 就是這些基址。使用這個指針可以進行很多操作,如讀取、寫入和執行等。

#include <Windows.h>
#include <heapapi.h>
#include <stdio.h>
#include <winnt.h>

int main() {
    PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);
    printf("Base address of allocated memory: 0x%p", pAddress);

    getchar();  // for debugging

    return 0;
}

用這段程式碼就可以看到 pAddress 的樣子。

HeapAlloc

當我們分配記憶體後,它可能會是空值或是包含隨機的資料。某些記憶體分配的函數在分配過程中會提供一個選項,可以在分配的時候將記憶體區域清零。

Allocated Memory

寫入記憶體

在記憶體分配後,下一步通常會是將資料寫進該緩衝區。有很多不同的方式可以寫入記憶體,在這個範例中,我們會使用 memcpy 函數。

#include <Windows.h>
#include <heapapi.h>
#include <stdio.h>
#include <string.h>
#include <winnt.h>

int main() {
    PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);

    CHAR *cString = "Hi, I'm CX330!";
    memcpy(pAddress, cString, strlen(cString));

    printf("Base of allocated memory address: 0x%p", pAddress);
    getchar();

    return 0;
}

這邊我們先用 HEAP_ZERO_MEMORY 把分配的記憶體清零,並用 memcpy 把我們的字串複製過去,接著用 x64dbg 驗證。

Written memory

釋放記憶體

當我們使用完我們分配的記憶體之後,需要釋放記憶體,以避免記憶體洩漏(Memory leak)。根據我們用的分配記憶體的函數,我們需要使用不同的記憶體釋放函數,比如:

  • malloc
    • free
  • HeapAlloc
    • HeapFree
  • LocalAlloc
    • LocalFree

我們一樣寫個範例程式碼,來看一下釋放記憶體的過程。

#include <Windows.h>
#include <heapapi.h>
#include <stdio.h>
#include <string.h>
#include <winnt.h>

int main() {
    PVOID pAddress = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 100);

    CHAR *cString = "Hi, I'm CX330!";
    memcpy(pAddress, cString, strlen(cString));

    printf("Base of allocated memory address: 0x%p", pAddress);

    getchar(); // as breakpoint

    HeapFree(GetProcessHeap(), 0, pAddress);

    getchar(); // as breakpoint

    return 0;
}

寫入後

Written

釋放後

Freed

可以發現釋放後被隨機數給覆蓋了,這個新的亂數很可能是作業系統在內部進行了新的分配等原因造成的。

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

結束了結束了,今日份完成。等等要去找牛肉湯的大家開會了,就先到這邊囉!今天看了 Windows 的架構還有 Windows API 是如何運作,也介紹了記憶體的分配規則等等。明天會來更加詳細的介紹一下 Windows API!

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


上一篇
Day05 - 綠鬣蜥軍團的秘密基地:Payload 的最佳藏身處
下一篇
Day07 - 一窺窗戶中的風景:深入淺出 Windows API
系列文
Zig 世代惡意程式戰記:暗影綠鬣蜥 の 獠牙與劇毒!7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言