Guide sur l'apk client-serveur inverse sur l'exemple du job NeoQUEST-2020


Aujourd'hui, nous avons un programme riche (encore, tant de domaines de la cybersécurité à la fois!): Envisagez de décompiler une application Android, d'intercepter le trafic pour obtenir des URL, de reconstruire apk sans code source, de travailler avec des cryptanalystes et bien plus encore :)

Selon la légende NeoQUEST-2020 , le héros a trouvé de vieilles pièces de robot qui devaient être utilisées pour obtenir la clé. Commençons!

1. Apk Reversim


Donc, devant nous, nous avons réussi à extraire un robot semi-démonté - une application apk qui devrait en quelque sorte nous aider à obtenir la clé. Faisons le plus évident: exécutez apk et regardez ses fonctionnalités. L'interface d'application plus que minimaliste ne laisse aucun doute - il s'agit d'un client de fichier FileDroid personnalisé qui vous permet de télécharger un fichier à partir d'un serveur distant. D'accord, ça a l'air facile. Nous connectons le téléphone à Internet, essayons de télécharger (immédiatement key.txt - et si?) - sans succès, le fichier est manquant sur le serveur.



Nous passons à l'événement suivant en termes de complexité - nous décompilons apk en utilisant JADXet analyser le code source de l'application, qui, heureusement, n'est pas du tout obscurci. Notre tâche actuelle consiste à comprendre quels fichiers le serveur distant propose au téléchargement et à sélectionner celui avec la clé qu'ils contiennent.

Nous commençons par la classe com.ctf.filedroid.MainActivity, qui contient la méthode onClick (), qui est la plus intéressante pour nous, dans laquelle le clic sur le bouton «Télécharger» est traité. Dans cette méthode, la classe ConnectionHandler est appelée deux fois: d'abord, la méthode ConnectionHandler.getToken () est appelée, et ensuite seulement, ConnectionHandler.getEncryptedFile (), qui transmet le nom du fichier demandé par l'utilisateur.



Oui, c'est-à-dire que nous avons d'abord besoin d'un jeton! Nous examinerons un peu plus le processus d'obtention.
La méthode ConnectionHandler.getToken () prend deux lignes d'entrée, puis envoie une demande GET, en passant ces lignes comme paramètres «crc» et «sign». En réponse, le serveur envoie les données au format JSON, à partir desquelles notre application extrait le jeton d'accès et l'utilise pour télécharger le fichier. Bien sûr, tout cela est bien, mais que sont «crc» et «signe»?



Pour comprendre cela, nous allons plus loin vers la classe Checks, fournissant gentiment les méthodes badHash () et badSign (). Le premier calcule la somme de contrôle à partir de classes.dex et resources.arsc, concatène ces deux valeurs et l'encapsule dans Base64 (faites attention à l'indicateur 10 = NO_WRAP | URL_SAFE, cela vous sera utile). Et qu'en est-il de la deuxième méthode? Et il fait de même avec l'empreinte SHA-256 de la signature de l'application. Eh, il semble que FileDroid ne soit pas vraiment impatient d'être reconstruit :(



Ok, disons que nous avons reçu le jeton. Et après? Nous le transmettons à l'entrée de la méthode ConnectionHandler.getEncryptedFile (), qui ajoute le nom du fichier demandé au token et génère une autre requête GET, cette fois avec les paramètres «token» et «file». Le serveur en réponse (à en juger par le nom de la méthode) envoie un fichier crypté, qui est stocké sur / sdcard /.

Donc, pour résumer un peu le sous-total: nous avons deux nouvelles, et ... les deux sont mauvaises. Premièrement, FileDroid ne supporte pas vraiment notre zèle pour la modification d'apk (la somme de contrôle et la signature sont vérifiées), et deuxièmement, le fichier reçu du serveur promet d'être crypté.

D'accord, nous allons résoudre les problèmes à mesure qu'ils deviennent disponibles, et maintenant notre principal problème est que nous ne savons toujours pas quel fichier nous devons télécharger. Cependant, en étudiant la classe ConnectionHandler, nous ne pouvions pas nous empêcher de remarquer que juste entre les méthodes getToken () et getEncryptedFile (), les développeurs FileDroid ont oublié une autre méthode très séduisante appelée le nom getListing (). Donc, le serveur prend en charge une telle fonctionnalité ... Il semble que c'est ce dont vous avez besoin!



Pour obtenir la liste, nous aurons besoin du «crc» et du «signe» bien connus - pas un problème, nous savons déjà d'où ils viennent. Nous lisons les valeurs, envoyons une requête GET et ... Alors, arrêtez. Où allons-nous envoyer la demande GET? Ce serait bien d'obtenir d'abord l'URL du serveur distant. Eh, nous revenons à MainActivity.onClick () et voyons comment les arguments netPath sont générés pour appeler les méthodes getToken () et 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"})

D'étranges combinaisons de lettres «fnks» et «qdmk» nous obligent à nous tourner vers le résultat de la décompilation de la méthode wat.getSecure (). Spoiler: JADX a ce résultat comme ça.



En y regardant de plus près, il devient clair que tout ce contenu peu agréable de la méthode peut être remplacé par le boîtier de commutation habituel de ce type:

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

Étant donné que «fnks» et «qdmk» sont déjà utilisés pour obtenir un jeton et télécharger un fichier, «tkog» devrait donner l'URL requise pour demander une liste des fichiers disponibles sur le serveur. Il semble qu'il y ait un espoir d'obtenir le chemin requis à moindre coût ... Tout d'abord, voyons comment les URL sont stockées dans l'application. Nous ouvrons la fonction com.ctf.filedroid.x37AtsW8g.rlieh786d () et voyons que chaque URL est enregistrée en tant que tableau d'octets codés, et la fonction elle-même forme une chaîne à partir de ces octets et la renvoie.



Bien. Mais ensuite la ligne est passée à la fonction com.ctf.filedroid.wat.radon (), dont l'implémentation est soumise à la bibliothèque native libae3d8oe1.so. Bras inversé64? Bien essayé, FileDroid, mais reviens une autre fois?

2. Obtenez les URL du serveur


Essayons d'approcher de l'autre côté: pour intercepter le trafic, obtenir des URL en texte clair (et en bonus, également des valeurs de contrôle et de signature!), Les faire correspondre aux tableaux d'octets de com.ctf.filedroid.x37AtsW8g.rlieh786d () - cela peut Le chiffrement s'avère-t-il être le chiffrement Caesar ou XOR habituel? .. Il ne sera alors pas difficile de restaurer la troisième URL et d'effectuer la liste.

Pour rediriger le trafic, vous pouvez utiliser n'importe quel proxy pratique ( Charles , violoneux , BURPetc.). Nous configurons le transfert sur un appareil mobile, installons le certificat approprié, vérifions que l'interception est réussie et lançons FileFroid. Nous essayons de télécharger un fichier arbitraire et ... voir "NetworkError". Cette erreur a été provoquée par la présence d'épinglage de certificat (voir méthode com.ctf.filedroid.ConnectionHandler.sendRequest): le client de fichier vérifie que le certificat «câblé» dans l'application correspond au serveur avec lequel il interagit. Maintenant, il est clair pourquoi l'intégrité des ressources d'application est contrôlée!



Cependant, dans le trafic intercepté, nous pouvons voir au moins le nom de domaine du serveur auquel accède le client de fichiers, ce qui signifie que l'espoir de décrypter les URL demeure!



Revenons à la fonction com.ctf.filedroid.x37AtsW8g.rlieh786d () et notons que les premières dizaines d'octets coïncident dans tous les tableaux:

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

En outre, le dernier octet du troisième tableau indique qu'il n'était pas sans base64. Essayons de décoder et de piquer les octets résultants avec une partie connue de l'URL:



il semble que personne n'ait jamais été aussi heureux avec ARMag3dd0n! La chose est petite: décodez séquentiellement les URL base64 et xorim avec la clé trouvée. Mais ... et si ce n'était pas XOR, mais un chiffrement de permutation fait par vous-même, que vous ne pouvez pas ramasser même avec cent tentatives?

3. Reconstruisez apk avec Frida


Dans le cadre de cet article, nous envisagerons une méthode de solution plus indolore (et, à notre avis, plus belle) - en utilisant le cadre Frida , qui nous permettra d'exécuter des méthodes d'application apk arbitraires avec les arguments dont nous avons besoin au moment de l'exécution. Pour ce faire, vous avez besoin d'un téléphone avec des droits root ou d'un émulateur. Nous supposons le plan d'action suivant:

  1. Installation des composants Frida sur un PC et un téléphone de test.
  2. Récupérez les URL correspondant au jeton ou aux demandes de liste et téléchargez le fichier (en utilisant Frida).
  3. Récupération de la somme de contrôle et des valeurs de signature de l'application d'origine.
  4. Obtenir une liste des fichiers stockés sur le serveur et identifier le fichier souhaité.
  5. Téléchargez et déchiffrez le fichier.

Tout d'abord, nous clarifierons la relation entre le téléphone rooté et l'apk. Nous installons l'application, l'exécutons, mais le client de fichiers ne veut pas démarrer complètement, il clignote et se ferme uniquement. Nous vérifions les messages via logcat - oui, c'est vrai, FileDroid sent déjà que quelque chose ne va pas et résiste comme il peut.



Nous nous tournons à nouveau vers la classe MainActivity et constatons que la méthode doChecks () est appelée dans onCreate (), et elle affiche les erreurs suivantes dans le journal:



De plus, onResume () vérifie également si le port typique de Frida est ouvert:



notre client de fichier est un peu intolérant au débogage, root et Frida lui-même. Une telle opposition n'est absolument pas incluse dans nos plans, nous obtenons donc le code smali de l'application en utilisant l'utilitaire apktool, ouvrez le fichier MainActivity.smali dans n'importe quel éditeur de texte, trouvez la méthode onCreate () et transformez l'appel doChecks () en commentaire inoffensif:



Ensuite, nous privons la méthode suicide () de la possibilité de vraiment fermer l'application:



Ensuite, reconstruisons notre application légèrement améliorée à l'aide d'apktool et signez lui en exécutant les commandes suivantes (vous pouvez avoir besoin des droits d'administrateur):

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

Nous réinstallons l'application sur le téléphone, l'exécutons - hourra, le téléchargement se passe sans incident, le journal est propre!



Nous procédons à l'installation du framework Frida sur un PC et un appareil mobile:

$ 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

Lancez le serveur Frida Framework sur un appareil mobile:

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

Nous préparons un simple script get-urls.js qui appellera wat.getSecure () pour tous les serveurs de requêtes pris en charge:

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

Nous lançons FileDroid sur l'appareil mobile et «nous accrochons» avec notre script au processus correspondant:



4. Obtenez la liste des fichiers sur le serveur


Enfin, le serveur distant est devenu un peu plus proche de nous! Nous savons maintenant que le serveur prend en charge les demandes des manières suivantes:

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

Afin d'obtenir une liste des fichiers disponibles, il reste à calculer la somme de contrôle et les valeurs de signature de l'application d'origine, puis à les encoder en base64.

Un tel script en python3 vous permettra de faire ceci:

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


Vous pouvez également manuellement. Nous considérons CRC32 de classes.dex et resources.arsc comme n'importe quel outil pratique (par exemple, pour Linux - l'utilitaire crc32 standard), nous obtenons respectivement les valeurs 1276945813 et 2814166583, les concaténons (12769458132814166583 sortira) et encodons en base64, par exemple, ici :



Afin d'effectuer une procédure similaire pour signer l'application, dans la fenêtre JADX, accédez à la section «Signature APK», copiez la valeur «SHA-256 Fingerprint» et codez-la dans base64 sous la forme d'un tableau d'octets:



Important:dans l'apk d'origine, le codage base64 est effectué avec le drapeau URL_SAFE, c'est-à-dire au lieu des caractères «+» et «/», «-» et «_» sont respectivement utilisés. Il est nécessaire de s'assurer que cela sera également observé avec l'auto-codage. Pour ce faire, lors du codage en ligne, vous pouvez remplacer l'alphabet utilisé par "ABCDEFGHIJKLMNOPQRSTUVWXYZabcde fghijklmnopqrstuvwxyz0123456789 + /" par "ABCDEFGHIJKLMNOPQRSTUVWXYZabc script64 à l'aide de 64 script.jp64.jp64cjc64

Enfin, nous avons tous les ingrédients pour réussir à lister les fichiers:

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

Nous exécutons la demande GET - et applaudissons, notre liste! De plus, le nom d'un des fichiers parle de lui-même - "open-if-you-want-to-escape" - il semble que nous en ayons besoin.



Ensuite, nous demandons un jeton d'accès unique et téléchargeons le fichier:

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)

Nous ouvrons le fichier téléchargé et rappelons une petite circonstance que nous avons laissée pour plus tard:



5. Ajoutez une pincée de cryptographie ...


Eh, trop tôt, nous avons mis FileDroid hors tension. Revenons à JADX et voyons si les développeurs de clients de fichiers nous ont laissé quelque chose d'utile. Oui, c'est le cas lorsque le nettoyage de code n'est clairement pas populaire: la méthode decryptFile () inutilisée attend tranquillement notre attention dans la classe ConnectionHandler. Ce que nous avons?

Le cryptage en mode AES de la CBC , sinhroposylka occupe les 16 premiers octets ... Paresse - le moteur du progrès, il vaut mieux utiliser à nouveau Frida et déchiffrer notre 0p3n1fuw4nt2esk4p3.jpg sans effort. Mais passez simplement la clé de cryptage? Il n'y a pas beaucoup d'options, mais étant donné la présence d'une autre méthode savePlainFile (fichier String, Token String) «oubliée», le choix est évident.

Préparez le script decrypt.js suivant (en tant que jeton indiquer la valeur réelle, par exemple, '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);
});

Nous mettons le fichier crypté 0p3n1fuw4nt2esk4p3.jpg sur / sdcard /, exécutons FileDroid et injectons le script decrypt.js à l'aide de Frida. Une fois le script exécuté, le fichier plainfile.jpg apparaîtra sur / sdcard /. Nous l'ouvrons et ... juste résolu!



Cette tâche difficile nécessitait que les participants aient à la fois des connaissances et des compétences dans plusieurs domaines de la sécurité de l'information, et nous sommes heureux que la plupart des concurrents y aient réussi!

Nous espérons que ceux qui n'ont pas eu suffisamment de temps ou de connaissances avant de recevoir la clé réussiront désormais des tâches similaires dans n'importe quel CTF :)

All Articles