Buscando vulnerabilidades en el navegador UC


Introduccion


A fines de marzo, informamos que descubrimos una capacidad oculta para descargar y ejecutar código no verificado en el navegador UC. Hoy analizaremos en detalle cómo se produce esta descarga y cómo los hackers pueden usarla para sus propios fines.

Hace algún tiempo, UC Browser se publicitó y distribuyó de manera muy agresiva: se instaló en los dispositivos de los usuarios que usan malware, distribuido desde varios sitios bajo la apariencia de archivos de video (es decir, los usuarios pensaron que descargaron, por ejemplo, un clip porno, pero en cambio recibieron un APK con esto navegador), utilizaba pancartas aterradoras con mensajes que decían que el navegador está desactualizado, vulnerable y cosas así. El grupo oficial de UC Browser en VK tiene un tema en el que los usuarios pueden quejarse de publicidad injusta, hay muchos ejemplos. En 2016, incluso hubo publicidad en video en ruso (sí, anuncios de un navegador que bloquea los anuncios).

Al momento de escribir este artículo, UC Browser tiene más de 500,000,000 de instalaciones en Google Play. Esto es impresionante: solo Google Chrome tiene más. Entre las revisiones, puede ver muchas quejas sobre publicidad y redirecciones a algunas aplicaciones en Google Play. Esta fue la razón del estudio: decidimos ver si UC Browser está haciendo algo malo. ¡Y resultó que lo estaba haciendo!

El código de la aplicación reveló la capacidad de descargar y ejecutar código ejecutable, lo que contradice las reglas para publicar aplicaciones en Google Play. Además del hecho de que UC Browser descarga el código ejecutable, lo hace inseguro, lo que puede usarse para llevar a cabo un ataque MitM. Veamos si logramos llevar a cabo tal ataque.

Todo lo que está escrito a continuación es relevante para la versión de UC Browser que estaba presente en Google Play en el momento del estudio:

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

Vector de ataque


En el manifiesto del navegador UC, puede encontrar un servicio llamado 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 notar en el tráfico algún tiempo después del inicio. En respuesta, puede recibir un comando para descargar una actualización o un nuevo módulo. En el proceso de análisis, el servidor no dio dichos comandos, pero notamos que al intentar abrir el PDF en el navegador, realiza una segunda solicitud en la dirección anterior, luego de lo cual descarga la biblioteca nativa. Para llevar a cabo el ataque, decidimos usar esta función del navegador UC: la capacidad de abrir PDF usando una biblioteca nativa, que no está en el APK y que, si es necesario, se descarga de Internet. Vale la pena señalar que, teóricamente, UC Browser puede verse obligado a descargar algo sin la interacción del usuario, si da una respuesta correctamente formada a una solicitud que se ejecuta después de que se inicia el navegador. Pero para esto necesitamos estudiar el protocolo de interacción con el servidor con más detalle, por lo que decidimos que era más fácil editar la respuesta interceptada y reemplazar la biblioteca para trabajar con PDF.

Entonces, cuando un usuario quiere abrir un PDF directamente en un navegador, las siguientes solicitudes se pueden ver en el tráfico:



Primero viene una solicitud POST a puds.ucweb.com/upgrade/index.xhtml , después de lo cual
Descargue el archivo con la biblioteca para ver PDF y formatos de oficina. Es lógico suponer que en la primera solicitud se transmite información sobre el sistema (al menos la arquitectura para proporcionar la biblioteca necesaria), y en respuesta a esto, el navegador recibe cierta información sobre la biblioteca que debe descargarse: la dirección y, posiblemente, otra cosa. El problema es que esta solicitud está encriptada.

Fragmento de solicitud

Fragmento de respuesta







La biblioteca en sí está empaquetada en ZIP y no está encriptada.



Buscar código de descifrado de tráfico



Intentemos descifrar la respuesta del servidor. Observamos el código de la clase com.uc.deployment.UpgradeDeployService : desde el método onStartCommand, vaya a com.uc.deployment.bx , y desde allí 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); } 

Vemos aquí la formación de la solicitud POST. Llamamos la atención sobre la creación de una matriz de 16 bytes y su relleno: 0x5F, 0, 0x1F, -50 (= 0xCE). Coincide con lo que vimos en la solicitud anterior.

En la misma clase, puede observar una clase anidada en la que hay 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"); } } 

El método recibe una matriz de bytes como entrada y comprueba que el byte cero es 0x60 o el tercer byte es 0xD0 y el segundo byte es 1, 11 o 0x1F. Observamos la respuesta del servidor: cero byte - 0x60, segundo - 0x1F, tercero - 0x60. Parece lo que necesitamos. A juzgar por las líneas ("up_decrypt", por ejemplo), se debe llamar a un método aquí que descifre la respuesta del servidor.
Pasamos al método gj . Tenga en cuenta que el byte en el desplazamiento 2 (es decir, 0x1F en nuestro caso) se transfiere a él como primer argumento, y la respuesta del servidor sin
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, hay una opción de algoritmo de descifrado, y el mismo byte, que en nuestro
case es 0x1F, denota una de las tres opciones posibles.

Seguimos analizando el código. Después de un par de saltos, entramos en un método con el nombre parlante decryptBytesByKey .

Aquí, dos bytes más se separan de nuestra respuesta, y se obtiene una cadena de ellos. Está claro que de esta manera 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; } 

Mirando hacia el futuro, notamos que en esta etapa la clave aún no se obtiene, sino solo su "identificador". Obtener la llave es un poco más complicado.

En el siguiente método, se agregan dos más a los parámetros existentes, y hay cuatro de ellos: el número mágico 16, el identificador de clave, los datos cifrados y una cadena incomprensible (en nuestro caso, vacía).

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

Después de una serie de transiciones, llegamos al método staticBinarySafeDecryptNoB64 de la interfaz com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent . No hay clases en el código de la aplicación principal que implementen esta interfaz. Dicha clase está en el archivo lib / armeabi-v7a / libsgmain.so , que en realidad no es .so, sino .jar. El método de interés para nosotros 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. A juzgar por
para todos, 2 significa descifrado, como en el método doFinal de la clase de sistema javax.crypto.Cipher . Y todo esto se transfiere a un determinado enrutador con el número 10601; aparentemente, este es el número de comando.

Después de la siguiente cadena de transición, encontramos una clase que implementa la interfaz IRouterComponent 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); } } 

Y también la clase JNICLibrary , en la que 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. Y entonces comienza la diversión.

Ofuscación de código de máquina


Hay una biblioteca nativa en el archivo libsgmain.so (que en realidad es .jar y en la que encontramos una pequeña implementación de algunas interfaces de cifrado un poco más altas): libsgmainso-6.4.36.so . Ábralo en la IDA y obtenga un montón de cuadros de diálogo con errores. El problema es que la tabla de encabezado de sección no es válida. Esto se hace específicamente para complicar el análisis.



Pero tampoco es necesario: para cargar correctamente el archivo ELF y analizarlo, la tabla de encabezado del programa es suficiente. Por lo tanto, simplemente eliminamos la tabla de secciones, anulando los campos correspondientes en el encabezado.



Nuevamente abra el archivo en la IDA.

Hay dos formas de decirle a la máquina virtual Java exactamente en qué parte de la biblioteca nativa se encuentra la implementación del método declarado en el código Java como nativo. El primero es darle un nombre de la forma Java_package_name_ClassName_Method_name .

El segundo es registrarlo al cargar la biblioteca (en la función JNI_OnLoad )
llamando a la función RegisterNatives .

En nuestro caso, si utiliza el primer método, el nombre debería ser así: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative .

Entre las funciones exportadas, no existe ninguna, por lo que debe buscar la llamada RegisterNatives .
Vamos a la función JNI_OnLoad y vemos la siguiente imagen:



¿Qué está pasando aquí? A primera vista, el principio y el final de la función son típicos de la arquitectura ARM. La primera instrucción en la pila almacena el contenido de los registros que la función utilizará en su trabajo (en este caso, R0, R1 y R2), así como el contenido del registro LR, en el que se encuentra la dirección de retorno de la función. Con la última instrucción, los registros guardados se restauran y la dirección de retorno se coloca inmediatamente en el registro de la PC, lo que devuelve la función. Pero si observa detenidamente, notará que la penúltima instrucción cambia la dirección de retorno almacenada en la pila. Calculamos lo que será después
ejecución de código Una cierta dirección 0xB130 se carga en R1, 5 se resta de ella, luego se transfiere a R0 y se le agrega 0x10. Resulta 0xB13B. Por lo tanto, la IDA piensa que en la última instrucción hay un retorno normal de la función, pero de hecho hay una transición a la dirección calculada 0xB13B.

Vale la pena recordar que los procesadores ARM tienen dos modos y dos conjuntos de instrucciones: ARM y Thumb. El bit menos significativo de la dirección le dice al procesador qué conjunto de instrucciones se está utilizando. Es decir, la dirección es en realidad 0xB13A, y la unidad en el bit bajo indica el modo Thumb.

Al comienzo de cada función en esta biblioteca se agrega un "adaptador" similar y
código basura Además, no nos detendremos en ellos en detalle: solo recordamos
que el comienzo real de casi todas las funciones está un poco más allá.

Como no hay una transición explícita a 0xB13A en el código, la IDA misma no reconoció que el código estaba en este lugar. Por la misma razón, no reconoce la mayor parte del código en la biblioteca como código, lo que dificulta un poco el análisis. Le decimos a la AIF que el código está aquí, y aquí está el resultado:



En 0xB144, la tabla comienza claramente. ¿Qué pasa con sub_494C?



Cuando se llama a esta función en el registro LR, obtenemos la dirección de la tabla mencionada anteriormente (0xB144). En R0, el índice en esta tabla. Es decir, se toma un valor de la tabla, se agrega a LR y se obtiene
dirección para ir. Intentemos calcularlo: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Vamos a la dirección recibida y vemos solo un par de instrucciones útiles y nuevamente la transición a 0xB140:



Ahora habrá un cambio por desplazamiento con el índice 0x20 de la tabla.

A juzgar por el tamaño de la tabla, hay muchas transiciones en el código. Se plantea la cuestión de si es posible tratar esto de manera más automática, sin calcular direcciones manualmente. Y los scripts y la capacidad de parchear el código en la IDA nos ayudan:

 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 línea 0xB26A, ejecutamos el script y vemos la transición a 0xB4B0:



La IDA nuevamente no reconoció este sitio como un código. La ayudamos y vemos otra construcción allí:



Las instrucciones después de BLX no parecen muy significativas, es más como un tipo de sesgo. Nos fijamos en sub_4964:



De hecho, aquí la palabra clave se toma en la dirección que se encuentra en LR, se agrega a esta dirección, después de lo cual el valor en la dirección recibida se toma y se inserta en la pila. Además, se agrega 4 a LR, de modo que después de regresar de la función para saltar este mismo desplazamiento. Luego, el comando POP {R1} extrae el valor recibido de la pila. Si observa la dirección 0xB4BA + 0xEA = 0xB5A4, puede ver algo similar a la tabla de direcciones:



Para parchear este diseño, debe obtener dos parámetros del código: el desplazamiento y el número de registro en el que desea colocar el resultado. Para cada posible registro, deberá preparar un código de antemano.

 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 el cursor al comienzo de la construcción que queremos reemplazar - 0xB4B2 - y ejecutamos el script:



Además de las construcciones ya mencionadas en el código, aquí también están las siguientes:



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



Tomamos el desplazamiento a la dirección de LR, lo agregamos a LR y vamos allí. 0x72044 + 0xC = 0x72050. El guión para este diseño es muy 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 del guión:



Después de que todo esté parcheado en la función, puede apuntar el IDA a su comienzo real. Recopilará todo el código de función en partes, y se puede descompilar usando HexRays.

Decodificar 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 para la 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; } 

Considere cuidadosamente las siguientes líneas:

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

La función sub_73E24 descifra explícitamente el nombre de la clase. Como parámetros de esta función, se pasa un puntero a datos similares a los encriptados, un búfer y un número. , , . . 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 . Genial .


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

, doCommandNative . . , .

 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 .

¿Qué hacer al respecto? 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/451936/


All Articles