Buscamos vulnerabilidades en UC Browser

Buscamos vulnerabilidades en UC Browser

Introducción

A finales de marzo, encontramos una oportunidad oculta para cargar y ejecutar código no verificado en UC Browser. Hoy analizaremos en detalle cómo se realiza esta carga y cómo los hackers pueden utilizarla a su favor. informadoHace algún tiempo, UC Browser fue promocionado y distribuido de manera muy agresiva: se instalaba en los dispositivos de los usuarios mediante malware, se distribuía desde varios sitios haciéndose pasar por archivos de video (es decir, los usuarios pensaban que estaban descargando, por ejemplo, un video para adultos, pero en su lugar recibían un APK con este navegador), se utilizaban banners alarmantes con mensajes sobre que el navegador estaba desactualizado, era vulnerable y cosas por el estilo. En el grupo oficial de UC Browser en VK hay

un tema, en el que los usuarios pueden quejarse sobre publicidad engañosa; hay muchos ejemplos. En 2016, incluso hubo publicidad en videoen ruso (sí, publicidad del navegador que bloquea anuncios). Al momento de redactar el artículo, UC Browser había alcanzado más de 500,000,000 instalaciones en Google Play. Eso es impresionante; solo Google Chrome tiene más. Entre las reseñas se pueden ver bastantes quejas sobre la publicidad y redirecciones a algunas aplicaciones en Google Play. Esto fue lo que motivó la investigación: decidimos averiguar si UC Browser estaba haciendo algo malo. ¡Y resultó que sí! En el código de la aplicación se descubrió la posibilidad de cargar y ejecutar código ejecutable,

lo cual contradice las normas de publicación de aplicaciones

en Google Play. Además, UC Browser carga código ejecutable de manera insegura, lo que podría ser utilizado para llevar a cabo un ataque MitM. Veamos si podemos realizar tal ataque. Todo lo que se detalla a continuación es aplicable a la versión de UC Browser que estaba disponible en Google Play en el momento de la investigación: package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 del archivo APK: f5edb2243413c777172f6362876041eb0c3a928c

En el manifiesto de UC Browser se puede encontrar un servicio con un nombre elocuente

com.uc.deployment.UpgradeDeployService

Vector de ataque

En el manifiesto de UC Browser se puede encontrar un servicio con un nombre muy elocuente Al iniciar este servicio, el navegador realiza una solicitud POST a.

    puds.ucweb.com/upgrade/index.xhtml

Al iniciar este servicio, el navegador realiza una solicitud POST a puds.ucweb.com/upgrade/index.xhtml, que se puede notar en el tráfico después de un tiempo tras el inicio. En respuesta, puede recibir un comando para cargar alguna actualización o nuevo módulo. Durante el análisis, el servidor no emitió tales comandos, pero notamos que al intentar abrir un PDF en el navegador, este hace una nueva solicitud a la dirección indicada anteriormente, después de lo cual descarga una biblioteca nativa. Para llevar a cabo el ataque, decidimos aprovechar esta característica de UC Browser: la capacidad de abrir PDFs utilizando una biblioteca nativa que no está en el APK y que carga desde Internet cuando es necesario. Cabe destacar que teóricamente se puede obligar a UC Browser a descargar algo sin la interacción del usuario: si se proporciona una respuesta correctamente formada a la solicitud que se realiza tras iniciar el navegador. Pero para esto es necesario estudiar más a fondo el protocolo de interacción con el servidor, por lo que decidimos que era más sencillo editar la respuesta interceptada y sustituir la biblioteca para trabajar con PDFs.

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

Buscamos vulnerabilidades en UC Browser

Primero se envía una solicitud POST a puds.ucweb.com/upgrade/index.xhtml, después de la cual
se descarga un archivo comprimido con la biblioteca para ver PDFs y formatos de oficina. Es lógico suponer que en la primera solicitud se envía información sobre el sistema (como mínimo, la arquitectura, para proporcionar la biblioteca adecuada), y en respuesta a esto, el navegador recibe cierta información sobre la biblioteca que necesita descargar: la dirección y, posiblemente, algo más. El problema es que esta solicitud está cifrada.

Fragmento de la solicitud

Fragmento de la respuesta

Buscamos vulnerabilidades en UC Browser

Buscamos vulnerabilidades en UC Browser

La propia biblioteca está empaquetada en un ZIP y no está cifrada.

Buscamos vulnerabilidades en UC Browser

Búsqueda del código para descifrar el tráfico

Intentemos descifrar la respuesta del servidor. Miramos el código de la clase Al iniciar este servicio, el navegador realiza una solicitud POST a: del método onStartCommand pasamos a com.uc.deployment.b.x, y desde ahí a com.uc.browser.core.d.c.f.e:

    public final void e(l arg9) {
        int v4_5;
        String v3_1;
        byte[] v3;
        byte[] v1 = null;
        if(arg9 == null) {
            v3 = v1;
        }
        else {
            v3_1 = arg9.iGX.ipR;
            StringBuilder v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]producto:");
            v4.append(arg9.iGX.ipR);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]versión:");
            v4.append(arg9.iGX.iEn);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tipo_de_actualización:");
            v4.append(arg9.iGX.mMode);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]fuerza_flag:");
            v4.append(arg9.iGX.iEo);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]modo_silencioso:");
            v4.append(arg9.iGX.iDQ);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tipo_silencioso:");
            v4.append(arg9.iGX.iEr);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]estado_silencioso:");
            v4.append(arg9.iGX.iEp);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]archivo_silencioso:");
            v4.append(arg9.iGX.iEq);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]apk_md5:");
            v4.append(arg9.iGX.iEl);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tipo_de_descarga:");
            v4.append(arg9.mDownloadType);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]grupo_de_descarga:");
            v4.append(arg9.mDownloadGroup);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]ruta_de_descarga:");
            v4.append(arg9.iGH);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]versión_hijo_apolo:");
            v4.append(arg9.iGX.iEx);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]serie_apolo:");
            v4.append(arg9.iGX.iEw);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]arquitectura_cpu_apolo:");
            v4.append(arg9.iGX.iEt);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cpu_vfp3_apolo:");
            v4.append(arg9.iGX.iEv);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cpu_vfp_apolo:");
            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("]nombre_componente:");
                    v5.append(((au)v4_1).getName());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]nombre_ver_componente:");
                    v5.append(((au)v4_1).aDA());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]código_ver_componente:");
                    v5.append(((au)v4_1).gBl);
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]tipo_req_componente:");
                    v5.append(((au)v4_1).gBq);
                }
            }
 
            j v3_4 = new j();
            m.b(v3_4);
            h v4_2 = new h();
            m.b(v4_2);
            ay v5_1 = new ay();
            v3_4.hS("");
            v3_4.setImsi("");
            v3_4.hV("");
            v5_1.bPQ = v3_4;
            v5_1.bPP = v4_2;
            v5_1.yr(arg9.iGX.ipR);
            v5_1.gBF = arg9.iGX.mMode;
            v5_1.gBI = arg9.iGX.iEz;
            v3_2 = v5_1.gAr;
            c.aBh();
            v3_2.add(g.fs("os_ver", c.getRomInfo()));
            v3_2.add(g.fs("arquitectura_procesador", com.uc.b.a.a.c.getCpuArch()));
            v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
            String v4_3 = com.uc.b.a.a.c.Pd();
            v3_2.add(g.fs("cpu_vfp", v4_3));
            v3_2.add(g.fs("tipo_net", String.valueOf(com.uc.base.system.a.Jo())));
            v3_2.add(g.fs("host_origen", arg9.iGX.iEm));
            v3_2.add(g.fs("versión_plugin", arg9.iGX.iEn));
            v3_2.add(g.fs("lang_objetivo", arg9.iGX.iEs));
            v3_2.add(g.fs("arquitectura_cpu_vitamio", arg9.iGX.iEt));
            v3_2.add(g.fs("vfp_vitamio", arg9.iGX.iEu));
            v3_2.add(g.fs("vfp3_vitamio", arg9.iGX.iEv));
            v3_2.add(g.fs("versión_hijo_plugin", arg9.iGX.iEx));
            v3_2.add(g.fs("versión_serie", arg9.iGX.iEw));
            v3_2.add(g.fs("versión_hijo", r.aVw()));
            v3_2.add(g.fs("cur_version_md5", arg9.iGX.iEl));
            v3_2.add(g.fs("cur_version_signature", SystemHelper.getUCMSignature()));
            v3_2.add(g.fs("registro_actualización", i.bjt()));
            v3_2.add(g.fs("instalación_silenciosa", String.valueOf(arg9.iGX.iDQ)));
            v3_2.add(g.fs("estado_silencioso", String.valueOf(arg9.iGX.iEp)));
            v3_2.add(g.fs("archivo_silencioso", arg9.iGX.iEq));
            v3_2.add(g.fs("tipo_silencioso", String.valueOf(arg9.iGX.iEr)));
            v3_2.add(g.fs("arquitectura_cpu", com.uc.b.a.a.c.Pc()));
            v3_2.add(g.fs("conjunto_cpu", 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("nucleos_cpu", String.valueOf(com.uc.b.a.a.c.Jl())));
            v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
            v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
            c.aBh();
            v3_2.add(g.fs("rom_1", c.getRomInfo()));
            v4_5 = e.getScreenWidth();
            int v6 = e.getScreenHeight();
            StringBuilder v7 = new StringBuilder();
            v7.append(v4_5);
            v7.append("*");
            v7.append(v6);
            v3_2.add(g.fs("ss", v7.toString()));
            v3_2.add(g.fs("nivel_api", String.valueOf(Build$VERSION.SDK_INT)));
            v3_2.add(g.fs("lista_apk_uc", SystemHelper.getUCMobileApks()));
            Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
            while(v4_6.hasNext()) {
                Object v6_1 = v4_6.next();
                v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
            }
 
            v3 = v5_1.toByteArray();
        }
 
        if(v3 == null) {
            this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
            return;
        }
 
        v4_5 = this.iGY.iGw ? 0x1F : 0;
        if(v3 == null) {
        }
        else {
            v3 = g.i(v4_5, v3);
            if(v3 == null) {
            }
            else {
                v1 = new byte[v3.length + 16];
                byte[] v6_2 = new byte[16];
                Arrays.fill(v6_2, 0);
                v6_2[0] = 0x5F;
                v6_2[1] = 0;
                v6_2[2] = ((byte)v4_5);
                v6_2[3] = -50;
                System.arraycopy(v6_2, 0, v1, 0, 16);
                System.arraycopy(v3, 0, v1, 16, v3.length);
            }
        }
 
        if(v1 == null) {
            this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
            return;
        }
 
        if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
            this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
            return;
        }
 
        StringBuilder v0 = new StringBuilder("[");
        v0.append(arg9.iGX.ipR);
        v0.append("]url:");
        v0.append(this.iGY.mUpgradeUrl);
        com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
        v3_1 = this.iGY.mUpgradeUrl;
        com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
        v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
        n v3_5 = v0_2.uc(v3_1);
        m.b(v3_5, false);
        v3_5.setMethod("POST");
        v3_5.setBodyProvider(v1);
        v0_2.b(v3_5);
        this.iGY.iGI.a(arg9, "up_null", "yes", "success");
        this.iGY.iGI.b(arg9);
    }

Aquí vemos la formación de la solicitud POST. Prestamos atención a la creación de un array de 16 bytes y su llenado: 0x5F, 0, 0x1F, -50 (=0xCE). Coincide con lo que vimos en la solicitud anterior.

En esta misma clase, se puede notar una clase anidada que contiene 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 = g.j(arg11[2], v7);
                }
                if(v7 == null) {
                    goto label_57;
                }
                if(v5 != 0) {
                    v1_1 = g.P(v7);
                    goto label_57;
                }
                v1_1 = v7;
            }
        label_57:
            if(v1_1 == null) {
                v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
                return;
            }
            q v11 = g.b(arg10, v1_1);
            if(v11 == null) {
                v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
                return;
            }
            if(v0.iGY.iGt) {
                v0.d(arg10);
            }
            if(v0.iGY.iGo != null) {
                v0.iGY.iGo.a(0, ((o)v11));
            }
            if(v0.iGY.iGs) {
                v0.iGY.a(((o)v11));
                v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
                v0.iGY.iGI.a(v11);
                return;
            }
            v0.iGY.iGI.a(v11, "up_silent", "no", "success");
        }
    }

El método recibe un array de bytes y verifica que el byte cero sea igual a 0x60 o que el byte tres sea igual a 0xD0, y que el byte dos sea 1, 11 o 0x1F. Observamos la respuesta del servidor: el byte cero es 0x60, el segundo es 0x1F, el tercero es 0x60. Parece que es lo que necesitamos. Según las líneas ("up_decrypt", por ejemplo), debería llamarse un método que descifre la respuesta del servidor.
Pasemos al método g.j. Notamos que como primer argumento se pasa el byte en la posición 2 (o sea, 0x1F en nuestro caso), y como segundo, la respuesta del servidor sin
los primeros 16 bytes.

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

Es evidente que aquí se elige el algoritmo de descifrado, y ese mismo byte que en nuestro
En el caso de que sea 0x1F, representa una de las tres posibles variaciones.

Continuamos analizando el código. Después de un par de saltos, llegamos a un método con un nombre muy ilustrativo. decryptBytesByKey.

Aquí, de nuestra respuesta, se separan otros dos bytes, y de ellos se obtiene una cadena. 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 bytes
                    System.arraycopy(bytes, 0, prefix, 0, prefix.length);
                    String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Selección de la clave
                    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;
    }

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

En el siguiente método, se agregan dos parámetros más a los que ya existen, resultando en cuatro: el número mágico 16, el identificador de la 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. En el código principal de la aplicación no hay clases que implementen esta interfaz. Tal clase se encuentra en el archivo lib/armeabi-v7a/libsgmain.so, que en realidad no es un .so, sino un .jar. El método que nos interesa se implementa de la siguiente manera:

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

Aquí nuestra lista de parámetros se complementa con dos números enteros más: 2 y 0. Según parece,
2 significa descifrado, como en el método doFinal de la clase del sistema javax.crypto.Cipher. Y todo esto se transmite a un Router con el número 10601, que parece ser el número del comando.

Después de otra cadena de transiciones, encontramos la clase que implementa la interfaz IRouterComponent y un método doCommand:

paquete com.alibaba.wireless.security.mainplugin;

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

Y también la clase JNICLibrary, donde se declara el método nativo doCommandNative:

paquete com.taobao.wireless.security.adapter;
 
pública clase JNICLibrary {
    pública estática nativa Object doCommandNative(int arg0, Object[] arg1);
}

Así que necesitamos encontrar el método en el código nativo. doCommandNative. Y aquí comienza la diversión.

Ofuscación de código máquina

En el archivo libsgmain.so (que en realidad es .jar y en el que encontramos anteriormente la implementación de algunas interfaces relacionadas con el cifrado) hay una biblioteca nativa: libsgmainso-6.4.36.so. La abrimos en IDA y obtenemos un montón de ventanas de diálogo con errores. El problema es que la tabla de secciones (section header table) es inválida. Esto se hace intencionadamente para dificultar el análisis.

Buscamos vulnerabilidades en UC Browser

Pero no la necesitamos: para cargar y analizar correctamente un archivo ELF, es suficiente con la tabla de segmentos (program header table). Por lo tanto, simplemente eliminamos la tabla de secciones, poniendo a cero los campos correspondientes en el encabezado.

Buscamos vulnerabilidades en UC Browser

Abrimos nuevamente el archivo en IDA.

Hay dos formas de informar a la máquina virtual de Java dónde se encuentra exactamente en la biblioteca nativa la implementación del método declarado en el código Java como nativo. La primera es darle un nombre del tipo Java_nombre_paquete_NombreClase_nombreMétodo.

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

En nuestro caso, si utilizamos el primer método, el nombre debe ser así: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Entre las funciones exportadas no hay ninguna así, lo que significa que hay que buscar la llamada RegisterNatives.
Vamos a la función JNI_OnLoad y vemos esta imagen:

Buscamos vulnerabilidades en UC Browser

¿Qué está pasando aquí? A primera vista, el inicio y el final de la función son típicos para la arquitectura ARM. La primera instrucción guarda en la pila el contenido de los registros que la función usará en su trabajo (en este caso R0, R1 y R2), así como el contenido del registro LR, que contiene la dirección de retorno de la función. La última instrucción restaura los registros guardados, y la dirección de retorno se coloca inmediatamente en el registro PC: así es como se realiza el retorno de la función. Pero si observamos con atención, podemos notar que la penúltima instrucción modifica la dirección de retorno guardada en la pila. Calculamos cuál será después
de ejecutar el código. En R1 se carga cierta dirección 0xB130, se le resta 5, luego se transfiere a R0 y se le suma 0x10. Resulta 0xB13B. De esta manera, IDA piensa que en la última instrucción ocurre un retorno normal de la función, mientras que en realidad se produce un salto a la dirección calculada 0xB13B.

Aquí cabe 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 indica al procesador qué conjunto de instrucciones se está utilizando. Es decir, la dirección es en realidad 0xB13A, y la unidad en el bit menos significativo indica el modo Thumb.

Al inicio de cada función en esta biblioteca se ha añadido un «adaptador» similar y
código basura. No nos detendremos en detalles sobre ellos; simplemente recordamos
que el verdadero inicio de casi todas las funciones está un poco más allá.

Dado que en el código no hay un salto explícito a 0xB13A, IDA no reconoce por sí sola que en este lugar hay código. Por la misma razón, no reconoce la mayor parte del código en la biblioteca como tal, lo que dificulta un poco el análisis. Le decimos a IDA que aquí hay código, y esto es lo que obtenemos:

Buscamos vulnerabilidades en UC Browser

En 0xB144 claramente comienza la tabla. ¿Y qué hay en sub_494C?

Buscamos vulnerabilidades en UC Browser

Al llamar a esta función, en el registro LR obtendremos la dirección de la tabla mencionada anteriormente (0xB144). En R0 está el índice en esta tabla. Es decir, se toma el valor de la tabla, se suma a LR y se obtiene
la dirección a la que debemos ir. Intentemos calcularla: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Vamos a la dirección obtenida y vemos literalmente un par de instrucciones útiles y otra vez un salto a 0xB140:

Buscamos vulnerabilidades en UC Browser

Ahora habrá un salto en desplazamiento con el índice 0x20 de la tabla.

Dado el tamaño de la tabla, habrá muchos saltos como este en el código. Surge la pregunta, ¿podemos hacer esto de manera más automatizada, sin calcular direcciones manualmente? Aquí entran en juego los scripts y la posibilidad de parchear código en IDA:

def put_unconditional_branch(source, destination):
    offset = (destination - source - 4) >> 1
    if offset > 2097151 or offset  1023 or offset > 11) & 0x7ff)
        instruction2 = 0xb800 | (offset & 0x7ff)
        patch_word(source, instruction1)
        patch_word(source + 2, instruction2)
    else:
        instruction = 0xe000 | (offset & 0x7ff)
        patch_word(source, instruction)
 
 
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
    ea1 = ea + 2
    if get_wide_word(ea1) == 0xbf00: #NOP
        ea1 += 2
    if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
        index = get_wide_dword(get_operand_value(ea1, 1))
        print "índice =", hex(index)
        ea1 += 2
        if get_operand_type(ea1, 0) == 7:
            table = get_operand_value(ea1, 0) + 4
        elif get_operand_type(ea1, 1) == 2:
            table = get_operand_value(ea1, 1) + 4
        else:
            print "Tipo de operando incorrecto en", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
            table = None
        if table is None:
            print "No se pudo encontrar la tabla"
        else:
            print "tabla =", hex(table)
            offset = get_wide_dword(table + (index << 2))
            put_unconditional_branch(ea, table + offset)
    else:
        print "Código desconocido", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
    print "No se pudo detectar la primera instrucción"

Colocamos el cursor en la línea 0xB26A, ejecutamos el script y vemos un salto a 0xB4B0:

Buscamos vulnerabilidades en UC Browser

IDA nuevamente no reconoció esta sección como código. La ayudamos y vemos allí otra construcción:

Buscamos vulnerabilidades en UC Browser

Las instrucciones después de BLX no parecen muy coherentes, parece más un desplazamiento. Miramos en sub_4964:

Buscamos vulnerabilidades en UC Browser

Y de hecho, aquí se toma un dword de la dirección que está en LR, se suma a esta dirección, después se toma el valor de la dirección resultante y se coloca en la pila. También se suma 4 a LR, para saltar este desplazamiento después de regresar de la función. Después, el comando POP {R1} saca el valor obtenido de la pila. Si miramos lo que hay en la dirección 0xB4BA + 0xEA = 0xB5A4, podemos ver algo que se asemeja a una tabla de direcciones:

Buscamos vulnerabilidades en UC Browser

Para parchear esta construcción, será necesario obtener dos parámetros del código: el desplazamiento y el número del registro en el que se debe colocar el resultado. Para cada registro posible, será necesario preparar un fragmento de código con antelación.

patches = {}
patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0)
patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0)
patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0)
patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0)
patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0)
patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0)
patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0)
patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0)
patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0)
patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0)

ea = here()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
        and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
    if get_operand_type(ea + 4, 0) == 7:
        pop = get_bytes(ea + 12, 4, 0)
        if pop[1] == 'xbc':
            register = -1
            r = get_wide_byte(ea + 12)
            for i in range(8):
                if r == (1 <> 4
            if register in patches:
                address = get_wide_dword(ea + 8) + ea + 8
                for b in patches[register]:
                    patch_byte(ea, b)
                    ea += 1
                patch_dword(ea, address)
        else:
            print "Instrucción POP no encontrada"
    else:
        print "Tipo de operando incorrecto en +4:", get_operand_type(ea + 4, 0)
else:
    print "No se pudo detectar las primeras instrucciones"

Colocamos el cursor al comienzo de la construcción que queremos reemplazar — 0xB4B2 — y ejecutamos el script:

Buscamos vulnerabilidades en UC Browser

Además de las construcciones mencionadas, en el código también encontramos las siguientes:

Buscamos vulnerabilidades en UC Browser

Al igual que en el caso anterior, después de la instrucción BLX, sigue un desplazamiento:

Buscamos vulnerabilidades en UC Browser

Tomamos el desplazamiento de la dirección desde LR, lo sumamos a LR y vamos allí. 0x72044 + 0xC = 0x72050. El script para esta construcción es muy simple:

def put_unconditional_branch(source, destination):
    offset = (destination - source - 4) >> 1
    if offset > 2097151 or offset  1023 or offset > 11) & 0x7ff)
        instruction2 = 0xb800 | (offset & 0x7ff)
        patch_word(source, instruction1)
        patch_word(source + 2, instruction2)
    else:
        instruction = 0xe000 | (offset & 0x7ff)
        patch_word(source, instruction)


ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
    ea1 = ea + 6
    if get_wide_word(ea + 2) == 0xbf00: #NOP
        ea1 += 2
    offset = get_wide_dword(ea1)
    put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
    print "No se pudo detectar la primera instrucción"

Resultado de la ejecución del script:

Buscamos vulnerabilidades en UC Browser

Después de que la función ha sido parcheada, se puede indicar a IDA su verdadero inicio. Esta recolectará el código de la función en fragmentos, y podrá ser descompilado con HexRays.

Descifrando cadenas

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

Examinemos más de cerca las siguientes líneas:

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

En la función sub_73E24 se lleva a cabo claramente la desencriptación del nombre de la clase. Esta función recibe como parámetros un puntero a datos que parecen estar encriptados, un búfer y un número. Es evidente que después de la llamada a la función, el búfer contendrá la cadena desencriptada, ya que se pasa a la función FindClass, que acepta como segundo parámetro el nombre de la clase. Por lo tanto, el número es el tamaño del búfer o la longitud de la cadena. Intentemos desencriptar el nombre de la clase, debería indicarnos si vamos en la dirección correcta. Veamos más de cerca qué 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 un contenedor para arrays de bytes del tamaño especificado (no vamos a entrar en detalles sobre estos contenedores). Se crean dos de estos contenedores: uno para la cadena «DcO/lcK+h?m3c*q@» (no es difícil adivinar que esta es la clave), el otro para los datos cifrados. Luego ambos objetos se colocan en una estructura que se pasa a la función sub_6115C. También señalemos que en esta estructura hay un campo con el valor 3. Veamos qué sucede con esta estructura a continuación.

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

Se pasa un campo de la estructura como parámetro switch, al que previamente se le ha asignado el valor 3. Vamos a case 3: a la función sub_6364C se pasan parámetros de la estructura, que se guardaron allí en la función anterior, es decir, la clave y los datos encriptados. Si observamos atentamente sub_6364C, podemos reconocer el algoritmo RC4 en ella.

Tenemos el algoritmo y la clave. Intentemos descifrar el nombre de la clase. Esto es lo que obtuvimos: com/taobao/wireless/security/adapter/JNICLibrary. ¡Excelente! Estamos en el camino correcto.

Árbol de comandos

Ahora necesitamos encontrar la llamada RegisterNatives, que nos indicará la función doCommandNative. Revisamos las funciones llamadas desde JNI_OnLoad, y la 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 efectivamente, aquí se registra un método nativo llamado doCommandNative. Ahora conocemos su dirección. Veamos qué 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;
}

Por el nombre se puede inferir que aquí se encuentra el punto de entrada de todas las funciones que los desarrolladores decidieron trasladar a la biblioteca nativa. Nos interesa la función número 10601.

Por el código, se puede ver que del número de comando se generan tres números: command / 10000, command % 10000 / 100 y command % 10, es decir, en nuestro caso, 1, 6 y 1. Estos tres números, junto con un puntero a JNIEnv y los argumentos pasados a la función, se combinan en una estructura y se envían a continuación. Con los tres números obtenidos (los denominaremos N1, N2 y N3) se construye un árbol de comandos.

Algo como esto:

Buscamos vulnerabilidades en UC Browser

El árbol se llena dinámicamente en JNI_OnLoad.
Tres números codifican el camino en el árbol. Cada hoja del árbol contiene la dirección desofuscada de la función correspondiente. La clave está en el nodo padre. Encontrar el lugar en el código donde se añade la función que necesitamos al árbol no es muy complicado si se comprenden todas las estructuras utilizadas (no incluimos su descripción para no alargar innecesariamente este artículo ya bastante largo).

Más sobre la ofuscación

Hemos obtenido la dirección de la función que debería desofuscar el tráfico: 0x5F1AC. Pero no hay que alegrarse todavía: los desarrolladores de UC Browser nos han preparado otra sorpresa.

Después de obtener los parámetros del array que fue formado en el código Java, llegamos
a la función en la dirección 0x4D070. Y aquí nos espera otro tipo de ofuscación del código.

Colocamos en R7 y R4 dos índices:

Buscamos vulnerabilidades en UC Browser

Movemos el primer índice a R11:

Buscamos vulnerabilidades en UC Browser

Para obtener la dirección de la tabla, usamos el índice:

Buscamos vulnerabilidades en UC Browser

Después de la transición por la primera dirección, se utiliza el segundo índice, que está en R4. Hay 230 elementos en la tabla.

¿Qué hacer con esto? Se puede decirle a IDA que es un switch: Editar -> Otro -> Especificar idiomática de switch.

Buscamos vulnerabilidades en UC Browser

El código resultante es aterrador. Pero, al avanzar a través de sus laberintos, uno puede notar la llamada a la función que ya conocemos. sub_6115C:

Buscamos vulnerabilidades en UC Browser

Allí había un switch, donde en el case 3 estaba la desofuscación utilizando el algoritmo RC4. Y en este caso, la estructura que se pasa a la función se llena con los parámetros transmitidos en doCommandNative. Recordamos que teníamos magicInt con el valor 16. Miramos el case correspondiente y, tras varias transiciones, encontramos el código que permite identificar el algoritmo.

Buscamos vulnerabilidades en UC Browser

¡Es AES!

Tenemos el algoritmo, solo falta obtener sus parámetros: modo, clave y, posiblemente, vector de inicialización (su presencia depende del modo de operación del algoritmo AES). La estructura con ellos debe formarse en algún lugar antes de la llamada a la función sub_6115C, pero esta parte del código está especialmente bien ofuscada, por lo que surge la idea de parchear el código para que todos los parámetros de la función de desofuscación se almacenen en un archivo.

Parche

Para no escribir todo el código del parche en lenguaje ensamblador manualmente, se puede abrir Android Studio, escribir allí una función que reciba los mismos parámetros que nuestra función de desofuscación, y escriba en un archivo, después de lo cual copiar y pegar el código que generará el compilador.

Nuestros amigos del equipo de UC Browser también se han «preocupado» por la comodidad de agregar código. Recordamos que al principio de cada función tenemos código de relleno que se puede reemplazar fácilmente por cualquier otro. Muy conveniente 🙂 Sin embargo, al inicio de la función de destino, hay poco espacio para el código que guarda todos los parámetros en un archivo. Tuvimos que dividirlo en partes y usar bloques de relleno de funciones adyacentes. En total, resultaron ser cuatro partes.

Primera parte:

Buscamos vulnerabilidades en UC Browser

En la arquitectura ARM, los primeros cuatro parámetros de la función se pasan a través de los registros R0-R3, y los demás, si hay, a través de la pila. En el registro LR se pasa la dirección de retorno. Todo esto es necesario para que la función pueda ejecutarse después de que descarguemos sus parámetros. También es necesario guardar todos los registros que utilizaremos en el proceso, así que hacemos PUSH.W {R0-R10,LR}. En R7 obtendremos la dirección de la lista de parámetros pasados a la función a través de la pila.

Con la función fopen abrimos el archivo /data/local/tmp/aes en modo «ab»,
es decir, para agregar. Cargamos en R0 la dirección del nombre del archivo y en R1 la dirección de la cadena que indica el modo. Y aquí termina el código de relleno, por lo que pasamos a la siguiente función. Para que continúe funcionando, colocamos al principio un salto al código real de la función, evadiendo el relleno, y en lugar del relleno añadimos la continuación del parche.

Buscamos vulnerabilidades en UC Browser

Llamamos a fopen.

Los primeros tres parámetros de la función aes tienen el tipo int. Dado que al principio guardamos los registros en la pila, simplemente podemos pasar a la función fwrite sus direcciones en la pila.

Buscamos vulnerabilidades en UC Browser

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

Buscamos vulnerabilidades en UC Browser

Al final cerramos el archivo, restauramos los registros y devolvemos el control a la función real aes.

Compilamos el APK con la biblioteca parcheada, lo firmamos, lo subimos al dispositivo/emulador y lo ejecutamos. Vemos que nuestra descarga se crea y que se escribe mucha información. El navegador utiliza cifrado no solo para el tráfico, y todo el cifrado pasa por la función en cuestión. Sin embargo, no encontramos los datos que necesitamos, y no vemos la solicitud necesaria en el tráfico. Para no esperar a que UC Browser se digne a hacer la solicitud necesaria, tomaremos la respuesta cifrada del servidor, obtenida anteriormente, y parcheremos la aplicación una vez más: añadiremos la descifrado en 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

Recogemos, firmamos, instalamos y ejecutamos. Obtenemos NullPointerException porque el método devolvió null.

Durante un análisis posterior del código se descubrió una función que descifra líneas interesantes: «META-INF\/» y «.RSA». Parece que la aplicación verifica su certificado. O incluso genera claves a partir de él. No queremos lidiar con lo que sucede con el certificado, así que simplemente le proporcionaremos el certificado correcto. Parcheamos la línea cifrada de tal manera que en lugar de «META-INF\/» obtengamos «BLABLINF\/», creamos una carpeta con ese nombre en el APK y le proporcionamos allí el certificado del navegador.

Recogemos, firmamos, instalamos y ejecutamos. ¡Bingo! ¡Tenemos la clave!

MitM

Hemos obtenido la clave y el vector de inicialización, que es igual a la clave. Intentemos descifrar la respuesta del servidor en modo CBC.

Buscamos vulnerabilidades en UC Browser

Vemos la URL del archivo, algo que se parece a MD5, «extract_unzipsize» y un número. Verificamos: el MD5 del archivo coincide, el tamaño de la biblioteca descomprimida coincide. Intentamos parchear esta biblioteca y entregársela al navegador. Para demostrar que nuestra biblioteca parcheada se ha cargado, vamos a iniciar un Intent para crear un SMS con el texto «PWNED!». Vamos a reemplazar dos respuestas del servidor: puds.ucweb.com/upgrade/index.xhtml y en la descarga del archivo. En el primero reemplazamos el MD5 (el tamaño después de la descompresión no cambia), en el segundo entregamos el archivo con la biblioteca parcheada.

El navegador intenta descargar el archivo varias veces, luego arroja un error. Parece que algo
no le gusta. Como resultado del análisis de este formato turbio, se descubrió que el servidor también transmite el tamaño del archivo:

Buscamos vulnerabilidades en UC Browser

Está codificado en LEB128. Después del parche, el tamaño del archivo con la biblioteca cambió un poco, por lo que el navegador consideró que el archivo se descargó incorrectamente, y después de varios intentos, arrojó un error.

Corregimos el tamaño del archivo... ¡Y – victoria! 🙂 El resultado en video.

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

Consecuencias y reacción del desarrollador.

De la misma manera, los hackers podrían utilizar la función insegura del UC Browser para distribuir y ejecutar bibliotecas maliciosas. Estas bibliotecas funcionarían en el contexto del navegador, por lo que tendrían todos sus permisos del sistema. Como consecuencia, podrían mostrar ventanas de phishing y acceder a archivos de trabajo de la ardilla china naranja, incluyendo inicios de sesión, contraseñas y cookies almacenadas en la base de datos.

Nos pusimos en contacto con los desarrolladores de UC Browser y les informamos sobre el problema encontrado, intentando señalar la vulnerabilidad y su peligrosidad, pero no quisieron discutir nada con nosotros. Mientras tanto, el navegador continuaba presumiblemente con la función peligrosa a la vista de todos. Pero tan pronto como revelamos los detalles de la vulnerabilidad, ya no se podía ignorar como antes. El 27 de marzo se
lanzó una nueva versión del UC Browser 12.10.9.1193, que se conectaba al servidor por HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Además, después de la "corrección" y hasta el momento de escribir el artículo, intentar abrir un PDF en el navegador resultaba en un mensaje de error con el texto "¡Ups, algo salió mal!". La solicitud al servidor al intentar abrir el PDF no se ejecutaba, pero se ejecutaba al iniciar el navegador, lo que sugiere una posibilidad persistente de cargar código ejecutable en violación de las normas de Google Play.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster