Einführung
Ende März haben wir
Vor einiger Zeit wurde UC Browser sehr aggressiv beworben und verbreitet: Er wurde mithilfe von Malware auf den Geräten der Benutzer installiert und von verschiedenen Websites unter dem Deckmantel von Videodateien verbreitet (d. h. Benutzer dachten, sie würden beispielsweise ein Pornovideo herunterladen, aber stattdessen eine APK mit diesem Browser erhalten hat), verwendete gruselige Banner mit Meldungen, dass der Browser veraltet, anfällig und ähnliches sei. In der offiziellen UC-Browser-Gruppe auf VK gibt es
Zum Zeitpunkt des Verfassens dieses Artikels hat UC Browser über 500 Installationen bei Google Play. Das ist beeindruckend – nur Google Chrome bietet mehr. In den Rezensionen finden Sie zahlreiche Beschwerden über Werbung und Weiterleitungen zu einigen Anwendungen bei Google Play. Dies war der Grund für unsere Recherche: Wir beschlossen herauszufinden, ob UC Browser etwas Schlechtes macht. Und es stellte sich heraus, dass er es tut!
Im Anwendungscode wurde die Möglichkeit entdeckt, ausführbaren Code herunterzuladen und auszuführen.
Alles, was unten geschrieben steht, ist für die Version von UC Browser relevant, die zum Zeitpunkt der Studie bei Google Play verfügbar war:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c
Angriffsvektor
Im UC Browser-Manifest finden Sie einen Dienst mit einem selbsterklärenden Namen com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Wenn dieser Dienst startet, stellt der Browser eine POST-Anfrage an
Wenn ein Benutzer also ein PDF direkt im Browser öffnen möchte, sind im Datenverkehr folgende Anfragen zu sehen:
Zuerst gibt es eine POST-Anfrage an
Ein Archiv mit einer Bibliothek zum Anzeigen von PDF- und Office-Formaten wird heruntergeladen. Es ist logisch anzunehmen, dass die erste Anfrage Informationen über das System übermittelt (zumindest die Architektur zur Bereitstellung der erforderlichen Bibliothek) und der Browser als Antwort darauf einige Informationen über die Bibliothek erhält, die heruntergeladen werden muss: die Adresse und möglicherweise , etwas anderes. Das Problem ist, dass diese Anfrage verschlüsselt ist.
Fragment anfordern
Antwortfragment
Die Bibliothek selbst ist im ZIP-Format verpackt und nicht verschlüsselt.
Suchen Sie nach dem Verkehrsentschlüsselungscode
Versuchen wir, die Serverantwort zu entschlüsseln. Schauen wir uns den Klassencode an com.uc.deployment.UpgradeDeployService: von Methode onStartCommand gehe zu com.uc.deployment.bx, und von ihm 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();
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);
}
Wir sehen hier die Bildung einer POST-Anfrage. Wir achten auf die Erstellung eines Arrays von 16 Bytes und dessen Füllung: 0x5F, 0, 0x1F, -50 (=0xCE). Stimmt mit dem überein, was wir in der obigen Anfrage gesehen haben.
In derselben Klasse können Sie eine verschachtelte Klasse sehen, die über eine weitere interessante Methode verfügt:
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 verwendet ein Byte-Array als Eingabe und prüft, ob das Null-Byte 0x60 oder das dritte Byte 0xD0 und das zweite Byte 1, 11 oder 0x1F ist. Wir schauen uns die Antwort des Servers an: Das Nullbyte ist 0x60, das zweite ist 0x1F, das dritte ist 0x60. Klingt nach dem, was wir brauchen. Den Zeilen nach zu urteilen (zum Beispiel „up_decrypt“) sollte hier eine Methode aufgerufen werden, die die Antwort des Servers entschlüsselt.
Kommen wir zur Methode gj. Beachten Sie, dass das erste Argument das Byte bei Offset 2 (in unserem Fall also 0x1F) ist und das zweite die Serverantwort ohne ist
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 wählen wir hier einen Entschlüsselungsalgorithmus und dasselbe Byte aus, das sich in unserem befindet
Fall gleich 0x1F, bezeichnet eine von drei möglichen Optionen.
Wir analysieren weiterhin den Code. Nach ein paar Sprüngen befinden wir uns in einer Methode mit einem selbsterklärenden Namen decryptBytesByKey.
Hier werden zwei weitere Bytes von unserer Antwort getrennt und daraus ein String gewonnen. Es ist klar, dass auf diese Weise der Schlüssel zum Entschlüsseln der Nachricht ausgewählt wird.
private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
if(bytes != null) {
try {
if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
return v0;
}
else {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE]; // 2 байта
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Выбор ключа
if(keyId == null) {
return v0;
}
else {
a v2 = EncryptHelper.ayL();
if(v2 == null) {
return v0;
}
else {
byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE];
System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length);
return v2.l(keyId, enrypted);
}
}
}
}
catch(SecException v7_1) {
EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode());
return v0;
}
catch(Throwable v7) {
EncryptHelper.handleDecryptException(v7, 2);
return v0;
}
}
return v0;
}
Mit Blick auf die Zukunft stellen wir fest, dass wir zu diesem Zeitpunkt noch keinen Schlüssel erhalten, sondern nur dessen „Identifikator“. Den Schlüssel zu bekommen ist etwas komplizierter.
Bei der nächsten Methode werden zwei weitere Parameter zu den vorhandenen hinzugefügt, sodass vier davon entstehen: die magische Zahl 16, die Schlüsselkennung, 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 Schnittstelle com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Im Hauptanwendungscode gibt es keine Klassen, die diese Schnittstelle implementieren. Es gibt eine solche Klasse in der Datei lib/armeabi-v7a/libsgmain.so, was eigentlich kein .so, sondern ein .jar ist. Die Methode, an der wir interessiert sind, wird 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 < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}
Hier wird unsere Parameterliste um zwei weitere ganze Zahlen ergänzt: 2 und 0. Gemessen an
Alles, 2 bedeutet Entschlüsselung, wie in der Methode doFinal Systemklasse javax.crypto.Cipher. Und das alles wird an einen bestimmten Router mit der Nummer 10601 übertragen – das ist offenbar die Befehlsnummer.
Nach der nächsten Übergangskette finden wir eine Klasse, die die Schnittstelle implementiert IRouterComponent und 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 Klasse JNICLibrary, in dem 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, dass wir eine Methode im nativen Code finden müssen doCommandNative. Und hier beginnt der Spaß.
Verschleierung von Maschinencode
Im Ordner libsgmain.so (bei dem es sich eigentlich um eine .jar-Datei handelt und in der wir oben die Implementierung einiger verschlüsselungsbezogener Schnittstellen gefunden haben) gibt es eine native Bibliothek: libsgmainso-6.4.36.so. Wir öffnen es in IDA und erhalten eine Reihe von Dialogfeldern mit Fehlern. Das Problem besteht darin, dass die Abschnittskopftabelle ungültig ist. Dies geschieht mit Absicht, um die Analyse zu erschweren.
Dies ist jedoch nicht erforderlich: Um eine ELF-Datei korrekt zu laden und zu analysieren, reicht eine Programm-Header-Tabelle aus. Daher löschen wir einfach die Abschnittstabelle und löschen die entsprechenden Felder in der Kopfzeile.
Öffnen Sie die Datei erneut in IDA.
Es gibt zwei Möglichkeiten, der Java Virtual Machine mitzuteilen, wo genau in der nativen Bibliothek sich die Implementierung einer im Java-Code als nativ deklarierten Methode befindet. Die erste besteht darin, ihm einen Artnamen zu geben Java_package_name_ClassName_MethodName.
Die zweite besteht darin, es beim Laden der Bibliothek zu registrieren (in der Funktion JNI_OnLoad)
mit einem Funktionsaufruf RegistrierenNatives.
Wenn wir in unserem Fall die erste Methode verwenden, sollte der Name so lauten: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Unter den exportierten Funktionen gibt es keine solche Funktion, was bedeutet, dass Sie nach einem Aufruf suchen müssen RegistrierenNatives.
Kommen wir zur Funktion JNI_OnLoad und wir sehen dieses Bild:
Was ist denn hier los? Auf den ersten Blick sind Anfang und Ende der Funktion typisch für die ARM-Architektur. Der erste Befehl auf dem Stapel speichert den Inhalt der Register, die die Funktion bei ihrer Operation verwenden wird (in diesem Fall R0, R1 und R2), sowie den Inhalt des LR-Registers, das die Rücksprungadresse der Funktion enthält . Der letzte Befehl stellt die gespeicherten Register wieder her und die Rücksprungadresse wird sofort im PC-Register abgelegt – 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. Berechnen wir mal, wie es danach sein wird
Codeausführung. Eine bestimmte Adresse 1xB0 wird in R130 geladen, 5 davon subtrahiert, dann nach R0 übertragen und 0x10 dazu addiert. Es stellt sich 0xB13B heraus. Daher geht IDA davon aus, dass der letzte Befehl eine normale Funktionsrückgabe ist, tatsächlich geht er jedoch an die berechnete Adresse 0xB13B.
An dieser Stelle sei daran erinnert, dass ARM-Prozessoren über zwei Modi und zwei Befehlssätze verfügen: ARM und Thumb. Das niedrigstwertige Bit der Adresse teilt dem Prozessor mit, welcher Befehlssatz verwendet wird. Das heißt, die Adresse lautet tatsächlich 0xB13A und eins im niedrigstwertigen Bit gibt den Thumb-Modus an.
Am Anfang jeder Funktion in dieser Bibliothek wurde ein ähnlicher „Adapter“ hinzugefügt
Müllcode. Wir werden nicht näher darauf eingehen – wir erinnern uns einfach
dass der eigentliche Beginn fast aller Funktionen etwas weiter entfernt liegt.
Da der Code nicht explizit zu 0xB13A springt, hat IDA selbst nicht erkannt, dass sich der Code an dieser Stelle befindet. Aus dem gleichen Grund erkennt es den Großteil des Codes in der Bibliothek nicht als Code, was die Analyse etwas erschwert. Wir teilen IDA mit, dass dies der Code ist, und Folgendes passiert:
Die Tabelle beginnt eindeutig bei 0xB144. Was ist in sub_494C?
Beim Aufruf dieser Funktion im LR-Register erhalten wir die Adresse der zuvor genannten Tabelle (0xB144). In R0 - Index in dieser Tabelle. Das heißt, der Wert wird aus der Tabelle genommen, zu LR addiert und das Ergebnis ist
die Adresse, zu der man gehen soll. Versuchen wir es zu berechnen: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Wir gehen zur empfangenen Adresse und sehen buchstäblich ein paar nützliche Anweisungen und gehen wieder zu 0xB140:
Nun erfolgt ein Übergang am Offset mit Index 0x20 aus der Tabelle.
Gemessen an der Größe der Tabelle wird es im Code viele solcher Übergänge geben. Es stellt sich die Frage, ob es möglich ist, dies irgendwie automatisierter zu bewältigen, ohne die Adressen manuell zu berechnen. Und Skripte und die Möglichkeit, Code in IDA zu patchen, kommen uns zu Hilfe:
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"
Platzieren Sie den Cursor auf Zeile 0xB26A, führen Sie das Skript aus und sehen Sie sich den Übergang zu 0xB4B0 an:
IDA erkannte diesen Bereich erneut nicht als Code. Wir helfen ihr und sehen dort ein weiteres Design:
Die Anweisungen nach BLX scheinen wenig Sinn zu ergeben, es handelt sich eher um eine Art Verschiebung. Schauen wir uns sub_4964 an:
Und tatsächlich wird hier ein Dword an der in LR liegenden Adresse genommen, zu dieser Adresse addiert, woraufhin der Wert an der resultierenden Adresse genommen und auf den Stapel gelegt wird. Außerdem wird 4 zu LR hinzugefügt, sodass nach der Rückkehr von der Funktion derselbe Offset übersprungen wird. Anschließend übernimmt der Befehl POP {R1} den resultierenden Wert vom Stapel. Wenn Sie sich ansehen, was sich an der Adresse 0xB4BA + 0xEA = 0xB5A4 befindet, sehen Sie etwas Ähnliches wie eine 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] == 'x5dxf8x04':
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 platzieren den Cursor am Anfang der Struktur, die wir ersetzen möchten – 0xB4B2 – und führen das Skript aus:
Zusätzlich zu den bereits erwähnten Strukturen enthält der Code auch Folgendes:
Wie im vorherigen Fall gibt es nach der BLX-Anweisung einen Offset:
Wir nehmen den Offset zur Adresse von LR, addieren ihn zu LR und gehen dorthin. 0x72044 + 0xC = 0x72050. Das Skript für dieses Design ist recht einfach:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 6
if get_wide_word(ea + 2) == 0xbf00: #NOP
ea1 += 2
offset = get_wide_dword(ea1)
put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
print "Unable to detect first instruction"
Ergebnis der Skriptausführung:
Sobald alles in der Funktion gepatcht ist, können Sie IDA auf seinen eigentlichen Anfang verweisen. Der gesamte Funktionscode wird zusammengesetzt und kann mithilfe von HexRays dekompiliert werden.
Zeichenfolgen dekodieren
Wir haben gelernt, mit der Verschleierung von Maschinencode in der Bibliothek umzugehen libsgmainso-6.4.36.so vom UC Browser und erhielt 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;
}
Schauen wir uns die folgenden Zeilen genauer an:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
In Funktion sub_73E24 Der Klassenname wird eindeutig entschlüsselt. Als Parameter dieser Funktion werden ein Zeiger auf Daten, die verschlüsselten Daten ähneln, ein bestimmter Puffer und eine Nummer übergeben. Offensichtlich befindet sich nach dem Aufruf der Funktion eine entschlüsselte Zeile im Puffer, da diese an die Funktion übergeben wird Findclass, der den Klassennamen als zweiten Parameter annimmt. Daher ist die Zahl die Größe des Puffers oder die Länge der Zeile. Versuchen wir, den Klassennamen zu entschlüsseln. Er sollte uns sagen, ob wir in die richtige Richtung gehen. Schauen wir uns genauer an, was passiert sub_73E24.
int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4; // r6
int v7; // r11
int v8; // r9
int v9; // r4
size_t v10; // r5
int v11; // r0
struc_1 v13; // [sp+0h] [bp-30h]
int v14; // [sp+1Ch] [bp-14h]
int v15; // [sp+20h] [bp-10h]
v4 = 0;
v15 = *(_DWORD *)off_8AC00;
v14 = 0;
v7 = sub_7AF78(17);
v8 = sub_7AF78(size);
if ( !v7 )
{
v9 = 0;
goto LABEL_12;
}
(*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16);
if ( !v8 )
{
LABEL_9:
v4 = 0;
goto LABEL_10;
}
v4 = 0;
if ( !in )
{
LABEL_10:
v9 = 0;
goto LABEL_11;
}
v9 = 0;
if ( out )
{
memset(out, 0, size);
v10 = size - 1;
(*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10);
memset(&v13, 0, 0x14u);
v13.field_4 = 3;
v13.field_10 = v7;
v13.field_14 = v8;
v11 = sub_6115C(&v13, &v14);
v9 = v11;
if ( v11 )
{
if ( *(_DWORD *)(v11 + 4) == v10 )
{
qmemcpy(out, *(const void **)v11, v10);
v4 = *(_DWORD *)(v9 + 4);
}
else
{
v4 = 0;
}
goto LABEL_11;
}
goto LABEL_9;
}
LABEL_11:
sub_7B148(v7);
LABEL_12:
if ( v8 )
sub_7B148(v8);
if ( v9 )
sub_7B148(v9);
return v4;
}
Funktion sub_7AF78 Erstellt eine Instanz eines Containers für Byte-Arrays der angegebenen Größe (wir werden uns nicht im Detail mit diesen Containern befassen). Hier werden zwei solcher Container erstellt: Einer enthält die Zeile „DcO/lcK+h?m3c*q@“ (Es ist leicht zu erraten, dass es sich hierbei um einen Schlüssel handelt), der andere enthält verschlüsselte Daten. Anschließend werden beide Objekte in einer bestimmten Struktur platziert, die an die Funktion übergeben wird sub_6115C. Markieren wir in dieser Struktur auch ein Feld mit dem Wert 3. Schauen wir mal, was als nächstes mit dieser Struktur 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;
}
Der Schalterparameter ist ein Strukturfeld, dem zuvor der Wert 3 zugewiesen wurde. Schauen Sie sich Fall 3: der Funktion an sub_6364C Es werden Parameter aus der Struktur übergeben, die dort in der vorherigen Funktion hinzugefügt wurden, also der Schlüssel und verschlüsselte Daten. Wenn man genau hinschaut sub_6364C, man erkennt darin den RC4-Algorithmus.
Wir haben einen Algorithmus und einen Schlüssel. Versuchen wir, den Klassennamen zu entschlüsseln. Folgendes ist passiert: com/taobao/wireless/security/adapter/JNICLibrary. Großartig! Wir sind auf dem richtigen Weg.
Befehlsbaum
Jetzt müssen wir eine Herausforderung finden RegistrierenNatives, was uns auf die Funktion verweisen wird doCommandNative. Schauen wir uns die aufgerufenen Funktionen an JNI_OnLoad, und wir finden es darin 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 ist hier eine native Methode mit dem Namen registriert doCommandNative. 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;
}
Anhand des Namens können Sie erraten, dass sich hier der Einstiegspunkt aller Funktionen befindet, die die Entwickler beschlossen haben, in die native Bibliothek zu übertragen. Uns interessiert die Funktion Nummer 10601.
Sie können dem Code entnehmen, dass die Befehlsnummer drei Zahlen erzeugt: Befehl/10000, Befehl % 10000 / 100 и Befehl % 10, also in unserem Fall 1, 6 und 1. Diese drei Zahlen sowie ein Zeiger auf JNIEnv und die an die Funktion übergebenen Argumente werden einer Struktur hinzugefügt und weitergegeben. Unter Verwendung der drei erhaltenen Zahlen (nennen wir sie N1, N2 und N3) wird ein Befehlsbaum erstellt.
Etwas wie das:
Der Baum wird dynamisch ausgefüllt JNI_OnLoad.
Drei Zahlen kodieren den Pfad im Baum. Jedes Blatt des Baums enthält die gepockte Adresse der entsprechenden Funktion. Der Schlüssel befindet sich im übergeordneten Knoten. Wenn Sie alle verwendeten Strukturen verstehen, ist es nicht schwierig, die Stelle im Code zu finden, an der die von uns benötigte Funktion zum Baum hinzugefügt wird (wir beschreiben sie nicht, um einen ohnehin schon recht umfangreichen Artikel nicht aufzublähen).
Mehr Verschleierung
Wir haben die Adresse der Funktion erhalten, die den Datenverkehr entschlüsseln soll: 0x5F1AC. Doch zum Jubeln ist es noch zu früh: Die Entwickler von UC Browser haben eine weitere Überraschung für uns vorbereitet.
Nachdem wir die Parameter aus dem Array erhalten haben, das im Java-Code gebildet wurde, erhalten wir
zur Funktion an Adresse 0x4D070. Und hier erwartet uns eine weitere Art der Code-Verschleierung.
Wir haben zwei Indizes in R7 und R4 eingefügt:
Wir verschieben den ersten Index nach R11:
Um eine Adresse aus einer Tabelle abzurufen, verwenden Sie einen Index:
Nach dem Aufrufen der ersten Adresse wird der zweite Index verwendet, der sich in R4 befindet. Die Tabelle enthält 230 Elemente.
Was tun dagegen? Sie können IDA mitteilen, dass es sich um einen Schalter handelt: Bearbeiten -> Andere -> Schaltersprache angeben.
Der resultierende Code ist schrecklich. Aber wenn Sie sich durch den Dschungel bewegen, können Sie einen Aufruf einer Funktion bemerken, die uns bereits bekannt ist sub_6115C:
Es gab einen Schalter, bei dem im Fall 3 eine Entschlüsselung mit dem RC4-Algorithmus erfolgte. Und in diesem Fall wird die an die Funktion übergebene Struktur aus den übergebenen Parametern gefüllt doCommandNative. Erinnern wir uns daran, was wir dort hatten magicInt mit dem Wert 16. Wir schauen uns den entsprechenden Fall an – und finden nach mehreren Übergängen den Code, anhand dessen der Algorithmus identifiziert werden kann.
Das ist AES!
Der Algorithmus existiert, es müssen nur noch seine Parameter ermittelt werden: Modus, Schlüssel und möglicherweise der Initialisierungsvektor (sein Vorhandensein hängt vom Betriebsmodus des AES-Algorithmus ab). Die Struktur mit ihnen muss irgendwo vor dem Funktionsaufruf gebildet werden sub_6115C, aber dieser Teil des Codes ist besonders gut verschleiert, sodass die Idee entsteht, den Code so zu patchen, dass alle Parameter der Entschlüsselungsfunktion in einer Datei abgelegt werden.
Patch
Um nicht den gesamten Patchcode manuell in Assemblersprache schreiben zu müssen, können Sie Android Studio starten, dort eine Funktion schreiben, die dieselben Eingabeparameter wie unsere Entschlüsselungsfunktion empfängt und in eine Datei schreibt, und dann den Code kopieren und einfügen, den der Compiler benötigt generieren.
Unsere Freunde vom UC Browser-Team haben sich auch um die Bequemlichkeit des Code-Hinzufügens gekümmert. Denken wir daran, dass wir am Anfang jeder Funktion Müllcode haben, der leicht durch jeden anderen ersetzt werden kann. Sehr praktisch 🙂 Allerdings ist am Anfang der Zielfunktion nicht genügend Platz für den Code, der alle Parameter in einer Datei speichert. Ich musste es in Teile aufteilen und Müllblöcke aus benachbarten Funktionen verwenden. Insgesamt gab es vier Teile.
Der erste Teil:
In der ARM-Architektur werden die ersten vier Funktionsparameter über die Register R0–R3 geleitet, der Rest, falls vorhanden, wird über den Stapel geleitet. Das LR-Register trägt die Rücksprungadresse. All dies muss gespeichert werden, damit die Funktion funktionieren kann, nachdem wir ihre Parameter gesichert haben. Wir müssen auch alle Register speichern, die wir im Prozess verwenden werden, also führen wir PUSH.W {R0-R10,LR} aus. In R7 erhalten wir die Adresse der Parameterliste, die über den Stack an die Funktion übergeben wird.
Verwendung der Funktion öffnen Lasst uns die Datei öffnen /data/local/tmp/aes im „ab“-Modus
d.h. zur Ergänzung. In R0 laden wir die Adresse des Dateinamens, in R1 die Adresse der Zeile, die den Modus angibt. Und hier endet der Müllcode, also fahren wir mit der nächsten Funktion fort. Damit es weiterhin funktioniert, setzen wir am Anfang den Übergang zum echten Code der Funktion unter Umgehung des Mülls und fügen anstelle des Mülls eine Fortsetzung des Patches hinzu.
Berufung öffnen.
Die ersten drei Parameter der Funktion Aes Typ haben int. Da wir die Register zu Beginn auf dem Stack gespeichert haben, können wir die Funktion einfach übergeben fschreiben ihre Adressen auf dem Stapel.
Als nächstes 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.
Schließen Sie am Ende die Datei, stellen Sie die Register wieder her und übergeben Sie die Kontrolle an die eigentliche Funktion Aes.
Wir sammeln eine APK mit einer gepatchten Bibliothek, signieren sie, laden sie auf das Gerät/den Emulator hoch und starten sie. Wir sehen, dass unser Dump erstellt wird und viele Daten dorthin geschrieben werden. Der Browser verwendet Verschlüsselung nicht nur für den Datenverkehr, und die gesamte Verschlüsselung erfolgt über die jeweilige Funktion. Aber aus irgendeinem Grund sind die notwendigen Daten nicht vorhanden und die erforderliche Anfrage ist im Datenverkehr nicht sichtbar. Um nicht zu warten, bis UC Browser die erforderliche Anfrage stellt, nehmen wir die verschlüsselte Antwort vom Server, die wir zuvor erhalten haben, und patchen die Anwendung erneut: Fügen Sie die Entschlüsselung zu 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;)I
Wir montieren, signieren, installieren, starten. Wir erhalten eine NullPointerException, weil die Methode null zurückgegeben hat.
Bei der weiteren Analyse des Codes wurde eine Funktion entdeckt, die interessante Zeilen entschlüsselt: „META-INF/“ und „.RSA“. Es sieht so aus, als ob die Anwendung ihr Zertifikat überprüft. Oder sogar Schlüssel daraus generiert. Ich möchte mich nicht wirklich damit befassen, was mit dem Zertifikat passiert, also schieben wir ihm einfach das richtige Zertifikat zu. Lassen Sie uns die verschlüsselte Zeile so patchen, dass wir anstelle von „META-INF/“ „BLABLIF/“ erhalten, erstellen Sie einen Ordner mit diesem Namen in der APK und fügen Sie dort das Squirrel-Browser-Zertifikat hinzu.
Wir montieren, signieren, installieren, starten. Bingo! Wir haben den Schlüssel!
MitM
Wir haben einen Schlüssel und einen dem Schlüssel entsprechenden Initialisierungsvektor erhalten. Versuchen wir, die Serverantwort im CBC-Modus zu entschlüsseln.
Wir sehen die Archiv-URL, etwas Ähnliches wie MD5, „extract_unzipsize“ und eine Zahl. Wir prüfen: Der MD5 des Archivs ist gleich, die Größe der entpackten Bibliothek ist gleich. Wir versuchen, diese Bibliothek zu patchen und dem Browser zur Verfügung zu stellen. Um zu zeigen, dass unsere gepatchte Bibliothek geladen wurde, starten wir einen Intent, um eine SMS mit dem Text „PWNED!“ zu erstellen. Wir werden zwei Antworten vom Server ersetzen:
Der Browser versucht mehrmals, das Archiv herunterzuladen, woraufhin er eine Fehlermeldung ausgibt. Anscheinend etwas
er mag nicht. Als Ergebnis der Analyse dieses unklaren Formats stellte sich heraus, dass der Server auch die Größe des Archivs übermittelt:
Es ist in LEB128 kodiert. Nach dem Patch änderte sich die Größe des Archivs mit der Bibliothek ein wenig, sodass der Browser davon ausging, dass das Archiv falsch heruntergeladen wurde, und nach mehreren Versuchen einen Fehler ausgab.
Wir passen die Größe des Archivs an... Und – Sieg! 🙂 Das Ergebnis gibt es im Video.
Konsequenzen und Entwicklerreaktion
Auf die gleiche Weise könnten Hacker die unsichere Funktion von UC Browser nutzen, um schädliche Bibliotheken zu verbreiten und auszuführen. Diese Bibliotheken funktionieren im Kontext des Browsers und erhalten daher alle dessen Systemberechtigungen. Dadurch besteht die Möglichkeit, Phishing-Fenster anzuzeigen, sowie Zugriff auf die Arbeitsdateien des Orange Chinese Squirrel, einschließlich in der Datenbank gespeicherter Logins, Passwörter und Cookies.
Wir haben die Entwickler von UC Browser kontaktiert und sie über das gefundene Problem informiert und versucht, auf die Schwachstelle und ihre Gefahr hinzuweisen, aber sie haben nichts mit uns besprochen. Unterdessen stellte der Browser seine gefährliche Funktion weiterhin öffentlich zur Schau. Aber nachdem wir die Details der Schwachstelle enthüllt hatten, war es nicht mehr möglich, sie wie zuvor zu ignorieren. Der 27. März war
Es wurde eine neue Version von UC Browser 12.10.9.1193 veröffentlicht, die über HTTPS auf den Server zugreift:
Darüber hinaus führte der Versuch, eine PDF-Datei in einem Browser zu öffnen, nach der „Behebung“ und bis zum Zeitpunkt des Verfassens dieses Artikels zu einer Fehlermeldung mit dem Text „Ups, etwas ist schiefgelaufen!“ Beim Versuch, eine PDF-Datei zu öffnen, wurde keine Anfrage an den Server gestellt, wohl aber beim Starten des Browsers, was darauf hindeutet, dass weiterhin die Möglichkeit besteht, ausführbaren Code herunterzuladen, was gegen die Google Play-Regeln verstößt.
Source: habr.com