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:
Kod:
public static boolean cf(MainActivity mainActivity, String str) {
boolean z = false;
try {
cfa(mainActivity, p1EncFn);
cfa(mainActivity, p5EncFn);
cfa(mainActivity, randEncFn);
cfa(mainActivity, rand2EncFn);
if (str.startsWith("OOO{") && str.endsWith("}") && str.length() == 45) {
if (cf(mainActivity, dp1(mainActivity, new File(mainActivity.getFilesDir(), p1EncFn), g0(str.substring(4, 44))), str)) {
File file = new File(mainActivity.getFilesDir(), "bam.html");
WebView webView = mainActivity.mWebView;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("file:///");
stringBuilder.append(file.getAbsolutePath());
stringBuilder.append("?flag=");
stringBuilder.append(Uri.encode(str));
webView.loadUrl(stringBuilder.toString());
z = mValid;
}
}
} catch (Exception e) {
}
return z;
}
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 :
Kod:
public static byte[] g0(String str) {
int i;
byte[] bArr = new byte[4];
byte[] bytes = str.getBytes();
for (i = 0; i < 4; i++) {
bArr[i] = (byte) null;
}
for (int i2 = 0; i2 < 10; i2++) {
for (i = 0; i < 4; i++) {
bArr[i] = (byte) ((byte) (bArr[i] ^ bytes[(i2 * 4) + i]));
}
}
return bArr;
}
Kod:
byte[0] = flag[0]^flag[8]^flag[16]^flag[24]^flag[32]
byte[1] = flag[1]^flag[9]^flag[17]^flag[25]^flag[33]
byte[2] = flag[2]^flag[10]^flag[18]^flag[26]^flag[34]
byte[3] = flag[3]^flag[11]^flag[19]^flag[27]^flag[35]
dp1(mainActivity, new File(mainActivity.getFilesDir(), p1EncFn), g0(str.substring(4, 44)))
Kod:
private static File dp1(Context context, File file, byte[] bArr) throws Exception {
byte[] hash = hash(bArr);
byte[] readAllBytes = Files.readAllBytes(file.toPath());
try {
AlgorithmParameterSpec ivParameterSpec = new IvParameterSpec(initVector);
Key secretKeySpec = new SecretKeySpec(hash, "AES");
Cipher instance = Cipher.getInstance("AES/CBC/PKCS5PADDING");
instance.init(2, secretKeySpec, ivParameterSpec);
readAllBytes = instance.doFinal(readAllBytes);
File file2 = new File(context.getFilesDir(), p1Fn);
OutputStream fileOutputStream = new FileOutputStream(file2);
fileOutputStream.write(readAllBytes, 0, readAllBytes.length);
fileOutputStream.flush();
fileOutputStream.close();
return file2;
} catch (Exception e) {
return null;
}
}
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 ?
Kod:
private static boolean cf(Context context, File file, String str) {
File file2 = new File(context.getFilesDir().getAbsolutePath());
try {
Class loadClass = new DexClassLoader(file.getAbsolutePath(), file2.getAbsolutePath(), file2.getAbsolutePath(), ClassLoader.getSystemClassLoader()).loadClass("ooo.p1.P1");
return ((Boolean) loadClass.getDeclaredMethod("cf", new Class[]{Context.class, String.class}).invoke(loadClass, new Object[]{context, str})).booleanValue();
} catch (Exception e) {
return false;
}
}
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.
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:
Kod:
from Crypto.Cipher import AES
import hashlib
IV = bytes([19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55])
N_BIT = 4
S_BIT = 2**7 + 1
DATA = open('ckxalskuaewlkszdva', 'rb').read(32)
for key_1 in range(S_BIT):
for key_2 in range(S_BIT):
print(key_1, key_2, end='\r')
for key_3 in range(S_BIT):
for key_4 in range(S_BIT):
key = hashlib.md5(
bytes([key_1, key_2, key_3, key_4])).digest()
aes = AES.new(key, AES.MODE_CBC, IV)
decrypted_data = aes.decrypt(DATA)
if decrypted_data.startswith(b'\x50\x4b\x03\x04'):
print(key_1, key_2, key_3, key_4)
IV = bytes([19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55, 19, 55])
f= open("ckxalskuaewlkszdva","rb")
DATA = f.read()
f.close()
key = hashlib.md5(
bytes([key_1, key_2, key_3, key_4])).digest()
aes = AES.new(key, AES.MODE_CBC, IV)
decrypted_data = aes.decrypt(DATA)
f = open("out.zip","wb")
f.write(decrypted_data)
f.close()
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.
Kod:
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]));
}
}
Kod:
byte[0] = flag[4]^flag[12]^flag[20]^flag[28]^flag[36]
byte[1] = flag[5]^flag[13]^flag[21]^flag[29]^flag[37]
byte[2] = flag[6]^flag[14]^flag[22]^flag[30]^flag[38]
byte[3] = flag[7]^flag[15]^flag[23]^flag[31]^flag[39]
\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.
Kod:
private native String xxx(String str, String str2);

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:
Kod:
K2_0 = 0;
flaga = flag + 4;
for ( i = 3; i < 10; i += 3 )
K2_0 ^= *&flaga[4 * (i - 1)];
return K2_0;
Kod:
byte[0] = flag[8]^flag[20]^flag[32]
byte[1] = flag[9]^flag[21]^flag[33]
byte[2] = flag[10]^flag[22]^flag[34]
byte[3] = flag[11]^flag[23]^flag[35]
Kod:
for ( i = 0; i < 100; ++i )
{
*&p3enc[4 * i] ^= K2_0;
K2_0 += 0x31333337;
}

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
Kod:
from pwn import *
f = open("xtszswemcwohpluqmi","rb")
data = f.read()
f.close()
#K2_0 = hex(0x90909090^0xfef3b7de)
#endianess
K2_0 = 0x4e27636e
f = open("shell_out.dat","wb")
for i in range(100):
f.write(xor(data[4*i:4*i+4],p32(K2_0)))
K2_0 += 0x31333337
K2_0 = K2_0 & 0xffffffff
f.close()
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. Shellkod su sekildeFonksiyona girmeden önce stackteki değerlerimize bakalım:
Kod:
.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
Kod:
...
...
90 nop
E800000000 call 0x25 // 0x25i stacke pushla
5B pop ebx // ebx = 0x25
83EB05 sub ebx,byte +0x5 // ebx = 0x20
83EB20 sub ebx,byte +0x20 // ebx = 0x0 + (Shellkodun yüklendiği base)
8B7C2404 mov edi,[esp+0x4] //flag
31C9 xor ecx,ecx
83C710 add edi,byte +0x10 //4byte kaydır = flag[0]
BA02000000 mov edx,0x2
330F xor ecx,[edi] //flag[i:i+4] ^ ecx
83C710 add edi,byte +0x10 //4byte kaydır
83EA01 sub edx,byte +0x1 //edxi azalt
75F6 jnz 0x3a //loop 2 yapıcak yani flag[0:4] ^ flag[16:20]
83EF20 sub edi,byte +0x20 //flagın basına geri dön
89D8 mov eax,ebx //eax = 0
05C8000000 add eax,0xc8 //eax = 200;
BA32000000 mov edx,0x32 //edx = 50;
3108 xor [eax],ecx //ecxte key vardı; 200 ofsetindeki değerler anlamsız
83C004 add eax,byte +0x4 //4byte ilerle
83EA01 sub edx,byte +0x1 //edx-1 ; 50 kez xorlanıcak; 50*4 =200; 200+200=400 tam dosya boyu
75F6 jnz 0x53 //loop; yine bir decryption
89D8 mov eax,ebx //eax = 0
052C010000 add eax,0x12c //eax = 300
BA19000000 mov edx,0x19 //edx = 25
0118 add [eax],ebx //[eax]taki değerlere basei ekle
83C004 add eax,byte +0x4 //4byte ilerle
83EA01 sub edx,byte +0x1 //dec
75F6 jnz 0x69 //loop
89D8 mov eax,ebx //eax = base
052C010000 add eax,0x12c //eax = 300
89E2 mov edx,esp //save esp
8B7C2404 mov edi,[esp+0x4] //edi = flag
8B742408 mov esi,[esp+0x8] //esi = data
8B5C240C mov ebx,[esp+0xc] //ebx = size
89C4 mov esp,eax //esp = 300
C3 ret
- 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
200-400 arasını XORlayacak keyi bulmak için bu aralığa bir bakalım.
Kod:
000000c0: 9090 9090 9090 9090 fa18 3313 42db b0d3 ..........3.B...
000000d0: 46db f09a b2db b815 8129 3dd0 c1de 37d0 F........)=...7.
000000e0: c1f3 37d0 3c19 f04b 819b df0b 81a0 3a13 ..7.<..K......:.
000000f0: 4218 f0ab 750b 3013 8191 ebd0 cbd3 f09a B...u.0.........
00000100: 8adb f2d2 4adb 02da 812b 7c07 812b 7c3b ....J....+|..+|;
00000110: 8188 a383 d288 a383 d288 a383 d288 a383 ................
00000120: d288 a383 d288 a383 d288 a383 4419 3313 ............D.3.
00000130: 4b19 3313 4f19 3313 9b18 3313 9e18 3313 K.3.O.3...3...3.
00000140: 4019 3313 a218 3313 a618 3313 ab18 3313 @.3...3...3...3.
00000150: b118 3313 c918 3313 4218 3313 4218 3313 ..3...3.B.3.B.3.
00000160: 4218 3313 4218 3313 4218 3313 4218 3313 B.3.B.3.B.3.B.3.
00000170: 4218 3313 4218 3313 4218 3313 4218 3313 B.3.B.3.B.3.B.3.
00000180: 4218 3313 4218 3313 4218 3313 4218 3313 B.3.B.3.B.3.B.3.
Kod:
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
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.
Kod:
0000002B B837130300 mov eax,0x31337
00000030 C3 ret
Bu aşamadaki key şu şekilde hesaplanıyor :
Kod:
byte[0] = flag[16]^flag[32]
byte[1] = flag[17]^flag[33]
byte[2] = flag[18]^flag[34]
byte[3] = flag[19]^flag[35]
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ı oluşuyor. Html dosyasini rop chain oluşturuyor. Cağırılan gadgetlara bakalim:
Kod:
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
Key5: = hex(0x3c68746d^0x6a452630):
\x56\x2d\x52\x5d
Kod:
byte[0] = flag[20]^flag[40]
byte[1] = flag[21]^flag[41]
byte[2] = flag[22]^flag[42]
byte[3] = flag[23]^flag[43]
Kod:
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 in range(int(len(data)/4)):
f.write(xor(data[4*i:4*i+4],p32(a[i%4])))
f.close()
Kod:
File file = new File(mainActivity.getFilesDir(), "bam.html");
WebView webView = mainActivity.mWebView;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("file:///");
stringBuilder.append(file.getAbsolutePath());
stringBuilder.append("?flag=");
stringBuilder.append(Uri.encode(str));
webView.loadUrl(stringBuilder.toString());
z = mValid;
=> pHd_1w_e4rL13r;)}
'a eşit olması gerektiğini anlıyoruz.Elimizde 5 key ve flagin 24-44 arası karakterleri var.
Kod:
key0_0 = "4 ^ 12 ^ 20 ^ 28 ^ 36"
key0_1 = "5 ^ 13 ^ 21 ^ 29 ^ 37"
key0_2 = "6 ^ 14 ^ 22 ^ 30 ^ 38"
key0_3 = "7 ^ 15 ^ 23 ^ 31 ^ 39"
key1_0 = "8 ^ 16 ^ 24 ^ 32 ^ 40"
key1_1 = "9 ^ 17 ^ 25 ^ 33 ^ 41"
key1_0 = "10 ^ 18 ^ 26 ^ 34 ^ 42"
key1_1 = "11 ^ 19 ^ 27 ^ 35 ^ 43"
key2_0 = "12 ^ 24 ^ 36"
key2_1 = "13 ^ 25 ^ 37"
key2_2 = "14 ^ 26 ^ 38"
key2_3 = "15 ^ 27 ^ 39"
key3_0 = "16 ^ 32"
key3_1 = "17 ^ 33"
key3_2 = "18 ^ 34"
key3_3 = "19 ^ 35"
key4_0 = "20 ^ 40"
key4_1 = "21 ^ 41"
key4_2 = "22 ^ 42"
key4_3 = "23 ^ 43"
Kod:
from pwn import *
key0 = b'\x17\x01\x2f\x03'
key1 = b'\x3c\x27\x43\x60'
key2 = b'\x6e\x63\x27\x4e'
key3 = b'\x42\x18\x33\x13'
key4 = b'\x56\x2d\x52\x5d'
f6, f7, f8, f9, f10 = group(4, ' => pHd_1w_e4rL13r;)')
f5 = xor(key4, f10)
f4 = xor(key3, f8)
f3 = xor(xor(key2, f6), f9)
f2 = xor(xor(xor(xor(key1, f4), f6), f8), f10)
f1 = xor(xor(xor(xor(xor(key0, key1), f3), f5), f7), f9)
theflag = 'OOO{' + f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + '}'
print theflag
OOO{pox&mpuzz,U_solve_it => pHd_1w_e4rL13r;)}