ZARARLILAR
BELLEK İÇİ KOD ENJEKSİYONU
Zararlı yazılımların yazılması ve bellek işlemleri
Zararlı olarak tabir ettiğimiz yazılımlar bir çok dil ile yazılabilir. Bu yazımızda C dili üzerinden gidecek olsakta C#, C++ gibi diller de tercih edilebilir. Bu dilleri seçmemin sebebi ise dinamik bellek yönetimi ve Windows API kullanımıdır.
Windows API’lar nedir ?
Windows API’lar belirli bellek işlemlerini daha kolay ve daha az dışa bağımlı şekilde yapmamızı sağlar. Örneğin python ile bir kod yazacak olsaydım dışarıdan bir çok kütüphane import etmem ve kurmam gerekirdi bu da dışa bağımlılığı artırır. Aslında python çok fazla kütüphaneye ve avantaja sahiptir fakat çok dışa bağımlıdır, C ise python kadar geliştiricisi ve değişik kütüphaneleri olmasa da bellek işlemlerinde çok daha esnek bir geliştirme gücü sunar. Bu dillerin ikisinin de ortasında olan C# aslında en ideal RAT geliştirme dilidir. Hem python gibi güzel kütüphanelere sahip yenilikçi hem de dinamik bellek yönetimine sahip bir dildir.
Belleğe Erişim
Önce C dilinde bellekteki bir alana pointerlar ile ulaşırız bu pointerlar bir değişkenin virtual ram üzerindeki adresini tutar.
Pointer Türleri
1- Structer Pointer
2- Function Pointer
3- Variable Pointer
Yukarıdaki pointer türlerinden bizi en çok ilgilendiren Function pointerdır. Bu pointer türü bir işlevi işaret eder. Bir işlev(fonksiyon) birden fazla veri içerir peki bir pointer bunu nasıl tutar.
Örnek;
Yukarıdaki resimde yazmış olduğum kod 10 değerini sergen fonksiyonuna göndererek 10–1 yani 9 değerini döndürüyor. Bu kod üzerinde bulunan sergen fonksiyonu bir işlevdir. Peki bunu bir pointer’a atıp nasıl çalıştıracağız.
Yukarıda bu fonksiyonu SergenPointer adında bir işlev işaretçisine atadım ve bunu çağırdım, cevap gene 9 oldu. Aslında iki farklı işlem yaptım birinde bir fonksiyona değer atayarak onu çalıştırdım, diğerinde ise bir pointer’a değer atayarak pointerın işaret ettiği alanda ne varsa onu çalıştırmasını söyledim.
Bir diziyi işlev işaretçisi olarak kullanma
İşlev işaretçilerinin nasıl çalıştığını öğrendiğimize göre şimdi yavaş yavaş ısınmaya başlayalım. Elimde unsigned tanımlanmış bir dizi var ve bunun içerisinde yazan verileri işlev olarak kullanmak istiyorum.
Not: C dilindeki Unsigned değerler aslında C# dilindeki byte’a değişken tipine denk gelir yani yazdığımız dizenin içerisindeki her şey aslında 1 ve 0 olarak tutulur gibi düşünebilirsiniz.
Yukarıda yazmış olduğum kodda bir unsigned char tipindeki dizinin içerisinde hexadecimal olarak 0x90 yani hiçbir işlem yapma anlamına gelen none değeri bulunuyor. Eğer ben bu diziyi bir işlev olarak çağırırsam hiçbir şey yapmayacak. Peki bir dizinin içindeki veriyi nasıl fonksiyon gibi çalıştırabilirim ?
Görüldüğü gibi önce Unsigned Char tipindeki diziyi bir işleve çevirdim ve daha sonra ise işlev işaretçisine atadım. Çevirme işlemi dikkat ederseniz aslında bir tip dönüşümü yani “(int)Sergen” den bir farkı yok.
Artık bir dizinin içerisine hexadecimal olarak veri yazıp çalıştırabiliyoruz. Peki zararlı işlemi yapacak kodumuzu bu diziye hexadecimal olarak yazsak ve karşı bilgisayarın belleğinde çalıştırırsak ?
Bellek üzerinde zararlı kod çalıştırma
Bir kod yazıp derlediğiniz zaman bir çalıştırılabilir dosya oluşur. Bu dosya sizin yazdığınız kodun yaptığı işlevdir. Yani artık bu C , Python, C# gibi bir dil olmaktan çıkmış ve hepsinin derlendiği zaman ortak noktası olan executable formata geçmiştir, yani artık hangi dil ile yazıldığının bir önemi yoktur. Linux üzerindeki bazı araçlar yardımıyla bu çalıştırılabilir dosya tekrardan kaynak kod hiç bilinmeden koda çevrilebilir. Örneğin Objdump ve Objcopy gibi araçlar yardımıyla -d yani dissassamble komutu kullanılarak exe dosyasının assambly dilinde ne iş yaptığını görebilirsiniz tabi eğer assambly okumayı biliyorsanız. Bu yüzden “eğer assambly biliyorsanız tüm her şey açık kaynaklıdır” derler. Bunun yanında xxd gibi bir araç kullanırsanız bu çalıştırılabilir dosyasnın yaptığı işlevin hexadecimal formatta gösterildiğini görebilirsiniz. Taşlar yavaş yavaş yerine oturuyor.
Biz bu işlem için C üzerinde kullanabileceğimiz, hex formatında hazır bir binary kullanacağız.
WINDOWS API
Windows API’lar çekirdek ile sistem çağrıları yardımıyla konuşmamızı sağlayan bazı hazır fonksiyonlardır. Yukarıda pointerlar ile yaptığımız işlemleri hazır olarak yaparlar ve kernel ile konuşurlar.
Bu API’ların bazılarına göz atalım.
Shellcode injection
VirtualAlloc : Virtual(sanal) Ram üzerinde belli bir alan ayırmaya yarar
memcpy : Bir API değildir fakat ayrılan virtual ram üzerine bir veri yazmak için kullanılır.
CreateThread: Bir iş parçacığı oluşturmaya yarar
WaitForSingleObject: Bir iş parçacığını belirli bir işlemi yapmasını bekler
VirtualFree: VirtualAlloc ile ayrılan alanı serbest bırakır
Örnek;
Yukarıda yazmış olduğum kod da aslında Function Pointerlar ile yaptığım şey aynı, bunu Windows API’lar kullanarak gerçekleştirdim.
İlk önce VirtualAlloc ile Virtual Memory (sanal bellek) üzerinde bir alan ayırdım verdiğim parametrelerde bu sanal belleğin boyutunun Unsigned Char olarak tanımladığım dizimin boyutu olarak verdim. Bundan sonra memcpy ile bu ayırdığım alana Unsigned Char olarak tanımladığım dizimin içersindeki değeri yazdırdım ve CreateThread ile yeni bir Thread oluşturdum ve “0" parametresini vererek Thread oluşturulduktan hemen sonra başlamasını söyledim.
Thread nedir ?
Thread aslında hepimizin bildiği bir işlemin bir çok işlevini yerine getiren iş parçacıklarıdır.
Örnek olarak eğer görev yöneticisini açar ve işlemler kısmından şuan kullandığınız browserı bulup yanındaki ok işaretine tıklarsanız bir çok aynı browser iconundan olduğunu görürsünüz, aslında bunlar o çalışan process’in threadleridir.
Yani bizim aslında yukarıda CreateThread ile yaptığımız şey, VirtualAlloc ile ayırdığımız bir alana memcpy ile yazdığımız veriyi çalıştıracak bir Thread üretmektir. Oluşturulan thread’ler C dilinde HANDLE tipindeki değişkenlerde tutulurlar.
Process Injection
Process injection kendi işlemimize tahsis edilmiş sanal bellek alanı yerine daha önceden zaten çalışan bir işlemin sanal bellek alanına yerleşmemizi sağlar. Bu işlem için kullanılan Windows API’lar şunlardır;
OpenProcess: Zaten çalışan bir processing process id’si yardımı ile o process’in içini açmamızı sağlar.
VirtualAllocEx: VirtualAlloc gibi alan ayırmamıza yarar fakat uzak bir process’de işlem yaptığımız için VirtualAllocEx kullanılır. Açılan process’in içerisinde alan ayırır.
WriteProcessMemory: memcpy’nin Windows API karşılığıdır belirli bir uzak sanal bellek alanına veri yazmamıza yarar. Uzak işlemler için kullanılır.
CreateRemoteThread: Uzak bir process üzerinde Thread oluşturmamıza ve çalıştırmamıza olanak tanır.
Örnek;
NOT: ProcessID değerini powershell üzerinde Get-Process diyerek öğrenebilirsiniz. Eğer belirli bir process üzerinde işlem yapmak istiyorsanız “Get-Process -Name <process-adı>” şeklinde process id öğrenilebilir.
DLL Injection
DLL injection zararlı bir DLL dosyasını sistemde çalıştırmamızı sağlar. Bir zararlı DLL oluşturulur ve bu DLL dosyasının dosya yolu char tipinde bir diziye yazılır ve daha sonra her zaman yaptığımız gibi virtual ram’de bir alan oluşturulur bu alan OpenProcess ile uzak bir işlem olarak Process injection gibi yapılabilir. Bu açılan alana WriteProcessMemory ile dizimizin içinde bulunan DLL yolu yazılır. Artık DLL path ram’de bir adres üzerinde tutuluyor peki bu DLL’i nasıl yükleyeceğiz ? Bu DLL’i yüklemek için LoadLibary Windows API fonksiyonu kullanılır fakat LoadLibary bir kernel32.dll fonksiyonu olduğu için önce bu fonksiyona erişmemiz gerekir. LoadLibary işlevini kullanmak için onun kernel32.dll içinden çekip adresini bulmamız gerekiyor. Bunun için GetModuleHandle ve GetProcAddress gibi Windows API’ları kullanabiliriz. GetModuleHandle bir dll dosyasının handle’nı döndürürken bu döndürülen handle içerisinde GetProcAddress ile LoadLibary fonksiyonunu çekebiliriz.
IntPtr LoadLibary = GetProcAddress(GetModuleHandle(“kernel32.dll”), “LoadLibaryA”)
Yukarıdaki gibi bir kullanımı vardır artık LoadLibary pointerı yardımıyla LoadLibaryA işlevine erişebiliriz. Bir sonraki adımda oluşturacağımız thread’e bu adresi işaret eden pointerı işlev olarak vereceğiz.
Son olarak CreateRemoteThread ile bir thread oluşturup, bu thread’in yapmasını istediğimiz işleve LoadLibary pointer’nı ve alacağı parametreye ise char bir dizi içerisine yazdığımız zararlı DLL dosyamızın yolu bulunan diziyi vereceğiz (Ram’de yazdığımız adresi).
NOT: Normalde Process Injection yaparken CreateRemoteThread ile oluşturduğumuz thread’in parametre kısmına 0 verip işlev kısmına kendi zararlı kodumuzun yazılı olduğu dizinin yazıldığı adresi verirdik fakat bu sefer işlev kısmına LoadLibary fonksiyonunun adresini vereceğiz çünkü çalışmasını istediğimiz işlev LoadLibary. Parametre olarak ise genelde 0 verdiğimizi söylemiştim fakat bu sefer parametre olarak dizimizin içerisinde yazılan DLL yolunu yazdığımız adresi vereceğiz ve böylece oluşturduğumuz thread, dll yolunu parametre olarak alıp LoadLibary fonksiyonunda çalıştıracak ve zararlı DLL dosyamız çalışacak.
ZARARLI OLUŞTURMA
Yukarıdaki işlemleri öğrendik, şimdi sıra bir zararlı yazmakta. Zararlımız bir shellcode injection şeklinde olacak.
- Zararlı binary dosyamızı oluşturuyoruz. (El ile yazılmış bir kod da kullanabilirsiniz fakat ben Havoc C2 sunucusunun ürettiği bir binary kullanacağım.
Havoc ile shellcode oluşturma
2. Oluşturduğumuz binary dosyasını, Shellcode injection için kullanacağımızdan dolayı C dilinde kullanabileceğimiz hex formatına dönüştürmeliyiz.
3. Dönüştürdüğümüz hex formatındaki diziyi shellcode injection kodumuza ekleyeceğiz. Unutmayın enjekte etmeye çalıştığımız kod dizinin içerisine yazdığımız kod iken, enjekte edecek kod iskelettir.
4. Tüm C kodunu hazırladığımıza göre dosyamızı derleyip çalıştıralım.
Dinleyicimize bağlantı geldi.
Ayrıca eğitim amaçlı shellcodelar inceleyip nasıl yazıldıklarını merak ediyorsanız, buradan bir çok platform için yazılmış farklı bir çok shellcode’ a ve nasıl obfuscate (KARARTMA) edildiklerine erişebilirsiniz.
ZARARLI YAZILIMIN İNCELENMESİ
Şimdi birde bu işe tersinden bakalım, bu oluşturduğumuz zararlı yazılımı bir Blue Team üyesi gibi inceleyelim. Bunun için PE-Tree kullanacağım.
Çalıştırılabilir-yürütülebilir (Portable-Executable) dosyalar kendi içerisinde bölümlere ayrılır.
DOS-STUB BÖLÜMÜ
Ünlü “!This program cannot be run in DOS mode.” yazısının bulunduğu kısımdır.
DOS-HEADER BÖLÜMÜ
Aşağıdaki resimde de görmüş olduğunuz gibi DOS-HEADER kısmı “MZ” değerinin bulunduğu kısımdır en son adresi ise e_lfanaw kısmında bulunan adrestir. Bu son adresten sonra NT-HEADER bölümü başlar.
NOT: “MZ” değeri bir magic number’dır ve bir programın çalıştırılabilir bir dosya olduğunu gösterir. Bunun gibi bir çok magic numbur bulunur, örneğin JPEG dosyalarının magic number’ı “JFIF” dir. Bu magic number değerleri, zararlı yazılımlarda evasion teknikleri için kullanılabiliyor.
NT-HEADER BÖLÜMÜ
NT Header kendi içerisinde ikiye ayrılır. Bu bölümler File-Header ve Optional-Header kısımlarıdır.
Optional-Header’ın Machine kısmında programın 64 bit mi yoksa 32 bit bir mimaride mi olduğunu belirtir, AddressOfEntryPoint kısmında ise programın yürütülecek ilk kodu bulunur. File-Header kısmında güzel iki adet bilgi edinebiliriz. Bu bilgilerden bir tanesi bu programın hangi işlemci ile derlendiği ikincisi ise bu programın derlendiği tarihtir. Bu bilgileri Machine ve TimeDateStamp kısımlarında görebiliriz.
SECTION HEADER
Section Header en önemli bölümlerden biridir. Bu bölümde .text veya diğer adıyla .code , .data, .rdata, .idata gibi bölümler bulunur.
- .text veya .code: Bu alt bölüm çalıştırılabilir dosyanın kodunun bulunduğu kısımdır.
- .data: Bu alt bölümde veriler tutulur. Okuma ve yazma izni var ama execute izni yoktur.
- .ndata: Bu alt bölüm başlatılmış verileri içerir.
- .rdata/.idata: Bu alt bölümde dışardan içeri import edilen kütüphaneler ve windows API’lar tutulur.
- .rsrc: Resimler vb. tutulur.
IMAGE IMPORT DESCRIPTOR
PE(Portable Executable) dosyamız yürütüldüğü zaman kullanılan Windows API’ları gösterir.
Aşağıda da görüldüğü gibi bizim oluşturduğumuz zararlı dosya KERNEL32.dll üzerinden bir çok Windows API içe aktarıyor. Bunların en başında gelen CreateTheared bile bu dosyadan şüphelenmemiz için yeterli.
SON SÖZ
Bu tür incelemelere ve tersine mühendisliğe karşı zararlı yazılım geliştiricileri bu yazılımları yazarken çok fazla karartma tekniği kullanılır. Örneğin değişken adlarını karmaşık şeyler koyar, bir çok switch case ve if else yapıları ile kodu çok karmaşık hale getirir, döngüler koyarak bir dizi içerisinden gelen harfleri tek tek çekebilir veya bir değişkene tek tek basılıp birleştirilebilir, böylece “CreateThread” yazısını kod çalıştıktan sonra yazılacak şekilde ayarlayabilir. Bunların yanı sıra paketleyiciler kullanılabilir böylece zararlı yazılımı paket açılmadan inceleyemez duruma gelebiliriz. XOR gibi encryption teknikleri kullanılabilir, böylece yine program çalışmadan zararlı kod çözülmeyecek ve anlaşılamayacaktır. Bunların yanında CreateRemoteThread yerine bir APC(asynchronous procedure call) oluşturarak bu işlemler yapılabilir.