嗨大家,我 CX330。今天事情有點多,不知道寫不寫得完 XD。
昨天聊了 Shellcode 和 Payload 的放置等等,今天想要來把窗戶打開,揭開它的神秘面紗,會和大家一起初步認識 Windows 的架構和 Windows 如何做記憶體管理等等的。
好啦那就開始囉!
中華民國刑法第 362 條:「製作專供犯本章之罪之電腦程式,而供自己或他人犯本章之罪,致生損害於公眾或他人者,處五年以下有期徒刑、拘役或科或併科六十萬元以下罰金。」
本系列文章涉及多種惡意程式的技術,旨在提升個人技術能力與資安意識。本人在此強烈呼籲讀者,切勿使用所學到的知識與技術從事任何違法行為!
本系列文章中使用的 Zig 版本號為 0.14.1。
Windows 作業系統中的處理器可以跑在兩種不同的模式上:User mode 和 Kernel mode。一般的應用程式會是在 User mode 之下運行,而操作系統的元件們則會在 Kernel mode 運行。如果今天一般的應用程式想要完成某些系統任務,例如建立檔案,那它其實是不能獨立做完這件事的。它需要去遵守某個函數呼叫的流程,才能夠請 Kernel 幫忙做這件事。我們可以看一下以下的圖。
kernel32.dll
匯出 CreateFile
Windows API 函數。還有一些其他常見的 Subsystem DLL 包括 ntdll.dll
、advapi32.dll
和 user32.dll
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 的驅動和模組來完成這個任務。
我們先寫一個呼叫 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
。
接著會在 NtCreateFile
裡面使用 syscall
這個組合語言指令從 User mode 切換到 Kernel mode,再由 Kernel 創建檔案。
很重要的一個點是,應用程式其實可以直接呼叫系統調用(NTDLL 函數),而不需要透過 Windows API,因為 Windows API 其實只是 NTAPI 的包裝。換句話說,NTAPI 使用起來較為複雜,因為它沒有被 Microsoft 官方正式寫入公開文檔給開發者使用,此外 Microsoft 也建議不要使用 NTAPI 函數,因為他們可能會被無預警的修改。
不過直接呼叫 Native API 在惡意程式開發中卻有它的好處,不過我們不會在今天講這個,以後的文章會在來聊聊這件事。(我是老高嗎?)
現代的作業系統的記憶體並不會直接映射到物理的記憶體(RAM),而是會讓進程使用虛擬的記憶體地址(Virtual memory),這些虛擬地址會映射到物理的記憶體地址。這樣做最重要的目的是盡可能地節省物理的記憶體。
虛擬記憶體可以映射到物理記憶體,但也可能會存放在硬碟(Disk)上。利用虛擬記憶體地址,多個進程可以共用相同的物理地址,同時擁有獨立的虛擬記憶體地址。虛擬記憶體依賴記憶體分頁的概念,會把記憶體分成一個一個 4KB 的塊,並稱為「頁」,不過我從現在開始要叫它 Page。
下圖為虛擬記憶體和實體記憶體的映射關係,圖片來源於《Windows Internals 第七版》。
根據 Microsoft 文檔,在進程的虛擬地址空間(Virtual Address Space, VAS)中的頁面會處於以下三種不同的狀態的其中一種:
當 Page 被提交,就需要給它設置保護選項。完整的記憶體保護常數(Memory protection constants)的列表可以在官方文檔中找到,這邊僅列出一些常見、常用的範例:
PAGE_NOACCESS
PAGE_EXECUTE_READWRITE
PAGE_READONLY
現代的作業系統通常都會內建記憶體保護功能,防止惡意攻擊,包括但不限於:
PAGE_READONLY
的記憶體我們來寫一個 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
的樣子。
當我們分配記憶體後,它可能會是空值或是包含隨機的資料。某些記憶體分配的函數在分配過程中會提供一個選項,可以在分配的時候將記憶體區域清零。
在記憶體分配後,下一步通常會是將資料寫進該緩衝區。有很多不同的方式可以寫入記憶體,在這個範例中,我們會使用 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 驗證。
當我們使用完我們分配的記憶體之後,需要釋放記憶體,以避免記憶體洩漏(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;
}
寫入後
釋放後
可以發現釋放後被隨機數給覆蓋了,這個新的亂數很可能是作業系統在內部進行了新的分配等原因造成的。
結束了結束了,今日份完成。等等要去找牛肉湯的大家開會了,就先到這邊囉!今天看了 Windows 的架構還有 Windows API 是如何運作,也介紹了記憶體的分配規則等等。明天會來更加詳細的介紹一下 Windows API!
如果對惡意程式開發或是惡意程式分析有興趣的話,這個系列會很適合你!最後也感謝大家的閱讀,歡迎順手按讚留言訂閱轉發(轉發可以讓朋友們知道你都在讀這種很技術的文章,他們會覺得你好帥好強好電,然後開始裝弱互相吹捧)~明天見!