Traženje ranjivosti u UC pregledniku

Traženje ranjivosti u UC pregledniku

Uvod

Krajem ožujka mi prijavio, da su otkrili skrivenu mogućnost učitavanja i pokretanja neprovjerenog koda u UC pregledniku. Danas ćemo detaljno pogledati kako dolazi do ovog preuzimanja i kako ga hakeri mogu koristiti za svoje potrebe.

Prije nekog vremena UC Browser je reklamiran i distribuiran vrlo agresivno: instaliran je na uređaje korisnika pomoću zlonamjernog softvera, distribuiran s raznih stranica pod krinkom video datoteka (tj. korisnici su mislili da preuzimaju, na primjer, porno video, ali umjesto toga dobio APK s ovim preglednikom), koristio zastrašujuće bannere s porukama da je preglednik zastario, ranjiv i slične stvari. U službenoj grupi UC Browser na VK postoji tema, u kojem se korisnici mogu žaliti na nepošteno oglašavanje, ima mnogo primjera. U 2016. bilo je čak video oglašavanje na ruskom (da, reklama za preglednik za blokiranje oglasa).

U vrijeme pisanja, UC Browser ima više od 500 instalacija na Google Playu. Ovo je impresivno - samo Google Chrome ima više. Među recenzijama možete vidjeti dosta pritužbi na oglašavanje i preusmjeravanja na neke aplikacije na Google Playu. To je bio razlog našeg istraživanja: odlučili smo vidjeti radi li UC Browser nešto loše. I pokazalo se da ima!

U kodu aplikacije otkrivena je mogućnost preuzimanja i pokretanja izvršnog koda, što je u suprotnosti s pravilima objave prijava na Google Playu. Osim što UC Browser preuzima izvršni kod, on to čini na nesiguran način, što se može koristiti za izvođenje MitM napada. Da vidimo možemo li izvesti takav napad.

Sve dolje napisano relevantno je za verziju UC Browsera koja je bila dostupna na Google Playu u vrijeme istraživanja:

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

Vektor napada

U manifestu preglednika UC možete pronaći uslugu s imenom koje samo po sebi objašnjava com.uc.deployment.UpgradeDeployService.

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

Kada se ova usluga pokrene, preglednik postavlja POST zahtjev za puds.ucweb.com/upgrade/index.xhtml, što se može vidjeti u prometu neko vrijeme nakon starta. Kao odgovor, može dobiti naredbu za preuzimanje nekog ažuriranja ili novog modula. Tijekom analize poslužitelj nije davao takve naredbe, ali primijetili smo da kada pokušamo otvoriti PDF u pregledniku, on upućuje drugi zahtjev na gore navedenu adresu, nakon čega preuzima izvornu biblioteku. Za izvođenje napada odlučili smo upotrijebiti ovu značajku UC Browsera: mogućnost otvaranja PDF-a pomoću izvorne biblioteke koja se ne nalazi u APK-u i koju preuzima s interneta ako je potrebno. Vrijedno je napomenuti da se, teoretski, UC Browser može natjerati da preuzme nešto bez interakcije korisnika – ako date dobro oblikovan odgovor na zahtjev koji se izvršava nakon pokretanja preglednika. Ali da bismo to učinili, moramo detaljnije proučiti protokol interakcije s poslužiteljem, pa smo odlučili da bi bilo lakše urediti presretnuti odgovor i zamijeniti biblioteku za rad s PDF-om.

Dakle, kada korisnik želi otvoriti PDF izravno u pregledniku, u prometu se mogu vidjeti sljedeći zahtjevi:

Traženje ranjivosti u UC pregledniku

Prvo postoji POST zahtjev za puds.ucweb.com/upgrade/index.xhtml, onda
Preuzima se arhiva s bibliotekom za pregled PDF i uredskih formata. Logično je pretpostaviti da prvi zahtjev prenosi informacije o sustavu (barem o arhitekturi za pružanje tražene biblioteke), a kao odgovor na njega preglednik prima neke informacije o biblioteci koju treba preuzeti: adresu i, eventualno, , nešto drugo. Problem je što je ovaj zahtjev šifriran.

Fragment zahtjeva

Fragment odgovora

Traženje ranjivosti u UC pregledniku

Traženje ranjivosti u UC pregledniku

Sama biblioteka je pakirana u ZIP i nije šifrirana.

Traženje ranjivosti u UC pregledniku

Potražite kod za dešifriranje prometa

Pokušajmo dešifrirati odgovor poslužitelja. Pogledajmo šifru klase com.uc.deployment.UpgradeDeployService: iz metode onStartCommand ići com.uc.deployment.bx, a 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);
}

Ovdje vidimo formiranje POST zahtjeva. Obraćamo pozornost na kreiranje niza od 16 bajtova i njegovo punjenje: 0x5F, 0, 0x1F, -50 (=0xCE). Podudara se s onim što smo vidjeli u gornjem zahtjevu.

U istoj klasi možete vidjeti ugniježđenu klasu koja ima još jednu zanimljivu metodu:

        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 uzima niz bajtova kao ulaz i provjerava je li nulti bajt 0x60 ili treći bajt 0xD0, a drugi bajt 1, 11 ili 0x1F. Gledamo odgovor poslužitelja: nulti bajt je 0x60, drugi je 0x1F, treći je 0x60. Zvuči kao ono što nam treba. Sudeći prema redcima (na primjer, "up_decrypt"), ovdje bi se trebala pozvati metoda koja će dekriptirati odgovor poslužitelja.
Prijeđimo na metodu g j. Imajte na umu da je prvi argument bajt na pomaku 2 (tj. 0x1F u našem slučaju), a drugi je odgovor poslužitelja bez
prvih 16 bajtova.

     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čito, ovdje odabiremo algoritam za dešifriranje i isti bajt koji je u našem
slučaj jednak 0x1F, označava jednu od tri moguće opcije.

Nastavljamo analizirati kod. Nakon nekoliko skokova nalazimo se u metodi samorazumljivog naziva decryptBytesByKey.

Ovdje se još dva bajta odvajaju od našeg odgovora i iz njih se dobiva niz. Jasno je da se na taj način odabire ključ za dekriptiranje poruke.

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

Gledajući unaprijed, napominjemo da u ovoj fazi još ne dobivamo ključ, već samo njegov "identifikator". Dobijanje ključa je malo kompliciranije.

U sljedećoj metodi postojećim parametrima dodaju se još dva parametra, čime ih čini četiri: magični broj 16, identifikator ključa, šifrirani podatak i nerazumljivi string (u našem slučaju prazan).

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

Nakon niza prijelaza dolazimo do metode staticBinarySafeDecryptNoB64 sučelje com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Ne postoje klase u kodu glavne aplikacije koje implementiraju ovo sučelje. Postoji takva klasa u datoteci lib/armeabi-v7a/libsgmain.so, što zapravo nije .so, već .jar. Metoda koja nas zanima provodi se na sljedeći 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);
}
//...
}

Ovdje je naš popis parametara dopunjen s još dva cijela broja: 2 i 0. Sudeći po
sve, 2 znači dešifriranje, kao u metodi učinitiZavršno klasa sustava javax.crypto.Cipher. I sve se to prenosi na određeni Router s brojem 10601 - to je očito broj naredbe.

Nakon sljedećeg lanca prijelaza nalazimo klasu koja implementira sučelje IRouterComponent i 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);
}
}

I klasa JNIClibrary, u kojem je deklarirana izvorna metoda doCommandNative:

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

To znači da moramo pronaći metodu u izvornom kodu doCommandNative. I tu počinje zabava.

Zamagljivanje strojnog koda

U spisu libsgmain.so (što je zapravo .jar i u kojem smo gore pronašli implementaciju nekih sučelja povezanih s enkripcijom) postoji jedna izvorna biblioteka: libsgmainso-6.4.36.so. Otvorimo ga u IDA-i i dobijemo hrpu dijaloških okvira s pogreškama. Problem je u tome što je tablica zaglavlja odjeljka nevažeća. Ovo je učinjeno namjerno kako bi se zakomplicirala analiza.

Traženje ranjivosti u UC pregledniku

Ali nije potrebno: za ispravno učitavanje ELF datoteke i njezinu analizu dovoljna je tablica zaglavlja programa. Stoga jednostavno brišemo tablicu odjeljaka, uklanjajući odgovarajuća polja u zaglavlju na nulu.

Traženje ranjivosti u UC pregledniku

Ponovno otvorite datoteku u IDA-i.

Postoje dva načina da se Java virtualnom stroju kaže gdje se točno u izvornoj biblioteci nalazi implementacija metode deklarirane u Java kodu kao izvorna. Prvo je dati mu ime vrste Java_package_name_ClassName_MethodName.

Drugi je da ga registrirate prilikom učitavanja biblioteke (u funkciji JNI_OnLoad)
pomoću poziva funkcije Registriraj domorodce.

U našem slučaju, ako koristimo prvu metodu, naziv bi trebao biti ovakav: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Među izvezenim funkcijama ne postoji takva funkcija, što znači da morate tražiti poziv Registriraj domorodce.
Idemo na funkciju JNI_OnLoad i vidimo ovu sliku:

Traženje ranjivosti u UC pregledniku

Što se ovdje događa? Na prvi pogled, početak i kraj funkcije su tipični za ARM arhitekturu. Prva instrukcija na stogu pohranjuje sadržaj registara koje će funkcija koristiti u svom radu (u ovom slučaju R0, R1 i R2), kao i sadržaj LR registra koji sadrži povratnu adresu iz funkcije . Zadnja instrukcija vraća spremljene registre, a povratna adresa se odmah stavlja u registar računala - čime se vraća iz funkcije. Ali ako pažljivo pogledate, primijetit ćete da pretposljednja instrukcija mijenja povratnu adresu pohranjenu na stogu. Izračunajmo što će biti poslije
izvođenje koda. U R1 se učita određena adresa 0xB130, od nje se oduzme 5, zatim se prenese u R0 i doda joj se 0x10. Ispada 0xB13B. Dakle, IDA misli da je zadnja instrukcija normalan povrat funkcije, ali zapravo ide na izračunatu adresu 0xB13B.

Ovdje je vrijedno podsjetiti da ARM procesori imaju dva načina rada i dva skupa instrukcija: ARM i Thumb. Najmanje značajan bit adrese govori procesoru koji se skup instrukcija koristi. To jest, adresa je zapravo 0xB13A, a jedan u najmanje značajnom bitu označava način rada Thumb.

Sličan "adapter" dodan je na početak svake funkcije u ovoj biblioteci i
kod smeća. Nećemo se detaljnije zadržavati na njima - samo se sjećamo
da je pravi početak gotovo svih funkcija malo dalje.

Budući da kod ne skače eksplicitno na 0xB13A, sama IDA nije prepoznala da se kod nalazi na ovoj lokaciji. Iz istog razloga ne prepoznaje većinu koda u biblioteci kao kod, što donekle otežava analizu. Kažemo IDA-i da je ovo šifra i evo što se događa:

Traženje ranjivosti u UC pregledniku

Tablica jasno počinje od 0xB144. Što je u sub_494C?

Traženje ranjivosti u UC pregledniku

Pozivom ove funkcije u LR registru dobivamo adresu prethodno navedene tablice (0xB144). U R0 - indeks u ovoj tablici. Odnosno, vrijednost se uzima iz tablice, dodaje LR i rezultat je
adresu na koju treba otići. Pokušajmo to izračunati: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Idemo na primljenu adresu i vidimo doslovno nekoliko korisnih uputa i ponovno idemo na 0xB140:

Traženje ranjivosti u UC pregledniku

Sada će doći do prijelaza na offsetu s indeksom 0x20 iz tablice.

Sudeći po veličini tablice, u kodu će biti mnogo takvih prijelaza. Postavlja se pitanje može li se to nekako više automatski rješavati, bez ručnog izračunavanja adresa. A u pomoć nam dolaze skripte i mogućnost krpanja koda u IDA-i:

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"

Postavite kursor na liniju 0xB26A, pokrenite skriptu i pogledajte prijelaz na 0xB4B0:

Traženje ranjivosti u UC pregledniku

IDA ponovno nije prepoznala ovo područje kao šifru. Pomažemo joj i tamo vidimo još jedan dizajn:

Traženje ranjivosti u UC pregledniku

Upute nakon BLX-a kao da nemaju previše smisla, to je više kao nekakav pomak. Pogledajmo sub_4964:

Traženje ranjivosti u UC pregledniku

I doista, ovdje se uzima dword na adresi koja leži u LR, dodaje se ovoj adresi, nakon čega se uzima vrijednost na rezultirajućoj adresi i stavlja na stog. Također, 4 se dodaje LR tako da se nakon povratka iz funkcije taj isti pomak preskače. Nakon toga naredba POP {R1} uzima rezultirajuću vrijednost sa stoga. Ako pogledate što se nalazi na adresi 0xB4BA + 0xEA = 0xB5A4, vidjet ćete nešto slično adresnoj tablici:

Traženje ranjivosti u UC pregledniku

Da biste zakrpali ovaj dizajn, morat ćete dobiti dva parametra iz koda: pomak i broj registra u koji želite staviti rezultat. Za svaki mogući registar morat ćete unaprijed pripremiti dio koda.

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"

Postavljamo kursor na početak strukture koju želimo zamijeniti - 0xB4B2 - i pokrećemo skriptu:

Traženje ranjivosti u UC pregledniku

Uz već navedene strukture kodeks sadrži i sljedeće:

Traženje ranjivosti u UC pregledniku

Kao i u prethodnom slučaju, nakon instrukcije BLX postoji pomak:

Traženje ranjivosti u UC pregledniku

Uzimamo offset na adresu iz LR-a, dodajemo ga u LR i idemo tamo. 0x72044 + 0xC = 0x72050. Skripta za ovaj dizajn je vrlo jednostavna:

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 izvršenja skripte:

Traženje ranjivosti u UC pregledniku

Nakon što je sve zakrpano u funkciji, možete uputiti IDA-u na njezin pravi početak. Sastavit će sav funkcijski kod i može se dekompilirati pomoću HexRaysa.

Dekodiranje nizova

Naučili smo se nositi s maskiranjem strojnog koda u knjižnici libsgmainso-6.4.36.so iz UC preglednika i primili funkcijski kod 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;
}

Pogledajmo pobliže sljedeće retke:

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

U funkciji sub_73E24 ime klase se jasno dešifrira. Kao parametri ovoj funkciji prosljeđuju se pokazivač na podatke slične šifriranim podacima, određeni međuspremnik i broj. Očito, nakon poziva funkcije, u međuspremniku će biti dešifrirana linija, budući da se prosljeđuje funkciji FindClass, koji uzima naziv klase kao drugi parametar. Stoga je broj veličina međuspremnika ili duljina linije. Pokušajmo dešifrirati naziv klase, on bi nam trebao reći idemo li u dobrom smjeru. Pogledajmo pobliže što se događa u 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 stvara instancu spremnika za nizove bajtova navedene veličine (nećemo se detaljnije zadržavati na ovim spremnicima). Ovdje se stvaraju dva takva spremnika: jedan sadrži liniju "DcO/lcK+h?m3c*q@" (lako je pogoditi da je ovo ključ), drugi sadrži šifrirane podatke. Zatim se oba objekta smještaju u određenu strukturu koja se prosljeđuje funkciji sub_6115C. Označimo i polje s vrijednošću 3 u ovoj strukturi. Pogledajmo što će se sljedeće dogoditi s ovom strukturom.

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

Parametar prekidača je polje strukture kojem je prethodno dodijeljena vrijednost 3. Pogledajte slučaj 3: funkciji sub_6364C prosljeđuju se parametri iz strukture koji su tamo dodani u prethodnoj funkciji, tj. ključ i šifrirani podaci. Ako pažljivo pogledate sub_6364C, u njemu možete prepoznati RC4 algoritam.

Imamo algoritam i ključ. Pokušajmo dešifrirati naziv klase. Evo što se dogodilo: com/taobao/wireless/security/adapter/JNICLibrary. Sjajno! Na dobrom smo putu.

Stablo naredbi

Sada moramo pronaći izazov Registriraj domorodce, koji će nas uputiti na funkciju doCommandNative. Pogledajmo funkcije koje se pozivaju iz JNI_OnLoad, i nalazimo ga u pod_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;
}

I doista, ovdje je registrirana izvorna metoda s imenom doCommandNative. Sada znamo njegovu adresu. Da vidimo što će učiniti.

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 nazivu možete pogoditi da je ovdje ulazna točka svih funkcija koje su programeri odlučili prenijeti u matičnu knjižnicu. Zanima nas funkcija broj 10601.

Iz koda možete vidjeti da naredba broj proizvodi tri broja: naredba/10000, naredba % 10000 / 100 и naredba % 10, tj. u našem slučaju 1, 6 i 1. Ova tri broja, kao i pokazivač na JNIEnv a argumenti proslijeđeni funkciji dodaju se strukturi i prosljeđuju dalje. Pomoću tri dobivena broja (označimo ih N1, N2 i N3) gradi se stablo naredbi.

Nešto kao ovo:

Traženje ranjivosti u UC pregledniku

Stablo se popunjava dinamički JNI_OnLoad.
Tri broja kodiraju put u stablu. Svaki list stabla sadrži ubačenu adresu odgovarajuće funkcije. Ključ je u nadređenom čvoru. Pronalaženje mjesta u kodu gdje je funkcija koja nam je potrebna dodana u stablo nije teško ako razumijete sve korištene strukture (ne opisujemo ih kako ne bismo napuhali već prilično velik članak).

Više zamagljivanja

Dobili smo adresu funkcije koja bi trebala dekriptirati promet: 0x5F1AC. Ali prerano je za radost: programeri UC Browsera pripremili su nam još jedno iznenađenje.

Nakon primanja parametara iz niza koji je formiran u Java kodu, dobivamo
na funkciju na adresi 0x4D070. I ovdje nas čeka druga vrsta maskiranja koda.

Stavili smo dva indeksa u R7 i R4:

Traženje ranjivosti u UC pregledniku

Prebacujemo prvi indeks na R11:

Traženje ranjivosti u UC pregledniku

Da biste dobili adresu iz tablice, koristite indeks:

Traženje ranjivosti u UC pregledniku

Nakon odlaska na prvu adresu koristi se drugi indeks koji se nalazi u R4. U tablici se nalazi 230 elemenata.

Što učiniti u vezi s tim? IDA-i možete reći da je ovo prekidač: Uredi -> Ostalo -> Odredi idiom prekidača.

Traženje ranjivosti u UC pregledniku

Rezultirajući kod je zastrašujući. No, probijajući se kroz njegovu džunglu, možete primijetiti poziv funkcije koja nam je već poznata sub_6115C:

Traženje ranjivosti u UC pregledniku

Postojao je prekidač u kojem je u slučaju 3 došlo do dešifriranja pomoću RC4 algoritma. I u ovom slučaju, struktura proslijeđena funkciji popunjava se parametrima koji su proslijeđeni doCommandNative. Prisjetimo se što smo tamo imali magicInt s vrijednošću 16. Gledamo odgovarajući slučaj – i nakon nekoliko prijelaza nalazimo kod po kojem se algoritam može identificirati.

Traženje ranjivosti u UC pregledniku

Ovo je AES!

Algoritam postoji, preostaje samo nabaviti njegove parametre: mod, ključ i, eventualno, inicijalizacijski vektor (njegova prisutnost ovisi o načinu rada AES algoritma). Struktura s njima mora biti formirana negdje prije poziva funkcije sub_6115C, ali ovaj dio koda je posebno dobro zamagljen, pa se javlja ideja zakrpati kod tako da se svi parametri funkcije dešifriranja bace u datoteku.

Zakrpa

Kako ne biste ručno pisali sav kod zakrpe u asemblerskom jeziku, možete pokrenuti Android Studio, tamo napisati funkciju koja prima iste ulazne parametre kao naša funkcija za dešifriranje i piše u datoteku, a zatim kopirajte i zalijepite kod koji će kompajler generirati.

Za praktičnost dodavanja koda pobrinuli su se i naši prijatelji iz UC Browser tima. Podsjetimo se da na početku svake funkcije imamo otpadni kod koji se lako može zamijeniti bilo kojim drugim. Vrlo zgodno 🙂 Međutim, na početku ciljne funkcije nema dovoljno mjesta za kod koji sprema sve parametre u datoteku. Morao sam ga podijeliti na dijelove i koristiti blokove smeća iz susjednih funkcija. Bila su ukupno četiri dijela.

Prvi dio:

Traženje ranjivosti u UC pregledniku

U arhitekturi ARM, prva četiri parametra funkcije prolaze kroz registre R0-R3, ostali, ako ih ima, prolaze kroz stog. LR registar nosi povratnu adresu. Sve ovo treba spremiti kako bi funkcija mogla raditi nakon što izbacimo njezine parametre. Također moramo spremiti sve registre koje ćemo koristiti u procesu, pa radimo PUSH.W {R0-R10,LR}. U R7 dobivamo adresu popisa parametara proslijeđenih funkciji preko stoga.

Korištenje funkcije otvorena otvorimo datoteku /podaci/lokalni/tmp/aes u "ab" modu
tj. za dodavanje. U R0 učitavamo adresu naziva datoteke, u R1 - adresu retka koji označava način rada. I ovdje kod smeća završava, pa prelazimo na sljedeću funkciju. Kako bi nastavio raditi, na početak stavljamo prijelaz na pravi kod funkcije, zaobilazeći smeće, a umjesto smeća dodajemo nastavak zakrpe.

Traženje ranjivosti u UC pregledniku

zovem otvorena.

Prva tri parametra funkcije AES imati tip int. Budući da smo registre spremili na stog na početku, možemo jednostavno proslijediti funkciju fpisati njihove adrese na stogu.

Traženje ranjivosti u UC pregledniku

Zatim imamo tri strukture koje sadrže veličinu podataka i pokazivač na podatke za ključ, inicijalizacijski vektor i šifrirane podatke.

Traženje ranjivosti u UC pregledniku

Na kraju zatvorite datoteku, vratite registre i prenesite kontrolu na stvarnu funkciju AES.

Skupljamo APK sa zakrpanom bibliotekom, potpisujemo ga, učitavamo na uređaj/emulator i pokrećemo. Vidimo da se stvara naš dump i tu se upisuje mnogo podataka. Preglednik koristi enkripciju ne samo za promet, a sva enkripcija prolazi kroz dotičnu funkciju. Ali iz nekog razloga potrebni podaci nisu tamo, a traženi zahtjev nije vidljiv u prometu. Kako ne bismo čekali dok se UC Browser ne udostoji napraviti potreban zahtjev, uzmimo šifrirani odgovor s poslužitelja primljen ranije i ponovno zakrpimo aplikaciju: dodajte dešifriranje u onCreate glavne aktivnosti.

    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

Sastavljamo, potpisujemo, montiramo, puštamo u rad. Primamo NullPointerException jer je metoda vratila null.

Tijekom daljnje analize koda otkrivena je funkcija koja dešifrira zanimljive retke: “META-INF/” i “.RSA”. Čini se da aplikacija provjerava svoj certifikat. Ili čak generira ključeve iz njega. Zapravo se ne želim baviti time što se događa s certifikatom, pa ćemo mu samo ubaciti ispravan certifikat. Zakrpimo šifriranu liniju tako da umjesto "META-INF/" dobijemo "BLABLINF/", stvorimo mapu s tim nazivom u APK-u i tamo dodamo certifikat preglednika vjeverica.

Sastavljamo, potpisujemo, montiramo, puštamo u rad. Bingo! Imamo ključ!

MitM

Dobili smo ključ i inicijalizacijski vektor jednak ključu. Pokušajmo dešifrirati odgovor poslužitelja u CBC načinu rada.

Traženje ranjivosti u UC pregledniku

Vidimo URL arhive, nešto slično MD5, "extract_unzipsize" i broj. Provjeravamo: MD5 arhive je isti, veličina neraspakirane biblioteke je ista. Pokušavamo zakrpati ovu biblioteku i dati je pregledniku. Kako bismo pokazali da se naša zakrpana biblioteka učitala, pokrenut ćemo namjeru za stvaranje SMS-a s tekstom "PWNED!" Zamijenit ćemo dva odgovora s poslužitelja: puds.ucweb.com/upgrade/index.xhtml i za preuzimanje arhive. U prvom zamjenjujemo MD5 (veličina se ne mijenja nakon raspakiranja), u drugom dajemo arhivu sa zakrpanom bibliotekom.

Preglednik nekoliko puta pokušava preuzeti arhivu, nakon čega daje pogrešku. Očigledno nešto
on ne voli. Kao rezultat analize ovog mutnog formata, pokazalo se da poslužitelj također prenosi veličinu arhive:

Traženje ranjivosti u UC pregledniku

Kodiran je u LEB128. Nakon zakrpe, veličina arhive s bibliotekom se malo promijenila, pa je preglednik smatrao da je arhiva pogrešno preuzeta i nakon nekoliko pokušaja izbacio je pogrešku.

Prilagođavamo veličinu arhive... I – pobjeda! 🙂 Rezultat je u videu.

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

Posljedice i reakcija programera

Na isti način, hakeri bi mogli koristiti nesigurnu značajku UC preglednika za distribuciju i pokretanje zlonamjernih biblioteka. Ove će biblioteke raditi u kontekstu preglednika, tako da će dobiti sve njegove sistemske dozvole. Kao rezultat, mogućnost prikaza prozora za krađu identiteta, kao i pristup radnim datotekama narančaste kineske vjeverice, uključujući prijave, lozinke i kolačiće pohranjene u bazi podataka.

Kontaktirali smo programere UC Browsera i obavijestili ih o problemu koji smo pronašli, pokušali ukazati na ranjivost i njezinu opasnost, ali s nama nisu ništa razgovarali. U međuvremenu, preglednik je nastavio isticati svoju opasnu značajku naočigled. Ali nakon što smo otkrili detalje ranjivosti, više je nije bilo moguće ignorirati kao prije. 27. ožujka bio je
izašla je nova verzija UC Browsera 12.10.9.1193 koji je poslužitelju pristupao putem HTTPS-a: puds.ucweb.com/upgrade/index.xhtml.

Osim toga, nakon "popravka" i do trenutka pisanja ovog članka, pokušaj otvaranja PDF-a u pregledniku rezultirao je porukom o pogrešci s tekstom "Ups, nešto je pošlo po zlu!" Zahtjev poslužitelju nije upućen prilikom pokušaja otvaranja PDF-a, ali je zahtjev poslan prilikom pokretanja preglednika, što ukazuje na nastavak mogućnosti preuzimanja izvršnog koda u suprotnosti s pravilima Google Playa.

Izvor: www.habr.com

Dodajte komentar