Briser le navigateur UC



Présentation


Fin mars, nous avons signalé le potentiel caché de téléchargement et d'exécution de code non vérifié dans UC Browser. Aujourd'hui, nous examinerons en détail comment cela se produit et comment les pirates peuvent l'utiliser.

Il y a quelque temps, UC Browser a été promu et distribué de manière assez agressive. Il a été installé sur des appareils par des logiciels malveillants, distribué via des sites Web sous couvert de fichiers vidéo (c'est-à-dire que les utilisateurs pensaient télécharger de la pornographie ou quelque chose, mais obtenaient des fichiers APK avec ce navigateur), annoncés à l'aide de bannières inquiétantes sur le navigateur d'un utilisateur obsolète ou vulnérable. Le groupe officiel UC Browser VK avait un sujet sur lequel les utilisateurs pouvaient se plaindre de publicité mensongère et de nombreux utilisateurs ont fourni des exemples. En 2016, il y avait même une publicité en russe (oui, une publicité d'un navigateur qui bloque les publicités).

Au moment où nous écrivons cet article, UC Browser a été installé 500 000 000 fois depuis Google Play. C'est impressionnant puisque seul Google Chrome a réussi à dépasser cela. Parmi les avis, vous pouvez voir de nombreuses plaintes d'utilisateurs concernant la publicité et la redirection vers d'autres applications sur Google Play. C'était la raison de notre étude: nous voulions voir si UC Browser faisait quelque chose de mal. Et ça l'est! L'application est capable de télécharger et d'exécuter du code exécutable, ce qui viole la politique de Google Play pour la publication d'applications . Et UC Browser ne télécharge pas seulement du code exécutable; il le fait en toute sécurité, ce qui peut être utilisé pour une attaque MitM. Voyons voir si nous pouvons l'utiliser de cette façon.

Tout ce qui suit s'applique à la version d'UC Browser qui a été distribuée via Google Play au moment de notre étude:

package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-file: f5edb2243413c777172f6362876041eb0c3a928c 

Vecteur d'attaque


Le manifeste UC Browser contient un service avec un nom révélateur com.uc.deployment.UpgradeDeployService .

  <service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" /> 

Lorsque ce service est lancé, le navigateur envoie une demande POST à puds.ucweb.com/upgrade/index.xhtml qui peut être vu dans le trafic pendant un certain temps après le lancement. En réponse, le navigateur peut recevoir une commande pour télécharger toute mise à jour ou un nouveau module. Au cours de notre analyse, nous n'avons jamais reçu de telles commandes du serveur, mais nous avons remarqué qu'en essayant d'ouvrir un fichier PDF dans le navigateur, il répète la demande à l'adresse ci-dessus, puis télécharge une bibliothèque native. Pour simuler une attaque, nous avons décidé d'utiliser cette fonctionnalité de UC Browser - la possibilité d'ouvrir des fichiers PDF à l'aide d'une bibliothèque native - non présente dans le fichier APK, mais téléchargeable sur Internet. Techniquement, UC Browser peut télécharger quelque chose sans l'autorisation d'un utilisateur lorsqu'il reçoit une réponse appropriée à une demande envoyée au démarrage. Mais pour cela, nous devons étudier le protocole d'interaction avec le serveur plus en détail, nous avons donc pensé qu'il était plus facile de simplement accrocher et modifier la réponse, puis de remplacer la bibliothèque nécessaire pour ouvrir les fichiers PDF par quelque chose de différent.

Ainsi, lorsqu'un utilisateur souhaite ouvrir un fichier PDF directement dans le navigateur, le trafic peut contenir les demandes suivantes:



Tout d'abord, il y a une demande POST à puds.ucweb.com/upgrade/index.xhtml , puis la bibliothèque compressée pour visualiser les fichiers PDF et les documents bureautiques est téléchargée. Logiquement, nous pouvons supposer que la première demande envoie des informations sur le système (au moins l'architecture, car le serveur doit sélectionner une bibliothèque appropriée), et le serveur répond avec quelques informations sur la bibliothèque à télécharger, comme son adresse et peut-être autre chose. Le problème est que cette demande est cryptée.

Fragment de demande

Fragment de réponse






La bibliothèque est compressée dans un fichier ZIP et non cryptée.



Recherche de code de déchiffrement du trafic


Essayons de décrypter la réponse du serveur. Jetez un œil au code de la classe com.uc.deployment.UpgradeDeployService : à partir de la méthode onStartCommand , nous accédons à com.uc.deployment.bx , puis à com.uc.browser.core.dcfe :

  public final void e(l arg9) { int v4_5; String v3_1; byte[] v3; byte[] v1 = null; if(arg9 == null) { v3 = v1; } else { v3_1 = arg9.iGX.ipR; StringBuilder v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]product:"); v4.append(arg9.iGX.ipR); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]version:"); v4.append(arg9.iGX.iEn); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]upgrade_type:"); v4.append(arg9.iGX.mMode); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]force_flag:"); v4.append(arg9.iGX.iEo); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_mode:"); v4.append(arg9.iGX.iDQ); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_type:"); v4.append(arg9.iGX.iEr); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_state:"); v4.append(arg9.iGX.iEp); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_file:"); v4.append(arg9.iGX.iEq); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apk_md5:"); v4.append(arg9.iGX.iEl); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_type:"); v4.append(arg9.mDownloadType); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_group:"); v4.append(arg9.mDownloadGroup); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_path:"); v4.append(arg9.iGH); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_child_version:"); v4.append(arg9.iGX.iEx); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_series:"); v4.append(arg9.iGX.iEw); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_arch:"); v4.append(arg9.iGX.iEt); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp3:"); v4.append(arg9.iGX.iEv); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp:"); v4.append(arg9.iGX.iEu); ArrayList v3_2 = arg9.iGX.iEz; if(v3_2 != null && v3_2.size() != 0) { Iterator v3_3 = v3_2.iterator(); while(v3_3.hasNext()) { Object v4_1 = v3_3.next(); StringBuilder v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_name:"); v5.append(((au)v4_1).getName()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_name:"); v5.append(((au)v4_1).aDA()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_code:"); v5.append(((au)v4_1).gBl); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_req_type:"); v5.append(((au)v4_1).gBq); } } j v3_4 = new j(); mb(v3_4); h v4_2 = new h(); mb(v4_2); ay v5_1 = new ay(); v3_4.hS(""); v3_4.setImsi(""); v3_4.hV(""); v5_1.bPQ = v3_4; v5_1.bPP = v4_2; v5_1.yr(arg9.iGX.ipR); v5_1.gBF = arg9.iGX.mMode; v5_1.gBI = arg9.iGX.iEz; v3_2 = v5_1.gAr; c.aBh(); v3_2.add(g.fs("os_ver", c.getRomInfo())); v3_2.add(g.fs("processor_arch", com.uc.baacgetCpuArch())); v3_2.add(g.fs("cpu_arch", com.uc.baacPb())); String v4_3 = com.uc.baacPd(); v3_2.add(g.fs("cpu_vfp", v4_3)); v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo()))); v3_2.add(g.fs("fromhost", arg9.iGX.iEm)); v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn)); v3_2.add(g.fs("target_lang", arg9.iGX.iEs)); v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt)); v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu)); v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv)); v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx)); v3_2.add(g.fs("ver_series", arg9.iGX.iEw)); v3_2.add(g.fs("child_ver", r.aVw())); v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl)); v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature())); v3_2.add(g.fs("upgrade_log", i.bjt())); v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ))); v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp))); v3_2.add(g.fs("silent_file", arg9.iGX.iEq)); v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr))); v3_2.add(g.fs("cpu_archit", com.uc.baacPc())); v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction())); boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true; v3_2.add(g.fs("neon", String.valueOf(v4_4))); v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.baacJl()))); v3_2.add(g.fs("ram_1", String.valueOf(com.uc.baahPo()))); v3_2.add(g.fs("totalram", String.valueOf(com.uc.baahOL()))); c.aBh(); v3_2.add(g.fs("rom_1", c.getRomInfo())); v4_5 = e.getScreenWidth(); int v6 = e.getScreenHeight(); StringBuilder v7 = new StringBuilder(); v7.append(v4_5); v7.append("*"); v7.append(v6); v3_2.add(g.fs("ss", v7.toString())); v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT))); v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks())); Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator(); while(v4_6.hasNext()) { Object v6_1 = v4_6.next(); v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue())); } v3 = v5_1.toByteArray(); } if(v3 == null) { this.iGY.iGI.a(arg9, "up_encode", "yes", "fail"); return; } v4_5 = this.iGY.iGw ? 0x1F : 0; if(v3 == null) { } else { v3 = gi(v4_5, v3); if(v3 == null) { } else { v1 = new byte[v3.length + 16]; byte[] v6_2 = new byte[16]; Arrays.fill(v6_2, 0); v6_2[0] = 0x5F; v6_2[1] = 0; v6_2[2] = ((byte)v4_5); v6_2[3] = -50; System.arraycopy(v6_2, 0, v1, 0, 16); System.arraycopy(v3, 0, v1, 16, v3.length); } } if(v1 == null) { this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail"); return; } if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) { this.iGY.iGI.a(arg9, "up_url", "yes", "fail"); return; } StringBuilder v0 = new StringBuilder("["); v0.append(arg9.iGX.ipR); v0.append("]url:"); v0.append(this.iGY.mUpgradeUrl); com.uc.browser.core.dci v0_1 = this.iGY.iGI; v3_1 = this.iGY.mUpgradeUrl; com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.dci$a(v0_1, arg9)); v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb"; n v3_5 = v0_2.uc(v3_1); mb(v3_5, false); v3_5.setMethod("POST"); v3_5.setBodyProvider(v1); v0_2.b(v3_5); this.iGY.iGI.a(arg9, "up_null", "yes", "success"); this.iGY.iGI.b(arg9); } 

Nous pouvons voir que c'est là que la demande POST est faite. Jetez un œil au tableau de 16 octets qui contient: 0x5F, 0, 0x1F, -50 (= 0xCE). Les valeurs sont les mêmes que dans la demande ci-dessus.

La même classe contient une classe imbriquée avec une autre méthode intéressante:

  public final void a(l arg10, byte[] arg11) { f v0 = this.iGQ; StringBuilder v1 = new StringBuilder("["); v1.append(arg10.iGX.ipR); v1.append("]:UpgradeSuccess"); byte[] v1_1 = null; if(arg11 == null) { } else if(arg11.length < 16) { } else { if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) { goto label_57; } int v3 = 1; int v5 = arg11[1] == 1 ? 1 : 0; if(arg11[2] != 1 && arg11[2] != 11) { if(arg11[2] == 0x1F) { } else { v3 = 0; } } byte[] v7 = new byte[arg11.length - 16]; System.arraycopy(arg11, 16, v7, 0, v7.length); if(v3 != 0) { v7 = gj(arg11[2], v7); } if(v7 == null) { goto label_57; } if(v5 != 0) { v1_1 = gP(v7); goto label_57; } v1_1 = v7; } label_57: if(v1_1 == null) { v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail"); return; } q v11 = gb(arg10, v1_1); if(v11 == null) { v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail"); return; } if(v0.iGY.iGt) { v0.d(arg10); } if(v0.iGY.iGo != null) { v0.iGY.iGo.a(0, ((o)v11)); } if(v0.iGY.iGs) { v0.iGY.a(((o)v11)); v0.iGY.iGI.a(v11, "up_silent", "yes", "success"); v0.iGY.iGI.a(v11); return; } v0.iGY.iGI.a(v11, "up_silent", "no", "success"); } } 

Cette méthode reçoit une entrée d'un tableau d'octets et vérifie si l'octet zéro est 0x60, ou le troisième octet est 0xD0 et si le deuxième octet est 1, 11 ou 0x1F. Consultez la réponse du serveur: zéro octet est 0x60, le deuxième octet est 0x1F, le troisième octet est 0x60. Cela ressemble à ce dont nous avons besoin. A en juger par les chaînes ("up_decrypt", par exemple), une méthode est censée être appelée ici pour décrypter la réponse du serveur. Voyons maintenant la méthode gj. Notez que le premier argument est un octet à l'offset 2 (c'est-à-dire 0x1F dans notre cas), et le second est la réponse du serveur sans les 16 premiers octets.

  public static byte[] j(int arg1, byte[] arg2) { if(arg1 == 1) { arg2 = cc(arg2, c.adu); } else if(arg1 == 11) { arg2 = m.aF(arg2); } else if(arg1 != 0x1F) { } else { arg2 = EncryptHelper.decrypt(arg2); } return arg2; } 

Évidemment, c'est la sélection de l'algorithme de déchiffrement, et l'octet qui dans notre cas est égal à 0x1F indique l'une des trois options possibles.

Revenons à l'analyse du code. Après quelques sauts, nous arrivons à la méthode avec le nom révélateur, decryptBytesByKey . Maintenant, deux octets supplémentaires sont séparés de notre réponse et forment une chaîne. Il est clair que c'est ainsi que la clé est sélectionnée pour déchiffrer le message.

  private static byte[] decryptBytesByKey(byte[] bytes) { byte[] v0 = null; if(bytes != null) { try { if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) { } else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) { return v0; } else { byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE]; // 2  System.arraycopy(bytes, 0, prefix, 0, prefix.length); String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); //   if(keyId == null) { return v0; } else { a v2 = EncryptHelper.ayL(); if(v2 == null) { return v0; } else { byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE]; System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length); return v2.l(keyId, enrypted); } } } } catch(SecException v7_1) { EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode()); return v0; } catch(Throwable v7) { EncryptHelper.handleDecryptException(v7, 2); return v0; } } return v0; } 

Sautant un peu, notez qu'à ce stade, ce n'est que l'identifiant de la clé, pas la clé elle-même. La sélection des clés va être un peu plus compliquée.

Dans la méthode suivante, deux paramètres supplémentaires sont ajoutés aux paramètres existants, nous obtenons donc un total de quatre. Le numéro magique 16, l'identifiant de la clé, les données chiffrées et une chaîne y sont ajoutés pour une raison quelconque (vide dans notre cas).

  public final byte[] l(String keyId, byte[] encrypted) throws SecException { return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, ""); } 

Après une série de sauts, nous voyons la méthode staticBinarySafeDecryptNoB64 de l'interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . Le code d'application principal n'a pas de classes qui implémentent cette interface. Cette classe est contenue dans le fichier lib / armeabi-v7a / libsgmain.so , qui n'est pas vraiment .SO, mais plutôt .JAR. La méthode qui nous intéresse est implémentée comme suit:

 package com.alibaba.wireless.security.ai; // ... public class a implements IStaticDataEncryptComponent { private ISecurityGuardPlugin a; // ... private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) { return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString}); } // ... private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) { return this.a(2, magicInt, 0, keyId, encrypted, magicString); } // ... public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException { if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt < 19 && encrypted != null && encrypted.length > 0) { return this.b(magicInt, keyId, encrypted, magicString); } throw new SecException("", 301); } //... } 

Ici, notre liste de paramètres est complétée par deux autres entiers: 2 et 0. Apparemment, 2 signifie le décryptage, comme dans la méthode doFinal de la classe système javax.crypto.Cipher . Ensuite, ces informations sont transmises à un certain routeur avec le numéro 10601, qui est apparemment le numéro de commande.

Après la prochaine chaîne de sauts, nous trouvons une classe qui implémente l'interface RouterComponent et la méthode doCommand :

 package com.alibaba.wireless.security.mainplugin; import com.alibaba.wireless.security.framework.IRouterComponent; import com.taobao.wireless.security.adapter.JNICLibrary; public class a implements IRouterComponent { public a() { super(); } public Object doCommand(int arg2, Object[] arg3) { return JNICLibrary.doCommandNative(arg2, arg3); } } 

Il existe également la classe JNIC Library , où la méthode native doCommandNative est déclarée:

 package com.taobao.wireless.security.adapter; public class JNICLibrary { public static native Object doCommandNative(int arg0, Object[] arg1); } 

Nous devons donc trouver la méthode doCommandNative dans le code natif. C'est là que le plaisir commence.

Obscurcissement du code machine


Il y a une bibliothèque native dans le fichier libsgmain.so (qui est en fait un fichier .JAR et, comme nous l'avons dit ci-dessus, implémente certaines interfaces liées au chiffrement): libsgmainso-6.4.36.so . Nous le chargeons dans IDA et obtenons un tas de dialogues avec des messages d'erreur. Le problème est que la table d'en-tête de section n'est pas valide. Ceci est fait exprès pour compliquer l'analyse.



Mais nous n'en avons pas vraiment besoin de toute façon. La table d'en-tête du programme suffit pour charger correctement le fichier ELF et l'analyser. Nous supprimons donc simplement la table d'en-tête de section, en annulant les champs correspondants dans l'en-tête.



Ensuite, nous ouvrons à nouveau le fichier dans IDA.

Nous avons deux façons de dire à la machine virtuelle Java exactement où la bibliothèque native contient l'implémentation de la méthode déclarée native dans le code Java. La première consiste à lui donner un nom comme celui-ci: Java_package_name_ClassName_methodName . La seconde consiste à l'enregistrer lors du chargement de la bibliothèque (dans la fonction JNI_OnLoad) en appelant la fonction RegisterNatives. Dans notre cas, si vous utilisez la première méthode, le nom doit être comme ceci: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . La liste des fonctions exportées ne contient pas ce nom, ce qui signifie que nous devons rechercher les RegisterNatives. Ainsi, nous allons à la fonction JNI_OnLoad et voyons ce qui suit:



Que se passe-t-il ici? À première vue, le début et la fin de la fonction sont typiques de l'architecture ARM. La première instruction envoie le contenu des registres que la fonction utilisera à la pile (dans ce cas, R0, R1 et R2), ainsi que le contenu du registre LR avec l'adresse de retour de la fonction. La dernière instruction restaure les registres sauvegardés et place l'adresse de retour dans le registre PC, revenant ainsi de la fonction. Mais si nous y regardons de plus près, nous pouvons remarquer que l'avant-dernière instruction modifie l'adresse de retour, stockée sur la pile. Calculons ce que ce sera lorsque le code sera exécuté. L'adresse 0xB130 se charge dans R1, en a soustrait 5, puis est déplacée vers R0 et reçoit un ajout de 0x10. Au final, il est égal à 0xB13B. Ainsi, IDA pense que l'instruction finale effectue un retour de fonction normal, alors qu'en fait, elle effectue un transfert vers l'adresse calculée 0xB13B.

Maintenant, rappelons-nous que les processeurs ARM ont deux modes et deux ensembles d'instructions - ARM et Thumb. Le bit de poids faible de l'adresse détermine le jeu d'instructions que le processeur utilisera. En d'autres termes, l'adresse est en fait 0xB13A, tandis que la valeur du bit de poids faible indique le mode Thumb.
Un «adaptateur» similaire et quelques ordures sémantiques sont ajoutés au début de chaque fonction de cette bibliothèque. Mais nous ne nous attarderons pas sur eux en détail. N'oubliez pas que le véritable début de presque toutes les fonctions est un peu plus loin.

Puisqu'aucune transition explicite vers 0xB13A dans le code n'existe, IDA ne peut pas reconnaître qu'il y a du code. Pour la même raison, il ne reconnaît pas la plupart du code de la bibliothèque comme du code, ce qui rend l'analyse un peu plus délicate. Donc, nous disons à l'IDA qu'il y a du code, et c'est ce qui se passe:



À partir de 0xB144, nous avons clairement le tableau. Mais qu'en est-il du sub_494C?



Lors de l'appel de cette fonction dans le registre LR, nous obtenons l'adresse de la table mentionnée ci-dessus (0xB144). R0 contient l'index dans ce tableau. Autrement dit, nous prenons la valeur de la table, l'ajoutons à LR et obtenons l'adresse à laquelle nous devons aller. Essayons de le calculer: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Nous accédons à cette adresse et voyons quelques instructions utiles, puis passons à 0xB140:



Maintenant, il y aura une transition à l'offset avec l'index 0x20 de la table.
A en juger par la taille de la table, il y aura beaucoup de telles transitions dans le code. Nous voudrions donc traiter cela automatiquement et éviter de calculer manuellement les adresses. Ainsi, les scripts et la possibilité de patcher du code dans IDA viennent à notre secours:

 def put_unconditional_branch(source, destination): offset = (destination - source - 4) >> 1 if offset > 2097151 or offset < -2097152: raise RuntimeError("Invalid offset") if offset > 1023 or offset < -1024: instruction1 = 0xf000 | ((offset >> 11) & 0x7ff) instruction2 = 0xb800 | (offset & 0x7ff) patch_word(source, instruction1) patch_word(source + 2, instruction2) else: instruction = 0xe000 | (offset & 0x7ff) patch_word(source, instruction) ea = here() if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR} ea1 = ea + 2 if get_wide_word(ea1) == 0xbf00: #NOP ea1 += 2 if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2: index = get_wide_dword(get_operand_value(ea1, 1)) print "index =", hex(index) ea1 += 2 if get_operand_type(ea1, 0) == 7: table = get_operand_value(ea1, 0) + 4 elif get_operand_type(ea1, 1) == 2: table = get_operand_value(ea1, 1) + 4 else: print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1) table = None if table is None: print "Unable to find table" else: print "table =", hex(table) offset = get_wide_dword(table + (index << 2)) put_unconditional_branch(ea, table + offset) else: print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2 else: print "Unable to detect first instruction" 

Nous plaçons le curseur sur la chaîne 0xB26A, exécutons le script et voyons la transition vers 0xB4B0:



Encore une fois, l'IDA ne reconnaît pas cet endroit comme un code. Nous l'aidons et y voyons une autre structure:



Les instructions qui vont après BLX ne semblent pas très significatives; ils ressemblent plus à une sorte de décalage. Nous regardons sub_4964:



En effet, il prend DWORD à l'adresse de LR, l'ajoute à cette adresse, puis prend la valeur à l'adresse résultante et la stocke dans la pile. De plus, il ajoute 4 à LR pour sauter ce même décalage après son retour de la fonction. La commande POP {R1} prend ensuite la valeur résultante de la pile. En regardant ce qui se trouve à l'adresse 0xB4BA + 0xEA = 0xB5A4, nous pouvons voir quelque chose de similaire à la table d'adresses:



Pour patcher cette structure, nous devons obtenir deux paramètres du code: l'offset et le numéro de registre où nous voulons pousser le résultat. Nous devrons préparer un morceau de code à l'avance pour chaque registre possible.

 patches = {} patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0) patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0) patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0) patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0) patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0) patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0) patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0) patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0) patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0) patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0) ea = here() if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8 and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR} if get_operand_type(ea + 4, 0) == 7: pop = get_bytes(ea + 12, 4, 0) if pop[1] == '\xbc': register = -1 r = get_wide_byte(ea + 12) for i in range(8): if r == (1 << i): register = i break if register == -1: print "Unable to detect register" else: address = get_wide_dword(ea + 8) + ea + 8 for b in patches[register]: patch_byte(ea, b) ea += 1 if ea % 4 != 0: ea += 2 patch_dword(ea, address) elif pop[:3] == '\x5d\xf8\x04': register = ord(pop[3]) >> 4 if register in patches: address = get_wide_dword(ea + 8) + ea + 8 for b in patches[register]: patch_byte(ea, b) ea += 1 patch_dword(ea, address) else: print "POP instruction not found" else: print "Wrong operand type on +4:", get_operand_type(ea + 4, 0) else: print "Unable to detect first instructions" 

Nous plaçons le curseur au début de la structure que nous voulons remplacer (ie 0xB4B2) et exécutons le script:



En plus des structures déjà mentionnées, le code comprend les éléments suivants:



Comme dans le cas précédent, il y a un décalage après l'instruction BLX:



Nous prenons le décalage à l'adresse de LR, l'ajoutons à LR et y naviguons. 0x72044 + 0xC = 0x72050. Le script de cette structure est assez simple:

 def put_unconditional_branch(source, destination): offset = (destination - source - 4) >> 1 if offset > 2097151 or offset < -2097152: raise RuntimeError("Invalid offset") if offset > 1023 or offset < -1024: instruction1 = 0xf000 | ((offset >> 11) & 0x7ff) instruction2 = 0xb800 | (offset & 0x7ff) patch_word(source, instruction1) patch_word(source + 2, instruction2) else: instruction = 0xe000 | (offset & 0x7ff) patch_word(source, instruction) ea = here() if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR} ea1 = ea + 6 if get_wide_word(ea + 2) == 0xbf00: #NOP ea1 += 2 offset = get_wide_dword(ea1) put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff) else: print "Unable to detect first instruction" 

Le résultat de l'exécution du script:



Après avoir tout corrigé dans cette fonction, nous pouvons pointer l'IDA vers son véritable début. Il collectera le code de fonction entier pièce par pièce et nous pourrons le décompiler en utilisant HexRays.

Déchiffrer les chaînes


Nous avons appris à gérer l'obscurcissement du code machine dans la bibliothèque libsgmainso-6.4.36.so de UC Browser et obtenu le code de fonction JNI_OnLoad .

 int __fastcall real_JNI_OnLoad(JavaVM *vm) { int result; // r0 jclass clazz; // r0 MAPDST int v4; // r0 JNIEnv *env; // r4 int v6; // [sp-40h] [bp-5Ch] int v7; // [sp+Ch] [bp-10h] v7 = *(_DWORD *)off_8AC00; if ( !vm ) goto LABEL_39; sub_7C4F4(); env = (JNIEnv *)sub_7C5B0(0); if ( !env ) goto LABEL_39; v4 = sub_72CCC(); sub_73634(v4); sub_73E24(&unk_83EA6, &v6, 49); clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6); if ( clazz && (sub_9EE4(), sub_71D68(env), sub_E7DC(env) >= 0 && sub_69D68(env) >= 0 && sub_197B4(env, clazz) >= 0 && sub_E240(env, clazz) >= 0 && sub_B8B0(env, clazz) >= 0 && sub_5F0F4(env, clazz) >= 0 && sub_70640(env, clazz) >= 0 && sub_11F3C(env) >= 0 && sub_21C3C(env, clazz) >= 0 && sub_2148C(env, clazz) >= 0 && sub_210E0(env, clazz) >= 0 && sub_41B58(env, clazz) >= 0 && sub_27920(env, clazz) >= 0 && sub_293E8(env, clazz) >= 0 && sub_208F4(env, clazz) >= 0) ) { result = (sub_B7B0(env, clazz) >> 31) | 0x10004; } else { LABEL_39: result = -1; } return result; } 

Examinons les chaînes suivantes:

  sub_73E24(&unk_83EA6, &v6, 49); clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6); 

Il est clair que la fonction sub_73E24 déchiffre le nom de la classe. Les paramètres de cette fonction contiennent un pointeur vers les données qui ressemblent à celles chiffrées, une sorte de tampon et un nombre. De toute évidence, une chaîne déchiffrée sera dans le tampon après un appel à la fonction, car le tampon va à la fonction FindClass , qui reçoit le même nom de classe que le deuxième paramètre. Le nombre correspond donc à la taille du tampon ou à la longueur de la chaîne. Essayons de décrypter le nom de la classe. Cela devrait indiquer si nous allons dans la bonne direction. Examinons de plus près ce qui se passe dans le sub_73E24 .

 int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size) { int v4; // r6 int v7; // r11 int v8; // r9 int v9; // r4 size_t v10; // r5 int v11; // r0 struc_1 v13; // [sp+0h] [bp-30h] int v14; // [sp+1Ch] [bp-14h] int v15; // [sp+20h] [bp-10h] v4 = 0; v15 = *(_DWORD *)off_8AC00; v14 = 0; v7 = sub_7AF78(17); v8 = sub_7AF78(size); if ( !v7 ) { v9 = 0; goto LABEL_12; } (*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16); if ( !v8 ) { LABEL_9: v4 = 0; goto LABEL_10; } v4 = 0; if ( !in ) { LABEL_10: v9 = 0; goto LABEL_11; } v9 = 0; if ( out ) { memset(out, 0, size); v10 = size - 1; (*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10); memset(&v13, 0, 0x14u); v13.field_4 = 3; v13.field_10 = v7; v13.field_14 = v8; v11 = sub_6115C(&v13, &v14); v9 = v11; if ( v11 ) { if ( *(_DWORD *)(v11 + 4) == v10 ) { qmemcpy(out, *(const void **)v11, v10); v4 = *(_DWORD *)(v9 + 4); } else { v4 = 0; } goto LABEL_11; } goto LABEL_9; } LABEL_11: sub_7B148(v7); LABEL_12: if ( v8 ) sub_7B148(v8); if ( v9 ) sub_7B148(v9); return v4; } 

La fonction sub_7AF78 crée une instance de conteneur pour les tableaux d'octets de la taille spécifiée (nous ne nous concentrerons pas sur eux en détail). Deux de ces conteneurs sont créés ici: l'un contient la chaîne " DcO / lcK + h? M3c * q @ " (il est facile de deviner qu'il s'agit de la clé), l'autre contient les données chiffrées. Les deux objets sont ensuite placés dans une certaine structure, qui est transférée dans la fonction sub_6115C . Nous pouvons également noter que cette structure contient un champ avec une valeur de 3. Voyons ce qui se passe ensuite.

 int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2) { int v3; // lr unsigned int v4; // r1 int v5; // r0 int v6; // r1 int result; // r0 int v8; // r0 *a2 = 820000; if ( a1 ) { v3 = a1->field_14; if ( v3 ) { v4 = a1->field_4; if ( v4 < 0x19 ) { switch ( v4 ) { case 0u: v8 = sub_6419C(a1->field_0, a1->field_10, v3); goto LABEL_17; case 3u: v8 = sub_6364C(a1->field_0, a1->field_10, v3); goto LABEL_17; case 0x10u: case 0x11u: case 0x12u: v8 = sub_612F4( a1->field_0, v4, *(_QWORD *)&a1->field_8, *(_QWORD *)&a1->field_8 >> 32, a1->field_10, v3, a2); goto LABEL_17; case 0x14u: v8 = sub_63A28(a1->field_0, v3); goto LABEL_17; case 0x15u: sub_61A60(a1->field_0, v3, a2); return result; case 0x16u: v8 = sub_62440(a1->field_14); goto LABEL_17; case 0x17u: v8 = sub_6226C(a1->field_10, v3); goto LABEL_17; case 0x18u: v8 = sub_63530(a1->field_14); LABEL_17: v6 = 0; if ( v8 ) { *a2 = 0; v6 = v8; } return v6; default: LOWORD(v5) = 28032; goto LABEL_5; } } } } LOWORD(v5) = -27504; LABEL_5: HIWORD(v5) = 13; v6 = 0; *a2 = v5; return v6; } 

Le champ avec la valeur 3 précédemment affectée est transformé en paramètre de commutation. Jetons un coup d'œil au cas 3 - les paramètres que la fonction précédente ajoutée à la structure (c'est-à-dire la clé et les données chiffrées) sont passés à la fonction sub_6364C. Si nous regardons de près le sub_6364C, nous pouvons reconnaître l'algorithme RC4.

Nous avons donc un algorithme et une clé. Essayons de décrypter le nom de la classe. Voici ce que nous avons: com / taobao / wireless / security / adapter / JNICLibrary. Génial! Nous sommes sur la bonne voie.

Arborescence des commandes


Maintenant, nous devons trouver l'appel à RegisterNatives , qui nous dirigera vers la fonction doCommandNative . Nous examinons donc les fonctions appelées à partir de JNI_OnLoad et le trouvons dans sub_B7B0 :

 int __fastcall sub_B7F6(JNIEnv *env, jclass clazz) { char signature[41]; // [sp+7h] [bp-55h] char name[16]; // [sp+30h] [bp-2Ch] JNINativeMethod method; // [sp+40h] [bp-1Ch] int v8; // [sp+4Ch] [bp-10h] v8 = *(_DWORD *)off_8AC00; decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u);// doCommandNative decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u);// (I[Ljava/lang/Object;)Ljava/lang/Object; method.name = name; method.signature = signature; method.fnPtr = sub_B69C; return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31; } 

Et en effet, une méthode native avec le nom doCommandNative est enregistrée ici. Nous connaissons maintenant son adresse. Voyons ce qu'il fait.

 int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args) { int v5; // r5 struc_2 *a5; // r6 int v9; // r1 int v11; // [sp+Ch] [bp-14h] int v12; // [sp+10h] [bp-10h] v5 = 0; v12 = *(_DWORD *)off_8AC00; v11 = 0; a5 = (struc_2 *)malloc(0x14u); if ( a5 ) { a5->field_0 = 0; a5->field_4 = 0; a5->field_8 = 0; a5->field_C = 0; v9 = command % 10000 / 100; a5->field_0 = command / 10000; a5->field_4 = v9; a5->field_8 = command % 100; a5->field_C = env; a5->field_10 = args; v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11); } free(a5); if ( !v5 && v11 ) sub_7CF34(env, v11, &byte_83ED7); return v5; } 

Le nom suggère qu'il s'agit du point d'entrée pour toutes les fonctions que les développeurs ont transférées vers la bibliothèque native. Nous sommes particulièrement intéressés par la fonction numéro 10601.

D'après le code, nous pouvons voir que le numéro de commande nous donne trois nombres: commande / 10000, commande% 10000/100 et commande% 10 (dans notre cas, 1, 6 et 1). Ces trois nombres, ainsi que le pointeur vers JNIEnv et les arguments transférés à la fonction, constituent une structure et sont transmis. Avec ces trois nombres (nous les désignerons N1, N2 et N3), un arbre de commande est construit. Quelque chose comme ça:



L'arbre est créé dynamiquement dans JNI_OnLoad. Trois nombres codent le chemin dans l'arbre. Chaque feuille de l'arbre contient l'adresse xorred de la fonction correspondante. La clé se trouve dans le nœud parent. Il est assez facile de trouver une place dans le code où la fonction dont nous avons besoin est ajoutée à l'arborescence si nous comprenons toutes les structures qui s'y trouvent (nous ne passerons pas de temps à les décrire dans cet article).

Plus d'obscurcissement


Nous avons l'adresse de la fonction censée déchiffrer le trafic: 0x5F1AC. Mais il est encore trop tôt pour se détendre - les développeurs de UC Browser ont une autre surprise pour nous.

Après avoir reçu les paramètres d'un tableau dans le code Java, nous passons à la fonction à 0x4D070. Un autre type d'obfuscation de code est déjà en attente.

Nous poussons ensuite deux indices en R7 et R4:



Le premier indice passe à R11:



Nous utilisons cet index pour obtenir l'adresse de la table:



Après le transfert vers la première adresse, nous utilisons le deuxième index de R4. Le tableau contient 230 éléments.

Qu'en faisons-nous? Nous pourrions dire à l'IDA qu'il s'agit d'une sorte de commutateur: Édition -> Autre -> Spécifier l'idiome du commutateur.



Le code résultant est horrible. However, we can see the call to the familiar sub_6115C function in its tangles:



There was the switch parameter with the RC4 decryption in case 3. In this case, the structure that transfers to the function is filled with the parameters transferred to doCommandNative. We recall that we had magicInt there with value 16. We look at the corresponding case and after several transitions, find the code that helps us identify the algorithm.



It's AES!

We have an algorithm and only need to get its parameters, such as mode, key, and (possibly) the initialization vector (its presence depends on the AES algorithm operation mode). The structure that contains them should be created somewhere before calling the sub_6115C function. But since this part of the code is particularly well obfuscated, we decided to patch the code so all parameters of the decryption function could be dumped into a file.

Patch


If you don't want to manually write all the patch code in the assembly language, you can run Android Studio, code a function that receives the same parameters as our decryption function and writes to the file, then copy the resulting code generated by the compiler.

Our good friends from the UC Browser team also “ensured” the convenience of adding code. We do remember that we have garbage code at the beginning of each function, which can easily be replaced with any other code. Very convenient :) However, there is not enough room at the beginning of the target function for the code that saves all parameters to a file. We had to divide it into parts and use the garbage blocks of neighboring functions. We got four parts in total.
Part one:



The first four function parameters in the ARM architecture are indicated in the R0-R3 registers, while the rest, if any, goes via the stack. The LR register indicates the return address. We need to save all this data so the function can work after we dump its parameters. We also need to save all the registers that we use in the process, so we use PUSH.W {R0-R10,LR}. In R7, we get the address of the list of parameters transferred to the function via the stack.

Using the fopen function, we open the /data/local/tmp/aes file in the “ab” mode (so that we could add something). We then load the file name address in R0 and the string address that indicates the mode in R1. This is where the garbage code ends, so we navigate to the next function. Since we want it to continue working, we put the transition to the actual function code in the beginning, before the garbage, and replace the garbage with the rest of the patch.



We then call fopen.

The first three parameters of the aes function are of the int type. Since we pushed the registers to the stack at the beginning, we can simply transfer their addresses in the stack to the fwrite function.



Next, we have three structures that indicate the size of the data and contain a pointer to the data for the key, the initialization vector, and the encrypted data.



At the end, we close the file, restore the registers, and give control back to the actual aes function.
We compile the APK file with the patched library, sign it, download it onto a device or emulator, and run it. Now we see the dump has been created with a lot of data. The browser does not just decrypt traffic, but other data too, and all decryption is performed via this function. For some reason, we don't see the data we need, and the request we are expecting is not visible in the traffic. Let's skip waiting until UC Browser has the chance to make this request and take the encrypted response obtained earlier from the server to patch the application again. We'll add the decryption to the onCreate of the main activity.

  const/16 v1, 0x62 new-array v1, v1, [B fill-array-data v1, :encrypted_data const/16 v0, 0x1f invoke-static {v0, v1}, Lcom/uc/browser/core/d/c/g;->j(I[B)[B move-result-object v1 array-length v2, v1 invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v2 const-string v0, "ololo" invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 

We compile it, sign, install, and run. Thus, we get a NullPointerException since the method returns a null value.

After analyzing of the code further, we found a function with rather interesting strings: “META-INF/” and “.RSA”. Looks like the app verifies its certificate, or even generates keys from it. We don't really want to dig into what is happening with the certificate, so let's just give it the correct certificate. We'll patch the encrypted string, so instead of “META-INF/” we get “BLABLINF/”, we create a folder with this name in the APK file, and save the browser certificate in it.

We compile it, sign, install, and run. Bingo! We have the key!

MitM


Now we've got the key and an equal initialization vector. Let's try to decrypt the server response in the CBC mode.



We see the archive URL, something like MD5, “extract_unzipsize”, and a number. Let us check. The MD5 of the archive is the same; the size of the unzipped library is the same. Now we'll try to patch this library and transmit it to the browser. To show that our patched library has loaded, we'll build an Intent to create the text message «PWNED!» We'll replace two responses from the server: puds.ucweb.com/upgrade/index.xhtml and the one that prompts the archive download. In the first, we substitute MD5 (the size remains the same after unzipping); in the second, we send the archive with the patched library.

The browser makes several attempts to download the archive, resulting in an error. Apparently, something fishy is happening there. Analyzing this bizarre format, we found that the server also transmits the archive size:



It is LEB128 encoded. The patch slightly changes the size of the compressed library, so the browser decided that the archive broke upon download and displayed an error after several attempts.
So we fix the archive size and… voila! :) See the result in the video.


Consequences and developer's response


In the same way, hackers can use this insecure feature of UC Browser to distribute and launch malicious libraries. These libraries will work in the context of the browser, resulting in full system privileges that the browser has. This grants them free reign to display phishing windows, as well as access to the browser's working files including logins, passwords, and cookies in the database.

We contacted the UC Browser developers and informed them about the problem we had found, tried to point out the vulnerability and its danger, but they refused to discuss the matter. Meanwhile, the browser with the dangerous function remained in plain sight. Though as soon as we revealed the details of the vulnerability, it was impossible to ignore it as before. A new version of UC Browser 12.10.9.1193 was released on March 27, which accessed the server via HTTPS puds.ucweb.com/upgrade/index.xhtml . In addition, between the “bug fixing” and the time we wrote this article, an attempt to open a PDF in the browser resulted in an error message with the text, “Oops, something is wrong.” There was no request to the server when trying to open the PDF file. This was performed upon startup, though, which is a sign that the ability to download the executable code in violation of Google Play policies is still present.

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


All Articles