EnquĂŞte sur une archive inconnue

Délocalisation. Nouvelle ville. Recherche d'emploi. Même pour un professionnel de l'informatique, cela peut prendre beaucoup de temps. Une série d'entretiens qui, en général, sont très similaires les uns aux autres. Et comme cela arrive généralement lorsque vous avez déjà trouvé un emploi, après un certain temps, un bureau intéressant est annoncé.

Il était difficile de comprendre ce qu'elle faisait spécifiquement, cependant, son domaine d'intérêt était la recherche sur les logiciels d'autres personnes. Cela semble intrigant, bien que lorsque vous réalisez que cela ne semble pas être un fournisseur qui publie des logiciels de cybersécurité, vous vous arrêtez une seconde et commencez à gratter vos navets.



En bref: ils ont jeté l'archive et ont proposé de l'examiner en tant que tâche de test et d'essayer de calculer une certaine signature sur la base des données d'entrée présentées. Il convient de noter que j'avais très peu d'expérience dans de telles activités et, probablement, c'est pourquoi lors de la première itération de la solution, je n'avais que quelques heures - alors la motivation pour le faire est tombée à néant. Et oui, bien sûr, la première chose que j'ai essayé de l'exécuter sur le téléphone / l'émulateur - cette application n'est pas valide.

Ce que nous avons: une archive avec l'extension ".apk" . J'ai placé la tâche elle-même sous le spoiler pour qu'elle ne soit pas indexée par les moteurs de recherche: que faire si les gars ne l'aiment pas, que je mette la solution sur Habr?

Tâche elle-même
L'APK contient des fonctionnalités pour générer des signatures pour un tableau associatif.
Essayez d'obtenir une signature pour l'ensemble de données suivant:

{ "user" : "LeetD3vM4st3R", "password": "__s33cr$$tV4lu3__", "hash": "34765983265937875692356935636464" } 


Retroussez les manches


On dit que l'archive contient la fonctionnalité de signature d'un tableau associatif. Par l'extension de fichier, nous comprenons immédiatement que nous avons affaire à une application écrite pour Android. Nous déballons d'abord l'archive. En fait, il s'agit d'une archive ZIP régulière, et tout archiveur y fera face à la légère. J'ai utilisé l'utilitaire apktool et, comme il s'est avéré, j'ai accidentellement contourné quelques râteaux. Oui, cela arrive (généralement le contraire, oui?). Le sort est assez simple:

 apktool d task.zip 

Il s'avère que le code et les ressources dans le fichier apk sont également stockés dans des fichiers binaires séparés, et d'autres logiciels seront nécessaires pour les extraire. apktool a implicitement extrait les octets de classe, les ressources et les a décomposés dans une hiérarchie de fichiers naturelle. Vous pouvez continuer.

 ├── AndroidManifest.xml ├── apktool.yml ├── lib │   └── arm64-v8a ├── original │   ├── AndroidManifest.xml │   └── META-INF ├── res │   ├── anim │   ├── color │   ├── drawable │   ├── layout │   ├── layout-watch-v20 │   ├── mipmap-anydpi-v26 │   ├── values │   └── values-af ├── smali │   ├── android │   ├── butterknife │   ├── com │   ├── net │   └── org └── unknown   └── org 

Nous voyons une hiérarchie similaire (à gauche sa version simplifiée) et essayons de comprendre par où commencer. Il convient de noter que j'ai néanmoins écrit une fois quelques petites applications pour Android, donc l'essence d'une partie des répertoires et, en général, les principes des applications Android de l'appareil, sont à peu près clairs pour moi.

Pour commencer, je décide de simplement «parcourir» les fichiers. J'ouvre AndroidManifest.xml et commence à lire de manière significative. Mon attention est attirée par un étrange attribut

 android:supportsRtl="true" 

Il s'avère qu'il est responsable du support des langues avec la lettre "de droite à gauche" dans l'application. Nous commençons à tendre. Pas bon.

De plus, mon regard s'accroche au dossier inconnu. En dessous se trouve une hiérarchie du formulaire: org.apache.commons.codec.language.bm et un grand nombre de fichiers texte au contenu obscur. Google le nom complet du paquet et il se trouve ce qui est stocké ici, quelque chose lié à l'algorithme de recherche de mots phonétiquement similaires à celui donné. Franchement, ici, j'ai commencé à m'efforcer davantage. Après avoir fouillé un peu dans les répertoires, j'ai trouvé le code lui-même, puis le plaisir a commencé. Je n'ai pas été accueilli par le bytecode Java habituel, avec lequel j'ai réussi à jouer, mais autre chose. Très similaire, mais différent.

En fin de compte, Android a sa propre machine virtuelle - Dalvik. Et, comme toute machine virtuelle respectée, elle a son propre bytecode. Il semble que lors de la première tentative pour résoudre ce problème, c'est sur cette triste note que j'ai annoncé l'entracte, salué, baissé le rideau et jeté le tout pendant 4 mois jusqu'à ce que ma curiosité me finisse complètement.

Retroussez les manches [2]


"Mais ne peut-il en être ainsi pour que tout soit plus facile?" - C'est la question que je me suis posée quand j'ai commencé la tâche pour la deuxième fois. J'ai commencé à chercher sur Internet un décompilateur de smali à Java. J'ai seulement vu qu'il est impossible de mener à bien ce processus clairement. Fronçant un peu les sourcils, il se rendit à Github et introduisit quelques phrases clés dans la ligne de recherche. Le premier est venu smali2java .

 git clone gradle build java -jar smali2java.jar .. 

Erreurs Je vois une énorme trace de pile et des erreurs sur plusieurs pages du terminal. Après avoir lu un peu sur l'essence du contenu (et restreindre les émotions de la taille de la trace de la pile), je trouve que cet outil fonctionne sur la base d'une grammaire décrite et le bytecode qu'elle a rencontré clairement ne lui correspond pas. J'ouvre le bytecode smali et j'y vois des annotations, des méthodes synthétiques et d'autres constructions étranges. Il n'y avait rien de tel dans le bytecode Java! Combien de temps? Supprimer!

Plus de détails
Il s'est avéré que la machine virtuelle Dalvik (ainsi que la JVM) n'est pas consciente de l'existence de concepts tels que les classes internes / externes (lire les classes imbriquées), et le compilateur génère les méthodes dites «synthétiques» pour fournir l'accès de la classe imbriquée à des champs externes, par exemple.

A titre d'exemple:


Si la classe externe (OuterClass) a un champ

 public class OuterClass { List a; ... } 

Pour que la classe privée puisse accéder au champ de la classe externe, le compilateur générera implicitement la méthode suivante:

 static synthetic java.util.List getList(OuterClass p1) { p1 = p1.a; return p1; } 

De plus, grâce à une telle cuisine «compartiment moteur», le travail de certains autres mécanismes fournis par la langue est atteint.

Vous pouvez commencer à étudier cette question plus en détail à partir d'ici .

N'aide pas. Il jure même à un bytecode apparemment pas suspect. J'ouvre le code source du décompilateur, lis et vois quelque chose de très étrange: même les programmeurs hindous (avec tout le respect que je leur dois) n'auraient pas écrit cela. Une pensée se glisse: pas vraiment le code généré. Je rejette l'idée pendant environ 30 minutes, essayant de comprendre quelle est l'erreur. COMPLIQUÉ. J'ouvre à nouveau Github - et vraiment, un analyseur généré par la grammaire. Et voici le générateur lui-même. Tout ranger et essayer d'approcher de l'autre côté.

Il convient de noter qu'un peu plus tard, j'ai toujours essayé de changer la grammaire et, par endroits, même le bytecode lui-même afin que le décompilateur puisse encore le digérer. Mais même lorsque le bytecode est devenu valide en termes de grammaire du décompilateur, le programme ne m'a tout simplement rien retourné. Open source ...

Je feuillette le bytecode et tombe sur des constantes que je ne connais pas. Googler, je rencontre la même chose dans le livre sur les applications Android inversées. Je rappelle que ce n'est que l'ID attribué par le préprocesseur du compilateur, qui est affecté aux ressources de l'application Android (la constante de temps d'écriture du code est R. *). Au cours de la prochaine demi-heure, j'examinerai brièvement quels registres sont responsables de quoi, dans quel ordre les arguments sont passés, et j'explore généralement la syntaxe.

À quoi ça ressemble?


J'ai trouvé la disposition de la fenêtre principale de l'application, et j'ai déjà compris ce qui se passait dans l'application: sur l'écran principal (Activité), il y a un RecyclerView (conditionnellement, une vue qui peut réutiliser des objets d'interface utilisateur qui ne sont pas actuellement affichés pour l'utilisation de la mémoire) avec des champs de saisie des paires clé / valeur, quelques boutons chargés d'ajouter une nouvelle paire clé / valeur à un certain conteneur abstrait et un bouton qui génère une signature (signature) pour ce conteneur.

En regardant les annotations et en observant une certaine quantité de code étrangement similaire à celui généré, je commence à google. Le projet utilise la bibliothèque ButterKnife, qui permet d'utiliser des annotations pour gonfler () -> bind () les éléments d'interface utilisateur automatiquement. S'il y a des annotations dans la classe, le processeur d'annotation ButterKnife crée implicitement une autre classe de classeur de la forme <original_class> __ViewBinding , qui fait tout le sale boulot sous le capot. En fait, j'ai obtenu toutes ces informations d'un seul fichier MainActivity après avoir recréé manuellement la similitude de la source Java à partir de celui-ci. Après une demi-heure, j'ai réalisé que les annotations de cette bibliothèque peuvent également définir un rappel sur les actions des boutons et j'ai trouvé les fonctions clés qui étaient réellement responsables de l'ajout d'une paire clé / valeur au conteneur et de la génération d'une signature.

Bien sûr, pendant l'étude, j'ai dû entrer dans les «abats» de diverses bibliothèques et plug-ins, car même les beaux landos avec cookies ne couvrent pas tous les cas d'utilisation et les détails, ce qui, pour tout «reverse», je pense, est une pratique courante.

La paresse est l'amie d'un programmeur


Après avoir passé un peu plus de temps sur la deuxième source, j'étais complètement fatigué et j'ai réalisé qu'il n'était pas possible de cuisiner de la bouillie. Je grimpe à nouveau sur Github, et cette fois je regarde de plus près. Je trouve le projet Smali2PsuedoJava - un décompilateur en «code pseudo-Java». Même si cet utilitaire, au moins quelque chose peut conduire à une apparence humaine, alors pour moi l'auteur est une chope de sa bière préférée (enfin, ou du moins mettre un astérisque sur Github, pour commencer).

Et ça marche vraiment! Effet sur le visage:



Rencontrez Cipher.so


Un peu plus tard, en étudiant le pseudo-code Java du projet et en le comparant incrédule avec le bytecode smali, je trouve une étrange bibliothèque dans le code - Cipher.so. Google, je découvre que c'est le cryptage d'un ensemble de valeurs de temps de compilation à l'intérieur de l'archive APK. Cela est généralement nécessaire lorsque l'application utilise des constantes du formulaire: adresses IP, informations d'identification pour une base de données externe, jetons d'autorisation, etc. - ce qui peut être obtenu à l'aide de l'ingénierie inverse de l'application. Certes, l'auteur écrit clairement que ce projet est abandonné, disent-ils, partez. Cela devient intéressant.

Cette bibliothèque donne accès aux valeurs via la bibliothèque Java, où la méthode spécifique est la clé qui nous intéresse. Cela ne fait que nourrir mon intérêt et je commence à grimper plus profondément.

En bref, que fait Cipher.so et comment cela fonctionne-t-il:

  • dans le fichier Gradle de notre projet les clĂ©s et les valeurs correspondantes sont enregistrĂ©es
  • toutes les valeurs clĂ©s seront automatiquement compressĂ©es dans une bibliothèque dynamique distincte (.so), qui sera gĂ©nĂ©rĂ©e au moment de la compilation. Oui - oui, sera gĂ©nĂ©rĂ©.
  • alors ces clĂ©s peuvent ĂŞtre obtenues Ă  partir des mĂ©thodes Java gĂ©nĂ©rĂ©es par Cipher.so
  • après la crĂ©ation de l'APK, les noms des clĂ©s sont hachĂ©s par MD5 (pour plus de sĂ©curitĂ©, bien sĂ»r)

Après avoir trouvé la bibliothèque dynamique dont j'ai besoin dans le dossier d'archives, je procède à sa sélection. Pour commencer, en tant qu'inverse expérimenté (non), j'essaie de commencer par un simple - je décide de regarder la section avec des constantes et des lignes intéressantes dans un binaire de type ELF. Malheureusement, les utilisateurs du Mac prêt à l'emploi sont manquants, et avant le début, nous disons le chéri:

 brew install binuitls 

Et n'oubliez pas d'écrire le chemin vers / usr / local dans PATH, car brew vous protège de tout de manière gentleman ...

 greadelf -p .rodata lib/arm64-v8a/libcipher-lib.so | head -n 15 

Nous limitons la sortie aux 15 premières lignes, sinon cela peut entraîner un choc pour un ingénieur non préparé.



Aux adresses inférieures, nous remarquons des lignes suspectes. Comme je l'ai découvert, en étudiant les sources de Cipher.so, les clés et les valeurs sont placées dans le std :: map habituel : cela donne peu d'informations, mais nous savons que dans le binaire lui-même, avec les mots de passe cryptés, il y a aussi des clés obscurcies.

Comment est le cryptage des valeurs? En étudiant la source, j'ai trouvé que le cryptage se produit en utilisant AES - le système de cryptage symétrique standard. Donc, s'il y a des valeurs chiffrées, alors la clé devrait être à proximité ... Après l'avoir étudié pendant un certain temps, je suis tombé sur un problème dans le même projet avec le titre provocateur "Stockage de clé non sécurisé: les secrets sont très faciles à retrouver" . En fait, j'ai découvert que la clé est stockée sous forme claire dans le binaire et j'ai trouvé l'algorithme de décryptage. Dans l'exemple, la clé était à l'adresse zéro, et bien que j'ai compris que le compilateur pouvait la placer à un autre endroit dans la section .rodata du fichier binaire, j'ai décidé que cette unité suspecte à l'adresse zéro était la clé.

Tentative n ° 1: je procède au déchiffrement des valeurs et je crois que la clé de chiffrement est la même. L'erreur. OpenSSL laisse entendre que quelque chose ne va pas. Après avoir lu un peu les sources de Cipher.so, je comprends que si l'utilisateur ne spécifie pas de clé lors de l'assemblage, alors la clé par défaut est utilisée - Cipher.so@DEFAULT .

Tentative n ° 2: erreur à nouveau. Hmm ... Est-ce vraiment redéfini par cette constante? Il est assez simple de se tromper: confondre du code écrit en Gradle, avec un formatage «disparu». Je vérifie encore. Tout semble être ainsi.

Au lieu des clés se trouvent leurs hachages MD5, puis j'essaie de tenter ma chance et d'ouvrir un service avec des tables arc-en-ciel. Voila - l'une des clés est le mot "mot de passe". Il n'y a pas de seconde. Bien sûr, cela ne nous donne pas grand-chose. Ces deux clés se trouvent respectivement aux adresses 240 et 2a2. En principe, il est facile de les reconnaître immédiatement - 32 caractères (MD5).

J'ai tout vérifié à nouveau et j'ai essayé de faire le déchiffrement avec toutes les autres lignes (qui se trouvent dans les adresses inférieures) comme clé de déchiffrement - tout est en vain.
Donc, il y a une autre clé secrète, l'algorithme des actions semble être correct. Je jette cette tâche de côté et essaie de ne pas m'enterrer.

Après avoir fouillé un peu dans l'algorithme de signature de conteneur, je vois toujours des appels à la bibliothèque et au code Cipher.so qui utilisent également les fonctions cryptographiques de la bibliothèque Java.

Une énigme (que je n'ai jamais résolue)


Dans la fonction responsable du chiffrement, au tout début, il y a une vérification des clés dans le conteneur.

 public byte[] a(java/util/Map p1) { v0 = p1.size() v1 = 0x0; if (v0 != 0) goto :cond_0 p1 = new byte[v1]; return p1; :cond_0 v0 = "user"; v0 = p1.containsKey(v0) if (v0 == 0) goto :cond_1 p1 = new byte[v1]; return p1; ... 

Littéralement: s'il existe une clé "utilisateur", alors ce conteneur n'est pas signé (une signature nulle est retournée). Un sentiment étrange: il semble que le problème soit résolu, mais il semble en quelque sorte étrangement simple. Alors pourquoi tout inventer? S'égarer? Alors pourquoi n'ai-je pas étudié ce code couramment auparavant? Hmm ...

Non, pas vrai. J'ai spécifié la réponse d'un certain utilisateur dans un messager bleu, dont les contacts m'ont été fournis lors de la mission. Creuser plus loin. Peut-être que la clé d'entrée / valeur définie change d'une manière ou d'une autre lorsqu'elle est ajoutée au conteneur? J'ai lu attentivement le code.

Veuillez noter que le décompilateur a supprimé les annotations du code smali. Et s'il enlevait quelque chose d'important? Je vérifie les fichiers principaux - il semble, rien de significatif. Tout ce qui est important est en place, mais le sens n'est pas perdu. Je vérifie les fonctions de rappel qui sont responsables de l'écriture d'une paire clé / valeur de TextBox conditionnelle dans des conteneurs internes. Je n'ai rien trouvé de criminel.

Je suis devenu aussi sceptique que possible Ă  propos de chaque ligne de code - je ne peux plus faire confiance Ă  personne.

Solution simple # 2: j'ai remarqué que la procédure de signature commence par la vérification de la présence d'une valeur (sous-chaîne dans la chaîne) dans la signature du certificat avec lequel l'application a été signée.

 @OnClick //   protected void huvot324yo873yvo837yvo() { String signature = "no data"; boolean result = some_packages.isKeyInSignature(this); if result { Map map = new HashMap(); ... 

Le sens lui-même, bien sûr, se trouve crypté dans ce malheureux binar. Et en fait, si cette valeur n'est pas dans la signature, l'algorithme ne signera rien, mais renverra simplement la chaîne «pas de données», comme signature ... Encore une fois, nous sommes pris pour Cipher ...

Combat final de déchiffrement de clés


Pour comprendre l'ampleur de la tragédie, je me suis confondu comme ceci:

J'ai fait un vidage hexadécimal de cette section et j'ai regardé dans les deux premières lignes, dont les soupçons ne se sont pas dissipés dès le début.



Si vous faites attention, le caractère qui sépare les lignes ici est '0x00'. Il est également couramment utilisé par la bibliothèque C standard, dans les fonctions de chaîne. De là, ce n'est pas moins intéressant, quel genre de caractère spatial se trouve au milieu de la première ligne? Ensuite, des tentatives folles commencent, où la clé est:

  • toute la première rangĂ©e
  • première ligne avant l'espace
  • première ligne de l'espace Ă  la fin
  • ...

Le degré de paranoïa peut déjà être estimé. Lorsque vous ne comprenez pas à quel point la tâche doit être difficile et astucieuse, vous commencez à conduire. Et pourtant, pas ça. Puis la pensée m'est venue à l'esprit: "L'algorithme fonctionne-t-il correctement à partir d'un problème sur ma machine?". En général, la séquence d'actions y est logique et ne soulève pas de questions, mais la question est: les commandes de ma machine font-elles ce qu'elles exigent d'eux? Alors qu'en pensez-vous?

Après avoir vérifié toutes les étapes manuellement, il s'est avéré que

 echo "some_base64_input" | openssl base64 -d 

sur certains arguments d'entrée, il renvoie soudainement une chaîne vide. Hmm.

En le remplaçant par le premier décodeur base64 sur la machine et en triant les principaux candidats, une clé appropriée a été immédiatement trouvée et les clés ont été déchiffrées en conséquence.

Récupération des signatures d'un certificat


 class a { public static boolean isKeyInSignature(android.content.Context p1) { v0 = 0x0; try TRY_0{ v1 = p1.getPackageManager() p0 = p1.getPackageName() v2 = 0x40; // GET_SIGNATURES PackageInfo p0 = v1.getPackageInfo(p0, v2) android.content.pm.Signature[] p0 = p0.signatures; // Order are not guaranteed v1 = p0.length; v2 = 0x0; :goto_0 if (v2 >= v1) goto :cond_1 v3 = p0[v2]; String v3 = v3.toCharsString() String v4 = net.idik.lib.cipher.so.CipherClient.a() v3 = v3.contains(v4) }TRY_0 catch TRY_0 (android/content/pm/PackageManager$NameNotFoundException) goto :catch_0; if (v3 == 0) goto :cond_0 p1 = 0x1; return p1; :cond_0 v2 = v2 + 0x1; goto :goto_0 :catch_0 p0 = Thrown Exception p1.printStackTrace() :cond_1 return v0; } 

Voici à quoi ressemble le pseudocode généré après mes modifications mineures. Confond quelques choses:

  • mauvaise connaissance de la cryptographie et de la "cuisine" des certificats des appareils
  • selon la documentation, cette mĂ©thode ne garantit pas l'ordre des certificats dans la collection retournĂ©e, et en consĂ©quence, il ne serait pas possible de boucler dans le mĂŞme ordre - et si l'application Ă©tait signĂ©e avec plus d'un certificat?
  • manque de connaissances sur la façon d'extraire le certificat de l'APK, Ă©tant donnĂ© qu'il n'est pas clair ce que fait Android Runtime dans ce cas

J'ai dû approfondir toutes ces questions et le résultat a été le suivant:

  • le certificat lui-mĂŞme se trouve dans le rĂ©pertoire original / META-INF / CERT.RSA

    dans ce répertoire, il n'y a qu'un seul fichier avec cette extension - ce qui signifie que l'application est signée avec un seul certificat
  • Sur le site sur l'ingĂ©nierie de recherche des applications Android, une liste a Ă©tĂ© trouvĂ©e qui peut extraire la signature dont nous avons besoin comme le fait Android. Selon l'auteur, au moins.

En exécutant ce code, je peux comprendre la signature, et en réalité, la clé dont nous avons besoin est une sous-chaîne. Allez-y. La solution simple n ° 2 est en train d'être balayée.

En effet, la clé est dans le certificat, il ne reste plus qu'à comprendre ce qui va suivre, car si nous avons la clé «utilisateur», nous obtenons tous également une signature nulle, et comme nous l'avons appris plus haut, c'est la mauvaise réponse.

Écrivez soigneusement la documentation!


De plus amples recherches sur le fait que les données saisies dans les champs de texte sont modifiées sont rejetées faute de preuves. La paranoïa roule avec une vigueur renouvelée: peut-être que le code qui a tiré la signature du certificat est incorrect ou est-ce une implémentation de code pour les anciennes versions d'Android? J'ouvre de nouveau la documentation et je vois ce qui suit: ( https://developer.android.com/reference/android/content/pm/Signature.html#toChars () ):



Remarque: la fonction code la signature sous forme de texte ASCII. Le résultat que j'ai reçu ci-dessus était une représentation hexadécimale des données. Cette API m'a paru étrange, mais si vous croyez à la documentation, il s'avère que j'ai de nouveau été bloqué, et la clé chiffrée n'est pas une sous-chaîne de la signature. Après m'être assis pensivement sur le code pendant un moment, je n'ai pas pu le supporter et j'ai ouvert le code source de cette classe. https://android.googlesource.com/platform/frameworks/base/+/e639da7/core/java/android/content/pm/Signature.java

La réponse ne tarda pas à venir. Et en fait, dans le code lui-même - une peinture à l'huile: le format de sortie est une chaîne hexadécimale ordinaire. Et maintenant, pensez: soit je ne comprends pas quelque chose, soit la documentation est écrite «légèrement» incorrectement. N'ayant aucunement grondé, je me mis à nouveau au travail.

Résumé


Les n heures suivantes se sont écoulées:

  • RecyclerView .. , StackOverflow
  • , , Java. , - («user») . .

, ( ).

Non. , . , , , , . . — , , , .

, , . . , . , - , , « », , .

- c – arturbrsg .

Stay tuned.

Source: https://habr.com/ru/post/fr450394/


All Articles