DEF CON Quals 2019 : Vitor
Vitor
Defcon 2019 qualifiersda sorulmuş android reverse kategorisine ait bir ctf sorusu. Matruşka misali birkaç bölümden oluşuyor. Soruda bize bir apk veriliyor. Uygulama bizden input olarak flagi istiyor ve doğruluğunu kontrol ediyor.
Aşama 1 : Dex
Verilen apkyı jadx
gibi araçlarla decompile edip java kodunu inceleyebiliyoruz. Classlara göz attığımızda fc
class’ını görüyoruz. Genel olarak her aşamada yapılacak olanlar birbirine benzer. Sadece ilk aşama için detaylı bir açıklama yapacağım. Flagi kontrol eden fonksiyonumuz şu şekilde:
1 | public static boolean cf(MainActivity mainActivity, String str) { |
İlk kontrolden anlıyoruzki Flag OOO{
ile başlayıp }
ile bitiyor. Uzunluğu 45. Ardından sırasıyla incelersek:
g0(str.substring(4, 44)))
: g0 fonksiyonuna flagimizin süslü parantezler içerisinde kalan input yollanıyor. g0 fonksiyonu :
1 | public static byte[] g0(String str) { |
Fonksiyon 4 bytelık bir array döndürüyor. Bu 4 byte’ı da şu şekilde hesaplıyor:
1 | byte[0] = flag[0]^flag[8]^flag[16]^flag[24]^flag[32] |
g0 fonksiyonundan dönen 4byte, dp1 fonksiyonuna 3. parametre olarak gidiyor.
dp1(mainActivity, new File(mainActivity.getFilesDir(), p1EncFn), g0(str.substring(4, 44)))
1 | private static File dp1(Context context, File file, byte[] bArr) throws Exception { |
Bu fonksiyonda önce 4byte’ın md5ini alıyor. Bu digest değerini aes keyi olarak kullanıyor ve input olarak verilen dosyayı decrypt ediyor. dp1’i çağırıken class’ın başında tanımlanmışpublic static String p1EncFn = "ckxalskuaewlkszdva";
değeri kullanılıyor. Apk’ımızın içerisindeki asset klasorune bakarsak bu dosyayı görebiliyoruz. Peki bu dosyayı decrypt edip ne yapıyor ?
1 | private static boolean cf(Context context, File file, String str) { |
Decrypt edilen dosya DexClassLoader APIsi ile contexte yükleniyor. Bu olayı android malwarelerinde çok görüyoruz. Tersine mühendisliği zorlaştırmak için “packing” yöntemini sıklıkla kullanıyorlar.
ooo.p1.P1 class’ı invoke edilip o class içerisinden bir fonksiyon çağırılıyor. Bu fonksiyon boolean bir değer döndürüyor ( O fonksiyonda baska fonksiyonlar cagiriyor tabi ). Ve flagin doğruluğu kontrol edilmiş oluyor .
Aşama 1’i toparlasak:
- Flagin belli indexlerini XORla ve 4byte’ı al
- md5
- ckxalskuaewlkszdva dosyasını bu keyle decrypt et
- decrypt edilen dosya içerisinden ooo.p1.P1 classını load et.
Peki biz bu 4byte’ı nasıl bulabiliriz ?
Biliyoruzki decrypt edilen dosyanın bir jar dosyası olması lazım. Neden ? Çünkü kullanılan DexClassLoader fonksiyonu dosya formatı olarak jar kabul ediyor. Jar dosyaları da bir nevi zip ve zip dosyalarının Magic Byte’ını yani dosya tipini anladığımız header kısmını biliyoruz. \x50\x4b\x03\x04
dosya headerı olarak bu byteları arayacağız.
Şimdi brute için şu şekilde bir python scripti yazalım:
1 | from Crypto.Cipher import AES |
Girdiğimiz stringler sonuçta ASCII yani 7bitlik aralıkta olduğu için XOR sonucu elde edilecek byte aralığıda bu aralığa sahip olmak zorunda.
Burda dikkatinizi DATA = open('ckxalskuaewlkszdva', 'rb').read(32)
satırına vermenizi rica ediyorum. Neden 32 byte okuyorum burada ?
AES CBC encryption şemasında sizin dosyanız ne kadar büyüse de ilk bloğun son halini ilgilendiren bir olay gerçekleşmiyor. Yani biz sadece ilk blok için deneme yaparak keyi bulabiliriz. Bu çok büyük ölçüde işimizi kolaylaştırıyor. 1.6 MB dosyanın boyutunu 32byte’a düşürmüş oluyoruz.
Bu scripti çalıştırdığımızda birkaç dakika içerisinde ilk keyimizi alıyoruz.
Key 1 : \x17\x01\x2f\x03
Her aşamamızda flagin belirli bytelarına dair bilgi ede ede ilerliyoruz.
Aşama 2 : SO
İlk aşamaya benzer bir dex dosyası ile karşılaşıyoruz.
Bu sefer jar dosyası yerine bir adet .so
dosyası oluşturulması gerekiyor ve XORlanan indexler değişik.
1 | for (int i2 = 0; i2 < 10; i2 += 2) { |
1 | byte[0] = flag[4]^flag[12]^flag[20]^flag[28]^flag[36] |
Aynı scripti kullanarak zip headeri yerine .so dosyanın yani ELF headerini \x7f\x45\x4c\x46
yazıyoruz, input dosyasını mmdffuoscjdamcnssn
olarak değiştirip ikinici keyimizi de alıyoruz.
Key2 : \x3c\x27\x43\x60
Aşama 3 Shellkod:
Bu sefer işler değişiyor. Biliyoruzki so dosyasından xxx fonksiyonu çağırılıyor.
1 | private native String xxx(String str, String str2); |
So dosyanı IDA ile açıp incelediğimizde bu fonksiyonu görebiliyoruz.
Daha kolay anlaşılması için değişken ve fonksiyonlari isimlendirdim.
Önce asset klasorunden xtszswemcwohpluqmi
dosyası okunuyor ve key hesaplandıktan sonra bu dosya decrypt ediliyor. Bu sefer biraz olaylar farklı. Keyi hesaplamak için fonksiyon şu şekilde:
1 | K2_0 = 0; |
yani :
1 | byte[0] = flag[8]^flag[20]^flag[32] |
Key hesaplandiktan sonra:
1 | for ( i = 0; i < 100; ++i ) |
fonksiyonuna veriliyor ve dosya bu şekilde decrypt ediliyor. Peki bu sefer neye göre decrypt edicez ? Decrypt edildikten sonra bir assembly kodu elde ediceğimiz belli. Çünkü fonksiyonun devamında bu alani fonksiyon şeklinde çağrılıyor.
Bu cepte. Fonksiyon çağırılmadan önceki android log’unun bastığı değere baktığımızda
Jumping to nopsled in 3, 2, 1, ...
stringini görüyoruz. Nop sled ne demekti shellkod demekti. NOP instructionunı shellkod yazan herkes illaki kullanmıştır. Instruction’ın byte karşılığı 0x90
. Sled olunca da bundan bir sürü demek. Decrypt ederken 4byte 4byte gidildiği için encrypted dosyanın ilk 4 byteını 0x90909090
ile XORlarsak keyimizi elde ederiz.
hex(0x90909090^0xfef3b7de)
: \x6e\x63\x27\x4e
keyini veriyor. Bu key ile dosyayi decrypt etmek istersek :
Key3: \x6e\x63\x27\x4e
1 | from pwn import * |
Oluşan dosyayı ndisasm -b 32 shell_out.dat
şeklinde kontrol edebilirsiniz.
Aşama 4 : ROP
En zor kısım burası. Shellkodun ne yaptığını anlamamız gerek.
Fonksiyona girmeden önce stackteki değerlerimize bakalım:1
2
3
4
5
6
7.text:00008C8F mov edx, [ebp+flag?]
.text:00008C92 mov ebx, [ebp+cx_data]
.text:00008C98 mov esi, [ebp+size]
.text:00008C9E mov [esp], edx ; s
.text:00008CA1 mov [esp+4], ebx ; src
.text:00008CA5 mov [esp+8], esi ; n
.text:00008CB5 call ecx
Shellkod çağırılırken ret adresi de pushlanacağı için stack değerlerimiz 4 artıyor.
1 | ... |
Simdi kodumuz ne yapıyor kabaca bahsedersek:
- Key hesapla
- 200-400 arasını bu key ile xorla
- 300den sonrası için shellkodun yüklendiği base’i ekle
- esp’ye 300un offsetini ata
- ret
- ret’ten sonra kod executionu 300den devam edicek
Şimdi bizim if’i geçebilmemiz için bu fonksiyondan dönen değerin 0x31337 olması lazım. Şuan görünürde bu değer yok. 300’e gittikten sonra bu işi yapması lazım.
200-400 arasını XORlayacak keyi bulmak için bu aralığa bir bakalım.
1 | 000000c0: 9090 9090 9090 9090 fa18 3313 42db b0d3 ..........3.B... |
Bloğun sonuna bakarsanız hep tekrar eden değerler görüyoruz. Bloğun başı işe anlamsız datalardan oluşuyor. Burda bloğun sonunun 0x0 lerle bittiğini düşünerek XOR keyi olarak 0x42183313 kullandım.
1 | 00000000 B800000000 mov eax,0x0 |
Bloğumuzun başı rop gadgetleri. Altı ise bu gadgetların adresleri. Disasm bozuk göstersede şu şekilde :
0x0106 0x0109 0x010d 0x00d9 .. 0x00f3
Bu değerler espye atanmıştı. ret yapıldıkça bu değerler poplana poplana gidicek. Yani bu bizim çalışacak kodumuz. Şimdi fonksiyonun 0x31337 dönmesini istiyorduk. Bakalım rop chain döndürüyormu. Son offset 0xf3
200’u 0 kabul edip hesaplarsak : hex(0xf3-200)
: 0x2b . Şimdi yukarıdan 0x2bye bakalım.
1 | 0000002B B837130300 mov eax,0x31337 |
evet istediğimiz değer dönüyor. Rop için key:
Bu aşamadaki key şu şekilde hesaplanıyor :
1 | byte[0] = flag[16]^flag[32] |
Bulduğumuz xor keyi :
Key4 : \x42\x18\x33\x13
Aşama 5 : JS
Tabi rop chain sadece 0x31337 döndürmüyor ondan öncesinde fonksiyona verilen başka bir encrypted datayı decrypt ediyor.
Bir html dosyası. Html dosyasini rop chain oluşturuyor. Cağırılan gadgetlara bakalim:
1 | xor_ecx_ecx |
Rotate left yapmadan önce ilk aşamada direk gelen xorkeyi kullaniliyor. Bu yüzden yine xor keyini hesaplayabiliriz. Headerin html olduğunu biliyoruz. Encrypted dosyanın ilk 4byte’ı ile xorlarsak
Key5: = hex(0x3c68746d^0x6a452630): \x56\x2d\x52\x5d
1 | byte[0] = flag[20]^flag[40] |
Decryption routinini implemente edicek python scripti yazalim:1
2
3
4
5
6
7
8
9
10
11
12
13f = open("assets/cxnvhaekljlkjxxqkq","rb")
data = f.read()
f.close()
#rol8 4 iterasyondan sonra kendisine geri dönücek
a = [0x562d525d,0x5d562d52,0x525d562d,0x2d525d56]
f = open("bam.html","wb")
for i in range(int(len(data)/4)):
f.write(xor(data[4*i:4*i+4],p32(a[i%4])))
f.close()
Bam.htmli ilk açtığımız apk dosyasından hatırlarsak :
1 | File file = new File(mainActivity.getFilesDir(), "bam.html"); |
Son aşama olarak flag stringi bu html dosyasına parametre olarak gidiyor. Html dosyasını açtığımızda obfuscated bir javascriptle karşılaşıyoruz. theempire_ saolsun bu adımı tak diye çözdü. Flagin 24. karakterinden sornasinın => pHd_1w_e4rL13r;)}
‘a eşit olması gerektiğini anlıyoruz.
Elimizde 5 key ve flagin 24-44 arası karakterleri var.
1 | key0_0 = "4 ^ 12 ^ 20 ^ 28 ^ 36" |
Biraz kopya cekerek script yazarsak.
1 | from pwn import * |
flag : OOO{pox&mpuzz,U_solve_it => pHd_1w_e4rL13r;)}