24 Mayıs 2015

Virüs 101 – Nasıl Virüs Yazılır – Bölüm 2


Nasıl virüs yazılır konulu makale serimizin bu bölümünde virüs geliştirme stratejimizi ve kabaca bu stratejiyi nasıl uygulayacağımızı anlatacağım.
Uygulamanın teknik olarak basit olmayacağını söylemeliyim, kodun tam olarak ne yaptığını anlamak için pek çok ön bilgi gerekiyor. Ama en karmaşık projenin bile stratejisi basit ve anlaşılabilir biçimde aktarılabilir. Uygulama aşamasındaki problemler ve teknik bilgiler için de kapsamımızla uyumlu biçimde teknik bilgi vereceğim.

Virüs yazılımları biyolojik isimdaşlarında olduğu gibi hayat bulmak için başka bir canlı sisteme ihtiyaç duyarlar. Bizim virüsümüz de başka bir çalıştırılabilir dosyanın bünyesine yerleşerek onun yaşam fonksiyonlarından faydalanacak. 

Bunun için virüs kodumuzu farklı bir çalıştırılabilir dosyaya kopyalamamız lazım. Teorik olarak bunu dosyanın çalıştırılabilir herhangi bir section’ına kodumuza kopyalayarak yapabiliriz. Section nedir, çalıştırılabilir olup olmadığı nerede belirtilir gibi soruların yanıtlarını “Stack Tabanlı Hafıza Taşma Açıklıkları” video serimizin ilk bölümlerinde bulabilirsiniz. Kabaca PE dosya formatı ile ilgili temel bilgiler burada çok işimize yarayacak. Biz kodumuzu dosyanın içindeki son section’ın sonuna ekleyeceğiz. Kopyalama stratejimiz bu olacak.

İlk stratejimiz virüs kodumuzun nasıl aktif olacağı ile ilgili. Bunun için virüs kodumuzun bünyesine yerleşeceği çalıştırılabilir dosya çalıştırıldıktan (yani OS loader tarafından hafızaya yüklenerek barındırdığı instruction’lara geçiş sağlandıktan) sonra bir noktada aktif hale gelmesi (yani virüs kodunun çalıştırılmaya başlaması) gerekecek. Bizim stratejimiz olabilecek en basit stratejilerden birisi olacak. Bünyesine yerleştiğimiz dosya yüklendiğinde ilk çalıştırılacak kod bölümünün virüs kodumuz olmasını sağlayacağız. Bunun için de PE dosya formatı ile ilgili temel bilgilere ihtiyacımız olacak.

Virüs kodumuz aktif olduktan sonra ne yapacağımız virüsü ne için yazdığımıza bağlı. Ancak her ne amaçla yazmış olursak olalım şu temel probleme çözüm bulmamız gerekecek; derlenmiş olan bir kodun hafıza alanında hiç bir import imkanına sahip olmayan bir kod ile anlamlı faaliyetler yapmak zorundayız. Bunun için exploit geliştirme alanından edindiğimiz temel shellcode tekniklerimizi kullanacağız. Özetle bu adımdaki stratejimiz, virüs kodu çalışmaya başladığında ihtiyaç duyduğumuz kütüphaneleri  hafıza alanına yükleyerek ihtiyacımız olan fonksiyonları doğru parametrelerle çalıştıracağız. Aslında işin özü bu bölümde. Diğer adımlar virüs kodunun kendini çoklayabilmesi, virüs kodunun çalıştırılması ve virüs kodunun işi bittiğinde hiçbirşey olmamış gibi bünyesinde yer aldığı uygulamanın olması gerektiği gibi çalışmaya devam etmesinin sağlanmasından ibaret. Bizim bu adımda yapacağımız şey kompleks bir virüse kıyasla çok basit olacak; bünyesinde bulunduğumuz çalıştırılabilir dosyanın dosya sisteminde bulunduğu dizin içinde yer alan ve adı “a.exe” olan bir dosyayı arayacağız. Eğer böyle bir dosya varsa virüs kendi kod bölümünü birinci strateji adımında belirttiğimiz şekilde “a.exe” dosyasının son section’ın sonuna yazacak.

Bir sonraki adım bünyesinde bulunduğumuz uygulamanın hiçbirşey olmamış gibi çalışmasına imkan tanımak olacak. Bunun için bulaştığımız dosyanın orjinal başlangıç instruction adresini bir yerde saklamış olmamız lazım. Biz bu yeri fonksiyonel olarak ihtiyaç duyulmadığını varsaydığımız bir PE dosyası başlık alanı olarak belirledik. 32 bit’lik mimari için geliştirdiğimiz virüs kodumuz bu veriyi saklamak için bulaştığı uygulamanın başlık alanları için de 32 bit’lik bir alanı kullanacak. Virüs kodumuz kendini a.exe isimli bir başka çalıştırılabilir dosyaya kopyaladıktan hemen sonra içinde bulunduğu prosesin hafıza alanında orjinal ilk instruction adresini okuyacak ve uygulama akışını bir “jmp” instruction’ı ile bu adrese yönlendirecek.

Aşağıda bu adımları tekrar ve özet olarak sıralayacağım, ancak yukarıdaki ifadeleri okurken bir yumurta tavuk durumu olduğunu farketmişsinizdir. Virüs kodu bir sonraki dosyaya atlarken oluşturduğu veri yapılarını kendi içinde bulunduğu uygulamanın veri yapılarında da bir önceki bulaşma sırasında hazırlanmış olarak bulmalı. Bu yüzden sıradan bir uygulama algoritmasından biraz daha karışık bir algoritmamız olacak.
  • Adım 1: Uygulamanın ilk instruction adresini kendi koduna işaret edecek biçimde düzenleme (ve bunu yaparken orijinal ilk instruction adresini de saklama)
  • Adım 1,5: Virüs kodumuzu PE dosyasının son section’ın sonuna ekleme ve ilgili PE section başlığındaki büyüklük alanlarını düzenleme (bu işlem ilk virüs elle enjekte edilirken gerekli olacak, bu işlem 2. adımda otomatikleştirilmiş biçimde uygulanacak)
  • Adım 2: Bulaşılan uygulama ile aynı dizin içinde “a.exe” isimli dosya arama ve kendini bu dosyaya kopyalama (ve bunu yaparken gerekli veri yapılarını düzenleme)
  • Adım 3: Virüs kodumuz işini bitirdikten hemen sonra bulaşılan uygulamanın orijinal ilk instruction’ından itibaren çalışmasına izin verme
İlk virüs kodumuzu elimizle enjekte edeceğiz. Bunun için “ldp.exe” adlı uygulamayı kullanacağız. Bu uygulama bir ldap sunucusuna bağlanarak sorgulama yapmak için kullanılan bir uygulama. Uygulamanın PE dosya’sı da olabildiğince sade, farklı uygulamalar ve derleme opsiyonları ile çok daha karmaşık PE dosyaları ile karşılaşmamız mümkün.

Bu uygulama dosyası içinde öncelikle uygulamanın orijinal ilk çalıştırılacak instruction adresini bulalım. Aşağıda virüs bulaştırılmamış olan ldp.exe’nin ilk çalıştırılabilir instruction adresinin bulunduğu PE dosya başlık alanını (IMAGE_OPTIONAL_HEADER’daki Address of Entry Point alanı) görüyorsunuz:

Yukarıda gördüğünüz adres OS loader uygulamayı hafızaya yükledikten ve runtime kodları çalıştırıldıktan hemen sonra uygulama içinde çalıştırılacak olan ilk instruction’ın adresi (teknik olarak farklı derleme opsiyonları ve PE dosya özelliklerinden faydalanılarak bu aşamadan önce çalıştırılabilecek kodları oluşturmak mümkün, ama bu tür kafa karıştırıcı ek detayları vermekten kaçınacağım).

Ne var ki, bu noktada uygulama hafızaya yüklendiğinde bu adresin yeniden düzenleneceğini bilmemiz lazım. PE dosyasının veya dosyanın yüklendiği işletim sisteminin ASLR desteği olmaması kaydıyla PE dosya imajının yükleneceği sıfır noktası yine IMAGE_OPTIONAL_HEADER başlığının içinde yer alan Image Base adresinde belirtilir.

Bu da uygulama hafızaya yüklendiğinde ilk instruction’ın adresinin aşağıdaki gibi olacağı anlamına gelir:

(Image Base) + (Address of Entry Point) = 0x1000000 +0x22272 = 0x1022272

ASLR desteği, işletim sistemi tarafından imaj başlangıç adresi ve stack alanı başlangıç adresinin rassallaştırılmasına imkan vermektedir. Eğer bu durum gerçekleşirse bizim hard coded olarak gömeceğimiz adresin geçerliliği ortadan kalkacaktır. ASLR desteğinin olup olmadığına ise aşağıdaki PE başlığından bakabiliriz:

IMAGE_OPTIONAL_HEADER başlığı içindeki DLL Characteristics alanı içinde ASLR desteği olup olmadığını görebiliriz. Eğer ASLR desteği olsaydı DLL Characteristics alanındaki 0x0040 bit’inin aktif olması gerekirdi. Burada gördüğümüz gibi “ldp.exe”nin zaten ASLR desteği yok. Ama bu virüs kodunun kendini kopyalayacağı bir diğer exe dosyasında ASLR desteğinin olmayacağı anlamına gelmez. Dolayısıyla virüs kodumuz içinde bulaşma sırasında bu engeli ortadan kaldırmamız gerekecek.

Bir sonraki problemimiz orijinal başlangıç adresini virüs kodumuzun başlangıç adresi ile ezdikten sonra bu bilgiyi nerede saklayacağımız. Bu bilgiye ihtiyacımız olacak çünkü virüs kodu işini bitirdikten sonra bu adrese atlamamız ve bulaştığımız uygulamanın işini yapmasına izin vermemiz lazım. Aksi takdirde kullanıcı neler olup bittiğinden şüphelenecektir.

Bu bilgiyi saklamak için pek çok yer düşünülebilir. Ben bunun için PE dosyasının DOS header başlığının uygun olabileceğini düşündüm. OS loader için bu alanın önemi nedir bilmiyorum ve tam olarak emin olabilmek için bu işlemi yapan kodların reverse edilmesi gerektiğini birinci bölümde belirtmiştim. Ancak bu varsayımım ciddi bir problem çıkarmadı. O yüzden bu başlık alanındaki 32 bitlik, yani 4 byte’lık bir alanı kullanacağız. Kullanacağımız alan OEM Identifier (2 byte) ve OEM Information (2 byte) alanları olacak:

Kısaca özetlersek orinal başlangıç adresini kendi virüs kodumuzun adresi (teknik olarak offset adresi ile imaj baz adresini toplayarak bulacağımız Virtual adresi ile) ile ezeceğiz ve bu değeri de DOS header içindeki bir alana saklayacağız.

Peki virüs kodumuzu nereye yazacağız? Bu konuda daha önce stratejimizin kodumuzu dosyanın sonuna eklemek olacağını söylemiştim. Manuel olarak kodumuzu kopyalarken çok sorun olmayacak olan bu problem virüs kendini kopyalarken maalesef o kadar kolay olmayacak.

Bu örnekte kullandığımız kodun (ve daha sonra bulaşmak için kullanacağımız kodun) PE dosya bölümlerinin çok karmaşık olmadığını tekrar etmeliyim. Örneğin PE dosya formatında son bölüm bir section olarak geçiyor, farklı bir derleyici, farklı bir kod veya farklı derleme opsiyonları son bölümün bir section olmamasına ve stratejimizin bozulmasına neden olabilir. Stabil bir virüs kodu tüm olasılıkları hesaba katmak zorundadır. Bizimkisi için bunu iddia etmek mümkün değil maalesef, bu sadece öğrenmeye yönelik bir kod.

Son section’ımızın son adresine PEView ile göz atabiliriz:

Burada görülen adres 0x0103A3F0 + 0x10 yani 0x0103A400. Bu noktada PEView’ın adres formatlarını açıklamam lazım. Daha önce kısaca değinmiştim, ve bu konuyla ilgili detaylı bilgiyi video programlarımızda bulabilirsiniz. PEView’ın öntanımlı adres formatı VA, yani virtual address formatı. Virtual adres değeri PE imajının hafızaya daha önce gördüğümüz Image Base adresinden yükleneceğini varsayan adres değeridir. Aşağıda, yukarıda gördüğünüz VA adresinin dosya’nın hangi offset adresinde (yani dosyanın ilk byte’ından itibaren kaçıncı byte’ının adresinde)  olduğunu görebilirsiniz:


Bu değer (yani 0x379F0 + 0x10 = 0x37A00 değeri) VA değerine pek de benzemiyor. Demek ki bu değeri hesaplamak için PE dosya formatı hakkında daha fazla bilgi sahibi olmamız lazım. Şimdilik manuel kopyalama yapacağımız için bu konuyu tartışmayı erteleyelim.

Şimdi manuel olarak dosyanın başlık alanlarını düzenlediğimizde ve virüs kodumuzu kopyaladığımızda neye benzeyeceğine bakalım. Bundan önce yukarıdaki görüntüde son section’ın son bölümlerinin “00” yani null karakterleri ile pad’lendiğine dikkatinizi çekmek istiyorum. Bunun elbette bir sebebi var. IMAGE_OPTIONAL_HEADER’ın Section Alignment alanında belirtilen değer section’ların hangi adresin katlarından başlayarak hafızaya yerleştirilmesi gerektiğini belirtiyor.

Buna göre tüm sectionların hex 1000’in katları olan RVA değerlerinden (RVA adresi yani Relative Virtual Address adresi ilgili verinin hafızaya yüklendiğinde imaj baz adresinden itibaren hangi adrese yerleştirileceğini gösterir) başladığını görebilirsiniz.




Section Alignment değerinin hemen altında ise File Alignment değeri yer alıyor. Bu değer ise section’ların büyüklüklerinin hangi büyüklüğün katlarına uygun olması gerektiğini belirtiyor.



Bu dosyanın File Alignment değerine göre her bir section’ın hex 200’ün katları büyüklüğünde olması lazım. Son section olan .rsrc section’ının son adresine baktığımızda da durumun bu şekilde olduğunu görürsünüz.

(0x3A3F0 + 0x10) – 0x33000 = 0x3A400 – 0x33000 = 0x7400

Bu bilgi bizim için önemli, çünkü virüs kodumuzu son section’ın sonuna eklediğimizde kodumuzun sonuna da File Alignment değerine uygun biçimde null byte’lar eklememiz gerekecek.

Şimdi manuel olarak gerekli düzenlemeleri yapılmış ve enfekte edilmiş olan ldp.exe uygulamamızın ilgili alanlarına göz atalım:
 
Öncelikle yeni Address of Entry Point alanımızın değerinin 0x3A400 olduğunu görüyoruz:
Buradaki alanın değeri RVA cinsinden olmalı idi, son section olan .rsrc section’ının da RVA cinsinden bitiş adresi yukarıdaki gibiydi. Tekrar etmem gerekirse PE dosya formatı ve bu tür bilgiler hakkında daha fazla bilgi sahibi olmak isterseniz Stack Tabanlı Hafıza Taşma Açıklıkları video serimizi izleyebilirsiniz.

Daha sonra buradaki orijinal değer olan 0x22272 değerini daha önce belirlemiş olduğumuz alana yazmalıyız:


Bu değeri yazmak için 4 byte’lık bir alana ihtiyacımız var. İlk 2 byte’lık alana 0x2272 değerinin yazıldığını görüyorsunuz. Bir sonraki 2 byte’lık alanda ise 0x2 değeri yer alıyor. Bunun sebebi intel mimarisinde hafıza organizasyonunun little endian formatına uygun olmasıdır. Bu virüs kodumuzu yazarken de bizim için birkaç noktada önemli olacak. Özetle burada 0x24 RVA veya offset adresinde 0x22272 değerinin bulunduğunu söyleyebiliriz. Bu konuda kendinizi daha rahat hissetmek istiyorsanız yapmanız gerekeni biliyorsunuz.

Derlenmiş olan virüs kodumuzu bir hex editörde açtığımızda aşağıdaki gibi bir başlangıcı olduğunu görebiliriz:


Toplam uzunluğun ise hex 520 + 15, yani hex 52F olduğunu görüyoruz:


Buna göre hex 200 kat olarak en yakın değer hex 600. Yani en az 0x600 – 0x52F byte kadar null byte ile pad’leme yapmamız gerekecektir.

Şimdi enfekte olmuş uygulamamızda virüs kodumuzun başlangıç noktasını ve son bölümünü inceleyelim:



Yukarıda gördüğünüz gibi virüs kodu bittikten sonra da null byte’lar devam ediyor. Null byte’ların sonu ise hex 200’ün katı şeklinde olmalı:


0x3ABF0 + 0x10 =  0x3AC00 hex 200’ün katıdır. Daha sonra dikkatle incelediğinizde farkedebileceğiniz bir durumu belirteyim. Burada virüs kodunun bittiği adres 0x3A92F idi. Yani pad’lenmiş alan büyüklüğü 0x3AC00 – 0x3A92F = 0x2D1, yani hex 200’e tamamlanmış ama bir hex 200 byte daha null byte eklenmiş. Bunun özel bir nedeni yok, virüs kodunu geliştirirken olur da kod daha da büyürse diye eklediğim bir alandı bu.

Şimdi biraz bu bahsettiklerimizi hareket halinde görelim. Bunun için örnek bir kurban uygulama seçtim, aslına bakarsanız bulaşma ve çalışma işlemini sorunsuz yapabildiğim bir uygulama buldum. VNC Viewer uygulamasının adını “a.exe” olarak değiştirdim. Bir binary debugger’da (bu örnekte Immunity Debugger) enfekte uygulamamızı çalıştıralım. Binary debugger’lar bizim için otomatik olarak Address of Entry Point’te bir breakpoint koyarlar. Debugger ile uygulamayı başlattıktan sonra F9 tuşu ile ilerlediğimizde Address of Entry Point’te duracağız:



Henüz assembly kodumuzu incelemediğimiz için virüs kodumuzu tanımıyorsunuz, ama adres tanıdık gelmeli. Virüs kodumuzun içinde orijinal entry point’e atlanan noktaya da bir breakpoint koyalım ve F9 ile bu noktaya kadar ilerleyelim:

Bu noktada JMP EAX instruction’ının parametresi olan EAX register değerinin de 0x01022272 olduğunu görebilirsiniz. Bu değeri de yukarıdaki tartışmalardan hatırlayabilirsiniz. F7 ile uygulamayı ilerlettiğimizde uygulamanın orijinal işlevini yerine getirmesi için ilk instruction’ına geliyoruz:

 Tekrar F9 tuşuna basarsak uygulamanın normal biçimde başladığını görebileceğiz:

Bu noktada a.exe koduna bulaşmayı başardık ve zaten enfekte olmuş olan kodumuz da normal biçimde çalıştı.

Son olarak bulaştığımız a.exe’nin başlık alanlarındaki değişiklikleri PEView ile inceleyelim:

Gördüğünüz gibi Address of Entry Point değeri 0xDA800 olarak görülüyor. Bu adresin son section olan .reloc section’ının sonlarına doğru olması lazım:

Virüs kodumuzun yazılması ve derlenmesi konularını bir sonraki makalemizde tartışacağız. 

Tekrar görüşmek üzere.


<<Önceki Bölüm                                                                                                      Sonraki Bölüm>>