Comment j'ai découvert un œuf de Pâques dans la sécurité d'Android et que je n'ai pas décroché d'emploi chez Google

Google adore les œufs de Pâques. Il les aime tellement, en fait, que vous pouvez les trouver dans pratiquement tous leurs produits. La tradition des œufs de Pâques Android a commencé dans les toutes premières versions du système d'exploitation (je pense que tout le monde sait ce qui se passe lorsque vous accédez aux paramètres généraux et appuyez plusieurs fois sur le numéro de version).

Mais parfois, vous pouvez trouver un œuf de Pâques dans les endroits les plus improbables. Il y a même une légende urbaine qu'un jour, un programmeur a recherché «verrouillage mutex» sur Google, mais au lieu de résultats de recherche, il a atterri sur foo.bar, a résolu toutes les tâches et a décroché un emploi chez Google.

Reconstruction
image

La même chose (sauf sans la fin heureuse) m'est arrivée. Messages cachés là où il n'y en avait certainement pas, inversant le code Java et ses bibliothèques natives, une machine virtuelle secrète, une interview de Google - tout cela est ci-dessous.

Droidguard


Une nuit ennuyeuse, j'ai réinitialisé mon téléphone aux réglages d'usine et j'ai pu le configurer à nouveau. Tout d'abord, une nouvelle installation d'Android m'a demandé de me connecter au compte Google. Et je me suis demandé: comment fonctionne le processus de connexion à Android? Et la nuit est soudainement devenue moins ennuyeuse.

J'utilise la suite Burp de PortSwigger pour intercepter et analyser le trafic réseau. La version communautaire gratuite est suffisante pour nos besoins. Pour voir les demandes https, nous devons d'abord installer le certificat de PortSwigger sur l'appareil. En tant qu'appareil de test, j'ai choisi un Samsung Galaxy S de 8 ans avec Android 4.4. Quoi de plus récent que cela et vous pourriez avoir des problèmes avec l'épinglage de certificats et d'autres choses.

En toute honnêteté, il n'y a rien de particulièrement spécial avec les demandes de l'API Google. L'appareil envoie des informations sur lui-même et obtient des jetons en réponse ... La seule étape curieuse est une demande POST au service anti-abus.



Une fois la demande effectuée, parmi de nombreux paramètres très normaux, il en apparaît un intéressant, nommé droidguard_result . C'est une très longue chaîne Base64:



DroidGuard est le mécanisme de Google pour détecter les robots et les émulateurs parmi les vrais appareils. SafetyNet, par exemple, utilise également les données de DroidGuard. Google a également une chose similaire pour les navigateurs - Botguard.

Mais quelles sont ces données? Voyons.

Tampons de protocole


Qu'est-ce qui génère ce lien ( www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI ) et à l'intérieur d'Android qui fait cette demande? Après une courte enquête, il s'est avéré que le lien, sous cette forme exacte, se trouve dans l'une des classes obscures des services Google Play:

public bdd(Context var1, bdh var2) { this(var1, "https://www.googleapis.com/androidantiabuse/v1/x/create?alt=PROTO&key=AIzaSyBofcZsgLSS7BOnBjZPEkk4rYwzOIz-lTI", var2); } 

Comme nous l'avons déjà vu dans Burp, les requêtes POST sur ce lien ont Content-Type - application / x-protobuf (Google Protocol Buffers, le protocole de Google pour la sérialisation binaire). Ce n'est pas json, cependant - il est difficile de découvrir exactement ce qui est envoyé.

Les tampons de protocole fonctionnent comme ceci:

  • Nous décrivons d'abord la structure du message dans un format spécial et l'enregistrons dans un fichier .proto;
  • Ensuite, nous compilons des fichiers .proto, et le compilateur de protocole génère du code source dans une langue choisie (dans le cas d'Android, c'est Java);
  • Enfin, nous utilisons les classes générées dans notre projet.

Nous avons deux façons de décoder les messages protobuf. La première consiste à utiliser un analyseur de protobuf et à essayer de recréer la description originale des fichiers .proto. La seconde consiste à extraire les classes générées par le protocole des services Google Play, ce que j'ai décidé de faire.

Nous prenons le fichier .apk des services Google Play de la même version que celle installée sur l'appareil (ou, si l'appareil est enraciné, prenez simplement le fichier directement à partir de là). En utilisant dex2jar, nous reconvertissons le fichier .dex en .jar et ouvrons dans un décompilateur de choix. J'aime personnellement Fernflower de JetBrains. Il fonctionne comme un plugin pour IntelliJ IDEA (ou Android Studio), nous lançons donc simplement Android Studio et ouvrons le fichier avec le lien que nous essayons d'analyser. Si proguard n'essayait pas trop, le code Java décompilé pour créer des messages protobuf pourrait simplement être copié-collé dans votre projet.

En regardant le code décompilé, nous voyons que Build. * Les constantes sont envoyées à l'intérieur du message protobuf. (d'accord, ce n'était pas trop difficile à deviner).

 ... var3.a("4.0.33 (910055-30)"); a(var3, "BOARD", Build.BOARD); a(var3, "BOOTLOADER", Build.BOOTLOADER); a(var3, "BRAND", Build.BRAND); a(var3, "CPU_ABI", Build.CPU_ABI); a(var3, "CPU_ABI2", Build.CPU_ABI2); a(var3, "DEVICE", Build.DEVICE); ... 

Mais malheureusement, dans la réponse du serveur, tous les champs de protobuf se sont transformés en soupe à l'alphabet après l'obscurcissement. Mais nous pouvons découvrir ce qu'il contient en utilisant un gestionnaire d'erreurs. Voici comment les données provenant du serveur sont vérifiées:

 if (!var7.d()) { throw new bdf("byteCode"); } if (!var7.f()) { throw new bdf("vmUrl"); } if (!var7.h()) { throw new bdf("vmChecksum"); } if (!var7.j()) { throw new bdf("expiryTimeSecs"); } 

Apparemment, c'est ainsi que les champs étaient appelés avant l'obfuscation: byteCode , vmUrl , vmChecksum et expiryTimeSecs . Ce schéma de dénomination nous donne déjà quelques idées.

Nous combinons toutes les classes décompilées de Google Play Services dans un projet de test, les renommons, générons le test Build. * Commandes et lancons (imitant tout appareil que nous voulons). Si quelqu'un veut le faire lui-même, voici le lien vers mon GitHub .

Si la demande est correcte, le serveur renvoie ceci:
00: 06: 26.761 [principal] INFO daresponse.AntiabuseResponse - octet Taille du code: 34446
00: 06: 26.761 [principal] INFO daresponse.AntiabuseResponse - vmChecksum: C15E93CCFD9EF178293A2334A1C9F9B08F115993
00: 06: 26.761 [principal] INFO daresponse.AntiabuseResponse - vmUrl: www.gstatic.com/droidguard/C15E93CCFD9EF178293A2334A1C9F9B08F115993
00: 06: 26.761 [principal] INFO daresponse.AntiabuseResponse - expiryTimeSecs: 10

Étape 1 terminée. Voyons maintenant ce qui se cache derrière le lien vmUrl .

Secret APK


Le lien nous mène directement à un fichier .apk, nommé d'après son propre hachage SHA-1. C'est plutôt minuscule - seulement 150 Ko. Et c'est tout à fait justifié: s'il est téléchargé par chacun des 2 milliards d'appareils Android, cela représente 270 To de trafic sur les services de Google.



DroidGuardService classe DroidGuardService , faisant partie des services Google Play, télécharge le fichier sur l'appareil, le décompresse, extrait .dex et utilise la classe com.google.ccc.abuse.droidguard.DroidGuard par réflexion. En cas d'erreur, DroidGuardService de DroidGuard à Droidguasso. Mais c'est une autre histoire entièrement.

Essentiellement, la classe DroidGuard est un simple wrapper JNI autour de la bibliothèque native .so. L'ABI de la bibliothèque native correspond à ce que nous avons envoyé dans le champ CPU_ABI dans la requête protobuf: nous pouvons demander armeabi, x86 ou même MIPS.

Le service DroidGuardService lui-même ne contient aucune logique intéressante pour travailler avec la classe DroidGuard . Il crée simplement une nouvelle instance de DroidGuard , lui envoie le byteCode à partir du message protobuf, appelle une méthode publique, qui retourne un tableau d'octets. Ce tableau est ensuite envoyé au serveur à l'intérieur du paramètre droidguard_result .

Pour avoir une idée approximative de ce qui se passe à l'intérieur de DroidGuard nous pouvons répéter la logique de DroidGuardService (mais sans télécharger le .apk, car nous avons déjà la bibliothèque native). Nous pouvons prendre un fichier .dex de l'APK secret, le convertir en .jar et ensuite l'utiliser dans notre projet. Le seul problème est de savoir comment la classe DroidGuard charge la bibliothèque native. Le bloc d'initialisation statique appelle la méthode loadDroidGuardLibrary() :

 static { try { loadDroidGuardLibrary(); } catch (Exception ex) { throw new RuntimeException(ex); } } 

Ensuite, la méthode loadDroidGuardLibrary() lit library.txt (située à la racine du fichier .apk) et charge la bibliothèque avec ce nom System.load(String filename) l' System.load(String filename) . Pas très pratique pour nous, car nous aurions besoin de construire le .apk d'une manière très spécifique pour mettre library.txt et le fichier .so dans sa racine. Il serait beaucoup plus pratique de conserver le fichier .so dans le dossier lib et de le charger via System.loadLibrary(String libname) .

Ce n'est pas difficile à faire. Nous utiliserons smali / baksmali - assembleur / désassembleur pour les fichiers .dex. Après l'avoir utilisé, classes.dex se transforme en un tas de fichiers .smali. La classe com.google.ccc.abuse.droidguard.DroidGuard doit être modifiée de sorte que le bloc d'initialisation statique appelle la System.loadLibrary("droidguard") au lieu de loadDroidGuardLibrary() . La syntaxe de Smali est assez simple, le nouveau bloc d'initialisation ressemble à ceci:

 .method static constructor <clinit>()V .locals 1 const-string v0, "droidguard" invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V return-void .end method 

Ensuite, nous utilisons backsmali pour tout reconstruire en .dex, puis nous le convertissons en .jar. À la fin, nous obtenons un fichier .jar que nous pouvons utiliser dans notre projet - le voici d'ailleurs.

Toute la section liée à DroidGuard est longue de quelques chaînes. La partie la plus importante est de télécharger le tableau d'octets que nous avons obtenu à l'étape précédente après avoir adressé le service anti-abus et de le remettre au constructeur DroidGuard :

 private fun runDroidguard() { var byteCode: ByteArray? = loadBytecode("bytecode.base64"); byteCode?.let { val droidguard = DroidGuard(applicationContext, "addAccount", it) val params = mapOf("dg_email" to "test@gmail.com", "dg_gmsCoreVersion" to "910055-30", "dg_package" to "com.google.android.gms", "dg_androidId" to UUID.randomUUID().toString()) droidguard.init() val result = droidguard.ss(params) droidguard.close() } } 

Maintenant, nous pouvons utiliser le profileur d'Android Studio et voir ce qui se passe pendant le travail de DroidGuard:



La méthode native initNative () collecte des données sur le périphérique et appelle les méthodes Java hasSystemFeature(), getMemoryInfo(), getPackageInfo() ... c'est quelque chose, mais je ne vois toujours pas de logique solide. Eh bien, il ne reste plus qu'à démonter le fichier .so.

libdroidguard.so


Heureusement, l'analyse de la bibliothèque native n'est pas plus difficile que de le faire avec des fichiers .dex et .jar. Nous aurions besoin d'une application similaire à Hex-Rays IDA et d'une certaine connaissance du code assembleur x86 ou ARM. J'ai choisi ARM, car j'avais un appareil enraciné à déboguer. Si vous n'en avez pas, vous pouvez prendre une bibliothèque x86 et déboguer à l'aide d'un émulateur.

Une application similaire à Hex-Rays IDA décompile le binaire en quelque chose qui ressemble au code C. Si nous ouvrons la méthode Java_com_google_ccc_abuse_droidguard_DroidGuard_ssNative , nous verrons quelque chose comme ceci:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9) ... v14 = (*(_DWORD *)v9 + 684))(v9, a5); v15 = (*(_DWORD *)v9 + 736))(v9, a5, 0); ... 

Ne semble pas trop prometteur. Nous devons d'abord faire quelques étapes préliminaires pour transformer cela en quelque chose de plus utile. Le décompilateur ne sait rien de JNI, nous installons donc Android NDK et importons le fichier jni.h. Comme nous le savons, les deux premiers paramètres d'une méthode JNI sont JNIEnv* et jobject (this) . Nous pouvons découvrir les types d'autres paramètres à partir du code Java de DroidGuard. Après avoir attribué les types corrects, les décalages sans signification se transforment en appels de méthode JNI:

 __int64 __fastcall Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative(_JNIEnv *env, jobject thiz, jobject context, jstring flow, jbyteArray byteCode, jobject runtimeApi, jobject extras, jint loggingFd, int runningInAppSide) { ... programLength = _env->functions->GetArrayLength)(_env, byteCode); programBytes = (jbyte *)_env->functions->GetByteArrayElements)(_env, byteCode, 0); ... 

Si nous avons assez de patience pour retracer le tableau d'octets reçu du serveur anti-abus, nous serons ... déçus. Malheureusement, il n'y a pas de réponse simple à "que se passe-t-il ici?". C'est du code d'octets pur et distillé, et la bibliothèque native est une machine virtuelle. Un chiffrement AES saupoudré sur le dessus, puis la machine virtuelle lit le code octet, octet par octet, et exécute des commandes. Chaque octet est une commande suivie d'opérandes. Il n'y a pas beaucoup de commandes, seulement environ 70: lire int, lire octet, lire chaîne, appeler méthode Java, multiplier deux nombres, if-goto etc.

Réveillez-vous néo


J'ai décidé d'aller encore plus loin et de comprendre la structure du code d'octet pour cette machine virtuelle. Il y a un autre problème avec les appels: parfois (une fois toutes les deux semaines) il y a une nouvelle version de la bibliothèque native où les paires octet-commande sont brouillées. Cela ne m'a pas arrêté et j'ai décidé de recréer la VM en utilisant Java.

Le code d'octet fait tout le travail de routine sur la collecte d'informations sur le périphérique. Par exemple, il charge une chaîne avec le nom d'une méthode, obtient son adresse via dlsym et s'exécute. Dans ma version Java de la machine virtuelle, j'ai recréé seulement 5 méthodes environ et j'ai appris à interpréter les 25 premières commandes du code d'octet du service anti-abus. Lors de la 26e commande, la machine virtuelle a lu une autre chaîne chiffrée à partir du code d'octet. Il s'est avéré que ce n'était pas le nom d'une autre méthode. Loin de là.
Commande de machine virtuelle # 26
Invocation de méthode vm-> vm_method_table [2 * 0x77]
Méthode vmMethod_readString
l'index est 0x9d
la longueur de la chaîne est 0x0066
(une nouvelle clé est générée)
les octets de chaîne codés sont EB 4E E6 DC 34 13 35 4A DD 55 B3 91 33 05 61 04 C0 54 FD 95 2F 18 72 04 C1 55 E1 92 28 11 66 04 DD 4F B3 94 33 04 35 0A C1 4E B2 DB 12 17 79 4F 92 55 FC DB 33 05 35 45 C6 01 F7 89 29 1F 71 43 C7 40 E1 9F 6B 1E 70 48 DE 4E B8 CD 75 44 23 14 85 14 A7 C2 7F 40 26 42 84 17 A2 BB 21 19 7A 43 DE 44 BD 98 29 1B
les octets de chaîne décodés sont 59 6F 75 27 72 65 20 6E 6F 74 20 6A 75 73 74 20 72 75 6E 6E 69 6E 67 20 73 74 72 69 6E 67 73 20 6F 6E 20 6F 75 72 20 2E 73 6F 21 20 54 61 6C 6B 20 74 6F 20 75 73 20 61 74 20 64 72 6F 69 64 67 75 61 72 64 2D 68 65 6C 6C 6F 2B 36 33 32 36 30 37 35 34 39 39 36 33 66 36 36 31 40 67 6F 6F 67 6C 65 2E 63 6F 6D
la valeur de chaîne décodée est ( Vous n'êtes pas en train d'exécuter des chaînes sur notre .so! Parlez-nous à droidguard@google.com )
C'est étrange. Une machine virtuelle ne m'a jamais parlé auparavant. Je pensais que si vous commencez à voir des messages secrets dirigés vers vous, vous devenez fou. Juste pour m'assurer que j'étais toujours sain d'esprit, j'ai exécuté quelques centaines de réponses différentes du service anti-abus via ma machine virtuelle. Littéralement toutes les 25 à 30 commandes, un message était caché dans le code d'octet. Ils ont souvent répété, mais voici quelques exemples uniques. J'ai cependant modifié les adresses e-mail: chaque message avait une adresse différente, quelque chose comme "droidguard+tag@google.com", la balise étant unique pour chacun.
droidguard@google.com: ne soyez pas un étranger!
Vous êtes entré! Parlez-nous à droidguard@google.com
Salutations de droidguard@google.com voyageur intrépide! Dites bonjour!
Était-ce facile de trouver ça? droidguard@google.com aimerait savoir
Les gens de droidguard@google.com apprécieraient d'avoir de vos nouvelles!
Quel est tout ce charabia? Demandez à droidguard@google.com ... ils sauraient!
Hé! Envie de vous voir ici. Avez-vous déjà parlé à droidguard@google.com?
Vous ne faites pas que courir des cordes sur notre .so! Parlez-nous à droidguard@google.com
Suis-je l'élu? Je pensais qu'il était temps d'arrêter de jouer avec DroidGuard et de parler à Google, car ils me l'avaient demandé.

Votre appel est très important pour nous


J'ai raconté mes découvertes sur l'e-mail que j'ai trouvé. Pour rendre les résultats un peu plus impressionnants, j'ai automatisé un peu le processus d'analyse. Le fait est que les chaînes et les tableaux d'octets sont stockés dans le code d'octets crypté. La machine virtuelle les décode à l'aide de constantes intégrées par le compilateur. En utilisant une application similaire à Hex-Rays IDA, vous pouvez les extraire assez facilement. Mais à chaque nouvelle version, les constantes changent et il est assez peu pratique de toujours les extraire manuellement.

Mais l'analyse Java de la bibliothèque native s'est avérée étonnamment simple. En utilisant jelf (une bibliothèque pour analyser les fichiers ELF), nous trouvons l'offset de la méthode Java_com_google_ccc_abuse_droidguard_DroidGuard_initNative dans le binaire, puis en utilisant Capstone (un cadre de démontage avec des liaisons pour diverses langues, y compris Java), nous obtenons le code assembleur et le recherchons pour charger des constantes dans registres.

À la fin, j'ai obtenu une application qui émulait l'ensemble du processus DroidGuard: fait une demande au service anti-abus, télécharge le .apk, le décompresse, analyse la bibliothèque native, extrait les constantes requises, sélectionne le mappage des commandes VM et interprète le code d'octet. J'ai tout compilé et envoyé à Google. Pendant que j'y étais, j'ai commencé à préparer un déménagement et j'ai cherché sur Glassdoor pour un salaire moyen chez Google. J'ai décidé de ne pas accepter moins de six chiffres.

La réponse ne tarda pas. Un e-mail d'un membre de l'équipe DroidGuard disait simplement: "Pourquoi faites-vous cela?"



«Parce que je peux» - ai-je répondu. Un employé de Google m'a expliqué que DroidGuard est censé protéger Android contre les pirates (vous ne le dites pas!) Et il serait sage de garder pour moi le code source de ma machine virtuelle DroidGuard. Notre conversation s'est terminée là.

Interview


Un mois plus tard, j'ai reçu un autre e-mail. L'équipe DroidGuard de Zurich avait besoin d'un nouvel employé. Étais-je intéressé à rejoindre? Bien sûr!

Il n'y a pas de raccourci pour accéder à Google. Tout ce que je pouvais faire était de transmettre mon CV au service RH. Après cela, j'ai dû passer par le marasme bureaucratique habituel et une série d'entretiens.

Il y a beaucoup d'histoires sur les interviews de Google. Les algorithmes, les tâches de l'Olympiade et la programmation de Google Docs ne sont pas mon truc, alors j'ai commencé mes préparatifs. J'ai lu le cours «Algorithmes» de Coursera des dizaines de fois, résolu des centaines de tâches sur Hackerrank et appris à contourner un graphique dans les deux dimensions les yeux fermés.

Deux mois se sont écoulés. Dire que je me sentais préparé serait un euphémisme. Google Docs est devenu mon IDE préféré. J'avais l'impression de tout savoir sur les algorithmes. Bien sûr, je connaissais mes faiblesses et j'ai réalisé que je ne passerais probablement pas la série de 5 interviews à Zurich, mais aller au Disneyland du programmeur gratuitement était une récompense en soi. La première étape a été un entretien téléphonique pour éliminer les candidats les plus faibles et ne pas perdre le temps des développeurs zurichois lors de réunions en personne. Le jour était fixé, le téléphone sonna ...



... et j'ai immédiatement échoué à mon premier test. J'ai eu de la chance - ils ont posé une question que j'ai déjà vue sur Internet et que j'ai déjà résolue. Il s'agissait de sérialiser un tableau de chaînes. J'ai proposé de coder des chaînes en Base64 et de les enregistrer via un diviseur. L'intervieweur m'a demandé de développer un algorithme Base64. Après cela, l'interview s'est transformée en une sorte de monologue, où l'interviewé m'a expliqué comment fonctionne Base64 et j'ai essayé de me souvenir des opérations sur les bits en Java.

Si quelqu'un chez Google lit ceci
Les gars, vous êtes des génies sanglants si vous y êtes! Sérieusement. Je ne peux pas imaginer comment on peut franchir tous les obstacles qu'ils mettent devant vous.

3 jours après l'appel, j'ai reçu un e-mail me disant qu'ils ne voulaient plus m'interviewer. Et c'est ainsi que ma communication avec Google s'est terminée.

Pourquoi il y a des messages dans DroidGuard demandant à discuter, je n'en ai toujours aucune idée. Probablement juste pour les statistiques. Le gars à qui j'ai écrit en premier m'a dit que les gens y écrivaient, mais la fréquence varie: parfois ils obtiennent 3 réponses en une semaine, parfois 1 par an.

Je pense qu'il existe des moyens plus faciles d'obtenir une interview chez Google. Après tout, vous pouvez simplement demander à l'un des 100 000 employés (bien que tous ne soient pas des développeurs, certes). Mais c'était quand même une expérience amusante.

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


All Articles