Skip to content

Latest commit

 

History

History
202 lines (166 loc) · 8.04 KB

0x0.md

File metadata and controls

202 lines (166 loc) · 8.04 KB

Hedefimiz

Bu bölüm için hedefimiz programın akışını değiştirerek normalde erişilemez olan cant_get_here fonksiyonuna ulaşmak.

Kod analizi

Bölüm için verilen C koduna baktığımız zaman güvenli olmayan scanf() fonksiyonun 32 bytelık uzunluğa sahip bir karakter dizisine kullanıcı girdisi kopyalamak için kullanıldığını görüyoruz:

  char overflow[32];
  ...
  // !! OVERFLOW HERE !!
  scanf("%s", overflow);

scanf() doğrudan %s formatı ile kullanıldığında, (bir yolu olmadığı için) overflow kontorlü yapmadığından kullanıcının 32 karakterdan daha uzun bir girdi sağlıyarak değeri stack'te tutulan overflow karakter dizisi üzerinden stack overflow zafiyetini exploit etmesi mümkün.

PoC exploiti

Hadi öncellikle burda gerçekten de bir zafiyet olduğunu doğrulamak ile başlayalım. Bunu doğrulamak adına, programa uzun bir karakter çıktısı sağlayabiliriz:

python3 -c "print('A'*100)" | ./0x0.elf

Bu programın çökmesine, yani segmentation faulta (segfault) neden olacaktır. Ama ne kadar karakterden sonra programın çökmesine sebep olmaya başlayabiliriz?

Burda anlamamız gereken şey programın neden çöktüğü. Program stack üzerinde kayıtlı olan dönüş adresinin üzerine yazdığımız zaman çöküyor. Çöktüğü noktada main() fonksiyonun dönüş yaptığı nokta. Bu noktaya geldiğimizde, program stack üzerinde kayıtlı olan dönüş adresine gitmeye çalışıyor. Ancak dönüş adresi artık tamamı ile anlamsız A harfleri ile dolduğundan hatalı bir adrese dönüyor ve segmentation fault hatası alıyoruz.

Bu bilgi ile aslında size neden aldığımız hatanın isminin segmentation fault olduğunu açıklayabilirim. Çalışma esnasında, kernel her program için sanal bir bellek oluşturur. Program, kendine özel bu belleğin dışına erişmeye çalışınca, program kendine ayrılan bellek bölgesinin dışına erişemiyeceğinden (bölge yani segment, isim burdan geliyor) kernel programın bu davranışını raporlar, ardından da programı sonlandırır.

Bu sonlandırma işlemi için kernel programa ESEGV (11) sinyalini gönderir. Bunu kill -11 <pid> komutu ile kendiniz de test edebilirsiniz.

Hangi noktadan sonra return adrese yazabiliriz?

Çok basit. 32 bytelık dizimizi zaten doldurmamız lazım, elde var 32. Return adresin üstünde anlık içinde bulunduğumuz fonksiyonun stack adresini belirleyen rbp (base pointer) var, 8 byte daha eklersek 40. Yani tekniken 40 karakterden sonra return adrese yazmaya başlayabiliriz.

İşte size bir soru! Programa tam olarak 40 karakterlik bir çıktı sağlarsak ne olur?

  • A) Program sorunsuz şekilde çalışmaya devam eder
  • B) Program çöker
  • C) Öylesine okuyordunuz ve cevabı bilmiyorsunuz

Az önce söylediğime göre tekniken 40 karakterden sonra return adrese yazmaya başlayabiliriz değil mi? Evet bu doğru yani cevap A seçeneği... Ya da öyle mi? Eğer aşağıdaki komutu çalıştırırsanız yine de bir segfault alıcaksınız:

python3 -c "print('A'*40)" | ./0x0.elf

Çok ilginç... Peki Watson, bu nasıl oluyor?

C'nin gizemlerle dolu dünyası...

Evet yukardaki şey hatalı değil, 40 karakterden sonra adrese yazmaya başlıyoruz. Ama sorun scanf() fonksiyonun 40 karakter okumaması.

Eğer scanf() fonksiyonun manual sayfasına bakarsanız, açıklamasında şunu görecekseniz:

       The scanf() family of functions scans formatted input like sscanf(3)...

Ve eğer sscanf() in manual sayfasına bakarsaksanız, %s formatının nasıl çalıştığına bakabiliriz:

       s      Matches  a  sequence of non-white-space characters; the next pointer must be a pointer to the initial
              element of a character array that is long enough to hold the input sequence and the terminating  null
              byte  ('\0'),  which is added automatically.  The input string stops at white space or at the maximum
              field width, whichever occurs first.

Yani scanf() en sondaki yeni satır karakterini veya EOF'u (End-Of-File), \0 (0x00) ile değiştiriyor. Peki bunu neden yapıyor? C bilenleriniz çoktan durumu kavradı bile. C'de karakter dizilerinin nerde bittiğini takip bir yolu olmadığından genel C fonksiyonları karakter dizeleri ile işlemlerini \0 karakterine kadar yapıyor. C için \0 karakteri "hey karakter dizesinin sonuna geldik dostum" işareti gibi birşey. O yüzden bu karakter null byte'ın yani sıra trailing null olarak da bilinir. Bu olaya da genel olarak null termination denir.

Hadi sonucu gelelim. Biz 40 karakter girdiğimizde scanf() aslında overflow dizesine esktra bir \0 yazdığından totalde 41 karakter ile return adrese yazmış oluyoruz ve program çöküyor.

Anlaşılan gizemi çözdük Watson!

Kullanışsız bir PoC

Hadi python ile basit bir PoC yazalım:

from struct import pack

filler = b"A"*40 # overflow[] + rsp
ret    = b"N"*8

f = open("/tmp/ex", "wb")
f.write(filler+ret)
f.close()

Bu PoC basitçe dönüş adresini N harfi ile (0x4e) doldurmalı. Bunu test etmek adına programımızı gdb'de açabiliriz.

Şimdi programı gdb ile başlatmak için

gdb ./0x0.elf

Hadi önce okunabilir olan intel assembly moduna geçelim:

set disassembly-flavor intel

Daha sonra gdb'de scanf() fonksiyonundan hemen sonraya bir breakpoint yerleştirebiliriz:

disassemble main

bu çıktıdan call ... <scanf...den sonraki adresi bulun, ardından breakpoint yerleştirmek için:

break *<adres>

Şimdi ise sıra programı /tmp/ex girdisi ile çalıştırmakta:

run < /tmp/ex

Anında breakpointe varacağız, şimdi hadi stack'e bakalım:

x/24g $rsp

Aşağıdaki gibi bir çıktı görüyor olmanız lazım:

...:	0x4141414141414141	0x4141414141414141
...:	0x4141414141414141	0x4141414141414141
...:	0x4141414141414141	0x4e4e4e4e4e4e4e4e

0x4eyi görüyor musunuz? İşte orası return adresin olduğu yerdi, üzerine yazmayı başardık!

Şidmi eğer programı çalıştırmaya devam ederseniz segfault alacağız:

c

Artık tüm konseptleri anladığımıza göre, işe yarar bir exploit yazabiliriz!

Son exploit

Şimdi programın akışını nasıl cant_get_here fonksiyonuna çevireceğimizi tahmin ettiğinizi düşünüyorum. Tek yapmamız gereken PoC exploitimizdeki ret değişkenine cant_get_here'ın adresini vermek. Bu durumda program bu adrese dönüş yapıcak, biz de hedefimize erişmiş olacağız.

cant_get_hereın adresini bulmak için objdump aracını kullanabiliriz:

objdump -d ./0x0.elf  | grep cant_get_here

baştaki adresi kopyalıyalım, bunu pack fonksiyonu ile exploitimize yerleştirebiliriz:

from struct import pack

filler = b"A"*40
ret    = pack("<Q", 0x<adres>)

f = open("/tmp/ex", "wb")
f.write(filler+ret)
f.close()

pack fonksiyonu little/big endian muhabbetini halletmemizi sağlıyor. İlk başta verdiğimiz <Q argümanındaki < basitçe verinin little endian ile paketlenmesini söylerken Q verinin unsigned long long olarak (8 byte) paketlenmesini söylüyor. Bu fonksiyon ve argümanları hakkında daha fazla bilgi için struct modülün dökümentasyonunu inceleyin.

Bunun ile beraber exploitimiz hazır, hadi deneyelim:

python3 exploit.py && cat /tmp/ex | ./0x0.elf

Alıcağınız çıktı şöyle birşey olmalı:

root@o101:~/0x0# cat /tmp/ex | ./0x0.elf
Hello, what's your name?
Nice to meet you AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF@!
How did we get here?
Segmentation fault (core dumped)

Ve başardık! cant_get_here fonksiyonuna ulaştık.

Fakat program yine de çökücektir. Bunun sebebi cant_get_hereın normalde başka bir fonksiyona ait olan stack frame'ini kullanması. Bu sebeple cant_get_here işini bitirdiğinde stack frame'i doğru şekilde temizlenemiyor, ve return adresi tamamiyle rastgele bir adrese denk geliyor.

Ama bu bizim için önemli değil, sonuç olarak hedefmize ulaştık!


Önceki | Sonraki