
Einführung
Ende März haben wir , dass wir eine versteckte Möglichkeit zur Ausführung von unprüfbarem Code im UC Browser entdeckt haben. Heute werden wir detailliert erläutern, wie dieser Download stattfindet und wie Hacker ihn für ihre Zwecke nutzen können.
Vor einiger Zeit wurde der UC Browser sehr aggressiv beworben und verbreitet: Er wurde durch Malware auf den Geräten der Nutzer installiert, auf verschiedenen Websites unter dem Vorwand von Videodateien verteilt (d.h. die Nutzer dachten, sie laden beispielsweise ein Erwachsenen-Video herunter, erhielten stattdessen jedoch das APK dieses Browsers), und es wurden erschreckende Banner mit Nachrichten verwendet, die besagten, dass der Browser veraltet und anfällig sei und dergleichen. In der offiziellen Gruppe des UC Browsers in VK gibt es , in der Nutzer gegen unlautere Werbung Beschwerde einlegen können, es gibt viele Beispiele. Im Jahr 2016 gab es sogar in russischer Sprache (ja, Werbung für einen Browser, der Werbung blockiert).
Zum Zeitpunkt des Schreibens hatte UC Browser mehr als 500.000.000 Installationen im Google Play. Das ist beeindruckend - nur Google Chrome hat mehr. Unter den Bewertungen finden sich viele Beschwerden über Werbung und Umleitungen zu bestimmten Anwendungen im Google Play. Dies war der Anlass für unsere Forschung: Wir wollten herausfinden, ob UC Browser etwas Unerfreuliches macht. Und es stellte sich heraus, dass es das tut!
Im Code der Anwendung wurde eine Möglichkeit entdeckt, ausführbaren Code herunterzuladen und auszuführen, im Google Play verstößt. Neben der Tatsache, dass UC Browser ausführbaren Code herunterlädt, geschieht dies unsicher, was für einen MitM-Angriff genutzt werden kann. Lassen Sie uns sehen, ob wir einen solchen Angriff durchführen können.
Alles, was im Folgenden geschrieben wird, ist relevant für die Version des UC Browsers, die zum Zeitpunkt der Untersuchung im Google Play verfügbar war:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-Datei: f5edb2243413c777172f6362876041eb0c3a928cAngriffsvektor
Im Manifest von UC Browser kann ein Dienst mit dem vielsagenden Namen gefunden werden com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Beim Start dieses Dienstes sendet der Browser eine POST-Anfrage an , der nach einiger Zeit nach dem Start im Datenverkehr wahrnehmbar ist. Als Antwort kann er den Befehl zum Herunterladen eines Updates oder eines neuen Moduls erhalten. Während der Analyse gab der Server solche Befehle nicht, aber wir stellten fest, dass beim Versuch, eine PDF-Datei im Browser zu öffnen, eine erneute Anfrage an die oben genannte Adresse gesendet wird, wonach die native Bibliothek heruntergeladen wird. Für den Angriff entschieden wir uns, diese Besonderheit des UC Browsers zu nutzen: die Fähigkeit, PDFs über eine native Bibliothek zu öffnen, die nicht in der APK enthalten ist und die er bei Bedarf aus dem Internet herunterlädt. Es ist anzumerken, dass der UC Browser theoretisch auch etwas ohne Benutzerinteraktion heruntergeladen werden kann - wenn die Antwort auf die Anfrage, die nach dem Start des Browsers ausgeführt wird, korrekt formatiert wird. Aber dafür müsste man das Protokoll der Interaktion mit dem Server genauer untersuchen, weshalb wir entschieden haben, dass es einfacher ist, die abgefangene Antwort zu bearbeiten und die Bibliothek für die Arbeit mit PDFs zu ersetzen.
Also, wenn der Benutzer möchte, eine PDF-Datei direkt im Browser zu öffnen, können im Datenverkehr folgende Anfragen gesehen werden:

Zuerst wird eine POST-Anfrage an , danach
wird ein Archiv mit der Bibliothek zum Anzeigen von PDFs und Office-Formaten heruntergeladen. Es ist logisch anzunehmen, dass in der ersten Anfrage Informationen über das System übermittelt werden (mindestens die Architektur, um die benötigte Bibliothek bereitzustellen), und als Antwort darauf erhält der Browser einige Informationen über die zu ladende Bibliothek: Adresse und möglicherweise noch etwas anderes. Das Problem ist, dass diese Anfrage verschlüsselt ist.
Ausschnitt der Anfrage
Ausschnitt der Antwort


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

Auf der Suche nach dem Code zur Entschlüsselung des Datenverkehrs
Versuchen wir, die Antwort des Servers zu entschlüsseln. Wir schauen uns den Code der Klasse com.uc.deployment.UpgradeDeployService: aus der Methode onStartCommand gehen wir zu com.uc.deployment.b.x, und von dort zu 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("]Produkt:");
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-Typ:");
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("]Stillen-Modus:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Stillen-Typ:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Stillen-Zustand:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Stillen-Datei:");
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-Typ:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Download-Gruppe:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Download-Pfad:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Apollo-Kind-Version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Apollo-Serie:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]Apollo CPU-Architektur:");
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("]Komponentenname:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]Komponenten-Version:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]Komponenten-Version-Code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]Komponenten-Anforderungstyp:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "ja", "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", "ja", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "ja", "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", "ja", "success");
this.iGY.iGI.b(arg9);
}Hier sehen wir die Bildung einer POST-Anfrage. Beachten Sie die Erstellung eines Arrays aus 16 Bytes und dessen Füllung: 0x5F, 0, 0x1F, -50 (=0xCE). Das stimmt mit dem überein, was wir in der vorherigen Anfrage gesehen haben.
In derselben Klasse kann man eine verschachtelte Klasse bemerken, in der sich eine andere interessante Methode befindet:
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");
}
} Die Methode erhält ein Byte-Array und überprüft, ob das Null-Byte gleich 0x60 oder das dritte Byte gleich 0xD0 ist und ob das zweite Byte 1, 11 oder 0x1F ist. Sehen wir uns die Antwort des Servers an: Das Null-Byte ist 0x60, das zweite ist 0x1F, das dritte ist 0x60. Das scheint das zu sein, was wir brauchen. Anhand der Zeilen (z.B. "up_decrypt") sollte hier eine Methode aufgerufen werden, die die Serverantwort entschlüsselt.
Kommen wir zur Methode g.j. Auffällig ist, dass als erstes Argument das Byte an der Offsetposition 2 (also 0x1F in unserem Fall) übergeben wird, während als zweites die Antwort des Servers ohne
die ersten 16 Bytes.
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;
} Hier scheint die Auswahl des Entschlüsselungsalgorithmus stattzufinden, und das besagte Byte ist in unserem
Im Fall von 0x1F bedeutet es eine von drei möglichen Varianten.
Wir setzen die Codeanalyse fort. Nach ein paar Sprüngen gelangen wir zu einer Methode mit dem sprechenden Namen decryptBytesByKey.
Hier werden von unserer Antwort noch zwei Bytes abgetrennt, aus denen ein String entsteht. Es ist klar, dass auf diese Weise der Schlüssel für die Entschlüsselung 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 Bytes
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); \/\/ Schlüssel Auswahl
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;
}Um nicht vorzugreifen, sei erwähnt, dass auf dieser Ebene noch kein Schlüssel, sondern nur dessen "Identifikator" erhalten wird. Die Erlangung des Schlüssels ist etwas komplizierter.
Im nächsten Schritt werden zu den vorhandenen Parametern noch zwei hinzugefügt, sodass es insgesamt vier werden: die magische Zahl 16, der Schlüssel-Identifikator, die verschlüsselten Daten und eine unverständliche Zeichenkette (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 Methode staticBinarySafeDecryptNoB64 Schnittstelle com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Im Hauptcode der Anwendung gibt es keine Klassen, die dieses Interface implementieren. Eine solche Klasse existiert in der Datei lib\/armeabi-v7a\/libsgmain.so, die in Wirklichkeit jedoch nicht .so, sondern .jar ist. Die für uns interessante Methode wird folgendermaßen implementiert:
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);
}
//...
} Hier wird unsere Parameterliste um zwei weitere Ganzzahlen ergänzt: 2 und 0. Anscheinend bedeutet
2 die Entschlüsselung, wie in der Methode doFinal der Systemklasse javax.crypto.Cipher. Und das alles wird an einen Router mit der Zahl 10601 übergeben – das ist offensichtlich die Befehlsnummer.
Nach einer weiteren Kette von Übergängen finden wir eine Klasse, die das Interface IRouterComponent und die Methode 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);
}
}Und auch die Klasse JNICLibrary, in der die native Methode doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Das bedeutet, wir müssen im nativen Code die Methode finden doCommandNative. Und hier beginnt der Spaß.
Die Obfuskation des Maschinencodes
In der Datei libsgmain.so (die eigentlich eine .jar ist, in der wir weiter oben einige Implementierungen von Interfaces im Zusammenhang mit Verschlüsselung gefunden haben) enthält eine native Bibliothek: libsgmainso-6.4.36.so. Wir öffnen sie in IDA und erhalten eine Menge Fehlermeldungen. Das Problem ist, dass die Abschnittstabelle (section header table) ungültig ist. Dies wurde absichtlich gemacht, um die Analyse zu erschweren.

Aber sie wird nicht benötigt: Um die ELF-Datei korrekt zu laden und zu analysieren, reicht die Segmenttabelle (program header table) vollständig aus. Daher löschen wir einfach die Abschnittstabelle, indem wir die entsprechenden Felder im Header nullen.

Wir öffnen die Datei erneut in IDA.
Es gibt zwei Möglichkeiten, der virtuellen Java-Maschine mitzuteilen, wo genau in der nativen Bibliothek sich die Implementierung der als native deklarierten Methode im Java-Code befindet. Die erste ist, ihr einen Namen im Format Java_Paketname_Klassenname_Methodenname.
Der zweite Schritt besteht darin, ihn beim Laden der Bibliothek (in der Funktion JNI_OnLoad)
durch den Aufruf der Funktion RegisterNatives.
In unserem Fall, wenn wir die erste Methode verwenden, sollte der Name wie folgt lauten: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Unter den exportierten Funktionen gibt es keinen solchen, also müssen wir nach dem Aufruf suchen. RegisterNatives.
Wir gehen zur Funktion JNI_OnLoad und sehen folgendes Bild:

Was passiert hier? Auf den ersten Blick sind Anfang und Ende der Funktion typisch für die ARM-Architektur. Die erste Anweisung speichert den Inhalt der Register, die die Funktion verwenden wird (in diesem Fall R0, R1 und R2), sowie den Inhalt des LR-Registers, in dem die Rücksprungadresse aus der Funktion gespeichert ist. Die letzte Anweisung stellt die gespeicherten Register wieder her, wobei die Rücksprungadresse sofort in das PC-Register gelegt wird – so erfolgt die Rückkehr aus der Funktion. Aber wenn man genauer hinsieht, kann man feststellen, dass die vorletzte Anweisung die Rücksprungadresse, die im Stack gespeichert ist, ändert. Lassen Sie uns berechnen, wie dieser sein wird nach
der Ausführung des Codes. In R1 wird eine bestimmte Adresse 0xB130 geladen, 5 davon abgezogen, dann wird sie in R0 verschoben und 0x10 hinzugefügt. Das ergibt 0xB13B. So denkt IDA, dass in der letzten Anweisung eine gewöhnliche Rückkehr aus der Funktion erfolgt, während tatsächlich ein Übergang zur berechneten Adresse 0xB13B stattfindet.
Hier sei daran erinnert, dass ARM-Prozessoren zwei Modi und zwei Befehlssätze haben: ARM und Thumb. Das niederwertige Bit der Adresse sagt dem Prozessor, welcher Befehlssatz verwendet wird. Das heißt, die Adresse ist tatsächlich 0xB13A, und die Eins im niederwertigen Bit bezeichnet den Thumb-Modus.
Am Anfang jeder Funktion in dieser Bibliothek wurde eine ähnliche "Brücke" hinzugefügt sowie
Schrottcode. Lassen Sie uns nicht näher darauf eingehen – wir merken uns nur,
dass der tatsächliche Beginn fast aller Funktionen etwas weiter liegt.
Da es im Code keinen expliziten Übergang zu 0xB13A gibt, hat IDA selbst nicht erkannt, dass sich an dieser Stelle Code befindet. Aus demselben Grund erkennt sie einen Großteil des Codes in der Bibliothek nicht als Code, was die Analyse etwas erschwert. Wir sagen IDA, dass hier Code ist, und das ergibt folgendes:

Bei 0xB144 beginnt eindeutig die Tabelle. Und was ist in sub_494C?

Bei dem Aufruf dieser Funktion erhalten wir im Register LR die Adresse der zuvor erwähnten Tabelle (0xB144). In R0 – den Index in dieser Tabelle. Das heißt, es wird ein Wert aus der Tabelle genommen, zu LR addiert und es ergibt sich.
Die Adresse, zu der wir gehen müssen. Lass uns versuchen, sie zu berechnen: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Wir gehen zur erhaltenen Adresse und sehen buchstäblich ein paar nützliche Anweisungen und wieder eine Weiterleitung zu 0xB140:

Jetzt wird es eine Weiterleitung mit dem Offset-Index 0x20 aus der Tabelle geben.
Gerade angesichts der Größe der Tabelle wird es viele solcher Weiterleitungen im Code geben. Die Frage stellt sich, ob man dem irgendwie automatisiert begegnen kann, ohne manuell Adressen zu berechnen. Und hier kommen uns Skripte und die Möglichkeit, den Code in IDA zu patchen, zur Hilfe:
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 "Falscher Operanden-Typ bei", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Tabelle nicht gefunden"
else:
print "Tabelle =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Unbekannter Code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Erste Anweisung nicht erkannt"Wir setzen den Cursor auf die Zeile 0xB26A, starten das Skript und sehen eine Weiterleitung zu 0xB4B0:

IDA hat diesen Abschnitt wieder nicht als Code erkannt. Wir helfen ihr und sehen dort eine andere Struktur:

Die Anweisungen nach BLX wirken nicht sehr sinnvoll, das sieht mehr nach einem Offset aus. Wir schauen uns sub_4964 an:

Und tatsächlich wird hier ein dword von der Adresse genommen, die in LR liegt, zu dieser Adresse wird addiert, danach wird der Wert von der erhaltenen Adresse in den Stack gelegt. Auch wird 4 zu LR addiert, um nach der Rückkehr aus der Funktion dieses Offset zu überspringen. Danach holt der Befehl POP {R1} den erhaltenen Wert vom Stack. Wenn man schaut, was sich an der Adresse 0xB4BA + 0xEA = 0xB5A4 befindet, sieht man etwas, das wie eine Adresstabelle aussieht:

Um diese Konstruktion zu patchen, müssen zwei Parameter aus dem Code abgerufen werden: der Offset und die Registernummer, in die das Ergebnis gelegt werden soll. Für jedes mögliche Register muss im Voraus ein Code-Snippet vorbereitet werden.
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 "POP-Befehl nicht gefunden"
else:
print "Falscher Operatortyp auf +4:", get_operand_type(ea + 4, 0)
else:
print "Erste Befehle konnten nicht erkannt werden"Setzen Sie den Cursor zu Beginn der Konstruktion, die Sie ersetzen möchten — 0xB4B2 — und starten Sie das Skript:

Neben den bereits genannten Konstruktionen finden sich im Code auch solche:

Wie im vorherigen Fall folgt nach dem BLX-Befehl ein Offset:

Wir nehmen den Offset von der Adresse aus LR, addieren ihn zu LR und wechseln dorthin. 0x72044 + 0xC = 0x72050. Das Skript für diese Konstruktion ist ganz einfach:
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 "Erste Anweisung konnte nicht erkannt werden"Das Ergebnis der Skriptausführung:

Nachdem die Funktion gepatcht wurde, kann IDA auf ihren tatsächlichen Beginn hinweisen. Es wird den gesamten Code der Funktion stückweise sammeln, und er kann mit HexRays dekompiliert werden.
Entschlüsselung von Zeichenfolgen
Wir haben gelernt, die Obfuskation von Maschinencode in der Bibliothek zu bekämpfen libsgmainso-6.4.36.so aus dem UC Browser und haben den Funktionscode erhalten. 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;
}Betrachten wir die nächsten Zeilen genauer:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);In der Funktion sub_73E24 findet offensichtlich die Entschlüsselung des Klassennamens statt. Als Parameter dieser Funktion wird ein Zeiger auf Daten übergeben, die wie verschlüsselte aussehen, ein gewisser Puffer und eine Zahl. Offensichtlich wird nach dem Funktionsaufruf im Puffer eine entschlüsselte Zeichenkette sein, da sie der Funktion übergeben wird. FindClass, die als zweiten Parameter den Klassennamen akzeptiert. Daher ist die Zahl die Größe des Puffers oder die Länge der Zeichenkette. Lassen Sie uns versuchen, den Klassennamen zu entschlüsseln, dieser sollte uns zeigen, ob wir in die richtige Richtung gehen. Lassen Sie uns genauer betrachten, 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;
}Funktion sub_7AF78 erstellt eine Instanz eines Containers für Byte-Arrays der angegebenen Größe (lassen wir die Details zu diesen Containern beiseite). Hier werden zwei solcher Container erstellt: einer für eine Zeichenkette, „DcO/lcK+h?m3c*q@“ (es ist nicht schwer zu erraten, dass dies ein Schlüssel ist), der andere für die verschlüsselten Daten. Beide Objekte werden dann in eine Struktur eingegeben, die an die Funktion übergeben wird. sub_6115C. Lassen Sie uns auch in dieser Struktur ein Feld mit dem Wert 3 bemerken. Schauen wir uns an, was mit dieser Struktur anschließend 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 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 switch-Parameter ist ein Feld der Struktur, dem zuvor der Wert 3 zugewiesen wurde. Werfen wir einen Blick auf case 3: in der Funktion sub_6364C die Parameter aus der Struktur, die in der vorherigen Funktion abgelegt wurden, nämlich den Schlüssel und die verschlüsselten Daten. Wenn wir genauer hinschauen, können wir sub_6364C, den RC4-Algorithmus erkennen.
Wir haben den Algorithmus und den Schlüssel. Lassen Sie uns versuchen, den Klassennamen zu entschlüsseln. Das ist das Ergebnis: com/taobao/wireless/security/adapter/JNICLibrary. Großartig! Wir sind auf dem richtigen Weg.
Befehlbaum
Jetzt müssen wir den Aufruf finden RegisterNatives, der uns zur Funktion führt doCommandNative. Wir durchsehen die Funktionen, die von JNI_OnLoad, und finden ihn 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;
}Und tatsächlich wird hier eine native Methode mit dem Namen doCommandNative. Jetzt kennen wir ihre Adresse. Lassen Sie uns sehen, was sie 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;
}Anhand des Namens kann man erraten, dass sich hier der Eingangspunkt aller Funktionen befindet, die die Entwickler in die native Bibliothek verschieben wollten. Wir interessieren uns für die Funktion mit der Nummer 10601.
Durch den Code kann man sehen, dass aus der Kommandonummer drei Zahlen resultieren: command / 10000, command % 10000 / 100 und command % 10, d.h. in unserem Fall, 1, 6 und 1. Diese drei Zahlen sowie der Zeiger auf JNIEnv und die an die Funktion übergebenen Argumente werden in einer Struktur zusammengefasst und weitergeleitet. Mit Hilfe der erhaltenen drei Zahlen (wir nennen sie N1, N2 und N3) wird der Befehlsbaum erstellt.
Ungefähr so:

Der Baum wird dynamisch gefüllt in JNI_OnLoad.
Drei Zahlen kodieren den Pfad im Baum. Jedes Blatt des Baumes enthält die gepufferte Adresse der entsprechenden Funktion. Der Schlüssel liegt im Elternknoten. Es ist nicht schwierig, den Ort im Code zu finden, an dem die gewünschte Funktion in den Baum eingefügt wird, wenn man sich mit all den verwendeten Strukturen auseinandersetzt (ihre Beschreibung lassen wir weg, um den ohnehin schon umfangreichen Artikel nicht unnötig aufzublähen).
Noch mehr Obfuskation
Wir haben die Adresse der Funktion erhalten, die den Verkehr entschlüsseln soll: 0x5F1AC. Aber sich darüber zu freuen ist verfrüht: Die Entwickler des UC Browsers haben uns eine weitere Überraschung vorbereitet.
Nachdem wir die Parameter aus dem Array erhalten haben, das im Java-Code erstellt wurde, gelangen wir
zu der Funktion an der Adresse 0x4D070. Und hier erwartet uns eine weitere Art der Obfuskation des Codes.
Wir legen zwei Indizes in R7 und R4:

Wir übertragen den ersten Index in R11:

Um die Adresse aus der Tabelle zu erhalten, verwenden wir den Index:

Nach dem Wechsel zur ersten Adresse wird der zweite Index verwendet, der in R4 ist. In der Tabelle sind 230 Elemente.
Was damit tun? Man kann IDA sagen, dass es sich um einen solchen Switch handelt: Edit -> Other -> Specify switch idiom.

Der resultierende Code ist furchterregend. Aber beim Durchdringen seiner Tiefen kann man einen Aufruf der uns bereits bekannten Funktion bemerken. sub_6115C:

Es gab einen Switch, in dem die Entschlüsselung im Fall 3 mit dem Algorithmus RC4 durchgeführt wurde. In diesem Fall wird die Struktur, die an die Funktion übergeben wird, aus den Parametern, die in doCommandNativeübergeben wurden, gefüllt. Wir erinnern uns, dass wir dort ein magicInt mit dem Wert 16 hatten. Wir schauen uns den entsprechenden Fall an – und nach mehreren Wechseln finden wir den Code, mit dem man den Algorithmus erkennen kann.

Das ist AES!
Der Algorithmus ist da, es bleibt nur noch, seine Parameter zu erhalten: Modus, Schlüssel und möglicherweise den Initialisierungsvektor (sein Vorhandensein hängt vom Modus des AES-Algorithmus ab). Die Struktur mit ihnen sollte irgendwo vor dem Funktionsaufruf sub_6115Cgebildet werden, aber dieser Teil des Codes ist besonders gut obfuskiert, daher kommt die Idee auf, den Code zu patchen, damit alle Parameter der Entschlüsselungsfunktion in eine Datei dumpen.
Patch
Um den gesamten Code des Patches nicht manuell in Assemblysprache zu schreiben, kann man Android Studio starten, dort eine Funktion schreiben, die dieselben Parameter wie unsere Entschlüsselungsfunktion erhält, und in eine Datei schreibt, und anschließend den Code kopieren, den der Compiler generiert.
Die Benutzerfreundlichkeit bei der Code-Eingabe wurde auch von unseren Freunden aus dem UC Browser-Team berücksichtigt. Wir erinnern uns daran, dass am Anfang jeder Funktion Müllcode steht, der leicht durch beliebigen anderen ersetzt werden kann. Sehr praktisch 🙂 Allerdings ist am Anfang der Ziel-Funktion der Platz für den Code, der alle Parameter in einer Datei speichert, etwas begrenzt. Wir mussten ihn in Teile aufteilen und die Müllblöcke benachbarter Funktionen verwenden. Insgesamt sind es vier Teile geworden.
Erster Teil:

In der ARM-Architektur werden die ersten vier Funktionsparameter über die Register R0-R3 übergeben, die restlichen, falls vorhanden, über den Stack. Im Register LR wird die Rücksprungadresse übergeben. Das alles muss gespeichert werden, damit die Funktion nach dem Dumpen ihrer Parameter weiterarbeiten kann. Außerdem müssen wir alle Register, die wir im Prozess verwenden, speichern, daher machen wir PUSH.W {R0-R10,LR}. In R7 haben wir die Adresse der Parameterliste, die der Funktion über den Stack übergeben wurde.
Mit der Funktion fopen öffnen wir die Datei /data/local/tmp/aes im Modus „ab“,
d. h. zum Hinzufügen. In R0 laden wir die Adresse des Dateinamens, in R1 die Adresse der Zeichenkette mit dem Modus. Und hier endet der Müllcode, also wechseln wir zur nächsten Funktion. Damit sie weiterarbeiten kann, setzen wir am Anfang einen Sprung zum eigentlichen Funktionscode und ersetzen den Müll durch die Fortsetzung des Patches.

Wir rufen auf fopen.
Die ersten drei Parameter der Funktion 29m41s sind vom Typ int. Da wir zu Beginn die Register auf dem Stack gespeichert haben, können wir einfach die Adressen der Funktion fwrite im Stack übergeben.

Dann haben wir drei Strukturen, die die Datengröße 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 übergeben die Kontrolle an die eigentliche Funktion. 29m41s.
Wir erstellen das APK mit der gepatchten Bibliothek, signieren es, laden es auf das Gerät/emulator, starten es. Wir sehen, dass unser Dump erstellt wird und viele Daten geschrieben werden. Der Browser verwendet Verschlüsselung nicht nur für den Datenverkehr, und die gesamte Verschlüsselung erfolgt über die betrachtete Funktion. Allerdings fehlen die benötigten Daten, und die erforderliche Anfrage ist im Verkehr nicht sichtbar. Um nicht zu warten, bis der UC Browser die erforderliche Anfrage sendet, nehmen wir die zuvor erhaltene verschlüsselte Antwort vom Server und patchen die Anwendung erneut: Wir fügen die Entschlüsselung in onCreate der Hauptaktivität hinzu.
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;)IWir sammeln, signieren, installieren, starten. Wir erhalten eine NullPointerException, da die Methode null zurückgegeben hat.
Bei der weiteren Analyse des Codes wurde eine Funktion gefunden, die interessante Zeilen entschlüsselt: „META-INF/“ und „.RSA“. Es sieht so aus, als würde die Anwendung ihr Zertifikat überprüfen. Oder sogar Schlüssel daraus generieren. Es ist nicht wirklich angenehm, sich mit dem Zertifikat zu beschäftigen, also fügen wir ihm einfach das richtige Zertifikat hinzu. Wir patchen die verschlüsselte Zeile so, dass anstelle von „META-INF/“ „BLABLINF/“ entsteht, erstellen einen Ordner mit diesem Namen im APK und werfen das Zertifikat des Belkobrauzers hinein.
Wir sammeln, signieren, installieren, starten. Bingo! Wir haben den Schlüssel!
MitM
Wir haben den Schlüssel und den Initialisierungsvektor, der dem Schlüssel entspricht. Lassen Sie uns versuchen, die Antwort des Servers im CBC-Modus zu entschlüsseln.

Wir sehen die URL des Archivs, etwas, das wie MD5 aussieht, „extract_unzipsize“ und eine Zahl. Wir überprüfen: Die MD5 des Archivs stimmt überein, die Größe der entpackten Bibliothek stimmt überein. Wir versuchen, diese Bibliothek zu patchen und dem Browser zu geben. Um zu zeigen, dass unsere gepatchte Bibliothek geladen wurde, starten wir ein Intent, um eine SMS mit dem Text „PWNED!“ zu erstellen. Wir werden zwei Antworten des Servers ersetzen: und das Herunterladen des Archivs. Im ersten ändern wir die MD5 (die Größe nach dem Entpacken bleibt gleich), im zweiten geben wir das Archiv mit der gepatchten Bibliothek zurück.
Der Browser versucht mehrmals, das Archiv herunterzuladen, und gibt dann einen Fehler aus. Offensichtlich gefällt ihm etwas nicht.
Bei der Analyse dieses mysteriösen Formats wurde festgestellt, dass der Server auch die Größe des Archivs überträgt:

Es ist in LEB128 kodiert. Nach dem Patch hat sich die Größe des Archivs mit der Bibliothek etwas verändert, weshalb der Browser annahm, dass das Archiv fehlerhaft heruntergeladen wurde, und nach mehreren Versuchen einen Fehler ausgab.
Wir korrigieren die Größe des Archivs… Und – Sieg! 🙂 Das Ergebnis im Video.
Folgen und Reaktion des Entwicklers
Auf die gleiche Weise könnten Hacker eine unsichere Funktion des UC Browsers nutzen, um schädliche Bibliotheken zu verbreiten und auszuführen. Diese Bibliotheken würden im Kontext des Browsers arbeiten und somit alle seine Systemberechtigungen erhalten. Dies würde die Möglichkeit bieten, Phishing-Fenster anzuzeigen und auf die Arbeitsdateien des orangefarbenen chinesischen Eichhörnchens zuzugreifen, einschließlich der in der Datenbank gespeicherten Logins, Passwörter und Cookies.
Wir haben die Entwickler von UC Browser kontaktiert und sie auf das gefundene Problem hingewiesen, haben versucht, auf die Schwachstelle und deren Gefahren hinzuweisen, aber sie wollten mit uns über irgendetwas sprechen. Unterdessen prahlte der Browser weiterhin mit der gefährlichen Funktion, die für alle sichtbar war. Aber sobald wir die Einzelheiten der Schwachstelle preisgegeben hatten, konnte man dies nicht länger ignorieren. Am 27. März wurde
eine neue Version des UC Browsers 12.10.9.1193 veröffentlicht, die sich über HTTPS mit dem Server verband: .
Darüber hinaus führte der Versuch, ein PDF im Browser zu öffnen, nach der "Behebung" bis zum Zeitpunkt der Erstellung des Artikels zu einer Fehlermeldung mit dem Text "Ups, etwas ist schief gelaufen!". Die Anfrage an den Server beim Versuch, das PDF zu öffnen, wurde nicht ausgeführt, aber die Anfrage beim Start des Browsers wurde ausgeführt, was darauf hindeutet, dass weiterhin die Möglichkeit bestand, ausführbaren Code entgegen den Google Play-Richtlinien zu laden.
Quelle: habr.com
