Présentation
Fin mars, nous avons
signalé que nous avions découvert une capacité cachée de télécharger et d'exécuter du code non vérifié dans UC Browser. Aujourd'hui, nous analyserons en détail comment ce téléchargement se produit et comment les pirates peuvent l'utiliser à leurs propres fins.
Il y a quelque temps, UC Browser a été annoncé et distribué de manière très agressive: il a été installé sur les appareils des utilisateurs à l'aide de logiciels malveillants, distribué à partir de divers sites sous couvert de fichiers vidéo (c'est-à-dire que les utilisateurs pensaient avoir téléchargé, par exemple, un clip porno, mais ont reçu à la place un APK avec ce navigateur), utilisé des bannières effrayantes avec des messages disant que le navigateur est obsolète, vulnérable et des trucs comme ça. Le groupe officiel UC Browser dans VK a un
sujet dans lequel les utilisateurs peuvent se plaindre de publicité déloyale, il existe de nombreux exemples. En 2016, il y avait même
de la publicité vidéo en russe (oui, des publicités provenant d'un navigateur qui bloque les publicités).
Au moment d'écrire ces lignes, UC Browser a plus de 500 000 000 d'installations sur Google Play. C'est impressionnant - seul Google Chrome en a plus. Parmi les critiques, vous pouvez voir beaucoup de plaintes concernant la publicité et les redirections vers certaines applications sur Google Play. C'était la raison de l'étude: nous avons décidé de voir si UC Browser faisait quelque chose de mal. Et il s'est avéré qu'il le faisait!
Le code d'application a révélé la possibilité de télécharger et d'exécuter du code exécutable,
ce qui contredit les règles de publication des applications sur Google Play. En plus du fait que UC Browser télécharge du code exécutable, il le rend dangereux, ce qui peut être utilisé pour mener une attaque MitM. Voyons voir si nous parvenons à mener une telle attaque.
Tout ce qui est écrit ci-dessous est pertinent pour la version de UC Browser qui était présente sur Google Play au moment de l'étude:
package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-: f5edb2243413c777172f6362876041eb0c3a928c
Vecteur d'attaque
Dans le manifeste UC Browser, vous pouvez trouver un service appelé
com.uc.deployment.UpgradeDeployService .
<service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" />
Lorsque ce service démarre, le navigateur effectue une demande POST à
puds.ucweb.com/upgrade/index.xhtml , ce qui peut être remarqué dans le trafic quelque temps après le démarrage. En réponse, il peut recevoir une commande pour télécharger une mise à jour ou un nouveau module. Au cours de l'analyse, le serveur n'a pas donné de telles commandes, mais nous avons remarqué qu'en essayant d'ouvrir le PDF dans le navigateur, il fait une deuxième demande à l'adresse ci-dessus, après quoi il télécharge la bibliothèque native. Pour mener à bien l'attaque, nous avons décidé d'utiliser cette fonctionnalité d'UC Browser: la possibilité d'ouvrir un PDF à l'aide d'une bibliothèque native, qui n'est pas dans l'APK et qui, si nécessaire, est téléchargée sur Internet. Il convient de noter qu'en théorie, UC Browser peut être forcé de télécharger quelque chose sans interaction avec l'utilisateur - si vous donnez une réponse correctement formée à une demande qui est exécutée après le démarrage du navigateur. Mais pour cela, nous devons étudier plus en détail le protocole d'interaction avec le serveur, nous avons donc décidé qu'il était plus facile de modifier la réponse interceptée et de remplacer la bibliothèque pour travailler avec PDF.
Ainsi, lorsqu'un utilisateur souhaite ouvrir un PDF directement dans un navigateur, les requêtes suivantes peuvent être vues dans le trafic:

Vient d'abord une demande POST à
puds.ucweb.com/upgrade/index.xhtml , après quoi
Téléchargez l'archive avec la bibliothèque pour visualiser les formats PDF et Office. Il est logique de supposer que dans la première demande des informations sur le système sont transmises (au moins l'architecture afin de donner la bibliothèque nécessaire), et en réponse à cela le navigateur reçoit des informations sur la bibliothèque à télécharger: l'adresse et, éventuellement, autre chose. Le problème est que cette demande est cryptée.
La bibliothèque elle-même est emballée en ZIP et non cryptée.

Rechercher le code de décryptage du trafic
Essayons de décrypter la réponse du serveur. Nous examinons le code de la classe
com.uc.deployment.UpgradeDeployService : à partir de la méthode
onStartCommand, accédez à
com.uc.deployment.bx et de celui-ci à
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 voyons ici la formation de la requête POST. Nous attirons l'attention sur la création d'un tableau de 16 octets et son remplissage: 0x5F, 0, 0x1F, -50 (= 0xCE). Correspond à ce que nous avons vu dans la demande ci-dessus.
Dans la même classe, vous pouvez remarquer une classe imbriquée dans laquelle il existe 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"); } }
La méthode reçoit un tableau d'octets en entrée et vérifie que l'octet zéro est 0x60 ou le troisième octet est 0xD0 et le deuxième octet est 1, 11 ou 0x1F. Nous regardons la réponse du serveur: zéro octet - 0x60, deuxième - 0x1F, troisième - 0x60. On dirait ce dont nous avons besoin. A en juger par les lignes ("up_decrypt", par exemple), une méthode doit être appelée ici qui décrypte la réponse du serveur.
Nous passons à la méthode
gj . Notez que l'octet à l'offset 2 (c'est-à-dire 0x1F dans notre cas) lui est transféré comme premier argument, et la réponse du serveur sans
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; }
De toute évidence, il existe un choix d'algorithme de décryptage, et le même octet, qui dans notre
le cas est 0x1F, indique l'une des trois options possibles.
Nous continuons d'analyser le code. Après quelques sauts, nous entrons dans une méthode avec le nom parlant
decryptBytesByKey .
Ici, deux octets supplémentaires sont séparés de notre réponse, et une chaîne est obtenue d'eux. Il est clair que de cette manière la clé est sélectionnée pour décrypter 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];
Pour l'avenir, nous notons qu'à ce stade, la clé n'est pas encore obtenue, mais uniquement son «identifiant». Obtenir la clé est un peu plus compliqué.
Dans la méthode suivante, deux autres sont ajoutés aux paramètres existants, et il y en a quatre: le nombre magique 16, l'identificateur de clé, les données chiffrées et une chaîne incompréhensible (dans notre cas, vide).
public final byte[] l(String keyId, byte[] encrypted) throws SecException { return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, ""); }
Après une série de transitions, nous arrivons à la méthode
staticBinarySafeDecryptNoB64 de l'interface
com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . Il n'y a aucune classe dans le code d'application principal qui implémente cette interface. Une telle classe se trouve dans le fichier
lib / armeabi-v7a / libsgmain.so , qui n'est en fait pas .so, mais .jar. La méthode qui nous intéresse est mise en œuvre comme suit:
package com.alibaba.wireless.security.ai;
Ici, notre liste de paramètres est complétée par deux autres entiers: 2 et 0. A en juger par
pour tous, 2 signifie déchiffrement, comme dans la méthode
doFinal de la classe système
javax.crypto.Cipher . Et tout cela est transféré à un certain routeur avec le numéro 10601 - c'est apparemment le numéro de commande.
Après la prochaine chaîne de transition, nous trouvons une classe qui implémente l'interface
IRouterComponent 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); } }
Et aussi la classe
JNICLibrary , dans laquelle 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. Et puis le plaisir commence.
Obfuscation du code machine
Il y a une bibliothèque native dans le fichier
libsgmain.so (qui est en fait .jar et dans laquelle nous avons trouvé un peu d'implémentation de certaines interfaces de chiffrement un peu plus haut):
libsgmainso-6.4.36.so . Ouvrez-le dans l'IDA et obtenez un tas de boîtes de dialogue avec des erreurs. Le problème est que la table d'en-tête de section n'est pas valide. Ceci est fait spécifiquement pour compliquer l'analyse.

Mais ce n'est pas non plus nécessaire: pour charger correctement le fichier ELF et l'analyser, la table d'en-tête du programme suffit. Par conséquent, nous supprimons simplement la table de section, annulant les champs correspondants dans l'en-tête.

Ouvrez à nouveau le fichier dans l'IDA.
Il existe deux façons de dire à la machine virtuelle Java exactement où se trouve dans la bibliothèque native l'implémentation de la méthode déclarée comme native dans le code Java. La première consiste à lui donner un nom de la forme
Java_package_name_ClassName_Method_name .
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 .
Parmi les fonctions exportées, il n'y en a aucune, vous devez donc rechercher l'appel
RegisterNatives .
Nous allons à la fonction
JNI_OnLoad et voyons l'image suivante:

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 sur la pile stocke le contenu des registres que la fonction utilisera dans son travail (dans ce cas R0, R1 et R2), ainsi que le contenu du registre LR, dans lequel se trouve l'adresse de retour de la fonction. Avec la dernière instruction, les registres sauvegardés sont restaurés et l'adresse de retour est immédiatement placée dans le registre PC - revenant ainsi de la fonction. Mais si vous regardez attentivement, vous remarquerez que l'avant-dernière instruction modifie l'adresse de retour stockée sur la pile. Nous calculons ce que ce sera après
exécution de code. Une certaine adresse 0xB130 est chargée dans R1, 5 en est soustraite, puis elle est transférée vers R0 et 0x10 y est ajoutée. Il s'avère 0xB13B. Ainsi, l'IDA pense que dans la dernière instruction il y a un retour normal de la fonction, mais en fait il y a une transition vers l'adresse calculée 0xB13B.
Il convient de rappeler que les processeurs ARM ont deux modes et deux ensembles d'instructions: ARM et Thumb. Le bit le moins significatif de l'adresse indique au processeur quel jeu d'instructions est utilisé. Autrement dit, l'adresse est en fait 0xB13A et l'unité dans le bit bas indique le mode Thumb.
Au début de chaque fonction de cette bibliothèque est ajouté un «adaptateur» similaire et
code poubelle. De plus nous ne nous attarderons pas sur eux en détail - nous nous souvenons juste
que le véritable début de presque toutes les fonctions est un peu plus loin.
Puisqu'il n'y a pas de transition explicite vers 0xB13A dans le code, l'IDA elle-même n'a pas reconnu que le code était à cet endroit. 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 quelque peu difficile. Nous disons à l'IDA que le code est ici, et voici le résultat:

À 0xB144, la table commence clairement. Qu'en est-il du sub_494C?

Lorsque cette fonction est appelée dans le registre LR, on obtient l'adresse du tableau mentionné ci-dessus (0xB144). Dans R0, l'index dans ce tableau. Autrement dit, une valeur est tirée du tableau, ajoutée à LR et obtenue
adresse où aller. Essayons de le calculer: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Nous allons à l'adresse reçue et voyons juste quelques instructions utiles et à nouveau la transition vers 0xB140:

Maintenant, il y aura un décalage par décalage avec l'index 0x20 de la table.
A en juger par la taille de la table, il existe de nombreuses transitions de ce type dans le code. La question se pose de savoir s'il est possible de traiter cette question de manière plus automatique, sans calculer manuellement les adresses. Et les scripts et la possibilité de patcher du code dans l'IDA nous sont utiles:
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:
Nous plaçons le curseur sur la ligne 0xB26A, exécutons le script et voyons la transition vers 0xB4B0:

L'IDA n'a de nouveau pas reconnu ce site comme un code. Nous l'aidons et y voyons une autre construction:

Les instructions après BLX n'ont pas l'air très significatives, c'est plutôt une sorte de parti pris. Nous regardons dans le sub_4964:

En effet, ici le dword est pris à l'adresse située en LR, ajouté à cette adresse, après quoi la valeur à l'adresse reçue est prise et poussée sur la pile. En outre, 4 est ajouté à LR, de sorte qu'après être revenu de la fonction pour sauter ce même décalage. La commande POP {R1} extrait ensuite la valeur reçue de la pile. Si vous regardez l'adresse 0xB4BA + 0xEA = 0xB5A4, vous pouvez voir quelque chose de similaire à la table d'adresses:

Pour patcher cette conception, vous devez obtenir deux paramètres du code: l'offset et le numéro de registre dans lequel vous souhaitez mettre le résultat. Pour chaque registre possible, vous devrez préparer un morceau de code à l'avance.
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
Nous plaçons le curseur au début de la construction que nous voulons remplacer - 0xB4B2 - et exécutons le script:

En plus des constructions déjà mentionnées dans le code, voici également:

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

Nous prenons l'offset à l'adresse de LR, l'ajoutons à LR et y allons. 0x72044 + 0xC = 0x72050. Le script pour cette conception est très 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:
Le résultat du script:

Une fois que tout est corrigé dans la fonction, vous pouvez pointer l'IDA vers son véritable début. Il va collecter tout le code de la fonction en morceaux, et il peut être décompilé en utilisant HexRays.
Décodage des chaînes
Nous avons appris à gérer l'obfuscation du code machine dans la bibliothèque
libsgmainso-6.4.36.so de UC Browser et
avons obtenu le code de la fonction
JNI_OnLoad .
int __fastcall real_JNI_OnLoad(JavaVM *vm) { int result;
Considérez attentivement les lignes suivantes:
sub_73E24(&unk_83EA6, &v6, 49); clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
La fonction
sub_73E24 déchiffre explicitement le nom de classe. En tant que paramètres de cette fonction, un pointeur vers des données similaires à cryptées, un tampon et un nombre est transmis. , , . .
FindClass , . , — . , , . ,
sub_73E24. int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size) { int v4;
sub_7AF78 ( ). :
«DcO/lcK+h?m3c*q@» ( , ), — . ,
sub_6115C . 3. , .
int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2) { int v3;
switch , 3. case 3:
sub_6364C , , . . .
sub_6364C , RC4.
. . :
com/taobao/wireless/security/adapter/JNICLibrary . Super! .
RegisterNatives ,
doCommandNative . ,
JNI_OnLoad, sub_B7B0 :
int __fastcall sub_B7F6(JNIEnv *env, jclass clazz) { char signature[41];
En effet, une méthode native appelée doCommandNative est enregistrée ici . Maintenant, nous connaissons son adresse. Voyons voir ce qu'il fait. int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args) { int v5;
, , . 10601.
, :
command / 10000 ,
command % 10000 / 100 command % 10 , . ., , 1, 6 1. ,
JNIEnv , , . ( N1, N2 N3) .
:
JNI_OnLoad .
. . — . , , , ( , ).
, : 0x5F1AC. : UC Browser .
, Java-,
0x4D070. .
R7 R4 :

R11:

, :

, R4. 230 .
Que faire à ce sujet? IDA, switch: Edit -> Other -> Specify switch idiom.

. , ,
sub_6115C :

switch, case 3 RC4. ,
doCommandNative . ,
magicInt 16. case – , .

AES!
, : , , , ( AES). -
sub_6115C , , , .
, Android Studio, , , , , , .
UC Browser «». , , . :) , , , . . .
:

ARM R0-R3, , — . LR . , , . , , PUSH.W {R0-R10,LR}. R7 , .
fopen /data/local/tmp/aes «ab»,
. . . R0 , R1 — . , . , , .
fopen .
aes int . ,
fwrite .

, , .

,
aes .
APK , , /, . , , . , . - , . , UC Browser , , , : onCreate .
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
, , , . NullPointerException, . . null.
, : «META-INF/» ".RSA". , . . , , , . , «META-INF/» «BLABLINF/», APK .
, , , . ! !
MitM
, . CBC.

URL , - MD5, «extract_unzipsize» . : MD5 , . . , , Intent «PWNED!». :
puds.ucweb.com/upgrade/index.xhtml . MD5 ( ), .
, . , -
. , :

LEB128. , , , .
… – ! :) .
https://www.youtube.com/watch?v=Nfns7uH03J8UC Browser, . , . — , , , .
UC Browser , , - . . , , , . 27
UC Browser 12.10.9.1193, HTTPS:
puds.ucweb.com/upgrade/index.xhtml .
, «» PDF «, - !». PDF , , Google Play.