
Introduction
À la fin mars, nous , avons découvert une vulnérabilité cachée permettant de télécharger et d'exécuter du code non vérifié dans UC Browser. Aujourd'hui, nous allons examiner en détail comment ce téléchargement se produit et comment les hackers peuvent l'exploiter à leurs fins.
Il y a quelque temps, UC Browser était commercialisé et diffusé de manière très agressive : il était installé sur les appareils des utilisateurs par le biais de logiciels malveillants, distribué depuis divers sites sous couvert de fichiers vidéo (c'est-à-dire que les utilisateurs pensaient qu'ils téléchargeaient, par exemple, une vidéo pour adultes, mais recevaient à la place un APK de ce navigateur), et utilisaient des bannières alarmantes annonçant que le navigateur était obsolète, vulnérable, et tout cela. Dans le groupe officiel de UC Browser sur VK, il y a , où les utilisateurs peuvent signaler de la publicité déloyale, avec de nombreux exemples. En 2016, il y a même eu en russe (oui, une publicité pour un navigateur qui bloque la publicité).
Au moment de la rédaction de cet article, UC Browser comptait plus de 500 000 000 installations sur Google Play. C'est impressionnant, seuls Google Chrome est au-dessus. Parmi les avis, on peut voir pas mal de plaintes concernant la publicité et les redirections vers diverses applications sur Google Play. Cela a suscité notre intérêt pour l'étude : nous avons décidé de vérifier si UC Browser faisait quelque chose de mal. Et il s'avère que oui !
Dans le code de l'application, nous avons découvert une possibilité de téléchargement et d'exécution de code exécutable, 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 pourrait être utilisé pour réaliser une attaque MitM. Voyons si nous pouvons mener une telle attaque.
Tout ce qui est écrit ci-dessous est pertinent pour la version d'UC Browser disponible sur Google Play au moment de l'étude :
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 du fichier APK : f5edb2243413c777172f6362876041eb0c3a928cVecteur d'attaque
Dans le manifeste de UC Browser, on peut trouver un service au nom évocateur com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Lorsqu'il démarre ce service, le navigateur effectue une requête POST vers , qui peut être observé dans le trafic après un certain temps après le lancement. En réponse, il peut recevoir une commande pour charger une mise à jour ou un nouveau module. Lors de l'analyse, le serveur n'a pas envoyé de telles commandes, mais nous avons remarqué qu'en essayant d'ouvrir un PDF dans le navigateur, il fait une nouvelle demande à l'adresse mentionné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é du UC Browser : la capacité d'ouvrir des PDF à l'aide d'une bibliothèque native, qui n'est pas présente dans l'APK et qu'il charge si nécessaire depuis Internet. Il convient de noter que théoriquement, le UC Browser peut être amené à télécharger quelque chose sans interaction de l'utilisateur – si une réponse correctement formée est donnée à la demande qui est exécutée après le démarrage du navigateur. Mais pour cela, il faut étudier plus en détail le protocole d'interaction avec le serveur, c'est pourquoi nous avons jugé plus simple d'éditer la réponse interceptée et de remplacer la bibliothèque pour le traitement des PDF.
Ainsi, lorsque l'utilisateur souhaite ouvrir un PDF directement dans le navigateur, on peut voir les requêtes suivantes dans le trafic :

Tout d'abord, il y a une requête POST à , après quoi
un archive contenant la bibliothèque pour visualiser des PDF et des formats bureautiques est téléchargé. Il est logique de supposer que dans la première demande, des informations sur le système (au minimum, l'architecture, afin de fournir la bonne bibliothèque) sont transmises, et en réponse, le navigateur reçoit certaines informations sur la bibliothèque à télécharger : adresse et, peut-être, d'autres informations. Le problème est que cette demande est chiffrée.
Fragment de la demande
Fragment de la réponse


La bibliothèque elle-même est empaquetée dans un ZIP et n'est pas chiffrée.

Recherche du code de déchiffrement 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 nous passons à com.uc.deployment.b.x, et de là à com.uc.browser.core.d.c.f.e:
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("]produit:");
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("]type_mise_a_niveau:");
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("]mode_silencieux:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]type_silencieux:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]etat_silencieux:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]fichier_silencieux:");
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("]type_telechargement:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]groupe_telechargement:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]chemin_telechargement:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version_enfant_apollo:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]serie_apollo:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]architecture_cpu_apollo:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]cpu_vfp3_apollo:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]cpu_vfp_apollo:");
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("]nom_composant:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]nom_ver_composant:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]code_ver_composant:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]type_req_composant:");
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("architecture_processeur", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("architecture_cpu", 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("type_net", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("version_plugin", arg9.iGX.iEn));
v3_2.add(g.fs("lang_cible", arg9.iGX.iEs));
v3_2.add(g.fs("architecture_vitamio_cpu", arg9.iGX.iEt));
v3_2.add(g.fs("vfp_vitamio", arg9.iGX.iEu));
v3_2.add(g.fs("vfp3_vitamio", arg9.iGX.iEv));
v3_2.add(g.fs("version_enfant_plugin", arg9.iGX.iEx));
v3_2.add(g.fs("serie_ver", arg9.iGX.iEw));
v3_2.add(g.fs("version_enfant", 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("journal_upgrade", i.bjt()));
v3_2.add(g.fs("installation_silencieuse", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("etat_silencieux", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("fichier_silencieux", arg9.iGX.iEq));
v3_2.add(g.fs("type_silencieux", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("architecture_cpu", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("ensemble_cpu", 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("cores_cpu", 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("ram_totale", 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("niveau_api", String.valueOf(Build.VERSION.SDK_INT)));
v3_2.add(g.fs("liste_apk_uc", 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 observons ici la formation d'une 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). Cela correspond à ce que nous avons vu dans la requête précédente.
Dans cette même classe, on peut remarquer une classe imbriquée qui contient 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 reçoit un tableau d'octets en entrée et vérifie que le premier octet est égal à 0x60 ou que le troisième octet est égal à 0xD0, tandis que le deuxième octet est 1, 11 ou 0x1F. Regardons la réponse du serveur : le premier octet est 0x60, le deuxième est 0x1F, le troisième est 0x60. Cela semble correspondre à ce dont nous avons besoin. À en juger par les lignes (« up_decrypt », par exemple), une méthode qui déchiffrera la réponse du serveur devrait être appelée ici.
Passons à la méthode g.j. Remarquons qu'en tant que premier argument, il prend l'octet à l'offset 2 (c'est-à-dire 0x1F dans notre cas), et en tant que second, la réponse du serveur sans
les premiers 16 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;
} Il est évident qu'ici, un algorithme de déchiffrement est sélectionné, et cet octet, qui dans notre
Dans le cas où il est égal à 0x1F, cela signifie l'une des trois options possibles.
Nous continuons l'analyse du code. Après quelques sauts, nous arrivons à une méthode dont le nom est explicite. decryptBytesByKey.
Ici, deux octets se séparent de notre réponse, et à partir de cela, nous obtenons une chaîne. Il est clair que c'est ainsi que la clé pour déchiffrer le message est choisie.
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 octets
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Choix de la clé
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;
}En allant de l'avant, notons qu'à ce stade, nous n'obtenons pas encore la clé, mais seulement son "identificateur". L'obtention de la clé est un peu plus compliquée.
Dans la méthode suivante, deux paramètres supplémentaires sont ajoutés, et nous en avons quatre : le nombre magique 16, l'identifiant de la 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 Le support de SVGGraphicsElement.nearestViewportElement et SVGGraphicsElement.farthestViewportElement a été supprimé, ces éléments ayant été déclarés obsolètes en février de cette année. com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Il n'y a pas de classes dans le code principal de l'application qui implémentent cette interface. Une telle classe existe dans le fichier. lib/armeabi-v7a/libsgmain.so, qui n'est en réalité pas 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 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
} Ici notre liste de paramètres est complétée par deux entiers supplémentaires : 2 et 0. Apparemment,
2 signifie déchiffrement, comme dans la méthode doFinal de la classe système javax.crypto.Cipher. Et tout cela est transmis à un certain Routeur avec le nombre 10601 — ce qui est probablement le numéro de commande.
Après une nouvelle chaîne de transitions, nous trouvons la classe qui implémente l'interface IRouterComponent et une 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 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 la méthode dans le code natif. doCommandNativeEt ici commence le vrai amusement.
L'obfuscation du code machine
Dans le fichier libsgmain.so (qui est en réalité un .jar et où nous avons trouvé tout à l'heure l'implémentation de certaines interfaces liées au chiffrement) contient une bibliothèque native : libsgmainso-6.4.36.so. Nous l'ouvrons dans IDA et recevons une multitude de fenêtres de dialogue avec des erreurs. Le problème est que la table des sections (section header table) est invalide. Cela a été fait délibérément pour compliquer l'analyse.

Mais elle n'est pas nécessaire : pour charger correctement un fichier ELF et l'analyser, il suffit d'avoir une table des segments (program header table). Donc, nous supprimons simplement la table des sections, en annulant les champs correspondants dans l'en-tête.

Nous ouvrons à nouveau le fichier dans IDA.
Il existe deux façons d'indiquer à la machine virtuelle Java où se trouve la mise en œuvre de la méthode, déclarée dans le code Java comme native. La première est de lui donner un nom de type Java_nom_du_paquet_NomDeClasse_nomDeMéthode.
Deuxième — l'enregistrer lors du chargement de la bibliothèque (dans la fonction JNI_OnLoad)
en appelant la fonction RegisterNatives.
Dans notre cas, si l'on utilise la première méthode, le nom doit être le suivant : Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Parmi les fonctions exportées, il n'y en a pas, il faut donc chercher l'appel RegisterNatives.
Allons dans la fonction JNI_OnLoad et voyons cette image :

Que se passe-t-il ici ? À première vue, le début et la fin de la fonction sont typiques pour l'architecture ARM. La première instruction sauvegarde dans la pile 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, qui contient l'adresse de retour de la fonction. La dernière instruction restaure les registres sauvegardés, et l'adresse de retour est immédiatement placée dans le registre PC — ainsi se fait le retour de la fonction. Mais en y regardant de plus près, on peut voir que l'avant-dernière instruction modifie l'adresse de retour sauvegardée dans la pile. Calculons quelle elle sera après
l'exécution du code. Dans R1, une certaine adresse 0xB130 est chargée, puis 5 lui est soustrait, ensuite elle est mise dans R0 et à cela s'ajoute 0x10. On obtient 0xB13B. Ainsi, IDA pense que dans la dernière instruction, il s'agit d'un retour normal de la fonction, alors qu'en réalité, il s'agit d'un saut 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 plus faible de l'adresse indique au processeur quel ensemble d'instructions est utilisé. C'est-à-dire que l'adresse est en réalité 0xB13A, et un dans le bit le plus faible indique le mode Thumb.
Au début de chaque fonction de cette bibliothèque, un tel « adaptateur » et
du code indésirable ont été ajoutés. Nous ne nous attarderons pas longtemps sur eux — souvenons-nous simplement
que le véritable début de presque toutes les fonctions se trouve un peu plus loin.
Étant donné qu'il n'y a pas de saut explicite vers 0xB13A dans le code, IDA ne reconnaît pas elle-même que le code se trouve à cet endroit. Pour cette raison, la plupart du code dans la bibliothèque n'est pas reconnue comme du code, ce qui complique un peu l'analyse. Disons à IDA que c'est du code, et voici ce que l'on obtient :

À 0xB144, une table commence clairement. Et que se trouve-t-il dans sub_494C ?

Lors de l'appel de cette fonction, le registre LR obtiendra l'adresse de la table mentionnée précédemment (0xB144). Dans R0 — l'index dans cette table. C'est-à-dire que la valeur de la table est prise, ajoutée à LR et obtenue.
l'adresse à laquelle il faut accéder. Essayons de le calculer : 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Nous accédons à l'adresse obtenue et voyons littéralement quelques instructions utiles et encore un passage à 0xB140 :

Il y aura maintenant un passage par décalage avec l'index 0x20 de la table.
À en juger par la taille de la table, il y aura beaucoup de tels passages dans le code. La question se pose : peut-on d'une manière plus automatisée se battre contre cela, sans calculer manuellement les adresses ? Et les scripts ainsi que la possibilité de patcher le code dans IDA viennent à notre aide :
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or 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 "Type d'opérande incorrect sur", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Impossible de trouver la table"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Code inconnu", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Impossible de détecter la première instruction"Mettons le curseur sur la ligne 0xB26A, lançons le script et voyons le passage à 0xB4B0 :

IDA n'a encore une fois pas reconnu cette zone comme du code. Nous l'aidons et voyons une autre construction là :

Les instructions après BLX semblent peu sensées, cela ressemble davantage à un certain décalage. Regardons dans sub_4964 :

Et effectivement, ici un dword est pris à l'adresse contenue dans LR, un ajout est effectué à cette adresse, puis une valeur est prise à l'adresse obtenue et placée sur la pile. De plus, 4 est ajouté à LR, afin que, après le retour de la fonction, ce décalage soit sauté. Ensuite, la commande POP {R1} récupère la valeur obtenue de la pile. Si l'on regarde ce qui se trouve à l'adresse 0xB4BA + 0xEA = 0xB5A4, on peut voir quelque chose ressemblant à une table d'adresses :

Pour patcher cette construction, vous devrez obtenir deux paramètres du code : le décalage et le numéro du registre dans lequel placer le résultat. Un morceau de code devra être préparé à 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 <> 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 "Instruction POP non trouvée"
else:
print "Type d'opérande incorrect sur +4:", get_operand_type(ea + 4, 0)
else:
print "Impossible de détecter les premières instructions"Placez le curseur au début de la construction que vous souhaitez remplacer — 0xB4B2 — et lancez le script :

En plus des constructions déjà mentionnées, on trouve également celles-ci dans le code :

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

Prenez le décalage à partir de l'adresse dans LR, additionnez-le à LR et allez là-bas. 0x72044 + 0xC = 0x72050. Le script pour cette construction est très simple :
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or 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 "Impossible de détecter la première instruction"Résultat de l'exécution du script :

Une fois que la fonction est patchée, on peut indiquer à IDA son véritable début. Elle rassemblera le code de la fonction morceau par morceau, et il pourra être décompilé à l'aide de HexRays.
Déchiffrement des chaînes
Nous avons appris à lutter contre l'obfuscation du code machine dans la bibliothèque libsgmainso-6.4.36.so de UC Browser et nous avons obtenu le code de la 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 de plus près les lignes suivantes :
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);Dans la fonction sub_73E24 il est clairement question de déchiffrer le nom de la classe. En tant que paramètres de cette fonction, un pointeur sur des données similaires à des données chiffrées, un certain tampon et un nombre sont passés. Il est évident qu'après l'appel de la fonction, le tampon contiendra une chaîne déchiffrée, car il est passé à la fonction FindClass, qui accepte comme deuxième paramètre le nom de la classe. Cela signifie que le nombre est la taille du tampon ou la longueur de la chaîne. Essayons de déchiffrer le nom de la classe, cela devrait nous indiquer si nous allons dans la bonne direction. Voyons plus en détail 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 des tableaux d'octets de la taille spécifiée (nous ne nous attarderons pas sur ces conteneurs). Ici, deux de ces conteneurs sont créés : l'un reçoit la chaîne «DcO/lcK+h?m3c*q@» (il est facile de deviner que c'est une clé), l'autre - les données cryptées. Ensuite, les deux objets sont placés dans une certaine structure, qui est transmise à la fonction sub_6115C. Notons également dans cette structure un champ avec la valeur 3. Voyons ce qui se passe avec cette structure par la suite.
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 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;
}En tant que paramètre switch, nous passons un champ de la structure qui a été précédemment affecté à la valeur 3. Examinons case 3 : dans la fonction sub_6364C les paramètres de la structure qui ont été ajoutés lors de la fonction précédente, c'est-à-dire la clé et les données chiffrées. Si l'on regarde attentivement sub_6364C, on peut y découvrir l'algorithme RC4.
Nous avons l'algorithme et la clé. Essayons de déchiffrer le nom de la classe. Voici ce que nous avons obtenu : com/taobao/wireless/security/adapter/JNICLibrary. Parfait ! Nous sommes sur la bonne voie.
Arbre des commandes
Maintenant, il faut trouver l'appel RegisterNatives, qui nous indiquera la fonction doCommandNative. Nous vérifions 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, un méthode native est enregistrée ici avec le nom doCommandNative. Maintenant, nous connaissons son adresse. Voyons ce qu'elle 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;
}D'après le nom, on peut supposer qu'il s'agit du point d'entrée de toutes les fonctions que les développeurs ont décidé de transférer dans la bibliothèque native. Nous sommes intéressés par la fonction numéro 10601.
Le code permet de voir que trois nombres sont extraits du numéro de commande : command / 10000, command % 10000 / 100 et command % 10, c'est-à-dire, dans notre cas, 1, 6 et 1. Ces trois nombres, ainsi qu'un pointeur vers JNIEnv et les arguments passés à la fonction, sont rassemblés dans une structure et transmis ensuite. Avec les trois nombres obtenus (appelons-les N1, N2 et N3), un arbre de commandes est construit.
Cela ressemble à :

L'arbre se remplit dynamiquement dans JNI_OnLoad.
Trois nombres codent le chemin dans l'arbre. Chaque feuille de l'arbre contient l'adresse correspondante de la fonction. La clé se trouve dans le nœud parent. Trouver l'endroit dans le code où la fonction souhaitée est ajoutée à l'arbre n'est pas très difficile si l'on comprend toutes les structures utilisées (nous ne fournissons pas leur description afin de ne pas alourdir cet article déjà conséquent).
Encore de l'obfuscation
Nous avons obtenu l'adresse de la fonction qui doit déchiffrer le trafic : 0x5F1AC. Mais il ne faut pas se réjouir trop tôt : les développeurs de UC Browser nous ont préparé une autre surprise.
Après avoir obtenu les paramètres du tableau qui a été formé dans le code Java, nous atteignons
la fonction à l'adresse 0x4D070. Et là, nous faisons face à un autre type d'obfuscation de code.
Nous plaçons dans R7 et R4 deux indices :

Nous transférons le premier indice dans R11 :

Pour obtenir l'adresse du tableau, nous utilisons l'indice :

Après être passé par la première adresse, nous utilisons le second indice, qui est dans R4. Le tableau contient 230 éléments.
Que faire avec ça ? On peut dire à IDA que c'est un switch : Édition -> Autre -> Spécifier l'idiome switch.

Le code résultant est effrayant. Mais en naviguant à travers ses méandres, on peut remarquer l'appel d'une fonction que nous connaissons déjà. sub_6115C:

Il y avait un switch, dans lequel le cas 3 contenait le déchiffrement utilisant l'algorithme RC4. Dans ce cas, la structure transmise à la fonction est remplie à partir des paramètres transmis dans doCommandNative. Nous nous souvenons qu'il y avait magicInt avec la valeur 16. Nous consultons le cas correspondant – et après quelques transitions, nous trouvons le code qui permet d'identifier l'algorithme.

C'est l'AES !
L'algorithme est identifié, il reste à obtenir ses paramètres : mode, clé et, peut-être, vecteur d'initialisation (sa présence dépend du mode de fonctionnement de l'algorithme AES). La structure contenant ces paramètres doit être formée quelque part avant l'appel de la fonction sub_6115C, mais cette partie du code est particulièrement bien obfusquée, c'est pourquoi l'idée de patcher le code émerge, afin que tous les paramètres de la fonction de déchiffrement soient sauvegardés dans un fichier.
Patch
Pour éviter d'écrire tout le code du patch en langage assembleur à la main, on peut lancer Android Studio, y écrire une fonction qui prend en entrée les mêmes paramètres que notre fonction de déchiffrement, et écrit dans un fichier, puis coller le code que générera le compilateur.
Nos amis de l'équipe UC Browser se sont également occupés de la commodité d'ajout de code. Rappelons-nous qu'au début de chaque fonction, nous avons du code temporaire qui peut facilement être remplacé par n'importe quel autre. C'est très pratique 🙂 Cependant, au début de la fonction cible, il y a peu d'espace pour le code qui enregistre tous les paramètres dans un fichier. Nous avons dû le diviser en plusieurs parties et utiliser les blocs temporaires des fonctions adjacentes. Au total, cela a donné quatre parties.
Première partie :

Dans l'architecture ARM, les quatre premiers paramètres de la fonction sont transmis via les registres R0-R3, les autres, s'il y en a, sont transmis via la pile. L'adresse de retour est transmise dans le registre LR. Tout cela doit être sauvegardé pour que la fonction puisse fonctionner après que nous ayons dumpé ses paramètres. Nous devons également sauvegarder tous les registres que nous utiliserons dans le processus, donc nous faisons PUSH.W {R0-R10,LR}. Dans R7, nous obtenons l'adresse de la liste des paramètres transmis à la fonction via la pile.
En utilisant la fonction fopen nous allons ouvrir le fichier /data/local/tmp/aes en mode « ab »,
c'est-à-dire pour ajout. Nous chargeons l'adresse du nom du fichier dans R0, l'adresse de la chaîne du mode dans R1. C'est ici que le code temporaire se termine, nous allons donc à la fonction suivante. Pour qu'elle continue à fonctionner, nous plaçons au début un saut vers le code réel de la fonction en contournant les déchets, et au lieu des déchets, nous ajoutons la continuation du patch.

Nous appelons fopen.
Les trois premiers paramètres de la fonction aes sont de type int. Comme nous avons au début sauvegardé les registres dans la pile, nous pouvons simplement transmettre à la fonction fwrite leurs adresses dans la pile.

Ensuite, nous avons trois structures qui contiennent la taille des données et un pointeur vers les données pour la clé, le vecteur d'initialisation et les données chiffrées.

Enfin, nous fermons le fichier, restaurons les registres et transférons le contrôle à la fonction réelle aes.
Nous rassemblons un APK avec la bibliothèque patchée, le signons, le téléchargeons sur l'appareil/emulateur, puis le lançons. Nous voyons que notre dump est créé, et beaucoup de données y sont écrites. Le navigateur utilise le chiffrement non seulement pour le trafic, et tout le chiffrement passe par la fonction examinée. Cependant, les données nécessaires manquent pour une raison quelconque, et la requête souhaitée n'est pas visible dans le trafic. Pour ne pas attendre que UC Browser daigne faire la requête nécessaire, prenons la réponse chiffrée du serveur obtenue précédemment et patchons à nouveau l'application : ajoutons le déchiffrement dans 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;)INous rassemblons, signons, installons, lançons. Nous obtenons NullPointerException, car la méthode a renvoyé null.
Au cours de l'analyse du code, une fonction a été découverte, dans laquelle des lignes intéressantes sont décryptées : «META-INF/» et «.RSA». Il semble que l'application vérifie son certificat. Ou même génère des clés à partir de celui-ci. Je n'ai pas du tout envie de comprendre ce qui se passe avec le certificat, alors nous allons simplement lui fournir le bon certificat. Nous allons patcher la ligne chiffrée de telle manière que, au lieu de «META-INF/», cela devienne «BLABLINF/», créer un dossier avec ce nom dans l'APK et y glisser le certificat du navigateur blanc.
Nous rassemblons, signons, installons, lançons. Bingo ! Nous avons la clé !
MitM
Nous avons obtenu la clé et le vecteur d'initialisation, égaux à la clé. Essayons de déchiffrer la réponse du serveur en mode CBC.

Nous voyons l'URL de l'archive, quelque chose qui ressemble à MD5, «extract_unzipsize» et un nombre. Vérifions : le MD5 de l'archive correspond, la taille de la bibliothèque décompressée correspond. Essayons de patcher cette bibliothèque et de la renvoyer au navigateur. Pour montrer que notre bibliothèque patchée a été chargée, nous allons lancer un Intent pour créer un SMS avec le texte «PWNED!». Nous modifierons deux réponses du serveur : et pour le téléchargement de l'archive. Dans le premier, nous modifions le MD5 (la taille après décompression ne change pas), dans le second, nous fournissons l'archive avec la bibliothèque patchée.
Le navigateur essaie plusieurs fois de télécharger l'archive, après quoi il génère une erreur. Apparemment, quelque chose
ne lui plaît pas. À l'issue de l'analyse de ce format trouble, il a été découvert que le serveur transmet également la taille de l'archive :

Elle est codée en LEB128. Après le patch, la taille de l'archive avec la bibliothèque a légèrement changé, c'est pourquoi le navigateur a pensé que l'archive avait été mal téléchargée, et après plusieurs tentatives, il a généré une erreur.
Nous corrigeons la taille de l'archive… Et – victoire ! 🙂 Le résultat en vidéo.
Conséquences et réaction du développeur
De la même manière, les hackers pourraient utiliser la fonction non sécurisée d'UC Browser pour diffuser et exécuter des bibliothèques malveillantes. Ces bibliothèques fonctionneraient dans le contexte du navigateur, ce qui leur donnerait tous ses droits d'accès système. Par conséquent, cela permettrait d'afficher des fenêtres de phishing, ainsi qu'un 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 pour les informer du problème trouvé, en tentant de leur signaler la vulnérabilité et son danger, mais ils n'ont pas souhaité discuter avec nous. Pendant ce temps, le navigateur continuait à arborer la fonction dangereuse au grand jour. Cependant, dès 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, une nouvelle version d'UC Browser 12.10.9.1193 a été
publiée, qui se connectait au serveur via HTTPS : .
De plus, après la "correction" et jusqu'au moment de la rédaction de l'article, tenter d'ouvrir un PDF dans le navigateur entraînait l'apparition d'un message d'erreur indiquant : "Oups, quelque chose s'est mal passé !". La requête au serveur lors de la tentative d'ouverture du PDF n'était pas exécutée, mais la requête lors du lancement du navigateur l'était, ce qui suggère qu'il restait une possibilité de télécharger du code exécutable en violation des règles de Google Play.
Source : habr.com
