Off-By-One/Frame Pointer Overwrite Buffer Overflow

Off-By-One/Frame Pointer Overwrite, bir tür buffer overflow zafiyeti olarak bilinmektedir. Return address değerinin güncellenebildiği, kullanıcı girdilerinin hiçbir kontrolden geçirilmeden buffer içerisine aktarılabildiği overflow zafiyetlerinden oldukça farklıdır.  Off-By-One/Frame Pointer Overwrite zafiyeti, saldırgan tarafından sadece ve sadece BP kaydedicisine ait LSB (Least Significant Bit) değerinin güncellenilebilmesini sağlamaktadır. Peki, sadece bir byte değerinin güncellenmesi nasıl bir tehdit oluşturabilmektedir?

Örneğin; “my_func(char[] buff)” isimli bir metot bulunduğunu varsayalım. “my_func(char[] buff)” metodu uygulama içerisinde çağırılmadan önce stack görüntüsü aşağıdaki şekilde olacaktır.

Görüleceği üzere, BP kaydedicisi stack yapısının başlangıç/referans konumunu, SP kaydedicisi de stack yapısının en üst değerini işaret etmektedir. 

Daha sonra metot içerisine aktarılacak olan parametrelerin stack içerisine push edilmesi ve prolog işlemleri sırası ile gerçekleştirilir. Bu işlemler sonucunda elde edilen stack yapısı aşağıdaki şekildedir.

Metot içerisindeki local değişken, BP kaydedicisi, SP kaydedicisi ve tüm işlemler sonucunda metodun çağırıldığı ana işleme dönüş adresi bilgilerinin stack içerisine yerleşimi görülmektedir.

Bir diğer önemli bilgide BP kaydedicisinin tüm stack yapısı için referans nokta olarak kullanılmasıdır. Tüm değişkenler ve return adress değerinin stack içerisindeki konumları BP kaydedicisi referans alarak belirlenmektedir. Örneğin; return address değerinin konumu x86 mimarisinde “ebp+4” şeklinde belirlenmektedir. Benzer şekilde lokal değişkenlerde “ebp-4” vb. şekilde ifade edilmektedir.

Bu nedenle, BP kaydedicisi uygulamanın doğru çalışması için kritik öneme sahiptir. Off-By-One/Frame Pointer Overwrite zafiyet tanımında ise BP kaydedicisinin son byte değerinin değiştirilmesinin gerçekleştirilebildiği belirtilmektedir. BP kaydedicisine ait LSB değerinin “0x0” şeklinde değiştirilmesi ile elde edilecek stack görüntüsü aşağıdaki şekilde olacaktır.

Görüleceği üzere “0xbffff238” olan BP kaydedici içeriği “0xbffff230” olarak güncellenmiştir. Buradaki kritik nokta ise return address değerinin BP kaydedicisine bağlı olarak değişim gösterecek olmasıdır.

“my_func(char[] buff)” metodunun tamamlanmasının ardından uygulama ana metot üzerinde çalışmaya devam etmek isteyecektir. Bu nedenle dönüş adresinin saklı bulunduğu “BP+4” adres değerliğine erişim sağlayacak ve içerisinde bulunan adres değerine dallanacaktır. Fakat BP kaydedici içeriğinin değiştirilmiş olması uygulamanın farklı bir akış ile ilerlemesine neden olacaktır.

Konunun daha iyi anlaşılabilmesi için aşağıdaki şekilde bir C kodu geliştirilmiştir.

“bar” metodu içeriği incelendiğinde kullanıcıdan alınan parametrenin “256” byte büyüklüğündeki buffer içerisine aktarıldığı görülmektedir. Fakat “main” metodu incelendiğinde ise kullanıcı girdisinin “256” byte büyüklüğü ile sınırlandırıldığı görülmektedir. Peki kullanıcı girdisi kontrol edilirken nasıl buffer alanı taşırılabilir?

Programlama dillerinde string ifadelerin hepsi bir karakter sonu elemanı içermektedir. Bu nedenle, kullanıcı tarafından “256” karakterlik string bir ifade gönderilmesi durumunda, sistem tarafından bu string ifadeye karakter sonu elemanı eklenilecektir ve kullanıcı tarafından gönderilen ifadenin toplam boyutu “257” karaktere ulaşacaktır.

Buffer ile stack alanının birbirlerine ters doğrultuda büyümesi göz önüne alındığında yaşanılacak olan taşma ile BP kaydedicisinin içeriği değiştirilecektir.

Bu zafiyet türündeki diğer bir önemli nokta ise buffer alanı ile BP kaydedicisi arasında herhangi bir değişken ve aligment alanı bulunmaması gerektiğidir. Çünkü sadece ve sadece tek bir byte taşması sağlanabilmektedir.

Yazılmış olan C kodu, gcc aracı ile aşağıdaki şekilde derlenmektedir.

Derleme işleminde kullanılan flag değerlerinden kısaca bahsetmek gerekirse;

  • -fno-stack-protector: Stack Smashing ve benzeri overflow zafiyetlerine önlem almak amacı ile hazırlanılmış olan, derleyici bazlı bir kontrol 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.
  • -mpreferred-stack-boundary: GCC derleyicisi varsayılan olarak 16-byte boundary adres değerlikleri kullanarak derleme işlemi gerçekleştirmektedir. 4-byte boundary adres değerliklerinin kullanılması için flag değeri “2” olarak seçilmektedir (Flag için belirtilen değer üstel ifade olarak işlemlere dahil olmaktadır. 2^X, bu nedenle 4-byte boundary adres değerliği elde edebilmek için “2” değeri parametre olarak belirlenir ve “2^2” işleminden “4” değeri elde edilir)

NOT: 16-Byte Boundary, bellek içerisindeki her segment adresinin 16 ve katları şeklinde olması gerektiğini belirtmektedir. Segmentlerin birbirlerine daha yakın olması amacı ile bu değer 4-byte olacak şekilde güncellenmiştir.

Bir sonraki aşamada derlenmiş olan uygulamanın GDB aracı ile debug edilmesi gerçekleştirilecektir. Bu işlem esnasında kaydedici içerisinde gerçekleşen değişimler net bir şekilde görülmektedir.

İlk olarak “257” adet “A” karakteri uygulamaya parametre olarak gönderilmektedir.

Görüleceği üzere uygulama tarafında kullanıcı girdisi kontrol edilmektedir. Bir sonraki denemede “256” adet “A” karakteri gönderilerek işlem sonucu incelenir.

“Segmentation Fault” hatası alındığı, dolayısı ile buffer bölgesinde taşma yaşandığı görülmektedir. Daha yakından incelemek ve breakpoint yerleştirmek amacı ile “foo” isimli metot içeriği disassemble edilerek.

Taşma işleminin “bar” metodu içerisinde gerçekleştiği bilinmektedir. Bu nedenle “bar” metodunun çağırıldığı satırın bir üst ve bir altında bulunan işlemlere breakpoint konularak meydana gelecek olan değişimler görüntülenmektedir.

Breakpoint yerleşiminin ardından 256 adet “A” karakteri parametre olarak gönderilerek uygulama yeniden çalıştırılır ve ilk breakpoint noktasına erişim sağlanır. “info register” komutu ile kaydedici bilgileri ekranda görüntülenir.

Daha sonra “x/x $ebp” komutu ile BP kaydedicisi içerisinde bulunan adres değeri görüntülenir.

“c” komutu ile uygulamanın bir sonraki breakpoint noktasına ilerlenmesi sağlanmaktadır.

Erişilen breakpoint noktasında kaydedicilerin genel durumları ve BP kaydedicisi içerisinde bulunan adres değerleri incelenmektedir.

Görüleceği üzere BP kaydedicisi içerisinde bulunan adres değeri, “0xbffff248” değerinden “0xbffff200” değerine dönüşmüştür. Yani, BP kaydedicisi içerisinde bulunan adres değerine ait son iki byte alanına string sonu ifade olan 0x00 değeri yazılmıştır ve BP içerisinde bulunan adres değerinin değiştirilmesi sağlanmıştır.

“c” komutu ile uygulamanın çalışmasına devam edilir ve yeniden kaydedici içerikleri yeniden kontrol edilir.

Görüleceği üzere IP kaydedicisinin içeriği “0x41” olacak şekilde değişim göstermiştir. Peki IP kaydedici içeriğinin değişimesin sebebi nedir? Cevap, BP kaydedicisi referans değer olarak kullanılmasıdır. Bir fonksiyon içerisinde bulunan BP kaydedicisi, fonksiyonun çağırıldığı ana metoda ait BP adresini saklamaktadır. Bu sayede çağırılan fonksiyonun işlemini tamamlamasının ardından, ana metoda ait BP adres değerini BP kaydedicisi içerisine yazabilecektir ve uygulama akışına bu alandan devam edebilecektir.

BP içerisinde bulunan adres değerinin değiştirilmesi ile bu değeri referans olarak kullanmakta olan return adress değerinin de değişim gösterdiği anlaşılmaktadır. (ebp-4 = return address) Return address değerinin değişimi de IP kaydedicisinin değişmesine neden olmaktadır.

Bir sonraki aşamada ise elde edilen bilgiler kullanılarak, saldırgan tarafından uygulama içerisinde shellcode çalıştırma işleminin nasıl gerçekleştirilebileceği incelenecektir. İlk olarak IP kaydedicisi içerisine yazılacak olan kullanıcı girdisinin offset değeri tespit edilmelidir. Bunun için 256 byte büyüklüğünde bir payload üretilir ve bu payload kullanılarak uygulama çalıştırılır (https://wiremask.eu/tools/buffer-overflow-pattern-generator/ adresinden payload üreticine ulaşılabilir)

Uygulama çalışması sonrasında IP kaydedici içeriğinin “0x39684138” olduğu görülmektedir.

İlgili payload ifadesinin üretildiği adres üzerinde bulunan “find the offset” özelliği kullanılarak “0x39684138” değerinin kaçıncı karakterden itibaren başladığı öğrenilebilmektedir.

“236” karakter sonrasında return adres değerinin değiştirilebileceği görülmektedir. Hazırlanılan payload yardımı ile bu değerin doğruluğu kontrol edilmektedir (python -c ‘print (“A”*236 + “B”*4 + “C”*16)’)

Görüleceği üzere “B – 0x42” karakterinin IP kaydedicisi içerisine yazıldığı görülmektedir.

Hesaplanması gereken bir diğer değer de shellcode ifadesinin yerleştirileceği adres değeridir. Bu adres değerinin hesaplanarak return adres ifadesi içerisine yerleştirilmesi gerekmektedir. Bu nedenle “A – 0x41” karakterinin bellek üzerine yazılmaya başlanıldığı adres değeri tespit edilmelidir.

Buffer overflow işlemi ile değiştirilmiş olan BP adresi – “0xbffff200” kullanılarak “A – 0x41” ifadelerinin bellek üzerinde yazılmaya başlanıldığı adres değeri belirlenebilmektedir. Offset değerinin 236 olduğu daha önceki adımlarda belirlenmişti, BP adresi üzerine tespit edilen offset değeri eklenilerek bellek içeriği okunması işlemi gerçekleştirilmektedir.

Görüleceği üzere “0x41” karakteri “0xbffff114” ile “0xbffff124” adres aralığında başlamaktadır. Bir sonraki denemede adres değerinin netleştirilebilmesi için BP adresine “232” değeri eklenilerek bellek içeriği okunur.

Buffer içerisindeki karakterlerin ilk okunmaya başlayacağı adres bilgisini bu şekilde elde edilmektedir. Tüm bu bilgiler doğrultusunda uygulamanın exploit edilmesi işlemi gerçekleştirilebilecektir.

İlk olarak, geliştirilecek olan payload ifadesinin aşağıdaki şekilde formülleştirilmesi gerekmektedir.

  • NOP*176 + shellcode (~25 byte) + PaddingCharacter*35 + Return Address (4-Byte) + 16 PaddingCharacter

X86 mimarisi üzerinde işlemlerimiz gerçekleştirileceği için bu mimariye uygun, “/bin/sh” komutunun çalıştırılmasını sağlayacak olan “\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90 – 28 Byte” shellcode ifadesi kullanılacaktır. Bu doğrultuda oluşturulan payload aşağıdaki şekilde olacaktır (Return address değeri little-endian formatında dönüştürülerek payload içerisine eklenmektedir)

  • “\x90″*176 + “\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90” + “A”*32 + “\xbf\xff\xf1\x18″[::-1] + “A”*16

Oluşturulan payload ifadesinin uygulamaya parametre olarak aktarılması sonrasında “/bin/sh” komut satırının açıldığı ve üzerinde işlem yapılabildiği görülmektedir.

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