Hľadáme zraniteľnosti v UC Browser

Hľadáme zraniteľnosti v UC Browser

Úvod

Na konci marca sme hlásili, že sme objavili skrytú možnosť načítania a spustenia nepotvrdeného kódu v UC Browser. Dnes si podrobne preberieme, ako sa toto načítanie deje a ako ho môžu hackeri využiť vo svoj prospech.

Pred nejakým časom UC Browser veľmi agresívne propagovali a šírili: inštalovali ho na zariadenia používateľov pomocou malvéru, šírili z rôznych stránok pod zámienkou video súborov (t. j. používatelia si mysleli, že sťahujú napríklad pornovideo, a namiesto toho dostali APK s týmto prehliadačom), používali desivé bannery s informáciami o tom, že prehliadač je zastaraný, zraniteľný a podobne. V oficiálnej skupine UC Browser na VK je tématická, kde môžu používatelia podať sťažnosť na nekalú reklamu, je tam veľa príkladov. V roku 2016 bola dokonca videoreklama v ruštine (áno, reklama prehliadača, ktorý blokuje reklamu).

V čase písania tohto článku mal UC Browser už viac ako 500 000 000 inštalácií v Google Play. To je pôsobivé — viac len Google Chrome. Medzi recenziami je vidieť dosť veľa sťažností na reklamu a presmerovania na nejaké aplikácie v Google Play. A to bolo dôvodom na výskum: rozhodli sme sa pozrieť, či UC Browser nerobí niečo zlé. A ukázalo sa, že robí!

V kóde aplikácie sa objavila možnosť načítania a spustenia vykonávacieho kódu, čo je v rozpore s pravidlami publikácie aplikácií v Google Play. Okrem toho, že UC Browser načítava vykonávací kód, robí to nebezpečne, čo môže byť použité na vykonanie MitM útoku. Pozrime sa, či sa nám podarí takýto útok vykonať.

Všetko, čo je napísané ďalej, je aktuálne pre verziu UC Browser, ktorá bola prítomná v Google Play v čase uskutočnenia výskumu:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK súboru: f5edb2243413c777172f6362876041eb0c3a928c

Vektor útoku

V manifeste UC Browser môžeme nájsť službu s výrečným názvom com.uc.deployment.UpgradeDeployService.

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

Pri spustení tejto služby prehliadač vykonáva POST požiadavku na puds.ucweb.com/upgrade/index.xhtml, ktorý je možné spozorovať v úrade po niekoľkých minútach po spustení. V odpovedi môže získať príkaz na stiahnutie nejakého aktualizácie alebo nového modulu. Počas analýzy server takýchto príkazov neposkytoval, ale všimli sme si, že pri pokuse o otvorenie PDF v prehliadači robí opakovaný požiadavok na vyššie uvedenú adresu, po čom sa stiahne natívna knižnica. Na uskutočnenie útoku sme sa rozhodli využiť túto vlastnosť UC Browser: schopnosť otvárať PDF pomocou natívnej knižnice, ktorá nie je v APK a ktorú v prípade potreby načítava z internetu. Je potrebné poznamenať, že teoreticky by sa UC Browser dal prinútiť niečo stiahnuť aj bez interakcie s používateľom – ak sa poskytne správne formulovaná odpoveď na požiadavok, ktorý sa vykonáva po spustení prehliadača. Ale na to je potrebné podrobnejšie študovať protokol interakcie so serverom, a preto sme sa rozhodli, že je jednoduchšie upraviť zachytenú odpoveď a vymeniť knižnicu na prácu s PDF.

Keď používateľ chce otvoriť PDF priamo v prehliadači, v úrade je možné vidieť nasledujúce požiadavky:

Hľadáme zraniteľnosti v UC Browser

Najprv ide o POST požiadavok na puds.ucweb.com/upgrade/index.xhtml, po čom
sa stiahne archív s knižnicou na prezeranie PDF a kancelárskych formátov. Logicky by sa dalo predpokladať, že v prvej požiadavke sa prenáša informácia o systéme (minimálne architektúra, aby sa odovzdala potrebná knižnica), a v odpovedi na ňu prehliadač dostáva určitú informáciu o knižnici, ktorú je potrebné stiahnuť: adresu a možno aj niečo ďalšie. Problém je, že táto požiadavka je šifrovaná.

Fragment požiadavky

Fragment odpovede

Hľadáme zraniteľnosti v UC Browser

Hľadáme zraniteľnosti v UC Browser

Samotná knižnica je zabalená v ZIP-e a nie je šifrovaná.

Hľadáme zraniteľnosti v UC Browser

Hľadanie kódu na dešifrovanie úradu

Skúsime dešifrovať odpoveď servera. Pozrieme sa na kód triedy com.uc.deployment.UpgradeDeployService: z metódy onStartCommand prechádzame do com.uc.deployment.b.x, a z neho do com.uc.browser.core.d.c.f.e:

    verejná finálna 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("]verzia:");
            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("]vlajka_núteného_režimu:");
            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_tichého_režimu:");
            v4.append(arg9.iGX.iEp);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]tichý_súbor:");
            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_stiahnutia:");
            v4.append(arg9.mDownloadType);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]skupina_stiahnutia:");
            v4.append(arg9.mDownloadGroup);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cesta_stiahnutia:");
            v4.append(arg9.iGH);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]verzia_apollo_dieťa:");
            v4.append(arg9.iGX.iEx);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]séria_apollo:");
            v4.append(arg9.iGX.iEw);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]architektúra_cpu_apollo:");
            v4.append(arg9.iGX.iEt);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cpu_vfp3_apollo:");
            v4.append(arg9.iGX.iEv);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]cpu_vfp_apollo:");
            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ázov_zložky:");
                    v5.append(((au)v4_1).getName());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]verzia_názvu_zložky:");
                    v5.append(((au)v4_1).aDA());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]verzia_kódu_zložky:");
                    v5.append(((au)v4_1).gBl);
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]typ_požiadavky_zložky:");
                    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("architektúra_processor", 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("typ_siete", String.valueOf(com.uc.base.system.a.Jo())));
            v3_2.add(g.fs("z_hosta", arg9.iGX.iEm));
            v3_2.add(g.fs("verzia_pluginu", arg9.iGX.iEn));
            v3_2.add(g.fs("cieľový_jazyk", 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("verzia_deti_pluginu", arg9.iGX.iEx));
            v3_2.add(g.fs("verzia_série", arg9.iGX.iEw));
            v3_2.add(g.fs("verzia_deti", r.aVw()));
            v3_2.add(g.fs("md5_cur_verzie", arg9.iGX.iEl));
            v3_2.add(g.fs("podpis_cur_verzie", SystemHelper.getUCMSignature()));
            v3_2.add(g.fs("denník_upgradu", i.bjt()));
            v3_2.add(g.fs("tichá_inštalácia", String.valueOf(arg9.iGX.iDQ)));
            v3_2.add(g.fs("stav_tichého_režimu", String.valueOf(arg9.iGX.iEp)));
            v3_2.add(g.fs("tichý_súbor", arg9.iGX.iEq));
            v3_2.add(g.fs("typ_tichého_režimu", String.valueOf(arg9.iGX.iEr)));
            v3_2.add(g.fs("architektúra_cpu", com.uc.b.a.a.c.Pc()));
            v3_2.add(g.fs("súbor_cpu", SystemHelper.getCpuInstruction()));
            boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
            v3_2.add(g.fs("neon", String.valueOf(v4_4)));
            v3_2.add(g.fs("cpu_jadra", 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("celková_ram", 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 formování POST-žádosti. Zaměřujeme se na vytvoření pole o 16 bajtech a jeho naplnění: 0x5F, 0, 0x1F, -50 (=0xCE). Shoduje se to s tím, co jsme viděli v předchozí žádosti.

Ve stejné třídě si 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 bajtů a kontroluje, že nulový bajt je roven 0x60 nebo třetí bajt je roven 0xD0, a druhý bajt - 1, 11 nebo 0x1F. Podívejme se na odpověď ze serveru: nulový bajt - 0x60, druhý - 0x1F, třetí - 0x60. Vypadá to, že máme to, co potřebujeme. Podle řádků („up_decrypt“, například) by zde 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 se předává bajt na posunu 2 (tj. 0x1F v našem případě), a jako druhý - odpověď serveru bez
prvních 16 bajtů.

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

Zjevně zde probíhá výběr algoritmu dešifrování, a ten konkrétní bajt, který máme v našich
v prípade je 0x1F, označuje jeden z troch možných variantov.

Pokračujeme v analýze kódu. Po niekoľkých skokoch sa dostaneme do metódy s výstižným názvom decryptBytesByKey.

Tu sa od našej odpovede oddelia ešte dva bajty a z nich vznikne reťazec. Je jasné, že týmto spôsobom sa vyberá kľúč na dešifrovanie sprá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ýber kľúč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;
    }

S výhľadom dopredu si všimneme, že v tejto fáze ešte nedostávame kľúč, ale iba jeho „identifikátor“. Získanie kľúča je o niečo zložitejšie.

V nasledujúcej metóde sa k existujúcim parametrom pridajú ešte dva, čím sa ich počet zvýši na štyri: magické číslo 16, identifikátor kľúča, zašifrované údaje a nejasný reťazec (v našom prípade prázdny).

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

Po sérii prechodov prichádzame k metóde staticBinarySafeDecryptNoB64 rozhranie com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V hlavnom kóde aplikácie nie sú žiadne triedy, ktoré implementujú tento rozhranie. Takáto trieda je v súbore lib/armeabi-v7a/libsgmain.so, ktorý v skutočnosti nie je .so, ale .jar. Zaujímavá metóda je implementovaná nasledovne:

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

Tu je náš zoznam parametrov, ktorý je doplnený o dve celé čísla: 2 a 0. Zjavne
2 znamená dešifrovanie, ako v metóde doFinal systémovej triedy javax.crypto.Cipher. A to všetko sa prenáša do akéhosi Routeru s číslom 10601 — to je pravdepodobne číslo príkazu.

Po ďalšej sérii prechodov nachádzame triedu, ktorá implementuje rozhranie IRouterComponent a metóda 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 tiež trieda JNICLibrary, v ktorej je deklarovaná natívna metóda doCommandNative:

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

To znamená, že musíme v natívnom kóde nájsť metódu doCommandNative. A tu začína najväčšia zábava.

Obfukovanie strojového kódu

V súbore libsgmain.so (ktorý je v skutočnosti .jar a v ktorom sme vyššie našli implementáciu niektorých rozhraní súvisiacich s šifrovaním) obsahuje jednu natívnu knižnicu: libsgmainso-6.4.36.so. Otvoríme ju v IDA a dostaneme množstvo dialógových okien s chybami. Problém je, že tabulka sekcií (section header table) je neplatná. Je to urobené zámerne, aby sa sťažilo analyzovanie.

Hľadáme zraniteľnosti v UC Browser

Ale tá nie je potrebná: na správne načítanie ELF súboru a jeho analýzu stačí tabulka segmentov (program header table). Preto jednoducho odstránime tabuľku sekcií vymazaním príslušných polí v hlavičke.

Hľadáme zraniteľnosti v UC Browser

Znovu otvárame súbor v IDA.

Existujú dva spôsoby, ako informovať virtuálnu Java mašinu, kde presne v natívnej knižnici sa nachádza implementácia metódy, deklarovanej v Java kóde ako natívna. Prvý — dať jej meno vo forme Java_názov_balíka_NázovTriedy_názovMetódy.

Druhý — zaregistrovať ho pri načítaní knižnice (vo funkcii JNI_OnLoad)
pomocou volania funkcie RegisterNatives.

V našom prípade, ak používame prvý spôsob, názov by mal byť nasledovný: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Medzi exportovanými funkciami taká nie je, čo znamená, že musíme hľadať volanie RegisterNatives.
Ideme do funkcie JNI_OnLoad a vidíme takýto obrazec:

Hľadáme zraniteľnosti v UC Browser

Čo sa tu deje? Na prvý pohľad, začiatok a koniec funkcie sú typické pre architektúru ARM. Prvým príkazom sa do zásobníka ukladá obsah registrov, ktoré funkcia použije vo svojej práci (v tomto prípade R0, R1 a R2), ako aj obsah registra LR, v ktorom sa nachádza adresa návratu z funkcie. Posledným príkazom sa obnovené registre obnovujú, pričom adresa návratu sa okamžite umiestňuje do registra PC — týmto spôsobom sa uskutočňuje návrat z funkcie. Ale ak sa pozriete bližšie, môžete si všimnúť, že predposledný príkaz mení adresu návratu uloženú v zásobníku. Vypočítajme, aká bude po
vykonaní kódu. Do R1 sa načíta určitá adresa 0xB130, od nej sa odčíta 5, potom sa presunie do R0 a k nej sa pripočíta 0x10. Výsledkom je 0xB13B. Týmto spôsobom IDA myslí, že v poslednom príkaze sa uskutočňuje bežný návrat z funkcie, zatiaľ čo sa v skutočnosti deje skok na vypočítanú adresu 0xB13B.

Tu treba pripomenúť, že procesory ARM majú dva režimy a dva súbory inštrukcií: ARM a Thumb. Najnižší bit adresy hovorí procesoru, ktorý súbor inštrukcií sa používa. T. j. adresa je v skutočnosti 0xB13A, pričom jednotka v najnižšom bite označuje režim Thumb.

Na začiatok každej funkcie v tejto knižnici bol pridaný podobný „adaptér“ a
zbytočný kód. Ďalej sa na nich nebudeme podrobne zastavovať – jednoducho si pamätáme,
že skutočný začiatok takmer všetkých funkcií je trocha ďalej.

Keďže v kóde nie je jasný skok na 0xB13A, IDA sama neidentifikovala, že v tomto mieste sa nachádza kód. Z tohto dôvodu veľkú časť kódu v knižnici neidentifikuje ako kód, čo sťažuje analýzu. Hovoríme IDA, že tu je kód, a takto to vyzerá:

Hľadáme zraniteľnosti v UC Browser

Na 0xB144 jasne začína tabuľka. A čo je v sub_494C?

Hľadáme zraniteľnosti v UC Browser

Pri volaní tejto funkcie získame v registri LR adresu spomínanej tabuľky (0xB144). V R0 — index v tejto tabuľke. T. j. vezme sa hodnota z tabuľky, pripočíta sa k LR a získa sa
adresa, na ktorú sa treba prejsť. Skúsime ju vypočítať: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Prejdeme na získanú adresu a vidíme doslova pár užitočných pokynov a opäť prechod na 0xB140:

Hľadáme zraniteľnosti v UC Browser

Teraz bude prechod podľa posunu s indexom 0x20 z tabuľky.

Súdiac podľa veľkosti tabuľky, takýchto prechodov v kóde sa objaví mnoho. Vzniká otázka, či s tým môžeme bojovať nejakými automatizovanými spôsobmi, bez ručného počítania adries. A na pomoc prichádzajú skripty a možnosť patchovania 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 "Nesprávny typ operandu na", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
            table = None
        if table is None:
            print "Nemožno nájsť tabuľku"
        else:
            print "table =", hex(table)
            offset = get_wide_dword(table + (index << 2))
            put_unconditional_branch(ea, table + offset)
    else:
        print "Neznámy kód", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
    print "Nemožno detekovať prvý príkaz"

Umiesťujeme kurzor na riadok 0xB26A, spúšťame skript a vidíme prechod na 0xB4B0:

Hľadáme zraniteľnosti v UC Browser

IDA opäť nerozpoznala tento úsek ako kód. Pomôžeme jej a vidíme tam inú konštrukciu:

Hľadáme zraniteľnosti v UC Browser

Pokyny po BLX vyzerajú menej zmysluplne, zrejme ide o nejaké posunutie. Pozrieme sa na sub_4964:

Hľadáme zraniteľnosti v UC Browser

A skutočne, tu sa berie dword z adresy, ktorá leží v LR, pridáva sa k tejto adrese, po čom sa vezme hodnota z získanej adresy a uloží sa na zásobník. Tiež sa k LR pridá 4, aby sa po návrate z funkcie preskočilo toto posunutie. Po čom príkaz POP {R1} vytiahne získanú hodnotu zo zásobníka. Ak sa pozrieme, čo sa nachádza na adrese 0xB4BA + 0xEA = 0xB5A4, môžeme vidieť niečo podobné ako tabuľku adries:

Hľadáme zraniteľnosti v UC Browser

Na to, aby ste tento konštrukt opravili, bude potrebné získať dva parametre z kódu: offset a číslo registra, do ktorého treba uložiť výsledok. Pre každý možný register bude potrebné vopred pripraviť kúsok 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 "POP instruction not found"
    else:
        print "Wrong operand type on +4:", get_operand_type(ea + 4, 0)
else:
    print "Unable to detect first instructions"

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

Hľadáme zraniteľnosti v UC Browser

Kromě už zmíněných konstrukcí se v kódu také objevují tyto:

Hľadáme zraniteľnosti v UC Browser

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

Hľadáme zraniteľnosti v UC Browser

Vezmeme offset z adresy v LR, přičteme ho k LR a přejdeme na tuto adresu. 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 "Unable to detect first instruction"

Výsledek provedení skriptu:

Hľadáme zraniteľnosti v UC Browser

Po tom, ako je funkcia úplne opravená, môžeme ukázať IDA na jej skutočný začiatok. Zozbiera celý kód funkcie po častiach a ten bude možné dekompilovať pomocou HexRays.

Dekódovanie reťazcov

Naučili sme sa bojovať s obfuskáciou strojového kódu v knižnici libsgmainso-6.4.36.so z UC Browser a získali sme kód funkcie 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;
}

Pozrime sa bližšie na nasledujúce riadky:

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

Vo funkcii sub_73E24 sa zjavne dešifruje názov triedy. Ako parametre tejto funkcie sa posiela ukazovateľ na údaje, ktoré sú podobné zašifrovaným, nejaký buffer a číslo. Je zrejmé, že po zavolaní funkcie bude v buffere dešifrovaný reťazec, pretože sa posiela funkcii FindClass, ktorá akceptuje ako druhý parameter názov triedy. To znamená, že číslo je veľkosť buffera alebo dĺžka reťazca. Pokúsme sa dešifrovať názov triedy, malo by nám to ukázať, či kráčame správnym smerom. Pozrime sa podrobnejšie, čo sa deje 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;
}

Funkcia sub_7AF78 vytvára inštanciu kontajnera pre pole bajtov určitej veľkosti (nebudeme sa podrobne zaoberať týmito kontajnermi). Tu sa vytvárajú dva takéto kontajnery: do jedného sa umiestňuje reťazec „DcO/lcK+h?m3c*q@“ (nie je ťažké uhádnuť, že je to kľúč), do druhého — zašifrované dáta. Následne sa obe objekty umiestnia do určitej štruktúry, ktorá sa odovzdáva funkcii sub_6115C. Takisto si v tejto štruktúre zaznačme pole so hodnotou 3. Pozrime sa, čo sa s touto štruktúrou deje ďalej.

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

Ako parameter switch sa odovzdáva pole štruktúry, ktorému bola predtým priradená hodnota 3. Pozrime sa na case 3: vo funkcii sub_6364C sa odovzdávajú parametre zo štruktúry, ktoré boli do nej uložené v predchádzajúcej funkcii, t. j. kľúč a zašifrované dáta. Ak sa pozriete pozorne na sub_6364C, môžeme v nej nájsť algoritmus RC4.

Máme algoritmus a kľúč. Pokúsme sa dešifrovať názov triedy. Tu je, čo sme dostali: com/taobao/wireless/security/adapter/JNICLibrary. Super! Sme na správnej ceste.

Strom príkazov

Teraz musíme nájsť volanie RegisterNatives, ktoré nám ukáže funkciu doCommandNative. Prezeráme funkcie, ktoré sú volané z JNI_OnLoad, a nachádzame to 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 skutočne, tu sa registruje natívna metóda s menom doCommandNative. Teraz poznáme jej adresu. Pozrime sa, čo robí.

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 môžeme usúdiť, že tu je vstupný bod všetkých funkcií, ktoré sa vývojári rozhodli presunúť do natívnej knižnice. Zaujíma nás funkcia s číslom 10601.

Z kódu môžeme vidieť, že z čísla príkazu sa vytvárajú tri čísla: command / 10000, command % 10000 / 100 a command % 10, t. j., v našom prípade, 1, 6 a 1. Tieto tri čísla, ako aj ukazovateľ na JNIEnv a argumenty predané funkcii, sa skombinujú do štruktúry a odovzdajú sa ďalej. S pomocou získaných troch čísiel (označme ich N1, N2 a N3) sa vytvorí strom príkazov.

Približne takto:

Hľadáme zraniteľnosti v UC Browser

Strom sa vyplňuje dynamicky v JNI_OnLoad.
Tri čísla kódujú cestu v strome. Každý list stromu obsahuje adresu zodpovedajúcej funkcie. Kľúč je v rodičovskom uzle. Nájdenie miesta v kóde, kde sa do stromu pridáva potrebná funkcia, nie je veľký problém, ak sa človek vyzná vo všetkých použitých štruktúrach (ich opis nezverejňujeme, aby sme nezhustili už aj tak rozsiahly článok).

Ešte obfuskovanie

Dostali sme adresu funkcie, ktorá by mala dešifrovať prenos: 0x5F1AC. Ale zatiaľ nie je dôvod na radosť: vývojári UC Browser nám pripravili ďalšie prekvapenie.

Po získaní parametrov z poľa, ktoré bolo vytvorené v Java kóde, sa dostávame
do funkcie na adrese 0x4D070. A tu nás čaká ďalší typ obfuskovania kódu.

Ukladáme do R7 a R4 dva indexy:

Hľadáme zraniteľnosti v UC Browser

Presúvame prvý index do R11:

Hľadáme zraniteľnosti v UC Browser

Aby sme získali adresu z tabuľky, používame index:

Hľadáme zraniteľnosti v UC Browser

Po prechode na prvú adresu sa používa druhý index, ktorý je v R4. V tabuľke je 230 prvkov.

Čo s tým robiť? Môžeme IDA povedať, že je to taký switch: Edit -> Other -> Specify switch idiom.

Hľadáme zraniteľnosti v UC Browser

Vzniknutý kód je desivý. Ale pri prechádzaní jeho spletitosti si môžeme všimnúť volanie už známej funkcie sub_6115C:

Hľadáme zraniteľnosti v UC Browser

Tam bol switch, v ktorom v case 3 bola dešifrovacia funkcia s využitím algoritmu RC4. A v tomto prípade sa štruktúra odovzdávaná do funkcie plní z parametrov, ktoré boli odovzdané v doCommandNative. Spomíname, že sme tam mali magicInt s hodnotou 16. Pozrieme sa na príslušný case – a po niekoľkých prechodoch nachádzame kód, podľa ktorého môžeme identifikovať algoritmus.

Hľadáme zraniteľnosti v UC Browser

Je to AES!

Algoritmus máme, zostáva nám získať jeho parametre: režim, kľúč a možno inicializačný vektor (jeho existencia závisí od režimu práce algoritmu AES). Štruktúra s nimi sa musí vytvárať niekde pred volaním funkcie sub_6115C, ale táto časť kódu je obfuskovaná obzvlášť dobre, takže vzniká myšlienka opraviť kód, aby sa všetky parametre dešifrovacej funkcie uložili do súboru.

Patch

Aby sme nemuseli písať celý kód patcha v assemblii ručne, môžeme spustiť Android Studio, napísať tam funkciu, ktorá dostáva na vstup rovnaké parametre ako naša dešifrovacia funkcia, a zapisuje do súboru, po ktorom skopírujeme kód, ktorý vygeneruje kompilátor.

O pohodlnosti pridávania kódu sa postarali naši priatelia z tímu UC Browser. Spomíname, že na začiatku každej funkcie máme odpadový kód, ktorý je možné ľahko nahradiť akýmkoľvek iným. Je to veľmi praktické 🙂 Pravda, na začiatku cieľovej funkcie je málo miesta na kód, ktorý ukladá všetky parametre do súboru. Museli sme ho rozdeliť na časti a využiť odpadové bloky susedných funkcií. Celkovo sa nám podarilo získať štyri časti.

Prvá časť:

Hľadáme zraniteľnosti v UC Browser

V architektúre ARM sú prvé štyri parametre funkcie prenášané prostredníctvom registrov R0-R3, ostatné, ak existujú, cez zásobník. Do registra LR sa prenáša adresa návratu. Toto všetko je potrebné uložiť, aby funkcia mohla pokračovať po tom, ako si urobíme dump jej parametrov. Taktiež je potrebné uložiť všetky registre, ktoré použijeme počas procesu, preto vykonáme PUSH.W {R0-R10, LR}. V R7 sa nám získa adresa zoznamu parametrov odovzdaných funkcii cez zásobník.

Pomocou funkcie fopen otvoríme súbor /data/local/tmp/aes v režime „ab“,
t. j. na pridávanie. Do R0 načítame adresu názvu súboru, do R1 — adresu reťazca so špecifikáciou režimu. A tu odpadový kód končí, preto prechádzame na ďalšiu funkciu. Aby mohla pokračovať v práci, na začiatok nastavíme skok na skutočný kód funkcie obchádzajúci odpad, a namiesto odpadu pridáme pokračovanie patchu.

Hľadáme zraniteľnosti v UC Browser

Voláme fopen.

Prvé tri parametre funkcie aes majú typ int. Keďže sme na začiatku uložili registre do zásobníka, môžeme jednoducho odovzdať funkcii fwrite ich adresy v zásobníku.

Hľadáme zraniteľnosti v UC Browser

Ďalej máme tri štruktúry, ktoré obsahujú veľkosť dát a ukazovateľ na dáta pre kľúč, inicializačný vektor a zašifrované dáta.

Hľadáme zraniteľnosti v UC Browser

Na záver zatvoríme súbor, obnovíme registre a odovzdáme riadenie skutočnej funkcii. aes.

Zbierzeme APK s upravenou knižnicou, podpíšeme, nahodíme na zariadenie/emulátor, spustíme. Vidíme, že náš dump sa vytvára a zapisuje sa tam veľa údajov. Prehliadač používa šifrovanie nielen pre prenos, a všetko šifrovanie ide cez diskutovanú funkciu. A potrebné údaje akosi chýbajú, a v prenosu nie je vidieť potrebný dotaz. Aby sme nečakali, kým UC Browser bude ochotný vykonať potrebný dotaz, vezmeme zašifrovanú odpoveď od servera, získanú skôr, a aplikáciu patchneme znova: pridáme dešifrovanie do onCreate hlavnej 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

Zbierame, podpisujeme, inštalujeme, spúšťame. Získavame NullPointerException, pretože metóda vrátila null.

Po ďalšej analýze kódu bola objavená funkcia, v ktorej sa dešifrujú zaujímavé riadky: „META-INF/“ a „.RSA“. Vyzerá to, že aplikácia kontroluje svoj certifikát. Alebo dokonca generuje kľúče z neho. Nechce sa mi zaoberať tým, čo sa deje s certifikátom, preto mu jednoducho poskytneme správny certifikát. Opravením zašifrovanej stunky tak, aby namiesto „META-INF/“ bolo „BLABLINF/“, vytvoríme priečinok s týmto menom v APK a pridáme tam certifikát pre belkobrauzer.

Zbierame, podpisujeme, inštalujeme, spúšťame. Bingo! Kľúč máme!

MitM

Získali sme kľúč a vektor inicializácie, rovný kľúču. Pokúsime sa dešifrovať odpoveď servera v režime CBC.

Hľadáme zraniteľnosti v UC Browser

Vidíme URL archívu, niečo podobné ako MD5, „extract_unzipsize“ a číslo. Kontrolujeme: MD5 archívu súhlasí, veľkosť rozbalenej knižnice súhlasí. Pokúšame sa opraviť túto knižnicu a odovzdať ju prehliadaču. Aby sme ukázali, že naša opravená knižnica sa načítala, spustíme Intent na vytvorenie SMS s textom „PWNED!“. Nahrádzame dve odpovede od servera: puds.ucweb.com/upgrade/index.xhtml a na stiahnutie archívu. V prvej nahrádzame MD5 (veľkosť po rozbalení sa nemení), v druhej posielame archív s opravenou knižnicou.

Prehliadač sa niekoľkokrát pokúša stiahnuť archív, po čom hlási chybu. Zjavne niečo
mu nevyhovuje. V dôsledku analýzy tohto nejasného formátu sa zistilo, že server posiela aj veľkosť archívu:

Hľadáme zraniteľnosti v UC Browser

Je zakódovaná v LEB128. Po oprave sa veľkosť archívu s knižnicou trochu zmenila, preto prehliadač usúdil, že archív sa stiahol zle, a po niekoľkých pokusoch hlásil chybu.

Opravený veľkosť archívu… A – víťazstvo! 🙂 Výsledok na videu.

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

Dôsledky a reakcia vývojára

Rovnako by hackeri mohli využiť nezabezpečenú funkciu UC Browser na šírenie a spúšťanie škodlivých knižníc. Tieto knižnice by pracovali v kontexte prehliadača, preto by získali všetky jeho systémové oprávnenia. To by viedlo k možnosti zobrazovať phishingové okná a prístupu k pracovným súborom oranžového čínskeho veveričky, vrátane databáz uložených prihlasovacích údajov, hesiel a cookies.

Skontaktovali sme sa s vývojármi UC Browser a informovali ich o zistenom probléme, snažili sme sa poukázať na zraniteľnosť a jej nebezpečenstvo, ale nechceli s nami o ničom diskutovať. Medzitým prehliadač naďalej vystavoval nebezpečnú funkciu na očiach všetkých. Ale ako náhle sme zverejnili detaily zraniteľnosti, ignorovať to, ako predtým, už nebolo možné. 27. marca bola
uvedená nová verzia UC Browser 12.10.9.1193, ktorá komunikovala so serverom cez HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Okrem toho, po «oprave» a do momentu napísania tohto článku, pokus o otvorenie PDF v prehliadači spôsoboval zobrazenie chybového hlásenia s textom «Ups, niečo sa pokazilo!». Požiadavka na server pri pokuse o otvorenie PDF nebola splnená, ale požiadavka sa uskutočnila pri spustení prehliadača, čo naznačuje, že stále má možnosť načítať spúšťací kód v rozpore s pravidlami Google Play.

Zdroj: habr.com

Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Kúpte si spoľahlivý hosting pre webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster