Traženje ranjivosti u UC Browseru

Traženje ranjivosti u UC Browseru

Uvod

Krajem marta mi prijavljen, da su otkrili skrivenu mogućnost učitavanja i pokretanja neprovjerenog koda u UC Browseru. Danas ćemo detaljno pogledati kako dolazi do ovog preuzimanja i kako ga hakeri mogu koristiti u svoje svrhe.

Prije nekog vremena, UC Browser se reklamirao i distribuirao vrlo agresivno: instaliran je na uređaje korisnika pomoću zlonamjernog softvera, distribuiran sa raznih stranica pod maskom video datoteka (tj. korisnici su mislili da preuzimaju, na primjer, porno video, ali umjesto toga dobio APK sa ovim pretraživačem), koristio je zastrašujuće banere s porukama da je preglednik zastario, ranjiv i slično. U zvaničnoj 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, oglašavanje pretraživača za blokiranje oglasa).

U vrijeme pisanja ovog teksta, UC Browser ima preko 500 instalacija na Google Play-u. 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 Play-u. Ovo je bio razlog našeg istraživanja: odlučili smo da vidimo da li UC Browser radi nešto loše. I ispostavilo se da ima!

U kodu aplikacije otkrivena je mogućnost preuzimanja i pokretanja izvršnog koda, što je suprotno pravilima za objavljivanje prijava na Google Play. Osim preuzimanja izvršnog koda, UC Browser to radi na nesiguran način, koji se može koristiti za pokretanje MitM napada. Da vidimo da li možemo izvesti takav napad.

Sve napisano ispod je relevantno za verziju UC Browser-a koja je bila dostupna na Google Play-u u vrijeme istraživanja:

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

Vektor napada

U manifestu UC Browser-a možete pronaći uslugu sa 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, pretraživač postavlja POST zahtjev za puds.ucweb.com/upgrade/index.xhtml, što se može vidjeti u saobraćaju neko vrijeme nakon starta. Kao odgovor, može dobiti komandu za preuzimanje nekog ažuriranja ili novog modula. Tokom analize, server nije dao takve komande, ali smo primijetili da kada pokušamo otvoriti PDF u pretraživaču, on upućuje drugi zahtjev na gore navedenu adresu, nakon čega preuzima izvornu biblioteku. Kako bismo izvršili napad, odlučili smo koristiti ovu značajku UC Browser-a: mogućnost otvaranja PDF-a koristeći izvornu biblioteku, koja nije u APK-u i koju po potrebi preuzima s interneta. Vrijedi napomenuti da, teoretski, UC Browser može biti prisiljen da preuzme nešto bez interakcije korisnika - ako pružite dobro formiran odgovor na zahtjev koji se izvršava nakon pokretanja pretraživača. Ali da bismo to učinili, moramo detaljnije proučiti protokol interakcije sa serverom, pa smo odlučili da će biti lakše urediti presretnuti odgovor i zamijeniti biblioteku za rad s PDF-om.

Dakle, kada korisnik želi da otvori PDF direktno u pretraživaču, u prometu se mogu vidjeti sljedeći zahtjevi:

Traženje ranjivosti u UC Browseru

Prvo postoji POST zahtjev za puds.ucweb.com/upgrade/index.xhtml, onda
Preuzima se arhiva sa bibliotekom za pregled PDF i office formata. Logično je pretpostaviti da se prvim zahtjevom prenose informacije o sistemu (barem arhitektura koja obezbjeđuje potrebnu biblioteku), a kao odgovor na njega pretraživač 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 Browseru

Traženje ranjivosti u UC Browseru

Sama biblioteka je upakovana u ZIP i nije šifrovana.

Traženje ranjivosti u UC Browseru

Potražite kod za dešifriranje saobraćaja

Pokušajmo dešifrirati odgovor servera. Pogledajmo šifru klase com.uc.deployment.UpgradeDeployService: iz metode onStartCommand idi 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 pažnju na kreiranje niza od 16 bajtova i njegovo popunjavanje: 0x5F, 0, 0x1F, -50 (=0xCE). Poklapa se sa onim što smo videli u zahtevu iznad.

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 da li je nulti bajt 0x60 ili je treći bajt 0xD0, a drugi bajt 1, 11 ili 0x1F. Gledamo odgovor sa servera: nulti bajt je 0x60, drugi je 0x1F, treći je 0x60. Zvuči kao ono što nam treba. Sudeći po redovima ("up_decrypt", na primjer), ovdje treba pozvati metodu koja će dešifrirati odgovor servera.
Pređimo na metodu gj. Imajte na umu da je prvi argument bajt na pomaku 2 (tj. 0x1F u našem slučaju), a drugi je odgovor servera 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čigledno, ovdje biramo algoritam dešifriranja, i to isti bajt koji je u našem
slučaj jednak 0x1F, označava jednu od tri moguće opcije.

Nastavljamo sa analizom koda. Nakon nekoliko skokova nalazimo se u metodi s imenom koje samo po sebi objašnjava decryptBytesByKey.

Ovdje su još dva bajta odvojena od našeg odgovora i iz njih se dobija niz. Jasno je da se na ovaj način odabire ključ za dešifriranje 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 dobijamo ključ, već samo njegov „identifikator“. Dobijanje ključa je malo komplikovanije.

U sljedećoj metodi postojećim se dodaju još dva parametra, čineći ih četiri: magični broj 16, identifikator ključa, šifrirani podaci i nerazumljivi niz (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 tranzicija dolazimo do metode staticBinarySafeDecryptNoB64 sučelje com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. U glavnom kodu aplikacije nema klasa 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 implementira 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ša lista parametara dopunjena sa još dva cijela broja: 2 i 0. Sudeći po
sve, 2 znači dešifriranje, kao u metodi doFinal klasa sistema javax.crypto.Cipher. I sve se to prenosi na određeni ruter s brojem 10601 - ovo je očigledno broj komande.

Nakon sljedećeg lanca prijelaza nalazimo klasu koja implementira interfejs IRouterComponent i metod 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 takođe klasa JNICLibrary, u kojem je deklarirana nativna 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 mašinskog koda

U fajlu libsgmain.so (koji je zapravo .jar i u kojem smo pronašli implementaciju nekih sučelja vezanih za enkripciju neposredno iznad) postoji jedna izvorna biblioteka: libsgmainso-6.4.36.so. Otvaramo ga u IDA-i i dobijamo gomilu dijaloških okvira s greškama. Problem je što je tabela zaglavlja odjeljka nevažeća. Ovo se radi namjerno kako bi se zakomplikovala analiza.

Traženje ranjivosti u UC Browseru

Ali nije potrebno: za ispravno učitavanje ELF datoteke i analizu, dovoljna je tabela zaglavlja programa. Stoga jednostavno brišemo tabelu sekcija, nulirajući odgovarajuća polja u zaglavlju.

Traženje ranjivosti u UC Browseru

Ponovo otvorite datoteku u IDA-i.

Postoje dva načina da se Java virtuelnoj mašini kaže gde se tačno u matičnoj biblioteci nalazi implementacija metode koja je u Java kodu deklarisana kao nativna. Prvi je da mu damo ime vrste Java_package_name_ClassName_MethodName.

Drugi je da ga registrirate prilikom učitavanja biblioteke (u funkciji JNI_OnLoad)
koristeći poziv funkcije RegisterNatives.

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

Takve funkcije nema među izvezenim funkcijama, što znači da morate potražiti poziv RegisterNatives.
Idemo na funkciju JNI_OnLoad i vidimo ovu sliku:

Traženje ranjivosti u UC Browseru

sta se desava ovde? Na prvi pogled, početak i kraj funkcije su tipični za ARM arhitekturu. Prva instrukcija na steku 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 . Posljednja instrukcija vraća sačuvane registre, a povratna adresa se odmah stavlja u PC registar - tako se vraća iz funkcije. Ali ako pažljivo pogledate, primijetit ćete da pretposljednja instrukcija mijenja povratnu adresu pohranjenu na steku. Izračunajmo kako će biti poslije
izvršavanje koda. Određena adresa 1xB0 se učitava u R130, od nje se oduzima 5, zatim se prenosi u R0 i dodaje joj se 0x10. Ispada 0xB13B. Dakle, IDA misli da je posljednja instrukcija normalan povratak funkcije, ali u stvari ide na izračunatu adresu 0xB13B.

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

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

Pošto kod eksplicitno ne skače na 0xB13A, IDA sama nije prepoznala da se kod nalazi na ovoj lokaciji. Iz istog razloga, ne prepoznaje većinu koda u biblioteci kao kod, što čini analizu donekle teškom. Kažemo IDA-i da je ovo kod, i evo šta se dešava:

Traženje ranjivosti u UC Browseru

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

Traženje ranjivosti u UC Browseru

Pozivanjem ove funkcije u LR registru dobijamo adresu prethodno navedene tabele (0xB144). U R0 - indeks u ovoj tabeli. Odnosno, vrijednost se uzima iz tabele, 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 par korisnih uputstava i opet idemo na 0xB140:

Traženje ranjivosti u UC Browseru

Sada će doći do prijelaza u pomaku s indeksom 0x20 iz tabele.

Sudeći po veličini tabele, u kodu će biti mnogo takvih prelaza. Postavlja se pitanje da li je to moguće nekako automatizirati, bez ručnog izračunavanja adresa. U pomoć nam dolaze skripte i mogućnost zakrpe 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 Browseru

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

Traženje ranjivosti u UC Browseru

Uputstva nakon BLX-a izgleda nemaju puno smisla, to je više kao neka vrsta pomaka. Pogledajmo sub_4964:

Traženje ranjivosti u UC Browseru

I zaista, 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 stek. Također, 4 se dodaje u LR tako da se nakon povratka iz funkcije ovaj isti pomak preskače. Nakon toga komanda POP {R1} uzima rezultujuću vrijednost iz steka. Ako pogledate šta se nalazi na adresi 0xB4BA + 0xEA = 0xB5A4, vidjet ćete nešto slično adresnoj tablici:

Traženje ranjivosti u UC Browseru

Da biste zakrpili ovaj dizajn, moraćete da dobijete dva parametra iz koda: ofset i broj registra u koji želite da stavite 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 Browseru

Pored već navedenih struktura, kod sadrži i sljedeće:

Traženje ranjivosti u UC Browseru

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

Traženje ranjivosti u UC Browseru

Uzimamo ofset na adresu od LR, dodajemo ga u LR i idemo tamo. 0x72044 + 0xC = 0x72050. Skripta za ovaj dizajn je prilično 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 Browseru

Kada se sve zakrpi u funkciji, možete uputiti IDA na njen pravi početak. On će spojiti sav kod funkcije i može se dekompilirati pomoću HexRays-a.

Dekodiranje nizova

Naučili smo da se nosimo sa zamagljivanjem mašinskog koda u biblioteci libsgmainso-6.4.36.so iz UC pretraživača i primio kod funkcije 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 bliže sljedeće redove:

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

U funkciji sub_73E24 naziv klase se očito dešifruje. Kao parametri ovoj funkciji, prosljeđuje se pokazivač na podatke koji su slični šifriranim podacima, određeni bafer i broj. Očigledno, nakon poziva funkcije, postojat će dešifrirani red u međuspremniku, jer se prosljeđuje funkciji FindClass, koji uzima ime klase kao drugi parametar. Dakle, broj je veličina bafera ili dužina linije. Pokušajmo dešifrirati naziv klase, trebalo bi nam reći da li idemo u pravom smjeru. Pogledajmo bliže šta se dešava 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 kreira instancu kontejnera za nizove bajtova određene veličine (nećemo se zadržavati na ovim kontejnerima u detalje). Ovdje se kreiraju dva takva kontejnera: 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 postavljaju u određenu strukturu, koja se prosljeđuje funkciji sub_6115C. Označimo u ovoj strukturi i polje sa vrijednošću 3. Da vidimo šta će se dalje dogoditi sa 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 switch je polje strukture kojem je prethodno bila dodijeljena vrijednost 3. Pogledajte slučaj 3: funkciji sub_6364C parametri se prosljeđuju iz strukture koja je tu dodana u prethodnoj funkciji, odnosno 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 šta se dogodilo: com/taobao/wireless/security/adapter/JNICLibrary. Odlično! Na pravom smo putu.

Komandno stablo

Sada moramo pronaći izazov RegisterNatives, što će nas uputiti na funkciju doCommandNative. Pogledajmo funkcije pozvane iz JNI_OnLoad, i nalazimo ga unutra 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;
}

I zaista, ovdje je registrirana nativna metoda s imenom doCommandNative. Sada znamo njegovu adresu. Hajde da vidimo šta radi.

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

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

Ovako nešto:

Traženje ranjivosti u UC Browseru

Stablo se popunjava dinamički JNI_OnLoad.
Tri broja kodiraju putanju u stablu. Svaki list stabla sadrži urezanu adresu odgovarajuće funkcije. Ključ je u roditeljskom čvoru. Pronalaženje mjesta u kodu gdje je funkcija koja nam treba dodana stablu nije teško ako razumijete sve korištene strukture (ne opisujemo ih kako ne bismo naduvali ionako prilično veliki članak).

Više zabune

Dobili smo adresu funkcije koja treba da dešifruje saobraćaj: 0x5F1AC. Ali prerano je za radovanje: programeri UC Browsera pripremili su nam još jedno iznenađenje.

Nakon što primimo parametre iz niza koji je formiran u Java kodu, dobijamo
na funkciju na adresi 0x4D070. I ovdje nas čeka još jedna vrsta zamagljivanja koda.

Stavili smo dva indeksa u R7 i R4:

Traženje ranjivosti u UC Browseru

Prebacujemo prvi indeks na R11:

Traženje ranjivosti u UC Browseru

Da biste dobili adresu iz tabele, koristite indeks:

Traženje ranjivosti u UC Browseru

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

Šta učiniti povodom toga? Možete reći IDA-i da je ovo prekidač: Uredi -> Ostalo -> Odredi idiom prekidača.

Traženje ranjivosti u UC Browseru

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

Traženje ranjivosti u UC Browseru

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 koja je proslijeđena funkciji se popunjava iz parametara kojima se prosljeđuje doCommandNative. Prisjetimo se šta smo tamo imali magicInt sa 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 Browseru

Ovo je AES!

Algoritam postoji, ostaje samo da dobijete njegove parametre: mod, ključ i, eventualno, vektor inicijalizacije (njegovo prisustvo zavisi od načina rada AES algoritma). Struktura s njima mora biti formirana negdje prije poziva funkcije sub_6115C, ali je ovaj dio koda posebno dobro zamagljen, pa se javlja ideja da se kod zakrpi tako da svi parametri funkcije dešifriranja budu izbačeni u datoteku.

Patch

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 i naša funkcija dešifriranja i upisuje u datoteku, a zatim kopirati i zalijepiti kod koji će kompajler generirati.

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

Prvi dio:

Traženje ranjivosti u UC Browseru

U ARM arhitekturi, prva četiri parametra funkcije prolaze kroz registre R0-R3, a ostali, ako ih ima, prolaze kroz stog. LR registar nosi povratnu adresu. Sve ovo treba sačuvati kako bi funkcija mogla raditi nakon što izbacimo njene parametre. Također moramo sačuvati sve registre koje ćemo koristiti u procesu, tako da radimo PUSH.W {R0-R10,LR}. U R7 dobijamo adresu liste parametara prosleđenih funkciji preko steka.

Korištenje funkcije fopen hajde da otvorimo fajl /data/local/tmp/aes u "ab" modu
odnosno za dodavanje. U R0 učitavamo adresu imena datoteke, u R1 - adresu linije koja označava način rada. I ovdje se smeće kod završava, pa prelazimo na sljedeću funkciju. Da bi nastavio da radi, na početak stavljamo prelazak na pravi kod funkcije, zaobilazeći smeće, a umjesto smeća dodajemo nastavak zakrpe.

Traženje ranjivosti u UC Browseru

Zovemo fopen.

Prva tri parametra funkcije AES imaju tip Int. Pošto smo na početku spremili registre u stog, možemo jednostavno proslijediti funkciju fwrite njihove adrese na steku.

Traženje ranjivosti u UC Browseru

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 Browseru

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

Sakupljamo APK sa zakrpljenom bibliotekom, potpisujemo ga, prenosimo na uređaj/emulator i pokrećemo. Vidimo da se naš dump kreira, i tu se upisuje mnogo podataka. Preglednik koristi enkripciju ne samo za promet, već sva enkripcija ide kroz dotičnu funkciju. Ali iz nekog razloga potrebni podaci ne postoje, a traženi zahtjev nije vidljiv u prometu. Kako ne bismo čekali dok se UC Browser ne udostoji podnijeti potreban zahtjev, uzmimo šifrirani odgovor od servera koji je ranije primljen i ponovo 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

Montiramo, potpisujemo, montiramo, lansiramo. Dobijamo NullPointerException jer je metoda vratila null.

Tokom dalje analize koda, otkrivena je funkcija koja dešifruje zanimljive redove: “META-INF/” i “.RSA”. Izgleda da aplikacija provjerava svoj certifikat. Ili čak generira ključeve iz njega. Zaista ne želim da se bavim onim što se dešava sa sertifikatom, pa ćemo mu samo dati ispravan sertifikat. Zakrpimo šifriranu liniju tako da umjesto “META-INF/” dobijemo “BLABLINF/”, kreiramo folder s tim imenom u APK-u i dodamo certifikat pretraživača vjeverica.

Montiramo, potpisujemo, montiramo, lansiramo. Bingo! Imamo ključ!

MitM

Dobili smo ključ i vektor inicijalizacije jednak ključu. Pokušajmo dešifrirati odgovor servera u CBC modu.

Traženje ranjivosti u UC Browseru

Vidimo URL arhive, nešto slično MD5, “extract_unzipsize” i broj. Provjeravamo: MD5 arhive je isti, veličina raspakovane biblioteke je ista. Pokušavamo zakrpiti ovu biblioteku i dati je pretraživaču. Kako bismo pokazali da se naša zakrpljena biblioteka učitala, pokrenućemo Namjeru za kreiranje SMS-a s tekstom “PWNED!” Zamijenit ćemo dva odgovora sa servera: puds.ucweb.com/upgrade/index.xhtml i da preuzmete arhivu. U prvom zamjenjujemo MD5 (veličina se ne mijenja nakon raspakivanja), u drugom dajemo arhivu sa zakrpljenom bibliotekom.

Preglednik pokušava preuzeti arhivu nekoliko puta, nakon čega daje grešku. Očigledno nešto
on ne voli. Kao rezultat analize ovog mutnog formata, pokazalo se da server prenosi i veličinu arhive:

Traženje ranjivosti u UC Browseru

Kodiran je u LEB128. Nakon zakrpe, veličina arhive sa bibliotekom se malo promijenila, pa je pretraživač smatrao da je arhiva krivo preuzeta i nakon nekoliko pokušaja je ispisao grešku.

Podešavamo veličinu arhive... I – pobeda! 🙂 Rezultat je u videu.

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

Posljedice i reakcija razvijača

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

Kontaktirali smo programere UC Browser-a i informisali ih o problemu koji smo pronašli, pokušali da ukažemo na ranjivost i njenu opasnost, ali sa nama nisu razgovarali ni o čemu. U međuvremenu, pretraživač je nastavio da se razmeće svojom opasnom karakteristikom naočigled. Ali kada smo otkrili detalje ranjivosti, više je nije bilo moguće ignorirati kao prije. 27. mart je bio
objavljena je nova verzija UC Browser-a 12.10.9.1193, koja je pristupila serveru preko HTTPS-a: puds.ucweb.com/upgrade/index.xhtml.

Osim toga, nakon "popravka" i do trenutka pisanja ovog članka, pokušaj otvaranja PDF-a u pretraživaču rezultirao je porukom o grešci s tekstom "Ups, nešto je pošlo po zlu!" Zahtjev prema serveru nije učinjen prilikom pokušaja otvaranja PDF-a, ali je zahtjev napravljen kada je pretraživač pokrenut, što ukazuje na nastavak mogućnosti preuzimanja izvršnog koda kršeći pravila Google Playa.

izvor: www.habr.com

Dodajte komentar