Rompiendo el navegador UC



Introduccion


A finales de marzo, informamos sobre el potencial oculto para descargar y ejecutar código no verificado en el navegador UC. Hoy examinaremos en detalle cómo sucede y cómo los hackers pueden usarlo.

Hace algún tiempo, UC Browser fue promovido y distribuido de manera bastante agresiva. Fue instalado en dispositivos por malware, distribuido a través de sitios web bajo la apariencia de archivos de video (es decir, los usuarios pensaron que estaban descargando pornografía o algo así, pero en su lugar estaban obteniendo archivos APK con este navegador), anunciados usando pancartas preocupantes sobre el navegador de un usuario desactualizado o vulnerable El grupo oficial UC Browser VK tenía un tema en el que los usuarios podían quejarse de publicidad falsa y muchos usuarios proporcionaban ejemplos. En 2016, incluso hubo un comercial en ruso (sí, un comercial de un navegador que bloquea los comerciales).

Mientras escribimos este artículo, UC Browser se instaló 500,000,000 veces desde Google Play. Esto es impresionante ya que solo Google Chrome logró superar eso. Entre las revisiones, puede ver muchas quejas de los usuarios sobre publicidad y ser redirigido a otras aplicaciones en Google Play. Esta fue la razón de nuestro estudio: queríamos ver si UC Browser está haciendo algo mal. Y lo es! La aplicación puede descargar y ejecutar código ejecutable, lo que viola la política de Google Play para la publicación de aplicaciones . Y UC Browser no solo descarga código ejecutable; lo hace de forma insegura, que puede usarse para un ataque MitM. Veamos si podemos usarlo de esta manera.

Todo lo que sigue se aplica a la versión de UC Browser que se distribuyó a través de Google Play en el momento de nuestro estudio:

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

Vector de ataque


El manifiesto del navegador UC contiene un servicio con un nombre revelador de com.uc.deployment.UpgradeDeployService .

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

Cuando se inicia este servicio, el navegador realiza una solicitud POST a puds.ucweb.com/upgrade/index.xhtml que se puede ver en el tráfico durante algún tiempo después del lanzamiento. En respuesta, el navegador puede recibir un comando para descargar cualquier actualización o un nuevo módulo. Durante nuestro análisis, nunca recibimos tales comandos del servidor, pero notamos que al intentar abrir un archivo PDF en el navegador, repite la solicitud a la dirección anterior y luego descarga una biblioteca nativa. Para simular un ataque, decidimos usar esta función del navegador UC, la capacidad de abrir archivos PDF usando una biblioteca nativa, no presente en el archivo APK, pero descargable desde Internet. Técnicamente, UC Browser puede descargar algo sin el permiso de un usuario cuando se le da una respuesta adecuada a una solicitud enviada al inicio. Pero para esto necesitamos estudiar el protocolo de interacción con el servidor con más detalle, por lo que pensamos que era más fácil conectar y editar la respuesta y luego reemplazar la biblioteca necesaria para abrir archivos PDF con algo diferente.

Entonces, cuando un usuario desea abrir un archivo PDF directamente en el navegador, el tráfico puede contener las siguientes solicitudes:



Primero, hay una solicitud POST a puds.ucweb.com/upgrade/index.xhtml , luego se descarga la biblioteca comprimida para ver archivos PDF y documentos de Office. Lógicamente, podemos suponer que la primera solicitud envía información sobre el sistema (al menos la arquitectura, porque el servidor necesita seleccionar una biblioteca apropiada), y el servidor responde con cierta información sobre la biblioteca que necesita ser descargada, como su dirección y tal vez algo más. El problema es que esta solicitud está encriptada.

Fragmento de solicitud

Fragmento de respuesta






La biblioteca está comprimida en un archivo ZIP y no está encriptada.



Buscando código de descifrado de tráfico


Intentemos descifrar la respuesta del servidor. Eche un vistazo al código de la clase com.uc.deployment.UpgradeDeployService : desde el método onStartCommand , navegamos a com.uc.deployment.bx y luego a 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 aquí es donde se realiza la solicitud POST. Eche un vistazo a la matriz de 16 bytes que contiene: 0x5F, 0, 0x1F, -50 (= 0xCE). Los valores son los mismos que en la solicitud anterior.

La misma clase contiene una clase anidada con otro método interesante:

  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 recibe una entrada de una matriz de bytes y comprueba si el byte cero es 0x60, o si el tercer byte es 0xD0 y si el segundo byte es 1, 11 o 0x1F. Consulte la respuesta del servidor: el byte cero es 0x60, el segundo byte es 0x1F, el tercer byte es 0x60. Se parece a lo que necesitamos. A juzgar por las cadenas ("up_decrypt", por ejemplo), se supone que se llama a un método aquí para descifrar la respuesta del servidor. Ahora veamos el método gj. Tenga en cuenta que el primer argumento es un byte en el desplazamiento 2 (es decir, 0x1F en nuestro caso), y el segundo es la respuesta del servidor sin los primeros 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, está seleccionando el algoritmo de descifrado, y el byte que en nuestro caso es igual a 0x1F indica una de las tres opciones posibles.

Volvamos al análisis de código. Después de un par de saltos, llegamos al método con el nombre revelador, decryptBytesByKey . Ahora, dos bytes más se separan de nuestra respuesta y forman una cadena. Está claro que así es como se selecciona la clave para descifrar el mensaje.

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

Avance un poco, tenga en cuenta que en esta etapa, solo es el identificador de la clave, no la clave en sí. La selección de teclas será un poco más complicada.

En el siguiente método, se agregan dos parámetros más a los existentes, por lo que obtenemos un total de cuatro. El número mágico 16, el identificador de clave, los datos cifrados y una cadena se agrega allí por alguna razón (vacía en nuestro caso).

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

Después de una serie de saltos, vemos el método staticBinarySafeDecryptNoB64 de la interfaz com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . El código de la aplicación principal no tiene clases que implementen esta interfaz. Esta clase está contenida en el archivo lib / armeabi-v7a / libsgmain.so , que no es realmente .SO, sino más bien .JAR. El método que nos interesa se implementa de la siguiente manera:

 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); } //... } 

Aquí, nuestra lista de parámetros se complementa con dos enteros más: 2 y 0. Aparentemente, 2 significa descifrado, como en el método doFinal de la clase de sistema javax.crypto.Cipher . Luego, esta información se transmite a un determinado enrutador junto con el número 10601, que aparentemente es el número de comando.

Después de la próxima cadena de saltos, encontramos una clase que implementa la interfaz RouterComponent y el 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); } } 

También está la clase JNIC Library , donde se declara el método nativo doCommandNative :

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

Entonces, necesitamos encontrar el método doCommandNative en el código nativo. Ahí es donde comienza la diversión.

Código de máquina de ofuscación


Hay una biblioteca nativa en el archivo libsgmain.so (que en realidad es un archivo .JAR y, como dijimos anteriormente, implementa algunas interfaces relacionadas con el cifrado): libsgmainso-6.4.36.so . Lo cargamos en IDA y obtenemos un montón de diálogos con mensajes de error. El problema es que la tabla de encabezado de sección no es válida. Esto se hace a propósito para complicar el análisis.



Pero realmente no lo necesitamos de todos modos. La tabla de encabezado del programa es suficiente para cargar correctamente el archivo ELF y analizarlo. Entonces simplemente eliminamos la tabla de encabezado de sección, anulando los campos correspondientes en el encabezado.



Luego abrimos el archivo en IDA nuevamente.

Tenemos dos formas de decirle a la máquina virtual Java exactamente dónde la biblioteca nativa contiene la implementación del método declarado como nativo en el código Java. El primero es darle un nombre como este: Java_package_name_ClassName_methodName . El segundo es registrarlo al cargar la biblioteca (en la función JNI_OnLoad) llamando a la función RegisterNatives. En nuestro caso, si usa el primer método, el nombre debería ser así: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . La lista de funciones exportadas no contiene este nombre, lo que significa que debemos buscar RegisterNatives. Por lo tanto, vamos a la función JNI_OnLoad y vemos lo siguiente:



Que esta pasando aqui A primera vista, el principio y el final de la función son típicos de la arquitectura ARM. La primera instrucción empuja el contenido de los registros que la función usará a la pila (en este caso, R0, R1 y R2), así como el contenido del registro LR con la dirección de retorno de la función. La última instrucción restaura los registros guardados y coloca la dirección de retorno en el registro de la PC, regresando así de la función. Pero si observamos más de cerca, podemos notar que la penúltima instrucción cambia la dirección de retorno, almacenada en la pila. Calculemos qué será cuando se ejecute el código. La dirección 0xB130 se carga en R1, tiene 5 restados de ella, luego se mueve a R0 y recibe una adición de 0x10. Al final, es igual a 0xB13B. Por lo tanto, IDA cree que la instrucción final realiza un retorno de función normal, mientras que, de hecho, realiza una transferencia a la dirección calculada 0xB13B.

Ahora permítanos recordarle que los procesadores ARM tienen dos modos y dos conjuntos de instrucciones: ARM y Thumb. El bit de orden inferior de la dirección determina qué conjunto de instrucciones usará el procesador. Es decir, la dirección es en realidad 0xB13A, mientras que el valor en el bit de orden inferior indica el modo Thumb.
Se agrega un “adaptador” similar y algo de basura semántica al comienzo de cada función en esta biblioteca. Pero no nos detendremos en ellos en detalle. Solo recuerde que el comienzo real de casi todas las funciones está un poco más allá.

Como no existe una transición explícita a 0xB13A en el código, IDA no puede reconocer que hay código allí. Por la misma razón, no reconoce la mayor parte del código en la biblioteca como código, lo que hace que el análisis sea un poco más complicado. Entonces, le decimos a IDA que hay código, y esto es lo que sucede:



A partir de 0xB144, tenemos claramente la tabla. Pero ¿qué pasa con sub_494C?



Al llamar a esta función en el registro LR, obtenemos la dirección de la tabla mencionada anteriormente (0xB144). R0 contiene el índice en esta tabla. Es decir, tomamos el valor de la tabla, lo agregamos a LR y obtenemos la dirección a la que debemos ir. Intentemos calcularlo: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Navegamos a esta dirección y vemos un par de instrucciones útiles, luego vamos a 0xB140:



Ahora habrá una transición en el desplazamiento con el índice 0x20 de la tabla.
A juzgar por el tamaño de la tabla, habrá muchas transiciones en el código. Por lo tanto, nos gustaría lidiar con esto automáticamente y evitar calcular direcciones manualmente. Por lo tanto, los scripts y la capacidad de parchear código en IDA vienen a nuestro rescate:

 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" 

Ponemos el cursor en la cadena 0xB26A, ejecutamos el script y vemos la transición a 0xB4B0:



Nuevamente, IDA no reconoce este lugar como código. Lo ayudamos y vemos otra estructura allí:



Las instrucciones que van después de BLX no parecen muy significativas; son más como una especie de compensación. Nos fijamos en sub_4964:



De hecho, toma DWORD en la dirección de LR, lo agrega a esta dirección, luego toma el valor en la dirección resultante y lo almacena en la pila. Además, agrega 4 a LR para saltar este mismo desplazamiento después de regresar de la función. Luego, el comando POP {R1} toma el valor resultante de la pila. Mirando lo que se encuentra en la dirección 0xB4BA + 0xEA = 0xB5A4, podemos ver algo similar a la tabla de direcciones:



Para parchear esta estructura, necesitamos obtener dos parámetros del código: el desplazamiento y el número de registro donde queremos empujar el resultado. Tendremos que preparar un código de antemano para cada posible registro.

 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" 

Ponemos el cursor al comienzo de la estructura que queremos reemplazar (es decir, 0xB4B2) y ejecutamos el script:



Además de las estructuras ya mencionadas, el código incluye lo siguiente:



Como en el caso anterior, hay un desplazamiento después de la instrucción BLX:



Tomamos el desplazamiento en la dirección de LR, lo agregamos a LR y navegamos allí. 0x72044 + 0xC = 0x72050. El script para esta estructura es bastante simple:

 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" 

El resultado de ejecutar el script:



Después de parchear todo en esta función, podemos señalar la IDA a su comienzo real. Recopilará todo el código de función pieza por pieza y podremos descompilarlo usando HexRays.

Descifrar las cuerdas


Aprendimos a manejar la ofuscación del código de máquina en la biblioteca libsgmainso-6.4.36.so del navegador UC y obtuvimos el código de función 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; } 

Veamos las siguientes cadenas:

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

Está bastante claro que la función sub_73E24 descifra el nombre de la clase. Los parámetros de esta función contienen un puntero a los datos que se parecen a los cifrados, un tipo de búfer y un número. Obviamente, una cadena descifrada estará en el búfer después de una llamada a la función, ya que el búfer va a la función FindClass , que recibe el mismo nombre de clase que el segundo parámetro. Entonces el número es el tamaño del búfer o la longitud de la cadena. Intentemos descifrar el nombre de la clase. Debe indicar si vamos en la dirección correcta. Echemos un vistazo más de cerca a lo que sucede en sub_73E24 .

 int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size) { int v4; // r6 int v7; // r11 int v8; // r9 int v9; // r4 size_t v10; // r5 int v11; // r0 struc_1 v13; // [sp+0h] [bp-30h] int v14; // [sp+1Ch] [bp-14h] int v15; // [sp+20h] [bp-10h] v4 = 0; v15 = *(_DWORD *)off_8AC00; v14 = 0; v7 = sub_7AF78(17); v8 = sub_7AF78(size); if ( !v7 ) { v9 = 0; goto LABEL_12; } (*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16); if ( !v8 ) { LABEL_9: v4 = 0; goto LABEL_10; } v4 = 0; if ( !in ) { LABEL_10: v9 = 0; goto LABEL_11; } v9 = 0; if ( out ) { memset(out, 0, size); v10 = size - 1; (*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10); memset(&v13, 0, 0x14u); v13.field_4 = 3; v13.field_10 = v7; v13.field_14 = v8; v11 = sub_6115C(&v13, &v14); v9 = v11; if ( v11 ) { if ( *(_DWORD *)(v11 + 4) == v10 ) { qmemcpy(out, *(const void **)v11, v10); v4 = *(_DWORD *)(v9 + 4); } else { v4 = 0; } goto LABEL_11; } goto LABEL_9; } LABEL_11: sub_7B148(v7); LABEL_12: if ( v8 ) sub_7B148(v8); if ( v9 ) sub_7B148(v9); return v4; } 

La función sub_7AF78 crea una instancia de contenedor para conjuntos de bytes del tamaño especificado (no nos centraremos en ellos en detalle). Aquí se crean dos contenedores de este tipo: uno contiene la cadena " DcO / lcK + h? M3c * q @ " (es fácil adivinar que esta es la clave), el otro tiene los datos cifrados. Ambos objetos se colocan en una determinada estructura, que se transfiere a la función sub_6115C . También podemos notar que esta estructura contiene un campo con un valor de 3. Veamos qué sucede después.

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

El campo con el valor 3 previamente asignado se cambia como parámetro de cambio. Echemos un vistazo al caso 3: los parámetros que la función anterior agregada a la estructura (es decir, la clave y los datos cifrados) se transfieren a la función sub_6364C. Si observamos de cerca sub_6364C, podemos reconocer el algoritmo RC4.

Entonces tenemos un algoritmo y una clave. Intentemos descifrar el nombre de la clase. Esto es lo que tenemos: com / taobao / wireless / security / adapter / JNICLibrary. Brillante! Estamos en el buen camino.

Árbol de comando


Ahora necesitamos encontrar la llamada a RegisterNatives , que nos indicará la función doCommandNative . Entonces miramos a través de las funciones llamadas desde JNI_OnLoad , y lo encontramos en 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; } 

Y, de hecho, un método nativo con el nombre doCommandNative está registrado aquí. Ahora sabemos su dirección. Echemos un vistazo a lo que hace.

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

El nombre sugiere que es el punto de entrada para todas las funciones que los desarrolladores transfirieron a la biblioteca nativa. Estamos específicamente interesados ​​en la función número 10601.

Del código, podemos ver que el número de comando nos da tres números: comando / 10000, comando% 10000/100 y comando% 10 (en nuestro caso, 1, 6 y 1). Estos tres números, así como el puntero a JNIEnv y los argumentos transferidos a la función, forman una estructura y se transmiten. Con estos tres números (los denominaremos N1, N2 y N3), se construye un árbol de comandos. Algo como esto:



El árbol se crea dinámicamente en JNI_OnLoad. Tres números codifican la ruta en el árbol. Cada hoja del árbol contiene la dirección xorred de la función correspondiente. La clave está en el nodo principal. Es bastante fácil encontrar un lugar en el código donde la función que necesitamos se agrega al árbol si entendemos todas las estructuras allí (no pasaremos tiempo describiéndolas en este artículo).

Más ofuscación


Tenemos la dirección de la función que se supone que descifra el tráfico: 0x5F1AC. Pero todavía es demasiado pronto para relajarse: los desarrolladores del navegador UC tienen otra sorpresa para nosotros.

Después de recibir los parámetros de una matriz en el código Java, vamos a la función en 0x4D070. Otro tipo de ofuscación de código ya está esperando.

Luego empujamos dos índices en R7 y R4:



El primer índice se mueve a R11:



Usamos este índice para obtener la dirección de la tabla:



Después de transferir a la primera dirección, usamos el segundo índice de R4. La tabla contiene 230 elementos.

¿Qué hacemos con eso? Podríamos decirle a la AIF que es una especie de cambio: Editar -> Otro -> Especificar modismo de cambio.



El código resultante es horrendo.Sin embargo, podemos ver la llamada a la función familiar sub_6115C en sus enredos:



estaba el parámetro de cambio con el descifrado RC4 en el caso 3. En este caso, la estructura que se transfiere a la función se llena con los parámetros transferidos a doCommandNative. Recordamos que teníamos magicInt con valor 16. Observamos el caso correspondiente y después de varias transiciones, encontramos el código que nos ayuda a identificar el algoritmo.



Es AES!

Tenemos un algoritmo y solo necesitamos obtener sus parámetros, como el modo, la clave y (posiblemente) el vector de inicialización (su presencia depende del modo de operación del algoritmo AES). La estructura que los contiene debe crearse en algún lugar antes de llamar a la función sub_6115C. Pero dado que esta parte del código está particularmente bien ofuscada, decidimos parchear el código para que todos los parámetros de la función de descifrado se puedan volcar en un archivo.

Parche


Si no desea escribir manualmente todo el código de parche en el lenguaje ensamblador, puede ejecutar Android Studio, codificar una función que recibe los mismos parámetros que nuestra función de descifrado y escribe en el archivo, luego copie el código resultante generado por el compilador

Nuestros buenos amigos del equipo de UC Browser también "aseguraron" la conveniencia de agregar código. Recordamos que tenemos un código basura al comienzo de cada función, que puede reemplazarse fácilmente con cualquier otro código. Muy conveniente :) Sin embargo, no hay suficiente espacio al comienzo de la función de destino para el código que guarda todos los parámetros en un archivo. Tuvimos que dividirlo en partes y usar los bloques de basura de las funciones vecinas. Tenemos cuatro partes en total.
Primera parte:



Los primeros cuatro parámetros de función en la arquitectura ARM se indican en los registros R0-R3, mientras que el resto, si lo hay, pasa por la pila. El registro LR indica la dirección de retorno. Necesitamos guardar todos estos datos para que la función pueda funcionar después de volcar sus parámetros. También necesitamos guardar todos los registros que usamos en el proceso, por lo que usamos PUSH.W {R0-R10, LR}. En R7, obtenemos la dirección de la lista de parámetros transferidos a la función a través de la pila.

Usando la función fopen, abrimos el archivo / data / local / tmp / aes en el modo "ab" (para poder agregar algo). Luego cargamos la dirección del nombre del archivo en R0 y la dirección de la cadena que indica el modo en R1. Aquí es donde termina el código basura, por lo que navegamos a la siguiente función. Como queremos que continúe funcionando, colocamos la transición al código de función real al principio, antes de la basura, y reemplazamos la basura con el resto del parche.



Luego llamamos a fopen.

Los primeros tres parámetros de la función aes son del tipo int. Como empujamos los registros a la pila al principio, simplemente podemos transferir sus direcciones en la pila a la función fwrite.



A continuación, tenemos tres estructuras que indican el tamaño de los datos y contienen un puntero a los datos para la clave, el vector de inicialización y los datos cifrados.



Al final, cerramos el archivo, restauramos los registros y devolvemos el control a la función aes real.
Compilamos el archivo APK con la biblioteca parcheada, lo firmamos, lo descargamos en un dispositivo o emulador y lo ejecutamos. Ahora vemos que el volcado se ha creado con muchos datos. El navegador no solo descifra el tráfico, sino también otros datos, y todo el descifrado se realiza a través de esta función. Por alguna razón, no vemos los datos que necesitamos, y la solicitud que esperamos no es visible en el tráfico. Saltemos la espera hasta que UC Browser tenga la oportunidad de hacer esta solicitud y tome la respuesta encriptada obtenida anteriormente del servidor para parchear la aplicación nuevamente. Agregaremos el descifrado a onCreate de la actividad 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 

Lo compilamos, firmamos, instalamos y ejecutamos. Por lo tanto, obtenemos una NullPointerException ya que el método devuelve un valor nulo.

Después de analizar más el código, encontramos una función con cadenas bastante interesantes: "META-INF /" y ".RSA". Parece que la aplicación verifica su certificado, o incluso genera claves a partir de él. Realmente no queremos profundizar en lo que está sucediendo con el certificado, así que solo demos el certificado correcto. Aplicaremos un parche a la cadena encriptada, por lo que en lugar de "META-INF /" obtenemos "BLABLINF /", creamos una carpeta con este nombre en el archivo APK y guardamos el certificado del navegador.

Lo compilamos, firmamos, instalamos y ejecutamos. Bingo! Tenemos la llave!

Mitm


Ahora tenemos la clave y un vector de inicialización igual. Intentemos descifrar la respuesta del servidor en el modo CBC.



Vemos la URL del archivo, algo así como MD5, "extract_unzipsize" y un número. Déjanos comprobarlo. El MD5 del archivo es el mismo; El tamaño de la biblioteca descomprimida es el mismo. Ahora intentaremos parchear esta biblioteca y transmitirla al navegador. Para mostrar que nuestra biblioteca parcheada se ha cargado, crearemos una intención para crear el mensaje de texto "PWNED!" Reemplazaremos dos respuestas del servidor: puds.ucweb.com/upgrade/index.xhtml y la que solicita la descarga del archivo. En el primero, sustituimos MD5 (el tamaño sigue siendo el mismo después de descomprimir); En el segundo, enviamos el archivo con la biblioteca parcheada.

El navegador realiza varios intentos de descargar el archivo, lo que genera un error. Aparentemente, algo sospechoso está sucediendo allí. Analizando este formato extraño, descubrimos que el servidor también transmite el tamaño del archivo:



está codificado con LEB128. El parche cambia ligeramente el tamaño de la biblioteca comprimida, por lo que el navegador decidió que el archivo se rompió con la descarga y mostró un error después de varios intentos.
Así que arreglamos el tamaño del archivo y ... ¡voilá! :) Ver el resultado en el video.


Consecuencias y respuesta del desarrollador.


Del mismo modo, los piratas informáticos pueden utilizar esta característica insegura del navegador UC para distribuir y lanzar bibliotecas maliciosas. Estas bibliotecas funcionarán en el contexto del navegador, dando como resultado privilegios completos del sistema que tiene el navegador. Esto les otorga un reinado gratuito para mostrar ventanas de phishing, así como acceso a los archivos de trabajo del navegador, incluidos inicios de sesión, contraseñas y cookies en la base de datos.

Contactamos a los desarrolladores de UC Browser y les informamos sobre el problema que habíamos encontrado, tratamos de señalar la vulnerabilidad y su peligro, pero se negaron a discutir el asunto. Mientras tanto, el navegador con la función peligrosa permaneció a la vista. Aunque tan pronto como revelamos los detalles de la vulnerabilidad, era imposible ignorarla como antes. El 27 de marzo se lanzó una nueva versión del UC Browser 10.10.9.1193, que accedió al servidor a través de HTTPS puds.ucweb.com/upgrade/index.xhtml. Además, entre la "corrección de errores" y el momento en que escribimos este artículo, un intento de abrir un PDF en el navegador resultó en un mensaje de error con el texto "Vaya, algo está mal". No se solicitó al servidor al intentar abrir el archivo PDF. Sin embargo, esto se realizó al inicio, lo que es una señal de que la capacidad de descargar el código ejecutable en violación de las políticas de Google Play todavía está presente.

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


All Articles