
Einführung
Ende März haben wir , dass wir eine versteckte Möglichkeit zur Installation und Ausführung von unbewertetem Code im UC Browser entdeckt haben. Heute werden wir im Detail analysieren, wie dieser Download erfolgt und wie Hacker ihn für ihre Zwecke nutzen können.
Vor einiger Zeit wurde der UC Browser sehr aggressiv beworben und verteilt: Er wurde mit Malware auf die Geräte der Nutzer installiert und unter dem Vorwand von Videodateien von verschiedenen Websites verbreitet (d. h. die Nutzer dachten, sie würden beispielsweise ein Porno-Video herunterladen, erhielten jedoch stattdessen eine APK mit diesem Browser). Außerdem wurden angstmachende Banner verwendet mit Nachrichten, dass der Browser veraltet, anfällig und dergleichen ist. In der offiziellen UC Browser-Gruppe auf VK gibt es , in der Nutzer über unseriöse Werbung Beschwerde einlegen können; dort sind viele Beispiele zu finden. 2016 gab es sogar in russischer Sprache (ja, Werbung für einen Browser, der Werbung blockiert).
Zum Zeitpunkt des Schreibens hatte der UC Browser über 500.000.000 Installationen im Google Play Store. Das ist beeindruckend – nur Google Chrome hat mehr. In den Bewertungen gibt es viele Beschwerden über Werbung und Umleitungen zu anderen Apps im Google Play Store. Das hat uns dazu veranlasst, zu untersuchen, ob der UC Browser etwas Unrechtes tut. Und tatsächlich stellt sich heraus, dass er es tut!
Im Code der App wurde die Möglichkeit entdeckt, ausführbaren Code herunterzuladen und auszuführen, im Google Play Store widerspricht. Neben der Tatsache, dass der UC Browser ausführbaren Code herunterlädt, geschieht dies unsicher, was für einen MitM-Angriff ausgenutzt werden kann. Mal sehen, ob wir einen solchen Angriff durchführen können.
Alles, was im Folgenden geschrieben wird, gilt für die Version des UC Browsers, die zum Zeitpunkt der Forschung im Google Play Store verfügbar war:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 der APK-Datei: f5edb2243413c777172f6362876041eb0c3a928cAngriffsvektor
Im Manifest des UC Browsers kann ein Dienst mit dem sprechenden Namen gefunden werden, com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Bei der Ausführung dieses Dienstes führt der Browser eine POST-Anfrage an , der nach einiger Zeit nach dem Start im Netzwerkverkehr erkennbar ist. Daraufhin kann er einen Befehl zum Herunterladen eines Updates oder eines neuen Moduls erhalten. Während der Analyse gab der Server solche Befehle nicht aus, jedoch bemerkten wir, dass beim Versuch, eine PDF im Browser zu öffnen, eine erneute Anfrage an die oben genannte Adresse gesendet wird, wonach die native Bibliothek heruntergeladen wird. Für den Angriff haben wir diese Besonderheit des UC Browsers genutzt: die Fähigkeit, PDFs mithilfe einer nativen Bibliothek zu öffnen, die nicht in der APK enthalten ist und die bei Bedarf aus dem Internet geladen wird. Es ist erwähnenswert, dass der UC Browser theoretisch auch ohne Benutzerinteraktion Dinge herunterladen kann – wenn eine korrekt formatierte Antwort auf die Anfrage gegeben wird, die nach dem Start des Browsers ausgeführt wird. Um dies zu erreichen, muss das Protokoll der Interaktion mit dem Server detaillierter untersucht werden, weshalb wir entschieden haben, dass es einfacher ist, die abgefangene Antwort zu bearbeiten und die Bibliothek für die PDF-Bearbeitung zu ersetzen.
Wenn der Benutzer also eine PDF direkt im Browser öffnen möchte, sind im Netzwerkverkehr folgende Anfragen zu sehen:

Zuerst erfolgt eine POST-Anfrage an , wonach
Eine Archivdatei mit der Bibliothek für die Anzeige von PDF- und Office-Formaten wird heruntergeladen. Es ist logisch anzunehmen, dass beim ersten Request Informationen über das System übermittelt werden (mindestens die Architektur, um die richtige Bibliothek bereitzustellen), und als Antwort darauf erhält der Browser Informationen über die zu ladende Bibliothek: die Adresse und möglicherweise noch etwas anderes. Das Problem ist, dass dieser Request verschlüsselt ist.
Fragment des Requests
Fragment der Antwort


Die Bibliothek selbst ist in einer ZIP-Datei verpackt und nicht verschlüsselt.

Suche nach dem Code zur Entschlüsselung des Traffics
Versuchen wir, die Antwort des Servers zu entschlüsseln. Wir schauen uns den Klassen-Code an 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("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}Hier sehen wir die Erstellung eines POST-Requests. Besonders hervorzuheben ist die Bildung eines Arrays aus 16 Bytes und dessen Füllung: 0x5F, 0, 0x1F, -50 (=0xCE). Dies stimmt mit dem überein, was wir im vorherigen Request gesehen haben.
In derselben Klasse können wir eine eingebettete Klasse bemerken, die eine weitere interessante Methode enthält:
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, während das zweite Byte 1, 11 oder 0x1F sein sollte. Betrachten wir die Antwort des Servers: Null-Byte ist 0x60, das zweite ist 0x1F und das dritte ist 0x60. Das scheint das zu sein, was wir benötigen. Anhand der Zeilen (z. B. „up_decrypt“) sollte hier eine Methode aufgerufen werden, die die Antwort des Servers entschlüsselt.
Kommen wir zur Methode g.j. Beachten Sie, dass als erstes Argument das Byte an Offset 2 (d. h. 0x1F in unserem Fall) übergeben wird, und 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;
} Offensichtlich erfolgt hier die Auswahl des Entschlüsselungsalgorithmus, und das besagte Byte, das in unserem Fall 0x1F ist, bezeichnet eine der drei möglichen Varianten.
Wir setzen die Analyse des Codes 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 eine Zeichenkette entsteht. Es ist klar, dass auf diese Weise der Schlüssel zur 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()); // Auswahl des Schlüssels
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;
}Vorweg möchten wir anmerken, dass an diesem Punkt noch nicht der Schlüssel selbst, sondern lediglich dessen „Identifikator“ erhalten wird. Die Beschaffung des Schlüssels ist etwas komplizierter.
Im nächsten Verfahren werden zwei weitere Parameter hinzugefügt, sodass es insgesamt vier gibt: die magische Zahl 16, die Schlüssel-ID, die verschlüsselten 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 Methode staticBinarySafeDecryptNoB64 des Interfaces com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Im Hauptcode der Anwendung gibt es keine Klassen, die dieses Interface implementieren. Solch eine Klasse finden wir in der Datei lib/armeabi-v7a/libsgmain.so, die eigentlich keine .so-Datei, sondern eine .jar-Datei ist. Die uns interessierende Methode ist wie folgt 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 Liste der Parameter durch zwei weitere ganze Zahlen ergänzt: 2 und 0. Anscheinend bedeutet 2 Entschlüsselung, wie in der Methode
doFinal der Systemklasse javax.crypto.Cipher . Und das alles wird an einen bestimmten Router mit der Nummer 10601 übergeben – das ist offenbar die Kommandonummer.Nach einer weiteren Reihe von Übergängen finden wir die Klasse, die das Interface
IRouterComponent implementiert und die Methode 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 deklariert ist doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Das bedeutet, wir müssen die Methode im nativen Code finden doCommandNative. Und hier beginnt der Spaß.
Obfuskation des Maschinen-Codes
In der Datei libsgmain.so (die eigentlich .jar ist und in der wir oben die Implementierung einiger Schnittstellen, die mit Verschlüsselung zu tun haben, gefunden haben) gibt es eine native Bibliothek: libsgmainso-6.4.36.so. Wir öffnen sie in IDA und bekommen eine Menge Dialogfenster mit Fehlern. Das Problem ist, dass die Abschnittstabelle (section header table) ungültig ist. Das wurde absichtlich getan, 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) 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 die Implementierung der als native deklarierten Methode in der nativen Bibliothek zu finden ist. Die erste ist, ihm einen Namen zu geben wie Java_Paketname_Klassenname_Methodenname.
Die zweite besteht darin, ihn bei der Bibliotheksladen zu registrieren (in der Funktion JNI_OnLoad)
durch einen Funktionsaufruf RegisterNatives.
In unserem Fall, wenn wir die erste Methode verwenden, sollte der Name folgendermaßen lauten: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Eine solche Funktion ist nicht unter den exportierten Funktionen zu finden, also müssen wir nach dem Aufruf suchen. RegisterNatives.
Gehen wir in die Funktion JNI_OnLoad und sehen ein solches Bild:

Was passiert hier? Auf den ersten Blick scheinen der Anfang und das Ende der Funktion typisch für die ARM-Architektur zu sein. Der erste Befehl sichert den Inhalt der Register, die die Funktion während ihrer Ausführung verwenden wird (in diesem Fall R0, R1 und R2), sowie den Inhalt des Registers LR, das die Rücksprungadresse aus der Funktion enthält. Der letzte Befehl stellt die gesicherten Register wieder her, und die Rücksprungadresse wird sofort im PC-Register platziert — so erfolgt der Rücksprung aus der Funktion. Doch wenn man genauer hinsieht, erkennt man, dass der vorletzte Befehl die im Stack gesicherte Rücksprungadresse verändert. Lassen Sie uns berechnen, wie diese nach
der Ausführung des Codes aussehen wird. In R1 wird eine Adresse 0xB130 geladen, von dieser wird 5 subtrahiert, dann wird sie in R0 verschoben und 0x10 dazu addiert. Es ergibt sich 0xB13B. So denkt IDA, dass im letzten Befehl ein normaler Rücksprung aus der Funktion erfolgt, während tatsächlich ein Sprung zur berechneten Adresse 0xB13B stattfindet.
Es ist wichtig zu erwähnen, dass ARM-Prozessoren zwei Modi und zwei Befehlssätze haben: ARM und Thumb. Das niederwertige Bit der Adresse gibt dem Prozessor an, welcher Befehlssatz verwendet wird. Das heißt, die Adresse ist tatsächlich 0xB13A, und die Eins im niederwertigen Bit zeigt den Thumb-Modus an.
Zu Beginn jeder Funktion in dieser Bibliothek wurde eine solche „Adapter“-Routine hinzugefügt, und
fehlerhaftem Code. Lassen Sie uns nicht weiter darauf eingehen – denken wir einfach daran,
dass der tatsächliche Beginn fast aller Funktionen etwas weiter hinten liegt.
Da es im Code keinen expliziten Sprung zu 0xB13A gibt, hat IDA nicht erkannt, dass sich an dieser Stelle Code befindet. Aus demselben Grund erkennt sie den Großteil des Codes in der Bibliothek nicht als Code, was die Analyse etwas erschwert. Wir sagen IDA, dass es sich hier um Code handelt, und das ist das Ergebnis:

An 0xB144 beginnt eindeutig eine Tabelle. Und was befindet sich in sub_494C?

Beim Aufruf dieser Funktion erhält der Registerwert 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 entnommen, zu LR addiert und es ergibt sich
Die Adresse, zu der wir wechseln müssen. Lassen Sie uns versuchen, sie zu berechnen: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Wir wechseln zur erhaltenen Adresse und sehen buchstäblich ein paar nützliche Anweisungen und erneut einen Übergang zu 0xB140:

Jetzt erfolgt der Übergang über den Offset mit dem Index 0x20 aus der Tabelle.
Angesichts der Größe der Tabelle wird es viele solcher Übergänge im Code geben. Die Frage ist, ob man dies automatisierter angehen kann, ohne die Adressen manuell zu berechnen. Hier kommen Skripte und die Möglichkeit, den Code in IDA zu patchen, ins Spiel:
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 Operandtyp auf", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Tabelle nicht gefunden"
else:
print "table =", 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"Setzen Sie den Cursor auf die Zeile 0xB26A, starten Sie das Skript und sehen Sie den Sprung zu 0xB4B0:

IDA hat diesen Bereich erneut nicht als Code erkannt. Wir helfen ihr und sehen eine andere Struktur:

Die Anweisungen nach BLX erscheinen nicht sehr sinnvoll, sie wirken eher wie eine Art Offset. Wir schauen in sub_4964:

Tatsächlich wird hier ein dword von der Adresse genommen, die im LR gespeichert ist, zu dieser Adresse wird addiert, dann wird der Wert von der erhaltenen Adresse genommen und auf den Stack gelegt. Gleichzeitig wird zu LR 4 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 sich ansieht, was sich an der Adresse 0xB4BA + 0xEA = 0xB5A4 befindet, kann man etwas sehen, das wie eine Adresstabelle aussieht:

Um diese Konstruktion zu patchen, müssen zwei Parameter aus dem Code extrahiert werden: das Offset und die Registrierungsnummer, in die das Ergebnis gelegt werden soll. Für jeden möglichen Register muss im Voraus ein Codeabschnitt 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 = hier()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
und get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
wenn get_operand_type(ea + 4, 0) == 7:
pop = get_bytes(ea + 12, 4, 0)
wenn pop[1] == 'xbc':
register = -1
r = get_wide_byte(ea + 12)
für i in range(8):
wenn r == (1 <> 4
wenn register in patches:
address = get_wide_dword(ea + 8) + ea + 8
für b in patches[register]:
patch_byte(ea, b)
ea += 1
patch_dword(ea, address)
sonst:
print "POP-Anweisung nicht gefunden"
sonst:
print "Falscher Operandtyp auf +4:", get_operand_type(ea + 4, 0)
sonst:
print "Erste Anweisungen konnten nicht erkannt werden"Wir setzen den Cursor an den Anfang der Konstruktion, die wir ersetzen möchten — 0xB4B2 — und starten das Skript:

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

Wie im vorherigen Fall folgt nach der BLX-Anweisung eine Verschiebung:

Wir nehmen die Verschiebung von der Adresse aus LR, addieren sie zu LR und gehen 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 kann nicht erkannt werden"Das Ergebnis der Ausführung des Skripts:

Nachdem die Funktion vollständig gepatcht wurde, kann IDA auf ihren tatsächlichen Anfang hingewiesen werden. Es wird den gesamten Code der Funktion stückweise sammeln, und dieser kann mit HexRays dekompiliert werden.
Entschlüsselung der Zeichenfolgen
Wir haben gelernt, wie man mit der Obfuskation von Maschinensprache in der Bibliothek umgeht. libsgmainso-6.4.36.so aus UC Browser und erhielten den Funktionscode 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;
}Lassen Sie uns die folgenden Zeilen näher betrachten:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);In der Funktion sub_73E24 Hier erfolgt klar die Entschlüsselung des Klassennamens. Als Parameter dieser Funktion werden ein Zeiger auf die datenähnlichen, verschlüsselten Informationen, ein gewisser Puffer und eine Zahl übergeben. Offensichtlich wird nach dem Aufruf der Funktion die entschlüsselte Zeichenkette im Puffer vorhanden sein, da er an die Funktion übergeben wird. FindClass, die als zweiten Parameter den Klassennamen erwartet. Somit ist die Zahl die Größe des Puffers oder die Länge der Zeichenkette. Lassen Sie uns versuchen, den Klassennamen zu entschlüsseln; er sollte uns anzeigen, ob wir uns auf dem richtigen Weg befinden. 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 создаёт экземпляр контейнера для байтовых массивов указанного размера (не будем подробно останавливаться на этих контейнерах). Тут создаётся два таких контейнера: в один помещается строчка «DcO/lcK+h?m3c*q@» (нетрудно догадаться, что это ключ), в другой — зашифрованные данные. Далее оба объекта помещаются в некую структуру, которая передаётся функции sub_6115C. Lassen Sie uns auch in dieser Struktur das Feld mit dem Wert 3 hervorheben. Schauen wir uns an, was mit dieser Struktur weiter 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;
}Als Parameter switch wird ein Feld der Struktur übergeben, dem zuvor der Wert 3 zugewiesen wurde. Wir schauen uns den Fall 3 an: in die Funktion sub_6364C werden die Parameter aus der Struktur übergeben, die zuvor in der Funktion abgelegt wurden, d.h. der Schlüssel und die verschlüsselten Daten. Wenn man sich genauer anschaut sub_6364C, kann man den RC4-Algorithmus darin 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. Ausgezeichnet! Wir sind auf dem richtigen Weg.
Befehlshierarchie
Jetzt müssen wir den Aufruf finden RegisterNatives, der uns auf die Funktion hinweist doCommandNative. Wir durchsuchen die Funktionen, die aus JNI_OnLoad, aufgerufen werden, 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;
}Und tatsächlich, hier wird eine native Methode mit dem Namen registriert doCommandNative. Jetzt kennen wir seine Adresse. Schauen wir uns an, was sie 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;
}Wie der Name schon sagt, handelt es sich hier um den Einstiegspunkt aller Funktionen, die die Entwickler in die native Bibliothek übertragen wollten. Uns interessiert die Funktion mit der Nummer 10601.
Anhand des Codes kann man sehen, dass aus der Befehlsnummer drei Zahlen generiert werden: command / 10000, command % 10000 / 100 und command % 10, d. h. in unserem Fall 1, 6 und 1. Diese drei Zahlen sowie ein Zeiger auf JNIEnv und die an die Funktion übergebenen Argumente werden in eine Struktur gepackt und weitergegeben. Mit den gewonnenen drei Zahlen (wir bezeichnen sie als N1, N2 und N3) wird ein Befehlsbaum erstellt.
Ungefähr so:

Der Baum wird dynamisch in JNI_OnLoad.
Drei Zahlen kodieren den Pfad im Baum. Jedes Blatt des Baumes enthält die verknüpfte Adresse der entsprechenden Funktion. Der Schlüssel befindet sich im Elternelement. Es ist nicht schwer, den Punkt im Code zu finden, an dem die benötigte Funktion in den Baum hinzugefügt wird, wenn man sich mit den verwendeten Strukturen auskennt (eine Beschreibung dieser lassen wir weg, um diesen bereits umfangreichen Artikel nicht noch mehr aufzublähen).
Weitere Obfuskation
Wir haben die Adresse der Funktion erhalten, die den Traffic entschlüsseln soll: 0x5F1AC. Doch freuen wir uns nicht zu früh: Die Entwickler des UC Browsers haben eine weitere Überraschung für uns vorbereitet.
Nach dem Abrufen der Parameter aus dem Array, das im Java-Code erstellt wurde, gelangen wir
zu der Funktion unter der Adresse 0x4D070. Und hier erwartet uns eine weitere Art der Code-Obfuskation.
Wir legen zwei Indizes in R7 und R4 ab:

Wir verschieben den ersten Index in R11:

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

Nach dem Zugriff auf die erste Adresse wird der zweite Index verwendet, der sich in R4 befindet. In der Tabelle sind 230 Elemente.
Was machen wir damit? Wir können IDA sagen, dass dies ein Switch ist: Edit -> Other -> Specify switch idiom.

Der resultierende Code ist schrecklich. Doch wenn man sich durch seine Unwege kämpft, kann man den Aufruf einer bereits bekannten Funktion bemerken. sub_6115C:

Es gab einen Switch, in dem im Fall 3 die Entschlüsselung mit dem RC4-Algorithmus stattfand. In diesem Fall wird die Struktur, die an die Funktion übergeben wird, aus den übergebenen Parametern gefüllt, doCommandNative. Denken wir daran, dass wir dort einen magicInt mit dem Wert 16 hatten. Wir schauen uns den entsprechenden Fall an – und nach mehreren Übergängen finden wir den Code, der den Algorithmus identifizieren kann.

Das ist AES!
Der Algorithmus steht fest, nun müssen wir seine Parameter erhalten: Modus, Schlüssel und eventuell den Initialisierungsvektor (ob dieser vorhanden ist, hängt vom Betriebsmodus des AES-Algorithmus ab). Die Struktur mit diesen Informationen sollte irgendwo vor dem Funktionsaufruf erstellt werden, sub_6115Caber dieser Teil des Codes ist besonders gut obfuskiert, also kommt die Idee, den Code zu patchen, um alle Parameter der Entschlüsselungsfunktion in eine Datei zu dumpen.
Patch
Um den gesamten Patch-Code nicht manuell in Assembly-Sprache zu schreiben, kann man Android Studio öffnen, dort eine Funktion schreiben, die die gleichen Parameter erhält wie unsere Entschlüsselungsfunktion und in eine Datei schreibt, und anschließend den Code kopieren, den der Compiler generiert.
Auch unsere Freunde von UC Browser haben sich um die Bequemlichkeit der Code-Integration gekümmert. Wir erinnern uns daran, dass am Anfang jeder Funktion unser 'Müll-Code' steht, den wir einfach durch einen anderen ersetzen können. Sehr praktisch 🙂 Allerdings ist am Anfang der Ziel-Funktion der Platz für den Code, der alle Parameter in einer Datei speichert, etwas begrenzt. Daher mussten wir ihn in Teile aufteilen und die Abfallblöcke benachbarter Funktionen nutzen. Insgesamt sind es vier Teile geworden.
Erster Teil:

In der ARM-Architektur werden die ersten vier Parameter einer Funktion über die Register R0-R3 übergeben; weitere, falls vorhanden, über den Stack. Die Rücksprungadresse wird im Register LR übermittelt. All dies muss gespeichert werden, damit die Funktion nach dem Dumpen ihrer Parameter korrekt arbeiten kann. Darüber hinaus müssen alle Register gespeichert werden, die wir im Prozess verwenden, daher führen wir PUSH.W {R0-R10,LR} aus. In R7 erhalten 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. zur Ergänzung. In R0 laden wir die Adresse des Dateinamens, in R1 — die Adresse der Zeichenkette mit dem Modus. An dieser Stelle endet der fehlerhafte Code, also wechseln wir zur nächsten Funktion. Damit sie weiterarbeitet, setzen wir am Anfang einen Sprung zu dem tatsächlichen Code der Funktion und fügen anstelle des fehlerhaften Codes die Fortsetzung des Patches hinzu.

Wir rufen auf fopen.
Die ersten drei Parameter der Funktion aes haben den Typ int. Da wir zu Beginn die Register im Stack gesichert haben, können wir einfach die Adressen der Register fwrite im Stack an die Funktion übergeben.

Zusätzlich 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.

Am Ende schließen wir die Datei, stellen die Register wieder her und übergeben die Kontrolle an die tatsächliche Funktion. aes.
Wir erstellen ein APK mit einer gepatchten Bibliothek, signieren es, laden es auf das Gerät/emulator und 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 alle Verschlüsselungen laufen über die betrachtete Funktion. Die benötigten Daten sind jedoch aus irgendeinem Grund nicht vorhanden, und die erforderliche Anfrage ist im Datenverkehr nicht sichtbar. Um nicht darauf zu warten, dass UC Browser die erforderliche Anfrage stellt, nehmen wir die zuvor erhaltene verschlüsselte Antwort vom Server und patchen die Anwendung erneut: Wir fügen die Entschlüsselung im onCreate der MainActivity 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 erstellen, signieren, installieren und starten. Wir erhalten eine NullPointerException, da die Methode null zurückgegeben hat.
Bei einer weiteren Analyse des Codes wurde eine Funktion entdeckt, die interessante Strings entschlüsselt: „META-INF/“ und „.RSA“. Anscheinend prüft die Anwendung ihr Zertifikat oder generiert sogar Schlüssel daraus. Wir haben keine Lust, uns mit dem Zertifikat zu beschäftigen, also fügen wir einfach das richtige Zertifikat ein. Wir patchen den verschlüsselten String so, dass anstelle von „META-INF/“ „BLABLINF/“ erscheint, erstellen einen Ordner mit diesem Namen im APK und legen dort das Zertifikat des Browsers ab.
Wir sammeln, signieren, installieren und 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. Überprüfen: MD5 des Archivs stimmt überein, Größe der entpackten Bibliothek stimmt überein. Wir versuchen, diese Bibliothek zu patchen und dem Browser zu übergeben. Um zu zeigen, dass unsere gepatchte Bibliothek geladen wurde, werden wir einen Intent zum Erstellen einer SMS mit dem Text „PWNED!“ auslösen. Wir werden zwei Antworten vom Server ersetzen: und zum Herunterladen des Archivs. Im ersten ersetzen wir MD5 (die Größe nach dem Entpacken bleibt unverändert), im zweiten geben wir das Archiv mit der modifizierten Bibliothek aus.
Der Browser versucht mehrmals, das Archiv herunterzuladen, gibt dann aber einen Fehler aus. Offensichtlich gibt es etwas,
was ihm nicht passt. Bei der Analyse dieses undurchsichtigen Formats stellte sich heraus, dass der Server auch die Größe des Archivs überträgt:

Sie ist in LEB128 kodiert. Nach dem Patch hat sich die Größe des Archivs mit der Bibliothek leicht verändert, was dazu führte, dass der Browser annahm, das Archiv sei fehlerhaft heruntergeladen worden, 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 schadhafte Bibliotheken zu verbreiten und auszuführen. Diese Bibliotheken würden im Kontext des Browsers arbeiten und somit alle Systemberechtigungen erhalten. Dies führt dazu, dass Phishing-Fenster angezeigt werden können und Zugriff auf sensible Dateien der orangefarbenen chinesischen Eule besteht, einschließlich gespeicherter Anmeldungen, Passwörter und Cookies aus der Datenbank.
Wir haben die Entwickler von UC Browser kontaktiert und ihnen das gefundene Problem gemeldet, dabei versucht, auf die Schwachstelle und deren Gefährlichkeit hinzuweisen. Eine Diskussion mit uns wollte jedoch nicht stattfinden. Währenddessen prahlte der Browser weiterhin mit einer riskanten Funktion im Scheinwerferlicht. Doch sobald wir die Details der Schwachstelle veröffentlichten, war es nicht mehr möglich, dies zu ignorieren, wie zuvor. Am 27. März wurde
eine neue Version des UC Browsers 12.10.9.1193 veröffentlicht, die den Server über HTTPS ansprach: .
Darüber hinaus führte der Versuch, ein PDF im Browser zu öffnen, nach der "Behebung" der Schwachstelle bis zum Zeitpunkt der Artikelverfassung zur Anzeige einer Fehlermeldung mit dem Text „Ups, etwas ist schiefgelaufen!“. Der Serveranfrage bei dem Versuch, ein PDF zu öffnen, wurde nicht nachgekommen, jedoch wurde die Anfrage beim Start des Browsers ausgeführt, was darauf hindeutet, dass die Möglichkeit, ausführbaren Code entgegen den Richtlinien von Google Play hochzuladen, weiterhin bestand.
Quelle: habr.com
