Iščem ranljivosti v brskalniku UC

Iščem ranljivosti v brskalniku UC

Predstavitev

Konec marca smo poročali, da so odkrili skrito zmožnost nalaganja in izvajanja nepreverjene kode v brskalniku UC. Danes si bomo podrobno ogledali, kako pride do tega prenosa in kako ga lahko hekerji uporabijo za svoje namene.

Pred časom so UC Browser oglaševali in distribuirali zelo agresivno: na naprave uporabnikov so ga nameščali z zlonamerno programsko opremo, distribuirali z različnih spletnih mest pod krinko video datotek (tj. uporabniki so mislili, da prenašajo na primer pornografski video, a namesto tega prejel APK s tem brskalnikom), uporabljal strašljive pasice s sporočili, da je brskalnik zastarel, ranljiv in podobno. V uradni skupini UC Browser na VK obstaja tema, v katerem se uporabniki lahko pritožijo zaradi nepoštenega oglaševanja, je tam veliko primerov. Leta 2016 je bilo celo video oglaševanje v ruščini (da, oglaševanje za brskalnik za blokiranje oglasov).

V času pisanja tega članka ima UC Browser več kot 500 namestitev v Googlu Play. To je impresivno – samo Google Chrome ima več. Med pregledi lahko vidite precej pritožb glede oglaševanja in preusmeritev na nekatere aplikacije v Google Play. To je bil razlog za našo raziskavo: odločili smo se preveriti, ali UC Browser počne kaj slabega. In izkazalo se je, da ga!

V kodi aplikacije je bila odkrita možnost prenosa in zagona izvršljive kode, kar je v nasprotju s pravili objavljanja prijav v storitvi Google Play. Poleg prenosa izvršljive kode UC Browser to počne na nevaren način, kar se lahko uporabi za začetek napada MitM. Poglejmo, ali lahko izvedemo tak napad.

Vse spodaj napisano je relevantno za različico brskalnika UC, ki je bila v času študije na voljo v Google Play:

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

Vektor napada

V manifestu UC Browser lahko najdete storitev s samoumevnim imenom com.uc.deployment.UpgradeDeployService.

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

Ko se ta storitev zažene, brskalnik pošlje zahtevo POST puds.ucweb.com/upgrade/index.xhtml, ki se nekaj časa po startu vidi v prometu. V odgovor lahko prejme ukaz za prenos posodobitve ali novega modula. Med analizo strežnik ni izdal takšnih ukazov, vendar smo opazili, da ko poskušamo odpreti PDF v brskalniku, pošlje drugo zahtevo na zgoraj navedeni naslov, nato pa prenese izvorno knjižnico. Za izvedbo napada smo se odločili uporabiti to funkcijo brskalnika UC: možnost odpiranja PDF-ja z izvorno knjižnico, ki je ni v APK-ju in jo po potrebi prenese z interneta. Treba je omeniti, da je teoretično mogoče UC Browser prisiliti, da nekaj prenese brez interakcije uporabnika - če zagotovite dobro oblikovan odgovor na zahtevo, ki se izvede po zagonu brskalnika. Toda za to moramo podrobneje preučiti protokol interakcije s strežnikom, zato smo se odločili, da bo lažje urediti prestreženi odgovor in zamenjati knjižnico za delo s PDF.

Torej, ko uporabnik želi odpreti PDF neposredno v brskalniku, so v prometu vidne naslednje zahteve:

Iščem ranljivosti v brskalniku UC

Najprej obstaja zahteva POST za puds.ucweb.com/upgrade/index.xhtml, potem
Prenese se arhiv s knjižnico za ogled PDF in pisarniških formatov. Logično je domnevati, da prva zahteva posreduje informacije o sistemu (vsaj o arhitekturi, ki zagotavlja zahtevano knjižnico), v odgovor nanjo pa brskalnik prejme nekaj informacij o knjižnici, ki jo je treba prenesti: naslov in, morda, , nekaj drugega. Težava je v tem, da je ta zahteva šifrirana.

Delček zahteve

Fragment odgovora

Iščem ranljivosti v brskalniku UC

Iščem ranljivosti v brskalniku UC

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

Iščem ranljivosti v brskalniku UC

Poiščite kodo za dešifriranje prometa

Poskusimo dešifrirati odgovor strežnika. Poglejmo kodo razreda com.uc.deployment.UpgradeDeployService: iz metode onStartCommand Pojdi do com.uc.deployment.bx, in od njega do 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);
}

Tukaj vidimo oblikovanje zahteve POST. Pozorni smo na ustvarjanje matrike 16 bajtov in njeno polnjenje: 0x5F, 0, 0x1F, -50 (=0xCE). Sovpada s tem, kar smo videli v zgornji zahtevi.

V istem razredu lahko vidite ugnezdeni razred, ki ima še eno zanimivo metodo:

        public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
}

Metoda vzame niz bajtov kot vhod in preveri, ali je ničelni bajt 0x60 ali tretji bajt 0xD0 in drugi bajt 1, 11 ali 0x1F. Pogledamo odgovor strežnika: ničelni bajt je 0x60, drugi je 0x1F, tretji je 0x60. Sliši se kot tisto, kar potrebujemo. Sodeč po vrsticah (na primer "up_decrypt"), je treba tukaj poklicati metodo, ki bo dešifrirala odgovor strežnika.
Pojdimo k metodi gj. Upoštevajte, da je prvi argument bajt pri odmiku 2 (tj. 0x1F v našem primeru), drugi pa je odgovor strežnika brez
prvih 16 bajtov.

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

Očitno tukaj izberemo algoritem za dešifriranje in isti bajt, ki je v našem
case enako 0x1F, označuje eno od treh možnih možnosti.

Nadaljujemo z analizo kode. Po nekaj skokih se znajdemo v metodi s samoumevnim imenom decryptBytesByKey.

Tu sta od našega odgovora ločena še dva bajta in iz njih dobimo niz. Jasno je, da je na ta način izbran ključ za dešifriranje sporočila.

    private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
if(bytes != null) {
try {
if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
return v0;
}
else {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE];  // 2 байта
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;
}

Če pogledamo naprej, ugotavljamo, da na tej stopnji še ne dobimo ključa, temveč le njegov "identifikator". Pridobivanje ključa je nekoliko bolj zapleteno.

Pri naslednji metodi obstoječim parametrom dodamo še dva parametra, s čimer nastanejo štirje: magično število 16, identifikator ključa, šifrirani podatek in nerazumljiv niz (v našem primeru prazen).

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

Po nizu prehodov pridemo do metode staticBinarySafeDecryptNoB64 vmesnik com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V kodi glavne aplikacije ni razredov, ki izvajajo ta vmesnik. V datoteki je tak razred lib/armeabi-v7a/libsgmain.so, ki pravzaprav ni .so, ampak .jar. Metoda, ki nas zanima, se izvaja na naslednji način:

package com.alibaba.wireless.security.a.i;
// ...
public class a implements IStaticDataEncryptComponent {
private ISecurityGuardPlugin a;
// ...
private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
}
// ...
private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException {
if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}

Tu je naš seznam parametrov dopolnjen še z dvema celima številoma: 2 in 0. Sodeč po
vse, 2 pomeni dešifriranje, kot v metodi doFinal sistemski razred javax.crypto.Cipher. In vse to se prenese na določen usmerjevalnik s številko 10601 - to je očitno številka ukaza.

Po naslednji verigi prehodov najdemo razred, ki implementira vmesnik IRouterComponent in metoda 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);
}
}

In tudi razred JNIClibrary, v katerem je deklarirana izvorna metoda doCommandNative:

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

To pomeni, da moramo najti metodo v izvorni kodi doCommandNative. In tu se začne zabava.

Zakrivanje strojne kode

V datoteki libsgmain.so (ki je pravzaprav .jar in v katerem smo zgoraj našli implementacijo nekaterih vmesnikov, povezanih s šifriranjem) obstaja ena izvorna knjižnica: libsgmainso-6.4.36.so. Odpremo ga v IDA in dobimo kup pogovornih oken z napakami. Težava je v tem, da je tabela glav razdelka neveljavna. To se naredi namenoma, da se analiza zaplete.

Iščem ranljivosti v brskalniku UC

Vendar ni potreben: za pravilno nalaganje datoteke ELF in njeno analizo zadostuje tabela z glavami programa. Zato preprosto izbrišemo tabelo odsekov in izničimo ustrezna polja v glavi.

Iščem ranljivosti v brskalniku UC

Ponovno odprite datoteko v IDA.

Obstajata dva načina, da navideznemu stroju Java poveste, kje točno v izvorni knjižnici se nahaja implementacija metode, ki je v kodi Java deklarirana kot izvorna. Prvi je, da mu damo ime vrste Java_package_name_ClassName_MethodName.

Drugi je, da ga registrirate ob nalaganju knjižnice (v funkciji JNI_OnLoad)
z uporabo funkcijskega klica RegisterNatives.

V našem primeru, če uporabimo prvo metodo, mora biti ime takšno: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Med izvoženimi funkcijami te funkcije ni, kar pomeni, da morate iskati klic RegisterNatives.
Pojdimo na funkcijo JNI_OnLoad in vidimo to sliko:

Iščem ranljivosti v brskalniku UC

Kaj se tukaj dogaja? Na prvi pogled sta začetek in konec funkcije tipična za arhitekturo ARM. Prvi ukaz na skladu shrani vsebino registrov, ki jih bo funkcija uporabljala pri svojem delovanju (v tem primeru R0, R1 in R2), ter vsebino registra LR, ki vsebuje povratni naslov funkcije . Zadnji ukaz obnovi shranjene registre, povratni naslov pa se takoj vnese v register osebnega računalnika - s čimer se vrne iz funkcije. Toda če pogledate natančno, boste opazili, da predzadnje navodilo spremeni povratni naslov, shranjen v skladu. Izračunajmo, kako bo potem
izvajanje kode. V R1 se naloži določen naslov 0xB130, od njega se odšteje 5, nato se prenese v R0 in se mu doda 0x10. Izkazalo se je 0xB13B. Tako IDA misli, da je zadnje navodilo običajna vrnitev funkcije, v resnici pa gre na izračunani naslov 0xB13B.

Tukaj je vredno spomniti, da imajo procesorji ARM dva načina in dva niza navodil: ARM in Thumb. Najmanj pomemben del naslova pove procesorju, kateri niz ukazov se uporablja. To pomeni, da je naslov dejansko 0xB13A in ena v najmanj pomembnem bitu označuje način Thumb.

Podoben »adapter« je bil dodan na začetek vsake funkcije v tej knjižnici in
smeti koda. Ne bomo se podrobneje ukvarjali z njimi - samo spomnimo se
da je pravi začetek skoraj vseh funkcij malo dlje.

Ker koda ne skoči izrecno na 0xB13A, IDA sama ni prepoznala, da se koda nahaja na tej lokaciji. Iz istega razloga večine kode v knjižnici ne prepozna kot kodo, kar nekoliko oteži analizo. IDA povemo, da je to koda, in to se zgodi:

Iščem ranljivosti v brskalniku UC

Tabela se jasno začne pri 0xB144. Kaj je v sub_494C?

Iščem ranljivosti v brskalniku UC

Pri klicu te funkcije v registru LR dobimo naslov prej omenjene tabele (0xB144). V R0 - indeks v tej tabeli. To pomeni, da se vrednost vzame iz tabele, doda LR in rezultat je
naslov, kamor greste. Poskusimo ga izračunati: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Gremo na prejeti naslov in vidimo dobesedno nekaj uporabnih navodil in spet pojdimo na 0xB140:

Iščem ranljivosti v brskalniku UC

Zdaj bo prišlo do prehoda pri odmiku z indeksom 0x20 iz tabele.

Sodeč po velikosti tabele bo takih prehodov v kodi veliko. Postavlja se vprašanje, ali se je mogoče s tem nekako ukvarjati bolj samodejno, brez ročnega računanja naslovov. In na pomoč nam priskočijo skripte in možnost popravka kode v IDA:

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"

Kazalec postavite na vrstico 0xB26A, zaženite skript in si oglejte prehod na 0xB4B0:

Iščem ranljivosti v brskalniku UC

IDA spet ni prepoznala tega področja kot kode. Pomagamo ji in tam vidimo drug dizajn:

Iščem ranljivosti v brskalniku UC

Navodila po BLX se mi ne zdijo preveč smiselna, gre bolj za nekakšen premik. Poglejmo sub_4964:

Iščem ranljivosti v brskalniku UC

In res, tukaj je beseda dword vzeta na naslovu, ki leži v LR, dodana temu naslovu, nakar se vrednost na dobljenem naslovu vzame in postavi na sklad. Poleg tega se LR doda 4, tako da se po vrnitvi iz funkcije ta isti odmik preskoči. Po tem ukaz POP {R1} vzame nastalo vrednost iz sklada. Če pogledate, kaj se nahaja na naslovu 0xB4BA + 0xEA = 0xB5A4, boste videli nekaj podobnega naslovni tabeli:

Iščem ranljivosti v brskalniku UC

Če želite popraviti to zasnovo, boste morali pridobiti dva parametra iz kode: odmik in številko registra, v katerega želite dati rezultat. Za vsak morebitni register boste morali predhodno pripraviti kodo.

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"

Kazalec postavimo na začetek strukture, ki jo želimo zamenjati - 0xB4B2 - in zaženemo skript:

Iščem ranljivosti v brskalniku UC

Poleg že omenjenih struktur vsebuje koda še naslednje:

Iščem ranljivosti v brskalniku UC

Kot v prejšnjem primeru je za navodilom BLX odmik:

Iščem ranljivosti v brskalniku UC

Iz LR vzamemo odmik na naslov, dodamo v LR in gremo tja. 0x72044 + 0xC = 0x72050. Skript za to zasnovo je precej preprost:

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"

Rezultat izvajanja skripta:

Iščem ranljivosti v brskalniku UC

Ko je vse popravljeno v funkciji, lahko IDA usmerite na njen pravi začetek. Sestavil bo vso funkcijsko kodo in jo je mogoče dekompilirati s pomočjo HexRays.

Dekodiranje nizov

Naučili smo se ravnati z zakrivanjem strojne kode v knjižnici libsgmainso-6.4.36.so iz UC Browserja in prejel funkcijsko kodo 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;
}

Oglejmo si podrobneje naslednje vrstice:

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

V funkciji sub_73E24 ime razreda se očitno dešifrira. Kot parametri tej funkciji so posredovani kazalec na podatke, podobne šifriranim podatkom, določen medpomnilnik in številka. Očitno je, da bo po klicu funkcije v medpomnilniku dešifrirana vrstica, saj je posredovana funkciji FindClass, ki vzame ime razreda kot drugi parameter. Zato je število velikost medpomnilnika ali dolžina vrstice. Poskusimo dešifrirati ime razreda, pove nam, ali gremo v pravo smer. Oglejmo si pobližje, kaj se dogaja v sub_73E24.

int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4; // r6
int v7; // r11
int v8; // r9
int v9; // r4
size_t v10; // r5
int v11; // r0
struc_1 v13; // [sp+0h] [bp-30h]
int v14; // [sp+1Ch] [bp-14h]
int v15; // [sp+20h] [bp-10h]
v4 = 0;
v15 = *(_DWORD *)off_8AC00;
v14 = 0;
v7 = sub_7AF78(17);
v8 = sub_7AF78(size);
if ( !v7 )
{
v9 = 0;
goto LABEL_12;
}
(*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16);
if ( !v8 )
{
LABEL_9:
v4 = 0;
goto LABEL_10;
}
v4 = 0;
if ( !in )
{
LABEL_10:
v9 = 0;
goto LABEL_11;
}
v9 = 0;
if ( out )
{
memset(out, 0, size);
v10 = size - 1;
(*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10);
memset(&v13, 0, 0x14u);
v13.field_4 = 3;
v13.field_10 = v7;
v13.field_14 = v8;
v11 = sub_6115C(&v13, &v14);
v9 = v11;
if ( v11 )
{
if ( *(_DWORD *)(v11 + 4) == v10 )
{
qmemcpy(out, *(const void **)v11, v10);
v4 = *(_DWORD *)(v9 + 4);
}
else
{
v4 = 0;
}
goto LABEL_11;
}
goto LABEL_9;
}
LABEL_11:
sub_7B148(v7);
LABEL_12:
if ( v8 )
sub_7B148(v8);
if ( v9 )
sub_7B148(v9);
return v4;
}

Funkcija sub_7AF78 ustvari primerek vsebnika za bajtne nize določene velikosti (ne bomo se podrobneje ukvarjali s temi vsebniki). Tukaj sta ustvarjena dva taka vsebnika: eden vsebuje vrstico "DcO/lcK+h?m3c*q@" (lahko je uganiti, da je to ključ), drugi pa vsebuje šifrirane podatke. Nato sta oba predmeta postavljena v določeno strukturo, ki se posreduje funkciji sub_6115C. V tej strukturi označimo tudi polje z vrednostjo 3. Poglejmo, kaj se s to strukturo zgodi naprej.

int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3; // lr
unsigned int v4; // r1
int v5; // r0
int v6; // r1
int result; // r0
int v8; // r0
*a2 = 820000;
if ( a1 )
{
v3 = a1->field_14;
if ( v3 )
{
v4 = a1->field_4;
if ( v4 < 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;
}

Parameter stikala je strukturno polje, ki mu je bila predhodno dodeljena vrednost 3. Poglejte primer 3: funkciji sub_6364C parametri se posredujejo iz strukture, ki je bila tam dodana v prejšnji funkciji, tj. ključ in šifrirani podatki. Če natančno pogledate sub_6364C, lahko v njem prepoznate algoritem RC4.

Imamo algoritem in ključ. Poskusimo dešifrirati ime razreda. Evo, kaj se je zgodilo: com/taobao/wireless/security/adapter/JNICLibrary. Super! Smo na pravi poti.

Drevo ukazov

Zdaj moramo najti izziv RegisterNatives, ki nas bo usmeril na funkcijo doCommandNative. Oglejmo si klicane funkcije iz JNI_OnLoad, in ga najdemo v sub_B7B0:

int __fastcall sub_B7F6(JNIEnv *env, jclass clazz)
{
char signature[41]; // [sp+7h] [bp-55h]
char name[16]; // [sp+30h] [bp-2Ch]
JNINativeMethod method; // [sp+40h] [bp-1Ch]
int v8; // [sp+4Ch] [bp-10h]
v8 = *(_DWORD *)off_8AC00;
decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u);// doCommandNative
decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u);// (I[Ljava/lang/Object;)Ljava/lang/Object;
method.name = name;
method.signature = signature;
method.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31;
}

In res je tukaj registrirana izvorna metoda z imenom doCommandNative. Zdaj vemo njegov naslov. Poglejmo, kaj počne.

int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args)
{
int v5; // r5
struc_2 *a5; // r6
int v9; // r1
int v11; // [sp+Ch] [bp-14h]
int v12; // [sp+10h] [bp-10h]
v5 = 0;
v12 = *(_DWORD *)off_8AC00;
v11 = 0;
a5 = (struc_2 *)malloc(0x14u);
if ( a5 )
{
a5->field_0 = 0;
a5->field_4 = 0;
a5->field_8 = 0;
a5->field_C = 0;
v9 = command % 10000 / 100;
a5->field_0 = command / 10000;
a5->field_4 = v9;
a5->field_8 = command % 100;
a5->field_C = env;
a5->field_10 = args;
v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11);
}
free(a5);
if ( !v5 && v11 )
sub_7CF34(env, v11, &byte_83ED7);
return v5;
}

Po imenu lahko uganete, da je tukaj vstopna točka vseh funkcij, ki so se jih razvijalci odločili prenesti v domačo knjižnico. Zanima nas številka funkcije 10601.

Iz kode lahko vidite, da številka ukaza ustvari tri številke: ukaz/10000, ukaz % 10000 / 100 и ukaz % 10, tj. v našem primeru 1, 6 in 1. Te tri številke in kazalec na JNIEnv in argumenti, posredovani funkciji, so dodani strukturi in posredovani naprej. Z dobljenimi tremi številkami (označimo jih z N1, N2 in N3) se zgradi ukazno drevo.

Nekaj ​​podobnega:

Iščem ranljivosti v brskalniku UC

Drevo se zapolni dinamično JNI_OnLoad.
Tri številke kodirajo pot v drevesu. Vsak list drevesa vsebuje narezan naslov ustrezne funkcije. Ključ je v nadrejenem vozlišču. Iskanje mesta v kodi, kjer je v drevo dodana funkcija, ki jo potrebujemo, ni težko, če razumete vse uporabljene strukture (ne opisujemo jih, da ne bi napihnili že tako obsežnega članka).

Več zamegljevanja

Prejeli smo naslov funkcije, ki naj dešifrira promet: 0x5F1AC. Vendar je še prezgodaj za veselje: razvijalci brskalnika UC so za nas pripravili še eno presenečenje.

Po prejemu parametrov iz matrike, ki je bila oblikovana v kodi Java, dobimo
na funkcijo na naslovu 0x4D070. In tukaj nas čaka druga vrsta zamegljenosti kode.

V R7 in R4 vstavimo dva indeksa:

Iščem ranljivosti v brskalniku UC

Prvi indeks premaknemo na R11:

Iščem ranljivosti v brskalniku UC

Če želite dobiti naslov iz tabele, uporabite indeks:

Iščem ranljivosti v brskalniku UC

Po prehodu na prvi naslov se uporabi drugi indeks, ki je v R4. V tabeli je 230 elementov.

Kaj storiti glede tega? IDA lahko poveste, da je to stikalo: Uredi -> Drugo -> Določi idiom stikala.

Iščem ranljivosti v brskalniku UC

Nastala koda je strašljiva. Toda, ko se prebijate skozi njegovo džunglo, lahko opazite klic funkcije, ki nam je že poznana sub_6115C:

Iščem ranljivosti v brskalniku UC

Bilo je stikalo, v katerem je v primeru 3 prišlo do dešifriranja z uporabo algoritma RC4. In v tem primeru je struktura, posredovana funkciji, napolnjena s parametri, ki so bili posredovani doCommandNative. Spomnimo se, kaj smo tam imeli magicInt z vrednostjo 16. Ogledamo si ustrezen primer - in po več prehodih najdemo kodo, po kateri lahko prepoznamo algoritem.

Iščem ranljivosti v brskalniku UC

To je AES!

Algoritem obstaja, vse kar ostane je, da pridobimo njegove parametre: način, ključ in po možnosti inicializacijski vektor (njegova prisotnost je odvisna od načina delovanja algoritma AES). Struktura z njimi mora biti oblikovana nekje pred klicem funkcije sub_6115C, vendar je ta del kode še posebej dobro zakrit, zato se pojavi ideja, da bi kodo popravili tako, da bi bili vsi parametri funkcije dešifriranja odloženi v datoteko.

Obliž

Da ne boste ročno zapisali vse kode popravkov v zbirnem jeziku, lahko zaženete Android Studio, tam napišete funkcijo, ki prejme enake vhodne parametre kot naša funkcija za dešifriranje in zapiše v datoteko, nato pa kopirajte in prilepite kodo, ki jo bo prevajalnik ustvariti.

Za udobje dodajanja kode so poskrbeli tudi naši prijatelji iz ekipe UC Browser. Spomnimo se, da imamo na začetku vsake funkcije odpadno kodo, ki jo lahko enostavno zamenjamo s katero koli drugo. Zelo priročno 🙂 Vendar na začetku ciljne funkcije ni dovolj prostora za kodo, ki shrani vse parametre v datoteko. Moral sem ga razdeliti na dele in uporabiti bloke smeti iz sosednjih funkcij. Skupaj so bili štirje deli.

Prvi del:

Iščem ranljivosti v brskalniku UC

V arhitekturi ARM se prvi štirje funkcijski parametri posredujejo skozi registre R0-R3, ostali, če sploh, pa skozi sklad. Register LR nosi povratni naslov. Vse to je treba shraniti, da lahko funkcija deluje, potem ko izpišemo njene parametre. Prav tako moramo shraniti vse registre, ki jih bomo uporabili v procesu, zato naredimo PUSH.W {R0-R10,LR}. V R7 dobimo naslov seznama parametrov, posredovanih funkciji prek sklada.

Uporaba funkcije odprto odprimo datoteko /data/local/tmp/aes v načinu "ab".
torej za dodatek. V R0 naložimo naslov imena datoteke, v R1 - naslov vrstice, ki označuje način. In tukaj se koda smeti konča, zato preidemo na naslednjo funkcijo. Da bi še naprej delovala, smo na začetku postavili prehod na pravo kodo funkcije, mimo smeti, namesto smeti pa dodamo nadaljevanje popravka.

Iščem ranljivosti v brskalniku UC

Klicanje odprto.

Prvi trije parametri funkcije aES imeti vrsto int. Ker smo registre na začetku shranili v sklad, lahko funkcijo preprosto posredujemo fwrite njihove naslove na skladu.

Iščem ranljivosti v brskalniku UC

Nato imamo tri strukture, ki vsebujejo velikost podatkov in kazalec na podatke za ključ, inicializacijski vektor in šifrirane podatke.

Iščem ranljivosti v brskalniku UC

Na koncu zaprite datoteko, obnovite registre in prenesite nadzor na pravo funkcijo aES.

Zberemo APK s popravljeno knjižnico, ga podpišemo, naložimo v napravo/emulator in zaženemo. Vidimo, da nastaja naš dump in tam se piše veliko podatkov. Brskalnik uporablja šifriranje ne samo za promet, ampak vse šifriranje gre skozi zadevno funkcijo. Toda iz nekega razloga potrebnih podatkov ni in zahtevana zahteva ni vidna v prometu. Da ne bi čakali, da se UC Browser spodobi, da izvede potrebno zahtevo, vzemimo šifriran odgovor prej prejetega strežnika in znova popravimo aplikacijo: dodajte dešifriranje v onCreate glavne dejavnosti.

    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

Montiramo, podpišemo, namestimo, lansiramo. Prejmemo izjemo NullPointerException, ker je metoda vrnila nič.

Pri nadaljnji analizi kode je bila odkrita funkcija, ki dešifrira zanimive vrstice: “META-INF/” in “.RSA”. Videti je, da aplikacija preverja svoje potrdilo. Ali celo ustvari ključe iz njega. Pravzaprav se ne želim ukvarjati s tem, kaj se dogaja s potrdilom, zato mu bomo samo vstavili pravilno potrdilo. Popravimo šifrirano vrstico, tako da namesto »META-INF/« dobimo »BLABLINF/«, ustvarimo mapo s tem imenom v APK-ju in vanjo dodamo potrdilo brskalnika veverica.

Montiramo, podpišemo, namestimo, lansiramo. Bingo! Imamo ključ!

MitM

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

Iščem ranljivosti v brskalniku UC

Vidimo URL arhiva, nekaj podobnega MD5, »extract_unzipsize« in številko. Preverimo: MD5 arhiva je enak, velikost nepakirane knjižnice je enaka. Poskušamo popraviti to knjižnico in jo dati brskalniku. Da pokažemo, da se je naša popravljena knjižnica naložila, bomo zagnali namen ustvariti SMS z besedilom "PWNED!" Zamenjali bomo dva odgovora s strežnika: puds.ucweb.com/upgrade/index.xhtml in za prenos arhiva. V prvem zamenjamo MD5 (velikost se po razpakiranju ne spremeni), v drugem damo arhiv s popravljeno knjižnico.

Brskalnik večkrat poskuša prenesti arhiv, nato pa izda napako. Očitno nekaj
ne mara. Kot rezultat analize tega mračnega formata se je izkazalo, da strežnik prenaša tudi velikost arhiva:

Iščem ranljivosti v brskalniku UC

Kodiran je v LEB128. Po popravku se je velikost arhiva s knjižnico nekoliko spremenila, zato je brskalnik menil, da je bil arhiv prenesen narobe, in po več poskusih je vrgel napako.

Prilagodimo velikost arhiva... In – zmaga! 🙂 Rezultat je v videu.

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

Posledice in reakcija razvijalca

Na enak način bi lahko hekerji uporabili nevarno funkcijo brskalnika UC za distribucijo in zagon zlonamernih knjižnic. Te knjižnice bodo delovale v kontekstu brskalnika, tako da bodo prejele vsa njegova sistemska dovoljenja. Kot rezultat, možnost prikaza oken za lažno predstavljanje, pa tudi dostop do delovnih datotek oranžne kitajske veverice, vključno s prijavami, gesli in piškotki, shranjenimi v bazi podatkov.

Stopili smo v stik z razvijalci UC Browserja in jih obvestili o težavi, ki smo jo našli, poskušali opozoriti na ranljivost in njeno nevarnost, vendar se z nami niso o ničemer pogovarjali. Medtem je brskalnik svojo nevarno funkcijo še naprej razkazoval na očeh. Ko pa smo razkrili podrobnosti ranljivosti, je ni bilo več mogoče ignorirati kot prej. 27. marec je bil
izdana je nova različica brskalnika UC Browser 12.10.9.1193, ki je do strežnika dostopal preko HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Poleg tega je po »popravku« in do časa pisanja tega članka poskus odpiranja PDF-ja v brskalniku povzročil sporočilo o napaki z besedilom »Ojoj, nekaj je šlo narobe!« Zahteva strežniku ni bila podana ob poskusu odpiranja PDF-ja, vendar je bila zahteva poslana ob zagonu brskalnika, kar namiguje na nadaljnjo možnost prenosa izvedljive kode v nasprotju s pravili Google Play.

Vir: www.habr.com

Dodaj komentar