Procurando vulnerabilidades no UC Browser

Procurando vulnerabilidades no UC Browser

Introdução

No final de março, nós informamos, que encontramos uma vulnerabilidade oculta para carregar e executar código não verificado no UC Browser. Hoje, vamos analisar detalhadamente como esse carregamento acontece e como os hackers podem usá-lo em seus objetivos.

Há algum tempo, o UC Browser foi promovido e distribuído de forma muito agressiva: ele era instalado nos dispositivos dos usuários através de softwares maliciosos, distribuído em vários sites sob a forma de arquivos de vídeo (ou seja, os usuários pensavam que estavam baixando, por exemplo, um vídeo pornográfico, mas recebiam em vez disso um APK desse navegador), utilizavam banners assustadores com mensagens de que o navegador estava obsoleto, vulnerável e coisas do tipo. No grupo oficial do UC Browser no VK, há tema, onde os usuários podem reclamar sobre publicidade enganosa, com muitos exemplos. Em 2016, houve até uma publicidade em vídeo em russo (sim, a propaganda de um navegador que bloqueia anúncios).

No momento em que este artigo foi escrito, o UC Browser já tinha mais de 500.000.000 instalações no Google Play. Isso é impressionante — só o Google Chrome superou isso. Entre as avaliações, podemos ver várias reclamações sobre anúncios e redirecionamentos para algum aplicativo no Google Play. Isso foi o que nos motivou a investigar: decidimos verificar se o UC Browser não estava fazendo algo errado. E constatamos que estava!

No código do aplicativo, foi descoberta uma vulnerabilidade para carregar e executar código executável, o que contraria as regras de publicação de aplicativos no Google Play. Além de carregar código executável, o UC Browser faz isso de maneira insegura, o que pode ser utilizado para realizar um ataque MitM. Vamos ver se conseguimos realizar tal ataque.

Tudo que é escrito a seguir é relevante para a versão do UC Browser que estava presente no Google Play no momento da pesquisa:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 do arquivo APK: f5edb2243413c777172f6362876041eb0c3a928c

Vetor de ataque

No manifesto do UC Browser, pode-se encontrar um serviço com um nome sugestivo com.uc.deployment.UpgradeDeployService.

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

Ao ativar esse serviço, o navegador realiza uma solicitação POST para puds.ucweb.com/upgrade/index.xhtml, que pode ser observado no tráfego algum tempo após o início. Em resposta, ele pode receber o comando para baixar alguma atualização ou novo módulo. Durante a análise, o servidor não enviou tais comandos, mas notamos que ao tentar abrir um PDF no navegador, ele faz uma nova solicitação para o endereço mencionado acima, após o qual baixa a biblioteca nativa. Para realizar o ataque, decidimos utilizar esta característica do UC Browser: a capacidade de abrir PDFs com a biblioteca nativa, que não está no APK e que ele baixa da Internet quando necessário. Vale a pena mencionar que, teoricamente, é possível fazer o UC Browser baixar algo sem interação do usuário - se uma resposta corretamente formatada for enviada à solicitação que é feita após o lançamento do navegador. No entanto, isso requer um estudo mais detalhado do protocolo de interação com o servidor, então decidimos que seria mais simples editar a resposta interceptada e substituir a biblioteca usada para PDFs.

Assim, quando o usuário deseja abrir um PDF diretamente no navegador, o tráfego pode mostrar as seguintes solicitações:

Procurando vulnerabilidades no UC Browser

Primeiro, uma solicitação POST é enviada para puds.ucweb.com/upgrade/index.xhtml, após a qual
é feito o download de um arquivo comprimido com a biblioteca para a visualização de PDFs e formatos de escritório. É lógico supor que a primeira solicitação transmite informações sobre o sistema (pelo menos, a arquitetura, para fornecer a biblioteca correta), e em resposta a isso, o navegador recebe algumas informações sobre a biblioteca que precisa ser baixada: o endereço e, possivelmente, mais alguma coisa. O problema é que essa solicitação é criptografada.

Fragmento da solicitação

Fragmento da resposta

Procurando vulnerabilidades no UC Browser

Procurando vulnerabilidades no UC Browser

A própria biblioteca está empacotada em ZIP e não está criptografada.

Procurando vulnerabilidades no UC Browser

Busca pelo código de descriptografia do tráfego

Vamos tentar descriptografar a resposta do servidor. Observamos o código da classe com.uc.deployment.UpgradeDeployService: do método onStartCommand vamos para com.uc.deployment.b.x, e dele para 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("]produto:");
            v4.append(arg9.iGX.ipR);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]versão:");
            v4.append(arg9.iGX.iEn);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tipo_de_atualização:");
            v4.append(arg9.iGX.mMode);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]forçar_flag:");
            v4.append(arg9.iGX.iEo);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]modo_silencioso:");
            v4.append(arg9.iGX.iDQ);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tipo_silencioso:");
            v4.append(arg9.iGX.iEr);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]estado_silencioso:");
            v4.append(arg9.iGX.iEp);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]arquivo_silencioso:");
            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_de_download:");
            v4.append(arg9.mDownloadType);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]grupo_de_download:");
            v4.append(arg9.mDownloadGroup);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]caminho_de_download:");
            v4.append(arg9.iGH);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]versão_filha_apollo:");
            v4.append(arg9.iGX.iEx);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]série_apollo:");
            v4.append(arg9.iGX.iEw);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]arquitetura_cpu_apollo:");
            v4.append(arg9.iGX.iEt);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]vfp3_cpu_apollo:");
            v4.append(arg9.iGX.iEv);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]vfp_cpu_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_do_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("]código_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("os_ver", c.getRomInfo()));
            v3_2.add(g.fs("arquitetura_processador", com.uc.b.a.a.c.getCpuArch()));
            v3_2.add(g.fs("arquitetura_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_de_rede", String.valueOf(com.uc.base.system.a.Jo())));
            v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
            v3_2.add(g.fs("versão_do_plugin", arg9.iGX.iEn));
            v3_2.add(g.fs("lang_alvo", arg9.iGX.iEs));
            v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
            v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
            v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
            v3_2.add(g.fs("versão_filha_plugin", arg9.iGX.iEx));
            v3_2.add(g.fs("série_ver", arg9.iGX.iEw));
            v3_2.add(g.fs("ver_filha", r.aVw()));
            v3_2.add(g.fs("md5_cur_ver", arg9.iGX.iEl));
            v3_2.add(g.fs("assinatura_cur_ver", SystemHelper.getUCMSignature()));
            v3_2.add(g.fs("log_de_atualização", i.bjt()));
            v3_2.add(g.fs("instalação_silenciosa", String.valueOf(arg9.iGX.iDQ)));
            v3_2.add(g.fs("estado_silencioso", String.valueOf(arg9.iGX.iEp)));
            v3_2.add(g.fs("arquivo_silencioso", arg9.iGX.iEq));
            v3_2.add(g.fs("tipo_silencioso", String.valueOf(arg9.iGX.iEr)));
            v3_2.add(g.fs("arquitetura_cpu_titulo", 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("núcleos_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("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
            c.aBh();
            v3_2.add(g.fs("rom_1", c.getRomInfo()));
            v4_5 = e.getScreenWidth();
            int v6 = e.getScreenHeight();
            StringBuilder v7 = new StringBuilder();
            v7.append(v4_5);
            v7.append("*");
            v7.append(v6);
            v3_2.add(g.fs("ss", v7.toString()));
            v3_2.add(g.fs("nível_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);
    }

Aqui vemos a formação da requisição POST. Observamos a criação de um array de 16 bytes e seu preenchimento: 0x5F, 0, 0x1F, -50 (=0xCE). Coincide com o que vimos na requisição acima.

Neste mesmo classe, podemos notar uma classe aninhada, na qual há outro método 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");
        }
    }

O método recebe um array de bytes e verifica se o byte zero é igual a 0x60 ou se o byte três é igual a 0xD0, e se o byte dois é 1, 11 ou 0x1F. Vejamos a resposta do servidor: o byte zero é 0x60, o segundo é 0x1F, o terceiro é 0x60. Parece que é isso que precisamos. De acordo com as linhas ("up_decrypt", por exemplo), deve ser chamado um método que decifre a resposta do servidor.
Vamos ao método g.j. Notamos que como primeiro argumento um byte no deslocamento 2 (ou seja, 0x1F no nosso caso) é passado, e como segundo, a resposta do servidor sem
os primeiros 16 bytes.

     public static byte[] j(int arg1, byte[] arg2) {
        if(arg1 == 1) {
            arg2 = c.c(arg2, c.adu);
        }
        else if(arg1 == 11) {
            arg2 = m.aF(arg2);
        }
        else if(arg1 != 0x1F) {
        }
        else {
            arg2 = EncryptHelper.decrypt(arg2);
        }
        return arg2;
    }

Obviamente, aqui ocorre a escolha do algoritmo de decodificação, e aquele byte que está no nosso
O caso é igual a 0x1F, que representa uma das três opções possíveis.

Continuamos a análise do código. Após alguns saltos, chegamos a um método com um nome sugestivo. decryptBytesByKey.

Aqui, dois bytes são separados da nossa resposta, e deles é gerada uma string. É evidente que, dessa forma, é escolhido uma chave para decifrar a mensagem.

    private static byte[] decryptBytesByKey(byte[] bytes) {
        byte[] v0 = null;
        if(bytes != null) {
            try {
                if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
                }
                else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
                    return v0;
                }
                else {
                    byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE];  // 2 bytes
                    System.arraycopy(bytes, 0, prefix, 0, prefix.length);
                    String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Escolha da chave
                    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;
    }

Para antecipar, notamos que neste estágio não é a chave que obtemos, mas apenas seu “identificador”. Conseguir a chave é um pouco mais complicado.

No próximo método, dois parâmetros adicionais são incluídos, totalizando quatro: o número mágico 16, o identificador da chave, os dados criptografados e uma string incompreensível (no nosso caso, vazia).

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

Após uma série de transições, chegamos ao método staticBinarySafeDecryptNoB64 interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Na parte principal do código do aplicativo, não há classes que implementem essa interface. Tal classe existe no arquivo lib/armeabi-v7a/libsgmain.so, que na verdade não é um .so, mas um .jar. O método que nos interessa é implementado da seguinte forma:

pacote com.alibaba.wireless.security.a.i;
 
// ...
 
pública classe a implementa IStaticDataEncryptComponent {
    privado ISecurityGuardPlugin a;
// ...
    privado byte[] a(int modo, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
        return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(modo), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
    }
// ...
    privado byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
        return this.a(2, magicInt, 0, keyId, encrypted, magicString);
    }
// ...
    pública byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) lança SecException {
        if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt  0) {
            return this.b(magicInt, keyId, encrypted, magicString);
        }
 
        throw new SecException("", 301);
    }
//...
}

Aqui nossa lista de parâmetros é complementada por mais dois números inteiros: 2 e 0. Ao que parece,
2 significa decriptação, como no método doFinal da classe do sistema javax.crypto.Cipher. E tudo isso é passado para um Router com o número 10601 — este, presumivelmente, é o número do comando.

Após uma nova cadeia de transições, encontramos a classe que implementa a interface IRouterComponent e o método doCommand:

pacote com.alibaba.wireless.security.mainplugin;

import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
 
pública classe a implementa IRouterComponent {
    pública a() {
        super();
    }
    pública Object doCommand(int arg2, Object[] arg3) {
        return JNICLibrary.doCommandNative(arg2, arg3);
    }
}

Além disso, a classe JNICLibrary, onde o método nativo é declarado doCommandNative:

pacote com.taobao.wireless.security.adapter;
 
pública classe JNICLibrary {
    public static native Object doCommandNative(int arg0, Object[] arg1);
}

Portanto, precisamos encontrar o método no código nativo doCommandNative. E aqui começa a parte divertida.

Ofuscação do código máquina

No arquivo libsgmain.so (que na verdade é .jar e onde encontramos anteriormente a implementação de algumas interfaces relacionadas à criptografia) há uma biblioteca nativa: libsgmainso-6.4.36.so. Abrimos no IDA e obtemos uma série de janelas de erro. O problema é que a tabela de seções (section header table) é inválida. Isso foi feito propositalmente para dificultar a análise.

Procurando vulnerabilidades no UC Browser

Mas ela não é necessária: para carregar corretamente um arquivo ELF e analisá-lo, é suficiente a tabela de segmentos (program header table). Portanto, basta remover a tabela de seções, zerando os campos correspondentes no cabeçalho.

Procurando vulnerabilidades no UC Browser

Abrimos novamente o arquivo no IDA.

Existem duas maneiras de informar à máquina virtual Java onde exatamente na biblioteca nativa está a implementação do método, declarado no código Java como nativo. A primeira é dar um nome no formato Java_nome_pacote_NomeClasse_nomeMetodo.

O segundo — registrá-lo ao carregar a biblioteca (na função JNI_OnLoad)
usando a chamada da função RegisterNatives.

No nosso caso, se usarmos o primeiro método, o nome deve ser assim: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Entre as funções exportadas não há uma assim, então precisamos procurar pela chamada RegisterNatives.
Vamos para a função JNI_OnLoad e vemos uma cena assim:

Procurando vulnerabilidades no UC Browser

O que está acontecendo aqui? À primeira vista, o início e o fim da função são típicos da arquitetura ARM. A primeira instrução salva no stack o conteúdo dos registradores que a função usará (neste caso, R0, R1 e R2), assim como o conteúdo do registrador LR, que contém o endereço de retorno da função. A última instrução restaura os registradores salvos, com o endereço de retorno sendo automaticamente colocado no registrador PC — assim ocorre o retorno da função. Mas se olharmos mais de perto, podemos notar que a penúltima instrução altera o endereço de retorno que foi salvo na pilha. Vamos calcular qual será depois
da execução do código. Um certo endereço 0xB130 é carregado em R1, dele é subtraído 5, depois é transferido para R0 e somamos 0x10. O resultado é 0xB13B. Assim, a IDA pensa que na última instrução ocorre um retorno normal da função, mas na verdade está ocorrendo uma transição para o endereço calculado 0xB13B.

Vale lembrar que os processadores ARM têm dois modos e dois conjuntos de instruções: ARM e Thumb. O bit menos significativo do endereço informa ao processador qual conjunto de instruções está sendo usado. Ou seja, o endereço é na verdade 0xB13A, e a unidade no bit menos significativo indica o modo Thumb.

No início de cada função nesta biblioteca foi adicionado um tipo de "adaptador" e
código lixo. Não vamos nos detiver em detalhes — apenas lembre-se,
que o verdadeiro início da maioria das funções está um pouco mais adiante.

Como no código não há uma transição explícita para 0xB13A, a IDA não reconheceu que há código neste ponto. Por este mesmo motivo, a maior parte do código na biblioteca não é identificada como código, o que dificulta um pouco a análise. Informamos à IDA que aqui há código, e veja o que acontece:

Procurando vulnerabilidades no UC Browser

Em 0xB144 claramente começa uma tabela. E o que há em sub_494C?

Procurando vulnerabilidades no UC Browser

Ao chamar esta função, no registrador LR obteremos o endereço da tabela mencionada anteriormente (0xB144). Em R0 — o índice nessa tabela. Ou seja, um valor é retirado da tabela, somado a LR, e obtemos
endereço para o qual você precisa ir. Vamos calculá-lo: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Vamos para o endereço obtido e vemos literalmente algumas instruções úteis e novamente um salto para 0xB140:

Procurando vulnerabilidades no UC Browser

Agora haverá um salto pelo deslocamento com o índice 0x20 da tabela.

Pela dimensão da tabela, muitos desses saltos aparecerão no código. Surge a pergunta, podemos lidar com isso de uma forma mais automatizada, sem cálculo manual de endereços. E os scripts e a possibilidade de patching de código no IDA vêm em nosso auxílio:

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 "índice =", 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 de operando errado em", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
            table = None
        if table is None:
            print "Incapaz de encontrar a tabela"
        else:
            print "tabela =", hex(table)
            offset = get_wide_dword(table + (index << 2))
            put_unconditional_branch(ea, table + offset)
    else:
        print "Código desconhecido", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
    print "Incapaz de detectar a primeira instrução"

Colocamos o cursor na linha 0xB26A, executamos o script e vemos um salto para 0xB4B0:

Procurando vulnerabilidades no UC Browser

IDA novamente não reconheceu esta parte como código. Ajudamos e vemos ali outra construção:

Procurando vulnerabilidades no UC Browser

As instruções após BLX parecem não muito significativas, isso mais se assemelha a algum deslocamento. Vamos olhar em sub_4964:

Procurando vulnerabilidades no UC Browser

E de fato, aqui um dword é retirado do endereço armazenado em LR, somado a este endereço, depois o valor do endereço obtido é colocado na pilha. Além disso, 4 é adicionado ao LR, para que após o retorno da função, esse deslocamento seja ignorado. Depois disso, o comando POP {R1} retira o valor obtido da pilha. Se olharmos o que está no endereço 0xB4BA + 0xEA = 0xB5A4, podemos ver algo semelhante a uma tabela de endereços:

Procurando vulnerabilidades no UC Browser

Para aplicar esse patch, será necessário obter dois parâmetros do código: o deslocamento e o número do registrador no qual o resultado deve ser armazenado. Para cada registrador possível, será preciso preparar antecipadamente um trecho de código.

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 "Instrução POP não encontrada"
    else:
        print "Tipo de operando incorreto em +4:", get_operand_type(ea + 4, 0)
else:
    print "Não foi possível detectar as primeiras instruções"

Coloque o cursor no início da construção que deseja substituir — 0xB4B2 — e inicie o script:

Procurando vulnerabilidades no UC Browser

Além das construções já mencionadas, também podem aparecer estas no código:

Procurando vulnerabilidades no UC Browser

Assim como no caso anterior, após a instrução BLX, há um deslocamento:

Procurando vulnerabilidades no UC Browser

Pegamos o deslocamento a partir do endereço em LR, somamos a ele e seguimos para lá. 0x72044 + 0xC = 0x72050. O script para esta construção é bem simples:

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 "Não foi possível detectar a primeira instrução"

Resultado da execução do script:

Procurando vulnerabilidades no UC Browser

Depois que a função estiver totalmente ajustada, você pode indicar ao IDA o seu verdadeiro início. Ela coletará todo o código da função em pedaços, e poderá ser decompilada com o HexRays.

Descriptografia de strings

Aprendemos a combater a ofuscação de código de máquina na biblioteca libsgmainso-6.4.36.so do UC Browser e obtivemos o código da função 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;
}

Vamos examinar mais de perto as seguintes linhas:

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

Na função sub_73E24 é claramente feita a descriptografia do nome da classe. Como parâmetros, essa função recebe um ponteiro para dados que parecem estar criptografados, um buffer e um número. Obviamente, após a chamada da função, o buffer conterá a string descriptografada, uma vez que é passado para a função FindClass, que aceita como segundo parâmetro o nome da classe. Portanto, o número é o tamanho do buffer ou o comprimento da string. Vamos tentar descriptografar o nome da classe; isso deve nos indicar se estamos indo na direção certa. Vamos considerar em mais detalhes o que ocorre em 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;
}

Função sub_7AF78 cria uma instância de contêiner para arrays de bytes de tamanho especificado (não vamos nos aprofundar nesses contêineres). Aqui são criados dois desses contêineres: uma string é armazenada em um deles. «DcO/lcK+h?m3c*q@» (não é difícil perceber que esta é a chave), e os dados criptografados são armazenados no outro. Depois, ambos os objetos são colocados em uma determinada estrutura que é passada para a função sub_6115C. Também devemos notar que neste estrutura, há um campo com o valor 3. Vamos ver o que acontece com essa estrutura em seguida.

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

O parâmetro switch recebe um campo da estrutura que anteriormente foi atribuído o valor 3. Vamos ver o case 3: na função sub_6364C são passados os parâmetros da estrutura, que foram armazenados lá na função anterior, ou seja, a chave e os dados criptografados. Se olharmos de perto para sub_6364C, podemos descobrir o algoritmo RC4 nele.

Temos o algoritmo e a chave. Vamos tentar decifrar o nome da classe. Aqui está o que conseguimos: com/tb/wireless/security/adapter/JNICLibrary. Excelente! Estamos no caminho certo.

Árvore de comandos

Agora precisamos encontrar a chamada RegisterNatives, que nos apontará para a função doCommandNative. Vamos revisar as funções chamadas de JNI_OnLoad, e encontramos na 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 de fato, aqui está registrado um método nativo com o nome doCommandNative. Agora sabemos seu endereço. Vamos ver o que ele faz.

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

Pelo nome, podemos deduzir que aqui está o ponto de entrada de todas as funções que os desenvolvedores decidiram transferir para a biblioteca nativa. Estamos interessados na função de número 10601.

Pelo código, podemos ver que do número do comando obtemos três números: command / 10000, command % 10000 / 100 e command % 10, ou seja, no nosso caso, 1, 6 e 1. Esses três números, bem como um ponteiro para JNIEnv e os argumentos passados para a função, são agrupados em uma estrutura e enviados adiante. Usando os três números obtidos (vamos chamá-los de N1, N2 e N3) construímos a árvore de comandos.

Mais ou menos assim:

Procurando vulnerabilidades no UC Browser

A árvore é preenchida dinamicamente em JNI_OnLoad.
Três números codificam o caminho na árvore. Cada folha da árvore contém o endereço da função correspondente. A chave está no nó pai. Encontrar o lugar no código onde a função desejada é adicionada à árvore não é uma tarefa difícil, se você entender todas as estruturas utilizadas (não apresentamos sua descrição para não alongar ainda mais o artigo já considerável).

Mais sobre ofuscação

Recebemos o endereço de uma função que deve decifrar o tráfego: 0x5F1AC. Mas ainda não é hora de comemorar: os desenvolvedores do UC Browser nos prepararam mais uma surpresa.

Após obter os parâmetros do array que foi formado no código Java, chegamos
à função no endereço 0x4D070. E aqui nos espera mais um tipo de ofuscação de código.

Colocamos em R7 e R4 dois índices:

Procurando vulnerabilidades no UC Browser

Transferimos o primeiro índice para R11:

Procurando vulnerabilidades no UC Browser

Para obter o endereço da tabela, usamos o índice:

Procurando vulnerabilidades no UC Browser

Após a transição para o primeiro endereço, usamos o segundo índice, que está em R4. Na tabela, há 230 elementos.

O que fazer com isso? Podemos informar à IDA que é um switch: Editar -> Outro -> Especificar idiom de switch.

Procurando vulnerabilidades no UC Browser

O código resultante é aterrador. Mas, ao nos aventurarmos por seus labirintos, podemos notar a chamada de uma função que já conhecemos sub_6115C:

Procurando vulnerabilidades no UC Browser

Lá havia um switch, onde no case 3 estava a decodificação usando o algoritmo RC4. E nesse caso, a estrutura passada para a função é preenchida com parâmetros fornecidos em doCommandNative. Lembramos que tínhamos magicInt com o valor 16. Observamos o case correspondente – e após algumas transições encontramos o código que nos permite identificar o algoritmo.

Procurando vulnerabilidades no UC Browser

É o AES!

Temos o algoritmo, resta obter seus parâmetros: modo, chave e, possivelmente, vetor de inicialização (sua presença depende do modo de operação do algoritmo AES). A estrutura com eles deve ser formada em algum lugar antes da chamada da função sub_6115C, mas essa parte do código está ofuscada de forma especialmente eficaz, então surgiu a ideia de patchar o código para que todos os parâmetros da função de decodificação fossem despejados em um arquivo.

Patch

Para não escrever todo o código do patch em linguagem assembly manualmente, é possível iniciar o Android Studio, escrever uma função que recebe como entrada os mesmos parâmetros que a nossa função de decodificação e grava em um arquivo, e depois copiar e colar o código que o compilador gerará.

A equipe do UC Browser também se preocupou com a facilidade de adicionar código. Lembramos que no início de cada função temos um código lixo, que pode ser facilmente substituído por qualquer outro. Muito conveniente 🙂 No entanto, no início da função alvo, não há muito espaço para o código que salva todos os parâmetros em um arquivo. Tive que dividi-lo em partes e usar blocos inúteis de funções vizinhas. No total, foram quatro partes.

Primeira parte:

Procurando vulnerabilidades no UC Browser

Na arquitetura ARM, os primeiros quatro parâmetros da função são passados pelos registradores R0-R3, os outros, se existirem — pela pilha. O endereço de retorno é passado no registrador LR. Tudo isso precisa ser salvo para que a função possa funcionar após darmos o dump dos seus parâmetros. Também precisamos salvar todos os registradores que usaremos no processo, por isso fazemos PUSH.W {R0-R10,LR}. Em R7, obtemos o endereço da lista de parâmetros passados para a função pela pilha.

Usando a função fopen abriremos o arquivo /data/local/tmp/aes em modo "ab",
ou seja, para adição. Em R0, carregamos o endereço do nome do arquivo, em R1 — o endereço da string que indica o modo. E aqui o código lixo termina, então passamos para a próxima função. Para que ela continue a funcionar, colocamos no início uma transição para o código real da função, desviando do lixo, e em vez do lixo adicionamos a continuação do patch.

Procurando vulnerabilidades no UC Browser

Chamamos fopen.

Os três primeiros parâmetros da função aes têm o tipo int. Como salvamos os registradores na pilha no início, podemos simplesmente passar os endereços deles na pilha para a função. fwrite Em seguida, temos três estruturas que contêm o tamanho dos dados e um ponteiro para os dados da chave, vetor de inicialização e dados criptografados.

Procurando vulnerabilidades no UC Browser

No final, fechamos o arquivo, restauramos os registradores e passamos o controle para a função real.

Procurando vulnerabilidades no UC Browser

Compilamos o APK com a biblioteca patchada, assinamos, carregamos no dispositivo/emulador e iniciamos. Vemos que nosso dump está sendo criado e muitas informações estão sendo gravadas. O navegador usa criptografia não apenas para o tráfego, e toda a criptografia passa pela função em questão. Porém, os dados necessários não aparecem, e não vemos a requisição desejada no tráfego. Para não ter que esperar que o UC Browser faça a requisição necessária, pegaremos a resposta criptografada do servidor obtida anteriormente e patcharemos o aplicativo novamente: adicionaremos a descriptografia no onCreate da atividade principal. aes.

Estamos criando um APK com a biblioteca modificada, assinamos, transferimos para o dispositivo/emulador e iniciamos. Vemos que nosso dump está sendo criado e muitos dados estão sendo gravados. O navegador utiliza criptografia não apenas para o tráfego, e toda a criptografia passa pela função em questão. No entanto, os dados necessários não estão presentes e não conseguimos ver a solicitação relevante no tráfego. Para não esperar que o UC Browser faça a solicitação necessária, vamos pegar a resposta criptografada do servidor que recebemos anteriormente e modificar o aplicativo novamente: vamos adicionar a descriptografia na onCreate da atividade principal.

    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

Compilamos, assinamos, instalamos, executamos. Recebemos NullPointerException, pois o método retornou null.

Durante a análise do código, foi encontrada uma função onde linhas interessantes são descriptografadas: "META-INF/" e ".RSA". Parece que o aplicativo checa seu certificado. Ou até mesmo gera chaves a partir dele. Não quero me aprofundar no que acontece com o certificado, então vamos simplesmente fornecer o certificado correto. Vamos modificar a linha criptografada de forma que em vez de "META-INF/" tenhamos "BLABLINF/", criaremos uma pasta com esse nome no APK e colocaremos o certificado lá.

Compilamos, assinamos, instalamos, executamos. Bingo! Temos a chave!

MitM

Obtivemos a chave e o vetor de inicialização, que é igual à chave. Vamos tentar descriptografar a resposta do servidor em modo CBC.

Procurando vulnerabilidades no UC Browser

Vemos a URL do arquivo, algo semelhante a MD5, "extract_unzipsize" e um número. Verificamos: o MD5 do arquivo corresponde, e o tamanho da biblioteca descompactada também corresponde. Vamos tentar modificar essa biblioteca e devolvê-la ao navegador. Para mostrar que nossa biblioteca modificada foi carregada, vamos disparar um Intent para criar um SMS com o texto "PWNED!". Vamos modificar duas respostas do servidor: puds.ucweb.com/upgrade/index.xhtml e para o download do arquivo. Na primeira, vamos modificar o MD5 (o tamanho após a descompactação não muda), na segunda, oferecemos o arquivo com a biblioteca modificada.

O navegador tenta baixar o arquivo várias vezes, após o que exibe um erro. Aparentemente, algo
não está certo. Como resultado da análise desse formato nebuloso, descobrimos que o servidor também transmite o tamanho do arquivo:

Procurando vulnerabilidades no UC Browser

Ele está codificado em LEB128. Após a modificação, o tamanho do arquivo com a biblioteca mudou um pouco, portanto o navegador considerou que o arquivo foi baixado de forma incorreta, e após várias tentativas exibiu um erro.

Corrigimos o tamanho do arquivo... E – vitória! 🙂 O resultado está no vídeo.

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

Consequências e reação do desenvolvedor

Da mesma forma, os hackers poderiam usar a função insegura do UC Browser para espalhar e executar bibliotecas maliciosas. Essas bibliotecas funcionariam no contexto do navegador, portanto, obteriam todas as suas permissões de sistema. Como consequência, haveria a possibilidade de exibir janelas de phishing, além do acesso a arquivos de trabalho da esquilo laranja chinesa, incluindo logins, senhas e cookies armazenados na base de dados.

Entramos em contato com os desenvolvedores do UC Browser e informamos sobre o problema encontrado, tentando apontar para a vulnerabilidade e seu perigo, mas não quiseram discutir nada conosco. Enquanto isso, o navegador continuava a exibir a função perigosa à vista de todos. Mas assim que revelamos os detalhes da vulnerabilidade, já não se podia ignorar isso como antes. Em 27 de março, foi
lançada uma nova versão do UC Browser 12.10.9.1193, que se conectava ao servidor via HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Além disso, após a "correção" e até o momento da redação deste artigo, tentar abrir um PDF no navegador resultava em uma mensagem de erro com o texto "Oops, algo saiu errado!". A solicitação ao servidor ao tentar abrir o PDF não era realizada, mas a solicitação ao iniciar o navegador era, o que sugere que ainda havia a possibilidade de carregar código executável em violação às regras do Google Play.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster