UC-Browser brechen



Einführung


Ende März berichteten wir über das versteckte Potenzial, nicht verifizierten Code im UC-Browser herunterzuladen und auszuführen. Heute werden wir im Detail untersuchen, wie es passiert und wie Hacker es verwenden können.

Vor einiger Zeit wurde UC Browser ziemlich aggressiv beworben und verbreitet. Es wurde von Malware auf Geräten installiert und über Websites unter dem Deckmantel von Videodateien verbreitet (d. H. Benutzer dachten, sie würden Pornografie oder ähnliches herunterladen, aber stattdessen APK-Dateien mit diesem Browser abrufen), und mit besorgniserregenden Bannern beworben, dass der Browser eines Benutzers veraltet sei oder anfällig. Die offizielle UC Browser VK-Gruppe hatte ein Thema, bei dem sich Benutzer über falsche Werbung beschweren konnten, und viele Benutzer lieferten Beispiele. Im Jahr 2016 gab es sogar einen Werbespot in russischer Sprache (ja, einen Werbespot eines Browsers, der Werbespots blockiert).

Während wir diesen Artikel schreiben, wurde UC Browser 500.000.000 Mal von Google Play installiert. Dies ist beeindruckend, da nur Google Chrome das geschafft hat. In den Bewertungen finden Sie viele Beschwerden von Nutzern über Werbung und die Weiterleitung zu anderen Anwendungen bei Google Play. Dies war der Grund für unsere Studie: Wir wollten sehen, ob UC Browser etwas falsch macht. Und das ist es! Die Anwendung kann ausführbaren Code herunterladen und ausführen, was gegen die Richtlinien von Google Play für die Veröffentlichung von Apps verstößt . Und UC Browser lädt nicht nur ausführbaren Code herunter. Dies geschieht unsicher, was für einen MitM-Angriff verwendet werden kann. Mal sehen, ob wir es so nutzen können.

Alles, was folgt, gilt für die Version von UC Browser, die zum Zeitpunkt unserer Studie über Google Play verbreitet wurde:

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

Angriffsvektor


Das Manifest des UC-Browsers enthält einen Dienst mit dem verräterischen Namen com.uc.deployment.UpgradeDeployService .

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

Wenn dieser Dienst gestartet wird , sendet der Browser eine POST-Anforderung an puds.ucweb.com/upgrade/index.xhtml , die einige Zeit nach dem Start im Datenverkehr angezeigt wird. Als Antwort erhält der Browser möglicherweise einen Befehl zum Herunterladen eines Updates oder eines neuen Moduls. Während unserer Analyse haben wir solche Befehle nie vom Server erhalten, aber wir haben festgestellt, dass beim Versuch, eine PDF-Datei im Browser zu öffnen, die Anforderung an die oben angegebene Adresse wiederholt und dann eine native Bibliothek heruntergeladen wird. Um einen Angriff zu simulieren, haben wir uns für diese Funktion des UC-Browsers entschieden - die Möglichkeit, PDF-Dateien mithilfe einer nativen Bibliothek zu öffnen -, die nicht in der APK-Datei vorhanden ist, aber aus dem Internet heruntergeladen werden kann. Technisch gesehen kann UC Browser etwas ohne die Erlaubnis eines Benutzers herunterladen, wenn eine entsprechende Antwort auf eine beim Start gesendete Anfrage gegeben wird. Dafür müssen wir jedoch das Interaktionsprotokoll mit dem Server genauer untersuchen. Daher dachten wir, es sei einfacher, die Antwort einfach zu verknüpfen und zu bearbeiten und dann die zum Öffnen von PDF-Dateien erforderliche Bibliothek durch etwas anderes zu ersetzen.

Wenn ein Benutzer eine PDF-Datei direkt im Browser öffnen möchte, kann der Datenverkehr die folgenden Anforderungen enthalten:



Zuerst erfolgt eine POST-Anfrage an puds.ucweb.com/upgrade/index.xhtml , dann wird die komprimierte Bibliothek zum Anzeigen von PDF-Dateien und Office-Dokumenten heruntergeladen. Logischerweise können wir davon ausgehen, dass die erste Anforderung Informationen über das System sendet (zumindest die Architektur, da der Server eine geeignete Bibliothek auswählen muss), und der Server antwortet mit einigen Informationen über die Bibliothek, die heruntergeladen werden muss, wie z. B. ihrer Adresse und vielleicht noch etwas anderes. Das Problem ist, dass diese Anfrage verschlüsselt ist.

Fragment anfordern

Antwortfragment






Die Bibliothek wird in einer ZIP-Datei komprimiert und nicht verschlüsselt.



Suche nach Verkehrsentschlüsselungscode


Versuchen wir, die Antwort des Servers zu entschlüsseln. Sehen Sie sich den Code der Klasse com.uc.deployment.UpgradeDeployService an : Von der onStartCommand-Methode navigieren wir zu com.uc.deployment.bx und dann zu 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); } 

Wir können sehen, dass hier die POST-Anfrage gestellt wird. Schauen Sie sich das 16-Byte-Array an, das Folgendes enthält: 0x5F, 0, 0x1F, -50 (= 0xCE). Die Werte sind die gleichen wie in der obigen Anfrage.

Dieselbe Klasse enthält eine verschachtelte Klasse mit einer anderen interessanten Methode:

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

Diese Methode empfängt eine Eingabe eines Bytearrays und prüft, ob das Nullbyte 0x60 oder das dritte Byte 0xD0 ist und ob das zweite Byte 1, 11 oder 0x1F ist. Überprüfen Sie die Serverantwort: Null-Byte ist 0x60, das zweite Byte ist 0x1F, das dritte Byte ist 0x60. Es sieht so aus, wie wir es brauchen. Gemessen an den Zeichenfolgen (z. B. "up_decrypt") soll hier eine Methode aufgerufen werden, um die Serverantwort zu entschlüsseln. Schauen wir uns nun die gj-Methode an. Beachten Sie, dass das erste Argument ein Byte bei Offset 2 ist (in unserem Fall 0x1F) und das zweite die Serverantwort ohne die ersten 16 Bytes.

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

Offensichtlich wird der Entschlüsselungsalgorithmus ausgewählt, und das Byte, das in unserem Fall gleich 0x1F ist, gibt eine von drei möglichen Optionen an.

Kehren wir zur Code-Analyse zurück. Nach ein paar Sprüngen gelangen wir zur Methode mit dem verräterischen Namen decryptBytesByKey . Jetzt werden zwei weitere Bytes von unserer Antwort getrennt und bilden eine Zeichenfolge. Es ist klar, dass auf diese Weise der Schlüssel zum Entschlüsseln der Nachricht ausgewählt wird.

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

Beachten Sie, dass es sich in diesem Stadium nur um die Schlüsselkennung handelt, nicht um den Schlüssel selbst. Die Schlüsselauswahl wird etwas komplizierter.

Bei der nächsten Methode werden zwei weitere Parameter zu den vorhandenen hinzugefügt, sodass insgesamt vier erhalten werden. Die magische Zahl 16, die Schlüsselkennung, die verschlüsselten Daten und eine Zeichenfolge werden dort aus irgendeinem Grund hinzugefügt (in unserem Fall leer).

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

Nach einer Reihe von Sprüngen sehen wir die staticBinarySafeDecryptNoB64- Methode der Schnittstelle com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . Der Hauptanwendungscode enthält keine Klassen, die diese Schnittstelle implementieren. Diese Klasse ist in der Datei lib / armeabi-v7a / libsgmain.so enthalten , die nicht wirklich .SO, sondern .JAR ist. Die Methode, an der wir interessiert sind, wird wie folgt implementiert:

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

Hier wird unsere Parameterliste durch zwei weitere Ganzzahlen ergänzt: 2 und 0. Anscheinend bedeutet 2 Entschlüsselung, wie bei der doFinal-Methode der Systemklasse javax.crypto.Cipher . Diese Informationen werden dann zusammen mit der Nummer 10601, die anscheinend die Befehlsnummer ist, an einen bestimmten Router übertragen.

Nach der nächsten Sprungkette finden wir eine Klasse, die die RouterComponent- Schnittstelle und die doCommand- Methode implementiert :

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

Es gibt auch die JNIC Library- Klasse, in der die native doCommandNative- Methode deklariert ist:

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

Wir müssen also die doCommandNative- Methode im nativen Code finden. Hier beginnt der Spaß.

Verschleierung des Maschinencodes


In der Datei libsgmain.so befindet sich eine native Bibliothek (die eigentlich eine .JAR-Datei ist und, wie oben erwähnt, einige verschlüsselungsbezogene Schnittstellen implementiert): libsgmainso-6.4.36.so . Wir laden es in IDA und erhalten eine Reihe von Dialogen mit Fehlermeldungen. Das Problem ist, dass die Abschnittsüberschriften-Tabelle ungültig ist. Dies geschieht absichtlich, um die Analyse zu erschweren.



Aber wir brauchen es sowieso nicht wirklich. Die Programm-Header-Tabelle reicht aus, um die ELF-Datei korrekt zu laden und zu analysieren. Also löschen wir einfach die Abschnittsüberschriften-Tabelle und setzen die entsprechenden Felder in der Überschrift auf Null.



Dann öffnen wir die Datei in IDA erneut.

Wir haben zwei Möglichkeiten, der virtuellen Java-Maschine genau mitzuteilen, wo die native Bibliothek die Implementierung der im Java-Code als native deklarierten Methode enthält. Der erste besteht darin, ihm einen Namen wie diesen zu geben: Java_package_name_ClassName_methodName . Die zweite Möglichkeit besteht darin, sie beim Laden der Bibliothek (in der Funktion JNI_OnLoad) durch Aufrufen der Funktion RegisterNatives zu registrieren. In unserem Fall sollte der Name bei Verwendung der ersten Methode folgendermaßen lauten : Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . Die Liste der exportierten Funktionen enthält diesen Namen nicht, was bedeutet, dass wir nach den RegisterNatives suchen müssen. Wir gehen also zur Funktion JNI_OnLoad und sehen Folgendes:



Was ist hier los? Anfang und Ende der Funktion sind auf den ersten Blick typisch für die ARM-Architektur. Der erste Befehl überträgt den Inhalt der Register, die die Funktion verwendet, an den Stapel (in diesem Fall R0, R1 und R2) sowie den Inhalt des LR-Registers mit der Rücksprungadresse der Funktion. Der letzte Befehl stellt die gespeicherten Register wieder her und legt die Rücksprungadresse in das PC-Register, wodurch von der Funktion zurückgekehrt wird. Bei näherer Betrachtung stellen wir jedoch möglicherweise fest, dass der vorletzte Befehl die auf dem Stapel gespeicherte Rücksprungadresse ändert. Berechnen wir, wie es sein wird, wenn der Code ausgeführt wird. Die Adresse 0xB130 wird in R1 geladen, von 5 subtrahiert, dann nach R0 verschoben und erhält eine Addition von 0x10. Am Ende ist es gleich 0xB13B. Somit glaubt IDA, dass der endgültige Befehl eine normale Funktionsrückgabe ausführt, während er tatsächlich eine Übertragung an die berechnete Adresse 0xB13B durchführt.

Wir möchten Sie nun daran erinnern, dass ARM-Prozessoren zwei Modi und zwei Befehlssätze haben - ARM und Thumb. Das niederwertige Bit der Adresse bestimmt, welchen Befehlssatz der Prozessor verwendet. Das heißt, die Adresse ist tatsächlich 0xB13A, während der Wert im niederwertigen Bit den Daumenmodus anzeigt.
Ein ähnlicher „Adapter“ und etwas semantischer Müll werden am Anfang jeder Funktion in dieser Bibliothek hinzugefügt. Aber wir werden nicht im Detail darauf eingehen. Denken Sie daran, dass der eigentliche Beginn fast aller Funktionen etwas weiter entfernt ist.

Da im Code kein expliziter Übergang zu 0xB13A vorhanden ist, kann IDA nicht erkennen, dass dort Code vorhanden ist. Aus dem gleichen Grund wird der größte Teil des Codes in der Bibliothek nicht als Code erkannt, was die Analyse etwas schwieriger macht. Also sagen wir IDA, dass es Code gibt, und Folgendes passiert:



Ab 0xB144 haben wir die Tabelle klar. Aber was ist mit sub_494C?



Beim Aufruf dieser Funktion im LR-Register erhalten wir die Adresse der oben genannten Tabelle (0xB144). R0 enthält den Index in dieser Tabelle. Das heißt, wir nehmen den Wert aus der Tabelle, fügen ihn zu LR hinzu und erhalten die Adresse, zu der wir gehen müssen. Versuchen wir es zu berechnen: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Wir navigieren zu dieser Adresse und sehen einige nützliche Anweisungen. Dann gehen wir zu 0xB140:



Jetzt gibt es einen Übergang am Offset mit dem Index 0x20 von der Tabelle.
Gemessen an der Größe der Tabelle gibt es viele solcher Übergänge im Code. Wir möchten uns also automatisch darum kümmern und vermeiden, Adressen manuell zu berechnen. Daher helfen uns Skripte und die Möglichkeit, Code in IDA zu patchen:

 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" 

Wir setzen den Cursor auf die Zeichenfolge 0xB26A, führen das Skript aus und sehen den Übergang zu 0xB4B0:



Auch hier erkennt IDA diesen Ort nicht als Code. Wir helfen es und sehen dort eine andere Struktur:



Anweisungen, die nach BLX gehen, erscheinen nicht sehr aussagekräftig. Sie sind eher eine Art Versatz. Wir schauen uns sub_4964 an:



Tatsächlich nimmt es DWORD an der Adresse von LR, fügt es dieser Adresse hinzu, nimmt dann den Wert an der resultierenden Adresse und speichert ihn im Stapel. Zusätzlich werden 4 zu LR hinzugefügt, um denselben Offset zu springen, nachdem von der Funktion zurückgekehrt wurde. Dann nimmt der Befehl POP {R1} den resultierenden Wert vom Stapel. Wenn wir uns ansehen, was sich unter der Adresse 0xB4BA + 0xEA = 0xB5A4 befindet, sehen wir etwas Ähnliches wie in der Adresstabelle:



Um diese Struktur zu patchen, müssen wir zwei Parameter aus dem Code abrufen: den Offset und die Registernummer, an die wir das Ergebnis senden möchten. Wir müssen für jedes mögliche Register im Voraus einen Code vorbereiten.

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

Wir setzen den Cursor an den Anfang der Struktur, die wir ersetzen möchten (dh 0xB4B2) und führen das Skript aus:



Zusätzlich zu den bereits erwähnten Strukturen enthält der Code Folgendes:



Wie im vorherigen Fall gibt es nach dem BLX-Befehl einen Offset:



Wir nehmen den Offset an der Adresse von LR, fügen ihn LR hinzu und navigieren dorthin. 0x72044 + 0xC = 0x72050. Das Skript für diese Struktur ist recht einfach:

 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" 

Das Ergebnis der Ausführung des Skripts:



Nachdem wir alles in dieser Funktion gepatcht haben, können wir die IDA auf ihren tatsächlichen Anfang verweisen. Es wird den gesamten Funktionscode Stück für Stück sammeln und wir können ihn mit HexRays dekompilieren.

Die Zeichenfolgen entschlüsseln


Wir haben gelernt, wie man mit der Verschleierung von Maschinencode in der Bibliothek libsgmainso-6.4.36.so vom UC-Browser umgeht, und haben den Funktionscode JNI_OnLoad erhalten .

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

Schauen wir uns die folgenden Zeichenfolgen an:

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

Es ist ziemlich klar, dass die Funktion sub_73E24 den Klassennamen entschlüsselt. Die Parameter dieser Funktion enthalten einen Zeiger auf die Daten, die den verschlüsselten ähnlich sind, eine Art Puffer und eine Zahl. Offensichtlich befindet sich nach einem Aufruf der Funktion eine entschlüsselte Zeichenfolge im Puffer, da der Puffer an die FindClass- Funktion geht, die denselben Klassennamen wie der zweite Parameter erhält. Die Zahl ist also die Größe des Puffers oder die Länge der Zeichenfolge. Versuchen wir, den Namen der Klasse zu entschlüsseln. Es sollte anzeigen, ob wir in die richtige Richtung gehen. Schauen wir uns genauer an, was in sub_73E24 passiert.

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

Die Funktion sub_7AF78 erstellt eine Containerinstanz für Byte-Arrays der angegebenen Größe (wir werden uns nicht im Detail darauf konzentrieren). Hier werden zwei solcher Container erstellt: Einer enthält die Zeichenfolge " DcO / lcK + h? M3c * q @ " (es ist leicht zu erraten, dass dies der Schlüssel ist), der andere enthält die verschlüsselten Daten. Beide Objekte werden dann in einer bestimmten Struktur platziert, die an die Funktion sub_6115C übertragen wird. Wir können auch feststellen, dass diese Struktur ein Feld mit dem Wert 3 enthält. Mal sehen, was als nächstes passiert.

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

Das Feld mit dem zuvor zugewiesenen Wert 3 wird als Schalterparameter übergeben. Schauen wir uns Fall 3 an - Parameter, die die der Struktur hinzugefügte vorherige Funktion (dh der Schlüssel und die verschlüsselten Daten) in die Funktion sub_6364C überführt. Wenn wir uns sub_6364C genau ansehen, können wir den RC4-Algorithmus erkennen.

Wir haben also einen Algorithmus und einen Schlüssel. Versuchen wir, den Namen der Klasse zu entschlüsseln. Folgendes haben wir: com / taobao / wireless / security / adapter / JNICLibrary. Genial! Wir sind auf dem richtigen Weg.

Befehlsbaum


Jetzt müssen wir den Aufruf von RegisterNatives finden , der uns auf die Funktion doCommandNative verweist . Also schauen wir uns die von JNI_OnLoad aufgerufenen Funktionen an und finden sie in 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; } 

Tatsächlich ist hier eine native Methode mit dem Namen doCommandNative registriert. Jetzt kennen wir seine Adresse. Schauen wir uns an, was es tut.

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

Der Name deutet darauf hin, dass dies der Einstiegspunkt für alle Funktionen ist, die die Entwickler in die native Bibliothek übertragen haben. Wir sind speziell an der Funktionsnummer 10601 interessiert.

Aus dem Code können wir erkennen, dass die Befehlsnummer drei Zahlen enthält: Befehl / 10000, Befehl% 10000/100 und Befehl% 10 (in unserem Fall 1, 6 und 1). Diese drei Zahlen sowie der Zeiger auf JNIEnv und die an die Funktion übertragenen Argumente bilden eine Struktur und werden weitergegeben. Mit diesen drei Zahlen (wir bezeichnen sie als N1, N2 und N3) wird ein Befehlsbaum erstellt. So etwas wie das:



Der Baum wird dynamisch in JNI_OnLoad erstellt. Drei Zahlen kodieren den Pfad im Baum. Jedes Blatt des Baums enthält die xorred-Adresse der entsprechenden Funktion. Der Schlüssel befindet sich im übergeordneten Knoten. Es ist ziemlich einfach, eine Stelle im Code zu finden, an der die benötigte Funktion zum Baum hinzugefügt wird, wenn wir alle Strukturen dort verstehen (wir werden keine Zeit damit verbringen, sie in diesem Artikel zu beschreiben).

Mehr Verschleierung


Wir haben die Adresse der Funktion, die den Datenverkehr entschlüsseln soll: 0x5F1AC. Aber es ist noch zu früh, um sich zu entspannen - die UC-Browser-Entwickler haben eine weitere Überraschung für uns.

Nachdem wir die Parameter von einem Array im Java-Code erhalten haben, gehen wir zur Funktion bei 0x4D070. Eine andere Art der Codeverschleierung wartet bereits.

Wir drücken dann zwei Indizes in R7 und R4:



Der erste Index bewegt sich zu R11:



Wir verwenden diesen Index, um die Adresse aus der Tabelle zu erhalten:



Nach der Übertragung an die erste Adresse verwenden wir den zweiten Index von R4. Die Tabelle enthält 230 Elemente.

Was machen wir damit? Wir könnten der IDA sagen, dass es sich um eine Art Schalter handelt: Bearbeiten -> Andere -> Schalter-Idiom angeben.



Der resultierende Code ist schrecklich.Wir können jedoch den Aufruf der bekannten sub_6115C- Funktion in ihren Verwicklungen sehen :



In Fall 3 gab es den switch-Parameter mit der RC4-Entschlüsselung. In diesem Fall wird die Struktur, die an die Funktion übertragen wird, mit den an doCommandNative übertragenen Parametern gefüllt. Wir erinnern uns, dass wir dort magicInt mit dem Wert 16 hatten. Wir betrachten den entsprechenden Fall und finden nach mehreren Übergängen den Code, mit dem wir den Algorithmus identifizieren können.



Es ist AES!

Wir haben einen Algorithmus und müssen nur seine Parameter wie Modus, Schlüssel und (möglicherweise) den Initialisierungsvektor abrufen (sein Vorhandensein hängt vom Betriebsmodus des AES-Algorithmus ab). Die Struktur, die sie enthält, sollte irgendwo erstellt werden, bevor die Funktion sub_6115C aufgerufen wird. Da dieser Teil des Codes jedoch besonders gut verschleiert ist, haben wir beschlossen, den Code zu patchen, damit alle Parameter der Entschlüsselungsfunktion in eine Datei kopiert werden können.

Patch


Wenn Sie nicht den gesamten Patch-Code manuell in der Assemblersprache schreiben möchten, können Sie Android Studio ausführen, eine Funktion codieren, die dieselben Parameter wie unsere Entschlüsselungsfunktion empfängt und in die Datei schreibt, und dann den vom Compiler

Unsere guten Freunde vom UC Browser-Team haben auch das bequeme Hinzufügen von Code „sichergestellt“. Wir erinnern uns, dass wir am Anfang jeder Funktion Müllcode haben, der leicht durch jeden anderen Code ersetzt werden kann. Sehr praktisch :) Am Anfang der Zielfunktion ist jedoch nicht genügend Platz für den Code, der alle Parameter in einer Datei speichert. Wir mussten es in Teile teilen und die Müllblöcke benachbarter Funktionen verwenden. Wir haben insgesamt vier Teile.
Teil eins:



Die ersten vier Funktionsparameter in der ARM-Architektur werden in den Registern R0-R3 angezeigt, während der Rest, falls vorhanden, über den Stapel geht. Das LR-Register zeigt die Rücksprungadresse an. Wir müssen alle diese Daten speichern, damit die Funktion funktionieren kann, nachdem wir ihre Parameter ausgegeben haben. Wir müssen auch alle Register speichern, die wir in diesem Prozess verwenden, also verwenden wir PUSH.W {R0-R10, LR}. In R7 erhalten wir die Adresse der Liste der Parameter, die über den Stapel an die Funktion übertragen werden.

Mit der Funktion fopen öffnen wir die Datei / data / local / tmp / aes im Modus "ab" (damit wir etwas hinzufügen können). Wir laden dann die Dateinamenadresse in R0 und die Zeichenfolgenadresse, die den Modus in R1 angibt. Hier endet der Müllcode, also navigieren wir zur nächsten Funktion. Da wir möchten, dass es weiter funktioniert, setzen wir den Übergang zum eigentlichen Funktionscode am Anfang vor den Müll und ersetzen den Müll durch den Rest des Patches.



Wir rufen dann fopen an.

Die ersten drei Parameter der aes-Funktion sind vom Typ int. Da wir die Register zu Beginn auf den Stapel verschoben haben, können wir einfach ihre Adressen im Stapel an die Funktion fwrite übertragen.



Als nächstes haben wir drei Strukturen, die die Größe der Daten angeben und einen Zeiger auf die Daten für den Schlüssel, den Initialisierungsvektor und die verschlüsselten Daten enthalten.



Am Ende schließen wir die Datei, stellen die Register wieder her und geben die Kontrolle über die eigentliche aes-Funktion zurück.
Wir kompilieren die APK-Datei mit der gepatchten Bibliothek, signieren sie, laden sie auf ein Gerät oder einen Emulator herunter und führen sie aus. Jetzt sehen wir, dass der Speicherauszug mit vielen Daten erstellt wurde. Der Browser entschlüsselt nicht nur den Datenverkehr, sondern auch andere Daten, und die gesamte Entschlüsselung erfolgt über diese Funktion. Aus irgendeinem Grund sehen wir die benötigten Daten nicht und die erwartete Anfrage ist im Datenverkehr nicht sichtbar. Lassen Sie uns das Warten überspringen, bis UC Browser die Möglichkeit hat, diese Anforderung zu stellen, und die zuvor vom Server erhaltene verschlüsselte Antwort verwenden, um die Anwendung erneut zu patchen. Wir werden die Entschlüsselung zu onCreate der Hauptaktivität hinzufügen.

  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 

Wir kompilieren es, signieren, installieren und führen es aus. Daher erhalten wir eine NullPointerException, da die Methode einen Nullwert zurückgibt.

Nachdem wir den Code weiter analysiert hatten, fanden wir eine Funktion mit ziemlich interessanten Zeichenfolgen: "META-INF /" und ".RSA". Es sieht so aus, als ob die App ihr Zertifikat überprüft oder sogar Schlüssel daraus generiert. Wir wollen nicht wirklich untersuchen, was mit dem Zertifikat passiert, also geben wir ihm einfach das richtige Zertifikat. Wir patchen die verschlüsselte Zeichenfolge, sodass wir anstelle von "META-INF /" "BLABLINF /" erhalten, einen Ordner mit diesem Namen in der APK-Datei erstellen und das Browserzertifikat darin speichern.

Wir kompilieren es, signieren, installieren und führen es aus. Bingo! Wir haben den Schlüssel!

Mitm


Jetzt haben wir den Schlüssel und einen gleichen Initialisierungsvektor. Versuchen wir, die Serverantwort im CBC-Modus zu entschlüsseln.



Wir sehen die Archiv-URL, so etwas wie MD5, "extract_unzipsize" und eine Nummer. Lassen Sie uns überprüfen. Das MD5 des Archivs ist dasselbe. Die Größe der entpackten Bibliothek ist gleich. Jetzt werden wir versuchen, diese Bibliothek zu patchen und an den Browser zu übertragen. Um zu zeigen, dass unsere gepatchte Bibliothek geladen wurde, erstellen wir eine Absicht, um die Textnachricht "PWNED!" Zu erstellen. Wir werden zwei Antworten vom Server ersetzen: puds.ucweb.com/upgrade/index.xhtml und die, die zum Herunterladen des Archivs auffordert. Im ersten Fall ersetzen wir MD5 (die Größe bleibt nach dem Entpacken gleich). Im zweiten Schritt senden wir das Archiv mit der gepatchten Bibliothek.

Der Browser unternimmt mehrere Versuche, das Archiv herunterzuladen, was zu einem Fehler führt. Anscheinend passiert dort etwas faul. Bei der Analyse dieses bizarren Formats haben wir festgestellt, dass der Server auch die Archivgröße überträgt:



Es ist LEB128-codiert. Der Patch ändert die Größe der komprimierten Bibliothek geringfügig, sodass der Browser feststellte, dass das Archiv beim Herunterladen beschädigt wurde und nach mehreren Versuchen ein Fehler angezeigt wurde.
Also korrigieren wir die Archivgröße und ... voila! :) Siehe das Ergebnis im Video.


Konsequenzen und Antwort des Entwicklers


Auf die gleiche Weise können Hacker diese unsichere Funktion von UC Browser verwenden, um schädliche Bibliotheken zu verteilen und zu starten. Diese Bibliotheken arbeiten im Kontext des Browsers, was zu vollständigen Systemberechtigungen des Browsers führt. Dies gibt ihnen die Möglichkeit, Phishing-Fenster anzuzeigen und auf die Arbeitsdateien des Browsers zuzugreifen, einschließlich Anmeldungen, Kennwörtern und Cookies in der Datenbank.

Wir haben die Entwickler des UC-Browsers kontaktiert und sie über das gefundene Problem informiert, versucht, auf die Sicherheitsanfälligkeit und ihre Gefahr hinzuweisen, aber sie haben sich geweigert, die Angelegenheit zu diskutieren. In der Zwischenzeit blieb der Browser mit der gefährlichen Funktion in Sichtweite. Sobald wir die Details der Sicherheitsanfälligkeit enthüllten, war es unmöglich, sie wie zuvor zu ignorieren. Am 27. März wurde eine neue Version von UC Browser 10.10.9.1193 veröffentlicht, die über HTTPS puds.ucweb.com/upgrade/index.xhtml auf den Server zugegriffen hat. Außerdem führte der Versuch, eine PDF-Datei im Browser zu öffnen, zwischen der Fehlerbehebung und dem Zeitpunkt, zu dem wir diesen Artikel geschrieben haben, zu einer Fehlermeldung mit dem Text "Ups, etwas stimmt nicht". Beim Versuch, die PDF-Datei zu öffnen, wurde keine Anforderung an den Server gesendet. Dies wurde jedoch beim Start durchgeführt. Dies ist ein Zeichen dafür, dass der ausführbare Code unter Verstoß gegen die Google Play-Richtlinien weiterhin heruntergeladen werden kann.

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


All Articles