Suchen nach Sicherheitslücken im UC-Browser


Einführung


Ende März berichteten wir , dass wir eine versteckte Möglichkeit entdeckt haben, nicht verifizierten Code im UC-Browser herunterzuladen und auszuführen. Heute werden wir detailliert analysieren, wie dieser Download erfolgt und wie Hacker ihn für ihre eigenen Zwecke verwenden können.

Vor einiger Zeit wurde UC Browser sehr aggressiv beworben und verbreitet: Er wurde auf den Geräten der Benutzer mithilfe von Malware installiert, die von verschiedenen Websites in Form von Videodateien verbreitet wurde (d. H. Benutzer dachten, sie hätten beispielsweise einen Pornoclip heruntergeladen, erhielten jedoch stattdessen eine APK damit Browser), verwendet erschreckende Banner mit Nachrichten, die besagen, dass der Browser veraltet, anfällig und ähnliches ist. Die offizielle UC Browser-Gruppe in VK hat ein Thema, in dem sich Benutzer über unfaire Werbung beschweren können, es gibt viele Beispiele. Im Jahr 2016 gab es sogar Videowerbung in russischer Sprache (ja, Anzeigen von einem Browser, der Anzeigen blockiert).

Zum Zeitpunkt des Schreibens verfügt UC Browser über mehr als 500.000.000 Installationen bei Google Play. Das ist beeindruckend - nur Google Chrome hat mehr. Unter den Bewertungen finden Sie viele Beschwerden über Werbung und Weiterleitungen zu einigen Anwendungen bei Google Play. Dies war der Grund für die Studie: Wir haben uns entschlossen zu prüfen, ob UC Browser etwas Schlechtes tut. Und es stellte sich heraus, dass er es tat!

Der Anwendungscode enthüllte die Möglichkeit, ausführbaren Code herunterzuladen und auszuführen, was den Regeln für die Veröffentlichung von Anwendungen auf Google Play widerspricht . Zusätzlich zur Tatsache, dass UC Browser ausführbaren Code herunterlädt, wird dieser unsicher, was zur Durchführung eines MitM-Angriffs verwendet werden kann. Mal sehen, ob es uns gelingt, einen solchen Angriff durchzuführen.

Alles, was unten geschrieben steht, ist für die Version von UC Browser relevant, die zum Zeitpunkt der Studie bei Google Play vorhanden war:

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

Angriffsvektor


Im UC-Browser-Manifest finden Sie einen Dienst namens com.uc.deployment.UpgradeDeployService .

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

Wenn dieser Dienst gestartet wird , führt der Browser eine POST-Anforderung an puds.ucweb.com/upgrade/index.xhtml aus , die einige Zeit nach dem Start im Datenverkehr festgestellt werden kann. Als Antwort erhält er möglicherweise einen Befehl zum Herunterladen eines Updates oder eines neuen Moduls. Während des Analyseprozesses gab der Server keine derartigen Befehle aus. Wir haben jedoch festgestellt, dass beim Versuch, die PDF-Datei im Browser zu öffnen, eine zweite Anforderung an die oben angegebene Adresse gestellt wird, wonach die native Bibliothek heruntergeladen wird. Um den Angriff auszuführen, haben wir uns für diese Funktion des UC-Browsers entschieden: die Möglichkeit, PDF-Dateien mit einer nativen Bibliothek zu öffnen, die nicht in der APK enthalten ist und bei Bedarf aus dem Internet heruntergeladen wird. Es ist erwähnenswert, dass UC Browser theoretisch gezwungen sein kann, etwas ohne Benutzerinteraktion herunterzuladen - wenn Sie eine korrekt formulierte Antwort auf eine Anforderung geben, die nach dem Start des Browsers ausgeführt wird. Dafür müssen wir jedoch das Protokoll der Interaktion mit dem Server genauer untersuchen. Daher haben wir beschlossen, dass es einfacher ist, die abgefangene Antwort zu bearbeiten und die Bibliothek für die Arbeit mit PDF zu ersetzen.

Wenn ein Benutzer eine PDF-Datei direkt in einem Browser öffnen möchte, werden im Datenverkehr die folgenden Anforderungen angezeigt:



Zuerst kommt eine POST-Anfrage an puds.ucweb.com/upgrade/index.xhtml , danach
Laden Sie das Archiv mit Bibliothek herunter, um PDF- und Office-Formate anzuzeigen. Es ist logisch anzunehmen, dass bei der ersten Anforderung Informationen über das System übertragen werden (zumindest Architektur, um die gewünschte Bibliothek bereitzustellen), und als Antwort darauf erhält der Browser einige Informationen über die Bibliothek, die heruntergeladen werden müssen: Adresse und möglicherweise etwas anderes. Das Problem ist, dass diese Anfrage verschlüsselt ist.

Snippet anfordern

Antwort-Snippet







Die Bibliothek selbst ist in ZIP gepackt und nicht verschlüsselt.



Suchverkehrsentschlüsselungscode



Versuchen wir, die Serverantwort zu entschlüsseln. Wir sehen uns den Code der Klasse com.uc.deployment.UpgradeDeployService an : Gehen Sie von der onStartCommand- Methode zu com.uc.deployment.bx und von dort 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 sehen hier die Bildung der POST-Anfrage. Wir machen auf die Erstellung eines Arrays von 16 Bytes und dessen Füllung aufmerksam: 0x5F, 0, 0x1F, -50 (= 0xCE). Entspricht dem, was wir in der obigen Anfrage gesehen haben.

In derselben Klasse können Sie eine verschachtelte Klasse feststellen, in der es eine andere interessante Methode gibt:

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

Die Methode empfängt ein Array von Bytes als Eingabe und prüft, ob das Nullbyte 0x60 oder das dritte Byte 0xD0 und das zweite Byte 1, 11 oder 0x1F ist. Wir sehen uns die Antwort vom Server an: Null Byte - 0x60, Sekunde - 0x1F, Dritte - 0x60. Sieht aus wie das, was wir brauchen. Nach den Zeilen zu urteilen (z. B. "up_decrypt"), sollte hier eine Methode aufgerufen werden, die die Antwort des Servers entschlüsselt.
Wir gehen zur gj- Methode über. Beachten Sie, dass das Byte bei Offset 2 (d. H. 0x1F in unserem Fall) als erstes Argument und die Serverantwort ohne dieses Byte übertragen wird
erste 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 gibt es eine Auswahl an Entschlüsselungsalgorithmen und das gleiche Byte, das in unserem
case ist 0x1F, bezeichnet eine von drei möglichen Optionen.

Wir analysieren den Code weiter. Nach ein paar Sprüngen gelangen wir zu einer Methode mit dem sprechenden Namen decryptBytesByKey .

Hier werden zwei weitere Bytes von unserer Antwort getrennt und eine Zeichenfolge daraus erhalten. Es ist klar, dass auf diese Weise der Schlüssel ausgewählt wird, um die Nachricht zu entschlüsseln.

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

Mit Blick auf die Zukunft stellen wir fest, dass zu diesem Zeitpunkt der Schlüssel noch nicht erhalten ist, sondern nur seine „Kennung“. Den Schlüssel zu bekommen ist etwas komplizierter.

Bei der nächsten Methode werden zwei weitere zu den vorhandenen Parametern hinzugefügt, und es gibt vier davon: magische Nummer 16, Schlüsselkennung, verschlüsselte Daten und eine unverständliche Zeichenfolge (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 Übergängen gelangen wir zur staticBinarySafeDecryptNoB64- Methode der Schnittstelle com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . Der Hauptanwendungscode enthält keine Klassen, die diese Schnittstelle implementieren. Eine solche Klasse befindet sich in der Datei lib / armeabi-v7a / libsgmain.so , die eigentlich nicht .so, sondern .jar ist. Die für uns interessante Methode 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 Liste der Parameter durch zwei weitere Ganzzahlen ergänzt: 2 und 0. Gemessen an
Für alle bedeutet 2 Entschlüsselung, wie bei der doFinal- Methode der Systemklasse javax.crypto.Cipher . Und all dies wird an einen bestimmten Router mit der Nummer 10601 übertragen - dies ist anscheinend die Befehlsnummer.

Nach der nächsten Übergangskette finden wir eine Klasse, die die IRouterComponent- 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); } } 

Und auch die JNICLibrary- Klasse, in der die native Methode doCommandNative 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. Und dann beginnt der Spaß.

Verschleierung des Maschinencodes


In der Datei libsgmain.so befindet sich eine native Bibliothek (die eigentlich .jar ist und in der wir eine kleine Implementierung einiger Verschlüsselungsschnittstellen etwas höher gefunden haben): libsgmainso-6.4.36.so . Öffnen Sie es in der IDA und erhalten Sie eine Reihe von Dialogfeldern mit Fehlern. Das Problem ist, dass die Abschnittsüberschriften-Tabelle ungültig ist. Dies geschieht speziell, um die Analyse zu erschweren.



Es wird aber auch nicht benötigt: Um die ELF-Datei korrekt zu laden und zu analysieren, reicht die Programm-Header-Tabelle aus. Daher löschen wir einfach die Abschnittstabelle und machen die entsprechenden Felder in der Kopfzeile ungültig.



Öffnen Sie die Datei erneut in der IDA.

Es gibt zwei Möglichkeiten, der virtuellen Java-Maschine genau mitzuteilen, wo sich in der nativen Bibliothek die Implementierung der im Java-Code als native deklarierten Methode befindet. Die erste besteht darin, ihm einen Namen in der Form Java_package_name_ClassName_Method_name zu geben .

Die zweite Möglichkeit besteht darin, sie beim Laden der Bibliothek zu registrieren (in der Funktion JNI_OnLoad ).
durch Aufrufen der Funktion RegisterNatives .

In unserem Fall sollte der Name bei Verwendung der ersten Methode folgendermaßen lauten : Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative .

Unter den exportierten Funktionen gibt es keine solche, daher müssen Sie nach dem Aufruf von RegisterNatives suchen.
Wir gehen zur Funktion JNI_OnLoad und sehen das folgende Bild:



Was ist hier los? Anfang und Ende der Funktion sind auf den ersten Blick typisch für die ARM-Architektur. Der erste Befehl auf dem Stapel speichert den Inhalt der Register, die die Funktion in ihrer Arbeit verwenden wird (in diesem Fall R0, R1 und R2), sowie den Inhalt des Registers LR, in dem sich die Rücksprungadresse der Funktion befindet. Mit dem letzten Befehl werden die gespeicherten Register wiederhergestellt und die Rücksprungadresse wird sofort in das PC-Register gestellt - und kehrt somit von der Funktion zurück. Wenn Sie jedoch genau hinschauen, werden Sie feststellen, dass die vorletzte Anweisung die auf dem Stapel gespeicherte Rücksprungadresse ändert. Wir berechnen, was danach sein wird
Codeausführung. Eine bestimmte Adresse 0xB130 wird in R1 geladen, 5 wird davon subtrahiert, dann wird sie an R0 übertragen und 0x10 wird hinzugefügt. Es stellt sich heraus, 0xB13B. Somit glaubt die IDA, dass es im letzten Befehl eine normale Rückkehr von der Funktion gibt, aber tatsächlich gibt es einen Übergang zur berechneten Adresse 0xB13B.

Es sei daran erinnert, dass ARM-Prozessoren zwei Modi und zwei Befehlssätze haben: ARM und Thumb. Das niedrigstwertige Bit der Adresse teilt dem Prozessor mit, welcher Befehlssatz verwendet wird. Das heißt, die Adresse ist tatsächlich 0xB13A, und die Einheit im niedrigen Bit zeigt den Daumenmodus an.

Zu Beginn jeder Funktion in dieser Bibliothek wird ein ähnlicher „Adapter“ und hinzugefügt
Müllcode. Außerdem werden wir nicht im Detail darauf eingehen - wir erinnern uns nur
dass der eigentliche Anfang fast aller Funktionen etwas weiter ist.

Da der Code keinen expliziten Übergang zu 0xB13A enthält, hat der IDA selbst nicht erkannt, dass sich der Code an dieser Stelle befindet. Aus dem gleichen Grund wird der größte Teil des Codes in der Bibliothek nicht als Code erkannt, was die Analyse etwas erschwert. Wir teilen der IDA mit, dass der Code hier ist und hier das Ergebnis:



Bei 0xB144 beginnt die Tabelle eindeutig. Was ist mit sub_494C?



Wenn diese Funktion im LR-Register aufgerufen wird, erhalten wir die Adresse der oben genannten Tabelle (0xB144). In R0 der Index in dieser Tabelle. Das heißt, ein Wert wird aus der Tabelle entnommen, zu LR addiert und erhalten
Adresse zu gehen. Versuchen wir es zu berechnen: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Wir gehen zur empfangenen Adresse und sehen nur ein paar nützliche Anweisungen und wieder den Übergang zu 0xB140:



Jetzt erfolgt eine Verschiebung um Offset mit dem Index 0x20 aus der Tabelle.

Gemessen an der Größe der Tabelle gibt es viele solcher Übergänge im Code. Es stellt sich die Frage, ob es irgendwie möglich ist, automatischer damit umzugehen, ohne die Adressen manuell zu berechnen. Skripte und die Möglichkeit, Code in der IDA zu patchen, helfen uns dabei:

 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 Zeile 0xB26A, führen das Skript aus und sehen den Übergang zu 0xB4B0:



Die IDA hat diese Site erneut nicht als Code erkannt. Wir helfen ihr und sehen dort eine andere Konstruktion:



Die Anweisungen nach BLX sehen nicht sehr aussagekräftig aus, es ist eher eine Art Voreingenommenheit. Wir schauen in sub_4964:



In der Tat wird hier das dword an der in LR liegenden Adresse genommen, die zu dieser Adresse hinzugefügt wird, wonach der Wert an der empfangenen Adresse genommen und auf den Stapel geschoben wird. Außerdem wird 4 zu LR hinzugefügt, so dass nach der Rückkehr von der Funktion derselbe Versatz übersprungen wird. Dann zieht der Befehl POP {R1} den empfangenen Wert vom Stapel. Wenn Sie sich die Adresse 0xB4BA + 0xEA = 0xB5A4 ansehen, sehen Sie etwas Ähnliches wie die Adresstabelle:



Um dieses Design zu patchen, müssen Sie zwei Parameter aus dem Code abrufen: den Offset und die Registernummer, in die Sie das Ergebnis einfügen möchten. Für jedes mögliche Register müssen Sie 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 Konstruktion, die wir ersetzen möchten - 0xB4B2 - und führen das Skript aus:



Zusätzlich zu den bereits erwähnten Konstruktionen im Code sind hier auch solche:



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



Wir nehmen den Offset zur Adresse von LR, fügen ihn LR hinzu und gehen dorthin. 0x72044 + 0xC = 0x72050. Das Skript für dieses Design ist sehr 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 des Skripts:



Nachdem alles in der Funktion gepatcht wurde, können Sie die IDA auf ihren tatsächlichen Anfang verweisen. Es sammelt den gesamten Funktionscode in Teilen und kann mit HexRays dekompiliert werden.

Zeichenfolgen dekodieren


Wir haben im UC-Browser gelernt, wie man mit der Verschleierung von Maschinencode in der Bibliothek libsgmainso-6.4.36.so umgeht, und haben den Code für die Funktion 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; } 

Betrachten Sie die folgenden Zeilen sorgfältig:

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

Die Funktion sub_73E24 entschlüsselt den Klassennamen explizit.Als Parameter dieser Funktion werden ein Zeiger auf ähnlich verschlüsselte Daten, ein Puffer und eine Nummer übergeben. Offensichtlich enthält der Puffer nach dem Aufruf der Funktion eine entschlüsselte Zeile, da diese an die FindClass- Funktion übergeben wird , die den Klassennamen als zweiten Parameter verwendet. Daher ist eine Zahl die Größe eines Puffers oder die Länge einer Zeichenfolge. Versuchen wir, den Namen der Klasse zu entschlüsseln. Er sollte uns sagen, 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; } 

sub_7AF78 ( ). : «DcO/lcK+h?m3c*q@» ( , ), — . , sub_6115C . 3. , .

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

switch , 3. case 3: sub_6364C , , . . . sub_6364C , RC4.

. . : com/taobao/wireless/security/adapter/JNICLibrary . Großartig! .


RegisterNatives , doCommandNative . , JNI_OnLoad, 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; } 

In der Tat ist hier eine native Methode namens doCommandNative registriert . Jetzt kennen wir seine Adresse. Mal sehen, was er macht.

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

, , . 10601.

, : command / 10000 , command % 10000 / 100 command % 10 , . ., , 1, 6 1. , JNIEnv , , . ( N1, N2 N3) .

:



JNI_OnLoad .
. . — . , , , ( , ).


, : 0x5F1AC. : UC Browser .

, Java-,
0x4D070. .

R7 R4 :



R11:



, :



, R4. 230 .

Was tun? IDA, switch: Edit -> Other -> Specify switch idiom.



. , , sub_6115C :



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



AES!

, : , , , ( AES). - sub_6115C , , , .


, Android Studio, , , , , , .

UC Browser «». , , . :) , , , . . .

:



ARM R0-R3, , — . LR . , , . , , PUSH.W {R0-R10,LR}. R7 , .

fopen /data/local/tmp/aes «ab»,
d.h. hinzufügen. In R0 laden wir die Adresse des Dateinamens, in R1 die Adresse der Zeile, die den Modus angibt. Und dann endet der Müllcode. Fahren Sie mit der nächsten Funktion fort. Damit es weiterhin funktioniert, setzen wir am Anfang den Übergang zum realen Funktionscode unter Umgehung von Garbage und fügen anstelle von Garbage die Fortsetzung des Patches hinzu.



Wir nennen fopen .

Die ersten drei Parameter der aes- Funktion sind vom Typ int . Da wir die Register zu Beginn auf dem Stapel gespeichert haben, können wir die Adressen auf dem Stapel einfach an die Funktion fwrite übergeben .



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



, aes .

APK , , /, . , , . , . - , . , UC Browser , , , : onCreate .

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

, , , . NullPointerException, . . null.

, : «META-INF/» ".RSA". , . . , , , . , «META-INF/» «BLABLINF/», APK .

, , , . Bingo! !

MitM


Wir haben den Schlüssel und den Initialisierungsvektor gleich dem Schlüssel. Versuchen wir, die Serverantwort im CBC-Modus zu entschlüsseln.



Wir sehen die Archiv-URL, ähnlich wie MD5, "extract_unzipsize" und eine Nummer. Überprüfen Sie: Das MD5-Archiv ist gleich, die Größe der entpackten Bibliothek ist gleich. Wir versuchen, diese Bibliothek zu patchen und an den Browser weiterzugeben. Um zu zeigen, dass unsere gepatchte Bibliothek geladen wurde, führen wir Intent aus, um SMS mit dem Text "PWNED!" Zu erstellen. Wir werden zwei Antworten vom Server ersetzen: puds.ucweb.com/upgrade/index.xhtml und zum Herunterladen des Archivs. Im ersten ersetzen wir MD5 (die Größe ändert sich nach dem Entpacken nicht), im zweiten geben wir das Archiv mit der gepatchten Bibliothek.

Der Browser versucht mehrmals, das Archiv herunterzuladen, woraufhin ein Fehler auftritt. Anscheinend etwas
. , :



LEB128. , , , .

… – ! :) .

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


UC Browser, . , . — , , , .

UC Browser , , - . . , , , . 27
UC Browser 12.10.9.1193, HTTPS: puds.ucweb.com/upgrade/index.xhtml .

, «» PDF «, - !». PDF , , Google Play.

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


All Articles