Computer Emergency Response Center

Windows applikasiya proqramlaşdırma interfeysinin hook edilməsi

Api hooking (qarmaq) istər zərərvericilər istər təhlükəsizlik proqram təminatları tərəfindən geniş istifadə edilən bir mexanizmdir. Məqalədə bu mexanizm haqqında qısa məlumat verililəcəkdir. Bu mexanizmin daha yaxşı anlamaq üçün ilk öncə api nədir sualına cavab verməliyik.

Windows Application Programming Interface nədir?

Proqramlaşdırma ilə məşğul olan insanlara yəgin ki, bu termin o qədərdə yad olmayacaqdır. Bəsit dil ilə izah etməyə çalışsaq hər hansı bir A tərəfinin digər tərəfə (B) verdiyi api üzərindən B tərəfi A tərəfinin təqdim etdiyi api üzərindən lazımı əməliyyatları görmək üçün istifadə edir. Win32 (və ya winapi) – Windows application programming interface eyni məntiq üzərindən fəaliyyət göstərən bir mexanizmdir. Misal olaraq müxtəlif proqramlaşdırma dillərində sistemdə hər hansı bir fayl yaratmaq istədiyiniz zaman həmin dildə yazılan kod, sistemdə fəaliyyət göstərən windows əməliyyat sisteminin təqdim etdiyi api üzərindən bunu icra edə bilir. Və ya hər hansı bir proses haqqında məlumat almaq üçün yenə eyni şəkildə api üzərindən prosesi açmağınız mütləqdir. Bu interfeysi digər proqramlaşdırma dillərinə təqdim edən məhz Win32 adı verilən bu interfeysdir. Yuxarıda qeyd edilənləri pratik olaraq görmək üçün kiçik bir test keçirək. Sistemdə fəaliyyət göstərən hər hansı bir prosesin fəaliyyətini dayandırmaq üçün python dilində kiçik bir kod yazaq.

Gördüyünüz kimi python ctypes modulu üzərindən 2496 PID (notepad) fəaliyyətini sonlandıra bildik. Digər proqramlaşdırma dillərində-də eyni əməliyyatı oxşar şəkildə etmək lazımdır. Burada OpenProcessTerminateProcess funksiyalarını Win32 KERNELBASE.DLL kitabxanası üzərindən bizə təqdim edir. Bu funksiyaları çağırmadan əməliyyat sistemində fəaliyyət göstərən proseslərə müdaxilə etmək şansımız yoxdur və bu bütün proqramlaşdırma dillərinə aiddir. Lakin məsələ bununla bitmir. Bu funksiyalarda həmçinin öz növbəsində əməliyyat sisteminin kernelinə müraciət edirlər. Belə ki, OpenProcess funksiyası çağrıldığı zaman arxa fonda NTDLL.DLL kitabxanası içərisində olan NtOpenProcess funksiyasına müraciət edilir. Bu funksiyada öz növbəsində kernel səviyyəsinə keçərək (syscall instr) prosesə handle alır və həmin handle dəyərini geri qaytarır. Aşağıda ki, şəkildə hər hansı bir fayldan məlumat oxumaq istədiyiniz zaman arxa fonda sisteminin nələr etdiyini daha yaxşı görmək mümkündür.

Oxşar əməliyyat biz OpenProcess funksiyasını çağırdığımız zamanda olur. OpenProcess funksiyası özünə gələn parametrləri NtOpenProcess funksiyasına göndərir. NtOpenProcess funksiyası isə lazımı sazlamaları edərək kernel(ring0) səviyyəsinə keçir. Kernel səviyyəsində prosesin handle dəyəri alındıqdan sonra thread yenidən user(ring0) səviyəsinə keçid edir və istifadəçiyə əldə etdiyi handle dəyərini qaytarır. Kernel bu məqalənin mövzusu olmadığı üçün bu hissə haqqında ətraflı məlumat verilməyəcək. Gəlin debugger ilə əməliyyat axınına baxaq. KERNELBASE.DLL kitabxanasında OpenProcess funksiyasına breakpoint təyin edirik və python da eyni əməliyyatı icra edirik.

Aşağıdakı şəkildə OpenProcess funksiyasına stack üzərindən gələn parametrleri görürsünüz.

Bu funksiya içərisində bəzi sazlamaları etdikdən sonra NtOpenProcess funksiyası çağrılır.

Bu funksiya isə EAX registerinə 0xBE dəyərini ataraq cari axını kernel səviyəsinə salmaq üçün KiFastSystemCall funksiyası çağrılır.

KiFastSystemCall isə sysenter əmri ilə cari axını kernel səviyyəsinə endirərək sistem servis dispatch table (SSDT) adı verilən tabloda 0xBE indeksinə qarşılıq gələn funksiyanı çağırır.

Win32 işləmə prinsipi haqqında qısa məlumat aldıqdan sonra keçirik hooking (qarmaq) mexanizminə.

Hooking yuxarıda qeyd edilən əməliyyat zəncirinə öz halqanızı daxil etmək mexanizminə verilən ümumi addır. Yəni OpenProcess funksiyası ilə NtOpenProcess funksiyası arasına öz proxy funksiyanızı yerləşdirərək əməliyyat zəncirinə müdaxilə edəbilərsiniz.

Normal və hook edilən əməliyyat zənciri:

Burada əsas məsələ funksiyaya (OpenProcess və s.) gələn parametrləri oxumaq vəya müdaxilə etməkdir. Yəni məsələn zərərli A prosesinin onu TerminateProcess ilə terminate etməsini istəmir. Bunun üçün Proxy funksiyası içərisində əgər OpenProcess funksiyasının özünə handle almaq istədiyini qeydə alar isə bu zaman həmin PID dəyərini var olmayan bir dəyər ilə dəyişir və OpenProcess funksiyası zərərli prosesə handle ala bilmir.

Hook əməliyyatını həyata keçirmək üçün hədəf prosesin yaddaşına müdaxilə etmək lazımdır. Bu tip əməliyyatlar zamanı ən effektiv metod dll inyeksiyadır. Proxy funksiyamızı bu dll içərisinə yazaraq dll faylını inyeksiya edirik. Daha sonra hook ediləcək funksiya adresinə bizim proxy funksiyasına tullanacaq(jump) şəklində patch edirik. Məqalədə bunun üçün jmp instructiondan istifadə edəcəyik. Lakin başqa instructionlarında köməkliyi ilə eyni işi görmək mümkündür. İlk olaraq hook funksiyamızı yazıram. Məqalə üçün OpenProcess funksiyasına gələn PID dəyərini ekrana yazdıran bir funksiya olacaq.

void Hook_OpenProcess(unsigned long dwDesiredAccess,
	unsigned long bInheritHandle,
	unisgned long dwProcessId)
{
	printf("HOOK funksiyası PID=%d\n", dwProcessId);
}

Hook funksiyasını yazdıqdan sonra dll faylını, müdaxilə etmək istədiyimiz prosesin yaddaşına inyeksiya edirik.

OpenProcess funksiyasını hook edəcəyimiz üçün orijinal OpenProcessHook_OpenProcess funksiyasının adreslərini öyrənirik. Daha sonra JMP instruction ilə orijinal funksiyanın bizim hook funksiyasına jump edəcək şəkildə patch etməliyik.

0DCE813C

005B1040

Burada jmp (0xE9) instruction üçün uzaqlıq məsafəsini hesablamalıyıq. Bunun üçün ilk olaraq hədəf ünvandan cari ünvanı çıxırıq. Daha sonra jmp opcode (+ünvan) ölçüsünü (5 bayt) çıxaraq offsetı tapırıq.

Offsetimizi aldıqdan sonra keçirik OpenProcess funkasiyasının olduğu adresdəki opcode-ları patch etməyə.

Gördüyünüz kimi artıq defolt OpenProcess funksiyası bizim hook funksiyamıza jump edəcək şəkildə patch edilib. Test üçün OpenProcess funksiyasını çağırıram.

Gördüyünüz kimi nəticə olaraq OpenProcess funksiyası bizim hook funksiyamızı çağırır. Artıq hook funksiyası içərisində defolt funksiyaya istədiyimiz kimi müdaxilə edə bilərik. Burada bir digər məsələ ortaya çıxır. Bəs hook funksiyasından sonra nə baş verəcək? Əlbətdəki hook funksiyası içərisində lazımı müdaxilələr edildikdən sonra proqram normal axışına davam etməlidir. Yəni hook funksiyasından sonra orijinal  OpenProcess funksiyası çağrılmalıdır. Lakin biz orijinal OpenProcess funksiyasında prolog qismində patch əməliyyatı aparmışıq.

Proqramın normal axışının davam etməsi üçün patch edilən baytların defolt baytlar ilə əvəzlənməsi lazmdır. Bunun üçün patch əməliyyatından öncə patch əməliyyatının ölçüsü qədər (5 bayt) məlumat hook dll içərisində bufferdə saxlanılır. Hook funksiyası çağrıldıqdan sonra və lazımı müdaxilələr edildikdən sonra bu baytlar defolt baytlar ilə əvəzlənir. Daha sonra hook funksiyası içərisindən defolt OpenProcess funksiyası çağrılır. Bundan sonra isə OpenProcess funksiyası hook funksiyasını çağıracaq şəkildə yenidən patch edilir. Bura qədər hook (qarmaq) mexanizminin işləməsi haqqında məlumat aldınız. Lakin bir hook engine (qarmaq mühərriki) yaratmaq bu qədər sadə deyil.

Burada bir çox şeylərə diqqət yetirmək lazımdır. Xüsusi ilə axın (thread) təhlükəsizliyi, trampoline funksiyaları, opcode uzunluğu və s.

 

 

İstinadlar

[1] https://en.wikipedia.org/wiki/Windows_API
[2] https://docs.microsoft.com/en-us/windows/win32/api
[3] https://attack.mitre.org/techniques/T1056/004
[4] https://github.com/TsudaKageyu/minhook
[5] https://www.codeproject.com/Articles/44326/MinHook-TheMinimalistic-x-x-API-Hooking-Libra

Press ESC to close