Procurando vulnerabilidades no navegador UC


1. Introdução


No final de março, informamos que descobrimos uma capacidade oculta de baixar e executar código não verificado no navegador UC. Hoje, analisaremos em detalhes como esse download ocorre e como os hackers podem usá-lo para seus próprios fins.

Há algum tempo, o UC Browser foi anunciado e distribuído de forma muito agressiva: foi instalado nos dispositivos dos usuários usando malware, distribuído em vários sites sob o disfarce de arquivos de vídeo (ou seja, os usuários pensaram ter baixado, por exemplo, um clipe pornô, mas receberam um APK com este navegador), usou banners assustadores com mensagens dizendo que o navegador está desatualizado, vulnerável e coisas assim. O grupo oficial do navegador UC na VK tem um tópico no qual os usuários podem reclamar de publicidade desleal; existem muitos exemplos. Em 2016, houve até publicidade em vídeo em russo (sim, anúncios de um navegador que bloqueia anúncios).

No momento da redação deste artigo, o UC Browser possui mais de 500 milhões de instalações no Google Play. Isso é impressionante - apenas o Google Chrome tem mais. Entre as análises, você pode ver muitas reclamações sobre publicidade e redirecionamentos para alguns aplicativos no Google Play. Esta foi a razão do estudo: decidimos ver se o UC Browser está fazendo algo ruim. E acabou que ele estava fazendo isso!

O código do aplicativo revelou a capacidade de baixar e executar o código executável, o que contradiz as regras para publicação de aplicativos no Google Play. Além do fato de o UC Browser fazer download de código executável, torna-o inseguro, que pode ser usado para conduzir um ataque do MitM. Vamos ver se conseguimos realizar tal ataque.

Tudo o que está escrito abaixo é relevante para a versão do UC Browser que estava presente no Google Play na época do estudo:

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

Vetor de ataque


No manifesto do navegador UC, você pode encontrar um serviço chamado com.uc.deployment.UpgradeDeployService .

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

Quando esse serviço é iniciado, o navegador executa uma solicitação POST para puds.ucweb.com/upgrade/index.xhtml , que pode ser percebida no tráfego algum tempo após o início. Em resposta, ele pode receber um comando para baixar uma atualização ou um novo módulo. No processo de análise, o servidor não deu esses comandos, mas notamos que, ao tentar abrir o PDF no navegador, ele faz uma segunda solicitação no endereço acima, após o qual baixa a biblioteca nativa. Para o ataque, decidimos usar esse recurso do UC Browser: a capacidade de abrir PDF usando uma biblioteca nativa, que não está no APK e que, se necessário, é baixada da Internet. É importante notar que, teoricamente, o UC Browser pode ser forçado a baixar algo sem a interação do usuário - se você der uma resposta corretamente formada a uma solicitação que é executada após o navegador iniciar. Porém, para isso, precisamos estudar o protocolo de interação com o servidor com mais detalhes, por isso decidimos que era mais fácil editar a resposta interceptada e substituir a biblioteca por trabalhar com PDF.

Portanto, quando um usuário deseja abrir um PDF diretamente em um navegador, as seguintes solicitações podem ser vistas no tráfego:



Primeiro, vem uma solicitação POST para puds.ucweb.com/upgrade/index.xhtml , após o que
Baixe o arquivo com a biblioteca para visualizar os formatos PDF e Office É lógico supor que, na primeira solicitação, as informações sobre o sistema sejam transmitidas (pelo menos a arquitetura para fornecer a biblioteca necessária) e, em resposta, o navegador recebe algumas informações sobre a biblioteca que precisa ser baixada: endereço e, possivelmente, outra coisa. O problema é que essa solicitação está criptografada.

Fragmento de solicitação

Snippet de resposta







A própria biblioteca é compactada em ZIP e não criptografada.



Código de descriptografia de tráfego de pesquisa



Vamos tentar descriptografar a resposta do servidor. Examinamos o código da classe com.uc.deployment.UpgradeDeployService : do método onStartCommand, acesse com.uc.deployment.bx e, a partir dele, 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); } 

Vemos aqui a formação da solicitação POST. Chamamos atenção para a criação de uma matriz de 16 bytes e seu preenchimento: 0x5F, 0, 0x1F, -50 (= 0xCE). Corresponde ao que vimos na solicitação acima.

Na mesma classe, você pode notar uma classe aninhada na qual existe 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"); } } 

O método recebe uma matriz de bytes como entrada e verifica se o byte zero é 0x60 ou o terceiro byte é 0xD0 e o segundo byte é 1, 11 ou 0x1F. Nós olhamos para a resposta do servidor: zero byte - 0x60, segundo - 0x1F, terceiro - 0x60. Parece que precisamos. A julgar pelas linhas ("up_decrypt", por exemplo), um método deve ser chamado aqui que descriptografa a resposta do servidor.
Passamos para o método gj . Observe que o byte no deslocamento 2 (ou seja, 0x1F no nosso caso) é transferido para ele como o primeiro argumento e a resposta do servidor sem
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, existe uma opção de algoritmo de descriptografia, e o mesmo byte, que em nossa
case for 0x1F, indica uma das três opções possíveis.

Continuamos a analisar o código. Após alguns saltos, entramos em um método com o nome falante decryptBytesByKey .

Aqui, mais dois bytes são separados da nossa resposta e uma string é obtida deles. É claro que dessa maneira 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; } 

No futuro, observamos que, nesta fase, a chave ainda não foi obtida, mas apenas seu "identificador". Obter a chave é um pouco mais complicado.

No próximo método, mais dois são adicionados aos parâmetros existentes, e existem quatro deles: o número mágico 16, o identificador da chave, 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 da interface com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . Não há classes no código principal do aplicativo que implementam essa interface. Essa classe está no arquivo lib / armeabi-v7a / libsgmain.so , que na verdade não é .so, mas .jar. O método de seu interesse é implementado da seguinte forma:

 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 por mais dois números inteiros: 2 e 0. A julgar por
para todos, 2 significa descriptografia, como no método doFinal da classe de sistema javax.crypto.Cipher . E tudo isso é transferido para um determinado roteador com o número 10601 - esse, aparentemente, é o número do comando.

Após a próxima cadeia de transição, encontramos uma classe que implementa a interface IRouterComponent 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); } } 

E também a classe JNICLibrary , na qual o método nativo doCommandNative é 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. E então a diversão começa.

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


Há uma biblioteca nativa no arquivo libsgmain.so (que na verdade é .jar e na qual encontramos uma pequena implementação de algumas interfaces de criptografia um pouco mais alta): libsgmainso-6.4.36.so . Abra-o na IDA e obtenha várias caixas de diálogo com erros. O problema é que a tabela de cabeçalho da seção é inválida. Isso é feito especificamente para complicar a análise.



Mas também não é necessário: para carregar corretamente o arquivo ELF e analisá-lo, a tabela de cabeçalho do programa é suficiente. Portanto, simplesmente excluímos a tabela de seção, anulando os campos correspondentes no cabeçalho.



Abra novamente o arquivo no IDA.

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

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 .

Entre as funções exportadas, não há nenhuma, portanto, é necessário procurar a chamada RegisterNatives .
Vamos para a função JNI_OnLoad e vemos a seguinte imagem:



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 na pilha armazena o conteúdo dos registradores que a função usará em seu trabalho (neste caso, R0, R1 e R2), bem como o conteúdo do registrador LR, no qual o endereço de retorno da função está localizado. Com a última instrução, os registradores salvos são restaurados e o endereço de retorno é imediatamente colocado no registrador do PC - retornando assim da função. Mas se você olhar atentamente, notará que a penúltima instrução altera o endereço de retorno armazenado na pilha. Nós calculamos o que será depois
execução de código. Um determinado endereço 0xB130 é carregado em R1, 5 é subtraído, depois é transferido para R0 e 0x10 é adicionado a ele. Acontece 0xB13B. Assim, a IDA pensa que na última instrução há um retorno normal da função, mas, de fato, há uma transição para o endereço calculado 0xB13B.

Vale lembrar que os processadores ARM possuem 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 é realmente 0xB13A e a unidade no bit baixo indica o modo Thumb.

No início de cada função nesta biblioteca é adicionado um "adaptador" semelhante e
código de lixo. Além disso, não iremos nos aprofundar neles em detalhes - apenas lembramos
que o início real de quase todas as funções está um pouco mais longe.

Como não há transição explícita para 0xB13A no código, o próprio IDA não reconheceu que o código estava nesse local. Pelo mesmo motivo, ele não reconhece a maior parte do código da biblioteca como código, o que dificulta a análise. Dizemos à IDA que o código está aqui, e aqui está o resultado:



No 0xB144, a tabela começa claramente. E o sub_494C?



Quando essa função é chamada no registro LR, obtemos o endereço da tabela mencionada acima (0xB144). Em R0, o índice nesta tabela. Ou seja, um valor é retirado da tabela, adicionado ao LR e obtido
endereço para onde ir. Vamos tentar calculá-lo: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Vamos para o endereço recebido e vemos apenas algumas instruções úteis e, novamente, a transição para 0xB140:



Agora haverá uma mudança pelo deslocamento com o índice 0x20 da tabela.

A julgar pelo tamanho da tabela, existem muitas transições no código. Surge a questão de saber se é possível lidar de alguma forma com isso mais automaticamente, sem calcular manualmente os endereços. E os scripts e a capacidade de corrigir o código na AID vêm em nosso auxílio:

 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 linha 0xB26A, executamos o script e vemos a transição para 0xB4B0:



A IDA novamente não reconheceu este site como um código. Nós a ajudamos e vemos outra construção lá:



As instruções após o BLX não parecem muito significativas, são mais como algum tipo de viés. Observamos em sub_4964:



De fato, aqui o dword é obtido no endereço localizado em LR, adicionado a esse endereço, após o qual o valor no endereço recebido é obtido e enviado para a pilha. Além disso, 4 é adicionado ao LR, para que, após retornar da função, pule esse mesmo deslocamento. Em seguida, o comando POP {R1} extrai o valor recebido da pilha. Se você olhar para o endereço 0xB4BA + 0xEA = 0xB5A4, poderá ver algo semelhante à tabela de endereços:



Para corrigir esse design, você precisa obter dois parâmetros do código: o deslocamento e o número do registro no qual deseja colocar o resultado. Para cada registro possível, você deverá preparar um código com antecedência.

 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 construção que queremos substituir - 0xB4B2 - e executamos o script:



Além das construções já mencionadas no código, aqui estão também:



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



Tomamos o deslocamento para o endereço da LR, adicionamos à LR e vamos lá. 0x72044 + 0xC = 0x72050. O script para este design é muito 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 do script:



Depois que tudo estiver corrigido na função, você poderá apontar o IDA para o seu verdadeiro começo. Ele coletará todo o código da função em partes e poderá ser descompilado usando HexRays.

Decodificando 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 para a 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; } 

Considere cuidadosamente as seguintes linhas:

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

A função sub_73E24 descriptografa explicitamente o nome da classe. , , . , , . . FindClass , . , — . , , . , 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; } 

sub_7AF78 ( ). : «DcO/lcK+h?m3c*q@» ( , ), — . , sub_6115C . 3. , .

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

switch , 3. case 3: sub_6364C , , . . . sub_6364C , RC4.

. . : com/taobao/wireless/security/adapter/JNICLibrary . Ótimo! .


RegisterNatives , doCommandNative . , JNI_OnLoad, 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; } 

De fato, um método nativo chamado doCommandNative é registrado aqui . Agora sabemos o endereço dele. 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; } 

, , . 10601.

, : command / 10000 , command % 10000 / 100 command % 10 , . ., , 1, 6 1. , JNIEnv , , . ( N1, N2 N3) .

:



JNI_OnLoad .
. . — . , , , ( , ).


, : 0x5F1AC. : UC Browser .

, Java-,
0x4D070. .

R7 R4 :



R11:



, :



, R4. 230 .

O que fazer sobre isso? IDA, switch: Edit -> Other -> Specify switch idiom.



. , , sub_6115C :



switch, case 3 RC4. , doCommandNative . , magicInt 16. case – , .



AES!

, : , , , ( AES). - sub_6115C , , , .


, Android Studio, , , , , , .

UC Browser «». , , . :) , , , . . .

:



ARM R0-R3, , — . LR . , , . , , PUSH.W {R0-R10,LR}. R7 , .

fopen /data/local/tmp/aes «ab»,
. . . R0 , R1 — . , . , , .



fopen .

aes int . , fwrite .



, , .



, aes .

APK , , /, . , , . , . - , . , UC Browser , , , : onCreate .

  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 

, , , . NullPointerException, . . null.

, : «META-INF/» ".RSA". , . . , , , . , «META-INF/» «BLABLINF/», APK .

, , , . Bingo! !

MitM


, . CBC.



URL , - MD5, «extract_unzipsize» . : MD5 , . . , , Intent «PWNED!». : puds.ucweb.com/upgrade/index.xhtml . MD5 ( ), .

, . , -
. , :



LEB128. , , , .

… – ! :) .

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


UC Browser, . , . — , , , .

UC Browser , , - . . , , , . 27
UC Browser 12.10.9.1193, HTTPS: puds.ucweb.com/upgrade/index.xhtml .

, «» PDF «, - !». PDF , , Google Play.

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


All Articles