UC-brauseris haavatavuste otsimine

UC-brauseris haavatavuste otsimine

Sissejuhatus

Märtsi lõpus me teatatud, et nad avastasid UC-brauseris varjatud võimaluse laadida ja käivitada kontrollimata koodi. Täna vaatame üksikasjalikult, kuidas see allalaadimine toimub ja kuidas häkkerid saavad seda oma eesmärkidel kasutada.

Mõni aeg tagasi reklaamiti ja levitati UC Browserit väga agressiivselt: see installiti kasutajate seadmetesse pahavara abil, mida levitati erinevatelt saitidelt videofailide varjus (st kasutajad arvasid, et laadivad alla näiteks pornovideot, kuid selle asemel sai selle brauseriga APK), kasutas hirmutavaid bännereid sõnumitega, et brauser on aegunud, haavatav ja muud sellist. VK ametlikus UC-brauseri grupis on olemas teema, milles kasutajad saavad kurta ebaausa reklaami üle, on seal palju näiteid. 2016. aastal oli isegi videoreklaam vene keeles (jah, reklaame blokeeriva brauseri reklaam).

Artikli kirjutamise ajal on UC Browseril Google Plays üle 500 000 000 installi. See on muljetavaldav – ainult Google Chrome'is on rohkem. Arvustuste hulgas näete üsna palju kaebusi reklaamide ja mõne Google Play rakenduste ümbersuunamise kohta. See oli meie uurimistöö põhjus: otsustasime vaadata, kas UC Browser teeb midagi halba. Ja selgus, et teebki!

Rakenduse koodis avastati võimalus käivitatavat koodi alla laadida ja käivitada, mis on vastuolus rakenduste avaldamise reeglitega Google Plays. Lisaks käivitatava koodi allalaadimisele teeb UC Browser seda ebaturvaliselt, mida saab kasutada MitM-i rünnaku käivitamiseks. Vaatame, kas suudame sellise rünnaku läbi viia.

Kõik allpool kirjutatud on asjakohane UC-brauseri versiooni jaoks, mis oli uuringu ajal Google Plays saadaval:

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

Rünnaku vektor

UC Browser manifestist leiate teenuse, millel on iseenesestmõistetav nimi com.uc.deployment.UpgradeDeployService.

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

Kui see teenus käivitub, teeb brauser POST-päringu puds.ucweb.com/upgrade/index.xhtml, mida on liikluses näha mõni aeg peale starti. Vastuseks võib ta saada käsu mõne värskenduse või uue mooduli allalaadimiseks. Analüüsi käigus server selliseid käske ei andnud, kuid märkasime, et kui proovime brauseris PDF-i avada, teeb see ülaltoodud aadressile teise päringu, misjärel laadib oma teegi alla. Rünnaku läbiviimiseks otsustasime kasutada seda UC-brauseri funktsiooni: võimalust avada PDF-i natiivse teegi abil, mida APK-s pole ja mille see vajadusel Internetist alla laadib. Väärib märkimist, et teoreetiliselt saab UC-brauserit sundida midagi alla laadima ilma kasutaja sekkumiseta – kui annate päringule hästi vormistatud vastuse, mis täidetakse pärast brauseri käivitamist. Kuid selleks peame üksikasjalikumalt uurima serveriga suhtlemise protokolli, mistõttu otsustasime, et pealtkuulatud vastust on lihtsam redigeerida ja raamatukogu PDF-iga töötamiseks asendada.

Seega, kui kasutaja soovib avada PDF-i otse brauseris, on liikluses näha järgmised päringud:

UC-brauseris haavatavuste otsimine

Esiteks on POST-i päring puds.ucweb.com/upgrade/index.xhtml, siis
Laaditakse alla arhiiv koos raamatukoguga PDF-i ja kontorivormingute vaatamiseks. Loogiline on eeldada, et esimene päring edastab teavet süsteemi kohta (vähemalt vajaliku teegi pakkumiseks vajaliku arhitektuuri kohta) ja vastuseks sellele saab brauser teatud teavet allalaaditava teegi kohta: aadressi ja võimalusel ka , midagi muud. Probleem on selles, et see päring on krüpteeritud.

Taotluse fragment

Vastuse fragment

UC-brauseris haavatavuste otsimine

UC-brauseris haavatavuste otsimine

Teek ise on pakitud ZIP-vormingusse ega ole krüptitud.

UC-brauseris haavatavuste otsimine

Otsige liikluse dekrüpteerimiskoodi

Proovime serveri vastust dešifreerida. Vaatame klassi koodi com.uc.deployment.UpgradeDeployService: meetodist on StartCommand minema com.uc.deployment.bx, ja sellest kuni 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);
}

Siin näeme POST-päringu moodustumist. Pöörame tähelepanu 16-baidise massiivi loomisele ja selle täitmisele: 0x5F, 0, 0x1F, -50 (=0xCE). Ühttub sellega, mida nägime ülaltoodud taotluses.

Samas klassis näete pesastatud klassi, millel on veel üks huvitav meetod:

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

Meetod kasutab sisendiks baitide massiivi ja kontrollib, kas nullbait on 0x60 või kolmas bait 0xD0 ja teine ​​bait on 1, 11 või 0x1F. Vaatame serveri vastust: nullbait on 0x60, teine ​​on 0x1F, kolmas on 0x60. Kõlab nagu see, mida me vajame. Ridade järgi otsustades (näiteks "up_decrypt") tuleks siin kutsuda meetod, mis dekrüpteerib serveri vastuse.
Liigume edasi meetodi juurde gj. Pange tähele, et esimene argument on bait nihkes 2 (st meie puhul 0x1F) ja teine ​​on serveri vastus ilma
esimesed 16 baiti.

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

Ilmselgelt valime siin dekrüpteerimisalgoritmi ja sama baidi, mis on meie käsutuses
suurjuht 0x1F, tähistab ühte kolmest võimalikust valikust.

Jätkame koodi analüüsimist. Pärast paari hüpet leiame end iseenesestmõistetava nimega meetodist decryptBytesByKey.

Siin eraldatakse meie vastusest veel kaks baiti ja neist saadakse string. On selge, et sel viisil valitakse sõnumi dekrüpteerimise võti.

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

Tulevikku vaadates märgime, et selles etapis me ei saa veel võtit, vaid ainult selle "identifikaatorit". Võtme saamine on veidi keerulisem.

Järgmise meetodi puhul lisatakse olemasolevatele veel kaks parameetrit, mis teeb neist neli: maagiline number 16, võtme identifikaator, krüptitud andmed ja arusaamatu string (meie puhul tühi).

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

Pärast mitmeid üleminekuid jõuame meetodini staticBinarySafeDecryptNoB64 liides com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Peamise rakenduse koodis pole klasse, mis seda liidest rakendaksid. Toimikus on selline klass lib/armeabi-v7a/libsgmain.so, mis tegelikult ei ole .so, vaid .jar. Meetod, millest oleme huvitatud, rakendatakse järgmiselt:

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

Siin on meie parameetrite loend täiendatud veel kahe täisarvuga: 2 ja 0. Otsustades
kõike, 2 tähendab dekrüpteerimist, nagu meetodis doFinal süsteemi klass javax.crypto.Cipher. Ja kõik see kantakse üle teatud ruuterile numbriga 10601 - see on ilmselt käsunumber.

Pärast järgmist üleminekute ahelat leiame klassi, mis rakendab liidest IRouterComponent ja meetod 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);
}
}

Ja ka klass JNICraamatukogu, milles on deklareeritud loomulik meetod doCommandNative:

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

See tähendab, et peame leidma meetodi loomulikus koodis doCommandNative. Ja siit algab lõbu.

Masinakoodi hägustamine

Failis libsgmain.so (mis on tegelikult .jar ja milles leidsime mõned krüptimisega seotud liidesed just ülalpool) on üks omateek: libsgmainso-6.4.36.so. Avame selle IDA-s ja saame hulga vigadega dialoogibokse. Probleem on selles, et jaotise päise tabel on kehtetu. Seda tehakse sihilikult analüüsi keerulisemaks muutmiseks.

UC-brauseris haavatavuste otsimine

Kuid seda pole vaja: ELF-faili korrektseks laadimiseks ja analüüsimiseks piisab programmi päise tabelist. Seetõttu kustutame lihtsalt jaotise tabeli, nullides päises vastavad väljad.

UC-brauseris haavatavuste otsimine

Avage fail uuesti IDA-s.

Java virtuaalmasinale on kaks võimalust öelda, kus täpselt natiivses teegis asub Java koodis natiivseks deklareeritud meetodi rakendamine. Esimene on anda sellele liiginimi Java_paketi_nimi_ClassName_MethodName.

Teine on selle registreerimine teegi laadimisel (funktsioonis JNI_OnLoad)
funktsioonikutset kasutades Registreeri Põliselanikud.

Meie puhul, kui kasutame esimest meetodit, peaks nimi olema selline: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Eksporditud funktsioonide hulgas sellist funktsiooni pole, mis tähendab, et peate helistama Registreeri Põliselanikud.
Läheme funktsiooni juurde JNI_OnLoad ja me näeme seda pilti:

UC-brauseris haavatavuste otsimine

Mis siin toimub? Esmapilgul on funktsiooni algus ja lõpp tüüpilised ARM-i arhitektuurile. Esimene käsk pinu salvestab registrite sisu, mida funktsioon oma töös kasutab (antud juhul R0, R1 ja R2), samuti LR registri sisu, mis sisaldab funktsiooni tagastusaadressi. . Viimane käsk taastab salvestatud registrid ning tagastusaadress kantakse koheselt PC registrisse – seega naaseb funktsioonist. Kuid kui vaatate tähelepanelikult, märkate, et eelviimane käsk muudab virna salvestatud tagastusaadressi. Arvutame välja, kuidas see pärast on
koodi täitmine. Teatud aadress 1xB0 laaditakse R130-sse, sellest lahutatakse 5, seejärel kantakse R0-sse ja sellele lisatakse 0x10. Selgub, et 0xB13B. Seega arvab IDA, et viimane käsk on tavaline funktsiooni tagastamine, kuid tegelikult läheb see arvutatud aadressile 0xB13B.

Siinkohal tasub meenutada, et ARM-protsessoritel on kaks režiimi ja kaks juhiste komplekti: ARM ja Thumb. Aadressi kõige vähem oluline bitt annab protsessorile teada, millist käsukomplekti kasutatakse. See tähendab, et aadress on tegelikult 0xB13A ja üks kõige vähem olulisest bitist näitab pöidla režiimi.

Sarnane "adapter" on lisatud iga funktsiooni algusesse selles teegis ja
prügi kood. Me ei peatu neil pikemalt - me lihtsalt mäletame
et peaaegu kõigi funktsioonide tegelik algus on veidi kaugemal.

Kuna kood ei hüppa selgelt 0xB13A-le, ei tuvastanud IDA ise, et kood asus selles kohas. Samal põhjusel ei tuvasta see enamikku teegis olevast koodist koodina, mis muudab analüüsi mõnevõrra keeruliseks. Me ütleme IDA-le, et see on kood, ja see juhtub:

UC-brauseris haavatavuste otsimine

Tabel algab selgelt 0xB144-st. Mis on sub_494C-s?

UC-brauseris haavatavuste otsimine

Seda funktsiooni LR registris kutsudes saame eelnevalt mainitud tabeli aadressi (0xB144). In R0 - indeks selles tabelis. See tähendab, et väärtus võetakse tabelist, lisatakse LR-i ja tulemus on
aadress, kuhu minna. Proovime seda arvutada: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Läheme saadud aadressile ja näeme sõna otseses mõttes paar kasulikku juhist ning läheme uuesti 0xB140 juurde:

UC-brauseris haavatavuste otsimine

Nüüd toimub tabelist nihkega üleminek indeksiga 0x20.

Tabeli suuruse järgi otsustades on selliseid üleminekuid koodis palju. Tekib küsimus, kas sellega saab kuidagi automaatsemalt hakkama, ilma käsitsi aadresse arvutamata. Ja meile tulevad appi skriptid ja võimalus IDA-s koodi parandada:

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"

Asetage kursor reale 0xB26A, käivitage skript ja vaadake üleminekut 0xB4B0-le:

UC-brauseris haavatavuste otsimine

IDA jällegi ei tunnistanud seda ala koodina. Aitame teda ja näeme seal teist kujundust:

UC-brauseris haavatavuste otsimine

Juhised pärast BLX-i ei tundu olevat eriti mõttekad, pigem on see mingi nihe. Vaatame sub_4964:

UC-brauseris haavatavuste otsimine

Ja tõepoolest, siin võetakse LR-is asuval aadressil dword, mis sellele aadressile lisatakse, mille järel võetakse saadud aadressi väärtus ja pannakse virna. Samuti lisatakse LR-i 4, nii et pärast funktsioonist naasmist jäetakse see sama nihe vahele. Pärast seda võtab käsk POP {R1} virust saadud väärtuse. Kui vaatate seda, mis asub aadressil 0xB4BA + 0xEA = 0xB5A4, näete midagi aadressitabeli sarnast:

UC-brauseris haavatavuste otsimine

Selle kujunduse lappimiseks peate koodist saama kaks parameetrit: nihe ja registrinumber, kuhu soovite tulemuse panna. Iga võimaliku registri jaoks peate eelnevalt ette valmistama koodijupi.

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"

Asetame kursori selle struktuuri algusesse, mida tahame asendada - 0xB4B2 - ja käivitame skripti:

UC-brauseris haavatavuste otsimine

Lisaks juba mainitud struktuuridele sisaldab kood ka järgmist:

UC-brauseris haavatavuste otsimine

Nagu eelmisel juhul, on pärast BLX-i käsku nihe:

UC-brauseris haavatavuste otsimine

Võtame nihke LR-ist aadressile, lisame selle LR-i ja läheme sinna. 0x72044 + 0xC = 0x72050. Selle kujunduse skript on üsna lihtne:

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"

Skripti täitmise tulemus:

UC-brauseris haavatavuste otsimine

Kui funktsioonis on kõik paigatud, saate suunata IDA selle tegelikule algusele. See koondab kogu funktsioonikoodi ja seda saab HexRaysi abil dekompileerida.

Stringide dekodeerimine

Oleme õppinud tegelema masinkoodi segamisega raamatukogus libsgmainso-6.4.36.so UC Browserist ja sai funktsioonikoodi 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;
}

Vaatame lähemalt järgmisi ridu:

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

Funktsioonis sub_73E24 klassi nimi on selgelt dekrüpteerimisel. Selle funktsiooni parameetritena edastatakse krüptitud andmetega sarnaste andmete osutaja, teatud puhver ja number. Ilmselt on pärast funktsiooni kutsumist puhvris dekrüpteeritud rida, kuna see edastatakse funktsioonile FindClass, mis võtab teise parameetrina klassi nime. Seetõttu on arv puhvri suurus või rea pikkus. Proovime klassi nime dešifreerida, see peaks meile ütlema, kas me liigume õiges suunas. Vaatame lähemalt, mis toimub 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;
}

Funktsioon sub_7AF78 loob konteineri eksemplari määratud suurusega baidimassiivide jaoks (üksikasjalikult me ​​nendel konteineritel ei peatu). Siin luuakse kaks sellist konteinerit: üks sisaldab rida "DcO/lcK+h?m3c*q@" (lihtne on arvata, et see on võti), teine ​​sisaldab krüptitud andmeid. Järgmisena paigutatakse mõlemad objektid teatud struktuuri, mis edastatakse funktsioonile sub_6115C. Märgime selles struktuuris ka välja väärtusega 3. Vaatame, mis sellest struktuurist edasi saab.

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

Lüliti parameeter on struktuuriväli, millele oli varem määratud väärtus 3. Vaadake funktsiooni juhtumit 3: sub_6364C parameetrid edastatakse struktuurist, mis sinna eelmises funktsioonis lisatud, st võtmest ja krüptitud andmetest. Kui tähelepanelikult vaadata sub_6364C, tunnete selles ära RC4 algoritmi.

Meil on algoritm ja võti. Proovime klassi nime lahti mõtestada. See juhtus järgmiselt. com/taobao/wireless/security/adapter/JNICLibrary. Suurepärane! Oleme õigel teel.

Käsupuu

Nüüd peame leidma väljakutse Registreeri Põliselanikud, mis suunab meid funktsioonile doCommandNative. Vaatame funktsioone, millest välja kutsutakse JNI_OnLoad, ja leiame selle sisse 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;
}

Ja tõepoolest, siin on registreeritud omapärane meetod nimega doCommandNative. Nüüd teame tema aadressi. Vaatame, mida ta teeb.

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

Nime järgi võite arvata, et siin on kõigi funktsioonide sisenemispunkt, mille arendajad otsustasid omaraamatukogusse üle kanda. Oleme huvitatud funktsioonist 10601.

Koodist on näha, et käsunumber annab kolm numbrit: käsk/10000, käsk % 10000/100 и käsk % 10, st meie puhul 1, 6 ja 1. Need kolm numbrit, samuti osuti JNIEnv ja funktsioonile edastatud argumendid lisatakse struktuuri ja antakse edasi. Saadud kolme arvu (tähistagem N1, N2 ja N3) abil koostatakse käsupuu.

Midagi sellist:

UC-brauseris haavatavuste otsimine

Puu täidetakse dünaamiliselt JNI_OnLoad.
Kolm numbrit kodeerivad puusse tee. Iga puu leht sisaldab vastava funktsiooni sisestatud aadressi. Võti asub vanemsõlmes. Koodis selle koha leidmine, kus meile vajalik funktsioon puule lisatakse, pole keeruline, kui mõistate kõiki kasutatud struktuure (me ei kirjelda neid, et mitte niigi üsna mahukat artiklit üles ajada).

Rohkem segadust

Saime selle funktsiooni aadressi, mis peaks liikluse dekrüpteerima: 0x5F1AC. Kuid veel on vara rõõmustada: UC Browseri arendajad on meile valmistanud veel ühe üllatuse.

Pärast Java-koodis moodustatud massiivi parameetrite saamist saame
funktsioonile aadressil 0x4D070. Ja siin ootab meid ees teist tüüpi koodide segamine.

Panime R7-sse ja R4-sse kaks indeksit:

UC-brauseris haavatavuste otsimine

Nihutame esimese indeksi väärtusele R11:

UC-brauseris haavatavuste otsimine

Tabelist aadressi saamiseks kasutage indeksit:

UC-brauseris haavatavuste otsimine

Pärast esimesele aadressile minekut kasutatakse teist indeksit, mis asub R4-s. Tabelis on 230 elementi.

Mida sellega teha? Saate IDA-le öelda, et see on lüliti: Redigeerimine -> Muu -> Määrake lüliti idioom.

UC-brauseris haavatavuste otsimine

Saadud kood on hirmutav. Kuid läbi selle džungli liikudes võite märgata väljakutset meile juba tuttavale funktsioonile sub_6115C:

UC-brauseris haavatavuste otsimine

Seal oli lüliti, milles 3. juhul toimus dekrüpteerimine RC4 algoritmi abil. Ja sel juhul täidetakse funktsioonile edastatud struktuur parameetritest, mis edastatakse doCommandNative. Meenutagem, mis meil seal oli magicInt väärtusega 16. Vaatame vastavat juhtumit - ja peale mitmeid üleminekuid leiame koodi, mille järgi saab algoritmi tuvastada.

UC-brauseris haavatavuste otsimine

See on AES!

Algoritm on olemas, jääb üle vaid hankida selle parameetrid: režiim, võti ja võib-olla ka initsialiseerimisvektor (selle olemasolu sõltub AES-algoritmi töörežiimist). Nendega struktuur peab olema moodustatud kuskil enne funktsiooni kutset sub_6115C, kuid see osa koodist on eriti hästi hägustatud, nii et tekib mõte kood lappida nii, et kõik dekrüpteerimisfunktsiooni parameetrid kantakse faili.

Plaaster

Selleks, et mitte kirjutada kogu paigakoodi käsitsi montaažikeeles, võite käivitada Android Studio, kirjutada sinna funktsiooni, mis saab samad sisendparameetrid kui meie dekrüpteerimisfunktsioon ja kirjutab faili, seejärel kopeerige-kleepige kood, mille kompilaator soovib. genereerida.

Koodi lisamise mugavuse eest hoolitsesid ka meie sõbrad UC Browseri meeskonnast. Pidagem meeles, et iga funktsiooni alguses on meil prügikood, mida saab kergesti asendada mis tahes muuga. Väga mugav 🙂 Kuid sihtfunktsiooni alguses ei ole piisavalt ruumi koodi jaoks, mis salvestab kõik parameetrid faili. Pidin selle osadeks jagama ja kasutama naaberfunktsioonide prügiplokke. Kokku oli neli osa.

Esimene osa:

UC-brauseris haavatavuste otsimine

ARM-i arhitektuuris edastatakse neli esimest funktsiooniparameetrit registrite R0-R3 kaudu, ülejäänud, kui neid on, suunatakse läbi pinu. LR register kannab tagastusaadressi. Kõik see tuleb salvestada, et funktsioon saaks pärast parameetrite kustutamist töötada. Samuti peame salvestama kõik registrid, mida protsessis kasutame, nii et teeme PUSH.W {R0-R10,LR}. R7-s saame virna kaudu funktsioonile edastatud parameetrite loendi aadressi.

Funktsiooni kasutamine avatud avame faili /data/local/tmp/aes "ab" režiimis
st lisamiseks. R0-s laadime failinime aadressi, R1-s - režiimi näitava rea ​​aadressi. Ja siin lõpeb prügikood, nii et liigume järgmise funktsiooni juurde. Et see edasi töötaks, panime algusesse ülemineku funktsiooni päriskoodile, jättes prügist mööda ja prügi asemele lisame plaastri jätku.

UC-brauseris haavatavuste otsimine

Helistamine avatud.

Funktsiooni kolm esimest parameetrit AES omama tüüpi int. Kuna me salvestasime registrid alguses virna, siis saame funktsiooni lihtsalt edasi anda fwrite nende aadressid virna peal.

UC-brauseris haavatavuste otsimine

Järgmisena on meil kolm struktuuri, mis sisaldavad andmete suurust ja osutit võtme, lähtestamisvektori ja krüptitud andmete andmetele.

UC-brauseris haavatavuste otsimine

Lõpus sulgege fail, taastage registrid ja viige juhtimine üle tegelikule funktsioonile AES.

Kogume paigatud teegiga APK, allkirjastame selle, laadime selle seadmesse/emulaatorisse ja käivitame. Näeme, et meie prügimägi luuakse ja sinna kirjutatakse palju andmeid. Brauser ei kasuta krüptimist mitte ainult liikluse jaoks, vaid kogu krüptimine läbib kõnealuse funktsiooni. Kuid millegipärast pole vajalikke andmeid olemas ja vajalikku päringut pole liikluses näha. Et mitte oodata, kuni UC-brauser vajaliku päringu esitama hakkab, võtame varem serverilt saadud krüpteeritud vastuse ja parandame rakendust uuesti: lisage dekrüpteerimine põhitegevuse onCreate'ile.

    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

Komplekteerime, allkirjastame, paigaldame, käivitame. Saame NullPointerExceptioni, kuna meetod tagastas nulli.

Koodi edasise analüüsi käigus avastati funktsioon, mis dešifreerib huvitavaid ridu: “META-INF/” ja “.RSA”. Näib, et rakendus kontrollib oma sertifikaati. Või isegi genereerib sellest võtmeid. Ma ei taha tegelikult sertifikaadiga toimuvaga tegeleda, seega paneme selle õige sertifikaadi vahele. Parandame krüpteeritud rida nii, et “META-INF/” asemel saame “BLABLINF/”, looge APK-s sellenimeline kaust ja lisage sinna oravabrauseri sertifikaat.

Komplekteerime, allkirjastame, paigaldame, käivitame. Bingo! Võti on meil käes!

MitM

Saime võtme ja võtmega võrdse initsialiseerimisvektori. Proovime serveri vastust CBC-režiimis dekrüpteerida.

UC-brauseris haavatavuste otsimine

Näeme arhiivi URL-i, mis on sarnane MD5-ga, "extract_unzipsize" ja number. Kontrollime: arhiivi MD5 on sama, lahtipakitud teegi suurus on sama. Püüame seda teeki parandada ja brauserile anda. Näitamaks, et meie paigatud teek on laaditud, käivitame kavatsuse luua SMS tekstiga "PWNED!" Asendame kaks vastust serverist: puds.ucweb.com/upgrade/index.xhtml ja arhiivi allalaadimiseks. Esimeses asendame MD5 (suurus ei muutu pärast lahtipakkimist), teises anname arhiivi koos paigatud teegiga.

Brauser proovib arhiivi mitu korda alla laadida, misjärel annab tõrketeate. Ilmselt midagi
talle ei meeldi. Selle häguse vormingu analüüsimise tulemusena selgus, et server edastab ka arhiivi suuruse:

UC-brauseris haavatavuste otsimine

See on kodeeritud LEB128-sse. Pärast paikamist muutus veidi arhiivi suurus koos teegiga, nii et brauser leidis, et arhiiv laaditi alla viltu ja pärast mitut katset viskas see vea.

Kohendame arhiivi suurust... Ja – võit! 🙂 Tulemus on videos.

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

Tagajärjed ja arendaja reaktsioon

Samamoodi võivad häkkerid kasutada UC-brauseri ebaturvalist funktsiooni pahatahtlike teekide levitamiseks ja käitamiseks. Need teegid töötavad brauseri kontekstis, nii et nad saavad kõik selle süsteemi load. Selle tulemusel on võimalus kuvada andmepüügiaknaid, samuti juurdepääs oranži hiina orava tööfailidele, sealhulgas andmebaasi salvestatud sisselogimisandmetele, paroolidele ja küpsistele.

Võtsime ühendust UC Browseri arendajatega ja teavitasime neid leitud probleemist, püüdsime välja tuua haavatavuse ja selle ohtlikkuse, kuid nad ei arutanud meiega midagi. Samal ajal jätkas brauser oma ohtliku funktsiooniga silmapiiril uhkeldamist. Kuid kui me haavatavuse üksikasjad paljastasime, ei olnud seda enam võimalik ignoreerida nagu varem. 27. märts oli
ilmus UC Browser 12.10.9.1193 uus versioon, mis pääses serverisse HTTPS-i kaudu: puds.ucweb.com/upgrade/index.xhtml.

Peale selle, pärast "parandust" ja kuni selle artikli kirjutamiseni, andis brauseris PDF-i avamine tõrketeate tekstiga "Oih, midagi läks valesti!" Pdf-i avamisel serverile päringut ei tehtud, küll aga brauseri käivitamisel, mis viitab jätkuvale võimalusele alla laadida täitmiskoodi Google Play reegleid rikkudes.

Allikas: www.habr.com

Lisa kommentaar