Ищем уязвимости в UC Browser

Ищем уязвимости в UC Browser

Uvod

Konec marca smo poročali, kar pomeni, da smo odkrili skrito možnost nalaganja in izvajanja nepreverjene kode v UC Browserju. Danes bomo podrobno obravnavali, kako ta prenos poteka in kako ga lahko hekerji izkoristijo v svoje namene.

Nekaj časa nazaj so UC Browser agresivno oglaševali in distribuirali: nameščali so ga na naprave uporabnikov s pomočjo zlonamerne programske opreme, distribuirali so ga s različnih spletnih strani pod pretvezo video datotek (tj. uporabniki so mislili, da prenašajo, na primer, pornografski video, v resnici pa so namesto tega dobili APK s tem brskalnikom), uporabili so strašljive bannere z obvestili, da je brskalnik zastarel, ranljiv in podobno. V uradni skupini UC Browser na VK je tematska, kjer lahko uporabniki prijavijo nepošteno oglaševanje, je tam veliko primerov. Leta 2016 je bila celo video oglaševanje v ruščini (da, oglaševanje brskalnika, ki blokira oglase).

Ob času pisanja tega članka je UC Browser dosegel več kot 500.000.000 prenosov v Google Play. To je impresivno – večje število ima le Google Chrome. Med mnenji lahko opazite kar nekaj pritožb glede oglasov in preusmeritev na nekatera aplikacije v Google Play. To je postalo povod za raziskavo: želeli smo preveriti, ali UC Browser počne kaj slabega. In izkazalo se je, da res počne!

V kodi aplikacije smo odkrili možnost nalaganja in izvajanja izvršne kode, kar je v nasprotju z pravili objave aplikacij v Google Play. Poleg tega, da UC Browser nalaga izvršno kodo, to počne na nevaren način, kar se lahko uporabi za izvajanje MitM napada. Poglejmo, ali nam bo uspelo izvesti tak napad.

Vse, kar je napisano naprej, velja za različico UC Browser, ki je bila prisotna v Google Play ob času raziskave:

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

Vektor napada

V manifestu UC Browser lahko odkrijete storitev s pomenljivim imenom com.uc.deployment.UpgradeDeployService.

    <service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />

Ob zagonu te storitve brskalnik izvede POST zahtevo na puds.ucweb.com/upgrade/index.xhtml, ki ga lahko opazimo v prometu po določenem času po zagonu. V odgovor lahko prejme ukaz za nalaganje kakšne posodobitve ali novega modula. Med analizo strežnik takšnih ukazov ni dajal, vendar smo opazili, da ob poskusu odpiranja PDF-ja v brskalniku ta izvede ponovni zahtevek na zgoraj navedeni naslov, nato pa prenese naravno knjižnico. Za izvedbo napada smo se odločili uporabiti to lastnost UC Browserja: sposobnost odpiranja PDF-jev s pomočjo naravne knjižnice, ki je ni v APK in jo prinaša iz interneta, ko je to potrebno. Omeniti velja, da bi teoretično UC Browser lahko prisilili, da prenese nekaj tudi brez interakcije s uporabnikom – če se povrne pravilno oblikovan odgovor na zahtevek, ki se izvede po zagonu brskalnika. Vendar pa bi bilo potrebno podrobneje preučiti protokol za interakcijo s strežnikom, zato smo se odločili, da bo preprosteje urediti prejeti odgovor in zamenjati knjižnico za delo s PDF-ji.

Torej, ko uporabnik želi odpreti PDF neposredno v brskalniku, lahko v prometu vidimo naslednje zahteve:

Ищем уязвимости в UC Browser

Sprva gre za POST zahtevo k puds.ucweb.com/upgrade/index.xhtml, po čemer
se prenese arhiv z knjižnico za ogled PDF in pisarniških formatov. Logično je domnevati, da se v prvi zahtevi posredujejo informacije o sistemu (vsaj arhitektura, da se da ustrezna knjižnica), v odgovor na to pa brskalnik prejme določene informacije o knjižnici, ki jo je treba prenesti: naslov in morebiti še kaj drugega. Težava je, da je ta zahteva šifrirana.

Fragment zahteve

Fragment odgovora

Ищем уязвимости в UC Browser

Ищем уязвимости в UC Browser

Sam knjižnica je zapakirana v ZIP in ni šifrirana.

Ищем уязвимости в UC Browser

Iskanje kode za dešifriranje prometa

Poskusimo dešifrirati odgovor strežnika. Poglejmo kodo razreda com.uc.deployment.UpgradeDeployService: iz metode onStartCommand pridemo v com.uc.deployment.b.x, od tam v 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("]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();
            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("processor_arch", 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("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.b.a.a.c.Pc()));
            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.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("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 = 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);
    }

Тут мы видим формирование POST-запроса. Обратите внимание на создание массива из 16 байт и его заполнение: 0x5F, 0, 0x1F, -50 (=0xCE). Это совпадает с тем, что мы видели в предыдущем запросе.

В этом классе замечаем вложенный класс, в котором есть еще один интересный метод:

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

Метод получает на вход массив байтов и проверяет, совпадает ли нулевой байт с 0x60 или третий байт с 0xD0, а второй байт равен 1, 11 или 0x1F. Проверяем ответ от сервера: нулевой байт — 0x60, второй — 0x1F, третий — 0x60. Это то, что нам нужно. Судя по строкам («up_decrypt», например), тут должен вызываться метод, который расшифрует ответ сервера.
Перейдем к методу g.j. Обратите внимание, что в качестве первого аргумента ему передается байт по смещению 2 (т.е. 0x1F в нашем случае), а в качестве второго — ответ сервера без
первых 16 байт.

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

Очевидно, здесь происходит выбор алгоритма расшифровки, и тот самый байт, который в нашем случае
v primeru, ko je enak 0x1F, označuje eno od treh možnih možnosti.

Nadaljujemo z analizo kode. Po nekaj skokih pridemo do metode s pomenljivim imenom. decryptBytesByKey.

Tukaj se od našega odgovora ločita še dva bajta, iz katerih nastane niz. Očitno se na ta način izbira ključ za dešifriranje sporočila.

    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 bajta
                    System.arraycopy(bytes, 0, prefix, 0, prefix.length);
                    String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Izbira ključa
                    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;
    }

Zabegamo naprej in ugotavljamo, da v tej fazi ne dobimo še ključa, temveč le njegov 'identifikator'. Z pridobitvijo ključa je vse nekoliko zapleteno.

V naslednji metodi se obstoječim parametrom dodata še dva, in teh postane štiri: čarobna številka 16, identifikator ključa, kriptirani podatki in nerazumljiv niz (v našem primeru prazen).

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

Po seriji prehodov pridemo do metode staticBinarySafeDecryptNoB64 interfejsa com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V glavni kodi aplikacije ni razredov, ki bi izvajali to vmesnik. Tak razred obstaja v datoteki lib/armeabi-v7a/libsgmain.so, ki v resnici ni .so, ampak .jar. Metoda, ki nas zanima, je implementirana na naslednji način:

package com.alibaba.wireless.security.a.i;
 
// ...
 
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  0) {
            return this.b(magicInt, keyId, encrypted, magicString);
        }
 
        throw new SecException("", 301);
    }
//...
}

Tuk je naš seznam parametrů doplněn o další dvě celá čísla: 2 a 0. Zdá se, že
2 znamená dešifrování, jak v metodě doFinal systémové třídy javax.crypto.Cipher. A to všechno je předáno nějakému Routeru s číslem 10601 — to je patrně číslo příkazu.

Po dalším řetězci přechodů nacházíme třídu, která implementuje rozhraní IRouterComponent a metodu 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);
    }
}

A také třídu JNICLibrary, ve které je deklarována nativní metoda doCommandNative:

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

To znamená, že potřebujeme v nativním kódu najít metodu. doCommandNative. A tady začíná zábava.

Obfuskace strojového kódu

V datoteki libsgmain.so (který je vlastně .jar a ve kterém jsme výše našli implementaci některých rozhraní souvisejících s šifrováním) existuje jedna nativní knihovna: libsgmainso-6.4.36.so. Otevíráme ji v IDA a dostáváme spoustu dialogových oken s chybami. Problém spočívá v tom, že tabulka sekcí (section header table) – je neplatná. Bylo to uděláno úmyslně, aby se analýza ztížila.

Ищем уязвимости в UC Browser

Ale ta není potřebná: pro správné načtení ELF souboru a jeho analýzu stačí tabulka segmentů (program header table). Takže jednoduše odstraníme tabulku sekcí tím, že nulujeme příslušná pole v hlavičce.

Ищем уязвимости в UC Browser

Znovu otevíráme soubor v IDA.

Existují dva způsoby, jak sdělit virtuální Java mašině, kde se v nativní knihovně nachází realizace metody, která je v Java kódu deklarována jako nativní. První — dát mu název ve formátu Java_jméno_paketua_NázevTřídy_jménoMetody.

Drugi — registrirati ga prilikom učitavanja biblioteke (u funkciji JNI_OnLoad)
putem poziva funkcije RegisterNatives.

U našem slučaju, ako koristimo prvi način, ime treba biti ovako: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Među izvoženim funkcijama takva ne postoji, to znači da treba tražiti poziv RegisterNatives.
Idemo u funkciju JNI_OnLoad i vidimo takvu sliku:

Ищем уязвимости в UC Browser

Šta se ovde dešava? Na prvi pogled, početak i kraj funkcije su tipični za arhitekturu ARM. Prva instrukcija u steku čuva sadržaj registara koje će funkcija koristiti u svom radu (u ovom slučaju R0, R1 i R2), kao i sadržaj registra LR, u kojem se nalazi adresa povratka iz funkcije. Poslednja instrukcija obnavlja spremljene registre, pri čemu se adresa povratka odmah stavlja u registar PC — tako se vrši povratak iz funkcije. No, ako se malo bolje pogleda, može se primetiti da predposlednja instrukcija menja adresu povratka, spremljenu u steku. Izračunaćemo kakva će ona biti nakon
izvršavanja koda. U R1 se učitava neka adresa 0xB130, od nje se oduzima 5, zatim se prebacuje u R0 i dodaje mu se 0x10. Dobija se 0xB13B. Tako da, IDA misli da se u poslednjoj instrukciji odvija običan povratak iz funkcije, a zapravo se dešava prelazak na izračunatu adresu 0xB13B.

Ovde treba podsetiti da procesori ARM imaju dva režima i dva skupa instrukcija: ARM i Thumb. Manji bit adrese govori procesoru koji se skup instrukcija koristi. To jest, adresa je zapravo 0xB13A, a jedinica u manjem bitu označava režim Thumb.

Na početak svake funkcije u ovoj biblioteci dodan je sličan "adapter" i
smešni kod. Nećemo se dalje detaljno zadržavati na njima – samo pamtimo,
da se pravo početak gotovo svih funkcija nalazi malo dalje.

Budući da u kodu nema jasnog prelaza na 0xB13A, IDA sama nije prepoznala da se na ovom mestu nalazi kod. Iz istog razloga, većinu koda u biblioteci ne prepoznaje kao kod, što donekle otežava analizu. Kažemo IDA da se ovde nalazi kod, i evo šta dobijamo:

Ищем уязвимости в UC Browser

Na 0xB144 jasno počinje tabela. A šta se nalazi u sub_494C?

Ищем уязвимости в UC Browser

Pri pozivu ove funkcije u registru LR dobijamo adresu prethodno pomenute tabele (0xB144). U R0 — indeks u ovoj tabeli. To jest, uzima se vrednost iz tabele, dodaje se na LR i dobija se
адрес, по которому нужно перейти. Попробуем его вычислить: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Переходим по полученному адресу и видим буквально пару полезных инструкций и опять переход на 0xB140:

Ищем уязвимости в UC Browser

Теперь будет переход по смещению с индексом 0x20 из таблицы.

Судя по размеру таблицы, таких переходов в коде встретится много. Возникает вопрос, можно ли как-то с этим бороться более автоматизированно, без ручного вычисления адресов. И на помощь нам приходят скрипты и возможность патчить код в 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 "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"

Ставим курсор на строчку 0xB26A, запускаем скрипт и видим переход на 0xB4B0:

Ищем уязвимости в UC Browser

IDA опять не опознала этот участок как код. Помогаем ей и видим там другую конструкцию:

Ищем уязвимости в UC Browser

Инструкции после BLX выглядят не очень осмысленными, это больше похоже на какое-то смещение. Смотрим в sub_4964:

Ищем уязвимости в UC Browser

И действительно, тут берётся dword по адресу, лежащему в LR, прибавляется к этому адресу, после чего берётся значение по полученному адресу и кладётся в стек. Также к LR прибавляется 4, чтобы после возврата из функции перескочить это самое смещение. После чего команда POP {R1} достаёт полученное значение из стека. Если посмотреть, что находится по адресу 0xB4BA + 0xEA = 0xB5A4, то можно увидеть нечто похожее на таблицу адресов:

Ищем уязвимости в UC Browser

Da bi se izvršila ova ispravka, potrebno je dobiti dva parametra iz koda: pomak i broj registra u koji treba staviti rezultat. Za svaki mogući registrar potrebno je unaprijed pripremiti deo koda.

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 "POP instrukcija nije pronađena"
    else:
        print "Pogrešna vrsta operanda na +4:", get_operand_type(ea + 4, 0)
else:
    print "Nije moguće otkriti prve instrukcije"

Postavljamo kursor na početak konstrukcije koju želimo zameniti — 0xB4B2 — i pokrećemo skript:

Ищем уязвимости в UC Browser

Pored već pomenutih konstrukcija u kodu se takođe nalaze i ovakve:

Ищем уязвимости в UC Browser

Kao i u prethodnom slučaju, nakon instrukcije BLX ide pomak:

Ищем уязвимости в UC Browser

Uzmemo pomak iz adrese iz LR, dodajemo ga LR-u i prelazimo tamo. 0x72044 + 0xC = 0x72050. Skript za ovu konstrukciju je vrlo jednostavan:

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 "Nije moguće otkriti prvu instrukciju"

Rezultat izvršavanja skripte:

Ищем уязвимости в UC Browser

Ko je funkcija v celoti popravljen, lahko IDA usmerimo na njen dejanski začetek. Zbrala bo celotno kodo funkcije po kosih, ki jo bomo lahko dekompilirali s HexRays.

Dekodiranje nizov

Naučili smo se boriti proti obfusikaciji strojne kode v knjižnici libsgmainso-6.4.36.so iz UC Browser in pridobili kodo funkcije 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;
}

Podrobneje si poglejmo naslednje vrstice:

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

V funkciji sub_73E24 se jasno izvrši dekodiranje imena razreda. Kot parametre tej funkciji predajamo kazalec na podatke, ki izgledajo šifrirani, nek prazen prostor in število. Očitno je, da bo po klicu funkcije v praznem prostoru dekodiran niz, saj je predan funkciji FindClass, ki kot drugi parameter sprejme ime razreda. Torej, število je velikost praznega prostora ali dolžina niza. Poskusili bomo dekodirati ime razreda, ki nam mora pokazati, ali gremo v pravo smer. Poglejmo podrobneje, kaj se dogaja v 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;
}

Funkcija sub_7AF78 ustvarja primerek kontejnerja za bajtne nize določene velikosti (ne bomo podrobno obravnavali teh kontejnerjev). Tu se ustvari dva taka kontejnerja: en vsebuje niz «DcO/lcK+h?m3c*q@» (ni težko ugotoviti, da je to ključ), drugi pa - šifrirane podatke. Nato se oba objekta shranita v neko strukturo, ki se posreduje funkciji sub_6115C. Prav tako omenimo v tej strukturi polje s vrednostjo 3. Oglejmo si, kaj se s to strukturo zgodi naprej.

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

Kot parameter switch se posreduje polje strukture, ki je prej dobilo vrednost 3. Oglejmo si primer 3: v funkcijo sub_6364C se posredujejo parametri iz strukture, ki so bili vanjo shranjeni v prejšnji funkciji, tj. ključ in šifrirani podatki. Če dobro pogledate na sub_6364C, lahko v njej najdemo algoritem RC4.

Imamo algoritem in ključ. Poskusimo dešifrirati ime razreda. Izkazalo se je: com/taobao/wireless/security/adapter/JNICLibrary. Odlično! Na pravi poti smo.

Drevo ukazov

Zdaj je treba poiskati klic RegisterNatives, ki nam bo pokazal na funkcijo doCommandNative. Pregledujemo funkcije, ki jih kličemo iz JNI_OnLoad, in ga najdemo v 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;
}

In res je zabeležen naravni metod s imenom doCommandNative. Zdaj poznamo njegov naslov. Poglejmo, kaj počne.

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

Iz imena lahko sklepamo, da je tukaj vstopna točka vseh funkcij, ki so jih razvijalci odločili prenesti v naravno knjižnico. Zanimiva nas funkcija z številko 10601.

Iz kode lahko vidimo, da iz številke ukaza dobimo tri številke: command / 10000, command % 10000 / 100 in command % 10, tj. v našem primeru 1, 6 in 1. Te tri številke, skupaj z kazalcem na JNIEnv in argumenti, posredovani funkciji, se združijo v strukturo in se posredujejo naprej. S pomočjo pridobljenih treh števil (naj jih imenujemo N1, N2 in N3) se zgradi drevo ukazov.

Približno tako:

Ищем уязвимости в UC Browser

Drevo se dinamično polni v JNI_OnLoad.
Tri števila kodirajo pot v drevesu. Vsak list drevesa vsebuje naslov ustrezne funkcije. Ključ je v starševskem vozlišču. Najti mesto v kodi, kjer se v drevo doda funkcija, ki jo potrebujemo, ni težavno, če se seznanimo z vsemi uporabljenimi strukturami (njihovega opisa ne navajamo, da ne bi razširjali že tako obsežnega članka).

Še obfuskacija

Prejeli smo naslov funkcije, ki bi morala dešifrirati promet: 0x5F1AC. A veselje je predčasno: razvijalci UC Browserja so nam pripravili še eno presenečenje.

Po prejemu parametrov iz masiva, ki je bil oblikovan v Java kodi, pridemo
do funkcije na naslovu 0x4D070. In tukaj nas čaka še en način obfuskacije kode.

V R7 in R4 položimo dva indeksa:

Ищем уязвимости в UC Browser

Prvi indeks prenesemo v R11:

Ищем уязвимости в UC Browser

Za pridobitev naslova iz tabele uporabimo indeks:

Ищем уязвимости в UC Browser

Po prehodu po prvem naslovu se uporabi drugi indeks, ki je v R4. V tabeli je 230 elementov.

Kaj s tem narediti? Lahko IDA-ju povemo, da gre za switch: Uredi -> Drugo -> Določi switch idiom.

Ищем уязвимости в UC Browser

Pridobljena koda je strašna. A ko se prebijaš skozi njene labirinte, lahko opaziš klic že poznane funkcije sub_6115C:

Ищем уязвимости в UC Browser

Tja je bil switch, v katerem se je v case 3 nahajala dešifriranje z uporabo algoritma RC4. V tem primeru se struktura, poslana funkciji, izpolnjuje iz parametrov, posredovanih v doCommandNative. Spomnimo se, da smo imeli tam magicInt z vrednostjo 16. Poglejmo ustrezni case – in po nekaj prehodih najdemo kodo, po kateri lahko prepoznamo algoritem.

Ищем уязвимости в UC Browser

To je AES!

Algoritem imamo, zdaj moramo pridobiti njegove parametre: način, ključ in morebiti vektor inicializacije (njegova prisotnost je odvisna od načina delovanja algoritma AES). Struktura z njimi bi se morala oblikovati nekje pred klicem funkcije sub_6115C, toda ta del kode je še posebej dobro obfuskiran, zato se pojavi ideja, da se kodo popravi, da se vsi parametri funkcije dešifriranja shranijo v datoteko.

Popravek

Da ne bi morali pisati celotne kode popravka v asemblerju ročno, lahko zaženemo Android Studio, napišemo funkcijo, ki sprejema iste parametre kot naša funkcija dešifriranja, in jih zapišemo v datoteko, nato pa skopiramo kodo, ki jo bo generiral prevajalnik.

Naše kolege iz tima UC Browser su se također potrudile oko jednostavnosti dodavanja koda. Podsjećamo da na početku svake funkcije imamo niz bezvrijednog koda koji se može lako zamijeniti bilo kojim drugim. Zaista praktično 🙂 Ipak, na početku ciljne funkcije nemamo dovoljno prostora za kod koji sprema sve parametre u datoteku. Morali smo ga podijeliti na dijelove i koristiti beskorisne blokove susjednih funkcija. Na kraju imamo četiri dijela.

Prvi dio:

Ищем уязвимости в UC Browser

U ARM arhitekturi prvih četiri parametra funkcije se prenose preko registara R0-R3, a ostali, ako ih ima, putem steka. U registru LR se prenosi adresa povratka. Sve ovo treba sačuvati kako bi funkcija mogla raditi nakon što ispišemo njene parametre. Također treba sačuvati sve registre koje ćemo koristiti tokom procesa, pa radimo PUSH.W {R0-R10,LR}. U R7 dobijamo adresu liste parametara koji su preneseni funkciji putem steka.

Pomoću funkcije fopen otvorit ćemo datoteku /data/local/tmp/aes u režimu "ab",
tj. za dodavanje. U R0 učitavamo adresu imena datoteke, a u R1 – adresu niza koji označava režim. I ovdje beskoristan kod završava, pa prelazimo na sljedeću funkciju. Da bi ona nastavila raditi, postavljamo na početak prijelaz na pravi kod funkcije mimo bespotrebnog koda, a umjesto toga dodajemo nastavak patcha.

Ищем уязвимости в UC Browser

Pozivamo fopen.

Prva tri parametra funkcije aes imaju tip int. Budući da smo na početku sačuvali registre u steku, možemo jednostavno prenijeti funkciji fwrite njihove adrese u steku.

Ищем уязвимости в UC Browser

Zatim imamo tri strukture koje sadrže veličinu podataka i pokazivač na podatke za ključ, vektor inicijalizacije i šifrirane podatke.

Ищем уязвимости в UC Browser

Na kraju zatvaramo datoteku, obnavljamo registre i predajemo kontrolu pravoj funkciji aes.

Skupljamo APK s ispravljenom bibliotekom, potpisujemo, prebacujemo na uređaj/emulator, pokrećemo. Vidimo da se naš dump stvara, i da se piše mnogo podataka. Preglednik koristi šifriranje ne samo za promet, i sve šifriranje ide kroz razmatranu funkciju. Međutim, potrebni podaci iz nekog razloga nedostaju, i u prometu se ne vidi potrebni zahtjev. Kako ne bismo čekali da UC Browser dostavi potrebni zahtjev, uzećemo šifrirani odgovor servera, dobijen ranije, i ponovo ćemo patchovati aplikaciju: dodajemo dešifrovanje u onCreate glavne aktivnosti.

    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

Sprevajamo, podpisujemo, nameščamo, zaženemo. Dobimo NullPointerException, ker je metoda vrnila null.

Pri nadaljnji analizi kode smo odkrili funkcijo, v kateri so dešifrirane zanimive vrstice: „META-INF/“ in „.RSA“. Zdi se, da aplikacija preverja svoj certifikat. Morda celo generira ključe iz njega. Ne želimo se ukvarjati s tem, kar se dogaja s certifikatom, zato mu bomo preprosto posredovali pravilen certifikat. Prepatčali bomo šifrirano vrstico tako, da namesto „META-INF/“ dobimo „BLABLINF/“, ustvarili bomo mapo s tem imenom v APK in tja dodali certifikat belkobrauzerja.

Sprevajamo, podpisujemo, nameščamo, zaženemo. Bingo! Ključ je naš!

MitM

Prejeli smo ključ in vektor inicializacije, enak ključu. Poskusimo dešifrirati odgovor strežnika v načinu CBC.

Ищем уязвимости в UC Browser

Vidimo URL arhiva, nekaj podobnega MD5, „extract_unzipsize“ in številko. Preverimo: MD5 arhiva se ujema, velikost razpakirane knjižnice se ujema. Poskusimo popraviti to knjižnico in jo predati brskalniku. Da dokažemo, da se je naša popravljená knjižnica naložila, bomo zagnali Intent za ustvarjanje SMS-a s sporočilom „PWNED!“. Zamenjali bomo dva odgovora s strežnika: puds.ucweb.com/upgrade/index.xhtml in za prenos arhiva. V prvem zamenjamo MD5 (velikost po razpakiranju se ne spremeni), v drugem predamo arhiv s popravljená knjižnico.

Brskalnik poskuša večkrat prenesti arhiv, nato pa prikaže napako. Zdi se, da mu nekaj
ni všeč. V rezultatu analize tega nejasnega formata je bilo ugotovljeno, da strežnik prenaša tudi velikost arhiva:

Ищем уязвимости в UC Browser

Ugotovimo, da je zakodiran v LEB128. Po popravku velikost arhiva s knjižnico se je nekoliko spremenila, zato je brskalnik menil, da je bil arhiv prenesen napačno, in po več poskusih prikaže napako.

Popravimo velikost arhiva… In – zmaga! 🙂 Rezultat na videu.

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

Posledice in reakcija razvijalca

Na enak način bi hekerji lahko uporabili nevarno funkcijo UC Browser, da bi širili in zagnali zlonamerne knjižnice. Te knjižnice bi delovale v kontekstu brskalnika, zato bi prejele vse njegove sistemske dovoljenja. Kot posledica - sposobnost prikazovanja phishing oken, pa tudi dostop do delovnih datotek oranžne kitajske veverice, vključno s podatki za prijavo, gesli in piškotki shranjenimi v bazi podatkov.

Stopili smo v stik z razvijalci UC Browser in jih obvestili o ugotovljeni težavi, poskušali smo opozoriti na ranljivost in njeno nevarnost, a se niso želeli pogovarjati z nami. Medtem pa je brskalnik še naprej pokazal nevarno funkcijo na vidnem mestu. Toda takoj, ko smo razkrili podrobnosti ranljivosti, tega ni bilo mogoče več prezreti. 27. marca je bila
izdana nova različica UC Browser 12.10.9.1193, ki se je povezovala s strežnikom preko HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Poleg tega je po "popravku" in do trenutka pisanja članka poskus odpiranja PDF-ja v brskalniku povzročil prikaz napake z besedilom "Ups, nekaj je šlo narobe!". Zahteva na strežnik ob poskusu odpiranja PDF-ja se ni izvedla, vendar se je zahteva izvedla ob zagonu brskalnika, kar nakazuje na ohranjeno možnost nalaganja izvršljive kode v nasprotju z pravili Google Play.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster