Cercando vulnerabilità in UC Browser

Cercando vulnerabilità in UC Browser

Introduzione

Alla fine di marzo abbiamo segnalato, che abbiamo scoperto una vulnerabilità nascosta che consente il caricamento e l'esecuzione di codice non verificato in UC Browser. Oggi esamineremo in dettaglio come avviene questo caricamento e come gli hacker possono sfruttarlo per i propri scopi.

Tempo fa, UC Browser è stato pubblicizzato e diffuso in modo molto aggressivo: veniva installato sui dispositivi degli utenti tramite software dannoso, distribuito da vari siti spacciandosi per file video (cioè gli utenti pensavano di scaricare, ad esempio, un video porno, mentre ricevevano invece un APK con questo browser), utilizzavano banner inquietanti con messaggi che avvertivano che il browser era obsoleto, vulnerabile e simili. Nella pagina ufficiale di UC Browser su VK c'è un tema, dove gli utenti possono segnalare pubblicità ingannevoli, ci sono molti esempi. Nel 2016 è stata anche una pubblicità video in lingua russa (sì, pubblicità di un browser che blocca la pubblicità).

Al momento della scrittura di questo articolo, UC Browser ha superato le 500.000.000 installazioni su Google Play. È impressionante, poiché solo Google Chrome ha numeri superiori. Tra le recensioni ci sono molte lamentele riguardo alla pubblicità e ai reindirizzamenti verso alcune applicazioni su Google Play. Questo ci ha spinti a indagare: volevamo verificare se UC Browser stesse facendo qualcosa di scorretto. E, a quanto pare, sì che lo fa!

Nel codice dell'app è stata trovata la possibilità di caricare ed eseguire codice eseguibile, il che è in contraddizione con le regole per la pubblicazione delle app su Google Play. Oltre al fatto che UC Browser carica codice eseguibile, lo fa in modo non sicuro, il che può essere sfruttato per condurre un attacco MitM. Vediamo se riusciamo a realizzare un tale attacco.

Tutto ciò che segue è pertinente per la versione di UC Browser presente su Google Play al momento dell'indagine:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 del file APK: f5edb2243413c777172f6362876041eb0c3a928c

Vettore di attacco

Nel manifesto di UC Browser è possibile trovare un servizio con un nome esplicativo com.uc.deployment.UpgradeDeployService.

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

All'avvio di questo servizio, il browser esegue una richiesta POST a puds.ucweb.com/upgrade/index.xhtml, che può essere notato nel traffico dopo un certo periodo dall'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 tentiamo di aprire un PDF nel browser, viene effettuata una nuova richiesta all'indirizzo sopra indicato, dopo di che viene scaricata una libreria nativa. Per condurre l'attacco, abbiamo deciso di sfruttare questa caratteristica di UC Browser: la capacità di aprire PDF utilizzando una libreria nativa, che non è presente nell'APK e che scarica da Internet se necessario. Vale la pena notare che teoricamente UC Browser può essere costretto a scaricare qualcosa senza interazione da parte dell'utente – se viene fornita una risposta correttamente formata alla richiesta che viene eseguita all'avvio del browser. Tuttavia, è necessario studiare più in dettaglio il protocollo di interazione con il server, quindi abbiamo deciso che fosse più semplice modificare la risposta intercettata e sostituire la libreria per lavorare con i PDF.

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

Cercando vulnerabilità in UC Browser

Inizialmente c'è una richiesta POST a puds.ucweb.com/upgrade/index.xhtml, dopo di che
si scarica un archivio con la libreria per visualizzare PDF e formati per ufficio. È ragionevole supporre che nella prima richiesta venga inviata l'informazione sul sistema (almeno l'architettura, per restituire la libreria corretta), e in risposta a essa il browser riceve alcune informazioni sulla libreria da scaricare: l'indirizzo e, possibilmente, qualcos'altro. Il problema è che questa richiesta è crittografata.

Frazione della richiesta

Frazione della risposta

Cercando vulnerabilità in UC Browser

Cercando vulnerabilità in UC Browser

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

Cercando vulnerabilità in UC Browser

Ricerca del codice di decrittazione del traffico

Proveremo a decriptare la risposta del server. Controlliamo il codice della classe com.uc.deployment.UpgradeDeployService: dal metodo onStartCommand passiamo a com.uc.deployment.b.x, e da qui a com.uc.browser.core.d.c.f.e:

    pubblica finale 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("]flag_force:");
            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_apollo_child:");
            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("]versione_nome_componente:");
                    v5.append(((au)v4_1).aDA());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]versione_codice_componente:");
                    v5.append(((au)v4_1).gBl);
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]tipo_richiesta_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("os_ver", c.getRomInfo()));
            v3_2.add(g.fs("architettura_processore", com.uc.b.a.a.c.getCpuArch()));
            v3_2.add(g.fs("architettura_cpu", 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("versione_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("versione_bambino_plugin", arg9.iGX.iEx));
            v3_2.add(g.fs("serie_versione", arg9.iGX.iEw));
            v3_2.add(g.fs("ver_bambino", 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("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("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("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("totale_ram", 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("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. Notiamo la creazione di un array di 16 byte e il suo riempimento: 0x5F, 0, 0x1F, -50 (=0xCE). Corrisponde a quanto abbiamo visto nella richiesta sopra.

Nello stesso classe si può 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 controlla che il primo byte sia uguale a 0x60 oppure che il terzo byte sia uguale a 0xD0, e il secondo byte sia 1, 11 o 0x1F. Guardiamo la risposta del server: il primo byte è 0x60, il secondo è 0x1F e il terzo byte è 0x60. Sembra proprio che sia ciò di cui abbiamo bisogno. Dalle righe (ad esempio, "up_decrypt"), sembra che venga chiamato un metodo per decrittografare la risposta del server.
Passiamo al metodo g.j. Notiamo che come primo argomento si passa il byte con offset 2 (cioè 0x1F nel nostro caso), e come secondo argomento 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;
    }

È chiaro che qui avviene la selezione dell'algoritmo di decrittografia, e quel byte che nel nostro
caso è 0x1F indica uno dei tre possibili opzioni.

Continuiamo ad analizzare il codice. Dopo un paio di salti, arriviamo in un metodo con un nome a dir poco esplicativo decryptBytesByKey.

Qui dalla nostra risposta vengono estratti altri due byte, da cui si ottiene una stringa. È evidente che in questo modo viene scelto il segreto 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, va notato che in questa fase non si ottiene ancora la chiave, ma solo il suo «identificatore». Ottenere la chiave è un po' più complicato.

Nel metodo successivo, vengono aggiunti due parametri agli esistenti, portandoli a quattro: il numero magico 16, l'identificativo 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 interfaccia com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Nel codice principale dell'applicazione non ci sono classi che implementano questa interfaccia. Esiste una classe in questo file lib/armeabi-v7a/libsgmain.so, che in realtà non è .so, ma .jar. Il metodo di nostro interesse è implementato nel seguente modo:

package com.alibaba.wireless.security.a.i;
 
// ...
 
public class a implements IStaticDataEncryptComponent {
    private ISecurityGuardPlugin a;
// ...
    private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
        return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
    }
// ...
    private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
        return this.a(2, magicInt, 0, keyId, encrypted, magicString);
    }
// ...
    public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException {
        if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt  0) {
            return this.b(magicInt, keyId, encrypted, magicString);
        }
 
        throw new SecException("", 301);
    }
//...
}

Qui la nostra lista dei parametri è completata da altri 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 — evidentemente il numero del comando.

Dopo una serie di passaggi arriviamo a una classe che implementa l'interfaccia IRouterComponent e il metodo 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);
    }
}

E anche la classe JNICLibrary, in cui è dichiarato il metodo nativo doCommandNative:

package 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 doCommandNative. E qui inizia il divertimento.

Obfuscazione del codice macchina

Nel file libsgmain.so (che in realtà è .jar e in cui 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 un sacco di finestre di dialogo con errori. Il problema è che la tabella delle sezioni (section header table) non è valida. Questo è stato fatto apposta per complicare l'analisi.

Cercando vulnerabilità in UC Browser

Ma non è necessaria: per caricare e analizzare correttamente un file ELF, è sufficiente la tabella dei segmenti (program header table). Quindi semplicemente rimuoviamo la tabella delle sezioni, azzerando i campi corrispondenti nell'intestazione.

Cercando vulnerabilità in UC Browser

Riapriamo il file in IDA.

Ci sono due modi per indicare alla Java Virtual Machine dove si trova l'implementazione del metodo, dichiarato nel codice Java come native, nella libreria nativa. Il primo è fornire un nome del tipo Java_nome_pacchetto_NomeClasse_nomeMetodo.

Il secondo è registrarlo durante il caricamento della libreria (nella funzione JNI_OnLoad)
utilizzando la chiamata alla funzione RegisterNatives.

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

Tra le funzioni esportate non ce n'è nessuna con questo nome, quindi bisogna cercare la chiamata RegisterNatives.
Andiamo alla funzione JNI_OnLoad e vediamo questa situazione:

Cercando vulnerabilità in UC Browser

Cosa sta succedendo qui? A prima vista, l'inizio e la fine della funzione sembrano tipici per l'architettura ARM. La prima istruzione salva nello stack il contenuto dei registri che la funzione utilizzerà nel suo lavoro (in questo caso R0, R1 e R2), così come il contenuto del registro LR, nel quale si trova l'indirizzo di ritorno dalla funzione. L'ultima istruzione ripristina i registri salvati, e l'indirizzo di ritorno viene subito posizionato nel registro PC — in questo modo avviene il ritorno dalla funzione. Ma se osserviamo attentamente, possiamo notare che la penultima istruzione modifica l'indirizzo di ritorno salvato nello stack. Calcoliamo quale sarà dopo
l'esecuzione del codice. In R1 viene caricato un certo indirizzo 0xB130, da cui si sottrae 5, poi viene trasferito in R0 e a questo si aggiunge 0x10. Si ottiene 0xB13B. In questo modo, IDA pensa che nell'ultima istruzione ci sia un normale ritorno dalla funzione, ma in realtà avviene 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 di questa libreria è stato aggiunto un simile 'adattatore' e
codice spazzatura. Non ci soffermeremo troppo su di essi; basta ricordare che
l'inizio effettivo di quasi tutte le funzioni si trova un po' più in là.

Poiché nel codice non c'è un passaggio esplicito a 0xB13A, IDA non ha riconosciuto che in questo punto ci sia codice. Per lo stesso motivo, non riconosce gran parte del codice nella libreria come codice, il che rende l'analisi un po' più complicata. Diciamo a IDA che qui c'è codice, e ecco cosa succede:

Cercando vulnerabilità in UC Browser

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

Cercando vulnerabilità in UC Browser

Quando chiamiamo questa funzione, nel registro LR otteniamo l'indirizzo della tabella menzionata precedentemente (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 dobbiamo andare. Proviamo a calcolarlo: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Andiamo all'indirizzo ottenuto e vediamo letteralmente un paio di istruzioni utili e di nuovo un passaggio a 0xB140:

Cercando vulnerabilità in UC Browser

Adesso ci sarà un passaggio con uno spostamento con l'indice 0x20 dalla tabella.

Dalla dimensione della tabella, ci saranno molte di queste transizioni nel codice. Si pone quindi la questione: possiamo affrontare questa situazione in modo più automatizzato, senza dover calcolare manualmente gli indirizzi? E qui 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 "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"

Posizioniamo il cursore sulla riga 0xB26A, avviamo lo script e vediamo un salto a 0xB4B0:

Cercando vulnerabilità in UC Browser

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

Cercando vulnerabilità in UC Browser

Le istruzioni dopo BLX sembrano poco sensate, sembrano più una sorta di offset. Guardiamo in sub_4964:

Cercando vulnerabilità in UC Browser

E in effetti, qui si prende un dword dall'indirizzo che si trova in LR, si aggiunge a questo indirizzo, dopodiché si prende il valore dall'indirizzo risultante e si mette nello stack. Inoltre, a LR si aggiunge 4 per saltare quest'offset dopo il ritorno dalla funzione. Dopo, 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 che assomiglia a una tabella degli indirizzi:

Cercando vulnerabilità in UC Browser

Per patchare questa costruzione, sarà necessario ottenere due parametri dal codice: l'offset e il numero del registro in cui inserire il risultato. Per ogni possibile registro sarà necessario preparare in anticipo un frammento 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 "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 costruzione che vogliamo sostituire — 0xB4B2 — e avviamo lo script:

Cercando vulnerabilità in UC Browser

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

Cercando vulnerabilità in UC Browser

Come nel caso precedente, dopo l'istruzione BLX c'è uno spostamento:

Cercando vulnerabilità in UC Browser

Prendiamo lo spostamento dall'indirizzo in LR, lo sommiamo a LR e andiamo lì. 0x72044 + 0xC = 0x72050. Lo script per questa costruzione è piuttosto 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 "Unable to detect first instruction"

Risultato dell'esecuzione dello script:

Cercando vulnerabilità in UC Browser

Dopo aver patchato tutto nella funzione, possiamo indicare a IDA il suo effettivo inizio. Essa raccoglierà tutto il codice della funzione a pezzi, e potrà essere decompilato usando HexRays.

Decrittazione delle stringhe

Abbiamo imparato a combattere l'offuscamento del codice macchina nella libreria libsgmainso-6.4.36.so da UC Browser e abbiamo ricevuto 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ù da vicino le seguenti righe:

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

Nella funzione sub_73E24 si verifica chiaramente la decrittazione del nome della classe. Come parametri di questa funzione viene passato un puntatore a dati simili a quelli crittografati, un certo buffer e un numero. È ovvio che dopo la chiamata della funzione nel buffer ci sarà la stringa decrittografata, poiché viene passata alla funzione. FindClass, che accetta come secondo parametro il nome della classe. Quindi, il numero è la dimensione del buffer o la lunghezza della stringa. Proviamo a decrittare il nome della classe, questo dovrebbe indicarci se stiamo andando nella direzione giusta. Esaminiamo più nel dettaglio cosa avviene 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 contenitore per array di byte della dimensione specificata (non ci soffermeremo su questi contenitori). Qui vengono creati due di questi contenitori: una stringa è inserita in uno «DcO/lcK+h?m3c*q@» (non è difficile indovinare che questa è la chiave), e i dati crittografati sono inseriti nell'altro. Successivamente, entrambi gli oggetti vengono messi in una struttura che viene passata alla funzione sub_6115CIn questa struttura, notiamo anche un campo con il valore di 3. Vediamo cosa succede a questa struttura in seguito.

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 che in precedenza è stato impostato su 3. Rivediamo il case 3: nella funzione sub_6364C vengono passati i parametri dalla struttura, che erano stati inseriti nella funzione precedente, ossia la chiave e i dati crittografati. Se guardiamo bene a sub_6364C, possiamo identificare l'algoritmo RC4.

Abbiamo l'algoritmo e la chiave. Proviamo a decrittografare il nome della classe. Ecco cosa è uscito: com/taobao/wireless/security/adapter/JNICLibrary. Ottimo! Siamo sulla strada giusta.

Albero dei comandi

Adesso dobbiamo trovare la chiamata RegisterNatives, che ci indicherà la funzione doCommandNative. Esaminiamo le funzioni richiamate 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 infatti, qui viene registrato il 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;
}

Dalla denominazione si può dedurre che qui si trova il punto d'ingresso di tutte le funzioni che gli sviluppatori hanno deciso di trasferire nella libreria nativa. Siamo interessati alla funzione con il numero 10601.

Dal codice è possibile vedere che dal numero del comando si ottengono 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 a JNIEnv e gli argomenti passati alla funzione, vengono accumulati in una struttura e passati oltre. Con i tre numeri ottenuti (designiamoli N1, N2 e N3) si costruisce un albero di comandi.

Circa in questo modo:

Cercando vulnerabilità in UC Browser

L'albero viene riempito dinamicamente in JNI_OnLoad.
Tre numeri codificano un percorso nell'albero. Ogni foglia dell'albero contiene l'indirizzo decifrato della funzione corrispondente. La chiave si trova nel nodo genitore. Trova il punto nel codice dove viene aggiunta la funzione di nostro interesse non è un'impresa difficile, se si comprendono tutte le strutture utilizzate (non forniamo la descrizione per non ingrandire ulteriormente un articolo già piuttosto lungo).

Ulteriori dettagli sull'offuscamento

Abbiamo ottenuto l'indirizzo della funzione che dovrebbe decifrare il traffico: 0x5F1AC. Ma non è ancora il momento di gioire: gli sviluppatori di UC Browser ci hanno riservato un altro colpo di scena.

Dopo aver ottenuto i parametri dall'array generato nel codice Java, ci troviamo
nella funzione all'indirizzo 0x4D070. E qui ci aspetta un'altra forma di offuscamento del codice.

Mettiamo in R7 e R4 due indici:

Cercando vulnerabilità in UC Browser

Spostiamo il primo indice in R11:

Cercando vulnerabilità in UC Browser

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

Cercando vulnerabilità in UC Browser

Dopo il passaggio al primo indirizzo, si utilizza il secondo indice, che si trova in R4. La tabella contiene 230 elementi.

Cosa fare con questo? Possiamo dire a IDA che si tratta di un switch: Edit -> Other -> Specify switch idiom.

Cercando vulnerabilità in UC Browser

Il codice risultante è spaventoso. Ma, attraversando le sue complessità, si può notare la chiamata della funzione già conosciuta. sub_6115C:

Cercando vulnerabilità in UC Browser

C'era uno switch, nel quale nel case 3 si trovava la decodifica usando l'algoritmo RC4. In questo caso, la struttura passata alla funzione è riempita con i parametri forniti in doCommandNative. Ricordiamo che avevamo magicInt con valore 16. Guardiamo il case corrispondente – e dopo alcuni passaggi troviamo il codice, tramite il quale possiamo identificare l'algoritmo.

Cercando vulnerabilità in UC Browser

È AES!

L'algoritmo è identificato, ora bisogna ottenere i suoi parametri: modalità, chiave e, possibilmente, il vettore di inizializzazione (la sua presenza dipende dalla modalità di funzionamento dell'algoritmo AES). La struttura con questi parametri deve essere formata da qualche parte prima della chiamata della funzione sub_6115C, ma questa parte del codice è particolarmente offuscata, quindi viene l'idea di patchare il codice, in modo che tutti i parametri della funzione di decodifica vengano dumpati in un file.

Patch

Per non scrivere manualmente tutto il codice della patch in linguaggio Assembly, possiamo avviare Android Studio, scrivere una funzione che accetta gli stessi parametri della nostra funzione di decodifica e scrive in un file, dopodiché possiamo copiare e incollare il codice che genererà il compilatore.

I nostri amici del team UC Browser si sono anche "occupati" 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' angusto. Ho dovuto dividerlo in parti e utilizzare i blocchi spazzatura delle funzioni vicine. In totale sono risultate quattro parti.

Prima parte:

Cercando vulnerabilità in UC Browser

Nell'architettura ARM, i primi quattro parametri di una funzione vengono passati tramite i registri R0-R3, gli altri, se presenti, tramite lo stack. L'indirizzo di ritorno viene passato nel registro LR. Tutto ciò deve essere salvato affinché la funzione possa elaborare i parametri dopo che li abbiamo dumpati. È necessario anche salvare tutti i registri che utilizzeremo nel processo, quindi facciamo PUSH.W {R0-R10,LR}. In R7 otteniamo l'indirizzo della lista di parametri passati alla funzione tramite stack.

Utilizzando la funzione fopen apriamo il file /data/local/tmp/aes in modalità "ab",
cioè, per l'aggiunta. In R0 carichiamo l'indirizzo del nome del file, in R1 l'indirizzo della stringa che indica la modalità. E qui il codice spazzatura finisce, quindi passiamo alla funzione successiva. Per farla continuare a funzionare, impostiamo all'inizio il salto al codice reale della funzione bypassando lo spazzatura, e invece dello spazzatura aggiungiamo la continuazione della patch.

Cercando vulnerabilità in UC Browser

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.

Cercando 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.

Cercando vulnerabilità in UC Browser

Alla fine chiudiamo il file, ripristiniamo i registri e trasferiamo il controllo alla vera funzione. aes.

Compiliamo l'APK con la libreria patchata, firmiamo, carichiamo sul dispositivo/emulatore e avviamo. Vediamo che il nostro dump viene creato e che vengono scritti molti dati. Il browser utilizza la crittografia non solo per il traffico, e tutta la crittografia passa attraverso la funzione in esame. Tuttavia, i dati necessari mancano e non si vede la richiesta giusta nel traffico. Per non aspettare che UC Browser si decida a fare la richiesta necessaria, prenderemo la risposta crittografata dal server ottenuta in precedenza e ripatcheremo di nuovo l'applicazione: aggiungeremo la decrittazione 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;)I

Compiliamo, firmiamo, installiamo e avviamo. Riceviamo un NullPointerException, poiché il metodo ha restituito null.

Durante un'analisi più approfondita del codice è stata trovata una funzione che decifra delle stringhe interessanti: «META-INF/» e «.RSA». Sembra che l'app controlli il proprio certificato. O addirittura generi chiavi da esso. Non vogliamo approfondire cosa succede con il certificato, quindi inseriremo semplicemente il certificato corretto. Modificheremo la stringa cifrata in modo che al posto di «META-INF/» ci sia «BLABLINF/», creeremo una cartella con quel nome nell'APK e ci metteremo dentro il certificato del browser.

Compiliamo, firmiamo, installiamo, avviamo. Bingo! Abbiamo la chiave!

MitM

Abbiamo ottenuto la chiave e il vettore di inizializzazione, uguale alla chiave. Proviamo a decifrare la risposta del server in modalità CBC.

Cercando vulnerabilità in UC Browser

Vediamo l'URL dell'archivio, qualcosa di simile a MD5, «extract_unzipsize» e un numero. Controlliamo: l'MD5 dell'archivio corrisponde, la dimensione della libreria estratta corrisponde. Proviamo a modificare questa libreria e a darla 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: puds.ucweb.com/upgrade/index.xhtml e per il download dell'archivio. Nella prima sostituiamo MD5 (la dimensione dopo l'estrazione rimane invariata), nella seconda forniamo l'archivio con la libreria patchata.

Il browser tenta più volte di scaricare l'archivio, dopo di che restituisce un errore. A quanto pare, qualcosa
non va. Dall'analisi di questo formato ambiguo è emerso che il server trasmette anche la dimensione dell'archivio:

Cercando vulnerabilità in UC Browser

È codificata 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 errato e dopo alcuni tentativi ha restituito un errore.

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

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

Le conseguenze e la reazione dello sviluppatore

Allo stesso modo, i hacker potrebbero utilizzare la funzione non sicura di UC Browser per distribuire e avviare librerie malevole. Queste librerie funzioneranno nel contesto del browser, ottenendo quindi tutti i permessi di sistema. Di conseguenza, sarà possibile visualizzare finestre di phishing e accedere ai file di lavoro della Nocciola Cinese Arancione, inclusi i logins, le password e i cookie memorizzati nel database.

Abbiamo contattato gli sviluppatori di UC Browser per informarli del problema riscontrato, cercando di far notare la vulnerabilità e il suo pericolo, ma non hanno voluto discutere nulla con noi. Nel frattempo, il browser continuava a vantare una funzione pericolosa alla vista di tutti. Tuttavia, non appena abbiamo rivelato i dettagli della vulnerabilità, non era più possibile ignorare la situazione come prima. Il 27 marzo è stata
rilasciata una nuova versione di UC Browser 12.10.9.1193, che si connette al server tramite HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Inoltre, dopo la "correzione" e fino al momento della scrittura dell'articolo, tentare di aprire un PDF nel browser portava a un messaggio di errore con il testo "Ops, qualcosa è andato storto!". La richiesta al server durante il tentativo di aprire il PDF non veniva eseguita, ma la richiesta al momento dell'avvio del browser sì, il che suggerisce la possibilità di caricare codice eseguibile in violazione delle regole di Google Play.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster