Register Now

Login

Lost Password

Lost your password? Please enter your email address. You will receive a link and will create a new password via email.

Login

Register Now

Canyoupwn.me ~

TR | Code Reuse Saldırıları (ret2libc & rop)

rop

Merhabalar, bu yazımda sizlere binary exploitationda sıkça kullanılan bir teknikten bahsedeceğim. Diğer bir teknik olan Egg Hunting hakkındaki yazımı okumadıysanız buradan okuyabilirsiniz.

Tekniğe giriş yapmadan önce binary exploitation saldırılarının yapısı ve bu tekniğe neden ihtiyaç duyulduğundan biraz bahsetmek istiyorum. Bildiğiniz üzere “program” dediğimiz şey belli bir işi yapmak üzere, belli bir algoritmaya göre bir araya getirilmiş talimatlardan başka birşey değildir. Bu programlar genellikle kullanıcıdan bir girdi verisi alır, bu veriyi işler ve bir çıktı verisi üretir. Örnek vermek gerekirse bir e-ticaret sitesi düşünelim. Bu sitede yer alan ürünlerin her birine atanmış özel bir ID değeri vardır. Kullanıcı bir ürünün özelliklerine bakmak istediğinde, (arkaplanda) ilgili ürünün ID bilgisini sunucu tarafındaki yazılıma girdi olarak verir. Yazılım bu ID bilgisini alır ve sahip olduğu veritabanında bu bilgiyi sorgulatarak o ID’ye sahip olan ürünün bilgilerini elde eder, ardından bu bilgileri kullanıcıya çıktı olarak gösterir.

Yine aynı örnek üzerinden devam edelim. Bu bilgileri elde etme sırasında yazılım arkaplanda bir SQL sorgusu çalıştırıyor olsun. SQL sorgumuz da şu şekilde;

SELECT fiyat,ozellikler FROM urunler WHERE urun_id=$id;

$id ile belirtilen yer bizim girdi verimizin yer alacağı değişken. Bu da demek oluyor ki siteye “?id=1″ şeklinde bir parametre gönderirsek arka tarafta “urun_id=1″ için sorgu dönecektir. Burada dikkatinizi çekmesi gereken şey, veritabanına talimat olarak gönderilen SQL komutlarının da aynı gönderdiğimiz ID değişkeni gibi bir veri olması. Yani programın kendisi de, verdiğimiz girdiler de, üretilen çıktılar da esasında birer veridir. Bilgisayar için bu iki veri türünün tek farkı bunların ne şekilde yorumlanacağıdır. Bu programa girdi olarak gelen ID verisi arkada dönecek SQL sorgusuna parametre olacak şekilde yorumlanırken, SQL sorgu verisinin kendisi veritabanında çalıştırılacak talimatlar olarak yorumlanmaktadır. Bundan dolayıdır ki “?id=1 OR 1=1″ şeklinde bir parametre gönderildiğinde sorgu;

SELECT fiyat,ozellikler FROM urunler WHERE urun_id=1 OR 1=1;

halini alacak, gönderdiğimiz girdi verisinin kendisi de bir SQL talimatı olarak algılanacak ve ona göre işlem görecektir. Burada varmak istediğim nokta şudur; bilgisayarlar hangi verinin “data” hangi verinin “talimat” olacağı ayırımını yapamamaktadır! (Burada data’dan kastım talimat olmayan, program tarafından işlenecek olan verilerdir). Çoğu saldırı türünde olayın özü girdi olarak verdiğimiz veriyi karşıdaki yazılımın bir talimat olarak yorumlamasını sağlayarak kendi komutlarımızı çalıştırmaktır.

Binary exploitation’da da durum aynen böyledir. Konumuza hafiften giriş yaparken çalıştırılabilir dosyaların bölümlerini biraz inceleyelim.

Program_memory_layout.pdf

Bir programı çalıştırdığınız zaman ilgili çalıştırılabilir dosya harddiskten okunur ve çalıştırılmaya başlamadan önce hafızaya sağdaki gibi bölümlere ayrılmış şekilde yerleştirilir. Bu bölümlerden text segmenti çalıştırılacak kodların yer aldığı bölümdür. Geri kalan data, bss, heap ve stack adlı bölümler bizim programımızın işleyeceği verilerin, yani az önce “data” olarak bahsettiğim verilerin yer alacağı bölümlerdir.

Zafiyetli bir uygulama istismar edilirken çoğu zaman amaç kod akışını bir şekilde değiştirerek programı kendi çalıştırmak istediğimiz kodlara yönlendirmektir. kod akışını değiştirmenin yollarından birkaçı EIP registerine dolaylı yoldan yazmak, GOT kaydını değiştirmek, program içerisinde yer alan fonksiyon işaretçisinin üzerine yazmak şeklinde sıralanabilir. Peki kod akışını değiştirdik ama nereye yönlendireceğiz? Programın işleyeceği normal verilerin data, bss, heap ve stack kısımlarından birinde tutulacağını söylemiştim. Bunlardan heap ve stack kullanıcıdan alınan girdi verilerini tutar. Dolayısıyla biz çalıştırmak istediğimiz talimatları shellcode adı verilen makine kodu formatında programa girdi olarak verdikten sonra bunun hafızada yer aldığı adresi tespit ederek kod akışımızı o adrese yönlendirebiliriz. Program bu iki türdeki verinin ayırımını yapamadığı için aslında girdi olarak verdiğimiz datayı talimat olarak yorumlayıp çalıştırmaya başlayacaktır. Stack overflow zafiyetli çok basit bir uygulamanın istismarına yönelik bir video çektim. Örnek olması açısından göz atabilirsiniz.

Saldırganların program akışını değiştirme amacını bilen geliştiriciler, bunu önlemek adına kimisinin Data Execution Prevention, kimisinin NX bit dediği ancak kimsenin adı hususunda ortak karara varamadığı bir güvenlik mekanizması geliştirdiler. Bu mekanizmayla beraber hafızada herhangi bir alan (varsayılan olarak) aynı anda hem yazılabilir hemde çalıştırılabilir olamıyordu. Bunun anlamı saldırgan kod akışını ele geçirse dahi shellcode’u yükleyebileceği stack, heap benzeri alanlar Not Executable olduğundan dolayı programı oraya yönlendiremeyecektir. Bu sayede buffer overflow ve benzeri saldırılar engellenmiş(?) oldu.

sigsegv

Bunu gören saldırganlar da boş durmadılar ve “hafızaya kod enjekte etmeden programa kendi istediğimizi nasıl yaptırırız?” sorusu üzerinde kafa yormaya başladılar. Ardından hali hazırda çalıştırılabilir olan hafıza alanı .text akıllarına geldi. Yukarıda da bahsetmiştim text segmenti program kodlarının bulunduğu hafıza alanıdır. Dolayısıyla programın çalışabilmesi için bu alanın Executable olması gerekmektedir. Peki bu alanla ne yapılabilir? Düşünün, kendi kodunuzu yazamıyorsunuz ancak hali hazırda bir takım kod hafızada yüklü halde bulunuyor. Aynı bir kolaj çalışması yapar gibi hafızada yüklü olan bu kodları parça parça birleştirip kendi istediğimiz işi yapacak bir kod bütünü ortaya çıkarabiliriz! Tam olarakta bu işlem Code reuse saldırısı olarak adlandırılır. Bu saldırıda kullanılabilecek iki adet teknik bulunmakta bunlar: ret2libc ve return-oriented-programming.

ret2libc Yöntemi

Bu teknikte kod akışı direkt olarak programda yer alan fonksiyonlara yönlendirilerek amaca ulaşılmaya çalışılır. Örneğin hedef uygulamanın kaynak kodu şu şekilde olsun;

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void foo(char * arg) {
	char buf[40];
	strcpy(buf, arg);
	printf("Girdi: %s\n", buf);
}

void findme(char * arg) {
	system(arg);
}

int main(int argc, char **argv) {
	foo(argv[1]);

	return 0;
}

Burada findme() fonksiyonuna dikkat edin. Argüman olarak bir string alıyor ve içeride system (shell komutları çalıştırmaya yarayan bir fonksiyon) fonksiyonuyla argümanda verilen shell komutunu çalıştırıyor.. Normal program akışına dahil olmasa bile biz zafiyetten faydalanarak kod akışını bu fonksiyona yönlendirir ve parametre olarakta “/bin/sh” stringini verir isek zafiyeti başarılı bir şekilde istismar etmiş oluruz. Çağıracağımız fonksiyon illa programın içinde yer almak zorunda değil, programın çağırdığı kütüphanelerdeki fonksiyonları da pek ala kullanabiliriz. Peki bu yolla bir fonksiyona nasıl argüman verebiliriz? Gelin bir fonksiyona ait stack hafızasını inceleyelim.

Stack hafızası, bir fonksiyonun geri dönüş adresini, o fonksiyona giren argümanları ve fonksiyon içerisindeki lokal değişkenleri depolar.

assembly_stack

Bir fonksiyon çağırıldığında stack hafızası yukarıdaki gibi bir hal alır. EBP ve ESP arasındaki alan fonksiyonun kendi lokal değişkenlerinin tutulduğu alandır. EBP’nin hemen üstünde o fonksiyonun geri dönüş adresi, onun da üstünde fonksiyona giren argümanlar yer alır. Yani biz bir fonksiyon çağıracağımız vakit payload’umuz şöyle bir hal alacaktır;

bellek_tasirma +  geri_donus_adresi + "JUNK" + arguman1 + . . . + argumanN

11

 

Aşağıda bu tekniğin kullanımına dair bir uygulama videosu hazırladım. Detayları orada görebilirsiniz.

ROP Yöntemi

Hedef uygulamada kullanabileceğimiz system() benzeri fonksiyonlar her zaman bulunmaz, hatta bazı geliştiriciler bu istismar yolunu bildiklerinden bilinçli bir şekilde bu fonksiyonları devre dışı bırakabilirler. Bu durumda return-oriented-programming adı verilen bu ikinci yönteme başvurulur. Bu yöntemin tek farkı daha ufak kod parçalarının kullanılmasıdır. Yine kolajdan örnek verecek olursak bütün bir kelimeyi kesip kullanmak ret2libc ise 2-3 harflik kesitler alıp kullanmak rop tekniğidir. Bu teknikte çalıştırmak istediğimiz assembly kodu satır satır incelenmeli, her satırdaki talimatın ya tam aynısı yada ona denk gelecek bir talimat hafızada bulunup adresleri tespit edilmelidir. Tabi her bulduğumuz adresi kullanmamız mümkün değil zira program sadece bizim istediğimiz talimatı çalıştırmalı, ondan sonra gelen talimatları çalıştırmadan durmalıdır. Bunun için sadece kendisinin ardından ret komutu gelen, gadget adını verdiğimiz kod parçalarını kullanabiliyoruz. Bir örnek verelim, istismarın ardından çalıştırmak istediğimiz komutlar aşağıdaki gibi olsun;

;; exit sistem çağrısı. bu shellcode çalıştırıldığında program çıkış yapar.

xor eax, eax
mov al, 0x1

int 0x80

ROP yöntemiyle böyle bir shellcode çalıştırmak için bize 3 adet gadget lazım olacak. Onlarda şu şekilde;

;; ilk gadget
xor eax, eax
ret

;; 2. gadget
mov al, 0x1
ret

;; 3. gadget
int 0x80
ret

Bu gadgetlar hafızada aranıp adresleri bulunduktan sonra ard arda payload’a eklenir. Her bir gadget istediğimiz talimatı çalıştırdıktan sonra program akışını bir sonraki gadget’a yönlendireceğinden ötürü bütün gadgetlar arka arkaya çalışacak ve istismarımız başarılı olacaktır. Dikkat ettiğiniz üzere iki teknikte de hafızaya hiçbir kod enjekte etmedik, sadece hali hazırda yer alan kodları kendi amacımız doğrultusunda kullandık. ROP tekniğine dair uygulama videosunu da aşağıda bulabilirsiniz.

Bir sonraki yazıda görüşmek üzere..

About CanYouPwnMe

Hero!

Follow Me