Recherche de vulnérabilités dans UC Browser

Recherche de vulnérabilités dans UC Browser

introduction

Fin mars, nous rapporté, qu'ils ont découvert une capacité cachée pour charger et exécuter du code non vérifié dans UC Browser. Aujourd'hui, nous examinerons 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 était annoncé et distribué de manière très agressive : il était installé sur les appareils des utilisateurs à l'aide de logiciels malveillants, distribués depuis divers sites sous couvert de fichiers vidéo (c'est-à-dire que les utilisateurs pensaient télécharger, par exemple, une vidéo porno, mais à la place, j'ai reçu un APK avec ce navigateur), j'ai utilisé des bannières effrayantes avec des messages indiquant que le navigateur était obsolète, vulnérable, etc. Dans le groupe officiel UC Browser sur VK, il y a sujet, dans lequel les utilisateurs peuvent se plaindre d'une publicité déloyale, il existe de nombreux exemples. En 2016, il y avait même publicité vidéo en russe (oui, publicité pour un navigateur bloquant les publicités).

Au moment de la rédaction de cet article, UC Browser compte plus de 500 000 000 d'installations sur Google Play. C'est impressionnant - seul Google Chrome en a plus. Parmi les avis, vous pouvez voir de nombreuses plaintes concernant la publicité et les redirections vers certaines applications sur Google Play. C'est la raison de notre recherche : nous avons décidé de voir si UC Browser faisait quelque chose de mal. Et il s’est avéré que oui !

Dans le code de l'application, la possibilité de télécharger et d'exécuter du code exécutable a été découverte, ce qui est contraire aux règles de publication des candidatures sur Google Play. En plus de télécharger du code exécutable, UC Browser le fait de manière non sécurisée, ce qui peut être utilisé pour lancer une attaque MitM. Voyons si nous pouvons mener une telle attaque.

Tout ce qui est écrit ci-dessous concerne la version d'UC Browser qui était disponible 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 du navigateur UC, vous pouvez trouver un service avec un nom explicite 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 requête POST pour puds.ucweb.com/upgrade/index.xhtml, visible dans la circulation quelque temps après le départ. En réponse, il peut recevoir une commande pour télécharger une mise à jour ou un nouveau module. Lors de l'analyse, le serveur n'a pas donné de telles commandes, mais nous avons remarqué que lorsque nous essayons d'ouvrir un PDF dans le navigateur, il fait une deuxième requête à l'adresse indiquée ci-dessus, après quoi il télécharge la bibliothèque native. Pour mener l'attaque, nous avons décidé d'utiliser cette fonctionnalité d'UC Browser : la possibilité d'ouvrir le PDF à l'aide d'une bibliothèque native, qui n'est pas dans l'APK et qu'il télécharge depuis Internet si nécessaire. Il convient de noter que, théoriquement, UC Browser peut être forcé de télécharger quelque chose sans interaction de l'utilisateur - si vous fournissez une réponse bien formée à une requête exécutée après le lancement du navigateur. Mais pour ce faire, nous devons étudier plus en détail le protocole d'interaction avec le serveur, nous avons donc décidé qu'il serait 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 le navigateur, les requêtes suivantes peuvent être constatées dans le trafic :

Recherche de vulnérabilités dans UC Browser

Il y a d’abord une requête POST pour puds.ucweb.com/upgrade/index.xhtml, après quoi
Une archive avec une bibliothèque pour visualiser les formats PDF et bureautiques est téléchargée. Il est logique de supposer que la première requête transmet des informations sur le système (au moins l'architecture pour fournir la bibliothèque requise) et qu'en réponse, le navigateur reçoit des informations sur la bibliothèque qui doit être téléchargée : l'adresse et, éventuellement , autre chose. Le problème est que cette requête est cryptée.

Fragment de demande

Fragment de réponse

Recherche de vulnérabilités dans UC Browser

Recherche de vulnérabilités dans UC Browser

La bibliothèque elle-même est conditionnée au format ZIP et n'est pas cryptée.

Recherche de vulnérabilités dans UC Browser

Rechercher le code de décryptage du trafic

Essayons de déchiffrer la réponse du serveur. Regardons le code de la classe com.uc.deployment.UpgradeDeployService: de la méthode onStartCommand aller à com.uc.deployment.bx, et de là à 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();
m.b(v3_4);
h v4_2 = new h();
m.b(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.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
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.b.a.a.c.Pc()));
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.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
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 = g.i(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.d.c.i 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.d.c.i$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);
m.b(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 d’une requête POST. On fait attention à la création d'un tableau de 16 octets et à son remplissage : 0x5F, 0, 0x1F, -50 (=0xCE). Coïncide avec ce que nous avons vu dans la demande ci-dessus.

Dans la même classe, vous pouvez voir une classe imbriquée qui a 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 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(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 = g.b(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 prend un tableau d'octets en entrée et vérifie que l'octet zéro est 0x60 ou que le troisième octet est 0xD0 et que le deuxième octet est 1, 11 ou 0x1F. Nous regardons la réponse du serveur : l'octet zéro est 0x60, le deuxième est 0x1F, le troisième est 0x60. Cela ressemble à ce dont nous avons besoin. À en juger par les lignes (« up_decrypt », par exemple), une méthode devrait être appelée ici pour décrypter la réponse du serveur.
Passons à la méthode gj. Notez que le premier argument est l'octet au décalage 2 (c'est-à-dire 0x1F dans notre cas), et le second est la réponse du serveur sans
16 premiers octets.

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

Évidemment, nous sélectionnons ici un algorithme de décryptage, et le même octet qui se trouve dans notre
cas égal à 0x1F, désigne l’une des trois options possibles.

Nous continuons à analyser le code. Après quelques sauts, nous nous retrouvons dans une méthode au nom explicite décrypterBytesByKey.

Ici, deux octets supplémentaires sont séparés de notre réponse et une chaîne en est obtenue. Il est clair que la clé de déchiffrement du message est ainsi sélectionnée.

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

Pour l'avenir, on constate qu'à ce stade on n'obtient pas encore de clé, mais seulement son « identifiant ». Obtenir la clé est un peu plus compliqué.

Dans la méthode suivante, deux paramètres supplémentaires sont ajoutés à ceux existants, ce qui en fait quatre : le nombre magique 16, l'identifiant de la clé, les données crypté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 interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Aucune classe dans le code de l’application principale n’implémente cette interface. Il y a une telle classe dans le fichier lib/armeabi-v7a/libsgmain.so, qui n'est pas réellement un .so, mais un .jar. La méthode qui nous intéresse est implémentée comme suit :

package com.alibaba.wireless.security.a.i;
// ...
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 nombres entiers : 2 et 0. À en juger par
tout, 2 signifie décryptage, comme dans la méthode faireFinal classe système javax.crypto.Cipher. Et tout cela est transféré vers un certain routeur portant le numéro 10601 - c'est apparemment le numéro de commande.

Après la prochaine chaîne de transitions, nous trouvons une classe qui implémente l'interface Composant IRouter et méthode faireCommande:

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 Bibliothèque JNIC, dans lequel la méthode native est déclarée doCommandNative:

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

Cela signifie que nous devons trouver une méthode dans le code natif doCommandNative. Et c'est là que le plaisir commence.

Obscurcissement du code machine

En fichier libsgmain.so (qui est en fait un .jar et dans lequel nous avons trouvé juste au-dessus l'implémentation de certaines interfaces liées au chiffrement) il existe une bibliothèque native : libsgmainso-6.4.36.so. Nous l'ouvrons dans IDA et obtenons un tas de boîtes de dialogue avec des erreurs. Le problème est que le tableau d’en-tête de section n’est pas valide. Ceci est fait exprès pour compliquer l’analyse.

Recherche de vulnérabilités dans UC Browser

Mais ce n'est pas nécessaire : pour charger correctement un fichier ELF et l'analyser, une table d'en-tête de programme suffit. Par conséquent, nous supprimons simplement le tableau des sections, en mettant à zéro les champs correspondants dans l'en-tête.

Recherche de vulnérabilités dans UC Browser

Ouvrez à nouveau le fichier dans IDA.

Il existe deux manières d'indiquer à la machine virtuelle Java où se trouve exactement dans la bibliothèque native l'implémentation d'une méthode déclarée dans le code Java comme native. La première est de lui donner un nom d'espèce Nom_package_Java_NomClasse_NomMéthode.

La seconde est de l'enregistrer lors du chargement de la bibliothèque (dans la fonction JNI_OnLoad)
en utilisant un appel de fonction Inscrivez-Natifs.

Dans notre cas, si nous utilisons la première méthode, le nom devrait ressembler à ceci : Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Il n'existe pas de fonction de ce type parmi les fonctions exportées, ce qui signifie que vous devez rechercher un appel Inscrivez-Natifs.
Passons à la fonction JNI_OnLoad et on voit cette photo :

Recherche de vulnérabilités dans UC Browser

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 fonctionnement (dans ce cas, R0, R1 et R2), ainsi que le contenu du registre LR, qui contient l'adresse de retour de la fonction. . La dernière instruction restaure les registres enregistré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. Calculons ce que ce sera après
exécution de code. Une certaine adresse 1xB0 est chargée dans R130, 5 en est soustrait, puis elle est transférée vers R0 et 0x10 y est ajouté. Il s'avère que 0xB13B. Ainsi, IDA pense que la dernière instruction est un retour de fonction normal, mais en fait elle va à l'adresse calculée 0xB13B.

Il convient de rappeler ici que les processeurs ARM disposent de deux modes et de deux jeux 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 un bit de poids faible indique le mode Thumb.

Un « adaptateur » similaire a été ajouté au début de chaque fonction de cette bibliothèque et
code poubelle. Nous ne nous y attarderons pas plus en détail - nous nous souvenons simplement
que le véritable début de presque toutes les fonctions est un peu plus éloigné.

Étant donné que le code ne saute pas explicitement à 0xB13A, IDA lui-même n'a pas reconnu que le code se trouvait à cet emplacement. 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 à IDA que voici le code, et voici ce qui se passe :

Recherche de vulnérabilités dans UC Browser

Le tableau commence clairement à 0xB144. Qu'y a-t-il dans sub_494C ?

Recherche de vulnérabilités dans UC Browser

En appelant cette fonction dans le registre LR, on obtient l'adresse de la table mentionnée précédemment (0xB144). En R0 - index dans ce tableau. Autrement dit, la valeur est extraite du tableau, ajoutée à LR et le résultat est
l'adresse où se rendre. Essayons de le calculer : 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Nous allons à l'adresse reçue et voyons littéralement quelques instructions utiles et revenons à 0xB140 :

Recherche de vulnérabilités dans UC Browser

Il y aura maintenant une transition au décalage avec l'index 0x20 de la table.

À en juger par la taille du tableau, il y aura de nombreuses transitions de ce type dans le code. La question se pose de savoir s’il est possible de gérer cela de manière plus automatique, sans calculer manuellement les adresses. Et les scripts et la possibilité de corriger le code dans IDA nous viennent en aide :

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"

Placez le curseur sur la ligne 0xB26A, exécutez le script et voyez la transition vers 0xB4B0 :

Recherche de vulnérabilités dans UC Browser

Encore une fois, l’IDA n’a pas reconnu cette zone comme un code. Nous l'aidons et y voyons un autre design :

Recherche de vulnérabilités dans UC Browser

Les instructions après BLX ne semblent pas avoir beaucoup de sens, cela ressemble plus à une sorte de déplacement. Regardons sub_4964 :

Recherche de vulnérabilités dans UC Browser

Et en effet, ici un dword est pris à l'adresse située dans LR, ajouté à cette adresse, après quoi la valeur à l'adresse résultante est prise et mise sur la pile. De plus, 4 est ajouté à LR afin qu'après le retour de la fonction, ce même décalage soit ignoré. Après quoi la commande POP {R1} prend la valeur résultante de la pile. Si vous regardez ce qui se trouve à l’adresse 0xB4BA + 0xEA = 0xB5A4, vous verrez quelque chose de similaire à une table d’adresses :

Recherche de vulnérabilités dans UC Browser

Pour patcher cette conception, vous aurez besoin d'obtenir deux paramètres du code : le décalage et le numéro de registre dans lequel vous souhaitez mettre le résultat. Pour chaque inscription possible, vous devrez préparer un bout de code au préalable.

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] == 'x5dxf8x04':
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"

On place le curseur au début de la structure que l'on souhaite remplacer - 0xB4B2 - et on exécute le script :

Recherche de vulnérabilités dans UC Browser

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

Recherche de vulnérabilités dans UC Browser

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

Recherche de vulnérabilités dans UC Browser

Nous prenons le décalage vers l'adresse de LR, l'ajoutons à LR et y allons. 0x72044 + 0xC = 0x72050. Le script de cette conception 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"

Résultat de l'exécution du script :

Recherche de vulnérabilités dans UC Browser

Une fois que tout est corrigé dans la fonction, vous pouvez diriger IDA vers son véritable début. Il rassemblera tout le code de la fonction et pourra être décompilé à l’aide de HexRays.

Chaînes de décodage

Nous avons appris à gérer l'obscurcissement du code machine dans la bibliothèque libsgmainso-6.4.36.so depuis UC Browser et reçu 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;
}

Regardons de plus près les lignes suivantes :

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

En fonction sub_73E24 le nom de la classe est clairement déchiffré. En tant que paramètres de cette fonction, un pointeur vers des données similaires aux données cryptées, un certain tampon et un numéro sont transmis. Évidemment, après avoir appelé la fonction, il y aura une ligne déchiffrée dans le tampon, puisqu'elle est passée à la fonction FindClass, qui prend le nom de la classe comme deuxième paramètre. Par conséquent, le nombre correspond à la taille du tampon ou à la longueur de la ligne. Essayons de déchiffrer le nom de la classe, il devrait nous dire si nous allons dans la bonne direction. Regardons de plus près ce qui se passe dans 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;
}

Fonction sub_7AF78 crée une instance d'un conteneur pour les tableaux d'octets de la taille spécifiée (nous ne nous attarderons pas sur ces conteneurs en détail). Ici, deux conteneurs de ce type sont créés : l'un contient la ligne "DcO/lcK+h?m3c*q@" (il est facile de deviner qu’il s’agit d’une clé), l’autre contient des données cryptées. Ensuite, les deux objets sont placés dans une certaine structure, qui est transmise à la fonction sub_6115C. Marquons également un champ avec la valeur 3 dans cette structure. Voyons ensuite ce qui arrive à cette structure.

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 paramètre switch est un champ de structure auquel a été précédemment attribué la valeur 3. Regardez le cas 3 : à la fonction sub_6364C les paramètres sont transmis à partir de la structure qui y a été ajoutée dans la fonction précédente, c'est-à-dire la clé et les données cryptées. Si vous regardez attentivement sub_6364C, vous pouvez y reconnaître l'algorithme RC4.

Nous avons un algorithme et une clé. Essayons de déchiffrer le nom de la classe. Voici ce qui s'est passé : com/taobao/wireless/security/adapter/JNICLibrary. Super! Nous sommes sur la bonne voie.

Arbre de commandes

Maintenant nous devons trouver un défi Inscrivez-Natifs, ce qui nous dirigera vers la fonction doCommandNative. Regardons les fonctions appelées depuis JNI_OnLoad, et nous 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 portant le nom est enregistrée ici doCommandNative. Nous connaissons désormais 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;
}

Par son nom, vous pouvez deviner que c'est ici le point d'entrée de toutes les fonctions que les développeurs ont décidé de transférer vers la bibliothèque native. Nous sommes intéressés par la fonction numéro 10601.

Vous pouvez voir dans le code que le numéro de commande produit trois nombres : commande/10000, commande % 10000 / 100 и commande % 10, soit dans notre cas 1, 6 et 1. Ces trois nombres, ainsi qu'un pointeur vers JNIEnv et les arguments passés à la fonction sont ajoutés à une structure et transmis. A l'aide des trois nombres obtenus (notons-les N1, N2 et N3), un arbre de commandes est construit.

Quelque chose comme ça:

Recherche de vulnérabilités dans UC Browser

L'arborescence est remplie dynamiquement JNI_OnLoad.
Trois nombres codent le chemin dans l'arborescence. Chaque feuille de l'arbre contient l'adresse poquée de la fonction correspondante. La clé est dans le nœud parent. Trouver l'endroit dans le code où la fonction dont nous avons besoin est ajoutée à l'arborescence n'est pas difficile si l'on comprend toutes les structures utilisées (nous ne les décrivons pas pour ne pas surcharger un article déjà assez volumineux).

Plus d'obscurcissement

Nous avons reçu l'adresse de la fonction qui doit décrypter le trafic : 0x5F1AC. Mais il est trop tôt pour se réjouir : les développeurs d’UC Browser nous ont préparé une autre surprise.

Après avoir reçu les paramètres du tableau formé dans le code Java, nous obtenons
à la fonction à l’adresse 0x4D070. Et ici, un autre type d’obscurcissement du code nous attend.

On met deux indices dans R7 et R4 :

Recherche de vulnérabilités dans UC Browser

On déplace le premier index vers R11 :

Recherche de vulnérabilités dans UC Browser

Pour obtenir une adresse d'une table, utilisez un index :

Recherche de vulnérabilités dans UC Browser

Après avoir accédé à la première adresse, le deuxième index est utilisé, qui se trouve dans R4. Il y a 230 éléments dans le tableau.

Que faire à ce sujet ? Vous pouvez indiquer à IDA qu'il s'agit d'un commutateur : Édition -> Autre -> Spécifier l'idiome du commutateur.

Recherche de vulnérabilités dans UC Browser

Le code résultant est terrible. Mais, en parcourant sa jungle, vous remarquerez un appel à une fonction qui nous est déjà familière sub_6115C:

Recherche de vulnérabilités dans UC Browser

Il y avait un commutateur dans lequel, dans le cas 3, il y avait un décryptage à l'aide de l'algorithme RC4. Et dans ce cas, la structure passée à la fonction est remplie à partir des paramètres passés à doCommandNative. Souvenons-nous de ce que nous avions là-bas magieInt avec la valeur 16. On regarde le cas correspondant - et après plusieurs transitions on trouve le code par lequel l'algorithme peut être identifié.

Recherche de vulnérabilités dans UC Browser

C'est AES !

L'algorithme existe, il ne reste plus qu'à obtenir ses paramètres : mode, clé et, éventuellement, le vecteur d'initialisation (sa présence dépend du mode de fonctionnement de l'algorithme AES). La structure avec eux doit être formée quelque part avant l'appel de fonction sub_6115C, mais cette partie du code est particulièrement bien obscurcie, donc l'idée surgit de patcher le code afin que tous les paramètres de la fonction de décryptage soient vidés dans un fichier.

Patch

Afin de ne pas écrire manuellement tout le code du patch en langage assembleur, vous pouvez lancer Android Studio, y écrire une fonction qui reçoit les mêmes paramètres d'entrée que notre fonction de décryptage et écrit dans un fichier, puis copier-coller le code que le compilateur va générer.

Nos amis de l'équipe UC Browser se sont également occupés de la commodité de l'ajout de code. Rappelons qu'au début de chaque fonction nous avons du code poubelle qui peut facilement être remplacé par n'importe quel autre. Très pratique 🙂 ​​Cependant, au début de la fonction cible, il n'y a pas assez d'espace pour le code qui enregistre tous les paramètres dans un fichier. J'ai dû le diviser en parties et utiliser des blocs poubelles des fonctions voisines. Il y avait quatre parties au total.

Первая часть:

Recherche de vulnérabilités dans UC Browser

Dans l'architecture ARM, les quatre premiers paramètres de fonction passent par les registres R0-R3, les autres, le cas échéant, passent par la pile. Le registre LR porte l'adresse de retour. Tout cela doit être sauvegardé pour que la fonction puisse fonctionner après avoir vidé ses paramètres. Nous devons également sauvegarder tous les registres que nous utiliserons dans le processus, nous faisons donc PUSH.W {R0-R10,LR}. Dans R7 on obtient l'adresse de la liste des paramètres passés à la fonction via la pile.

Utilisation de la fonction ouvrir ouvrons le fichier /data/local/tmp/aes en mode "ab"
c'est-à-dire pour l'ajout. En R0 nous chargeons l'adresse du nom de fichier, en R1 - l'adresse de la ligne indiquant le mode. Et ici le code poubelle se termine, nous passons donc à la fonction suivante. Pour que cela continue à fonctionner, nous mettons au début la transition vers le vrai code de la fonction, en contournant les déchets, et à la place des déchets nous ajoutons une suite du patch.

Recherche de vulnérabilités dans UC Browser

Nous appelons ouvrir.

Les trois premiers paramètres de la fonction Aes avoir du type int. Puisque nous avons sauvegardé les registres dans la pile au début, nous pouvons simplement passer la fonction fécrire leurs adresses sur la pile.

Recherche de vulnérabilités dans UC Browser

Nous avons ensuite trois structures contenant la taille des données et un pointeur vers les données pour la clé, le vecteur d'initialisation et les données cryptées.

Recherche de vulnérabilités dans UC Browser

A la fin, fermez le fichier, restaurez les registres et transférez le contrôle à la fonction réelle Aes.

Nous collectons un APK avec une bibliothèque corrigée, le signons, le téléchargeons sur l'appareil/l'émulateur et le lançons. Nous voyons que notre dump est en cours de création et que de nombreuses données y sont écrites. Le navigateur utilise le cryptage non seulement pour le trafic, et tout le cryptage passe par la fonction en question. Mais pour une raison quelconque, les données nécessaires ne sont pas là et la demande requise n'est pas visible dans le trafic. Afin de ne pas attendre qu'UC Browser daigne faire la requête nécessaire, prenons la réponse cryptée du serveur reçue précédemment et corrigeons à nouveau l'application : ajoutons le décryptage à onCreate de l'activité principale.

    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

Nous assemblons, signons, installons, lançons. Nous recevons une NullPointerException car la méthode a renvoyé null.

Lors d'une analyse plus approfondie du code, une fonction a été découverte qui déchiffre les lignes intéressantes : « META-INF/ » et « .RSA ». Il semble que l'application vérifie son certificat. Ou même en génère des clés. Je ne veux pas vraiment m’occuper de ce qui se passe avec le certificat, alors nous allons simplement lui glisser le bon certificat. Corrigeons la ligne cryptée pour qu'au lieu de « META-INF/ », nous obtenions « BLABLINF/ », créons un dossier avec ce nom dans l'APK et ajoutons-y le certificat du navigateur écureuil.

Nous assemblons, signons, installons, lançons. Bingo ! Nous avons la clé !

MitM

Nous avons reçu une clé et un vecteur d'initialisation égal à la clé. Essayons de décrypter la réponse du serveur en mode CBC.

Recherche de vulnérabilités dans UC Browser

Nous voyons l'URL de l'archive, quelque chose de similaire à MD5, « extract_unzipsize » et un numéro. On vérifie : le MD5 de l'archive est le même, la taille de la bibliothèque décompressée est la même. Nous essayons de patcher cette bibliothèque et de la donner au navigateur. Pour montrer que notre bibliothèque de correctifs est chargée, nous lancerons une intention de créer un SMS avec le texte « PWNED ! » Nous remplacerons deux réponses du serveur : puds.ucweb.com/upgrade/index.xhtml et pour télécharger l'archive. Dans le premier, nous remplaçons MD5 (la taille ne change pas après le déballage), dans le second, nous donnons l'archive avec la bibliothèque corrigée.

Le navigateur essaie de télécharger l'archive plusieurs fois, après quoi il renvoie une erreur. Apparemment quelque chose
il ne aime pas. Suite à l'analyse de ce format trouble, il s'est avéré que le serveur transmet également la taille de l'archive :

Recherche de vulnérabilités dans UC Browser

Il est codé en LEB128. Après le correctif, la taille de l'archive avec la bibliothèque a légèrement changé, le navigateur a donc considéré que l'archive avait été téléchargée de manière incorrecte et, après plusieurs tentatives, il a généré une erreur.

Nous ajustons la taille de l'archive... Et – victoire ! 🙂 Le résultat est dans la vidéo.

https://www.youtube.com/watch?v=Nfns7uH03J8

Conséquences et réaction des développeurs

De la même manière, les pirates pourraient utiliser la fonctionnalité non sécurisée d'UC Browser pour distribuer et exécuter des bibliothèques malveillantes. Ces bibliothèques fonctionneront dans le contexte du navigateur et recevront donc toutes ses autorisations système. En résulte la possibilité d'afficher des fenêtres de phishing, ainsi que l'accès aux fichiers de travail de l'écureuil chinois orange, y compris les identifiants, mots de passe et cookies stockés dans la base de données.

Nous avons contacté les développeurs d'UC Browser et les avons informés du problème que nous avons trouvé, essayé de souligner la vulnérabilité et son danger, mais ils n'ont rien discuté avec nous. Pendant ce temps, le navigateur a continué à afficher sa fonctionnalité dangereuse à la vue de tous. Mais une fois que nous avons révélé les détails de la vulnérabilité, il n’était plus possible de l’ignorer comme avant. Le 27 mars était
une nouvelle version d'UC Browser 12.10.9.1193 a été publiée, qui accède au serveur via HTTPS : puds.ucweb.com/upgrade/index.xhtml.

De plus, après le « correctif » et jusqu'au moment de la rédaction de cet article, la tentative d'ouverture d'un PDF dans un navigateur entraînait un message d'erreur avec le texte « Oups, quelque chose s'est mal passé ! Aucune requête n'a été adressée au serveur lors de la tentative d'ouverture d'un PDF, mais une requête a été effectuée lors du lancement du navigateur, ce qui laisse entendre qu'il est toujours possible de télécharger du code exécutable en violation des règles de Google Play.

Source: habr.com

Ajouter un commentaire