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:
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 ?
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:
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.
for (int i2 = 0; i2 < 10; i2 += 2) { for (i = 0; i < 4; i++) { bArr[i] = (byte) ((byte) (bArr[i] ^ bytes[((i2 + 1) * 4) + i])); } }
Aynı scripti kullanarak zip headeri yerine .so dosyanın yani ELF headerini \x73\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.
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:
K2_0 = 0; flaga = flag + 4; for ( i = 3; i < 10; i += 3 ) K2_0 ^= *&flaga[4 * (i - 1)]; return K2_0;
for ( i = 0; i < 100; ++i ) { *&p3enc[4 * i] ^= K2_0; K2_0 += 0x31333337; }
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
from pwn import *
f = open("xtszswemcwohpluqmi","rb") data = f.read() f.close()
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.
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.
00000000 B800000000 mov eax,0x0 00000005 C3 ret 00000006 83C004 add eax,byte +0x4 00000009 C3 ret 0000000A C3 ret 0000000B 89F0 mov eax,esi 0000000D C3 ret 0000000E 8B06 mov eax,[esi] 00000010 C3 ret 00000011 310E xor [esi],ecx 00000013 C3 ret 00000014 83C604 add esi,byte +0x4 00000017 C3 ret 00000018 83EB04 sub ebx,byte +0x4 0000001B C3 ret 0000001C 7E01 jng 0x1f 0000001E C3 ret 0000001F 58 pop eax 00000020 C3 ret 00000021 83EC18 sub esp,byte +0x18 00000024 C3 ret 00000025 B809000000 mov eax,0x9 0000002A C3 ret 0000002B B837130300 mov eax,0x31337 00000030 C3 ret 00000031 89D8 mov eax,ebx 00000033 C3 ret 00000034 89CB mov ebx,ecx 00000036 C3 ret 00000037 89C8 mov eax,ecx 00000039 C3 ret 0000003A C1C108 rol ecx,byte 0x8 0000003D C3 ret 0000003E 31C9 xor ecx,ecx 00000040 C3 ret 00000041 334F14 xor ecx,[edi+0x14] 00000044 C3 ret 00000045 334F28 xor ecx,[edi+0x28] 00000048 C3 ret ... 00000063 90 nop 00000064 06 push es 00000065 0100 add [eax],eax 00000067 0009 add [ecx],cl 00000069 0100 add [eax],eax 0000006B 000D010000D9 add [dword 0xd9000001],cl 00000071 0000 add [eax],al 00000073 00DC add ah,bl 00000075 0000 add [eax],al 00000077 0002 add [edx],al 00000079 0100 add [eax],eax 0000007B 00E0 add al,ah 0000007D 0000 add [eax],al 0000007F 00E4 add ah,ah 00000081 0000 add [eax],al 00000083 00E9 add cl,ch 00000085 0000 add [eax],al 00000087 00F3 add bl,dh
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.
0000002B B837130300 mov eax,0x31337 00000030 C3 ret
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:
xor_ecx_ecx xor_ecx_[edi+20] //flag[20:24] xor_ecx_[edi+40] //flag[40:44] ^ flag[20:24] xor_[esi]_ecx //eside encrypted data var add_esi_4 //esi_4 rol_ecx_8 //rotate left ecx sub_ebx_4 //size - 4 loop mov_eax_31337
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
f = 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 inrange(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 :
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.