
Introduzione
Alla fine di marzo abbiamo , che abbiamo scoperto una possibilità nascosta di caricare e avviare codice non verificato su UC Browser. Oggi analizzeremo in dettaglio come avviene questo caricamento e come gli hacker potrebbero utilizzarlo per i loro scopi.
Qualche tempo fa UC Browser è stato pubblicizzato e distribuito in modo molto aggressivo: veniva installato sui dispositivi degli utenti attraverso software malevoli, diffuso da vari siti sotto forma di file video (ovvero gli utenti pensavano di scaricare ad esempio un video pornografico, ma ricevevano invece un APK con questo browser), utilizzando banner allarmanti con messaggi che avvisavano che il browser era obsoleto, vulnerabile e simili. Nella pagina ufficiale di UC Browser su VK ci sono , dove gli utenti possono segnalare pubblicità ingannevoli, con molti esempi. Nel 2016 c'era addirittura in lingua russa (sì, pubblicità di un browser che blocca la pubblicità).
Al momento della scrittura di questo articolo UC Browser aveva accumulato oltre 500.000.000 installazioni su Google Play. Questo è impressionante: solo Google Chrome ha di più. Tra le recensioni ci sono molte lamentele riguardo la pubblicità e i reindirizzamenti a determinate applicazioni su Google Play. Questo è stato il motivo della nostra ricerca: abbiamo deciso di vedere se UC Browser stesse facendo qualcosa di male. E si è rivelato che lo stava facendo!
Nel codice dell'applicazione è stata trovata la possibilità di caricare e avviare codice eseguibile, su Google Play. Oltre al fatto che UC Browser carica codice eseguibile, lo fa in modo non sicuro, il che potrebbe essere usato per effettuare un attacco MitM. Vediamo se riusciamo a eseguire un tale attacco.
Tutto ciò che segue è attuale per la versione di UC Browser presente su Google Play al momento della nostra ricerca:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-fail: f5edb2243413c777172f6362876041eb0c3a928cVettore di attacco
Nel manifesto di UC Browser è possibile scoprire un servizio con un nome significativo 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 , che si può notare nel traffico qualche tempo dopo l'avvio. In risposta, può ricevere un comando per scaricare un aggiornamento o un nuovo modulo. Durante l'analisi, il server non ha inviato tali comandi, ma abbiamo notato che quando si tenta di aprire un PDF nel browser, fa una richiesta ripetuta all'indirizzo sopra indicato, dopodiché scarica la libreria nativa. Per condurre l'attacco, abbiamo deciso di sfruttare questa peculiarità di UC Browser: la capacità di aprire PDF tramite una libreria nativa, che non è presente nell'APK e che viene caricata da Internet se necessario. Vale la pena notare che, teoricamente, UC Browser può essere costretto a scaricare qualcosa anche senza interazione da parte dell'utente – se si fornisce una risposta ben formata alla richiesta eseguita dopo l'avvio del browser. Ma per fare ciò è necessario studiare più in dettaglio il protocollo di interazione con il server, quindi abbiamo deciso che era più semplice modificare la risposta intercettata e sostituire la libreria per lavorare con i PDF.
Quindi, quando l'utente desidera aprire un PDF direttamente nel browser, nel traffico si possono vedere le seguenti richieste:

Prima c'è una richiesta POST a , dopo di che
viene scaricato un archivio con la libreria per visualizzare PDF e formati Office. È lecito supporre che nella prima richiesta venga trasmessa l'informazione sul sistema (almeno, l'architettura, per fornire la libreria corretta), e in risposta a questa, il browser riceve alcune informazioni sulla libreria da scaricare: l'indirizzo e, forse, qualcos'altro. Il problema è che questa richiesta è crittografata.
Frammento della richiesta
Frammento della risposta


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

Ricerca del codice per decifrare il traffico
Proveremo a decifrare la risposta del server. Guardiamo il codice della classe com.uc.deployment.UpgradeDeployService: dal metodo onStartCommand ci spostiamo in com.uc.deployment.b.x, e da qui in 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("]prodotto:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]versione:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]tipo_aggiornamento:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]forza_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]modalità_silenziosa:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]tipo_silenzioso:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]stato_silenzioso:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]file_silenzioso:");
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("]tipo_download:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]gruppo_download:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]percorso_download:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]versione_figlio_apollo:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]serie_apollo:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]architettura_cpu_apollo:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]cpu_vfp3_apollo:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]cpu_vfp_apollo:");
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("]nome_componente:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]nome_ver_componente:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]codice_ver_componente:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]tipo_req_componente:");
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("ver_os", c.getRomInfo()));
v3_2.add(g.fs("architettura_processore", 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("tipo_rete", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("ver_plugin", arg9.iGX.iEn));
v3_2.add(g.fs("lingua_target", arg9.iGX.iEs));
v3_2.add(g.fs("architettura_cpu_vitamio", arg9.iGX.iEt));
v3_2.add(g.fs("vfp_vitamio", arg9.iGX.iEu));
v3_2.add(g.fs("vfp3_vitamio", arg9.iGX.iEv));
v3_2.add(g.fs("ver_figlio_plugin", arg9.iGX.iEx));
v3_2.add(g.fs("serie_ver", arg9.iGX.iEw));
v3_2.add(g.fs("ver_figlio", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_firma", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("log_aggiornamento", i.bjt()));
v3_2.add(g.fs("installazione_silenziosa", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("stato_silenzioso", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("file_silenzioso", arg9.iGX.iEq));
v3_2.add(g.fs("tipo_silenzioso", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("architettura_cpu", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("set_cpu", 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("nuclei_cpu", 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("totram", 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("livello_api", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("lista_apk_uc", 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. Facciamo attenzione alla creazione di un array di 16 byte e al suo riempimento: 0x5F, 0, 0x1F, -50 (=0xCE). Corrisponde a quanto abbiamo visto nella richiesta sopra.
Nello stesso classe possiamo notare una classe annidata, in cui c'è 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 riceve un array di byte e verifica che il byte zero sia pari a 0x60 o che il terzo byte sia pari a 0xD0, e che il secondo byte sia 1, 11 o 0x1F. Guardiamo la risposta del server: il byte zero è 0x60, il secondo è 0x1F, il terzo è 0x60. Sembra quello che ci serve. A giudicare dalle righe ("up_decrypt", ad esempio), qui dovrebbe essere chiamato un metodo che decrittografa la risposta del server.
Passiamo al metodo g.j. Notiamo che come primo argomento viene passato il byte all'offset 2 (cioè 0x1F nel nostro caso), e come secondo — la risposta del server senza
i 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 avviene la selezione dell'algoritmo di decrittazione, e quel byte, che nel nostro
Nel caso in cui sia pari a 0x1F, indica una delle tre possibili varianti.
Continuiamo ad analizzare il codice. Dopo un paio di salti, arriviamo a un metodo dal nome eloquente decryptBytesByKey.
Qui vengono separati ulteriori due byte dalla nostra risposta, da cui 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 byte
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Selezione della chiave
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;
}Anticipando, notiamo che a questo punto non si ottiene ancora la chiave, ma solo il suo "identificatore". Ottenere la chiave è un po’ più complesso.
Nel metodo successivo, ai parametri esistenti se ne aggiungono altri due, diventando così quattro: il numero magico 16, l'identificatore della chiave, i dati crittografati e una stringa misteriosa (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 interfaccia com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Nel codice principale dell'applicazione, non ci sono classi che implementano questa interfaccia. Una tale classe esiste nel file lib/armeabi-v7a/libsgmain.so, che in realtà non è un .so, ma un .jar. Il metodo che ci interessa è implementato nel seguente modo:
pacchetto 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);
}
//...
} Qui la nostra lista di parametri è completata da due numeri interi: 2 e 0. A quanto pare,
2 indica la decrittazione, come nel metodo doFinal della classe di sistema javax.crypto.Cipher. E tutto questo viene passato a un certo Router con il numero 10601 — questo, apparentemente, è il numero del comando.
Dopo una serie di passaggi troviamo una classe che implementa l'interfaccia IRouterComponent e il metodo doCommand:
pacchetto 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 la classe JNICLibrary, in cui è dichiarato il metodo nativo doCommandNative:
pacchetto com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Quindi, dobbiamo trovare il metodo nel codice nativo. doCommandNativeE qui inizia il vero divertimento.
Loffuscamento del codice macchina
Nel file libsgmain.so (che in realtà è .jar e in cui poco sopra abbiamo trovato l'implementazione di alcune interfacce legate alla crittografia) c'è una libreria nativa: libsgmainso-6.4.36.so. La apriamo in IDA e otteniamo una serie di finestre di dialogo con errori. Il problema è che la tabella delle sezioni (section header table) è non valida. Questo è stato fatto appositamente per complicare l'analisi.

Ma non è necessaria: per caricare correttamente il file ELF e analizzarlo, è sufficiente la tabella dei segmenti (program header table). Pertanto, basta eliminare la tabella delle sezioni, azzerando i campi corrispondenti nell'intestazione.

Riapriamo il file in IDA.
Ci sono due modi per indicare alla macchina virtuale Java dove si trova esattamente nella libreria nativa l'implementazione del metodo dichiarato nel codice Java come nativo. Il primo è dargli un nome del tipo Java_nome_pacchetto_NomeClasse_nomeMetodo.
Secondo: registrarlo durante il caricamento della libreria (nella funzione JNI_OnLoad)
utilizzando la chiamata della funzione RegisterNatives.
Nel nostro caso, se si utilizza il primo metodo, il nome deve essere il seguente: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Tra le funzioni esportate non ce n'è nessuna di questo tipo, quindi dobbiamo cercare la chiamata RegisterNatives.
Andiamo alla funzione JNI_OnLoad e vediamo questa situazione:

Cosa sta succedendo qui? A prima vista, l'inizio e la fine della funzione sono tipici per l'architettura ARM. La prima istruzione salva sullo stack il contenuto dei registri che la funzione utilizzerà nel proprio lavoro (in questo caso R0, R1 e R2), così come 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 PC: in questo modo avviene il ritorno dalla funzione. Ma se si guarda più attentamente, si può notare che l'istruzione penultima modifica l'indirizzo di ritorno salvato nello stack. Calcoliamo quale sarà dopo
l'esecuzione del codice. Viene caricato in R1 un certo indirizzo 0xB130, da cui si sottrae 5, quindi viene trasferito in R0 e a questo viene aggiunto 0x10. Si ottiene 0xB13B. Pertanto, IDA pensa che nell'ultima istruzione ci sia un normale ritorno dalla funzione, mentre in realtà c'è un salto all'indirizzo calcolato 0xB13B.
Vale la pena ricordare 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 l'unità nel bit meno significativo indica la modalità Thumb.
All'inizio di ogni funzione in questa libreria è stato aggiunto un simile 'adattatore' e
codice spazzatura. Non ci fermeremo a discuterli in dettaglio - ricordiamo semplicemente
che l'inizio reale di quasi tutte le funzioni si trova un po' più in là.
Poiché nel codice non c'è un salto esplicito a 0xB13A, IDA non ha riconosciuto da sola che in questo punto si trova un codice. Per la stessa ragione, gran parte del codice nella libreria non viene identificato come codice, il che rende l'analisi leggermente più difficile. Diciamo a IDA che qui c'è codice, e ecco cosa otteniamo:

A 0xB144 inizia chiaramente una tabella. E cosa c'è in sub_494C?

Quando si chiama questa funzione, nel registro LR otteniamo l'indirizzo della tabella menzionata in precedenza (0xB144). In R0 c'è l'indice in questa tabella. Cioè, si prende un valore dalla tabella, si aggiunge a LR e si ottiene
l'indirizzo a cui è necessario accedere. Proveremo a calcolarlo: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Accediamo all'indirizzo ottenuto e vediamo letteralmente un paio di istruzioni utili e di nuovo un accesso a 0xB140:

Ora ci sarà un accesso tramite offset con indice 0x20 dalla tabella.
A giudicare dalle dimensioni della tabella, tali accessi nel codice si incontreranno spesso. Sorge la domanda se ci sia un modo per affrontarli in modo più automatizzato, senza calcolare manualmente gli indirizzi. E ci vengono in aiuto gli script e la possibilità di patchare il codice in IDA:
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 "Tipo di operando errato su", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Impossibile trovare la tabella"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Codice sconosciuto", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Impossibile rilevare la prima istruzione"Posizioniamo il cursore sulla riga 0xB26A, avviamo lo script e vediamo un accesso a 0xB4B0:

IDA non ha identificato di nuovo questa sezione come codice. La aiutiamo e vediamo un'altra costruzione:

Le istruzioni dopo BLX non sembrano molto sensate, somigliano più a qualche offset. Dai un'occhiata a sub_4964:

E in effetti, qui viene preso un dword dall'indirizzo contenuto in LR, viene aggiunto a questo indirizzo, dopodiché viene preso il valore dall'indirizzo ottenuto e messo nello stack. Inoltre, a LR viene aggiunto 4, per saltare questo offset dopo il ritorno dalla funzione. Dopodiché, il comando POP {R1} estrae il valore ottenuto dallo stack. Se si guarda che cosa si trova all'indirizzo 0xB4BA + 0xEA = 0xB5A4, si può vedere qualcosa di simile a una tabella di indirizzi:

Per patchare questa costruzione, sarà necessario ottenere due parametri dal codice: l'offset e il numero del registro in cui bisogna collocare il risultato. Per ogni possibile registro, sarà necessario preparare in anticipo un segmento 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 <> 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 "Istruzione POP non trovata"
else:
print "Tipo di operando errato su +4:", get_operand_type(ea + 4, 0)
else:
print "Impossibile rilevare le prime istruzioni"Posizioniamo il cursore all'inizio della costruzione che vogliamo sostituire — 0xB4B2 — e avviamo lo script:

Oltre alle costruzioni già menzionate, nel codice si trovano anche queste:

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

Prendiamo l'offset dall'indirizzo in LR, lo sommiamo a LR e ci andiamo. 0x72044 + 0xC = 0x72050. Lo script per questa costruzione è molto semplice:
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 "Impossibile rilevare la prima istruzione"Risultato dell'esecuzione dello script:

Dopo aver patchato tutto nel funzionamento, è possibile indicare a IDA il suo vero inizio. Essa raccoglierà tutto il codice della funzione a pezzi e potrà essere decompilato con HexRays.
Decodifica delle stringhe
Abbiamo imparato a combattere l'offuscamento del codice macchina nella libreria libsgmainso-6.4.36.so da UC Browser e abbiamo ottenuto il codice della 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;
}Esaminiamo più attentamente le seguenti righe:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);Nella funzione sub_73E24 si svolge chiaramente la decodifica del nome della classe. Come parametri di questa funzione viene passato un puntatore ai dati, simile a quelli cifrati, un buffer e un numero. È evidente che dopo la chiamata della funzione, nel buffer ci sarà la stringa decodificata, dato che viene passata alla funzione FindClass, che accetta come secondo parametro il nome della classe. Dunque, il numero rappresenta la dimensione del buffer o la lunghezza della stringa. Proveremo a decodificare il nome della classe, dovrebbe indicarci se ci stiamo muovendo nella giusta direzione. Analizziamo più in dettaglio cosa accade 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;
}La funzione sub_7AF78 crea un'istanza di contenitore per array di byte della dimensione specificata (non ci soffermeremo troppo su questi contenitori). Qui vengono creati due contenitori di questo tipo: una stringa viene inserita in uno «DcO/lcK+h?m3c*q@» (non è difficile indovinare che questa è una chiave), nell'altro ci sono i dati criptati. Successivamente, entrambi gli oggetti vengono inseriti in una certa struttura, che viene passata alla funzione sub_6115C. Notiamo anche che in questa struttura c'è un campo con valore 3. Vediamo cosa succede ulteriormente 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 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;
}Come parametro switch viene passato il campo della struttura, a cui era stato precedentemente assegnato il valore 3. Guardiamo il case 3: nella funzione sub_6364C vengono passati i parametri dalla struttura, che erano stati immagazzinati lì nella funzione precedente, cioè la chiave e i dati criptati. Se guardiamo attentamente a sub_6364C, possiamo riconoscere l'algoritmo RC4.
Abbiamo l'algoritmo e la chiave. Proviamo a decrittare il nome della classe. Ecco cosa è venuto fuori: com/taobao/wireless/security/adapter/JNICLibrary. Ottimo! Siamo sulla strada giusta.
Albero dei comandi
Ora dobbiamo trovare la chiamata RegisterNatives, che ci indicherà la funzione doCommandNative. Guardiamo le funzioni chiamate da JNI_OnLoad, e la troviamo 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;
}E in effetti, qui viene registrato un metodo nativo con il 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 si può dedurre che qui si trova il punto di ingresso di tutte le funzioni che gli sviluppatori hanno deciso di spostare nella libreria nativa. Siamo interessati alla funzione con numero 10601.
Dal codice si può vedere che dal numero del comando ottenendo tre numeri: command / 10000, command % 10000 / 100 e command % 10, cioè, nel nostro caso, 1, 6 e 1. Questi tre numeri, così come il puntatore su JNIEnv e gli argomenti passati alla funzione, sono memorizzati in una struttura e passati oltre. Con i tre numeri ottenuti (indichiamoli come N1, N2 e N3) viene costruito un albero dei comandi.
Circa in questo modo:

L'albero viene riempito dinamicamente in JNI_OnLoad.
Tre numeri codificano un percorso nell'albero. Ogni foglia dell'albero contiene l'indirizzo mascherato della funzione corrispondente. La chiave è nel nodo genitore. Trovare il punto nel codice in cui viene aggiunta la funzione necessaria all'albero non è un compito difficile, se si comprendono tutte le strutture utilizzate (non forniamo la loro descrizione per non gonfiare ulteriormente un articolo già lungo).
Ancora obfuscazione
Abbiamo ottenuto l'indirizzo della funzione che dovrebbe decrittografare il traffico: 0x5F1AC. Ma è ancora presto per rallegrarsi: gli sviluppatori di UC Browser ci hanno riservato un'altra sorpresa.
Dopo aver ottenuto i parametri dall'array formato nel codice Java, entriamo
nella funzione all'indirizzo 0x4D070. E qui ci aspetta un'altra forma di obfuscazione del codice.
Mettiamo in R7 e R4 due indici:

Spostiamo il primo indice in R11:

Per ottenere l'indirizzo dalla tabella, utilizziamo l'indice:

Dopo aver passato il primo indirizzo, si utilizza il secondo indice, che si trova in R4. Nella tabella ci sono 230 elementi.
Cosa fare con questo? Possiamo dire a IDA che è un switch: Modifica -> Altro -> Specifica l'idioma dello switch.

Il codice risultante è spaventoso. Ma, attraversando le sue complessità, possiamo notare la chiamata a una funzione già a noi nota. sub_6115C:

C'era uno switch, in cui nel case 3 si trovava la decrittografia utilizzando l'algoritmo RC4. E in questo caso la struttura passata alla funzione viene compilata dai parametri passati a doCommandNative. Ricordiamo che avevamo lì un magicInt con il valore 16. Guardiamo il case corrispondente – e dopo diversi passaggi troviamo il codice che permette di riconoscere l'algoritmo.

È AES!
Abbiamo l'algoritmo, resta solo da ottenere i suoi parametri: modalità, chiave e, possibilmente, vettore di inizializzazione (la sua presenza dipende dalla modalità di funzionamento dell'algoritmo AES). La struttura che li contiene deve essere formata da qualche parte prima della chiamata della funzione sub_6115C, ma questa parte del codice è stata particolarmente ben offuscata, quindi viene in mente l'idea di patchare il codice in modo che tutti i parametri della funzione di decrittografia vengano salvati in un file.
Patch
Per non scrivere tutto il codice della patch in linguaggio assembly a mano, è possibile avviare Android Studio, scrivere una funzione che riceve in ingresso gli stessi parametri della nostra funzione di decrittografia e scrivere su un file, dopodiché incollare il codice che genererà il compilatore.
I nostri amici del team UC Browser si sono anche "preoccupati" 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, lo spazio per il codice che salva tutti i parametri in un file è un po' ridotto. Abbiamo dovuto suddividerlo in parti e utilizzare blocchi spazzatura delle funzioni vicine. In totale sono risultate quattro parti.
Prima parte:

Nell'architettura ARM, i primi quattro parametri della funzione vengono passati attraverso i registri R0-R3, gli altri, se presenti - attraverso lo stack. L'indirizzo di ritorno viene passato nel registro LR. Tutto ciò deve essere salvato affinché la funzione possa funzionare dopo che abbiamo dumpato i suoi parametri. È necessario anche salvare tutti i registri che utilizzeremo nel processo, quindi facciamo PUSH.W {R0-R10,LR}. In R7 otteniamo l'indirizzo dell'elenco dei parametri passati alla funzione tramite lo stack.
Utilizzando la funzione fopen apriamo il file /data/local/tmp/aes in modalità "ab",
ossia per aggiungere. In R0 carichiamo l'indirizzo del nome del file, in R1 - l'indirizzo della stringa che indica la modalità. Qui finisce il codice spazzatura, quindi passiamo alla funzione successiva. Affinché continui a funzionare, mettiamo all'inizio un salto al codice effettivo della funzione bypassando lo spazzatura e al posto dello spazzatura aggiungiamo la continuazione della patch.

Chiamiamo fopen.
I primi tre parametri della funzione aes hanno tipo int. Poiché all'inizio abbiamo salvato i registri nello stack, possiamo semplicemente passare alla funzione fwrite i loro indirizzi nello stack.

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

Alla fine chiudiamo il file, ripristiniamo i registri e trasferiamo il controllo alla vera funzione aes.
Compiliamo l'APK con la libreria patchata, lo firmiamo, lo carichiamo sul dispositivo/emulatore e lo avviamo. Vediamo che il nostro dump viene creato e che ci sono molte informazioni scritte. Il browser utilizza la crittografia non solo per il traffico, e tutta la crittografia passa attraverso la funzione considerata. Ma ci sono dati necessari che per qualche motivo mancano, e nel traffico non vediamo la richiesta necessaria. Per non aspettare che UC Browser si degni di fare la richiesta necessaria, prendiamo la risposta crittografata dal server ottenuta in precedenza e patchiamo nuovamente l'applicazione: aggiungiamo la decrittografia in 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;)IRaccogliamo, firmiamo, installiamo e avviamo. Otteniamo NullPointerException perché il metodo ha restituito null.
Durante un'ulteriore analisi del codice è stata scoperta una funzione che decripta righe interessanti: «META-INF/» e «.RSA». Sembra che l'app controlli il proprio certificato. O addirittura generi chiavi da esso. Non abbiamo voglia di indagare su cosa succeda con il certificato, quindi semplicemente gli forniremo il certificato corretto. Modificheremo la stringa crittografata in modo che invece di «META-INF/» ci sia «BLABLINF/», creeremo una cartella con questo nome nell'APK e ci metteremo dentro il certificato del browser.
Raccogliamo, firmiamo, installiamo e avviamo. Bingo! Abbiamo la chiave!
MitM
Abbiamo ottenuto la chiave e il vettore di inizializzazione, uguale alla chiave. Proviamo a decriptare la risposta del server in modalità CBC.

Vediamo l'URL dell'archivio, qualcosa di simile a MD5, «extract_unzipsize» e un numero. Controlliamo: l'MD5 dell'archivio coincide, la dimensione della libreria estratta coincide. Proviamo a modificare questa libreria e a restituirla al browser. Per dimostrare che la nostra libreria modificata è stata caricata, lanceremo un Intent per creare un SMS con il testo «PWNED!». Modificheremo due risposte dal server: e per il download dell'archivio. Nel primo modifichiamo l'MD5 (la dimensione dopo l'estrazione non cambia), nel secondo restituiamo l'archivio con la libreria modificata.
Il browser prova a scaricare l'archivio più volte, dopo di che restituisce un errore. Evidentemente, qualcosa
non gli piace. Come risultato dell'analisi di questo formato contorto, è emerso che il server trasmette anche la dimensione dell'archivio:

È codificata in LEB128. Dopo la modifica, la dimensione dell'archivio con la libreria è leggermente cambiata, quindi il browser ha pensato che l'archivio fosse stato scaricato in modo errato e, dopo diversi tentativi, ha restituito un errore.
Correggiamo la dimensione dell'archivio... E – vittoria! 🙂 Risultato nel video.
Conseguenze e reazione del sviluppatore
Allo stesso modo, gli hacker potrebbero utilizzare la funzione non sicura di UC Browser per diffondere ed eseguire librerie dannose. Queste librerie funzionerebbero nel contesto del browser, ottenendo così tutti i permessi di sistema. Di conseguenza, ciò potrebbe comportare la possibilità di mostrare finestre di phishing e accesso ai file di lavoro della scimmia cinese arancione, inclusi i nomi utente, le password e i cookie memorizzati nel database.
Abbiamo contattato gli sviluppatori di UC Browser per informarli del problema riscontrato, cercando di evidenziare la vulnerabilità e il suo pericolo, ma non hanno voluto discutere nulla con noi. Nel frattempo, il browser continuava a vantare la funzione pericolosa sotto gli occhi di tutti. Ma non appena abbiamo rivelato i dettagli della vulnerabilità, ignorarla come prima non era più possibile. Il 27 marzo è
stata rilasciata una nuova versione di UC Browser 12.10.9.1193, che si collegava al server tramite HTTPS: .
Inoltre, dopo la "correzione" e fino al momento della stesura dell'articolo, tentare di aprire un PDF nel browser portava a visualizzare un messaggio di errore con il testo "Oops, qualcosa è andato storto!". La richiesta al server quando tentavano di aprire un PDF non veniva eseguita, ma la richiesta al server veniva inviata all'avvio del browser, il che suggerisce che la possibilità di caricare codice eseguibile in violazione delle regole di Google Play era ancora attiva.
Fonte: habr.com
