Alla ricerca di vulnerabilità in UC Browser

Alla ricerca di vulnerabilità in UC Browser

Introduzione

Alla fine di marzo noi segnalati, che hanno scoperto la capacità nascosta di caricare ed eseguire codice non verificato in UC Browser. Oggi esamineremo in dettaglio come avviene questo download e come gli hacker possono utilizzarlo per i propri scopi.

Qualche tempo fa, UC Browser veniva pubblicizzato e distribuito in modo molto aggressivo: veniva installato sui dispositivi degli utenti tramite malware, distribuito da vari siti con il pretesto di file video (ovvero, gli utenti pensavano di scaricare, ad esempio, un video porno, ma invece ha ricevuto un APK con questo browser), utilizzava banner spaventosi con messaggi che indicavano che il browser era obsoleto, vulnerabile e cose del genere. Nel gruppo ufficiale UC Browser su VK c'è argomento, in cui gli utenti possono lamentarsi della pubblicità sleale, ci sono molti esempi. Nel 2016 c'era addirittura pubblicità video in russo (sì, pubblicità per un browser che blocca la pubblicità).

Al momento in cui scrivo, UC Browser ha oltre 500 di installazioni su Google Play. Questo è impressionante: solo Google Chrome ha di più. Tra le recensioni puoi vedere molti reclami sulla pubblicità e reindirizzamenti ad alcune applicazioni su Google Play. Questo è stato il motivo della nostra ricerca: abbiamo deciso di vedere se UC Browser stesse facendo qualcosa di brutto. E si è scoperto che lo fa!

Nel codice dell'applicazione è stata scoperta la possibilità di scaricare ed eseguire codice eseguibile, il che è contrario alle norme sulla pubblicazione delle domande su Google Play. Oltre a scaricare codice eseguibile, UC Browser lo fa in modo non sicuro, il che può essere utilizzato per lanciare un attacco MitM. Vediamo se riusciamo a portare a termine un simile attacco.

Tutto quanto scritto di seguito è rilevante per la versione di UC Browser disponibile su Google Play al momento dello studio:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c

Vettore di attacco

Nel manifest del browser UC puoi trovare un servizio con un nome autoesplicativo com.uc.deployment.UpgradeDeployService.

    <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />

All'avvio di questo servizio, il browser effettua una richiesta POST a puds.ucweb.com/upgrade/index.xhtml, visibile nel traffico qualche tempo dopo la partenza. In risposta, potrebbe ricevere un comando per scaricare qualche aggiornamento o un nuovo modulo. Durante l'analisi, il server non ha dato tali comandi, ma abbiamo notato che quando proviamo ad aprire un PDF nel browser, effettua una seconda richiesta all'indirizzo sopra specificato, dopodiché scarica la libreria nativa. Per sferrare l'attacco abbiamo deciso di sfruttare questa funzionalità di UC Browser: la possibilità di aprire PDF utilizzando una libreria nativa, che non si trova nell'APK e che all'occorrenza si scarica da Internet. Vale la pena notare che, in teoria, UC Browser può essere costretto a scaricare qualcosa senza l'interazione dell'utente, se si fornisce una risposta ben formata a una richiesta che viene eseguita dopo l'avvio del browser. Ma per fare ciò, dobbiamo studiare più in dettaglio il protocollo di interazione con il server, quindi abbiamo deciso che sarebbe stato più semplice modificare la risposta intercettata e sostituire la libreria per lavorare con PDF.

Pertanto, quando un utente desidera aprire un PDF direttamente nel browser, nel traffico possono essere visualizzate le seguenti richieste:

Alla ricerca di vulnerabilità in UC Browser

Innanzitutto c'è una richiesta POST a puds.ucweb.com/upgrade/index.xhtmldopo di che
Viene scaricato un archivio con una libreria per la visualizzazione di PDF e formati Office. È logico supporre che la prima richiesta trasmetta informazioni sul sistema (almeno l'architettura per fornire la libreria richiesta), e in risposta ad essa il browser riceve alcune informazioni sulla libreria che deve essere scaricata: l'indirizzo ed eventualmente , qualcos'altro. Il problema è che questa richiesta è crittografata.

Richiedi frammento

Frammento di risposta

Alla ricerca di vulnerabilità in UC Browser

Alla ricerca di vulnerabilità in UC Browser

La libreria stessa è confezionata in ZIP e non è crittografata.

Alla ricerca di vulnerabilità in UC Browser

Cerca il codice di decrittazione del traffico

Proviamo a decifrare la risposta del server. Diamo un'occhiata al codice della classe com.uc.deployment.UpgradeDeployService: dal metodo onStartCommand vai a com.uc.deployment.bx, e da esso a 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);
}

Qui vediamo la formazione di una richiesta POST. Prestiamo attenzione alla creazione di un array di 16 byte e al suo riempimento: 0x5F, 0, 0x1F, -50 (=0xCE). Coincide con quanto visto nella richiesta sopra.

Nella stessa classe puoi vedere una classe nidificata che ha un altro metodo interessante:

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

Il metodo accetta un array di byte come input e controlla che il byte zero sia 0x60 o il terzo byte sia 0xD0 e il secondo byte sia 1, 11 o 0x1F. Osserviamo la risposta del server: il byte zero è 0x60, il secondo è 0x1F, il terzo è 0x60. Sembra quello di cui abbiamo bisogno. A giudicare dalle righe (“up_decrypt”, ad esempio), qui dovrebbe essere chiamato un metodo che decodificherà la risposta del server.
Passiamo al metodo gj. Nota che il primo argomento è il byte all'offset 2 (cioè 0x1F nel nostro caso), e il secondo è la risposta del server senza
primi 16 byte.

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

Ovviamente, qui selezioniamo un algoritmo di decrittazione e lo stesso byte che si trova nel nostro
case pari a 0x1F, denota una delle tre possibili opzioni.

Continuiamo ad analizzare il codice. Dopo un paio di salti ci troviamo in un metodo dal nome autoesplicativo decryptBytesByKey.

Qui altri due byte vengono separati dalla nostra risposta e da essi si ottiene una stringa. È chiaro che in questo modo viene selezionata la chiave per decrittografare il messaggio.

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

Guardando al futuro, notiamo che in questa fase non otteniamo ancora una chiave, ma solo il suo “identificatore”. Ottenere la chiave è un po’ più complicato.

Nel metodo successivo si aggiungono altri due parametri a quelli esistenti, ottenendone quattro: il numero magico 16, l'identificatore della chiave, i dati crittografati e una stringa incomprensibile (nel nostro caso vuota).

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

Dopo una serie di transizioni arriviamo al metodo staticBinarySafeDecryptNoB64 dell'interfaccia com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Non esistono classi nel codice dell'applicazione principale che implementano questa interfaccia. C'è una classe del genere nel file lib/armeabi-v7a/libsgmain.so, che in realtà non è un .so, ma un .jar. Il metodo che ci interessa è implementato come segue:

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

Qui il nostro elenco di parametri è integrato con altri due numeri interi: 2 e 0. A giudicare da
tutto, 2 significa decrittazione, come nel metodo doFinal classe di sistema javax.crypto.Cipher. E tutto questo viene trasferito a un certo router con il numero 10601: apparentemente questo è il numero di comando.

Dopo la successiva catena di transizioni troviamo una classe che implementa l'interfaccia IRouterComponent e metodo doComando:

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

E anche classe Biblioteca JNIC, in cui viene dichiarato il metodo nativo doCommandNative:

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

Ciò significa che dobbiamo trovare un metodo nel codice nativo doCommandNative. Ed è qui che inizia il divertimento.

Offuscamento del codice macchina

In archivio libsgmain.so (che in realtà è un file .jar e in cui abbiamo trovato l'implementazione di alcune interfacce relative alla crittografia appena sopra) esiste una libreria nativa: libsgmainso-6.4.36.so. Lo apriamo in IDA e otteniamo una serie di finestre di dialogo con errori. Il problema è che la tabella dell'intestazione della sezione non è valida. Questo viene fatto apposta per complicare l'analisi.

Alla ricerca di vulnerabilità in UC Browser

Ma non serve: per caricare correttamente un file ELF e analizzarlo è sufficiente una tabella di intestazione del programma. Pertanto, eliminiamo semplicemente la tabella delle sezioni, azzerando i campi corrispondenti nell'intestazione.

Alla ricerca di vulnerabilità in UC Browser

Apri nuovamente il file in IDA.

Esistono due modi per indicare alla Java virtual machine dove si trova esattamente nella libreria nativa l'implementazione di un metodo dichiarato nel codice Java come nativo. Il primo è dargli un nome di specie Nome_pacchetto_Java_NomeClasse_NomeMetodo.

Il secondo è registrarlo durante il caricamento della libreria (nella funzione JNI_OnLoad)
utilizzando una chiamata di funzione Registrati Nativi.

Nel nostro caso, se utilizziamo il primo metodo, il nome dovrebbe essere così: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Non esiste una funzione simile tra le funzioni esportate, il che significa che è necessario cercare una chiamata Registrati Nativi.
Andiamo alla funzione JNI_OnLoad e vediamo questa immagine:

Alla ricerca di vulnerabilità in UC Browser

Cosa sta succedendo qui? A prima vista, l'inizio e la fine della funzione sono tipici dell'architettura ARM. La prima istruzione sullo stack memorizza il contenuto dei registri che la funzione utilizzerà nel suo funzionamento (in questo caso, R0, R1 e R2), nonché il contenuto del registro LR, che contiene l'indirizzo di ritorno dalla funzione . L'ultima istruzione ripristina i registri salvati e l'indirizzo di ritorno viene immediatamente inserito nel registro del PC, tornando così dalla funzione. Ma se guardi da vicino, noterai che la penultima istruzione cambia l'indirizzo del mittente memorizzato nello stack. Calcoliamo come sarà dopo
esecuzione del codice. Un certo indirizzo 1xB0 viene caricato in R130, da esso viene sottratto 5, quindi viene trasferito a R0 e ad esso viene aggiunto 0x10. Risulta 0xB13B. Pertanto, IDA pensa che l'ultima istruzione sia una normale funzione di ritorno, ma in realtà andrà all'indirizzo calcolato 0xB13B.

Vale la pena ricordare qui che i processori ARM hanno due modalità e due set di istruzioni: ARM e Thumb. Il bit meno significativo dell'indirizzo indica al processore quale set di istruzioni viene utilizzato. Cioè, l'indirizzo è in realtà 0xB13A e uno nel bit meno significativo indica la modalità Thumb.

Un "adattatore" simile è stato aggiunto all'inizio di ciascuna funzione in questa libreria e
codice spazzatura. Non ci soffermeremo ulteriormente su di loro in dettaglio: ricordiamo solo
che il vero inizio di quasi tutte le funzioni è un po’ più lontano.

Poiché il codice non salta esplicitamente a 0xB13A, IDA stessa non ha riconosciuto che il codice si trovasse in questa posizione. Per lo stesso motivo non riconosce come codice la maggior parte del codice presente nella libreria, il che rende l'analisi alquanto difficile. Diciamo a IDA che questo è il codice, e questo è ciò che accade:

Alla ricerca di vulnerabilità in UC Browser

La tabella inizia chiaramente da 0xB144. Cosa c'è nel sub_494C?

Alla ricerca di vulnerabilità in UC Browser

Quando chiamiamo questa funzione nel registro LR, otteniamo l'indirizzo della tabella precedentemente menzionata (0xB144). In R0 - indice in questa tabella. Cioè, il valore viene preso dalla tabella, aggiunto a LR e il risultato è
l'indirizzo a cui recarsi. Proviamo a calcolarlo: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Andiamo all'indirizzo ricevuto e vediamo letteralmente un paio di istruzioni utili e andiamo di nuovo a 0xB140:

Alla ricerca di vulnerabilità in UC Browser

Ora ci sarà una transizione all'offset con indice 0x20 dalla tabella.

A giudicare dalle dimensioni della tabella, ci saranno molte di queste transizioni nel codice. Sorge la domanda se sia possibile gestire la situazione in qualche modo in modo più automatico, senza calcolare manualmente gli indirizzi. E gli script e la possibilità di applicare patch al codice in IDA vengono in nostro aiuto:

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"

Posiziona il cursore sulla riga 0xB26A, esegui lo script e osserva la transizione a 0xB4B0:

Alla ricerca di vulnerabilità in UC Browser

Ancora una volta l'IDA non ha riconosciuto quest'area come codice. La aiutiamo e vediamo un altro disegno lì:

Alla ricerca di vulnerabilità in UC Browser

Le istruzioni dopo BLX non sembrano avere molto senso, è più come una sorta di spostamento. Diamo un'occhiata a sub_4964:

Alla ricerca di vulnerabilità in UC Browser

E infatti qui viene presa una dword all'indirizzo che si trova in LR, aggiunta a questo indirizzo, dopodiché viene preso il valore dell'indirizzo risultante e messo nello stack. Inoltre, 4 viene aggiunto a LR in modo che dopo il ritorno dalla funzione, lo stesso offset venga saltato. Dopodiché il comando POP {R1} prende il valore risultante dallo stack. Se guardi cosa si trova all'indirizzo 0xB4BA + 0xEA = 0xB5A4, vedrai qualcosa di simile ad una tabella di indirizzi:

Alla ricerca di vulnerabilità in UC Browser

Per applicare una patch a questo disegno, dovrai ottenere due parametri dal codice: l'offset e il numero di registro in cui vuoi inserire il risultato. Per ogni possibile registro, dovrai preparare preventivamente un pezzo di codice.

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"

Posizioniamo il cursore all'inizio della struttura che vogliamo sostituire - 0xB4B2 - ed eseguiamo lo script:

Alla ricerca di vulnerabilità in UC Browser

Oltre alle strutture già citate, il codice contiene anche quanto segue:

Alla ricerca di vulnerabilità in UC Browser

Come nel caso precedente, dopo l'istruzione BLX c'è un offset:

Alla ricerca di vulnerabilità in UC Browser

Prendiamo l'offset dell'indirizzo da LR, lo aggiungiamo a LR e andiamo lì. 0x72044 + 0xC = 0x72050. Lo script per questo progetto è abbastanza semplice:

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"

Risultato dell'esecuzione dello script:

Alla ricerca di vulnerabilità in UC Browser

Una volta che tutto è stato corretto nella funzione, puoi indirizzare IDA al suo vero inizio. Metterà insieme tutto il codice della funzione e potrà essere decompilato utilizzando HexRays.

Decrittazione delle stringhe

Abbiamo imparato a gestire l'offuscamento del codice macchina nella libreria libsgmainso-6.4.36.so da UC Browser e ho ricevuto il codice funzione 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;
}

Diamo un'occhiata più da vicino alle seguenti righe:

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

In funzione sub_73E24 il nome della classe viene chiaramente decrittografato. Come parametri di questa funzione vengono passati un puntatore a dati simili a dati crittografati, un determinato buffer e un numero. Ovviamente, dopo aver chiamato la funzione, nel buffer ci sarà una riga decriptata, poiché viene passata alla funzione TrovaClasse, che accetta il nome della classe come secondo parametro. Pertanto, il numero è la dimensione del buffer o la lunghezza della riga. Proviamo a decifrare il nome della classe, dovrebbe dirci se stiamo andando nella direzione giusta. Diamo uno sguardo più da vicino a cosa succede in 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;
}

Funzione sub_7AF78 crea un'istanza di un contenitore per array di byte della dimensione specificata (non ci soffermeremo su questi contenitori in dettaglio). Qui vengono creati due contenitori di questo tipo: uno contiene la riga "DcO/lcK+h?m3c*q@" (è facile intuire che si tratta di una chiave), l'altro contiene dati crittografati. Successivamente, entrambi gli oggetti vengono inseriti in una determinata struttura, che viene passata alla funzione sub_6115C. Contrassegniamo anche un campo con il valore 3 in questa struttura. Vediamo dopo cosa succede a questa struttura.

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

Il parametro switch è un campo della struttura a cui è stato precedentemente assegnato il valore 3. Guarda il caso 3: alla funzione sub_6364C i parametri vengono passati dalla struttura che erano stati aggiunti nella funzione precedente, cioè la chiave e i dati crittografati. Se guardi da vicino sub_6364C, puoi riconoscere l'algoritmo RC4 al suo interno.

Abbiamo un algoritmo e una chiave. Proviamo a decifrare il nome della classe. Ecco cosa è successo: com/taobao/wireless/security/adapter/JNICLibrary. Grande! Siamo sulla strada giusta.

Albero dei comandi

Ora dobbiamo trovare una sfida Registrati Nativi, che ci indicherà la funzione doCommandNative. Diamo un'occhiata alle funzioni chiamate da JNI_OnLoad, e lo troviamo dentro 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;
}

E infatti qui è registrato un metodo nativo con questo nome doCommandNative. Ora conosciamo il suo indirizzo. Vediamo cosa fa.

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

Dal nome puoi intuire che qui è il punto di ingresso di tutte le funzioni che gli sviluppatori hanno deciso di trasferire nella libreria nativa. Siamo interessati alla funzione numero 10601.

Puoi vedere dal codice che il numero di comando produce tre numeri: comando/10000, comando % 10000/100 и comando% 10, cioè, nel nostro caso, 1, 6 e 1. Questi tre numeri, così come un puntatore a JNIEnv e gli argomenti passati alla funzione vengono aggiunti a una struttura e trasmessi. Utilizzando i tre numeri ottenuti (denotiamoli N1, N2 e N3), viene costruito un albero dei comandi.

Qualcosa del genere:

Alla ricerca di vulnerabilità in UC Browser

L'albero viene riempito dinamicamente JNI_OnLoad.
Tre numeri codificano il percorso nell'albero. Ogni foglia dell'albero contiene l'indirizzo butterato della funzione corrispondente. La chiave è nel nodo genitore. Trovare il punto nel codice in cui viene aggiunta all'albero la funzione che ci serve non è difficile se si comprendono tutte le strutture utilizzate (non le descriviamo per non gonfiare un articolo già piuttosto grande).

Più offuscamento

Abbiamo ricevuto l'indirizzo della funzione che dovrebbe decriptare il traffico: 0x5F1AC. Ma è troppo presto per rallegrarsi: gli sviluppatori di UC Browser ci hanno preparato un’altra sorpresa.

Dopo aver ricevuto i parametri dall'array formato nel codice Java, otteniamo
alla funzione all'indirizzo 0x4D070. E qui ci aspetta un altro tipo di offuscamento del codice.

Inseriamo due indici in R7 e R4:

Alla ricerca di vulnerabilità in UC Browser

Spostiamo il primo indice in R11:

Alla ricerca di vulnerabilità in UC Browser

Per ottenere un indirizzo da una tabella, utilizzare un indice:

Alla ricerca di vulnerabilità in UC Browser

Dopo essere passati al primo indirizzo, viene utilizzato il secondo indice, che si trova in R4. Ci sono 230 elementi nella tabella.

Cosa fare al riguardo? Puoi dire a IDA che si tratta di uno switch: Modifica -> Altro -> Specifica l'idioma dello switch.

Alla ricerca di vulnerabilità in UC Browser

Il codice risultante è terribile. Ma, facendoti strada nella sua giungla, puoi notare un richiamo a una funzione a noi già familiare sub_6115C:

Alla ricerca di vulnerabilità in UC Browser

C'è stato un passaggio in cui nel caso 3 si è verificata una decrittazione utilizzando l'algoritmo RC4. E in questo caso, la struttura passata alla funzione viene riempita con i parametri passati a doCommandNative. Ricordiamo cosa avevamo lì magiaInt con il valore 16. Esaminiamo il caso corrispondente e dopo diverse transizioni troviamo il codice con cui è possibile identificare l'algoritmo.

Alla ricerca di vulnerabilità in UC Browser

Questo è AES!

L'algoritmo esiste, non resta che ricavarne i parametri: modalità, chiave ed eventualmente il vettore di inizializzazione (la sua presenza dipende dalla modalità operativa dell'algoritmo AES). La struttura con loro deve essere formata da qualche parte prima della chiamata della funzione sub_6115C, ma questa parte del codice è particolarmente ben offuscata, quindi nasce l'idea di patchare il codice in modo che tutti i parametri della funzione di decrittazione vengano scaricati in un file.

toppa

Per non scrivere manualmente tutto il codice patch in linguaggio assembly, puoi avviare Android Studio, scrivere lì una funzione che riceve gli stessi parametri di input della nostra funzione di decrittazione e scrive in un file, quindi copiare e incollare il codice che il compilatore creerà creare.

Anche i nostri amici del team UC Browser si sono presi cura della comodità di aggiungere codice. Ricordiamo che all'inizio di ogni funzione abbiamo del codice spazzatura che può essere facilmente sostituito con qualsiasi altro. Molto comodo 🙂 Tuttavia, all'inizio della funzione target non c'è abbastanza spazio per il codice che salva tutti i parametri in un file. Ho dovuto dividerlo in parti e utilizzare i blocchi spazzatura delle funzioni vicine. C'erano quattro parti in totale.

La prima parte:

Alla ricerca di vulnerabilità in UC Browser

Nell'architettura ARM, i primi quattro parametri della funzione vengono passati attraverso i registri R0-R3, il resto, se presente, viene passato attraverso lo stack. Il registro LR riporta l'indirizzo del mittente. Tutto ciò deve essere salvato in modo che la funzione possa funzionare dopo aver scaricato i suoi parametri. Dobbiamo anche salvare tutti i registri che utilizzeremo nel processo, quindi eseguiamo PUSH.W {R0-R10,LR}. In R7 otteniamo l'indirizzo della lista di parametri passati alla funzione tramite lo stack.

Utilizzo della funzione fope apriamo il file /data/local/tmp/aes in modalità "ab".
cioè per addizione. In R0 carichiamo l'indirizzo del nome del file, in R1 - l'indirizzo della riga che indica la modalità. E qui finisce il codice spazzatura, quindi passiamo alla funzione successiva. Affinché continui a funzionare, mettiamo all'inizio la transizione al codice reale della funzione, bypassando la spazzatura, e al posto della spazzatura aggiungiamo la continuazione della patch.

Alla ricerca di vulnerabilità in UC Browser

Noi chiamiamo fope.

I primi tre parametri della funzione aes avere tipo int. Dato che all'inizio abbiamo salvato i registri nello stack, possiamo semplicemente passare la funzione fscrivi i loro indirizzi nello stack.

Alla ricerca di vulnerabilità in UC Browser

Successivamente abbiamo tre strutture che contengono la dimensione dei dati e un puntatore ai dati per la chiave, il vettore di inizializzazione e i dati crittografati.

Alla ricerca di vulnerabilità in UC Browser

Al termine chiudere il file, ripristinare i registri e trasferire il controllo alla funzione reale aes.

Raccogliamo un APK con una libreria patchata, lo firmiamo, lo carichiamo sul dispositivo/emulatore e lo lanciamo. Vediamo che il nostro dump è in fase di creazione e molti dati vengono scritti lì. Il browser utilizza la crittografia non solo per il traffico e tutta la crittografia passa attraverso la funzione in questione. Ma per qualche motivo i dati necessari non sono presenti e la richiesta richiesta non è visibile nel traffico. Per non aspettare che UC Browser si degni di fare la richiesta necessaria, prendiamo la risposta crittografata dal server ricevuta in precedenza e patchamo nuovamente l'applicazione: aggiungiamo la decrittazione a onCreate dell'attività principale.

    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

Assembliamo, firmiamo, installiamo, lanciamo. Riceviamo una NullPointerException perché il metodo ha restituito null.

Durante l'ulteriore analisi del codice è stata scoperta una funzione che decifra le righe interessanti: “META-INF/” e “.RSA”. Sembra che l'applicazione stia verificando il suo certificato. O addirittura genera chiavi da esso. Non voglio davvero occuparmi di ciò che sta accadendo con il certificato, quindi gli invieremo semplicemente il certificato corretto. Patchiamo la riga crittografata in modo che invece di "META-INF/" otteniamo "BLABLINF/", creiamo una cartella con quel nome nell'APK e aggiungiamo lì il certificato del browser squirrel.

Assembliamo, firmiamo, installiamo, lanciamo. Bingo! Abbiamo la chiave!

MitM

Abbiamo ricevuto una chiave e un vettore di inizializzazione uguale alla chiave. Proviamo a decrittografare la risposta del server in modalità CBC.

Alla ricerca di vulnerabilità in UC Browser

Vediamo l'URL dell'archivio, qualcosa di simile a MD5, "extract_unzipsize" e un numero. Controlliamo: l'MD5 dell'archivio è lo stesso, la dimensione della libreria spacchettata è la stessa. Stiamo provando ad applicare una patch a questa libreria e a fornirla al browser. Per mostrare che la nostra libreria patchata è stata caricata, lanceremo un intento per creare un SMS con il testo "PWNED!" Sostituiremo due risposte dal server: puds.ucweb.com/upgrade/index.xhtml e per scaricare l'archivio. Nel primo sostituiamo MD5 (la dimensione non cambia dopo il disimballaggio), nel secondo diamo l'archivio con la libreria patchata.

Il browser tenta più volte di scaricare l'archivio, dopodiché restituisce un errore. Apparentemente qualcosa
non gli piace. Dall'analisi di questo formato oscuro si è scoperto che il server trasmette anche la dimensione dell'archivio:

Alla ricerca di vulnerabilità in UC Browser

È codificato in LEB128. Dopo la patch, la dimensione dell'archivio con la libreria è leggermente cambiata, quindi il browser ha ritenuto che l'archivio fosse stato scaricato in modo storto e dopo diversi tentativi ha generato un errore.

Adeguiamo la dimensione dell'archivio... E – vittoria! 🙂 Il risultato è nel video.

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

Conseguenze e reazione degli sviluppatori

Allo stesso modo, gli hacker potrebbero utilizzare la funzionalità non sicura di UC Browser per distribuire ed eseguire librerie dannose. Queste librerie funzioneranno nel contesto del browser, quindi riceveranno tutte le sue autorizzazioni di sistema. Di conseguenza, la possibilità di visualizzare finestre di phishing, nonché l'accesso ai file di lavoro dello scoiattolo cinese arancione, inclusi login, password e cookie memorizzati nel database.

Abbiamo contattato gli sviluppatori di UC Browser e li abbiamo informati del problema riscontrato, abbiamo cercato di segnalare la vulnerabilità e la sua pericolosità, ma non hanno discusso di nulla con noi. Nel frattempo, il browser continuava a ostentare in bella vista le sue caratteristiche pericolose. Ma una volta rivelati i dettagli della vulnerabilità, non era più possibile ignorarla come prima. Il 27 marzo era
è stata rilasciata una nuova versione di UC Browser 12.10.9.1193 che accedeva al server tramite HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Inoltre, dopo la “correzione” e fino al momento della stesura di questo articolo, tentando di aprire un PDF in un browser veniva visualizzato un messaggio di errore con il testo “Spiacenti, qualcosa è andato storto!” Non è stata effettuata una richiesta al server durante il tentativo di aprire un PDF, ma è stata effettuata una richiesta all'avvio del browser, il che suggerisce la continua capacità di scaricare codice eseguibile in violazione delle regole di Google Play.

Fonte: habr.com

Aggiungi un commento