Sebezünk sebezhetőségeket az UC Browserben

Sebezünk sebezhetőségeket az UC Browserben

Bevezetés

Március végén jelentettük, hogy felfedeztünk egy rejtett lehetőséget, amely lehetővé teszi a nem ellenőrzött kódok letöltését és futtatását az UC Browserben. Ma részletesen megvizsgáljuk, hogyan történik ez a letöltés, és hogyan használhatják ezt a hackerek a maguk céljára.

Néhány idővel ezelőtt az UC Browser-t nagyon agresszíven reklámozták és terjesztették: rosszindulatú programok segítségével telepítették a felhasználók eszközeire, különböző weboldalakon videófájlok álcájában terjesztették (azaz a felhasználók azt hitték, hogy például pornóvideót töltenek le, helyette viszont az APK-t kapták meg ezzel a böngészővel), ijesztő bannereket használtak, amelyek üzeneteket tartalmaztak arról, hogy a böngésző elavult, sebezhető, és hasonlók. Az UC Browser hivatalos VK csoportjában található téma, ahol a felhasználók panaszt tehetnek a tisztességtelen reklámokról, ott sok példa található. 2016-ban még volt egy videoreklám orosz nyelven (igen, a hirdetés egy reklámblokkoló böngészőről).

A cikk írásakor az UC Browser több mint 500 000 000 telepítést ért el a Google Playen. Ez lenyűgöző — csak a Google Chrome-nak van ennél több. A vélemények között elég sok panasz található a reklámokra és a különböző alkalmazásokra történő átirányításokra a Google Playen. Ez lett az oka a kutatásnak: úgy döntöttünk, hogy megnézzük, vajon az UC Browser nem csinál-e valami rosszat. És kiderült, hogy igenis csinál!

Az alkalmazás kódjában felfedeztük a lehetőséget, hogy végrehajtható kódot töltsenek le és futtassanak, ami ellentétes a Google Play alkalmazásainak közzétételi szabályaival. Azon kívül, hogy az UC Browser végrehajtható kódot tölt le, ezt nem biztonságos módon teszi, ami lehetővé teszi a MitM-támadások végrehajtását. Nézzük meg, sikerül-e végrehajtani egy ilyen támadást.

Minden, ami ezután következik, releváns az UC Browser azon verziójára, amely a kutatás idején a Google Playen volt:

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

Támadási vektor

Az UC Browser manifesztjében felfedezhető egy szolgáltatás, amelynek van egy beszédes neve, com.uc.deployment.UpgradeDeployService.

    Ennek a szolgáltatásnak a futtatásakor a böngésző POST kérést küld a

Ennek a szolgáltatásnak a futtatásakor a böngésző POST-igénylést hajt végre a puds.ucweb.com/upgrade/index.xhtml, amit a forgalomban észlelhetünk egy idő után a használat megkezdése után. Erre a válaszként parancsot kaphat egy frissítés vagy új modul letöltésére. Az elemzés során a szerver ilyen parancsokat nem adott, de észrevettük, hogy a PDF megnyitására tett kísérletkor a böngésző újabb kérést intéz a fent megadott címre, majd letölti a natív könyvtárat. A támadás lebonyolításához ezt a UC Browser különlegességét terveztük kihasználni: a PDF natív könyvtár használatával történő megnyitásának képességét, amely az APK-ban nincs és melyet szükség esetén online tölt le. Érdemes megjegyezni, hogy elméletileg a UC Browser képes lenne letölteni valamit felhasználói interakció nélkül is – ha megfelelően formált választ adunk a böngésző indulása után végrehajtott kérésre. De ehhez alaposabban meg kellene vizsgálni a szerverrel való interakciós protokollt, ezért úgy döntöttünk, hogy egyszerűbb a lehallgatott választ szerkeszteni és a PDF-feldolgozó könyvtárat helyettesíteni.

Tehát, amikor a felhasználó a PDF megnyitását kéri közvetlenül a böngészőben, a forgalomban a következő kérések láthatók:

Sebezünk sebezhetőségeket az UC Browserben

Először egy POST kérelem érkezik a puds.ucweb.com/upgrade/index.xhtml, ezt követően
letöltődik a PDF és irodai formátumok megtekintéséhez szükséges könyvtár archívuma. Logikus feltételezni, hogy az első kérésben információt adnak át a rendszerről (legalábbis az architektúrával kapcsolatban, hogy a megfelelő könyvtárat adják vissza), és a válaszban a böngésző valamilyen információt kap a letöltendő könyvtárról: a címét és esetleg még valamit. A probléma az, hogy ez a kérés titkosítva van.

A kérés részlete

A válasz részlete

Sebezünk sebezhetőségeket az UC Browserben

Sebezünk sebezhetőségeket az UC Browserben

A könyvtár maga ZIP fájlba csomagolt és nincs titkosítva.

Sebezünk sebezhetőségeket az UC Browserben

A forgalom dekódolásához szükséges kód keresése

Megpróbáljuk dekódolni a szerver válaszát. Megnézzük a com.uc.deployment.UpgradeDeployServiceosztálykódját: az onStartCommand metódusból átmegyünk a com.uc.deployment.b.x, onnan pedig a com.uc.browser.core.d.c.f.e:

    publikus véglegesen 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("]termék:");
            v4.append(arg9.iGX.ipR);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]verzió:");
            v4.append(arg9.iGX.iEn);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]frissítési_típus:");
            v4.append(arg9.iGX.mMode);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]kényszerített_jelző:");
            v4.append(arg9.iGX.iEo);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]csendes_mód:");
            v4.append(arg9.iGX.iDQ);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]csendes_típus:");
            v4.append(arg9.iGX.iEr);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]csendes_állapot:");
            v4.append(arg9.iGX.iEp);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]csendes_fájl:");
            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("]letöltési_típus:");
            v4.append(arg9.mDownloadType);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]letöltési_csoport:");
            v4.append(arg9.mDownloadGroup);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]letöltési_útvonal:");
            v4.append(arg9.iGH);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]apollo_gyermek_verzió:");
            v4.append(arg9.iGX.iEx);
            v4 = new StringBuilder("[");
            v4.append(v3_1);
            v4.append("]apollo_sorozat:");
            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("]komponens_név:");
                    v5.append(((au)v4_1).getName());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]komponens_verzió_név:");
                    v5.append(((au)v4_1).aDA());
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]komponens_verzió_kód:");
                    v5.append(((au)v4_1).gBl);
                    v5 = new StringBuilder("[");
                    v5.append(((au)v4_1).getName());
                    v5.append("]komponens_követelmény_típus:");
                    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("processzor_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_sorozat", arg9.iGX.iEw));
            v3_2.add(g.fs("child_ver", r.aVw()));
            v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
            v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
            v3_2.add(g.fs("frissítési_napló", i.bjt()));
            v3_2.add(g.fs("csendes_telepítés", String.valueOf(arg9.iGX.iDQ)));
            v3_2.add(g.fs("csendes_állapot", String.valueOf(arg9.iGX.iEp)));
            v3_2.add(g.fs("csendes_fájl", arg9.iGX.iEq));
            v3_2.add(g.fs("csendes_típus", 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_szint", String.valueOf(Build$VERSION.SDK_INT)));
            v3_2.add(g.fs("uc_apk_lista", 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", "igen", "sikertelen");
            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", "igen", "sikertelen");
            return;
        }
 
        if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
            this.iGY.iGI.a(arg9, "up_url", "igen", "sikertelen");
            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", "igen", "sikeres");
        this.iGY.iGI.b(arg9);
    }

Itt látható a POST-kérés kialakítása. Figyeljük meg a 16 bájtos tömb létrehozását és kitöltését: 0x5F, 0, 0x1F, -50 (=0xCE). Ez megegyezik azzal, amit a fenti kérésnél láttunk.

Ugyanebben az osztályban észlelhető egy beágyazott osztály, amelyben van egy másik érdekes metódus:

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

A metódus bemenetként egy bájt tömböt kap, és ellenőrzi, hogy a nulladik bájt 0x60 vagy a harmadik bájt 0xD0, míg a második bájt 1, 11 vagy 0x1F. Nézzük meg a szerver válaszát: a nulladik bájt – 0x60, a második – 0x1F, a harmadik – 0x60. Úgy tűnik, hogy ez az, amire szükségünk van. A sorok alapján („up_decrypt”, például) itt egy metódust kellene hívni, amely dekódolja a szerver válaszát.
Lépjünk a metódusra g.j. Vegyük észre, hogy az első argumentumként a 2. eltoláson lévő bájt (azaz 0x1F az esetünkben) kerül átadásra, a második pedig a szerver válasza az
első 16 bájt nélkül.

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

Nyilvánvaló, hogy itt a dekódolási algoritmus választása történik, és az a bájt, amely az esetünkben van.
az eset a 0x1F, három lehetséges opció egyikét jelenti.

Folytatjuk a kód elemzését. Néhány ugrás után eljutunk egy beszédes nevű metódushoz decryptBytesByKey.

Itt a válaszunktól még két bájt elkülönül, amelyekből egy string lesz. Nyilvánvaló, hogy ezen a módon választják ki a kulcsot az üzenet dekódolásához.

    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 bájt
                    System.arraycopy(bytes, 0, prefix, 0, prefix.length);
                    String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Kulcs kiválasztás
                    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;
    }

Előreugorva, megjegyezzük, hogy ezen a ponton még nem kulcsot kaptunk, csak annak "azonosítóját". A kulcs megszerzése ennél kicsit bonyolultabb.

A következő metódusban a meglévő paraméterekhez még kettő kerül, így négy lesz: a varázsszám 16, a kulcs azonosítója, a titkosított adatok és egy érthetetlen string (jelen esetben üres).

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

Egy sor ugrás után eljutunk a metódushoz staticBinarySafeDecryptNoB64 felület com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Az alkalmazás fő kódjában nincsenek olyan osztályok, amelyek megvalósítják ezt az interfészt. Ilyen osztály található a fájlban lib/armeabi-v7a/libsgmain.so, amely valójában nem .so, hanem .jar. Az általunk keresett metódus a következőképpen van megvalósítva:

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

Itt van a paraméterek listánk, amelyet további két egész számmal egészítünk ki: 2 és 0. Úgy tűnik, hogy
a 2 a dekódolást jelenti, mint a doFinal rendszerszintű osztályban javax.crypto.Cipher. Mindez pedig egy Routerhez kerül, amelynek száma 10601 — valószínűleg ez a parancs száma.

Miután átkeltünk néhány láncon, megtaláljuk az osztályt, amely megvalósítja az IRouterComponent és egy metódus doCommand:

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

Valamint az JNICLibrary, amelyben a natív metódust deklarálják doCommandNative:

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

Ezért szükségünk van a natív kódban a doCommandNativemetódus megtalálására. Itt kezdődik az igazi móka.

A gépi kód elrejtése

A fájlban libsgmain.so (amely valójában .jar, és amelyben korábban néhány interfész megvalósítását találtuk, amely a titkosítással kapcsolatos) van egy natív könyvtár: libsgmainso-6.4.36.so. Megnyitjuk az IDA-ban, és rengeteg hibaüzenetet kapunk. A probléma, hogy a szakasztáblázat (section header table) - érvénytelen. Ezt szándékosan csinálták, hogy megnehezítsék az elemzést.

Sebezünk sebezhetőségeket az UC Browserben

De nincs is rá szükség: a ELF-fájl korrekciós betöltéséhez és elemzéséhez elegendő a szegmensk táblázat (program header table). Ezért egyszerűen eltávolítjuk a szakasztáblázatot, nullázva a megfelelő mezőket a fejlécesben.

Sebezünk sebezhetőségeket az UC Browserben

Újból megnyitjuk a fájlt az IDA-ban.

Két módszer létezik arra, hogy a virtuális Java gép tudjon arról, hogy hol van a natív könyvtárban a Java kódban natív metódusként deklarált implementáció. Az egyik — hogy egy nevet adunk, amely minta szerint követi a Java_pakett_neve_KlasszNeve_metódusNeve.

A második lépés a könyvtár betöltésekor való regisztrálása (a függvényben JNI_OnLoad)
a RegisterNatives függvényhívásával A mi esetünkben, ha az első módszert alkalmazzuk, a névnek a következőnek kell lennie:.

Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative Az exportált függvények között ilyen nincs, tehát keresnünk kell a hívást..

Nézzük a függvényt A mi esetünkben, ha az első módszert alkalmazzuk, a névnek a következőnek kell lennie:.
és ezt látjuk: JNI_OnLoad Mi történik itt? Első ránézésre a függvény eleje és vége tipikus az ARM architektúrára. Az első utasítás a veremre menti a regiszterek tartalmát, amelyeket a függvény a munkája során fog használni (jelen esetben R0, R1 és R2), valamint az LR regisztert, amelyben a függvényből való visszatérés címe található. Az utolsó utasítás a rögzített regiszterek helyreállítása, ahol a visszatérési cím azonnal a PC regiszterbe kerül — így történik a visszatérés a függvényből. De ha alaposabban megnézzük, észrevehetjük, hogy a második utolsó utasítás megváltoztatja a veremben tárolt visszatérési címet. Számítsuk ki, hogy mi lesz az a kód végrehajtása után.

Sebezünk sebezhetőségeket az UC Browserben

Az R1-be betöltik egy bizonyos címet, 0xB130, amiből levonunk 5-öt, majd ezt áthelyezzük az R0-ba, és hozzáadunk 0x10-et. Tehát kapunk 0xB13B. Így az IDA azt hiszi, hogy az utolsó utasításban egy szokásos visszatérés történik, valójában pedig egy ugrás történik a kiszámított címre, 0xB13B.
Emlékezzünk arra, hogy az ARM processzoroknak két módjuk és két utasítássorozatuk van: ARM és Thumb. A cím legkisebb bitje jelzi a processzornak, hogy melyik utasítássorozatot használják. Tehát a cím valójában 0xB13A, és az alacsonyabb bitben lévő 1 a Thumb módot jelöli.

Minden függvény elejére ebben a könyvtárban hasonló „átjárót” és

zajkódot adtak hozzá. Továbbra sem részletezzük őket — csak emlékezzünk rá,
hogy a legtöbb függvény valódi kezdete kicsit később van.
Mivel a kódban nincs kifejezett ugrás a 0xB13A-ra, az IDA maga nem ismerte fel, hogy itt kód található. Ugyanezen okból a könyvtárban a kód jelentős részét nem ismeri fel kódként, ami némiképpen megnehezíti az elemzést. Mondjuk az IDA-nak, hogy itt kód van, és ez az, ami látható:

A 0xB144-nél egy táblázat kezdete egyértelműen látható. Mi a helyzet a sub_494C-vel?

Sebezünk sebezhetőségeket az UC Browserben

A függvény hívásakor az LR regiszterben megkapjuk a korábban említett táblázat címét (0xB144). Az R0-ban — az indexet a táblázatban. Tehát egy érték kerül a táblázatból, hozzáadódik az LR-hez, és megkapjuk.

Sebezünk sebezhetőségeket az UC Browserben

Amikor ezt a funkciót hívjuk, az LR regiszterben megkapjuk az említett táblázat címét (0xB144). Az R0-ban – az index ebben a táblázatban. Tehát az érték a táblázatból veszi, hozzáadódik az LR-hez, és ez keletkezik.
az a cím, ahová el kell jutni. Megpróbáljuk kiszámítani: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Lépjünk a kapott címre, és látunk egy pár hasznos utasítást, majd újra átmegyünk 0xB140-ra:

Sebezünk sebezhetőségeket az UC Browserben

Most a 0x20 eltolású indexre lépünk a táblázatból.

A tábla méretének alapján sok ilyen ugrást találunk a kódban. Felmerül a kérdés, hogyan lehetne ezt egy kicsit automatizálni, anélkül, hogy kézzel kellene kiszámolni a címeket. Itt jönnek a segítségünkre a szkriptek és a kód IDA-ban való patch-elésének lehetősége:

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 "Hibás operand típus: ", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
            table = None
        if table is None:
            print "A táblázat nem található"
        else:
            print "tábla =", hex(table)
            offset = get_wide_dword(table + (index << 2))
            put_unconditional_branch(ea, table + offset)
    else:
        print "Ismeretlen kód", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
    print "Nem sikerült észlelni az első utasítást"

Helyezd a kurzort a 0xB26A sorra, futtasd a szkriptet és látni fogod az ugrást a 0xB4B0-ra:

Sebezünk sebezhetőségeket az UC Browserben

IDA ismét nem ismerte fel ezt a szakaszt kódnak. Segítünk neki, és ott más szerkezetet látunk:

Sebezünk sebezhetőségeket az UC Browserben

A BLX utáni utasítások nem tűnnek túl értelmesnek, inkább valamilyen eltolásnak tűnnek. Nézzük meg a sub_4964-et:

Sebezünk sebezhetőségeket az UC Browserben

Valóban, itt egy dword értéket veszünk az LR-ben lévő címről, hozzáadunk ehhez a címhez, majd a kapott címnél lévő értéket betesszük a verembe. Az LR-hez 4-et is hozzáadunk, hogy a függvényből való visszatéréskor átugorjuk ezt az eltolást. Ezt követően a POP {R1} utasítás kinyeri a kapott értéket a veremből. Ha megnézzük, mi található a 0xB4BA + 0xEA = 0xB5A4 címnél, akkor láthatunk valami hasonlót, mint egy címek táblája:

Sebezünk sebezhetőségeket az UC Browserben

Ahhoz, hogy ezt a konstrukciót javítsuk, meg kell szereznünk két paramétert a kódból: az eltolást és a regisztert, amelybe a результата kell helyezni. Minden lehetséges regiszterhez előre elő kell készítenünk egy kódrészletet.

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 utasítás nem található"
    else:
        print "Hibás operandustípus +4-nél:", get_operand_type(ea + 4, 0)
else:
    print "Nem sikerült észlelni az első utasításokat"

Helyezzük a kurzort a kicserélni kívánt konstrukció elejére — 0xB4B2 — és futtassuk a szkriptet:

Sebezünk sebezhetőségeket az UC Browserben

Az eddig említett konstrukciók mellett a kódban ezek is előfordulnak:

Sebezünk sebezhetőségeket az UC Browserben

Ahogy az előző esetben, a BLX utasítás után ismét egy eltolás következik:

Sebezünk sebezhetőségeket az UC Browserben

Elvégezzük az eltolást az LR-ből származó címhez, hozzáadjuk az LR-hez, és ott folytatjuk. 0x72044 + 0xC = 0x72050. A szkript ennek a konstrukciónak egészen egyszerű:

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 "Nem sikerült észlelni az első utasítást"

A szkript futtatásának eredménye:

Sebezünk sebezhetőségeket az UC Browserben

Miután a funkció minden részét javítottuk, megadhatjuk az IDA-t a valódi kezdetére. Ez összegyűjti a funkció összes kódját darabonként, és dekódolhatjuk a HexRays segítségével.

A karakterláncok dekódolása

Megtudtuk, hogyan küzdjünk meg a gépi kódok obfuszkálásával a könyvtárban libsgmainso-6.4.36.so az UC Browserből és megkaptuk a funkció kódját. 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;
}

Vizsgáljuk meg részletesebben a következő sorokat:

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

A funkcióban sub_73E24 nyilvánvalóan a klassznév dekódolását végzi. A funkció paramétereként egy, a titkosítottakra hasonlító adatokra mutató pointer, egy puffer és egy szám kerül átadásra. Nyilvánvaló, hogy a funkció hívása után a pufferben dekódolt karakterlánc lesz, mivel azt a funkciónak adják át. FindClass, amely második paraméterként a klassz nevét várja. Így a szám a puffer mérete vagy a karakterlánc hossza. Próbáljuk meg dekódolni a klassz nevét, ez meg kell, hogy mondja nekünk, hogy jó irányba haladunk-e. Nézzük meg részletesebben, hogy mi történik a sub_73E24-ben.

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

Funkció sub_7AF78 példányt hoz létre a megadott méretű bájtok tömbjei számára (nem megyünk részletekbe ezekkel a tárolókkal). Itt két ilyen tároló jön létre: az egyikben egy sorozat található „DcO/lcK+h?m3c*q@” (nem nehéz kitalálni, hogy ez a kulcs), a másikban pedig a titkosított adatok. Ezután mindkét objektum egy struktúrába kerül, amelyet átadunk a funkciónak sub_6115C. Ezen a struktúrán is észlelhetünk egy mezőt, amelynek értéke 3. Nézzük meg, mi történik ezzel a struktúrával a továbbiakban.

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

A switch paraméterként átadjuk egy struktúra mezőjét, amely korábban 3 értéket kapott. Nézzük a case 3: a funkcióban sub_6364C átadjuk a struktúrából az előző funkcióban tárolt paramétereket, azaz a kulcsot és a titkosított adatokat. Ha alaposan ránézünk sub_6364C, az RC4 algoritmus ismeretéhez juthatunk.

Van egy algoritmusunk és egy kulcsunk. Próbáljuk meg visszafejteni az osztály nevét. Íme, mi lett az eredmény: com/taobao/wireless/security/adapter/JNICLibrary. Tökéletes! Jól haladunk.

Parancsfagyi fa

Most meg kell találni a hívást A mi esetünkben, ha az első módszert alkalmazzuk, a névnek a következőnek kell lennie:, amely megmutatja nekünk a funkciót doCommandNative. Átnézzük azokat a funkciókat, amelyek a következő helyről hívják meg JNI_OnLoad, és megtaláljuk a következőben 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;
}

Valóban, itt regisztrálunk egy natív módszert a névvel doCommandNative. Most már tudjuk a címét. Nézzük meg, mit csiná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;
}

A nevéből sejthetjük, hogy itt van az összes funkció belépési pontja, amelyet a fejlesztők a natív könyvtárba kívántak áthelyezni. Az 10601-es számú függvény érdekel bennünket.

A kódból látható, hogy a parancsszámból három számot kapunk: command / 10000, command % 10000 / 100 és command % 10, azaz az esetünkben 1, 6 és 1. Ez a három szám, valamint a mutató a JNIEnv és a függvénynek átadott argumentumok egy struktúrába kerülnek összegzésre, és továbbítódnak. A kapott három szám (jelöljük őket N1, N2 és N3) felhasználásával parancsfagyi fát alakítunk ki.

Körülbelül így néz ki:

Sebezünk sebezhetőségeket az UC Browserben

A fa dinamikusan töltődik fel JNI_OnLoad.
Három szám kódolja az utat a fában. A fa minden levélje tartalmazza a megfelelő funkció címét. A kulcs a szülő csomópontban található. Megtalálni a helyet a kódban, ahol a szükséges funkció hozzáadódik a fához, nem nehéz, ha megértjük az összes használt struktúrát (a leírásukat nem adjuk meg, hogy ne feszegessük túl az amúgy is hosszú cikket).

További obfuszkálás

Megkaptuk a funkció címét, amelynek a forgalmat kellene dekódolnia: 0x5F1AC. De még korai az öröm: az UC Browser fejlesztői egy újabb meglepetéssel készültek.

Miután megkaptuk a paramétereket a Java-kódban létrehozott tömbből, eljutunk
a 0x4D070 című funkcióhoz. És itt egy újabb kódobfuszkálás vár ránk.

Két indexet helyezünk el az R7 és R4 regiszterekben:

Sebezünk sebezhetőségeket az UC Browserben

Az első indexet az R11-be helyezzük:

Sebezünk sebezhetőségeket az UC Browserben

A cím megkapásához a táblázatból a következő indexet használjuk:

Sebezünk sebezhetőségeket az UC Browserben

Az első címre való ugrás után a második indexet használjuk, amely az R4-ben van. A táblázatban 230 elem található.

Mit tegyünk ezzel? Megmondhatjuk az IDA-nak, hogy ez egy switch: Edit -> Other -> Specify switch idiom.

Sebezünk sebezhetőségeket az UC Browserben

A kapott kód ijesztő. De a rejtelmein keresztül haladva észlelhetjük a már ismert funkció hívását. sub_6115C:

Sebezünk sebezhetőségeket az UC Browserben

Ott egy switch volt, amelyben a 3-as esetnél az RC4 algoritmus használatával történt dekódolás található. Ebben az esetben a funkciónak átadott struktúrát a paraméterek tölti ki, amelyeket a doCommandNative. Emlékezzünk arra, hogy ott volt nekünk a magicInt értékkel 16. Nézzük meg a megfelelő esetet - és néhány ugrás után megtaláljuk a kódot, amellyel az algoritmus azonosítható.

Sebezünk sebezhetőségeket az UC Browserben

Ez az AES!

Van algoritmusunk, most meg kell szereznünk a paramétereit: mód, kulcs és esetleg inicializáló vektor (a megléte az AES algoritmus működési módjától függ). A struktúrának valahol a funkció hívása előtt kell létrejönnie, sub_6115Cde ez a kód rész különösen jól obfuszkálva van, így felmerül az ötlet, hogy megpatch-eljük a kódot, hogy a dekódoló funkció összes paramétere a fájlba dumpolódjon.

Patch

Annak érdekében, hogy ne kelljen manuálisan megírni a patch kódot assembly nyelven, elindíthatjuk az Android Studiot, írhatunk egy olyan funkciót, amely ugyanolyan paramétereket kap, mint a dekódoló funkciónk, és a fájlba ír, majd bemásolhatjuk a kódot, amelyet a fordító generál.

A kód hozzáadásának kényelméről a UC Browser csapatának barátai is "gondoskodtak". Emlékezzünk, hogy minden funkció elején hulladékkódunk van, amit könnyen lecserélhetünk bármire. Nagyon praktikus 🙂 Igaz, hogy a célnak megfelelő funkció elején nem sok hely van az összes paraméter fájlba mentésére, ezért szét kellett darabolnunk és a szomszédos funkciók hulladékblokkjait kellett használnunk. Összesen négy részlettel végeztük el.

Első rész:

Sebezünk sebezhetőségeket az UC Browserben

Az ARM architektúrában az első négy paraméter a R0-R3 regisztereken keresztül kerül átadásra, a többi, ha van, a veremben. Az LR regiszterben a visszatérési cím található. Ezt mind meg kell őriznünk, hogy a funkció képes legyen működni, miután kiírtuk a paramétereit. Emellett meg kell őriznünk az összes regisztert, amit a folyamat során használunk, ezért végrehajtjuk a PUSH.W {R0-R10,LR} parancsot. Az R7 regiszterben kapjuk meg a funkcióhoz a veremben átadott paraméterek listájának címét.

A fopen funkció segítségével megnyitjuk a fájlt /data/local/tmp/aes «ab» módban,
azaz hozzáfűzésre. Az R0-ba a fájl nevét tartalmazó cím, az R1-be a módot jelző karakterlánc címe kerül. Itt véget ér a hulladék kód, ezért áttérünk a következő funkcióra. Ahhoz, hogy ez folytatódjon, a hulladékkód helyett valódi kódra történő ugrást helyezünk a kezdetére, és a hulladék helyett a folytatást adjuk hozzá.

Sebezünk sebezhetőségeket az UC Browserben

Hívjuk meg a fopen.

funkciót. Az első három paraméter típusa aes . Mivel az elején megőriztük a regisztereket a veremben, egyszerűen átadhatjuk a funkciónak egésza veremben található címüket. fwrite Ezután három struktúránk van, amelyek az adatok méretét és a kulcs, inicializáló vektor és titkosított adatokra mutató mutatót tartalmaznak.

Sebezünk sebezhetőségeket az UC Browserben

Végül bezárjuk a fájlt, helyreállítjuk a regisztereket és átadjuk az irányítást az igazi funkciónak.

Sebezünk sebezhetőségeket az UC Browserben

Összegyűjtjük az APK-t a javított könyvtárral, aláírjuk, feltöltjük az eszközre/emulátorra, elindítjuk. Látjuk, hogy a dumpunk létrejön, és sok adat kerül bele. A böngésző nemcsak az adatforgalmat titkosítja, és minden titkosítás a vizsgált funkción keresztül történik. De a szükséges adatok valamiért nem találhatóak, és a forgalomban nem látni a szükséges lekérést. Hogy ne kelljen megvárnunk, míg a UC Browser elvégezheti a szükséges lekérést, elvesszük a korábban kapott titkos választ a szervertől, és még egyszer patchelni fogjuk az alkalmazást: hozzáadunk egy dekódolást a fő tevékenység onCreate metódusába. aes.

Az alkalmazás befejezése után.

    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

Összeállítjuk, aláírjuk, telepítjük, elindítjuk. NullPointerException-t kapunk, mivel a metódus null-t adott vissza.

A kód további elemzése során felfedeztük egy függvényt, amelyben érdekes sorokat dekódolunk: "META-INF\/" és ".RSA". Úgy tűnik, az alkalmazás ellenőrzi a saját tanúsítványát. Vagy talán kulcsokat generál abból. Nem akarjuk kideríteni, mi történik a tanúsítvánnyal, ezért egyszerűen megadunk neki egy helyes tanúsítványt. A titkosított sort úgy patch-eljük meg, hogy "META-INF\/" helyett "BLABLINF\/" legyen, létrehozunk egy mappát ezzel a névvel az APK-ban, és belehelyezzük a belkó böngésző tanúsítványát.

Összeállítjuk, aláírjuk, telepítjük, elindítjuk. Bingó! Megvan a kulcs!

MitM

Megkaptuk a kulcsot és az inicializáló vektort, ami megegyezik a kulccsal. Próbáljuk dekódolni a szerver válaszát CBC módban.

Sebezünk sebezhetőségeket az UC Browserben

Meglátjuk az archívum URL-jét, ami valami MD5-re hasonlít, "extract_unzipsize" és egy számot. Ellenőrizzük: az archívum MD5-e stimmel, a kibővített könyvtár mérete is megegyezik. Próbáljuk meg patch-elni ezt a könyvtárat, és átadni a böngészőnek. Annak érdekében, hogy megmutassuk, hogy a patch-elt könyvtárunk betöltődött, Intent-et indítunk, hogy létrehozzunk egy SMS-t a következő szöveggel: "PWNED!". Két választ fogunk módosítani a szervertől: puds.ucweb.com/upgrade/index.xhtml és az archívum letöltésénél. Az első esetben módosítjuk az MD5-öt (a kibővített méret nem változik), a második esetben a patch-elt könyvtárral adjuk át az archívumot.

A böngésző többször megpróbálja letölteni az archívumot, majd hibát ad. Nyilvánvalóan valami
nem tetszik neki. A zavaros formátum elemzése után kiderült, hogy a szerver az archívum méretét is átadja:

Sebezünk sebezhetőségeket az UC Browserben

LEB128-ban van kódolva. A patch után az archívum mérete egy kicsit megváltozott, ezért a böngésző azt hitte, hogy az archívum hibásan töltődött le, és néhány próbálkozás után hibát adott.

Javítjuk az archívum méretét... És – győzelem! 🙂 Az eredmény videón.

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

A következmények és a fejlesztő reakciója

Hasonló módon a hackerek kihasználhatták volna a UC Browser nem biztonságos funkcióját, hogy kártékony könyvtárakat terjesszenek és futtassanak. Ezek a könyvtárak a böngésző kontextusában fognak működni, így megkapják az összes rendszerszintű engedélyét. Ennek következményeként - a lehetőség arra, hogy phishing ablakokat mutassanak, valamint hozzáférést kapjanak az Orange Kínai Mókus munkafájljaihoz, beleértve az adatbázisban tárolt bejelentkezési névjegyeket, jelszavakat és sütiket.

Kapcsolatba léptünk a UC Browser fejlesztőivel, és tájékoztattuk őket a felfedezett problémáról, próbáltunk rámutatni a sebezhetőségre és annak veszélyére, de nem akartak velünk semmilyen témát megvitatni. Eközben a böngésző továbbra is a veszélyes funkcióval büszkélkedett, mindenki szeme előtt. De miután felfedtük a sebezhetőség részleteit, már nem lehetett figyelmen kívül hagyni, ahogy korábban. Március 27-én
kiadták az UC Browser 12.10.9.1193 új verzióját, amely HTTPS-en keresztül kapcsolódott a szerverhez. puds.ucweb.com/upgrade/index.xhtml.

Ezen kívül a 'javítás' után és a cikk megírásáig a PDF megnyitási kísérlet a böngészőben egy 'Oops, valami rosszul sült el!' üzenettel végződött. A PDF megnyitásakor a szerverhez intézett kérés nem teljesült, de a böngésző indításakor a kérés megtörtént, ami arra utal, hogy a kód végrehajtásának lehetősége továbbra is fennáll, megszegve a Google Play szabályait.

Forrás: habr.com

Megbízható hosting vásárlása weboldalak számára, DDoS védelemmel, VPS VDS szerverek. 🔥 Megbízható webtárhely vásárlása DDoS védelemmel, VPS VDS szerverek | ProHoster