Hledáme zranitelnosti v UC Browser

Hledáme zranitelnosti v UC Browser

Úvod

Na konci března jsme uváděli, objevili skrytou možnost stažení a spuštění neověřeného kódu v UC Browseru. Dnes si podrobně rozebereme, jak toto stahování probíhá a jak ho hackeři mohou využít ve svůj prospěch.

Nějakou dobu zpět UC Browser agresivně propagovali a šířili: instalovali ho na zařízení uživatelů pomocí škodlivých programů, šířili z různých webů pod záminkou videofiler (tj. uživatelé si mysleli, že stahují například porno, ale místo toho dostávali APK s tímto prohlížečem), používali vyhrožující bannery s oznámeními, že prohlížeč je zastaralý, zranitelný a podobně. V oficiální skupině UC Browser na VK je téma, kde mohou uživatelé hlásit nekalou reklamu, je tam mnoho příkladů. V roce 2016 byla dokonce videoreklama v ruštině (ano, reklama na prohlížeč blokující reklamu).

K momentu psaní článku měl UC Browser více než 500 000 000 instalací v Google Play. To je impozantní — více jen Google Chrome. Mezi recenzemi lze vidět hodně stížností na reklamu a přesměrování na nějaké aplikace v Google Play. To nás přimělo k výzkumu: rozhodli jsme se zjistit, jestli UC Browser nedělá něco špatného. A ukázalo se, že ano!

V kódu aplikace byla nalezena možnost stahovat a spouštět spustitelný kód, což odporuje pravidlům publikování aplikací v Google Play. Kromě toho, že UC Browser stahuje spustitelný kód, dělá to nebezpečným způsobem, což může být využito k provedení MitM útoku. Podíváme se, zda se nám podaří takový útok provést.

Vše, co je napsáno dále, platí pro verzi UC Browseru, která byla k dispozici v Google Play v okamžiku provádění výzkumu:

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

Útokový vektor

V manifestu UC Browseru lze nalézt službu s výmluvným názvem com.uc.deployment.UpgradeDeployService.

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

Při spuštění této služby prohlížeč provádí POST požadavek na puds.ucweb.com/upgrade/index.xhtml, který lze zaznamenat v provozu po nějaké době od spuštění. Jako odpověď může obdržet příkaz ke stažení nějaké aktualizace nebo nového modulu. Při analýze server takové příkazy nevydával, ale všimli jsme si, že při pokusu o otevření PDF v prohlížeči provede opakovaný požadavek na výše uvedenou adresu, po čemž stáhne nativní knihovnu. Pro provedení útoku jsme se rozhodli využít tuto vlastnost UC Browseru: schopnost otevírat PDF pomocí nativní knihovny, která není v APK a kterou v případě potřeby načítá z internetu. Je třeba poznamenat, že teoreticky lze UC Browser přimět k něčemu stahování i bez interakce s uživateli – pokud by se správně sestavil odpověď na požadavek, který se vykoná po spuštění prohlížeče. Ale k tomu je třeba podrobněji prozkoumat protokol interakce se serverem, proto jsme se rozhodli, že bude snazší upravit zachycenou odpověď a vyměnit knihovnu pro práci s PDF.

Takže když chce uživatel otevřít PDF přímo v prohlížeči, v provozu lze vidět následující požadavky:

Hledáme zranitelnosti v UC Browser

Nejprve příjde POST požadavek na puds.ucweb.com/upgrade/index.xhtml, po němž
se stahuje archív s knihovnou pro prohlížení PDF a kancelářských formátů. Logicky se předpokládá, že v prvním požadavku se předávají informace o systému (minimálně architektura, aby se dala správná knihovna), a jako odpověď na něj prohlížeč získává nějaké informace o knihovně, kterou je třeba stáhnout: adresu a možná něco dalšího. Problém je v tom, že tento požadavek je šifrovaný.

Fragment požadavku

Fragment odpovědi

Hledáme zranitelnosti v UC Browser

Hledáme zranitelnosti v UC Browser

Samotná knihovna je zabalená v ZIP a není šifrovaná.

Hledáme zranitelnosti v UC Browser

Hledání dešifrovacího kódu provozu

Pokusíme se dešifrovat odpověď serveru. Podíváme se na kód třídy com.uc.deployment.UpgradeDeployService: z metody onStartCommand přejdeme do com.uc.deployment.b.x, a z něj do 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("]produkt:");
            v4.append(arg9.iGX.ipR);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]verze:");
            v4.append(arg9.iGX.iEn);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]typ_upgradu:");
            v4.append(arg9.iGX.mMode);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]flag_vynucení:");
            v4.append(arg9.iGX.iEo);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tichý_režim:");
            v4.append(arg9.iGX.iDQ);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]typ_tichého_režimu:");
            v4.append(arg9.iGX.iEr);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]stav_ticha:");
            v4.append(arg9.iGX.iEp);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]soubor_ticha:");
            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("]typ_stahování:");
            v4.append(arg9.mDownloadType);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]skupina_stahování:");
            v4.append(arg9.mDownloadGroup);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cesta_stahování:");
            v4.append(arg9.iGH);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]apollo_dětská_verze:");
            v4.append(arg9.iGX.iEx);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]apollo_série:");
            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("]název_componentu:");
                    v5.append(((au)v4_1).getName());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]verze_componentu:");
                    v5.append(((au)v4_1).aDA());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]kód_verze_componentu:");
                    v5.append(((au)v4_1).gBl);
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]typ_požadavku_componentu:");
                    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_série", arg9.iGX.iEw));
            v3_2.add(g.fs("dětská_verze", 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("tichá_instalace", String.valueOf(arg9.iGX.iDQ)));
            v3_2.add(g.fs("stav_ticha", String.valueOf(arg9.iGX.iEp)));
            v3_2.add(g.fs("soubor_ticha", arg9.iGX.iEq));
            v3_2.add(g.fs("typ_ticha", 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);
    }

Vidíme zde vytváření POST požadavku. Věnujme pozornost tvorbě pole o 16 bytech a jeho naplnění: 0x5F, 0, 0x1F, -50 (=0xCE). Shoduje se to s tím, co jsme viděli v předchozím požadavku.

V této třídě si také můžeme všimnout vnořené třídy, ve které je další zajímavá metoda:

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

Metoda přijímá jako vstup pole byte a kontroluje, že nulový byte je roven 0x60 nebo třetí byte je roven 0xD0, a druhý byte je 1, 11 nebo 0x1F. Podívejme se na odpověď ze serveru: nulový byte — 0x60, druhý — 0x1F, třetí — 0x60. Zdá se, že to je to, co potřebujeme. Zdá se, že podle řádků (například "up_decrypt") by měla být vyvolána metoda, která dešifruje odpověď serveru.
Přejdeme k metodě g.j. Všimněme si, že jako první argument je předáván byte na offsetu 2 (tj. 0x1F v našem případě), a jako druhý — odpověď serveru bez
prvních 16 byte.

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

Je jasné, že dochází k výběru dešifrovacího algoritmu, a ten byte, který máme v našem
pokud se rovná 0x1F, znamená to jednu ze tří možných variant.

Pokračujeme v analýze kódu. Po několika skocích se dostáváme do metody s výmluvným názvem decryptBytesByKey.

Zde se od našeho odpovědi oddělují další dva bajty a z nich vzniká řetězec. Je jasné, že tímto způsobem se vybírá klíč pro dešifrování zprávy.

    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 bajty
                    System.arraycopy(bytes, 0, prefix, 0, prefix.length);
                    String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Výběr klíče
                    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;
    }

Předem poznamenáváme, že v této fázi dostáváme ještě ne klíč, ale pouze jeho „identifikátor“. S získáním klíče je to trochu složitější.

V následující metodě se k existujícím parametrům přidávají ještě dva, a stávají se čtyřmi: magické číslo 16, identifikátor klíče, zašifrovaná data a nevysvětlitelný řetězec (v našem případě prázdný).

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

Po sérii přechodů přicházíme k metodě staticBinarySafeDecryptNoB64 rozhraní com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V hlavním kódu aplikace nejsou třídy, které implementují tento rozhraní. Taková třída je v souboru lib/armeabi-v7a/libsgmain.so, který ve skutečnosti není .so, ale .jar. Nás zajímající metoda je implementována následovně:

balíček com.alibaba.wireless.security.a.i;
 
// ...
 
veřejná třída a implementuje IStaticDataEncryptComponent {
    soukromé ISecurityGuardPlugin a;
// ...
    soukromé byte[] a(int móda, int magickýInt, int xzInt, String keyId, byte[] zašifrované, String magicString) {
        return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(móda), Integer.valueOf(magickýInt), Integer.valueOf(xzInt), keyId, zašifrované, magicString});
    }
// ...
    soukromé byte[] b(int magickýInt, String keyId, byte[] zašifrované, String magicString) {
        return this.a(2, magickýInt, 0, keyId, zašifrované, magicString);
    }
// ...
    veřejné byte[] staticBinarySafeDecryptNoB64(int magickýInt, String keyId, byte[] zašifrované, String magicString) vyhazuje SecException {
        if(keyId != null && keyId.length() > 0 && magickýInt >= 0 && magickýInt  0) {
            return this.b(magickýInt, keyId, zašifrované, magicString);
        }
 
        vyhoďte novou SecException("", 301);
    }
//...
}

Zde se náš seznam parametrů rozšiřuje o dvě celá čísla: 2 a 0. Zřejmě
2 znamená dešifrování, jak je uvedeno v metodě doFinal systémové třídy javax.crypto.Cipher. A vše je předáno nějakému Routeru s číslem 10601 — což je pravděpodobně číslo příkazu.

Po dalším řetězci přechodů nacházíme třídu, která implementuje rozhraní IRouterComponent a metoda doCommand:

balíček com.alibaba.wireless.security.mainplugin;

import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
 
veřejná třída a implementuje IRouterComponent {
    veřejná a() {
        super();
    }
    veřejný Object doCommand(int arg2, Object[] arg3) {
        return JNICLibrary.doCommandNative(arg2, arg3);
    }
}

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

balíček com.taobao.wireless.security.adapter;
 
veřejná třída JNICLibrary {
    veřejné statické native Object doCommandNative(int arg0, Object[] arg1);
}

To znamená, že musíme v nativním kódu najít metodu doCommandNative. A tady začíná ta pravá zábava.

Obfuskace strojového kódu

V souboru libsgmain.so (který je ve skutečnosti .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 je v tom, že tabulka sekcí (section header table) je neplatná. To bylo uděláno úmyslně, aby se ztížila analýza.

Hledáme zranitelnosti v UC Browser

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

Hledáme zranitelnosti v UC Browser

Znovu otevíráme soubor v IDA.

Existují dva způsoby, jak říct virtuální Java mašině, kde přesně v nativní knihovně se nachází implementace metody, která byla deklarována v Java kódu jako native. První — dát jí jméno ve formátu Java_jméno_balíčku_NázevTřídy_jménoMetody.

Druhý — zaregistrovat ho při načítání knihovny (v funkci JNI_OnLoad)
pomocí volání funkce RegisterNatives.

V našem případě, pokud použijeme první způsob, jméno by mělo být následující: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Mezi exportovanými funkcemi taková není, znamená to, že musíme hledat volání RegisterNatives.
Jdeme do funkce JNI_OnLoad a vidíme takový obrázek:

Hledáme zranitelnosti v UC Browser

Co se tu děje? Na první pohled, začátek a konec funkce jsou typické pro architekturu ARM. První instrukcí se na zásobník ukládá obsah registrů, které funkce bude používat, v tomto případě R0, R1 a R2, a také obsah registru LR, ve kterém je adresa návratu z funkce. Poslední instrukcí se uložené registry obnovují, přičemž adresa návratu se ihned umístí do registru PC — takto probíhá návrat z funkce. Ale když se podíváte blíže, můžete si všimnout, že předposlední instrukce mění adresu návratu uloženou na zásobníku. Vypočtěme, jaká bude po
provedení kódu. Do R1 se uloží nějaká adresa 0xB130, z které se odečte 5, poté se přenese do R0 a přičte se 0x10. Tím dostaneme 0xB13B. Takže IDA si myslí, že v poslední instrukci probíhá běžný návrat z funkce, ale ve skutečnosti k němu dochází na vypočítané adrese 0xB13B.

Tady si musíme připomenout, že procesory ARM mají dva režimy a dvě sady instrukcí: ARM a Thumb. Mladší bit adresy říká procesoru, jaká sada instrukcí se používá. T. j. adresa je ve skutečnosti 0xB13A, a jednička v mladším bitu označuje režim Thumb.

Na začátek každé funkce v této knihovně je přidán podobný „adaptér“ a
odpadkový kód. Dále se na ně podrobně nestavíme – jen si pamatujeme,
že skutečný začátek téměř všech funkcí je o něco dále.

Protože v kódu není jasný přechod na 0xB13A, IDA si sama nepoznala, že na tomto místě se nachází kód. Z téhož důvodu většinu kódu v knihovně nepoznává jako kód, což poněkud ztěžuje analýzu. Řekněme IDA, že zde je kód, a zde je, co se stane:

Hledáme zranitelnosti v UC Browser

Na 0xB144 jasně začíná tabulka. A co je v sub_494C?

Hledáme zranitelnosti v UC Browser

Při volání této funkce v registru LR dostaneme adresu zmíněné tabulky (0xB144). Do R0 — index v této tabulce. T. j. vezme se hodnota z tabulky, přičte se k LR a dostaneme
adresa, na kterou je potřeba přejít. Zkusíme ji vypočítat: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Přecházíme na získanou adresu a vidíme doslova pár užitečných instrukcí a opět přechod na 0xB140:

Hledáme zranitelnosti v UC Browser

Teď dojde k přechodu podle offsetu s indexem 0x20 z tabulky.

Podle velikosti tabulky se zdá, že s takovými přechody se v kódu potkáme mnohokrát. Vzniká otázka, zda se s tím dá nějak bojovat lépe automatizovaně, bez ručního výpočtu adres. A na pomoc nám přicházejí skripty a možnost patchování kódu v 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 "Špatný typ operandu na", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
            table = None
        if table is None:
            print "Nelze najít tabulku"
        else:
            print "table =", hex(table)
            offset = get_wide_dword(table + (index << 2))
            put_unconditional_branch(ea, table + offset)
    else:
        print "Neznámý kód", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
    print "Nelze rozpoznat první instrukci"

Umístíme kurzor na řádek 0xB26A, spustíme skript a vidíme přechod na 0xB4B0:

Hledáme zranitelnosti v UC Browser

IDA opět nepoznala tuto část jako kód. Pomáháme jí a vidíme tam jinou konstrukci:

Hledáme zranitelnosti v UC Browser

Instrukce po BLX nevypadají příliš smysluplně, vypadá to spíše jako nějaký offset. Podíváme se na sub_4964:

Hledáme zranitelnosti v UC Browser

A opravdu, zde se bere dword na adrese uložené v LR, přičítá se k této adrese, poté se vezme hodnota z získané adresy a vloží se na zásobník. Také se k LR přičítá 4, aby se po návratu z funkce přeskočil tento offset. Poté příkaz POP {R1} vytáhne získanou hodnotu ze zásobníku. Pokud se podíváme, co se nachází na adrese 0xB4BA + 0xEA = 0xB5A4, můžeme vidět něco, co vypadá jako tabulka adres:

Hledáme zranitelnosti v UC Browser

Chcete-li upravit tuto konstrukci, je třeba získat dva parametry z kódu: posun a číslo registru, do kterého je třeba umístit výsledek. Pro každý možný registr bude nutné předem připravit kousek kódu.

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 "Instrukce POP nebyla nalezena"
    else:
        print "Špatný typ operandu na +4:", get_operand_type(ea + 4, 0)
else:
    print "Nelze detekovat první instrukce"

Umístíme kurzor na začátek konstrukce, kterou chceme nahradit — 0xB4B2 — a spustíme skript:

Hledáme zranitelnosti v UC Browser

Kromě již zmíněných konstrukcí se v kódu také nacházejí tyto:

Hledáme zranitelnosti v UC Browser

Stejně jako v předchozím případě, po instrukci BLX následuje posun:

Hledáme zranitelnosti v UC Browser

Vezmeme posun podle adresy z LR, přičteme ho k LR a přejdeme tam. 0x72044 + 0xC = 0x72050. Skript pro tuto konstrukci je velmi jednoduchý:

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 "Nelze detekovat první instrukci"

Výsledek provedení skriptu:

Hledáme zranitelnosti v UC Browser

Poté, co je funkce plně upravena, můžete IDA nasměrovat na její skutečný začátek. Ten shromáždí celý kód funkce po částech, a ten lze dekompilovat pomocí HexRays.

Dekódování řetězců

Naučili jsme se bojovat s obfuskováním strojového kódu v knihovně libsgmainso-6.4.36.so z UC Browser a získali jsme kód funkce 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;
}

Podívejme se podrobněji na následující řádky:

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

Ve funkci sub_73E24 se zjevně dešifruje název třídy. Jako parametry této funkce se předává ukazatel na data, která vypadají jako šifrovaná, nějaký buffer a číslo. Je zřejmé, že po zavolání funkce bude ve bufferu dešifrovaný řetězec, protože je předávána funkci FindClass, která přijímá jako druhý parametr název třídy. To znamená, že číslo představuje velikost bufferu nebo délku řetězce. Pokusme se dešifrovat název třídy, měl by nám ukázat, zda jdeme správným směrem. Podívejme se podrobněji, co se děje 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;
}

Funkce sub_7AF78 vytváří instanci kontejneru pro byte array o dané velikosti (nebudeme se podrobně zaobírat těmito kontejnery). Zde se vytvářejí dva takové kontejnery: do jednoho se ukládá řetězec "DcO/lcK+h?m3c*q@" (není těžké odhadnout, že to je klíč), do druhého — zašifrovaná data. Následně jsou oba objekty uloženy do jakési struktury, která se předává funkci sub_6115C. Také si v této struktuře poznamenejme pole s hodnotou 3. Podívejme se, co se s touto strukturou děje dál.

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

Jako parametr přepínače se předává pole struktury, kterému byla dříve přiřazena hodnota 3. Sledujeme case 3: ve funkci sub_6364C předávají se parametry ze struktury, které byly uloženy do ní v předchozí funkci, tj. klíč a zašifrovaná data. pokud se podíváme pozorně na sub_6364C, lze v ní zjistit algoritmus RC4.

Máme algoritmus a klíč. Zkusíme dešifrovat název třídy. Tady je, co jsme dostali: com/taobao/wireless/security/adapter/JNICLibrary. Skvěle! Jsme na správné cestě.

Strom příkazů

Nyní je třeba najít volání RegisterNatives, které nám ukáže na funkci doCommandNative. Procházejeme funkce volané z JNI_OnLoad, a nacházíme ji 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;
}

A skutečně, zde se registruje nativní metoda s názvem doCommandNative. Nyní známe její adresu. Podívejme se, co dělá.

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

Z názvu lze usoudit, že zde se nachází vstupní bod všech funkcí, které vývojáři rozhodli přesunout do nativní knihovny. Zajímá nás funkce s číslem 10601.

Z kódu lze vidět, že z čísla příkazu vznikají tři čísla: command / 10000, command % 10000 / 100 a command % 10, tj. v našem případě 1, 6 a 1. Tato tři čísla, stejně jako ukazatel na JNIEnv a argumenty předané funkci, jsou uloženy do struktury a předány dále. S pomocí získaných tří čísel (označme je N1, N2 a N3) se vytváří strom příkazů.

Přibližně takto:

Hledáme zranitelnosti v UC Browser

Strom se dynamicky vyplňuje v JNI_OnLoad.
Tři čísla kódují cestu ve stromě. Každý list stromu obsahuje pokrslenou adresu příslušné funkce. Klíč je v rodičovském uzlu. Najít místo v kódu, kde se do stromu přidává potřebná funkce, není velký problém, pokud pochopíte všechny používané struktury (jejich popis nezmiňujeme, aby se článek ještě více nezaplnil).

Ještě obfuskace

Získali jsme adresu funkce, která by měla dešifrovat provoz: 0x5F1AC. Ale radovat se zatím není důvod: vývojáři UC Browser nám připravili další překvapení.

Po obdržení parametrů z pole, které bylo vytvořeno v Java kódu, se dostáváme
do funkce na adrese 0x4D070. A zde na nás čeká další typ obfuskace kódu.

Umístíme do R7 a R4 dva indexy:

Hledáme zranitelnosti v UC Browser

Přesuneme první index do R11:

Hledáme zranitelnosti v UC Browser

Abychom získali adresu z tabulky, použijeme index:

Hledáme zranitelnosti v UC Browser

Po přechodu na první adresu se používá druhý index, který je v R4. V tabulce je 230 položek.

Co s tím udělat? Můžeme IDA říct, že se jedná o switch: Edit -> Other -> Specify switch idiom.

Hledáme zranitelnosti v UC Browser

Výsledný kód je děsivý. Ale procházením jeho zákrutami můžeme spatřit volání již známé funkce sub_6115C:

Hledáme zranitelnosti v UC Browser

Tam byl switch, ve kterém v case 3 byla dešifrace pomocí algoritmu RC4. A v tomto případě se struktura předávaná do funkce zaplní parametry, které byly předány v doCommandNative. Vzpomeňme, že jsme měli magicInt s hodnotou 16. Podíváme se na příslušný case – a po několika přechodech nalezneme kód, podle kterého můžeme rozpoznat algoritmus.

Hledáme zranitelnosti v UC Browser

To je AES!

Algoritmus je tady, zbývá získat jeho parametry: režim, klíč a možná inicializační vektor (jeho existence závisí na režimu práce algoritmu AES). Struktura s nimi by měla být vytvářena někde před voláním funkce sub_6115C, ale tato část kódu je obfuskována zvlášť dobře, proto se objevila myšlenka patchnout kód, aby se všechny parametry dešifrovací funkce ukládaly do souboru.

Patch

Abychom nemuseli psát celý kód patche v assembleru ručně, můžeme spustit Android Studio, napsat tam funkci, která přijímá stejné parametry jako naše dešifrovací funkce a zapisuje do souboru, poté zkopírovat kód, který vygeneruje kompilátor.

O pohodlnosti přidání kódu se postarali i naši přátelé z týmu UC Browser. Připomínáme, že na začátku každé funkce máme odpadní kód, který lze snadno nahradit jakýmkoli jiným. Je to velmi praktické 🙂 Pravda, na začátku cílové funkce není moc místa pro kód, který ukládá všechny parametry do souboru, takže jsme ho museli rozdělit na části a použít odpadní bloky sousedních funkcí. Celkem vznikly čtyři části.

První část:

Hledáme zranitelnosti v UC Browser

V architektuře ARM jsou první čtyři parametry funkce předávány prostřednictvím registrů R0-R3, ostatní, pokud existují, - prostřednictvím zásobníku. V registru LR je předána adresa návratu. To vše je třeba uložit, aby funkce mohla pracovat poté, co dumpneme její parametry. Také je třeba uložit všechny registry, které budeme používat v procesu, proto provádíme PUSH.W {R0-R10,LR}. V R7 máme adresu seznamu parametrů předaných funkci přes zásobník.

Pomocí funkce fopen otevřeme soubor /data/local/tmp/aes v režimu „ab“,
tj. pro přidání. Do R0 načteme adresu názvu souboru, do R1 - adresu řetězce s uvedením režimu. A tady odpadní kód končí, takže přecházíme k další funkci. Aby dál fungovala, na začátek umístíme skok na skutečný kód funkce, abychom se vyhnuli odpadu, a místo odpadu přidáme pokračování patche.

Hledáme zranitelnosti v UC Browser

Vyvoláme fopen.

První tři parametry funkce aes mají typ int. Protože jsme na začátku uložili registry na zásobník, můžeme funkci jednoduše předat fwrite její adresy na zásobníku.

Hledáme zranitelnosti v UC Browser

Dále máme tři struktury, které obsahují velikost dat a ukazatel na data pro klíč, inicializační vektor a zašifrovaná data.

Hledáme zranitelnosti v UC Browser

Na konci zavřeme soubor, obnovíme registry a předáme řízení skutečné funkci. aes.

Složíme APK s upravenou knihovnou, podepíšeme ji, nahrajeme na zařízení/emulátor a spustíme. Vidíme, že náš dump se vytváří a zapisuje se do něj mnoho dat. Prohlížeč používá šifrování nejen pro provoz a veškeré šifrování probíhá přes zmiňovanou funkci. A potřebná data nějak chybí, v provozu není vidět požadovaný dotaz. Abyste nečekali, až UC Browser konečně provede požadovaný dotaz, vezmeme zašifrovanou odpověď ze serveru, kterou jsme dostali dříve, a aplikaci ještě jednou upravíme: přidáme dešifrování do onCreate hlavní aktivity.

    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

Sestavujeme, podepisujeme, instalujeme, spouštíme. Získáváme NullPointerException, protože metoda vrátila null.

Během dalšího analýzy kódu byla odhalena funkce, ve které se dešifrují zajímavé řádky: „META-INF/“ a „.RSA“. Zdá se, že aplikace kontroluje svůj certifikát. Nebo dokonce generuje klíče z něj. Nechceme se zabývat tím, co se děje s certifikátem, proto mu prostě podstrčíme správný certifikát. Propatcheme zašifrovaný řetězec tak, aby místo „META-INF/“ bylo „BLABLINF/“, vytvoříme složku s tímto názvem v APK a přidáme tam certifikát prohlížeče.

Sestavujeme, podepisujeme, instalujeme, spouštíme. Bingo! Klíč máme!

MitM

Získali jsme klíč a inicializační vektor, který je rovný klíči. Pokusíme se dešifrovat odpověď serveru v režimu CBC.

Hledáme zranitelnosti v UC Browser

Vidíme URL archivu, něco jako MD5, „extract_unzipsize“ a číslo. Kontrolujeme: MD5 archivu souhlasí, velikost rozbalené knihovny souhlasí. Pokusíme se propatchovat tuto knihovnu a předat ji prohlížeči. Abychom ukázali, že naše propatchovaná knihovna byla načtena, spustíme Intent k vytvoření SMS s textem „PWNED!“. Budeme podvádět dvě odpovědi od serveru: puds.ucweb.com/upgrade/index.xhtml a na stahování archivu. V první podvádíme MD5 (velikost po rozbalení se nemění), ve druhé dáváme archiv s propatchovanou knihovnou.

Prohlížeč se několikrát pokouší stáhnout archiv, pak vyhodí chybu. Zdá se, že mu něco
se mu nelíbí. V důsledku analýzy tohoto podivného formátu se ukázalo, že server také posílá velikost archivu:

Hledáme zranitelnosti v UC Browser

Je zakódován v LEB128. Po patchi se velikost archivu s knihovnou mírně změnila, takže prohlížeč si myslel, že archiv byl stáhnut špatně, a po několika pokusech vydal chybu.

Opravená velikost archivu… A – vítězství! 🙂 Výsledek na videu.

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

Následky a reakce vývojáře

Stejným způsobem by hackeři mohli využít nezabezpečenou funkci UC Browser k šíření a spouštění škodlivých knihoven. Tyto knihovny by pracovaly v kontextu prohlížeče, a proto získaly všechna jeho systémová oprávnění. To by vedlo k možnosti zobrazování phishingových oken a také k přístupu k pracovním souborům oranžové čínské veverky, včetně přihlašovacích údajů, hesel a cookies uložených v databázi.

Spojili jsme se s vývojáři UC Browser a informovali je o nalezeném problému, snažili jsme se upozornit na zranitelnost a její nebezpečnost, ale o jakoukoli diskusi s námi neměli zájem. Mezitím prohlížeč i nadále veřejně nabízel nebezpečnou funkci. Jakmile jsme však odhalili detaily zranitelnosti, ignorovat to, jako dříve, již nebylo možné. Dne 27. března byla
vydána nová verze UC Browser 12.10.9.1193, která se připojovala k serveru přes HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Kromě toho po „oprave“ a do okamžiku psaní tohoto článku pokus o otevření PDF v prohlížeči vyvolával chybovou zprávu „Omlouváme se, něco se pokazilo!“. Požadavek na server při pokusu o otevření PDF nebyl proveden, ale požadavek při spuštění prohlížeče byl úspěšně odeslán, což naznačuje zachovanou možnost stahovat spustitelný kód v rozporu s pravidly Google Play.

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster