The Stack Smashing Buffer Overflow

The Stack Smashing, en temel ve basit buffer overflow yöntemidir. 1995 yılında “Peiter Zatko” tarafından yayınlamıştır.

Zafiyetin iyi bir biçimde anlaşılabilmesi için bir fonksiyonun başka bir fonksiyon tarafından çağırılması durumunda gerçekleşen olayları incelemek gerekmektedir.

“my_func(param1, param2, …, paramN)” şeklinde bir metot olduğunu ve bu metodun “main” fonksiyonu içerisinde çağırıldığını varsayalım.

İlk olarak “my_func(param1, param2, … ,paramN)” metodu çalıştırıldığında; alınan parametre değerleri, local değişken olan var1, var2, … ,varN değerlerine aktarılacaktır.

“my_func()” fonksiyonu çalıştırılmadan önceki STACK durumu aşağıdaki şekilde ifade edilebilir.

Görüleceği üzere SP kaydedicisi, STACK yapısının en üst değerini saklamaktadır. IP kaydedicisi ise bir sonraki makine kodunun adres değeri olan “Z” ifadesini içermektedir. BP kaydedicisi ise STACK yapısının base/taban adresini içerisinde saklamaktadır.

“my_func()” metodunun çağırılması işlemine geri dönüldüğünde, ilk olarak fonksiyon parametrelerinin stack içerisinde taşınması gerçekleştirilecektir. STACK daha düşük değerlikli adres değerlerine doğru büyüme göstermektedir. Bu nedenle ilk fonksiyon parametresinin en son push edilmesi gerekmektedir (paramN, … ,param2 ,param1)

Dikkat edilmesi gereken bir diğer nokta ise “my_func()” metodunun tamamlanmasının ardından, uygulamanın kaldığı yerden devam edebilmesinin sağlamasıdır. Bunun için metot çağrısından sonra gerçekleştirilen ilk komuta ait adres (“V”), stack yapısının en üst bölümüne yerleştirilir.

Metot parametreleri ve dönüş adresi değerlerinin stack içerisindeki yerleşimi aşağıdaki şekilde görselleştirilebilir.

Sıra metodun çağırılması işlemi bulunmaktadır. Fakat bu işlemden önce gerçekleştirilmesi gereken bazı kontrol bulunmaktadır. Bunlar;

  1. Eski BP adresinin stack içerisinde saklanması
  2. SP kaydedicisi, değeri ile BP kaydedicisi içeriğinin güncellenmesi
  3. Stack içerisinde local değişkenler için bellek alanı ayrımı yapılır. Local değişkenler v1,v2 … vN şeklinde (stack ile zıt yönde) bellek üzerine yerleştirilmesi

şeklindedir.

Gerçekleştirilen bu üç işlem “prolog” olarak adlandırılmaktadır. Prolog sonrası elde edilen stack görüntüsü aşağıdaki şekildedir.

Tanımlı bulunan bir “prolog” bulunduğu gibi, bir de işlem sonrasında kaydedici içeriklerinin temizlenmesi amacı ile kullanılan “epilog” tanımı bulunmaktadır. Bu tanım üç maddeden oluşmaktadır.

  1. SP içeriği W’ adresi ile güncellenir ve bu sayede local değişkenlerin silinmesi sağlanmış olur.
  2. Daha önceden stack içerisine aktarılmış olan X (Eski BP değeri), BP içerisine aktarılır.
  3. Stack yapısı “prolog” öncesindeki görüntüsüne kavuşur. Tek fark IP değeri olacaktır. Burada R olarak tanımlayacağımız IP kaydedicisi içeriği, metot dönüş adresine erişmek için kullanılacak olan pop işlem adresini içermektedir (Burada verilen örnekte dönüş adresi olarak V tanımlanmıştır)

“epilog” sonrasında stack görüntüsü aşağıdaki şekilde olacaktır.

Peki burada oluşabilecek problem nedir? var1, var2, …, varN local değişkenlerinin bir tür buffer olduğunu varsayalım. Buffer yapı itibari ile stack ile ters yönde büyüme göstermektedir. Yani düşük adres değerliğinde en son girilen değeri barındırmaktadır. Bu nedenle local değişkenlerin bellek üzerinde sıralaması var1, var2, …, varN şeklinde olmaktadır.

Buffer büyüklüğünün uygulama tarafından kontrol edilmediği de düşünülürse, beklenilen miktarın dışında gönderilen veriler diğer local değişkenlerin üzerine yazılmaya başlanılacaktır. Daha sonra BP ve uygulamanın çalışmaya devam edeceği return adres değerleri üzerine taşan verilerin yazılması gerçekleştirilecektir.

Return adres değiştirilebilmesi durumunda, saldırgan tarafından uygulama içerisinde gerçekleşen dallanma yönetilebilecektir. Bu sayede saldırgan tarafından buffer içerisine eklenilebilecek olan zararlı işlemlerin çalıştırılması mümkün olabilecektir.

Kullanıcıdan alınan data ile oluşan buffer taşması sonucunda elde edilen stack görüntüsü aşağıdaki şekildedir.

Görüleceği üzere saldırgan tarafından buffer taşırılması ile metodun return adres değerinin saldırgan kodlarını içeren bellek adresine yönlendirilmesi gerçekleştirilmektedir.

Bu noktaya kadar herşey kolay olarak görülse de, buffer overflow zafiyetinin istismarı için bir takım engeller bulunmaktadır. Bunlar;

  1. Zorlukların ilki, belki de en önemlisi return adres değerini içeren bellek adresinin doğru tahmin edilmesi
  2. Taşma sonrası bellek üzerine yerleştirilecek olan shellcode bellek adresinin tahmin edilmesi

şeklindedir.

Aşağıda, buffer overflow zafiyeti içeren bir C kod örneği bulunmaktadır.

Görüleceği üzere “256” byte büyüklüğünde bir buffer alanı tanımlanmış olup, kullanıcıdan alınan girdi boyutu kontrol edilmeden buffer alanı “strcpy” fonksiyonu içerisinde kullanılmıştır. 

C kodu “gcc” aracı kullanılarak derlenmeden önce linux sistemler üzerinde bulunan ASLR koruma mekanizması kapatılmalıdır. Bu koruma mekanizması, uygulama içerisinde bulunan komutların adres değerlerinin tahmin edilebilirliğini önlemektedir.

Aşağıdaki görselde, ASLR özelliğinin varsayılan olarak “2 – Full Adress Space Randomization” değerine sahip olduğu görülmektedir.

“0 – Disable” olacak şekilde bu değerin güncellenmesi işlemi gerçekleştirilir.

Daha sonra “gcc -g -fno-stack-protector -z execstack -o sample sample.c” komutu kullanılarak hazırlanmış olan C kodunun derlenmesi işlemi gerçekleştirilir. Derleme işleminde kullanılan flag değerlerinden kısaca bahsetmek gerekirse;

  • -g: Uygulamanın debug sembolleri ile birlikte derlenmesini sağlamaktadır. Bu sayede “gdb” debugger aracı ile daha kolay bir şekilde debug işlemi gerçekleştirilebilmektedir (Bu flag değeri olmadan da debug işlemi gerçekleştirilebilmektedir)
  • -fno-stack-protector: Stack Smashing ve benzeri overflow zafiyetlerine önlem almak amacı ile hazırlanılmış olan, derleyici bazlı kontrollerin devre dışı bırakılmasını sağlamaktadır.
  • -z execstack: Buffer taşırılması yöntemi ile stack içerisine yerleştirilecek olan kodların çalıştırılabilmesi gereksinimi bulunmaktadır. Bu nedenle “execstack” değeri kullanılarak stack alanının “executable” olması sağlanır.

Gerçekleştirilen işlemlerin ardından “sample” isimli derlenmiş uygulamanın elde edildiği görülmektedir. Artık uygulama “gdb” aracı ile debug edilmeye hazırdır.

İlk olarak “r ASDF” komutu ile konsol üzerinden uygulama çalıştırılarak incelenmeye başlanır.

Görüldüğü üzere kullanıcıdan alınan girdi, ekrana bastırılmaktadır. Uygulama kodlarından da görüleceği üzere 256 byte büyüklüğünde bir buffer tanımlanmıştır. Bu nedenle 256 byte üzerindeki her ifade buffer alanının taşmasına neden olacaktır.

Uygulamanın exploit edilebilmesi için kullanıcı tarafından alınan girdinin, kaçıncı karakter değerinin hangi kaydedici içerisine yazıldığının tespit edilmesi gerekmektedir. Stack içerisinde bulunabilecek olası alignment alanları nedeni ile 257 byte değerinden daha büyük kullanıcı girdileri kullanılmasına neden olacaktır.

İlk olarak 300 karakter içeren bir dize parametre olarak aktarılmaktadır. Bu işlem için python yardımı ile aşağıdaki “gdb” komutu kullanılmaktadır.

Görüleceği üzere “segmentation fault” hatasını elde edilmektedir. Yani bellek taşması gerçekleştirilmiştir. Uygulama içerisinde bulunun kaydedici içerikleri incelenerek bir sonraki işlemin ne olacağı planlanmalıdır. “info register” komutu yardımı ile kaydedici içerikleri ekranda görüntülenebilir.

“rbp”, base pointer kaydedicisinin içeriği incelendiğinde “0x41” karakteri ile doldurulmuş olduğu görülmektedir. “0x41” değeri ASCII tablosu içerisinde “A” karakterine karşılık gelmektedir.

Aşağıdaki grafikte stack alanının buffer taşması durumundan önceki görüntüsü bulunmaktadır.

Bir sonraki grafikte ise buffer alanının taşması sonrasında elde edilen stack görüntüsü bulunmaktadır.

Görüldüğü üzere buffer alanı taşması sonucunda ilk olarak alignment alanı, daha sonrada BP kaydedicisi içeriği “0x41” değeri ile doldurulmaktadır. Buffer adres değerleri stack ile zıt yönde ilerlediği için taşma sonrasında doldurulacak olan bir sonraki alan “A” değeri yani return adres değeridir.

Uygulamanın taşma yaşadığı metot sonrasında çalışmaya devam edeceği adres değeri return adres alanında saklanmaktadır. Bu alan IP – “Instruction Pointer” kaydedicisine karşılık gelmektedir.

Uygulama girdisi değiştirilerek kaydedici içeriklerinin değişimi gözlemlemeye devam edilir. Buradaki amaç IP kaydedicisi içersine yazılan kullanıcı girdilerinin offset adresini öğrenmektedir.

İlk olarak 270 adet “A” karakteri ile deneme gerçekleştirilir.

Ekran görüntüsünde görüleceği üzere “rip -IP” kaydedicisi içerisine “0x41” değeri yazılmıştır. Fakat tam olarak kaçıncı karakterden itibaren IP kaydedicisi içerisine yazıldığı halen bilinmemektedir. Bunun için 264 adet “A” ve 6 adet “B” karakteri girdi olarak uygulamaya verilmektedir (Farklı bir karakter kullanılarak offset değeri tahmini gerçekleştirilmektedir)

Bingo! “B” karakterine ait ASCII değeri “0x42” şeklinde ifade edildiği bilinmektedir. Girilen ilk 264 karakter, IP kaydedicisine kadar olan bölümü taşarak doldurmakta olup, son 6 karakterde IP kaydedicisi içerisine yazılmaktadır.

Return adres alanın değiştirileceği offset değerinin bulunmasının ardından, bu noktaya girilecek olan adres değerinin tespit edilmesi gerekmektedir. Yeni oluşturulacak return adres, buffer içerisine yazılmış olan shellcode ifadesinin başlangıç adresi olacaktır. Bunun için SP kaydedicisini kullanarak bellek içerisindeki değerler okunmalıdır. İlk olarak SP kaydedici değerinden 280 byte büyüklüğünde bir alan çıkartılır ve içeriği okunur.

“0x41” ifadesi ile bellek alanının doldurulduğu görülmekte. Fakat başlangıç adresinin 0x7fffffffdf78 ile 0x7fffffffdf88 aralığında bulunduğu görülmektedir. Bir sonraki denemede SP kaydedicisinden 288 byte büyüklüğünde bir alan çıkartılır ve tekrar içeriği okunur.

0x7fffffffdf80 adresi incelendiğinde, “0x41” değerlerinin başlangıç noktası olduğu görülmektedir. Elde edilen bu bilgi sayesinde, return adres alanına 0x7fffffffdf80 adres değeri yerleştirerek shellcode ifadesi çalıştırılabilecektir.

Son aşamaya geçilirken hazırlanılacak olan uygulama girdisi formülleştirilmelidir. Elimizde 270 karakterlik bir alan olduğu bilinmektedir. 264 karakterlik alan shellcode ve padding değerlerini, 6 karakterlik bölüm ise return adres değerini içerecektir.

İnternet üzerinden kullanılan mimariye özel bir çok shellcode bulunabilir. Biz x64 mimarisinde işlemleri gerçekleştireceğimiz için “\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05” şeklinde bir shellcode kullanacağız. Bu shellcode, kullanıcının “/bin/sh” ekranına erişmesini sağlamak amacı ile geliştirilmiştir. Toplam byte değerinin 27 olduğu görülmektedir. Bu doğrultuda:

  1. 100 adet NOP (“x\90”) komutu (Shellcode başlangıç noktasının tam tahmin edilemeyeceği durumlarda NOP komutu ile doldurma işlemi yapılmaktadır. NOP komutu hiçbir işlem gerçekleştirmeden bir sonraki komuta geçilmesini sağlayan assembly komutudur. Olurda shellcode başlangıç adresine erişilemezse, NOP komutuna erişilerek shellcode ifadesine kadar uygulama tarafından ilerlenmesi amaçlanmaktadır)
  2. Kullanılacak shellcode ifadesi bu alana yerleştirilir. 27 byte
  3. Birinci aşamada 100 ikinci aşamada 27 byte büyüklüğünde alan kullanılmıştır. Return adres alanı haricinde 264 byte büyüklüğünde alan bulunduğu için 264 – 100 – 27 = 137 işlemi gerçekleştirilir. Bu 137 byte büyüklüğündeki alan “A” karakteri ile doldurulacaktır.
  4. Son olarak 6 byte büyüklüğünde return adres değeri girdi içerisine eklenir. Burada dikkat edilmesi gereken nokta return adres değerinin “little endian” formatında olması gerekliliğidir (Little endian formatında her bir byte ters sıralı olacak şekilde bulunmaktadır)

Belirtilen kurallar dahilinde oluşturulan python ifadesi aşağıdaki şekildedir.

python -c ‘print “\x90″*100 + “\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05” + “A”*137 + “\x7f\xff\xff\xff\xdf\x80″[::-1]’

NOT: Girilen adres değerini little endian formatına dönüştürmek için “[::1]” ifadesi kullanılmıştır.

Görüldüğü üzere hazırlanan parametre yardımı ile uygulama üzerinden “/bin/sh” komut satırı çalıştırılması gerçekleştirilmiştir.

Yazar: Ahmet Akan

2016 Karabük Üniversitesi Bilgisayar Mühendisliği Mezunu. Kariyerine Uygulama Güvenliği Analisti olarak başladı ve bu alanda görev almaya devam etmekte.

Bir cevap yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir