Leitfaden zur Reverse Client-Server-Apk am Beispiel des Jobs NeoQUEST-2020


Heute haben wir ein umfangreiches Programm (es würde so viele Cybersicherheitsbereiche gleichzeitig geben!): Erwägen Sie, eine Android-Anwendung zu dekompilieren, den Datenverkehr abzufangen, um URLs abzurufen, apk ohne Quellcode neu zu erstellen, mit Kryptoanalytikern zu arbeiten und vieles mehr :)

Der Legende nach fand der Held NeoQUEST-2020 alte Roboterteile, die verwendet werden müssen, um den Schlüssel zu erhalten. Lass uns anfangen!

1. Reversim apk


Vor uns liegt also ein wenig, das wir aus einem halb zerlegten Roboter extrahieren konnten - eine Apk-Anwendung , die uns irgendwie helfen sollte, den Schlüssel zu bekommen. Lassen Sie uns das offensichtlichste tun: Führen Sie apk aus und sehen Sie sich die Funktionalität an. Die mehr als minimalistische Anwendungsoberfläche lässt keinen Zweifel offen - dies ist ein benutzerdefinierter FileDroid-Datei-Client, mit dem Sie eine Datei von einem Remote-Server herunterladen können. Okay, es sieht einfach aus. Wir verbinden das Telefon mit dem Internet, machen einen Testversuch zum Herunterladen (sofort key.txt - na ja, was ist wenn?) - erfolglos fehlt die Datei auf dem Server.



Wir gehen hinsichtlich der Komplexität zum nächsten Ereignis über - wir dekompilieren apk mit JADXund analysieren Sie den Quellcode der Anwendung, der zum Glück überhaupt nicht verschleiert ist. Unsere aktuelle Aufgabe besteht darin, zu verstehen, welche Dateien der Remote-Server zum Herunterladen anbietet, und die Datei mit dem Schlüssel auszuwählen.

Wir beginnen mit der Klasse com.ctf.filedroid.MainActivity, die die für uns interessanteste onClick () -Methode enthält, bei der der Klick auf die Schaltfläche „Download“ verarbeitet wird. Innerhalb dieser Methode wird die ConnectionHandler-Klasse zweimal aufgerufen: Zuerst wird die ConnectionHandler.getToken () -Methode aufgerufen und erst dann ConnectionHandler.getEncryptedFile (), die den Namen der vom Benutzer angeforderten Datei übergibt.



Ja, das heißt, zuerst brauchen wir einen Token! Wir werden uns etwas näher mit dem Prozess der Erlangung befassen.
Die ConnectionHandler.getToken () -Methode verwendet zwei Eingabezeilen und sendet dann eine GET-Anforderung, wobei diese Zeilen als Parameter "crc" und "sign" übergeben werden. Als Antwort sendet der Server die Daten im JSON-Format, aus dem unsere Anwendung das Zugriffstoken extrahiert und zum Herunterladen der Datei verwendet. Das ist natürlich alles gut, aber was sind "crc" und "sign"?



Um dies zu verstehen, gehen wir weiter in Richtung der Checks-Klasse und stellen freundlicherweise die Methoden badHash () und badSign () zur Verfügung. Der erste berechnet die Prüfsumme aus classes.dex und resources.arsc, verkettet diese beiden Werte und verpackt sie in Base64 (beachten Sie das Flag 10 = NO_WRAP | URL_SAFE, dies ist praktisch). Und was ist mit der zweiten Methode? Dasselbe tut er mit dem SHA-256-Fingerabdruck der Anwendungssignatur. Eh, es sieht so aus, als ob FileDroid nicht wirklich darauf aus ist, wieder aufgebaut zu werden :(



Ok, sagen wir, wir haben den Token erhalten. Was weiter? Wir übergeben es an die Eingabe der ConnectionHandler.getEncryptedFile () -Methode, die den Namen der angeforderten Datei an das Token anfügt und eine weitere GET-Anforderung generiert, diesmal mit den Parametern "token" und "file". Der Server sendet als Antwort (gemessen am Namen der Methode) eine verschlüsselte Datei, die auf / sdcard / gespeichert ist.

Um eine kleine Zwischensumme zusammenzufassen: Wir haben zwei Neuigkeiten und ... beide sind schlecht. Erstens unterstützt FileDroid unseren Eifer, apk zu ändern (Prüfsumme und Signatur werden überprüft), nicht wirklich, und zweitens verspricht die vom Server empfangene Datei, verschlüsselt zu werden.

Okay, wir werden die Probleme lösen, sobald sie verfügbar sind, und jetzt besteht unser Hauptproblem darin, dass wir immer noch nicht wissen, welche Datei wir herunterladen müssen. Während des Studiums der ConnectionHandler-Klasse konnten wir jedoch feststellen, dass die FileDroid-Entwickler zwischen den Methoden getToken () und getEncryptedFile () eine weitere sehr verführerische Methode namens speakListing () vergessen haben. Der Server unterstützt also solche Funktionen ... Es scheint, dass Sie dies benötigen!



Um die Auflistung zu erhalten, benötigen wir das bekannte "crc" und "sign" - kein Problem, wir wissen bereits, woher sie kommen. Wir lesen die Werte, senden eine GET-Anfrage und ... Also, hör auf. Wohin senden wir die GET-Anfrage? Es wäre schön, zuerst die URL des Remote-Servers zu erhalten. Eh, wir kehren zu MainActivity.onClick () zurück und sehen, wie die netPath-Argumente generiert werden, um die Methoden getToken () und getEncryptedFile () aufzurufen:

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"})

Seltsame Buchstabenkombinationen „fnks“ und „qdmk“ zwingen uns, uns dem Ergebnis der Dekompilierung der Methode wat.getSecure () zuzuwenden. Spoiler: JADX hat dieses Ergebnis so lala.



Bei näherer Betrachtung wird deutlich, dass all dieser nicht sehr angenehme Inhalt der Methode durch den üblichen Schaltkasten dieser Art ersetzt werden kann:

// . . .
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 

Da "fnks" und "qdmk" bereits verwendet werden, um ein Token abzurufen und eine Datei herunterzuladen, sollte "tkog" die URL angeben, die erforderlich ist, um eine Liste der verfügbaren Dateien auf dem Server anzufordern. Es scheint, dass es eine Hoffnung gibt, den erforderlichen Pfad billig zu erhalten ... Lassen Sie uns zunächst sehen, wie URLs in der Anwendung gespeichert werden. Wir öffnen die Funktion com.ctf.filedroid.x37AtsW8g.rlieh786d () und sehen, dass jede URL als codiertes Bytearray gespeichert wird und die Funktion selbst aus diesen Bytes eine Zeichenfolge bildet und diese zurückgibt.



Gut. Dann wird die Zeile an die Funktion com.ctf.filedroid.wat.radon () übergeben, deren Implementierung an die native Bibliothek libae3d8oe1.so gesendet wird. Reverse Arm64? Netter Versuch, FileDroid, aber kommst du zu einem anderen Zeitpunkt?

2. Holen Sie sich die Server-URLs


Versuchen wir, uns von der anderen Seite zu nähern: Um den Datenverkehr abzufangen, URLs im Klartext abzurufen (und als Bonus auch Prüfsummen- und Signaturwerte!), Ordnen Sie sie Byte-Arrays von com.ctf.filedroid.x37AtsW8g.rlieh786d () zu - das kann Stellt sich heraus, dass die Verschlüsselung die übliche Caesar- oder XOR-Verschlüsselung ist? Dann ist es nicht schwierig, die dritte URL wiederherzustellen und die Auflistung durchzuführen.

Um den Datenverkehr umzuleiten, können Sie einen beliebigen Proxy verwenden ( Charles , Fiddler , BURP)usw.). Wir konfigurieren die Weiterleitung auf einem mobilen Gerät, installieren das entsprechende Zertifikat, überprüfen, ob das Abfangen erfolgreich ist, und starten FileFroid. Wir versuchen eine beliebige Datei herunterzuladen und ... siehe "NetworkError". Dieser Fehler wurde durch das Vorhandensein von Zertifikat-Pinning verursacht (siehe Methode com.ctf.filedroid.ConnectionHandler.sendRequest): Der Datei-Client überprüft, ob das in der Anwendung „verkabelte“ Zertifikat dem Server entspricht, mit dem es interagiert. Jetzt ist klar, warum die Integrität der Anwendungsressourcen kontrolliert wird!



Im abgefangenen Datenverkehr sehen wir jedoch mindestens den Domänennamen des Servers, auf den der Datei-Client zugreift, was bedeutet, dass die Hoffnung auf Entschlüsselung der URLs bestehen bleibt!



Kehren wir zur Funktion com.ctf.filedroid.x37AtsW8g.rlieh786d () zurück und beachten Sie, dass die ersten zehn Bytes in allen Arrays übereinstimmen:

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', . . ., '='};

Darüber hinaus weist das letzte Byte des dritten Arrays darauf hin, dass es nicht ohne base64 war. Versuchen wir, die resultierenden Bytes mit einem bekannten Teil der URL zu dekodieren und



zu stecken : Es scheint, dass noch nie jemand so glücklich mit ARMag3dd0n war! Die Sache ist klein: Decodieren Sie nacheinander base64-URLs und xorim mit dem gefundenen Schlüssel. Aber ... und wenn es nicht XOR war, sondern eine selbst erstellte Permutations-Chiffre, die Sie selbst mit hundert Versuchen nicht erfassen können?

3. Baue apk mit Frida wieder auf


Im Rahmen dieses Aufsatzes werden wir eine schmerzlosere (und unserer Meinung nach schönere) Lösungsmethode in Betracht ziehen - unter Verwendung des Frida- Frameworks , mit dem wir beliebige apk-Anwendungsmethoden mit den Argumenten ausführen können, die wir zur Laufzeit benötigen. Dazu benötigen Sie ein Telefon mit Root-Rechten oder einen Emulator. Wir gehen von folgendem Aktionsplan aus:

  1. Installieren von Frida-Komponenten auf einem PC und einem Testtelefon.
  2. Stellen Sie URLs wieder her, die mit Token- oder Listenanforderungen übereinstimmen, und laden Sie die Datei herunter (mithilfe von Frida).
  3. Abrufen der Prüfsummen- und Signaturwerte der ursprünglichen Anwendung.
  4. Abrufen einer Liste der auf dem Server gespeicherten Dateien und Identifizieren der gewünschten Datei.
  5. Laden Sie die Datei herunter und entschlüsseln Sie sie.

Zunächst werden wir die Beziehung zwischen dem verwurzelten Telefon und apk klären. Wir installieren die Anwendung, führen sie aus, aber der Datei-Client möchte nicht vollständig booten, sondern blinkt nur und wird geschlossen. Wir überprüfen Nachrichten über logcat - ja, FileDroid hat bereits das Gefühl, dass etwas nicht stimmt, und widersteht so gut es geht.



Wir wenden uns erneut der MainActivity-Klasse zu und stellen fest, dass die doChecks () -Methode in onCreate () aufgerufen wird und die folgenden Fehler im Protokoll anzeigt:



Außerdem prüft onResume (), ob der für Frida typische Port geöffnet ist:



Unser Datei-Client ist etwas intolerant zu debuggen, root und Frida selbst. Ein solcher Widerspruch ist in unseren Plänen absolut nicht enthalten, daher erhalten wir den Smali-Code der Anwendung mit dem Dienstprogramm apktoolÖffnen Sie die Datei MainActivity.smali in einem beliebigen Texteditor, suchen Sie die Methode onCreate () und verwandeln Sie den Aufruf von doChecks () in einen harmlosen Kommentar:



Dann entziehen wir der Methodeicide () die Möglichkeit, die Anwendung wirklich herunterzufahren:



Als Nächstes erstellen wir unsere leicht verbesserte Anwendung erneut mit apktool und sign Führen Sie die folgenden Befehle aus (möglicherweise benötigen Sie Administratorrechte):

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

Wir installieren die Anwendung auf dem Telefon neu, führen sie aus - Hurra, der Download verläuft ohne Zwischenfälle, das Protokoll ist sauber!



Wir fahren fort, das Frida-Framework auf einem PC und einem mobilen Gerät zu installieren:

$ 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

Starten Sie den Frida Framework-Server auf einem mobilen Gerät:

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

Wir bereiten ein einfaches get-urls.js-Skript vor, das wat.getSecure () für alle unterstützten Anforderungsserver aufruft:

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"));
});

Wir starten FileDroid auf dem mobilen Gerät und „klammern“ uns mit unserem Skript an den entsprechenden Prozess:



4. Rufen Sie die Liste der Dateien auf dem Server ab


Endlich ist uns der Remote-Server etwas näher gekommen! Jetzt wissen wir, dass der Server Anforderungen auf folgende Weise unterstützt:

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

Um eine Liste der verfügbaren Dateien zu erhalten, müssen die Prüfsummen- und Signaturwerte der ursprünglichen Anwendung berechnet und anschließend in base64 codiert werden.

Mit einem solchen Skript in Python3 können Sie Folgendes tun:

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')


Sie können auch manuell. Wir betrachten CRC32 von classes.dex und resources.arsc jedes geeignete Werkzeug zu sein (zB für Linux - die Standard - crc32 Dienstprogramm), wir die Werte 1276945813 und 2814166583 bzw. erhalten, verketten sie (12769458132814166583 kommt heraus) und codieren in base64 zum Beispiel hier :



Um ein ähnliches Verfahren zum Signieren der Anwendung auszuführen, gehen Sie im JADX-Fenster zum Abschnitt „APK-Signatur“, kopieren Sie den Wert „SHA-256 Fingerprint“ und codieren Sie ihn in base64 als Byte-Array:



Wichtig:in der ursprünglichen apk wird die base64-Codierung mit dem URL_SAFE-Flag durchgeführt, d.h. Anstelle der Zeichen "+" und "/" werden "-" und "_" verwendet. Es ist darauf zu achten, dass dies auch bei der Selbstcodierung beachtet wird. Um dies zu tun, können Sie beim Online-Codieren das mit "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" verwendete Alphabet durch "ABCDEFGHIJKLMNOPQRSTUVWXYZabc script64" verwenden

Schließlich haben wir alle Zutaten für eine erfolgreiche Auflistung von Dateien:

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

Wir führen die GET-Anfrage aus - und Prost, unsere Auflistung! Darüber hinaus spricht der Name einer der Dateien für sich selbst - "Öffnen, wenn Sie entkommen wollen" - es scheint, dass wir ihn brauchen.



Als nächstes fordern wir ein einmaliges Zugriffstoken an und laden die Datei herunter:

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)

Wir öffnen die heruntergeladene Datei und erinnern uns an einen kleinen Umstand, den wir für später hinterlassen haben:



5. Fügen Sie eine Prise Kryptographie hinzu ...


Eh, zu früh haben wir FileDroid verschoben. Kehren wir zu JADX zurück und prüfen, ob die Entwickler des Datei-Clients uns etwas Nützliches hinterlassen haben. Ja, dies ist der Fall, wenn die Codebereinigung eindeutig nicht beliebt ist: Die nicht verwendete decryptFile () -Methode wartet leise auf unsere Aufmerksamkeit in der ConnectionHandler-Klasse. Was wir haben?

Verschlüsselung AES- Modus der CBC , sinhroposylka belegt die ersten 16 Bytes ... Faulheit - der Motor des Fortschritts, es ist besser, wieder Frida zu verwenden und unser 0p3n1fuw4nt2esk4p3.jpg mühelos zu entschlüsseln. Aber einfach als Verschlüsselungsschlüssel übergeben? Es gibt nicht viele Optionen, aber angesichts des Vorhandenseins einer anderen "vergessenen" savePlainFile-Methode (String-Datei, String-Token) liegt die Wahl auf der Hand.

Bereiten Sie das folgende decrypt.js-Skript vor (als Token Geben Sie den tatsächlichen Wert an, z. B. '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);
});

Wir legen die verschlüsselte Datei 0p3n1fuw4nt2esk4p3.jpg auf / sdcard / ab, führen FileDroid aus und fügen das Skript decrypt.js mit Frida ein. Nachdem das Skript ausgeführt wurde, wird die Datei plainfile.jpg auf / sdcard / angezeigt. Wir öffnen es und ... gerade gelöst!



Für diese schwierige Aufgabe mussten die Teilnehmer über Kenntnisse und Fähigkeiten in verschiedenen Bereichen der Informationssicherheit gleichzeitig verfügen, und wir freuen uns, dass die meisten Wettbewerber erfolgreich damit fertig wurden!

Wir hoffen, dass diejenigen, die vor Erhalt des Schlüssels nicht genügend Zeit oder Wissen hatten, ähnliche Aufgaben in einer CTF erfolgreich bestehen werden :)

All Articles