İçindekiler

Hacktrick 2017 Tersine Mühendislik Eğitimi CTF

Giriş

Bu yazımda sizlere Hacktrick 2017 Tersine Mühendislik eğitiminin sonunda yapılmış olan ve öğrendiklerimizi kullandığımız basit 4 soruluk yarışmadan bahsedeceğim. Soruları GitHub‘a yükledim, eğitimde kullanılan sanal makinenin indirme linki ile soruların linkini yazının sonuna ekledim. Bir önceki yazımda Hacktrick 2017 etkinliğine biraz değinip Tersine Mühendislik eğitimine ve bu eğitime alım için yapılan 2 soruluk eleme sınavından bahsetmiştim. O yazıyı okumak isterseniz buraya tıklayabilir yada yazının sonunda paylaştığım linklerde bulabilirsiniz. Hemen sorulara geçiyorum.

Sorular

Soru 1

İlk soru için verilen dosyayı (soru1) çalıştırdığımızda bizden seri numarası girmemizi istiyor. Doğruysa “Serial is valid!”, değilse “Serial is not valid!” diye çıktı veriyor:

1
2
3
4
ab17@ab17:~$ ./soru1
Enter your license key: 123456
Serial is not valid!
ab17@ab17:~$ 

Tabi burada amaç doğru seri numarasını bulmak, yoksa bir hex editor ile “if” kontrolünün yapıldığı (koşullu dallanma) opcode’u (operation code/işlem kodu : 74) “if not” (75) olarak değiştirip kontrolü rahatça atlayabiliriz:

flag opcode edit

Bu değişiklikten sonra program, yanlış girilen tüm seri numaralarını doğru kabul edecektir:

1
2
3
4
5
6
7
ab17@ab17:~$ ./soru1
Enter your license key: 123456
Serial is valid!
ab17@ab17:~$ ./soru1
Enter your license key: qwerty
Serial is valid!
ab17@ab17:~$ 

Fakat bu soruda bize sorulan, doğru seri numarasını bulmamız o yüzden programın assembly kodlarını incelemeye başlıyoruz. Bu aşamada kullanabileceğimiz birçok araç bulunmakta (IDA, OllyDbg, vb.) ancak eğitimde Linux’ta bulunan gdb ve objdump araçları gösterildiği için onları kullanacağım. Öncelikle file komutu ile dosyanın stripped olup olmadığına bakıyorum çünkü stripped elf dosyalarında farklı bir yöntem izlemem gerekecek.

1
2
3
ab17@ab17:~$ file soru1
soru1: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.18, BuildID[sha1]=1fe069892bfca22cdbf33e85d47d87cb9ffd2ca3, not stripped
ab17@ab17:~$ 

Çıktıda da yazdığı üzere dosyanın stripped olmadığını anlıyoruz. Daha sonra terminale gdb -q soru1 yazıyorum ve ardından disas main diyerek programın (soru1) main fonksiyonunu disassemble ediyorum. (Intel syntax’ına alışkınsanız main’i disassemble etmeden önce set disassembly-flavor intel komutunu girmeniz yeterli):

soru1 gdb disassemble

Burada assembly kodları daha iyi anlaşılabilmesi için parçalara bölerek numaralandırdım ve numara sırasına göre sizlere bu işlemleri anlatacağım. Tabi sadece ilk soru için bütün assembly kodlarını koyup açıklıyorum, diğer sorular için sadece gerekli kısımları koyacağım. GitHub‘a yüklediğim soruları siz de indirip deneyerek bu işlemleri yapabilirsiniz.

Assembly Bölüm 1

Üstteki resmin “1” diye işaretlenen kısmında, başlangıç adresi 0x8048590 olan değer, eax register’ına kopyalıyor, daha sonra buradan stack’e kopyalayıp printf fonksiyonu ile ekrana bastırıyor. Bu adresin içerisine baktığımızda:

1
2
3
(gdb) x/s 0x8048590
0x8048590:    "Enter your license key: "
(gdb) 

Buradaki “x” parametresi, belleğin belirli bir adresindeki içeriği kendi belirlediğimiz formatta görüntülememizi sağlar. Belirlediğimiz format ise “s”, yani string. Anladığımız üzere bu adres, “Enter your licence key:” yazı dizisinin başlangıç adresi.

Assembly Bölüm 2

Burada yani <main+19>’da yine bir adresteki değeri eax register’ına kopyalıyor. Hemen bakalım içindeki değere:

1
2
3
(gdb) x/s 0x80485a9
0x80485a9:    "%7d-%7d-%7d-%7d"
(gdb)

Anlıyoruz ki burada kullanıcıdan 4 tane “???????-???????-???????-???????” formatında değer isteniyor. Zaten devamında da kullanıcıdan 4 tane değer alıyor. Bunuda “[esp+0x…]”daki “+” dan anlıyoruz.

Assembly Bölüm 3

Bu kısımda (main+60) kullanıcıdan alıp stack’e koyduğu değerleri, stack’ten register’a kopyalayıp teker teker birbirleri ile xor işlemine sokuyor ve en son çıkan değeri “0x1ce7ea” hex değeri ile karşılaştırıyor (cmp:compare). Eğer bu değere eşit ise (je:jump if equal) “0x80484b7” adresine (main+99) atlıyor, yani 5. adıma atlıyor. 5. adımda puts komutu ile “0x80485ce” adresinden başlayan değeri ekrana basıyor:

1
2
3
(gdb) x/s 0x80485ce
0x80485ce:    "Serial is valid!"
(gdb)

Daha sonra 6. adıma geçip sıfır değeri döndürerek programdan çıkıyor. Eğer xor işleminden çıkan değer, “0x1ce7ea” hex değerine eşit değil ise 4. adımdan devam ederek “0x80485b9” adresinden başlayan değeri ekrana basıyor:

1
2
3
(gdb) x/s 0x80485b9
0x80485b9:    "Serial is not valid!"
(gdb)

Ardından jmp (jump) komutu ile “0x80484c3” adresine (main+111) yani 6. adıma atlıyor ve programdan çıkış yapıyor. Şimdi akıllarda şöyle bir soru olabilir, neden “0x1ce7ea” değeri ile karşılaştırılıyor. Cevabı basit, soruyu hazırlayan Robin Dimyanoğlu’nun Twitter adresi (@1ce7ea) oluyor bu hex değeri 😃

Programın baştan sona assembly kodlarını anlatmaya gerek yoktu elbette ama ilk soru olduğu için gerekli olduğunu düşündüm. Şuan programın nasıl çalıştığını aşama aşama öğrendiğimize göre doğru bir seri numarası bulma işlemine geçebiliriz. Programda girilen 4 tane değer, birbiri ile xor işlemine girmeseydi sadece “0x1ce7ea” değerinin decimal (ondalık) karşılığını girip çok kolay bir şekilde doğru seri numarasına erişebilirdik ancak burada ihtiyacımız olan xor bilgisi. Hemen kısaca xor tablosuna bakalım ve ne olduğunu öğrenelim:

Seri Numarası Bulma

Programın baştan sona assembly kodlarını anlatmaya gerek yoktu elbette ama ilk soru olduğu için gerekli olduğunu düşündüm. Şuan programın nasıl çalıştığını aşama aşama öğrendiğimize göre doğru bir seri numarası bulma işlemine geçebiliriz. Programda girilen 4 tane değer, birbiri ile xor işlemine girmeseydi sadece “0x1ce7ea” değerinin decimal (ondalık) karşılığını girip çok kolay bir şekilde doğru seri numarasına erişebilirdik ancak burada ihtiyacımız olan xor bilgisi. Hemen kısaca xor tablosuna bakalım ve ne olduğunu öğrenelim:

xor table

Tabloyu kısaca size özetlemem gerekirse xor işleminde binary değeri aynı olanlar 0 (sıfır), farklı olanlar 1 (bir) olarak dışarı çıkıyor. “0x1ce7ea” hexadecimal (16 tabanında) sayısının decimal (on tabanında) karşılığına bakarsak:

1
2
3
(gdb) print 0x1ce7ea
$1 = 1894378
(gdb)

1ce7ea = 1894378 olduğunu görürüz. “0x1ce7ea” binary karşılığı ise: “0000 0000 0001 1100 1110 0111 1110 1010”. Şimdi xor’un güzelliği burada ortaya çıkıyor 🙂 ne demiştik, aynılar 0 (sıfır), farklılar 1 (bir). Biz bu binary’i sıfır ile xor işlemine sokarsak ne çıkar bakalım hemen:

xor operation

Gördüğümüz gibi sayının kendisi çıkıyor. Yani biz elimizdeki sayıyı (1894378) sıfır değerleri ile xor işlemine sokarsak yine aynı sayı çıkacak ve girdiğimiz seri numarası doğru olacak. Örnek: “1894378-0000000-0000000-0000000”:

valid serial key found

Tabii ki tek doğru serial key bunlar değil, örneğin: “1894378-45-4-41” veya “75-1894320-58-43”. Nasıl olduğunu merak ediyorsanız binary’e çevirip xor işlemlerini yapabilirsiniz 🙂 hatta C, Python, vb. dilleri ile seri numarası oluşturan çok kısa bir program yazabilirsiniz. Evet birinci sorunun çözümü bitti, diğer soruları kısa tutmaya çalışacağım 😃.

Soru 2

İkinci soruda verilen dosyayı (soru2) çalıştırdığımızda çıkan sonuç:

1
2
3
4
ab17@ab17:~$ ./soru2
This is not flag my dear ;) 
Look a little deep
ab17@ab17:~$ 

Terminale gdb -q soru2, ardından disas main yazıp assembly kodlarını incelediğimde sadece puts komutu ile ekrana yukarıdaki çıktıyı verdiğini gördüm, yani programın main fonksiyonunda flag olabilecek hiçbir şey yoktu. Ardından programın içindeki fonksiyonlara göz atmak istedim. Bunun için terminalde gdb için info functions, readelf: readelf -s soru2 veya objdump ile objdump -d -M intel soru2 diyerek programın içindeki fonksiyonları inceleyebilirsiniz.

1
2
3
4
5
6
7
8
9
(gdb) info function
All defined functions:

Non-debugging symbols:
...
0x080483c4  hiding
0x08048413  main
...
(gdb) 

Programın içinde hiding adlı bir fonksiyon tanımlanmış ancak bu fonksiyon çağırılmadığı için görmüyoruz tabi 🙂 fonksiyonun içine bakmak için disas hiding yazmak yeterli:

hiding function in flag

Fonksiyonun çok ayrıntısına girmeyeceğim, kırmızı oklar bu fonksiyonun bir for loop (döngü) olduğunu gösteriyor. İlk başta stack’e 0 (sıfır) sayısını atıyor, daha sonra bunu 0xa (10) değeri ile karşılaştırıyor. küçük eşit ise içerisindeki işlemleri 11 kez gerçekleştirip çıkıyor. Tabi burada ekrana birşey basmadığı için fonksiyonu çalıştırabilsek bile ekrana bir çıktı vermeyecek, o yüzden yapmamız gereken bu fonksiyona atlamak için main’e bir break point koyup (break main) daha sonra hiding fonksiyonu çalışıp çıkmasın diye o fonksiyonun sonuna da bir break point koyacağız: break *0x080483f3. Bu aşamada programı run veya r komutu ile çalıştırdığımızda fonksiyona atlayıp for döngüsü bittikten sonra program kapanmadan duracaktır. Başka bir adrese atlamak için bir komut daha var, programı çalıştırdıktan sonra jump hiding veya jump *[adres] şeklinde girerseniz oraya da atlayacaktır. Şimdi son bir aşama kaldı, o da sonucu ekrana yazdırmak:

flag output
Görüldüğü gibi flag’i bulduk: “x0r_s0_3asy” 🙂

Soru 3

Üçüncü soruyu da biraz zorlaştırmak için striplemişler. Soru yine kolay tabi ama amaç burada öğrendiklerimizi kullanmak. soru3ü çalıştırdığımızdaki çıktı:

1
2
3
ab17@ab17:~$ ./soru3
Do you know how to look heap !
ab17@ab17:~$

Burada heap hafızasına bakmamızı istiyor muhtemelen o yüzden main’de neler oluyor bir bakalım. Tabi dosya stripped olduğu için gdb‘de disassemble main dediğimizde hata verecektir çünkü öyle bir fonksiyon bulamayacaktir. main fonksiyonunun başladığı adresi bulabilmek için objdump aracını kullanacağım. Terminale objdump -d -M intel soru3 yazarak programı komple disassemble ediyorum. Burada bakacağım ilk yer .text bölümü olacak.

stripped flag main address

main fonksiyonunun başladığı adresi hemen __libc_start_main@plt fonksiyonunun çağırıldığı satırın bir üstündeki satırda görebilirsiniz (“0x80483f4”). Daha sonra yine objdump çıktısından bu adresi bulalım ve main’e ulaşalım. main’i biraz incelemeye değer bulduğum için disassemble edilmiş kodların tamamını koymayı tercih ediyorum:

stripped flag main code

  • 1 olarak işaretlenen bölümde malloc fonksiyonu ile heap hafızadan 16’lık bir boş alan ayrılıyor.

Daha sonra oklardan da anlaşılacağı üzere for döngüsüne giriliyor. Bu for döngüsü de tam 16 kez dönüyor. Döngü her döndüğünde sırasıyla 0x8049640 adresinden bir değer alıp bu değerden 3 çıkararak tekrar sırası ile heap hafızadan ayırdığımız yere kopyalıyor. Peki bu (0x8049640) adreste ne var diye bakarsak grb|rxboryhblw yazısını görüyoruz. Bu karakterlerden teker teker 3 çıkarırsak, yani alfabede 3 geri kaydırırsak do you love it yazısının çıktığını görürüz 🙂 tabii bunun daha kısa yolu ise heap hafızaya bakmak olacaktır. Onu da şöyle yapıyoruz:

flag heap memory

Burada programı çalıştırdığımızda çıkmasın diye programın sonuna bir break point koyuyoruz, ardından programı çalıştırıyoruz. Program break point’te durduğunda x/3xw $sp komutu ile 3 tane 32 bit hex word şeklinde stack pointer’dan adres göster diyoruz. İlk adres “Do you know how to look heap !” cümlesinin başlangıç adresi, ikinci ise bizim aradığımız heap alanı. x/s 0x0804a008 komutu ile de ekrana bastırıyor ve “do_you_love_it\n” flag’ini buluyoruz.

Eveeet 3. sorunun da anlatımı bitti. soru_sonun çözümünü başka bir yazıya saklıyorum, bu yazı beklediğimden çok uzun oldu. 4. sorunun cevabı zaten başlı başına upuzun 🙂 Umarım sıkılmadan okumuşsunuzdur. Aklınıza takılan yerleri sormayı unutmayın veya bir yanlışım varsa beni düzeltirseniz sevinirim 🙂

Paylaşılanlar