Giriş
“Process Hallowing” və ya “RunPE” uzun zamandır zərərvericilər tərəfindən istifadə edilən, zərərvericinin özünü gizlətmək üçün başqa proses adından (əksər hallarda əs legitim proqram təminatları hədəf olaraq seçilir) işə salınması metoduna verilən addır. Sözü gedən metod ilə zərərvericinin əsas məqsədi özünü antiviruslar-dan (və ya istifadəçidən), həmçinin firewall kimi proqram təminatlarından gizlətməkdir. Zərərverici, zərərli kodları işə salmaq üçün hədəf olaraq seçdiyi proqram təminatını dondurulmuş rejimdə işə salır (calc.exe) və ona aid icra edilə bilən kodlarının olduğunu yaddaş bölgəsini boşaldaraq bu əraziyə zərərli kodları köçürür. Bəzi sazlamaları etdikdən sonra isə zərərli kodların olduğu “legitim” proses qaldığı yerdən fəaliyyətinə davam edir. Ətraflı məlumat üçün sizlər üçün hazırladığımız aşağıdakı məqalələri oxumağınızı məsləhət görürük.
https://mrl.cert.gov.az/az/articles/view/98
https://mrl.cert.gov.az/az/articles/view/94
Bu məqalənin əsas mövzusu legitim proses içərisindən sözü gedən metod ilə işə salınan zərərverici kodları aşkar etməkdir. Bunun üçün proses yaddaşı və PE fayl formatı ilə yaxından işləyəcəyik. İlk öncə testlərimizi aparacağımız test proqramımızı hazır edirik.
Testapp.asm
format PE GUI
entry start
section '.text' code readable executable
start:
push 0
push _caption
push _message
push 0
call [MessageBoxA]
push 0
call [ExitProcess]
section '.data' data readable writeable
_caption db 'Malware Research Lab',0
_message db 'mrl.cert.gov.az',0
section '.idata' import data readable writeable
dd 0,0,0,RVA kernel_name,RVA kernel_table
user_name db 'USER32.DLL',0
dd 0,0,0,RVA user_name,RVA user_table
dd 0,0,0,0,0
kernel_table:
ExitProcess dd RVA _ExitProcess
dd 0
user_table:
MessageBoxA dd RVA _MessageBoxA
dd 0
kernel_name db 'KERNEL32.DLL',0
_ExitProcess dw 0
db 'ExitProcess',0
_MessageBoxA dw 0
db 'MessageBoxA',0
section '.reloc' fixups data readable discardable
Test proqramını hazır etdikdən sonra icra edilə bilən faylın formatına göz gəzdirək. Not: Məqalədə PE fayl fomatına tam baxış (təhlil) nəzərdə tutulmayıb. İstinadlar bölməsindən ətraflı məlumatları əldə edə bilərsiniz. Aşkarlanma qisminə keçməzdən öncə hazırladığımız test proqramının ümumi strukturuna baxaq.
Test proqramında qarşımıza ilk olaraq DOS (IMAGE_DOS_HEADER) ( +DosStub) header adı verilən hissə çıxır.
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
USHORT e_magic; // Magic number
USHORT e_cblp; // Bytes on last page of file
USHORT e_cp; // Pages in file
USHORT e_crlc; // Relocations
USHORT e_cparhdr; // Size of header in paragraphs
USHORT e_minalloc; // Minimum extra paragraphs needed
USHORT e_maxalloc; // Maximum extra paragraphs needed
USHORT e_ss; // Initial (relative) SS value
USHORT e_sp; // Initial SP value
USHORT e_csum; // Checksum
USHORT e_ip; // Initial IP value
USHORT e_cs; // Initial (relative) CS value
USHORT e_lfarlc; // File address of relocation table
USHORT e_ovno; // Overlay number
USHORT e_res[4]; // Reserved words
USHORT e_oemid; // OEM identifier (for e_oeminfo)
USHORT e_oeminfo; // OEM information; e_oemid specific
USHORT e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
Yuxarıda göstərilən struktur içərisində sonuncu LONG tipində e_lfanew dəyəri (offset) bizi PE içərisində növbəti vacib hissəyə aparır. PE fayl başlığı.
typedef struct _IMAGE_NT_HEADERS
{
ULONG Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS, *PIMAGE_NT_HEADERS;
Signature içərisində PE fayl imzası saxlanılır. Defolt olaraq bu dəyər PE fayl üçün 0x4450 (0000) (PE)-dir.
PE imzasından sonra IMAGE_FILE_HEADER strukturu gəlir. Burada hədəf arxitektura, icra edilə bilən faylın nə qədər bölmədən ibarət olduğu, fayl xarakteristikası, yaradılma tarixi və s. kimi məlumatlar saxlanılır.
typedef struct _IMAGE_FILE_HEADER {
USHORT Machine;
USHORT NumberOfSections;
ULONG TimeDateStamp;
ULONG PointerToSymbolTable;
ULONG NumberOfSymbols;
USHORT SizeOfOptionalHeader;
USHORT Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
Ardınca IMAGE_OPTIONAL_HEADER adı verilən hissə (başlıq) gəlir.
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
Bu başlıq fayl checksum dəyəri, proqramın (thread) hardan başlayacağı haqqında məlumat, yaddaş bölgəsində hizalanma (alignment) dəyəri, icra edilə bilən faylın arxitekturası (32/64) və s. kimi məlumatları özündə ehtiva edir. Struktur sonunda 16 ədad massivdən ibarət başqa bir vacib struktur IMAGE_DATA_DIRECTORY saxlanılır.
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
Bu struktur içərisində icra edilə bilən faylın fəaliyyət göstərməsi üçün lazım olan tablolar haqqında kritik məlumatlar (qovluq offseti və ölçüsü) saxlanılır. Misal import, export, resurs qovluğu. Optinal header-dən həmən sonra icra edilə bilən fayla aid əsas məlumatların (kodlar, resurslar, import edilən kitabxanalar haqqında məlumatlar, export edilən funksiyalar, fəaliyyət zamanı istifadə ediləcək mətnlər və s.) saxlanıldığı bölmələr (PE sections) gəlir. Sözü gedən bölmələr IMAGE_SECTION_HEADER strukturu ilə təsvir edilir.
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress;
DWORD SizeOfRawData;
DWORD PointerToRawData;
DWORD PointerToRelocations;
DWORD PointerToLinenumbers;
WORD NumberOfRelocations;
WORD NumberOfLinenumbers;
DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Burada misal olaraq .text bölməsində yazdığımız kodlar,
.data bölməsində proqram içərisində istifadə etdiyimiz məlumatlar (initialized),
.idata bölməsində isə istifadə etdiyimiz kitabxana və onlara aid funksiyaların yer aldığını görə bilərsiniz.
Müqayisə və aşkar etmə
“Process hallowing və ya RunPE” metodu ilə işə salınan zərərverici kodları aşkar etmək üçün icra edilən bilən fayl (disk üzərində saxlanılan) məlumatları ilə fəaliyyət göstərən prosesin PE məlumatlarını (yaddaş üzərində saxlanılan) müqayisə etmək lazımdır. Bununla da aradakı fərqlər üzərindən, prosesin icra etdiyi kodların orijinal icra edilə bilən fayla aid olub-olmadığını təyin etmək mümkündür. 100 dəfə eşitməkdənsə, 1 dəfə görmək yaxşıdır!
Testapp proqramını işə salaraq proses yaddaşına nəzər salaq. Proqramı işə salarkən Windows və ya PE loader sözü gedən proqramı (disk üzərində saxlanılan PE faylı) oxuyaraq xəritələmə adı verilən əməliyyatı icra edir. PE formatını təhlil edir və uyğun şəkildə yaddaş ərazisinə xəritələyir (lazım olan dəyişikliklər ilə birlikdə).
ƏS sözü gedən proqramı 0x400000 adresindən başlayaraq xəritələyib. Yuxarıda göstərilən ərazilərə baxaq.
0x400000 adresində IMAGE_DOS_HEADER,
0x401000 adresində isə .text bölməsinin xəritələndiyini görürük.
Oxşar şəkildə 0x402000 adresində isə .data bölməsi.
PE başlıqlarına Windbg aləti ilə baxaq.
0:001> dt _IMAGE_DOS_HEADER 00400000
ntdll!_IMAGE_DOS_HEADER
+0x000 e_magic : 0x5a4d
+0x002 e_cblp : 0x80
+0x004 e_cp : 1
+0x006 e_crlc : 0
+0x008 e_cparhdr : 4
+0x00a e_minalloc : 0x10
+0x00c e_maxalloc : 0xffff
+0x00e e_ss : 0
+0x010 e_sp : 0x140
+0x012 e_csum : 0
+0x014 e_ip : 0
+0x016 e_cs : 0
+0x018 e_lfarlc : 0x40
+0x01a e_ovno : 0
+0x01c e_res : [4] 0
+0x024 e_oemid : 0
+0x026 e_oeminfo : 0
+0x028 e_res2 : [10] 0
+0x03c e_lfanew : 0n128
0:001> dt _IMAGE_FILE_HEADER 0x00400000 + 0n128 + 0x004
ntdll!_IMAGE_FILE_HEADER
+0x000 Machine : 0x14c
+0x002 NumberOfSections : 4
+0x004 TimeDateStamp : 0x656e4146
+0x008 PointerToSymbolTable : 0
+0x00c NumberOfSymbols : 0
+0x010 SizeOfOptionalHeader : 0xe0
+0x012 Characteristics : 0x10e
0:001> dt _IMAGE_OPTIONAL_HEADER 0x00400000 + 0n128 + 0x018
ntdll!_IMAGE_OPTIONAL_HEADER
+0x000 Magic : 0x10b
+0x002 MajorLinkerVersion : 0x1 ''
+0x003 MinorLinkerVersion : 0x49 'I'
+0x004 SizeOfCode : 0x200
+0x008 SizeOfInitializedData : 0x600
+0x00c SizeOfUninitializedData : 0
+0x010 AddressOfEntryPoint : 0x1000
+0x014 BaseOfCode : 0x1000
+0x018 BaseOfData : 0x2000
+0x01c ImageBase : 0x400000
+0x020 SectionAlignment : 0x1000
+0x024 FileAlignment : 0x200
+0x028 MajorOperatingSystemVersion : 1
+0x02a MinorOperatingSystemVersion : 0
+0x02c MajorImageVersion : 0
+0x02e MinorImageVersion : 0
+0x030 MajorSubsystemVersion : 3
+0x032 MinorSubsystemVersion : 0xa
+0x034 Win32VersionValue : 0
+0x038 SizeOfImage : 0x5000
+0x03c SizeOfHeaders : 0x400
+0x040 CheckSum : 0x2e9d
+0x044 Subsystem : 2
+0x046 DllCharacteristics : 0
+0x048 SizeOfStackReserve : 0x1000
+0x04c SizeOfStackCommit : 0x1000
+0x050 SizeOfHeapReserve : 0x10000
+0x054 SizeOfHeapCommit : 0
+0x058 LoaderFlags : 0
+0x05c NumberOfRvaAndSizes : 0x10
+0x060 DataDirectory : [16] _IMAGE_DATA_DIRECTORY
Debug alətin əldə edilən dəyərlər ilə disk üzərində mövcud olan fayl dəyərləri –ni qarşılaşdırsanız 99%-lik bir oxşarlığın olduğuna şahid ola bilərsiniz. Əgər zərərverici, zərərli kodları hər hansı bir legitim proses adından icra edirsə bu zaman sözü gedən oxşarlıq (lar) aradan qalxır. Nümunə üçün test proqramını Windows legitim aləti olan calc.exe içərisindən işə salaq. RunPE üçün https://github.com/joren485/HollowProcess alətindən istifadə edilmişdir.
Şəkildə gördüyünüz kimi sistemdə fəaliyyət göstərən proses calc.exe legitim olaraq görünür. Lakin icra edilən kodlar tamamı ilə fərqlidir. Bunu aşkar etmək üçün sözü gedən prosesə aid modulların siyahısını ekrana yazdıran kiçik bir proqram yazaq.
int main(void)
{
HANDLE hModuleSnapshot = INVALID_HANDLE_VALUE;
MODULEENTRY32W MOD32 = {0};
hModuleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, 556);
MOD32.dwSize = sizeof(MODULEENTRY32W);
Module32FirstW(hModuleSnapshot, &MOD32);
do{
wprintf(L"%08X:%s\n", MOD32.modBaseAddr, MOD32.szExePath);
}while(Module32NextW(hModuleSnapshot, &MOD32));
}
Nəticə:
Hər şey qaydasında kimi görsənir. Prosesin yükləndiyi adresə Windbg ilə baxaq. !dh əmri ilə yuxarıda göstərilən adresdə mövcud olan PE başlıqlarını analiz edək və ilk müqayisəni IMAGE_FILE_HEADER içərisində saxlanılan TimeDateStamp üzərindən edək. Yaddaş ərazisində olan TimeDateStamp dəyəri:
Legitim calc.exe TimeDateStamp dəyəri:
Növbəti müqayisə-ni DataDirectory (IMAGE_DATA_DIRECTORY) üzərində edirik.
Proses IMAGE_DATA_DIRECTORY (16 ədad massiv):
calc.exe:
Disk üzərində olan PE məlumat qovluqları ilə yaddaş ərazisində olan qovluqlar arasında fərq olduqca çoxdur. Səbəbi yaddaş ərazisində olan PE-nin legitim calc.exe deyil bizim “zərərvericimiz” testapp.exe olmasıdır. Son olaraq icra ediləcək kodların olduğu bölməyə (.text) nəzər salaq.
Burada əsas kodlar bizim testapp ilə eynidir. Lakin calc.exe .text bölməsində olan kodlar tamamı ilə fərqlidir.
Aşkarlama prosesinin avtomatlaşdırılması
Artıq müqayisəni necə edəcəyimizi bildiyimizə görə prosesi avtomatlaşdıra bilərik. İlk olaraq prosesin yükləndiyi adresi aşağıdakı funksiyanın köməkliyi ilə öyrənirik.
DWORD GetModuleBaseAddr(DWORD dwPID)
{
HANDLE hModuleSnapshot = INVALID_HANDLE_VALUE;
MODULEENTRY32W mod32 = {0};
hModuleSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPID);
mod32.dwSize = sizeof(mod32);
Module32FirstW(hModuleSnapshot, &mod32);
return (DWORD)mod32.modBaseAddr;
}
// Proses yükləndiyi adres
ModuleBase = GetModuleBaseAddr(3080); // 3080 -> Proses identifikasiya nömrəsi
Prosesin xəritələndiyi adresi öyrəndikdən sonra disk üzərindəki fayl (calc.exe) başlıqlarını oxuyuruq.
DWORD ModuleBase = 0;
HANDLE hProcess = NULL;
HANDLE hFile = INVALID_HANDLE_VALUE;
DWORD dwRead = 0;
BOOL bRet = FALSE;
IMAGE_DOS_HEADER file_dos_header = {0};
IMAGE_DOS_HEADER process_dos_header = {0};
IMAGE_NT_HEADERS32 file_nt_header = {0};
IMAGE_NT_HEADERS32 process_nt_header = {0};
// Proses yükləndiyi adres
ModuleBase = GetModuleBaseAddr(3080);
// Fayl başlıqlarını oxu
hFile = CreateFileW(L"c:\\Windows\\system32\\calc.exe",
GENERIC_READ,
FILE_SHARE_READ,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
// Fayl dos başlığını oxu
bRet = ReadFile(hFile, &file_dos_header, sizeof(IMAGE_DOS_HEADER), &dwRead, NULL);
SetFilePointer(hFile, file_dos_header.e_lfanew, NULL, FILE_BEGIN);
// Fayl NT başlığını oxu
bRet = ReadFile(hFile, &file_nt_header, sizeof(IMAGE_NT_HEADERS32), &dwRead, NULL);
Daha sonra müqayisə əməliyyatı üçün yaddaş ərazisində saxlanılan PE (testapp.exe) başlıqlarını oxuyuruq.
// Proses başlıqlarını oxu
hProcess = OpenProcess(PROCESS_VM_READ, FALSE, 3080);
// Proses dos başlığını oxu
ReadProcessMemory(hProcess, (LPCVOID)ModuleBase, &process_dos_header,
sizeof(IMAGE_DOS_HEADER), &dwRead);
// Proses NT başlığını oxu
ReadProcessMemory(hProcess, (LPCVOID)(ModuleBase + process_dos_header.e_lfanew),
&process_nt_header, sizeof(IMAGE_NT_HEADERS32), &dwRead);
Başlıqları oxuduqdan sonra aşağıdakı funkiyanın köməkliyi ilə NT başlıqlarını müqayisə edirik.
VOID Compare_Nt_Headers(PIMAGE_NT_HEADERS32 pFileNt, PIMAGE_NT_HEADERS32 pProcessNt)
{
if (pFileNt->FileHeader.TimeDateStamp != pProcessNt->FileHeader.TimeDateStamp)
{
// TimeDateStamp fərqlidir
}
if (pFileNt->FileHeader.Characteristics != pProcessNt->FileHeader.Characteristics)
{
// PE xüsusiyyəti fərqlidir
}
}
Əlbətdə yalnız bu məlumatlardan yola çıxaraq proses yaddaşına müdaxilə edilib-edilmədiyinə qərar vermək o qədər uğurlu nəticələr verməyə bilər. Dəqiqliyə yaxın nəticə əldə etmək üçün PE başlıqlarının, bölmələrinin müqayisəsi labüddür.
Not: Proqramlar normal fəaliyyətləri zamanı real vaxt rejimində öz dəyişənlərində, kodlarında dəyişikliklər edə bilər. Bu baxımdan bölmələri bayt bayt qarşılaşdırma məqsədə uyğun deyil. İdeal aşkarlama üçün ssdeep kimi məlumatları hissələrə bölüb onların xeş dəyərləri üzərindən müqayisə edən alqoritmların (fuzzy hash) istifadəsi məsləhətdir.
İstinadlar
[1] https://www.microsoft.com/en-us/security/blog/2021/07/27/combing-through-the-fuzz-using-fuzzy-hashing-and-deep-learning-to-counter-malware-detection-evasion-techniques/
[2] https://www.adlice.com/runpe-hide-code-behind-legit-process/
[3] https://learn.microsoft.com/en-us/windows/win32/debug/pe-format