Sebezhetőségek keresése az UC Browserben

Sebezhetőségek keresése az UC Browserben

Bevezetés

Március végén mi jelentették, hogy felfedeztek egy rejtett képességet a nem ellenőrzött kód betöltésére és futtatására az UC Browserben. Ma részletesen megvizsgáljuk, hogyan történik ez a letöltés, és hogyan használhatják fel a hackerek saját céljaikra.

Valamivel 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, amelyeket különböző oldalakról terjesztettek videofájlok leple alatt (azaz a felhasználók azt hitték, hogy például pornóvideót töltenek le, de ehelyett egy APK-t kapott ezzel a böngészővel), ijesztő szalaghirdetéseket használt, amelyek azt jelezték, hogy a böngésző elavult, sebezhető és hasonlók. A VK hivatalos UC Browser csoportjában van téma, amelyben a felhasználók tisztességtelen reklámozásra panaszkodhatnak, számos példa van rá. 2016-ban még volt videó reklám oroszul (igen, reklámblokkoló böngésző).

A cikk írásakor az UC Browser több mint 500 000 000 telepítve van a Google Playen. Ez lenyűgöző – ennél csak a Google Chrome rendelkezik. A vélemények között meglehetősen sok panaszt láthat a hirdetésekkel és a Google Play egyes alkalmazásaira való átirányításokkal kapcsolatban. Ez volt az oka kutatásunknak: úgy döntöttünk, hogy megnézzük, nem csinál-e valami rosszat az UC Browser. És kiderült, hogy igen!

Az alkalmazás kódjában felfedezték a végrehajtható kód letöltésének és futtatásának lehetőségét, ami ellentétes a pályázatok közzétételére vonatkozó szabályokkal a Google Playen. A futtatható kód letöltése mellett az UC Browser ezt nem biztonságos módon teszi, amivel MitM támadást is indíthat. Lássuk, végre tudunk-e hajtani egy ilyen támadást.

Az alábbiakban leírtak az UC Browser azon verziójára vonatkoznak, amely a vizsgálat idején elérhető volt a Google Playen:

package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c

Támadás vektor

Az UC Browser jegyzékében egy magától értetődő nevű szolgáltatást talál com.uc.deployment.UpgradeDeployService.

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

Amikor ez a szolgáltatás elindul, a böngésző POST kérést küld a címre puds.ucweb.com/upgrade/index.xhtml, ami a rajt után valamivel látható a forgalomban. Válaszul parancsot kaphat valamilyen frissítés vagy új modul letöltésére. Az elemzés során a szerver nem adott ilyen parancsokat, de azt vettük észre, hogy amikor megpróbálunk megnyitni egy PDF-t a böngészőben, akkor egy második kérést küld a fent megadott címre, ami után letölti a natív könyvtárat. A támadás végrehajtásához úgy döntöttünk, hogy az UC Browser ezen funkcióját használjuk: a PDF megnyitásának lehetőségét egy natív könyvtár használatával, amely nem szerepel az APK-ban, és amelyet szükség esetén letölt az internetről. Érdemes megjegyezni, hogy elméletileg az UC Browser felhasználói beavatkozás nélkül is kényszeríthető valami letöltésére – ha jól megformált választ adunk a böngésző elindítása után végrehajtott kérésre. Ehhez azonban részletesebben meg kell vizsgálnunk a szerverrel való interakció protokollját, ezért úgy döntöttünk, hogy könnyebb lesz szerkeszteni az elfogott választ, és lecserélni a könyvtárat a PDF-el való munkavégzéshez.

Tehát amikor egy felhasználó közvetlenül a böngészőben akar megnyitni egy PDF-fájlt, a következő kérések jelennek meg a forgalomban:

Sebezhetőségek keresése az UC Browserben

Először egy POST kérés érkezik a címre puds.ucweb.com/upgrade/index.xhtml, azután
Letöltésre kerül egy archívum könyvtárral a PDF és az irodai formátumok megtekintéséhez. Logikus az a feltételezés, hogy az első kérés információt továbbít a rendszerről (legalábbis a szükséges könyvtárat biztosító architektúráról), és erre válaszul a böngésző kap néhány információt a letöltendő könyvtárról: a címet és esetleg , valami más. A probléma az, hogy ez a kérés titkosított.

Kérelem töredék

Válaszrészlet

Sebezhetőségek keresése az UC Browserben

Sebezhetőségek keresése az UC Browserben

Maga a könyvtár ZIP-be van csomagolva, és nincs titkosítva.

Sebezhetőségek keresése az UC Browserben

Keresse meg a forgalom visszafejtő kódját

Próbáljuk megfejteni a szerver válaszát. Nézzük az osztálykódot com.uc.deployment.UpgradeDeployService: módszerből onStartCommand menj com.uc.deployment.bx, és abból a com.uc.browser.core.dcfe:

    public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}

Itt látjuk a POST kérés kialakulását. Figyelünk egy 16 bájtos tömb létrehozására és annak kitöltésére: 0x5F, 0, 0x1F, -50 (=0xCE). Egybeesik azzal, amit a fenti kérésben láttunk.

Ugyanebben az osztályban láthat egy beágyazott osztályt, amely egy másik érdekes módszerrel rendelkezik:

        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 egy bájttömböt vesz fel bemenetként, és ellenőrzi, hogy a nulla bájt 0x60 vagy a harmadik bájt 0xD0, a második bájt pedig 1, 11 vagy 0x1F. Megnézzük a szerver válaszát: a nulla bájt 0x60, a második 0x1F, a harmadik 0x60. Úgy hangzik, amire szükségünk van. A sorokból ítélve (például „up_decrypt”) itt kell meghívni egy metódust, amely visszafejti a szerver válaszát.
Térjünk át a módszerre gj. Vegye figyelembe, hogy az első argumentum a 2-es eltolásnál lévő bájt (azaz esetünkben 0x1F), a második pedig a szerver válasza nélkül.
első 16 bájt.

     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 kiválasztunk egy dekódoló algoritmust, és ugyanazt a bájtot, amely a miénkben van
kis- és nagybetű 0x1F, a három lehetséges opció egyikét jelöli.

Folytatjuk a kód elemzését. Pár ugrás után egy magától értetődő nevű módszerben találjuk magunkat decryptBytesByKey.

Itt további két bájt választ el a válaszunktól, és egy karakterláncot kapunk belőlük. Nyilvánvaló, hogy ilyen módon kerül kiválasztásra az üzenet visszafejtésére szolgáló kulcs.

    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 байта
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Выбор ключа
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;
}

A jövőre nézve megjegyezzük, hogy ebben a szakaszban még nem kapunk kulcsot, csak annak „azonosítóját”. A kulcs beszerzése kicsit bonyolultabb.

A következő metódusban a meglévők mellé még két paramétert adunk, így négy lesz belőlük: a 16-os varázsszám, a kulcsazonosító, a titkosított adat és egy értelmezhetetlen karakterlánc (esetünkben üres).

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

Egy sor átmenet után elérkezünk a módszerhez staticBinarySafeDecryptNoB64 felület com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. A fő alkalmazáskódban nincsenek olyan osztályok, amelyek megvalósítják ezt a felületet. Van egy ilyen osztály a fájlban lib/armeabi-v7a/libsgmain.so, ami valójában nem .so, hanem .jar. A minket érdeklő módszer a következőképpen valósul meg:

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 < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}

Itt a paraméterlistánk két további egész számmal egészül ki: 2 és 0. Ebből ítélve
mindent, a 2 dekódolást jelent, mint a metódusban doFinal rendszer osztály javax.crypto.Cipher. És mindez átkerül egy bizonyos 10601-es számú routerre - ez nyilvánvalóan a parancs száma.

A következő átmeneti lánc után találunk egy osztályt, amely megvalósítja az interfészt IRouterComponent és módszer 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);
}
}

És az osztály is JNICLibrary, amelyben a natív metódus deklarálva van doCommandNative:

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

Ez azt jelenti, hogy meg kell találnunk egy metódust a natív kódban doCommandNative. És itt kezdődik a móka.

A gépi kód elhomályosítása

Fájlban libsgmain.so (ami valójában egy .jar, és amelyen néhány titkosítással kapcsolatos interfész megvalósítását találtuk fent) van egy natív könyvtár: libsgmainso-6.4.36.so. Megnyitjuk az IDA-ban, és egy csomó hibás párbeszédablakot kapunk. A probléma az, hogy a szakaszfejléc tábla érvénytelen. Ez szándékosan történik az elemzés bonyolítása érdekében.

Sebezhetőségek keresése az UC Browserben

De nincs rá szükség: egy ELF fájl helyes betöltéséhez és elemzéséhez elegendő egy programfejléc táblázat. Ezért egyszerűen töröljük a szakasztáblázatot, nullázva a megfelelő mezőket a fejlécben.

Sebezhetőségek keresése az UC Browserben

Nyissa meg újra a fájlt az IDA-ban.

Kétféleképpen lehet megmondani a Java virtuális gépnek, hogy a natív könyvtárban pontosan hol található a Java kódban natívként deklarált metódus megvalósítása. Az első az, hogy adjunk neki egy fajnevet Java_package_name_ClassName_MethodName.

A második az, hogy regisztrálja a könyvtár betöltésekor (a függvényben JNI_OnLoad)
függvényhívás segítségével Regisztráció Natives.

Esetünkben, ha az első módszert használjuk, a név a következő legyen: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Az exportált függvények között nincs ilyen funkció, ami azt jelenti, hogy hívást kell keresni Regisztráció Natives.
Menjünk a függvényre JNI_OnLoad és ezt a képet látjuk:

Sebezhetőségek keresése az UC Browserben

Mi folyik itt? Első pillantásra a funkció kezdete és vége jellemző az ARM architektúrára. A verem első utasítása tárolja azon regiszterek tartalmát, amelyeket a függvény működése során használni fog (jelen esetben R0, R1 és R2), valamint az LR regiszter tartalmát, amely tartalmazza a függvény visszatérési címét. . Az utolsó utasítás visszaállítja az elmentett regisztereket, és a visszatérési cím azonnal bekerül a PC regiszterébe - így visszatér a funkcióból. De ha alaposan megnézed, észre fogod venni, hogy az utolsó előtti utasítás megváltoztatja a veremben tárolt visszatérési címet. Számoljuk ki, milyen lesz utána
kód végrehajtása. Egy bizonyos 1xB0 címet betöltünk az R130-be, ebből levonunk 5-öt, majd átkerül az R0-ba, és hozzáadjuk a 0x10-et. Kiderült, hogy 0xB13B. Így az IDA úgy gondolja, hogy az utolsó utasítás egy normál függvényvisszaadás, de valójában a számított 0xB13B címre megy.

Itt érdemes felidézni, hogy az ARM processzorok két üzemmóddal és két utasításkészlettel rendelkeznek: ARM és hüvelykujj. A cím legkisebb jelentőségű bitje közli a processzorral, hogy melyik utasításkészletet használja. Vagyis a cím valójában 0xB13A, és a legkisebb jelentőségű bitben az egyik a hüvelykujj módot jelzi.

Egy hasonló „adapter” minden függvény elejére került ebben a könyvtárban és
szemét kód. Nem foglalkozunk velük részletesebben – csak emlékezünk
hogy szinte minden függvény valódi kezdete kicsit távolabb van.

Mivel a kód nem ugrik kifejezetten 0xB13A-ra, az IDA maga nem ismerte fel, hogy a kód ezen a helyen található. Ugyanezen okból kifolyólag nem ismeri fel kódként a legtöbb kódot a könyvtárban, ami némileg megnehezíti az elemzést. Megmondjuk az IDA-nak, hogy ez a kód, és ez történik:

Sebezhetőségek keresése az UC Browserben

A táblázat egyértelműen 0xB144-gyel kezdődik. Mi van a sub_494C-ben?

Sebezhetőségek keresése az UC Browserben

Ennek a függvénynek az LR regiszterben történő meghívásakor a korábban említett tábla címét kapjuk (0xB144). R0-ban - index ebben a táblázatban. Vagyis az értéket a táblázatból veszik, hozzáadják az LR-hez és az eredmény
a cím, ahová menni kell. Próbáljuk meg kiszámolni: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Megyünk a kapott címre, és szó szerint látunk néhány hasznos utasítást, majd ismét a 0xB140-hez megyünk:

Sebezhetőségek keresése az UC Browserben

Most lesz egy átmenet eltoláskor 0x20 indexszel a táblázatból.

A táblázat méretéből ítélve sok ilyen átmenet lesz a kódban. Felmerül a kérdés, hogy lehet-e ezt valahogy automatikusabban kezelni, kézi címszámítás nélkül. A szkriptek és az IDA kódjavítási képessége pedig a segítségünkre vannak:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 2
if get_wide_word(ea1) == 0xbf00: #NOP
ea1 += 2
if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
index = get_wide_dword(get_operand_value(ea1, 1))
print "index =", hex(index)
ea1 += 2
if get_operand_type(ea1, 0) == 7:
table = get_operand_value(ea1, 0) + 4
elif get_operand_type(ea1, 1) == 2:
table = get_operand_value(ea1, 1) + 4
else:
print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Unable to find table"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Unable to detect first instruction"

Helyezze a kurzort a 0xB26A sorra, futtassa a szkriptet, és nézze meg az átmenetet a 0xB4B0-ra:

Sebezhetőségek keresése az UC Browserben

Az IDA ismét nem ismerte fel kódként ezt a területet. Segítünk neki, és egy másik dizájnt látunk ott:

Sebezhetőségek keresése az UC Browserben

Úgy tűnik, a BLX utáni utasításoknak nem sok értelme van, inkább valami elmozdulás. Nézzük a sub_4964-et:

Sebezhetőségek keresése az UC Browserben

És valóban, itt egy dword-ot veszünk az LR-ben lévő címen, hozzáadjuk ehhez a címhez, majd a kapott címen lévő értéket veszik és a verembe helyezik. Ezenkívül 4-et adunk az LR-hez, így a függvényből való visszatérés után ugyanez az eltolás kimarad. Ezt követően a POP {R1} parancs átveszi a kapott értéket a veremből. Ha megnézi, hogy mi található a 0xB4BA + 0xEA = 0xB5A4 címen, akkor valami hasonlót fog látni, mint egy címtáblázat:

Sebezhetőségek keresése az UC Browserben

Ennek a tervnek a foltozásához két paramétert kell lekérnie a kódból: az eltolást és a regiszterszámot, amelybe az eredményt el szeretné helyezni. Minden lehetséges regisztrációhoz előzetesen el kell készítenie 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 << i):
register = i
break
if register == -1:
print "Unable to detect register"
else:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
if ea % 4 != 0:
ea += 2
patch_dword(ea, address)
elif pop[:3] == 'x5dxf8x04':
register = ord(pop[3]) >> 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"

Helyezzük a kurzort a lecserélni kívánt szerkezet elejére - 0xB4B2 -, és futtassuk a szkriptet:

Sebezhetőségek keresése az UC Browserben

A kód a már említett struktúrákon kívül a következőket is tartalmazza:

Sebezhetőségek keresése az UC Browserben

Az előző esethez hasonlóan a BLX utasítás után van egy eltolás:

Sebezhetőségek keresése az UC Browserben

Az eltolást a címhez vesszük LR-ből, hozzáadjuk az LR-hez és oda megyünk. 0x72044 + 0xC = 0x72050. Ennek a tervnek a forgatókönyve meglehetősen egyszerű:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((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"

A szkript végrehajtásának eredménye:

Sebezhetőségek keresése az UC Browserben

Miután mindent javított a függvényben, rámutathat az IDA-ra a valódi kezdetére. Összeállítja az összes függvénykódot, és a HexRays segítségével visszafordítható.

Húrok dekódolása

Megtanultuk kezelni a gépi kódok elhomályosítását a könyvtárban libsgmainso-6.4.36.so az UC Browserből, és megkapta a funkciókódot 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;
}

Nézzük meg közelebbről a következő sorokat:

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

Funkcióban sub_73E24 az osztálynév egyértelműen dekódolás alatt áll. A funkció paramétereiként a titkosított adatokhoz hasonló adatokra mutató mutató, egy bizonyos puffer és egy szám kerül átadásra. Nyilvánvalóan a függvény meghívása után lesz egy dekódolt sor a pufferben, mivel az átkerül a függvénynek FindClass, amely második paraméterként az osztály nevét veszi fel. Ezért a szám a puffer mérete vagy a vonal hossza. Próbáljuk megfejteni az osztály nevét, meg kell mondania, hogy jó irányba haladunk-e. Nézzük meg közelebbről, mi történik benne 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;
}

Funkció sub_7AF78 létrehoz egy tároló példányt a megadott méretű bájttömbök számára (ezekkel a tárolókkal nem foglalkozunk részletesen). Itt két ilyen konténer jön létre: az egyik tartalmazza a sort "DcO/lcK+h?m3c*q@" (könnyű kitalálni, hogy ez egy kulcs), a másik titkosított adatokat tartalmaz. Ezután mindkét objektumot egy bizonyos struktúrába helyezzük, amelyet átadunk a függvénynek sub_6115C. Jelöljünk meg ebben a struktúrában egy 3-as értékű mezőt is, lássuk, mi történik ezután ezzel a szerkezettel.

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 < 0x19 )
{
switch ( v4 )
{
case 0u:
v8 = sub_6419C(a1->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 kapcsolóparaméter egy szerkezetmező, amelyhez korábban 3 értéket rendeltek. Nézze meg a 3. esetet: a függvényhez sub_6364C paraméterek az előző függvényben hozzáadott struktúrából, azaz a kulcsból és a titkosított adatokból kerülnek átadásra. Ha alaposan megnézed sub_6364C, felismerheti benne az RC4 algoritmust.

Van egy algoritmusunk és egy kulcsunk. Próbáljuk megfejteni az osztály nevét. Íme, mi történt: com/taobao/wireless/security/adapter/JNICLibrary. Nagy! Jó úton haladunk.

Parancsfa

Most kihívást kell találnunk Regisztráció Natives, amely rámutat a függvényre doCommandNative. Nézzük meg a fromból meghívott függvényeket JNI_OnLoad, és megtaláljuk benne 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;
}

És valóban, itt van bejegyezve egy natív módszer a névvel doCommandNative. Most már tudjuk a címét. Lássuk, 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 név alapján kitalálható, hogy itt van az összes olyan funkció belépési pontja, amelyeket a fejlesztők úgy döntöttek, hogy áthelyezik a natív könyvtárba. Érdekel minket a 10601-es funkció.

A kódból látható, hogy a parancs száma három számot eredményez: parancs/10000, parancs % 10000 / 100 и parancs % 10, azaz esetünkben 1, 6 és 1. Ez a három szám, valamint egy mutató JNIEnv és a függvénynek átadott argumentumokat hozzáadjuk egy struktúrához és továbbadjuk. A kapott három szám (jelöljük N1, N2 és N3) felhasználásával egy parancsfát építünk fel.

Valami ilyesmi:

Sebezhetőségek keresése az UC Browserben

A fa dinamikusan töltődik be JNI_OnLoad.
Három szám kódolja az utat a fában. A fa minden levele tartalmazza a megfelelő függvény beszúrt címét. A kulcs a szülőcsomópontban van. Nem nehéz megtalálni a kódban azt a helyet, ahol a szükséges függvény hozzá van adva a fához, ha megérti az összes használt struktúrát (nem írjuk le őket, hogy ne duzzadjunk fel egy amúgy is meglehetősen nagy cikket).

Még több zavarás

Megkaptuk annak a függvénynek a címét, amelynek dekódolnia kell a forgalmat: 0x5F1AC. De még korai örülni: az UC Browser fejlesztői újabb meglepetéssel készültek számunkra.

Miután megkaptuk a paramétereket a Java kódban kialakított tömbből, megkapjuk
a 0x4D070 címen található függvényhez. És itt egy másik típusú kódzavarás vár ránk.

Két indexet teszünk R7-be és R4-be:

Sebezhetőségek keresése az UC Browserben

Az első indexet áthelyezzük R11-re:

Sebezhetőségek keresése az UC Browserben

Ha egy táblázatból szeretne címet kapni, használjon indexet:

Sebezhetőségek keresése az UC Browserben

Az első cím elérése után a második index kerül felhasználásra, amely az R4-ben található. A táblázatban 230 elem található.

Mit kell tenni ellene? Megmondhatja az IDA-nak, hogy ez egy kapcsoló: Szerkesztés -> Egyéb -> Kapcsoló idióma megadása.

Sebezhetőségek keresése az UC Browserben

A kapott kód szörnyű. Ám a dzsungelen áthaladva észrevehet egy olyan funkciót, amelyet már ismerünk sub_6115C:

Sebezhetőségek keresése az UC Browserben

Volt egy kapcsoló, amiben a 3. esetben RC4 algoritmussal történt visszafejtés. És ebben az esetben a függvénynek átadott struktúra a számára átadott paraméterekből töltődik ki doCommandNative. Emlékezzünk, mi volt ott magicInt 16-os értékkel. Megnézzük a megfelelő esetet - és több átmenet után megtaláljuk azt a kódot, amely alapján az algoritmus azonosítható.

Sebezhetőségek keresése az UC Browserben

Ez az AES!

Az algoritmus létezik, már csak a paramétereit kell megszerezni: mód, kulcs és esetleg az inicializálási vektor (jelenléte az AES algoritmus működési módjától függ). A velük lévő szerkezetet valahol a függvényhívás előtt kell kialakítani sub_6115C, de a kódnak ez a része különösen jól el van takarva, így felmerül az ötlet, hogy javítsuk a kódot úgy, hogy a visszafejtő funkció minden paramétere egy fájlba kerüljön.

Tapasz

Annak érdekében, hogy ne írjon ki minden javítási kódot manuálisan assembly nyelven, elindíthatja az Android Studiót, írhat oda egy függvényt, amely ugyanazokat a bemeneti paramétereket kapja, mint a visszafejtési funkciónk, és fájlba ír, majd másolja be a kódot, amelyet a fordító fogja generál.

Barátaink, az UC Browser csapatából is gondoskodtak a kód hozzáadásának kényelméről. Ne felejtsük el, hogy minden függvény elején van szemétkódunk, amely könnyen lecserélhető bármely másikra. Nagyon kényelmes 🙂 A célfüggvény elején azonban nincs elég hely az összes paramétert fájlba mentő kódnak. Részekre kellett osztanom, és a szomszédos funkciók szemetes blokkjait kellett használni. Összesen négy rész volt.

Az első rész:

Sebezhetőségek keresése az UC Browserben

Az ARM architektúrában az első négy funkcióparaméter az R0-R3 regisztereken, a többi, ha van, a veremen keresztül kerül átadásra. Az LR regiszter tartalmazza a visszaküldési címet. Mindezt el kell menteni, hogy a függvény működni tudjon, miután kiírattuk a paramétereit. Ezenkívül mentenünk kell az összes regisztert, amelyet a folyamat során használni fogunk, ezért a PUSH.W {R0-R10,LR} parancsot tesszük. Az R7-ben megkapjuk a veremen keresztül a függvénynek átadott paraméterlista címét.

A funkció használata nyisd ki nyissuk meg a fájlt /data/local/tmp/aes "ab" módban
mármint hozzáadásra. R0-ban a fájlnév címét töltjük be, R1-be - a módot jelző sor címét. És itt a szemétkód véget ér, így továbblépünk a következő függvényre. Annak érdekében, hogy továbbra is működjön, az elejére tesszük az átmenetet a függvény valódi kódjára, megkerülve a szemetet, és a szemét helyett a javítás folytatását adjuk hozzá.

Sebezhetőségek keresése az UC Browserben

Hívás nyisd ki.

A függvény első három paramétere aes van típusa int. Mivel a regisztereket az elején a verembe mentettük, egyszerűen átadhatjuk a függvényt fwrite címük a veremben.

Sebezhetőségek keresése az UC Browserben

Ezután három struktúránk van, amelyek tartalmazzák az adatméretet és a kulcs adataira mutató mutatót, az inicializálási vektort és a titkosított adatokat.

Sebezhetőségek keresése az UC Browserben

A végén zárja be a fájlt, állítsa vissza a regisztereket és vigye át a vezérlést a valódi funkcióra aes.

Összegyűjtünk egy javított könyvtárral rendelkező APK-t, aláírjuk, feltöltjük az eszközre/emulátorra, és elindítjuk. Látjuk, hogy a dumpunk készül, és rengeteg adatot írnak oda. A böngésző nem csak a forgalomhoz használ titkosítást, és minden titkosítás a kérdéses funkción megy keresztül. De valamiért a szükséges adatok nincsenek meg, és a szükséges kérés nem látszik a forgalomban. Annak érdekében, hogy ne várjuk meg, amíg az UC Browser megteszi a szükséges kérést, vegyük a szervertől korábban kapott titkosított választ, és javítsuk újra az alkalmazást: adjuk hozzá a visszafejtést a fő tevékenység onCreate-éhez.

    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

Összeszereljük, aláírjuk, telepítjük, elindítjuk. NullPointerException kivételt kapunk, mert a metódus nullát adott vissza.

A kód további elemzése során felfedeztek egy függvényt, amely érdekes sorokat fejt meg: „META-INF/” és „.RSA”. Úgy tűnik, hogy az alkalmazás ellenőrzi a tanúsítványát. Vagy akár kulcsokat generál belőle. Nem igazán akarok foglalkozni azzal, hogy mi történik a tanúsítvánnyal, ezért csak a megfelelő bizonyítványt csúsztatjuk rá. Foltozzuk a titkosított sort, hogy a „META-INF/” helyett „BLABLINF/” legyen, hozzunk létre egy ilyen nevű mappát az APK-ban, és adjuk hozzá a mókus böngésző tanúsítványát.

Összeszereljük, aláírjuk, telepítjük, elindítjuk. Bingó! Nálunk van a kulcs!

WithM

Kaptunk egy kulcsot és a kulccsal megegyező inicializálási vektort. Próbáljuk meg visszafejteni a szerver válaszát CBC módban.

Sebezhetőségek keresése az UC Browserben

Látjuk az archív URL-t, valami hasonlót az MD5-höz, „extract_unzipsize” és egy számot. Ellenőrizzük: az archívum MD5-je megegyezik, a kicsomagolt könyvtár mérete megegyezik. Megpróbáljuk javítani ezt a könyvtárat, és átadni a böngészőnek. Annak bizonyítására, hogy a javított könyvtárunk betöltődött, elindítunk egy szándékot SMS létrehozására a „PWNED!” szöveggel. Két választ fogunk cserélni a szerverről: puds.ucweb.com/upgrade/index.xhtml és letöltheti az archívumot. Az elsőben lecseréljük az MD5-öt (kicsomagolás után a méret nem változik), a másodikban az archívumot adjuk meg a javított könyvtárral.

A böngésző többször megpróbálja letölteni az archívumot, ami után hibát ad. Nyilván valamit
neki nem tetszik. Ennek a homályos formátumnak az elemzése során kiderült, hogy a szerver az archívum méretét is továbbítja:

Sebezhetőségek keresése az UC Browserben

LEB128 kódolású. A javítás után kicsit változott a könyvtárral ellátott archívum mérete, így a böngésző úgy ítélte meg, hogy az archívum ferdén lett letöltve, és többszöri próbálkozás után hibát dobott.

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

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

Következmények és fejlesztői reakció

Ugyanígy a hackerek az UC Browser nem biztonságos funkcióját használhatják rosszindulatú könyvtárak terjesztésére és futtatására. Ezek a könyvtárak a böngésző kontextusában működnek, így megkapják annak összes rendszerengedélyét. Ennek eredményeként lehetőség nyílik az adathalász ablakok megjelenítésére, valamint hozzáférés a narancssárga kínai mókus munkafájljaihoz, beleértve a bejelentkezési adatokat, jelszavakat és az adatbázisban tárolt cookie-kat.

Megkerestük az UC Browser fejlesztőit és tájékoztattuk őket a talált problémáról, próbáltunk rámutatni a sérülékenységre és annak veszélyére, de nem beszéltek velünk semmit. Eközben a böngésző továbbra is láthatóan fitogtatta veszélyes funkcióját. De miután felfedtük a sebezhetőség részleteit, többé nem lehetett figyelmen kívül hagyni, mint korábban. március 27-e volt
megjelent az UC Browser 12.10.9.1193 új verziója, amely HTTPS-en keresztül érte el a szervert: puds.ucweb.com/upgrade/index.xhtml.

Ezenkívül a „javítás” után és a cikk megírásáig a PDF böngészőben történő megnyitása egy hibaüzenetet eredményezett a következő szöveggel: „Hoppá, valami hiba történt!” A szerverhez nem a PDF megnyitásakor érkezett kérés, hanem a böngésző indításakor, ami arra utal, hogy a Google Play szabályait megsértve továbbra is le lehet tölteni a végrehajtható kódot.

Forrás: will.com

Hozzászólás