Panduan tentang apk klien-server terbalik pada contoh pekerjaan NeoQUEST-2020


Hari ini kami memiliki program yang kaya (akan ada begitu banyak area keamanan siber pada satu waktu!): Pertimbangkan untuk mendekompilasi aplikasi Android, memotong lalu lintas untuk mendapatkan URL, membangun kembali apk tanpa kode sumber, bekerja dengan cryptanalysts dan banyak lagi :)

Menurut legenda NeoQUEST-2020 , sang pahlawan menemukan bagian-bagian robot tua yang harus digunakan untuk mendapatkan kunci. Mari kita memulainya!

1. Reversim apk


Jadi, sebelum kita sedikit yang kita berhasil mengekstrak dari robot semi-disassembled - aplikasi apk yang entah bagaimana seharusnya membantu kita mendapatkan kuncinya. Mari kita lakukan yang paling jelas: jalankan apk dan lihat fungsinya. Antarmuka aplikasi yang lebih minimalis tidak meninggalkan keraguan - ini adalah klien file FileDroid khusus yang memungkinkan Anda untuk mengunduh file dari server jauh. Oke, itu terlihat mudah. Kami menghubungkan ponsel ke Internet, mencoba melakukan percobaan untuk mengunduh (segera key.txt - yah, bagaimana jika?) - tidak berhasil, file hilang di server.



Kami melanjutkan ke acara berikutnya dalam hal kompleksitas - kami mendekompilasi apk menggunakan JADXdan menganalisis kode sumber aplikasi, yang, untungnya, sama sekali tidak dikaburkan. Tugas kami saat ini adalah untuk memahami file apa yang ditawarkan server jauh untuk diunduh, dan pilih satu dengan kunci dari mereka.

Kita mulai dengan kelas com.ctf.filedroid.MainActivity, yang berisi metode onClick () yang paling menarik bagi kami, di mana klik pada tombol "Unduh" diproses. Di dalam metode ini, kelas ConnectionHandler dipanggil dua kali: pertama, metode ConnectionHandler.getToken () dipanggil, dan hanya kemudian, ConnectionHandler.getEncryptedFile (), yang meneruskan nama file yang diminta oleh pengguna, dipanggil.



Ya, itu, pertama kita perlu token! Kami akan memeriksa sedikit lebih banyak dengan proses mendapatkannya.
Metode ConnectionHandler.getToken () mengambil dua baris input, dan kemudian mengirimkan permintaan GET, melewati baris ini sebagai parameter "crc" dan "sign". Sebagai tanggapan, server mengirim data dalam format JSON, dari mana aplikasi kami mengekstrak token akses dan menggunakannya untuk mengunduh file. Ini semua, tentu saja, bagus, tapi apa itu "crc" dan "sign"?



Untuk memahami hal ini, kami bergerak lebih jauh menuju kelas Checks, silakan menyediakan metode badHash () dan badSign (). Yang pertama menghitung checksum dari classes.dex dan resources.arsc, menggabungkan kedua nilai ini dan membungkusnya di Base64 (perhatikan flag 10 = NO_WRAP | URL_SAFE, ini akan berguna). Dan bagaimana dengan metode kedua? Dan dia melakukan hal yang sama dengan sidik jari SHA-256 pada tanda tangan aplikasi. Eh, sepertinya FileDroid tidak benar-benar ingin dibangun kembali :(



Oke, misalkan kita menerima token. Apa berikutnya? Kami meneruskannya ke input metode ConnectionHandler.getEncryptedFile (), yang menambahkan nama file yang diminta ke token dan menghasilkan permintaan GET lain, kali ini dengan parameter "token" dan "file". Server sebagai respons (dilihat dari nama metode) mengirimkan file terenkripsi, yang disimpan di / sdcard /.

Jadi, untuk meringkas sedikit subtotal: kami memiliki dua berita, dan ... keduanya buruk. Pertama, FileDroid tidak benar-benar mendukung semangat kami untuk memodifikasi apk (checksum dan tanda tangan diperiksa), dan kedua, file yang diterima dari server berjanji untuk dienkripsi.

Oke, kita akan menyelesaikan masalah saat tersedia, dan sekarang masalah utama kita adalah kita masih belum tahu file mana yang perlu kita unduh. Namun, dalam proses mempelajari kelas ConnectionHandler, kami tidak bisa tidak melihat bahwa tepat antara metode getToken () dan getEncryptedFile (), pengembang FileDroid lupa metode lain yang sangat menggoda yang disebut dengan berbicara getListing (). Jadi, server mendukung fungsi seperti itu ... Sepertinya ini yang Anda butuhkan!



Untuk mendapatkan listing, kita akan membutuhkan "crc" dan "sign" yang terkenal - bukan masalah, kita sudah tahu dari mana mereka berasal. Kami membaca nilainya, mengirim permintaan GET dan ... Jadi, berhenti. Di mana kita akan mengirim permintaan GET? Akan menyenangkan untuk mendapatkan URL server jarak jauh terlebih dahulu. Eh, kita kembali ke MainActivity.onClick () dan melihat bagaimana argumen netPath dihasilkan untuk memanggil metode getToken () dan getEncryptedFile ():

Method getSecureMethod = 
wat.class.getDeclaredMethod("getSecure", new Class[]{String.class});

// . . .

// netPath --> ConnectionHandler.getToken()
(String) getSecureMethod.invoke((Object) null, new Object[]{"fnks"})

// netPath --> ConnectionHandler. getEncryptedFile()
(String) getSecureMethod.invoke((Object) null, new Object[]{"qdkm"})

Kombinasi huruf aneh "fnks" dan "qdmk" memaksa kita untuk beralih ke hasil dekompilasi metode wat.getSecure (). Spoiler: JADX memiliki hasil ini begitu-begitu.



Setelah diperiksa lebih dekat, menjadi jelas bahwa semua konten yang tidak terlalu menyenangkan dari metode ini dapat diganti dengan case-switch seperti ini:

// . . .
switch(CODE)
{
    case «qdkm»: 
        r.2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(2);
        break;
    case «tkog»: 
        r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(1);
        break;
    case «fnks»: 
        String r2 = com.ctf.filedroid.x37AtsW8g.rlieh786d(0);
	break;
}
java.lang.StringBuilder r1 = new java.lang.StringBuilder 
r1.<init>(r2) 
java.lang.String r0 = r1.toString() 
java.lang.String r1 = radon(r0)
return r1 

Karena "fnks" dan "qdmk" sudah digunakan untuk mendapatkan token dan mengunduh file, "tkog" harus memberikan URL yang diperlukan untuk meminta daftar file yang tersedia di server. Tampaknya ada harapan untuk mendapatkan jalur yang diperlukan dengan murah ... Pertama-tama, mari kita lihat bagaimana URL disimpan dalam aplikasi. Kami membuka fungsi com.ctf.filedroid.x37AtsW8g.rlieh786d () dan melihat bahwa setiap URL disimpan sebagai array byte yang disandikan, dan fungsi itu sendiri membentuk string dari byte ini dan mengembalikannya.



Baik. Tapi kemudian garis dilewatkan ke fungsi com.ctf.filedroid.wat.radon (), implementasi yang diserahkan ke perpustakaan asli libae3d8oe1.so. Membalikkan arm64? Usaha yang bagus, FileDroid, tetapi datang di lain waktu?

2. Dapatkan url server


Mari kita coba pendekatan dari sisi lain: untuk mencegat traffic, dapatkan URL dalam teks yang jelas (dan sebagai bonus, juga nilai checksum dan tanda tangan!), Sesuaikan mereka dengan byte array dari com.ctf.filedroid.x37AtsW8g.rlieh786d () - itu bisa Apakah enkripsi berubah menjadi cipher Caesar atau XOR yang biasa? .. Maka tidak akan sulit untuk mengembalikan URL ketiga dan melakukan listing.

Untuk mengarahkan lalu lintas, Anda dapat menggunakan proxy yang nyaman ( Charles , fiddler , BURPdll.) Kami mengonfigurasi penerusan pada perangkat seluler, memasang sertifikat yang sesuai, memverifikasi bahwa intersepsi berhasil, dan meluncurkan FileFroid. Kami mencoba mengunduh file sewenang-wenang dan ... lihat "NetworkError". Kesalahan ini disebabkan oleh adanya pin-sertifikat (lihat com.ctf.filedroid.ConnectionHandler.sendRequest metode): klien file memverifikasi bahwa sertifikat "kabel" dalam aplikasi sesuai dengan server yang berinteraksi. Sekarang sudah jelas mengapa integritas sumber daya aplikasi dikontrol!



Namun, dalam lalu lintas yang disadap, kita dapat melihat setidaknya nama domain dari server yang diakses oleh klien file, yang berarti harapan untuk mendekripsi URL tetap ada!



Mari kita kembali ke fungsi com.ctf.filedroid.x37AtsW8g.rlieh786d () dan perhatikan bahwa beberapa byte pertama bertepatan dalam semua array:

cArr[0] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'i', . . .};

cArr[1] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . .};

cArr[2] = new char[]{'K', 'S', 'Y', '5', 'E', 'R', 'Q', 'J', 'S', '0', 't', 'W', 'B', '2', 'w', 'k', 'N', 'j', '8', 'O', 'D', 'l', 'd', 'K', 'C', 'l', 'U', 'B', 'c', 'T', 'Q', '3', 'P', 'h', 'V', 'J', 'Q', 'R', 'F', 'L', 'U', 'R', '5', 'p', 'b', 'j', . . ., '='};

Selain itu, byte terakhir dari array ketiga mengisyaratkan bahwa itu bukan tanpa base64. Mari kita coba mendekode dan menyodok byte yang dihasilkan dengan bagian URL yang diketahui:



Tampaknya tidak ada yang pernah begitu senang dengan ARMag3dd0n! Masalahnya kecil: secara berturut-turut decode URL base64 dan xorim dengan kunci yang ditemukan. Tapi ... dan jika itu bukan XOR, tetapi cipher permutasi yang dibuat sendiri, yang tidak dapat Anda ambil bahkan dengan seratus upaya?

3. Rebuild apk dengan Frida


Sebagai bagian dari penulisan ini, kami akan mempertimbangkan metode solusi yang lebih menyakitkan (dan, menurut kami, lebih indah) - menggunakan kerangka kerja Frida , yang akan memungkinkan kami untuk mengeksekusi metode aplikasi apk sewenang-wenang dengan argumen yang kami butuhkan dalam run-time. Untuk melakukan ini, Anda memerlukan ponsel dengan hak root atau emulator. Kami menganggap rencana aksi berikut:

  1. Memasang komponen Frida pada PC dan telepon percobaan.
  2. Pulihkan URL yang cocok dengan permintaan token atau cantuman dan unduh file (menggunakan Frida).
  3. Mengambil nilai checksum dan tanda tangan dari aplikasi asli.
  4. Mendapatkan daftar file yang disimpan di server, dan mengidentifikasi file yang diinginkan.
  5. Unduh dan dekripsi file tersebut.

Pertama, kami akan mengklarifikasi hubungan antara ponsel yang di-root dan apk. Kami menginstal aplikasi, menjalankannya, tetapi klien file tidak ingin boot sepenuhnya, hanya berkedip dan ditutup. Kami memeriksa pesan melalui logcat - ya, memang, FileDroid sudah merasa ada sesuatu yang salah dan menolaknya.



Kami kembali beralih ke kelas MainActivity dan menemukan bahwa metode doChecks () dipanggil di onCreate (), dan itu menampilkan kesalahan berikut dalam log:



Selain itu, onResume () juga memeriksa untuk melihat apakah port khas untuk Frida terbuka:



File klien kami sedikit tidak toleran untuk debugging, root dan Frida sendiri. Oposisi seperti itu sama sekali tidak termasuk dalam rencana kami, jadi kami mendapatkan kode smali dari aplikasi menggunakan utilitas apktool, buka file MainActivity.smali di editor teks apa pun, temukan metode onCreate () dan ubah panggilan doChecks () menjadi komentar yang tidak berbahaya:



Kemudian kita menghilangkan metode bunuh diri () kesempatan untuk benar-benar mematikan aplikasi:



Selanjutnya, mari kita buat aplikasi yang sedikit lebih baik lagi menggunakan apktool dan tanda tangani dia dengan menjalankan perintah berikut (Anda mungkin perlu hak Administrator):

cd "C:\Program Files\Java\jdk-14\bin"
.\keytool -genkey -v -keystore filedroid.keystore -alias filedroid_alias -keyalg RSA -keysize 2048 -validity 10000
.\jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore  filedroid.keystore filedroid_patched.apk filedroid_alias
.\jarsigner -verify -verbose -certs filedroid_patched.apk

Kami instal ulang aplikasi di telepon, jalankan - hore, unduhan tanpa insiden, log bersih!



Kami melanjutkan untuk menginstal kerangka kerja Frida pada PC dan perangkat seluler:

$ sudo pip3 install frida-tools
$ wget https://github.com/frida/frida/releases/download/$(frida --version)/frida-server-$(frida --version)-android-arm.xz
$ unxz frida-server-$(frida --version)-android-arm.xz
$ adb push frida-server-$(frida --version)-android-arm /data/local/tmp/frida-server

Luncurkan server kerangka Frida di perangkat seluler:

$ adb shell su - "chmod 755 /data/local/tmp/frida-server"
$ adb shell su - "/data/local/tmp/frida-server &"  

Kami sedang mempersiapkan skrip get-urls.js sederhana yang akan memanggil wat.getSecure () untuk semua server permintaan yang didukung:

Java.perform(function () 
{
     const wat = Java.use('com.ctf.filedroid.wat');
	console.log(wat.getSecure("fnks"));
	console.log(wat.getSecure("qdmk"));
	console.log(wat.getSecure("tkog"));
});

Kami meluncurkan FileDroid di perangkat seluler dan "melekat" dengan skrip kami ke proses yang sesuai:



4. Dapatkan daftar file di server


Akhirnya, server jarak jauh menjadi sedikit lebih dekat dengan kami! Sekarang kita tahu bahwa server mendukung permintaan dengan cara berikut:

  1. diajukanroid.neoquest.ru/api/verifyme?crc= {crc} & sign = {sign}
  2. diajukanroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  3. diajukanroid.neoquest.ru/api/file?file= {file} & token = {token}

Untuk mendapatkan daftar file yang tersedia, masih harus menghitung nilai checksum dan tanda tangan dari aplikasi asli, dan kemudian menyandikannya di base64.

Skrip seperti itu di python3 akan memungkinkan Anda melakukan ini:

Spoiler
import hashlib
import binascii
import base64
from asn1crypto import cms, x509
from zipfile import ZipFile


def get_info(apk):
    with ZipFile(apk, 'r') as zipObj:
        classes = zipObj.read("classes.dex")
        resources = zipObj.read("resources.arsc")
        cert = zipObj.read("META-INF/CERT.RSA")
        crc = "%s%s" % (get_crc(classes), get_crc(resources))
        return get_full_crc(classes, resources).decode("utf-8"), get_sign(cert).decode("utf-8")


def get_crc(file):
    crc = binascii.crc32(file) & 0xffffffff
    return crc


def get_full_crc(classes, resources):
    crc = "%s%s" % (get_crc(classes), get_crc(resources))
    return base64.urlsafe_b64encode(bytes(crc, "utf-8"))


def get_sign(file):
    pkcs7 = cms.ContentInfo.load(file)
    data = pkcs7['content']['certificates'][0].chosen.dump()
    sha256 = hashlib.sha256()
    sha256.update(data)
    return base64.urlsafe_b64encode(sha256.digest())  

get_info('filedroid.apk')


Anda juga bisa secara manual. Kami menganggap CRC32 dari classes.dex dan resources.arsc sebagai alat yang praktis (misalnya, untuk Linux - utilitas standar CRC32), kami mendapatkan nilai masing-masing 1276945813 dan 2814166583, menggabungkannya (12769458132814166583 akan keluar) dan menyandikan di base64, misalnya, di sini :



Untuk melakukan prosedur serupa untuk menandatangani aplikasi, di jendela JADX, buka bagian "APK Signature", salin nilai "SHA-256 Fingerprint" dan mengkodekannya di base64 sebagai array byte:



Penting:di apk asli, pengkodean base64 dilakukan dengan bendera URL_SAFE, mis. alih-alih karakter “+” dan “/”, “-” dan “_” digunakan, masing-masing. Penting untuk memastikan bahwa ini juga akan diamati dengan self-coding. Untuk melakukan ini, ketika melakukan pengkodean secara online, Anda dapat mengganti alfabet yang digunakan dengan "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" dengan "ABCDEFGHIJKLMNOPQRcjjpcjcjcjcjjcjcjcjjcjcjcjcjcjcjcjcjcjcjcjcjcnjcnjcjcjcjcjcjcnjcjcj dengan

Akhirnya, kami memiliki semua bahan untuk membuat daftar file yang berhasil:

  1. diajukanroid.neoquest.ru/api/list_post_apocalyptic_collection?crc= {crc} & sign = {sign}
  2. crc: MTI3Njk0NTgxMzI4MTQxNjY1ODM =
  3. sign: HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY =

Kami menjalankan permintaan GET - dan tepuk tangan, daftar kami! Selain itu, nama salah satu file berbicara untuk dirinya sendiri - "terbuka-jika-Anda-ingin-untuk-melarikan diri" - tampaknya kita membutuhkannya.



Selanjutnya, kami meminta token akses satu kali dan mengunduh file:

import requests

response = requests.get('https://filedroid.neoquest.ru/api/verifyme', 
    		params={    'crc': 'MTI3Njk0NTgxMzI4MTQxNjY1ODM=', 
'sign': HeiTSPWdCuhpbmVxqLxW-uhrozfG_QWpTv9ygn45eHY=},
verify=False)
    
token = response.json()['token']
print(token)
response = requests.get('https://filedroid.neoquest.ru/api/file', 
params={'token': token, 'file': '0p3n1fuw4nt2esk4p3.jpg'}, verify=False)

with open("0p3n1fuw4nt2esk4p3.jpg", 'wb') as fd:
        fd.write(response.content)

Kami membuka file yang diunduh dan mengingat satu keadaan kecil yang kami tinggalkan kemudian:



5. Tambahkan sejumput kriptografi ...


Eh, terlalu dini kita menunda FileDroid. Mari kita kembali ke JADX dan melihat apakah pengembang klien file telah meninggalkan sesuatu yang bermanfaat bagi kita. Ya, ini adalah kasus ketika pembersihan kode jelas tidak populer: metode decryptFile () yang tidak digunakan diam-diam menunggu perhatian kita di kelas ConnectionHandler. Apa yang kita miliki? Mode

enkripsi AES CBC , sinhroposylka menempati 16 byte pertama ... Kemalasan - mesin kemajuan, lebih baik lagi menggunakan Frida dan menguraikan 0p3n1fuw4nt2esk4p3.jpg kami dengan mudah. Tetapi hanya lulus sebagai kunci enkripsi? Tidak ada banyak pilihan, tetapi mengingat adanya metode savePlainFile (String file, token) string "lupa" lainnya, pilihannya jelas.

Siapkan skrip decrypt.js berikut (sebagai token menunjukkan nilai aktual, misalnya, 'HoHknc572mVpZESSQN1Xa7S9zOidxX1PMbykdoM1EXI ='):

Java.perform(function () {
    const JavaString = Java.use('java.lang.String');
    const file_name = JavaString.$new('0p3n1fuw4nt2esk4p3.jpg');
    const ConnectionHandler = Java.use('com.ctf.filedroid.ConnectionHandler');
    const result = ConnectionHandler.savePlainFile(file_name, <token>);
    console.log(result);
});

Kami meletakkan file terenkripsi 0p3n1fuw4nt2esk4p3.jpg di / sdcard /, jalankan FileDroid dan menyuntikkan skrip decrypt.js menggunakan Frida. Setelah skrip berjalan, file plainfile.jpg akan muncul di / sdcard /. Kami membukanya dan ... baru dipecahkan!



Tugas sulit ini mengharuskan peserta untuk memiliki pengetahuan dan keterampilan dalam beberapa bidang keamanan informasi sekaligus, dan kami senang bahwa sebagian besar pesaing berhasil mengatasinya!

Kami berharap bahwa mereka yang tidak memiliki cukup waktu atau pengetahuan sebelum menerima kunci sekarang akan berhasil melewati tugas serupa di CTF :)

All Articles