Quebrando o navegador UC



1. Introdução


No final de março, relatamos o potencial oculto de baixar e executar código não verificado no navegador UC. Hoje vamos examinar em detalhes como isso acontece e como os hackers podem usá-lo.

Há algum tempo, o UC Browser foi promovido e distribuído de forma bastante agressiva. Ele foi instalado nos dispositivos por malware, distribuído em sites sob o disfarce de arquivos de vídeo (ou seja, os usuários pensavam que estavam baixando pornografia ou algo assim, mas estavam obtendo arquivos APK com este navegador), anunciados usando banners preocupantes sobre o navegador de um usuário estar desatualizado ou vulnerável. O grupo oficial UC Browser VK tinha um tópico em que os usuários podiam reclamar sobre publicidade falsa e muitos usuários forneceram exemplos. Em 2016, houve até um comercial em russo (sim, um comercial de um navegador que bloqueia comerciais).

Enquanto escrevemos este artigo, o UC Browser foi instalado 500.000.000 de vezes no Google Play. Isso é impressionante, pois apenas o Google Chrome conseguiu superar isso. Entre as análises, você pode ver muitas reclamações de usuários sobre publicidade e ser redirecionado para outros aplicativos no Google Play. Esta foi a razão do nosso estudo: queríamos ver se o UC Browser está fazendo algo errado. E é isso! O aplicativo pode baixar e executar código executável, o que viola a política do Google Play para publicação de aplicativos . E o UC Browser não baixa apenas o código executável; isso é inseguro, o que pode ser usado para um ataque do MitM. Vamos ver se podemos usá-lo dessa maneira.

Tudo o que se segue se aplica à versão do UC Browser que foi distribuída pelo Google Play no momento do nosso estudo:

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

Vetor de ataque


O manifesto do UC Browser contém um serviço com o nome de com.uc.deployment.UpgradeDeployService .

  <service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" /> 

Quando esse serviço é iniciado, o navegador faz uma solicitação POST para puds.ucweb.com/upgrade/index.xhtml que pode ser vista no tráfego por algum tempo após o lançamento. Em resposta, o navegador pode receber um comando para baixar qualquer atualização ou um novo módulo. Durante nossa análise, nunca recebemos esses comandos do servidor, mas notamos que, ao tentar abrir um arquivo PDF no navegador, ele repete a solicitação no endereço acima e baixa uma biblioteca nativa. Para simular um ataque, decidimos usar esse recurso do UC Browser - a capacidade de abrir arquivos PDF usando uma biblioteca nativa - não presente no arquivo APK, mas disponível para download na Internet. Tecnicamente, o UC Browser pode baixar algo sem a permissão de um usuário quando receber uma resposta apropriada a uma solicitação enviada na inicialização. Porém, para isso, precisamos estudar o protocolo de interação com o servidor com mais detalhes, por isso achamos mais fácil conectar e editar a resposta e substituir a biblioteca necessária para abrir arquivos PDF por algo diferente.

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



Primeiro, há uma solicitação POST para puds.ucweb.com/upgrade/index.xhtml , e a biblioteca compactada para visualizar arquivos PDF e documentos do escritório é baixada. Logicamente, podemos assumir que a primeira solicitação envia informações sobre o sistema (pelo menos a arquitetura, porque o servidor precisa selecionar uma biblioteca apropriada) e o servidor responde com algumas informações sobre a biblioteca que precisa ser baixada, como seu endereço e talvez outra coisa. O problema é que essa solicitação está criptografada.

Fragmento de solicitação

Fragmento de resposta






A biblioteca é compactada em um arquivo ZIP e não criptografada.



Procurando código de descriptografia de tráfego


Vamos tentar descriptografar a resposta do servidor. Dê uma olhada no código da classe com.uc.deployment.UpgradeDeployService : no método onStartCommand , navegamos para com.uc.deployment.bx e depois para com.uc.browser.core.dcfe :

  public final void e(l arg9) { int v4_5; String v3_1; byte[] v3; byte[] v1 = null; if(arg9 == null) { v3 = v1; } else { v3_1 = arg9.iGX.ipR; StringBuilder v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]product:"); v4.append(arg9.iGX.ipR); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]version:"); v4.append(arg9.iGX.iEn); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]upgrade_type:"); v4.append(arg9.iGX.mMode); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]force_flag:"); v4.append(arg9.iGX.iEo); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_mode:"); v4.append(arg9.iGX.iDQ); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_type:"); v4.append(arg9.iGX.iEr); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_state:"); v4.append(arg9.iGX.iEp); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_file:"); v4.append(arg9.iGX.iEq); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apk_md5:"); v4.append(arg9.iGX.iEl); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_type:"); v4.append(arg9.mDownloadType); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_group:"); v4.append(arg9.mDownloadGroup); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_path:"); v4.append(arg9.iGH); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_child_version:"); v4.append(arg9.iGX.iEx); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_series:"); v4.append(arg9.iGX.iEw); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_arch:"); v4.append(arg9.iGX.iEt); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp3:"); v4.append(arg9.iGX.iEv); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp:"); v4.append(arg9.iGX.iEu); ArrayList v3_2 = arg9.iGX.iEz; if(v3_2 != null && v3_2.size() != 0) { Iterator v3_3 = v3_2.iterator(); while(v3_3.hasNext()) { Object v4_1 = v3_3.next(); StringBuilder v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_name:"); v5.append(((au)v4_1).getName()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_name:"); v5.append(((au)v4_1).aDA()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_code:"); v5.append(((au)v4_1).gBl); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_req_type:"); v5.append(((au)v4_1).gBq); } } j v3_4 = new j(); mb(v3_4); h v4_2 = new h(); mb(v4_2); ay v5_1 = new ay(); v3_4.hS(""); v3_4.setImsi(""); v3_4.hV(""); v5_1.bPQ = v3_4; v5_1.bPP = v4_2; v5_1.yr(arg9.iGX.ipR); v5_1.gBF = arg9.iGX.mMode; v5_1.gBI = arg9.iGX.iEz; v3_2 = v5_1.gAr; c.aBh(); v3_2.add(g.fs("os_ver", c.getRomInfo())); v3_2.add(g.fs("processor_arch", com.uc.baacgetCpuArch())); v3_2.add(g.fs("cpu_arch", com.uc.baacPb())); String v4_3 = com.uc.baacPd(); v3_2.add(g.fs("cpu_vfp", v4_3)); v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo()))); v3_2.add(g.fs("fromhost", arg9.iGX.iEm)); v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn)); v3_2.add(g.fs("target_lang", arg9.iGX.iEs)); v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt)); v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu)); v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv)); v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx)); v3_2.add(g.fs("ver_series", arg9.iGX.iEw)); v3_2.add(g.fs("child_ver", r.aVw())); v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl)); v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature())); v3_2.add(g.fs("upgrade_log", i.bjt())); v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ))); v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp))); v3_2.add(g.fs("silent_file", arg9.iGX.iEq)); v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr))); v3_2.add(g.fs("cpu_archit", com.uc.baacPc())); v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction())); boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true; v3_2.add(g.fs("neon", String.valueOf(v4_4))); v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.baacJl()))); v3_2.add(g.fs("ram_1", String.valueOf(com.uc.baahPo()))); v3_2.add(g.fs("totalram", String.valueOf(com.uc.baahOL()))); c.aBh(); v3_2.add(g.fs("rom_1", c.getRomInfo())); v4_5 = e.getScreenWidth(); int v6 = e.getScreenHeight(); StringBuilder v7 = new StringBuilder(); v7.append(v4_5); v7.append("*"); v7.append(v6); v3_2.add(g.fs("ss", v7.toString())); v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT))); v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks())); Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator(); while(v4_6.hasNext()) { Object v6_1 = v4_6.next(); v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue())); } v3 = v5_1.toByteArray(); } if(v3 == null) { this.iGY.iGI.a(arg9, "up_encode", "yes", "fail"); return; } v4_5 = this.iGY.iGw ? 0x1F : 0; if(v3 == null) { } else { v3 = gi(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.dci 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.dci$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); mb(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); } 

Podemos ver que é aqui que a solicitação POST é feita. Observe a matriz de 16 bytes que contém: 0x5F, 0, 0x1F, -50 (= 0xCE). Os valores são os mesmos da solicitação acima.

A mesma classe contém uma classe aninhada com 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 = gj(arg11[2], v7); } if(v7 == null) { goto label_57; } if(v5 != 0) { v1_1 = gP(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 = gb(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"); } } 

Este método recebe uma entrada de uma matriz de bytes e verifica se o byte zero é 0x60, ou o terceiro byte é 0xD0 e se o segundo byte é 1, 11 ou 0x1F. Confira a resposta do servidor: zero byte é 0x60, o segundo byte é 0x1F, o terceiro byte é 0x60. Parece o que precisamos. A julgar pelas seqüências de caracteres ("up_decrypt", por exemplo), um método deve ser chamado aqui para descriptografar a resposta do servidor. Agora vamos ver o método gj. Observe que o primeiro argumento é um byte no deslocamento 2 (ou seja, 0x1F no nosso caso) e o segundo é a resposta do servidor sem os primeiros 16 bytes.

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

Obviamente, ele está selecionando o algoritmo de descriptografia, e o byte que, no nosso caso, é igual a 0x1F indica uma das três opções possíveis.

Vamos voltar à análise de código. Após alguns saltos, chegamos ao método com o nome do indicador, decryptBytesByKey . Agora, mais dois bytes são separados da nossa resposta e formam uma string. É claro que é assim que a chave é selecionada para descriptografar 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  System.arraycopy(bytes, 0, prefix, 0, prefix.length); String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); //   if(keyId == null) { return v0; } else { a v2 = EncryptHelper.ayL(); if(v2 == null) { return v0; } else { byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE]; System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length); return v2.l(keyId, enrypted); } } } } catch(SecException v7_1) { EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode()); return v0; } catch(Throwable v7) { EncryptHelper.handleDecryptException(v7, 2); return v0; } } return v0; } 

Avançando um pouco, observe que, neste estágio, é apenas o identificador da chave, não a chave em si. A seleção das teclas será um pouco mais complicada.

No próximo método, mais dois parâmetros são adicionados aos já existentes, obtendo um total de quatro. O número mágico 16, o identificador da chave, os dados criptografados e uma string são adicionados por algum motivo (vazio no nosso caso).

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

Após uma série de saltos, vemos o método staticBinarySafeDecryptNoB64 da interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . O código principal do aplicativo não possui classes que implementam essa interface. Essa classe está contida no arquivo lib / armeabi-v7a / libsgmain.so , que não é realmente .SO, mas sim .JAR. O método em que estamos interessados ​​é implementado da seguinte maneira:

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

Aqui, nossa lista de parâmetros é complementada com mais dois números inteiros: 2 e 0. Aparentemente, 2 significa descriptografia, como no método doFinal da classe de sistema javax.crypto.Cipher . Em seguida, essas informações são transmitidas para um determinado roteador junto com o número 10601, que aparentemente é o número do comando.

Após a próxima cadeia de saltos, encontramos uma classe que implementa a interface RouterComponent e o método 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); } } 

Há também a classe JNIC Library , em que o método doCommandNative nativo é declarado:

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

Portanto, precisamos encontrar o método doCommandNative no código nativo. É aí que a diversão começa.

Ofuscação do código da máquina


Há uma biblioteca nativa no arquivo libsgmain.so (que na verdade é um arquivo .JAR e, como dissemos acima, implementa algumas interfaces relacionadas à criptografia): libsgmainso-6.4.36.so . Nós o carregamos no IDA e recebemos várias caixas de diálogo com mensagens de erro. O problema é que a tabela de cabeçalho da seção é inválida. Isso é feito de propósito para complicar a análise.



Mas realmente não precisamos disso de qualquer maneira. A tabela de cabeçalho do programa é suficiente para carregar corretamente o arquivo ELF e analisá-lo. Então, simplesmente excluímos a tabela de cabeçalho da seção, anulando os campos correspondentes no cabeçalho.



Em seguida, abrimos o arquivo no IDA novamente.

Temos duas maneiras de informar à máquina virtual Java exatamente onde a biblioteca nativa contém a implementação do método declarado como nativo no código Java. A primeira é atribuir um nome assim: Java_package_name_ClassName_methodName . O segundo é registrá-lo ao carregar a biblioteca (na função JNI_OnLoad) chamando a função RegisterNatives. No nosso caso, se você usar o primeiro método, o nome deverá ser assim: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . A lista de funções exportadas não contém esse nome, o que significa que precisamos procurar os RegisterNatives. Assim, vamos à função JNI_OnLoad e vemos o seguinte:



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 envia o conteúdo dos registradores que a função usará para a pilha (neste caso, R0, R1 e R2), bem como o conteúdo do registrador LR com o endereço de retorno da função. A última instrução restaura os registradores salvos e coloca o endereço de retorno no registrador do PC, retornando assim da função. Mas se dermos uma olhada mais de perto, podemos notar que a penúltima instrução altera o endereço de retorno, armazenado na pilha. Vamos calcular o que será quando o código for executado. O endereço 0xB130 é carregado em R1, subtraído 5, depois é movido para R0 e recebe uma adição de 0x10. No final, é igual a 0xB13B. Assim, a IDA pensa que a instrução final executa um retorno normal da função, enquanto na verdade realiza uma transferência para o endereço calculado 0xB13B.

Agora, lembremos que os processadores ARM têm dois modos e dois conjuntos de instruções - ARM e Thumb. O bit de ordem inferior do endereço determina qual conjunto de instruções o processador usará, ou seja, o endereço é realmente 0xB13A, enquanto o valor no bit de ordem inferior indica o modo Thumb.
Um "adaptador" semelhante e algum lixo semântico são adicionados ao início de cada função nesta biblioteca. Mas não vamos nos deter neles em detalhes. Lembre-se de que o início real de quase todas as funções está um pouco mais longe.

Como não existe uma transição explícita para 0xB13A no código, o IDA não pode reconhecer que existe um código lá. Pelo mesmo motivo, ele não reconhece a maior parte do código da biblioteca como código, o que torna a análise um pouco mais complicada. Então, dizemos à IDA que há código, e é isso que acontece:



A partir de 0xB144, temos claramente a tabela. Mas e o sub_494C?



Ao chamar esta função no registro LR, obtemos o endereço da tabela acima mencionada (0xB144). R0 contém o índice nesta tabela. Ou seja, pegamos o valor da tabela, adicionamos ao LR e obtemos o endereço para o qual precisamos ir. Vamos tentar calculá-lo: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Navegamos para este endereço e vemos algumas instruções úteis, e depois vamos para 0xB140:



Agora, haverá uma transição no deslocamento com o índice 0x20 da tabela.
A julgar pelo tamanho da tabela, haverá muitas transições no código. Portanto, gostaríamos de lidar com isso automaticamente e evitar o cálculo manual de endereços. Assim, os scripts e a capacidade de corrigir códigos no IDA vêm em nosso socorro:

 def put_unconditional_branch(source, destination): offset = (destination - source - 4) >> 1 if offset > 2097151 or offset < -2097152: raise RuntimeError("Invalid offset") if offset > 1023 or offset < -1024: instruction1 = 0xf000 | ((offset >> 11) & 0x7ff) instruction2 = 0xb800 | (offset & 0x7ff) patch_word(source, instruction1) patch_word(source + 2, instruction2) else: instruction = 0xe000 | (offset & 0x7ff) patch_word(source, instruction) ea = here() if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR} ea1 = ea + 2 if get_wide_word(ea1) == 0xbf00: #NOP ea1 += 2 if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2: index = get_wide_dword(get_operand_value(ea1, 1)) print "index =", hex(index) ea1 += 2 if get_operand_type(ea1, 0) == 7: table = get_operand_value(ea1, 0) + 4 elif get_operand_type(ea1, 1) == 2: table = get_operand_value(ea1, 1) + 4 else: print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1) table = None if table is None: print "Unable to find table" else: print "table =", hex(table) offset = get_wide_dword(table + (index << 2)) put_unconditional_branch(ea, table + offset) else: print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2 else: print "Unable to detect first instruction" 

Colocamos o cursor na string 0xB26A, executamos o script e vemos a transição para 0xB4B0:



Mais uma vez, a IDA não reconhece esse lugar como código. Ajudamos e vemos outra estrutura lá:



As instruções que seguem o BLX não parecem muito significativas; eles são mais como algum tipo de compensação. Nós olhamos para sub_4964:



De fato, ele pega DWORD no endereço da LR, adiciona-o a esse endereço, pega o valor no endereço resultante e o armazena na pilha. Além disso, adiciona 4 ao LR para pular esse mesmo deslocamento depois de retornar da função. Em seguida, o comando POP {R1} pega o valor resultante da pilha. Observando o que está localizado no endereço 0xB4BA + 0xEA = 0xB5A4, podemos ver algo semelhante à tabela de endereços:



Para corrigir essa estrutura, precisamos obter dois parâmetros do código: o deslocamento e o número do registro em que queremos enviar o resultado. Teremos que preparar um pedaço de código com antecedência para cada registro possível.

 patches = {} patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0) patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0) patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0) patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0) patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0) patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0) patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0) patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0) patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0) patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0) ea = here() if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8 and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR} if get_operand_type(ea + 4, 0) == 7: pop = get_bytes(ea + 12, 4, 0) if pop[1] == '\xbc': register = -1 r = get_wide_byte(ea + 12) for i in range(8): if r == (1 << i): register = i break if register == -1: print "Unable to detect register" else: address = get_wide_dword(ea + 8) + ea + 8 for b in patches[register]: patch_byte(ea, b) ea += 1 if ea % 4 != 0: ea += 2 patch_dword(ea, address) elif pop[:3] == '\x5d\xf8\x04': register = ord(pop[3]) >> 4 if register in patches: address = get_wide_dword(ea + 8) + ea + 8 for b in patches[register]: patch_byte(ea, b) ea += 1 patch_dword(ea, address) else: print "POP instruction not found" else: print "Wrong operand type on +4:", get_operand_type(ea + 4, 0) else: print "Unable to detect first instructions" 

Colocamos o cursor no início da estrutura que queremos substituir (ou seja, 0xB4B2) e executamos o script:



Além das estruturas já mencionadas, o código inclui o seguinte:



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



Tomamos o deslocamento no endereço da LR, adicionamos à LR e navegamos até lá. 0x72044 + 0xC = 0x72050. O script para essa estrutura é bastante simples:

 def put_unconditional_branch(source, destination): offset = (destination - source - 4) >> 1 if offset > 2097151 or offset < -2097152: raise RuntimeError("Invalid offset") if offset > 1023 or offset < -1024: instruction1 = 0xf000 | ((offset >> 11) & 0x7ff) instruction2 = 0xb800 | (offset & 0x7ff) patch_word(source, instruction1) patch_word(source + 2, instruction2) else: instruction = 0xe000 | (offset & 0x7ff) patch_word(source, instruction) ea = here() if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR} ea1 = ea + 6 if get_wide_word(ea + 2) == 0xbf00: #NOP ea1 += 2 offset = get_wide_dword(ea1) put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff) else: print "Unable to detect first instruction" 

O resultado da execução do script:



Depois de corrigirmos tudo nesta função, podemos apontar o IDA para o seu verdadeiro começo. Ele coletará todo o código da função, peça por peça, e poderemos descompilar usando HexRays.

Descriptografando as strings


Aprendemos como lidar com a ofuscação de código de máquina na biblioteca libsgmainso-6.4.36.so do UC Browser e obtivemos o código de 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 analisar as seguintes strings:

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

É bastante claro que a função sub_73E24 descriptografa o nome da classe. Os parâmetros desta função contêm um ponteiro para os dados que se parecem com aqueles criptografados, um tipo de buffer e um número. Obviamente, uma string descriptografada estará no buffer após uma chamada para a função, pois o buffer vai para a função FindClass , que recebe o mesmo nome de classe que o segundo parâmetro. Portanto, o número é o tamanho do buffer ou o comprimento da string. Vamos tentar decifrar o nome da classe. Deve indicar se estamos indo na direção certa. Vamos dar uma olhada no que acontece no 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; } 

A função sub_7AF78 cria uma instância de contêiner para matrizes de bytes do tamanho especificado (não iremos focar nelas em detalhes). Dois desses contêineres são criados aqui: um contém a sequência " DcO / lcK + h? M3c * q @ " (é fácil supor que essa seja a chave), o outro possui os dados criptografados. Ambos os objetos são colocados em uma determinada estrutura, que é transferida para a função sub_6115C . Também podemos observar que essa estrutura contém um campo com o valor 3. Vamos ver o que acontece a seguir.

 int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2) { int v3; // lr unsigned int v4; // r1 int v5; // r0 int v6; // r1 int result; // r0 int v8; // r0 *a2 = 820000; if ( a1 ) { v3 = a1->field_14; if ( v3 ) { v4 = a1->field_4; if ( v4 < 0x19 ) { switch ( v4 ) { case 0u: v8 = sub_6419C(a1->field_0, a1->field_10, v3); goto LABEL_17; case 3u: v8 = sub_6364C(a1->field_0, a1->field_10, v3); goto LABEL_17; case 0x10u: case 0x11u: case 0x12u: v8 = sub_612F4( a1->field_0, v4, *(_QWORD *)&a1->field_8, *(_QWORD *)&a1->field_8 >> 32, a1->field_10, v3, a2); goto LABEL_17; case 0x14u: v8 = sub_63A28(a1->field_0, v3); goto LABEL_17; case 0x15u: sub_61A60(a1->field_0, v3, a2); return result; case 0x16u: v8 = sub_62440(a1->field_14); goto LABEL_17; case 0x17u: v8 = sub_6226C(a1->field_10, v3); goto LABEL_17; case 0x18u: v8 = sub_63530(a1->field_14); LABEL_17: v6 = 0; if ( v8 ) { *a2 = 0; v6 = v8; } return v6; default: LOWORD(v5) = 28032; goto LABEL_5; } } } } LOWORD(v5) = -27504; LABEL_5: HIWORD(v5) = 13; v6 = 0; *a2 = v5; return v6; } 

O campo com o valor atribuído anteriormente 3 é transferido como o parâmetro switch. Vamos dar uma olhada no caso 3 - parâmetros que a função anterior adicionada à estrutura (ou seja, a chave e os dados criptografados) são transferidos para a função sub_6364C. Se olharmos atentamente para sub_6364C, podemos reconhecer o algoritmo RC4.

Portanto, temos um algoritmo e uma chave. Vamos tentar decifrar o nome da classe. É isso que temos: com / taobao / wireless / security / adapter / JNICLibrary. Brilhante! Estamos no caminho certo.

Árvore de comando


Agora precisamos encontrar a chamada para RegisterNatives , que nos indicará a função doCommandNative . Então, examinamos as funções chamadas de JNI_OnLoad e a encontramos em 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, um método nativo com o nome doCommandNative é registrado aqui. Agora sabemos o seu endereço. Vamos dar uma olhada no que 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; } 

O nome sugere que é o ponto de entrada para todas as funções que os desenvolvedores transferiram para a biblioteca nativa. Estamos especificamente interessados ​​na função número 10601.

A partir do código, podemos ver que o número do comando nos fornece três números: comando / 10000, comando% 10000/100 e comando% 10 (no nosso caso, 1, 6 e 1). Esses três números, assim como o ponteiro para JNIEnv e os argumentos transferidos para a função, formam uma estrutura e são transmitidos. Com esses três números (vamos denotá-los N1, N2 e N3), uma árvore de comando é construída. Algo assim:



A árvore é criada dinamicamente no JNI_OnLoad. Três números codificam o caminho na árvore. Cada folha da árvore contém o endereço xorred da função correspondente. A chave está no nó pai. É muito fácil encontrar um local no código em que a função que precisamos seja adicionada à árvore se entendermos todas as estruturas existentes (não passaremos tempo descrevendo-as neste artigo).

Mais ofuscação


Temos o endereço da função que deve descriptografar o tráfego: 0x5F1AC. Mas ainda é cedo para relaxar - os desenvolvedores do UC Browser têm outra surpresa para nós.

Depois de receber os parâmetros de uma matriz no código Java, vamos para a função em 0x4D070. Outro tipo de ofuscação de código já está aguardando.

Em seguida, pressionamos dois índices em R7 e R4:



O primeiro índice passa para o R11:



Usamos esse índice para obter o endereço da tabela:



Após a transferência para o primeiro endereço, usamos o segundo índice de R4. A tabela contém 230 elementos.

O que fazemos com isso? Poderíamos dizer à IDA que é uma espécie de opção: Editar -> Outro -> Especifique o idioma da chave.



O código resultante é horrendo.No entanto, podemos ver a chamada para a função familiar sub_6115C em seus emaranhados:



havia o parâmetro switch com a descriptografia RC4 no caso 3. Nesse caso, a estrutura que transfere para a função é preenchida com os parâmetros transferidos para doCommandNative. Lembramos que tivemos magicInt com o valor 16. Examinamos o caso correspondente e, após várias transições, encontramos o código que nos ajuda a identificar o algoritmo.



É AES!

Temos um algoritmo e só precisamos obter seus parâmetros, como modo, chave e (possivelmente) o vetor de inicialização (sua presença depende do modo de operação do algoritmo AES). A estrutura que os contém deve ser criada em algum lugar antes de chamar a função sub_6115C. Mas como essa parte do código é particularmente ofuscada, decidimos corrigir o código para que todos os parâmetros da função de descriptografia pudessem ser despejados em um arquivo.

Patch


Se você não quiser escrever manualmente todo o código de correção na linguagem assembly, poderá executar o Android Studio, codificar uma função que recebe os mesmos parâmetros da nossa função de descriptografia e gravar no arquivo e copiar o código resultante gerado pelo compilador

Nossos bons amigos da equipe do UC Browser também "garantiram" a conveniência de adicionar código. Lembramos que temos código de lixo no início de cada função, que pode ser facilmente substituído por qualquer outro código. Muito conveniente :) No entanto, não há espaço suficiente no início da função de destino para o código que salva todos os parâmetros em um arquivo. Tivemos que dividi-lo em partes e usar os blocos de lixo das funções vizinhas. Temos quatro partes no total.
Parte um:



Os quatro primeiros parâmetros de função na arquitetura ARM são indicados nos registradores R0-R3, enquanto o restante, se houver, passa pela pilha. O registro LR indica o endereço de retorno. Precisamos salvar todos esses dados para que a função funcione depois de despejar seus parâmetros. Também precisamos salvar todos os registros que usamos no processo, para usar PUSH.W {R0-R10, LR}. Em R7, obtemos o endereço da lista de parâmetros transferidos para a função via pilha.

Usando a função fopen, abrimos o arquivo / data / local / tmp / aes no modo “ab” (para que possamos adicionar algo). Em seguida, carregamos o endereço do nome do arquivo em R0 e o endereço da string que indica o modo em R1. É aqui que o código do lixo termina, então navegamos para a próxima função. Como queremos que ele continue funcionando, colocamos a transição para o código de função real no início, antes do lixo, e substituímos o lixo pelo restante do patch.



Então chamamos fopen.

Os três primeiros parâmetros da função aes são do tipo int. Desde que colocamos os registradores na pilha no início, podemos simplesmente transferir seus endereços na pilha para a função fwrite.



Em seguida, temos três estruturas que indicam o tamanho dos dados e contêm um ponteiro para os dados da chave, o vetor de inicialização e os dados criptografados.



No final, fechamos o arquivo, restauramos os registradores e devolvemos o controle à função aes real.
Compilamos o arquivo APK com a biblioteca corrigida, assinamos, fazemos o download em um dispositivo ou emulador e o executamos. Agora vemos que o dump foi criado com muitos dados. O navegador não apenas descriptografa o tráfego, mas também outros dados, e toda a descriptografia é realizada por meio dessa função. Por alguma razão, não vemos os dados de que precisamos e a solicitação que esperamos não é visível no tráfego. Vamos pular a espera até que o UC Browser tenha a chance de fazer essa solicitação e obter a resposta criptografada obtida anteriormente do servidor para corrigir o aplicativo novamente. Adicionaremos a descriptografia ao 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 

Nós o compilamos, assinamos, instalamos e executamos. Assim, obtemos uma NullPointerException, pois o método retorna um valor nulo.

Após uma análise mais aprofundada do código, encontramos uma função com sequências bastante interessantes: "META-INF /" e ".RSA". Parece que o aplicativo verifica seu certificado ou até gera chaves a partir dele. Nós realmente não queremos descobrir o que está acontecendo com o certificado, então vamos dar o certificado correto. Corrigiremos o patch da string criptografada. Em vez de "META-INF /", obtemos "BLABLINF /", criamos uma pasta com esse nome no arquivo APK e salvamos o certificado do navegador.

Nós o compilamos, assinamos, instalamos e executamos. Bingo! Nós temos a chave!

Mitm


Agora temos a chave e um vetor de inicialização igual. Vamos tentar descriptografar a resposta do servidor no modo CBC.



Vemos o URL do arquivo, algo como MD5, "extract_unzipsize" e um número. Vamos verificar. O MD5 do arquivo é o mesmo; o tamanho da biblioteca descompactada é o mesmo. Agora, tentaremos corrigir esta biblioteca e transmiti-la ao navegador. Para mostrar que nossa biblioteca corrigida foi carregada, criaremos uma Intenção para criar a mensagem de texto "PWNED!" Substituiremos duas respostas do servidor: puds.ucweb.com/upgrade/index.xhtml e a que solicitar o download do arquivo. No primeiro, substituímos o MD5 (o tamanho permanece o mesmo após descompactar); no segundo, enviamos o arquivo com a biblioteca corrigida.

O navegador faz várias tentativas para baixar o arquivo morto, resultando em um erro. Aparentemente, algo suspeito está acontecendo lá. Analisando esse formato bizarro, descobrimos que o servidor também transmite o tamanho do arquivo:



é codificado em LEB128. O patch altera ligeiramente o tamanho da biblioteca compactada; portanto, o navegador decidiu que o arquivo era interrompido durante o download e exibia um erro após várias tentativas.
Então, nós corrigimos o tamanho do arquivo e ... pronto! :) Veja o resultado no vídeo.


Consequências e resposta do desenvolvedor


Da mesma forma, os hackers podem usar esse recurso inseguro do UC Browser para distribuir e iniciar bibliotecas maliciosas. Essas bibliotecas funcionarão no contexto do navegador, resultando em privilégios completos do sistema que o navegador possui. Isso lhes concede liberdade para exibir janelas de phishing, além de acessar os arquivos de trabalho do navegador, incluindo logins, senhas e cookies no banco de dados.

Entramos em contato com os desenvolvedores do UC Browser e informamos sobre o problema que havíamos encontrado, tentamos apontar a vulnerabilidade e seu perigo, mas eles se recusaram a discutir o assunto. Enquanto isso, o navegador com a função perigosa permaneceu à vista. Embora assim que revelássemos os detalhes da vulnerabilidade, era impossível ignorá-la como antes. Uma nova versão do UC Browser 10.10.9.1193 foi lançada em 27 de março, que acessava o servidor via HTTPS puds.ucweb.com/upgrade/index.xhtml. Além disso, entre a “correção de bugs” e a época em que escrevemos este artigo, uma tentativa de abrir um PDF no navegador resultou em uma mensagem de erro com o texto “Ops, algo está errado”. Não houve solicitação ao servidor ao tentar abrir o arquivo PDF. Porém, isso foi realizado na inicialização, o que é um sinal de que a capacidade de baixar o código executável violando as políticas do Google Play ainda está presente.

Source: https://habr.com/ru/post/pt452076/


All Articles