Etsitään haavoittuvuuksia UC-selaimesta

Etsitään haavoittuvuuksia UC-selaimesta

Esittely

Maaliskuun lopussa me raportoitu, että he löysivät piilotetun mahdollisuuden ladata ja suorittaa vahvistamatonta koodia UC-selaimessa. Tänään tarkastelemme yksityiskohtaisesti, kuinka tämä lataus tapahtuu ja kuinka hakkerit voivat käyttää sitä omiin tarkoituksiinsa.

Jokin aika sitten UC-selainta mainostettiin ja levitettiin erittäin aggressiivisesti: se asennettiin käyttäjien laitteisiin haittaohjelmien avulla, jaettuna eri sivustoilta videotiedostojen varjolla (eli käyttäjät luulivat lataavansa esimerkiksi pornovideon, mutta sen sijaan sai APK:n tällä selaimella), käytti pelottavia bannereita, joissa kerrottiin, että selain on vanhentunut, haavoittuva ja muuta sellaista. VK:n virallisessa UC-selainryhmässä on teema, jossa käyttäjät voivat valittaa epäreilusta mainonnasta, esimerkkejä on monia. Vuonna 2016 oli jopa videomainonta venäjäksi (kyllä, mainoksia estävän selaimen mainonta).

Kirjoitushetkellä UC Browserilla on yli 500 000 000 asennusta Google Playssa. Tämä on vaikuttava – vain Google Chromessa on enemmän. Arvostelujen joukossa voit nähdä melko paljon valituksia mainonnasta ja uudelleenohjauksista joihinkin Google Play -sovelluksiin. Tämä oli syy tutkimuksellemme: päätimme nähdä, tekikö UC Browser jotain pahaa. Ja kävi ilmi, että hän tekee!

Sovelluskoodissa havaittiin kyky ladata ja suorittaa suoritettavaa koodia, mikä on hakemusten julkaisemista koskevien sääntöjen vastaista Google Playssa. Suoritettavan koodin lataamisen lisäksi UC Browser tekee sen epävarmalla tavalla, jota voidaan käyttää MitM-hyökkäyksen käynnistämiseen. Katsotaan, pystymmekö toteuttamaan tällaisen hyökkäyksen.

Kaikki alla kirjoitettu koskee UC-selaimen versiota, joka oli saatavilla Google Playsta tutkimuksen aikaan:

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

Hyökkäysvektori

UC Browser -luettelosta löydät palvelun, jonka nimi on itsestään selvä com.uc.deployment.UpgradeDeployService.

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

Kun tämä palvelu käynnistyy, selain lähettää POST-pyynnön puds.ucweb.com/upgrade/index.xhtml, joka näkyy liikenteessä jonkin aikaa alun jälkeen. Vastauksena hän voi saada komennon ladata päivityksen tai uuden moduulin. Analyysin aikana palvelin ei antanut tällaisia ​​komentoja, mutta huomasimme, että kun yritämme avata PDF-tiedoston selaimessa, se tekee toisen pyynnön yllä mainittuun osoitteeseen, jonka jälkeen se lataa alkuperäisen kirjaston. Hyökkäyksen toteuttamiseen päätimme käyttää tätä UC Browserin ominaisuutta: kykyä avata PDF käyttämällä alkuperäistä kirjastoa, joka ei ole APK:ssa ja jonka se lataa tarvittaessa Internetistä. On syytä huomata, että teoriassa UC Browser voidaan pakottaa lataamaan jotain ilman käyttäjän vuorovaikutusta - jos annat hyvin muotoillun vastauksen pyyntöön, joka suoritetaan selaimen käynnistämisen jälkeen. Mutta tehdäksesi tämän, meidän on tutkittava yksityiskohtaisemmin vuorovaikutusprotokollaa palvelimen kanssa, joten päätimme, että on helpompi muokata siepattua vastausta ja korvata kirjasto PDF-työskentelyä varten.

Joten kun käyttäjä haluaa avata PDF-tiedoston suoraan selaimessa, seuraavat pyynnöt näkyvät liikenteessä:

Etsitään haavoittuvuuksia UC-selaimesta

Ensin on POST-pyyntö puds.ucweb.com/upgrade/index.xhtml, sitten
Ladataan arkisto, jossa on kirjasto PDF- ja toimistomuotojen katselua varten. On loogista olettaa, että ensimmäinen pyyntö välittää tietoa järjestelmästä (ainakin tarvittavan kirjaston arkkitehtuurista) ja vastauksena selain saa jonkin verran tietoa ladattavasta kirjastosta: osoitteen ja mahdollisesti , jotain muuta. Ongelmana on, että tämä pyyntö on salattu.

Pyydä fragmentti

Vastauksen fragmentti

Etsitään haavoittuvuuksia UC-selaimesta

Etsitään haavoittuvuuksia UC-selaimesta

Itse kirjasto on pakattu ZIP-muotoon, eikä sitä ole salattu.

Etsitään haavoittuvuuksia UC-selaimesta

Etsi liikenteen salauksen koodi

Yritetään tulkita palvelimen vastaus. Katsotaanpa luokan koodia com.uc.deployment.UpgradeDeployService: menetelmästä on StartCommand mene com.uc.deployment.bx, ja siitä 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);
}

Näemme tässä POST-pyynnön muodostumisen. Kiinnitämme huomiota 16 tavun taulukon luomiseen ja sen täyttöön: 0x5F, 0, 0x1F, -50 (=0xCE). Vastaa sitä, mitä näimme yllä olevassa pyynnössä.

Samassa luokassa näet sisäkkäisen luokan, jolla on toinen mielenkiintoinen menetelmä:

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

Menetelmä ottaa syötteeksi joukon tavuja ja tarkistaa, että nollatavu on 0x60 tai kolmas tavu 0xD0 ja toinen tavu 1, 11 tai 0x1F. Tarkastelemme vastausta palvelimelta: nollatavu on 0x60, toinen on 0x1F, kolmas on 0x60. Kuulostaa siltä, ​​mitä tarvitsemme. Rivien perusteella (esimerkiksi "up_decrypt") päätellen tässä tulisi kutsua menetelmä, joka purkaa palvelimen vastauksen.
Siirrytään menetelmään gj. Huomaa, että ensimmäinen argumentti on tavu offsetissa 2 (eli 0x1F meidän tapauksessamme), ja toinen on palvelimen vastaus ilman
ensimmäiset 16 tavua.

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

Ilmeisesti tässä valitsemme salauksenpurkualgoritmin ja saman tavun, joka on meidän
tapaus on yhtä suuri kuin 0x1F, tarkoittaa yhtä kolmesta mahdollisesta vaihtoehdosta.

Jatkamme koodin analysointia. Parin hypyn jälkeen löydämme itsemme menetelmästä, jolla on itsestään selvä nimi decryptBytesByKey.

Tässä vastauksestamme erotetaan vielä kaksi tavua ja niistä saadaan merkkijono. On selvää, että tällä tavalla valitaan avain viestin salauksen purkamiseksi.

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

Tulevaisuudessa huomaamme, että tässä vaiheessa emme vielä saa avainta, vaan ainoastaan ​​sen "tunnisteen". Avaimen saaminen on hieman monimutkaisempaa.

Seuraavassa menetelmässä olemassa oleviin parametreihin lisätään kaksi muuta, jolloin niistä tulee neljä: maaginen numero 16, avaimen tunniste, salattu data ja käsittämätön merkkijono (tapauksessamme tyhjä).

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

Muutosten jälkeen pääsemme menetelmään staticBinarySafeDecryptNoB64 liitäntä com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Pääsovelluskoodissa ei ole luokkia, jotka toteuttavat tämän käyttöliittymän. Tiedostossa on tällainen luokka lib/armeabi-v7a/libsgmain.so, joka ei itse asiassa ole .so, vaan .jar. Meitä kiinnostava menetelmä toteutetaan seuraavasti:

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

Tässä parametriluetteloamme on täydennetty kahdella muulla kokonaisluvulla: 2 ja 0. Päätellen
kaikki, 2 tarkoittaa salauksen purkamista, kuten menetelmässä doFinal järjestelmäluokka javax.crypto.Cipher. Ja kaikki tämä siirretään tietylle reitittimelle numerolla 10601 - tämä on ilmeisesti komentonumero.

Seuraavan siirtymäketjun jälkeen löydämme luokan, joka toteuttaa rajapinnan IRouterComponent ja menetelmä 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 myös luokkaa JNIC-kirjasto, jossa natiivi menetelmä on ilmoitettu doCommandNative:

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

Tämä tarkoittaa, että meidän on löydettävä menetelmä alkuperäisestä koodista doCommandNative. Ja tästä hauskuus alkaa.

Konekoodin hämärtäminen

Tiedostossa libsgmain.so (joka on itse asiassa .jar ja josta löysimme joitain salaukseen liittyviä liitäntöjä juuri yllä) on yksi natiivikirjasto: libsgmainso-6.4.36.so. Avaamme sen IDA:ssa ja saamme joukon valintaikkunoita, joissa on virheitä. Ongelmana on, että osion otsikkotaulukko on virheellinen. Tämä tehdään tarkoituksella analyysin vaikeuttamiseksi.

Etsitään haavoittuvuuksia UC-selaimesta

Mutta sitä ei tarvita: ELF-tiedoston lataamiseksi oikein ja sen analysoimiseksi riittää ohjelman otsikkotaulukko. Siksi yksinkertaisesti poistamme osiotaulukon ja nollaamme vastaavat kentät otsikosta.

Etsitään haavoittuvuuksia UC-selaimesta

Avaa tiedosto uudelleen IDA:ssa.

On kaksi tapaa kertoa Java-virtuaalikoneelle, missä tarkalleen alkuperäisessä kirjastossa Java-koodissa alkuperäiseksi julistetun menetelmän toteutus sijaitsee. Ensimmäinen on antaa sille lajinimi Java_package_name_ClassName_MethodName.

Toinen on rekisteröidä se kirjastoa ladattaessa (funktiossa JNI_OnLoad)
käyttämällä funktiokutsua Rekisteröidy Alkuperäiset.

Meidän tapauksessamme, jos käytämme ensimmäistä menetelmää, nimen tulisi olla tällainen: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Viedyissä funktioissa ei ole tällaista toimintoa, mikä tarkoittaa, että sinun on etsittävä puhelua Rekisteröidy Alkuperäiset.
Mennään toimintoon JNI_OnLoad ja näemme tämän kuvan:

Etsitään haavoittuvuuksia UC-selaimesta

Mitä täällä tapahtuu? Ensi silmäyksellä toiminnon alku ja loppu ovat tyypillisiä ARM-arkkitehtuurille. Ensimmäinen käsky pinoon tallentaa niiden rekisterien sisällön, joita toiminto käyttää toiminnassaan (tässä tapauksessa R0, R1 ja R2), sekä LR-rekisterin sisällön, joka sisältää funktion paluuosoitteen. . Viimeinen käsky palauttaa tallennetut rekisterit ja palautusosoite sijoitetaan välittömästi PC-rekisteriin - palaten siten toiminnosta. Mutta jos katsot tarkasti, huomaat, että toiseksi viimeinen käsky muuttaa pinoon tallennettua palautusosoitetta. Lasketaan, miltä sen jälkeen tulee
koodin suoritus. Tietty osoite 1xB0 ladataan R130:een, siitä vähennetään 5, sitten se siirretään R0:aan ja siihen lisätään 0x10. Osoittautuu 0xB13B. Näin ollen IDA ajattelee, että viimeinen käsky on normaali funktion palautus, mutta itse asiassa se menee laskettuun osoitteeseen 0xB13B.

Tässä on syytä muistaa, että ARM-prosessoreissa on kaksi tilaa ja kaksi ohjesarjaa: ARM ja peukalo. Osoitteen vähiten merkitsevä bitti kertoo prosessorille, mitä käskysarjaa käytetään. Toisin sanoen osoite on itse asiassa 0xB13A, ja yksi vähiten merkitsevästä bitistä osoittaa peukalotilan.

Samanlainen "sovitin" on lisätty jokaisen toiminnon alkuun tässä kirjastossa ja
roskakoodi. Emme käsittele niitä tarkemmin - muistamme vain
että lähes kaikkien toimintojen todellinen alku on hieman kauempana.

Koska koodi ei nimenomaisesti hyppää 0xB13A:han, IDA ei itse tunnistanut koodin olevan tässä paikassa. Samasta syystä se ei tunnista suurinta osaa kirjaston koodista koodiksi, mikä tekee analysoinnista jonkin verran vaikeaa. Kerromme IDA:lle, että tämä on koodi, ja näin tapahtuu:

Etsitään haavoittuvuuksia UC-selaimesta

Taulukko alkaa selvästi 0xB144:stä. Mitä sub_494C sisältää?

Etsitään haavoittuvuuksia UC-selaimesta

Kutsuttaessa tätä funktiota LR-rekisterissä saamme aiemmin mainitun taulukon osoitteen (0xB144). In R0 - indeksi tässä taulukossa. Eli arvo otetaan taulukosta, lisätään LR:ään ja tulos on
osoite, johon mennään. Yritetään laskea se: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Menemme vastaanotettuun osoitteeseen ja näemme kirjaimellisesti pari hyödyllistä ohjetta ja siirrymme jälleen kohtaan 0xB140:

Etsitään haavoittuvuuksia UC-selaimesta

Nyt tulee siirtymä offsetin indeksillä 0x20 taulukosta.

Taulukon koosta päätellen koodissa on monia tällaisia ​​siirtymiä. Herää kysymys, onko mahdollista käsitellä tätä jotenkin automaattisemmin ilman manuaalista osoitteiden laskemista. Ja skriptit ja kyky korjata koodia IDA:ssa tulevat avuksi:

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"

Aseta kohdistin riville 0xB26A, suorita komentosarja ja katso siirtymistä 0xB4B0:een:

Etsitään haavoittuvuuksia UC-selaimesta

IDA ei taaskaan tunnistanut tätä aluetta koodiksi. Autamme häntä ja näemme siellä toisen mallin:

Etsitään haavoittuvuuksia UC-selaimesta

BLX:n jälkeisissä ohjeissa ei tunnu olevan juurikaan järkeä, se on enemmän kuin jonkinlainen syrjäytyminen. Katsotaanpa sub_4964:

Etsitään haavoittuvuuksia UC-selaimesta

Ja todellakin, tässä LR:ssä olevasta osoitteesta otetaan dword, joka lisätään tähän osoitteeseen, minkä jälkeen tuloksena olevan osoitteen arvo otetaan ja laitetaan pinoon. Lisäksi LR:ään lisätään 4, jotta funktiosta palattuaan tämä sama siirtymä ohitetaan. Tämän jälkeen POP {R1} -komento ottaa tuloksena olevan arvon pinosta. Jos katsot mitä sijaitsee osoitteessa 0xB4BA + 0xEA = 0xB5A4, näet jotain osoitetaulukon kaltaista:

Etsitään haavoittuvuuksia UC-selaimesta

Tämän mallin korjaamiseksi sinun on saatava koodista kaksi parametria: offset ja rekisterinumero, johon haluat sijoittaa tuloksen. Jokaista mahdollista rekisteriä varten sinun on valmisteltava koodinpätkä etukäteen.

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"

Asetamme kohdistimen korvattavan rakenteen alkuun - 0xB4B2 - ja suoritamme komentosarjan:

Etsitään haavoittuvuuksia UC-selaimesta

Jo mainittujen rakenteiden lisäksi koodi sisältää myös seuraavaa:

Etsitään haavoittuvuuksia UC-selaimesta

Kuten edellisessä tapauksessa, BLX-käskyn jälkeen on siirtymä:

Etsitään haavoittuvuuksia UC-selaimesta

Otamme offsetin osoitteeseen LR:stä, lisäämme sen LR:ään ja menemme sinne. 0x72044 + 0xC = 0x72050. Tämän suunnittelun käsikirjoitus on melko yksinkertainen:

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"

Skriptin suorituksen tulos:

Etsitään haavoittuvuuksia UC-selaimesta

Kun kaikki on korjattu funktiossa, voit osoittaa IDA:n sen todelliseen alkuun. Se kokoaa yhteen kaikki toimintokoodit, ja se voidaan purkaa HexRaysilla.

merkkijonojen dekoodaus

Olemme oppineet käsittelemään konekoodin hämärtämistä kirjastossa libsgmainso-6.4.36.so UC Browserista ja vastaanotti toimintokoodin 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;
}

Tarkastellaanpa tarkemmin seuraavia rivejä:

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

Toiminnassa sub_73E24 luokan nimen salaus puretaan selvästi. Tämän toiminnon parametreina välitetään osoitin salatun datan kaltaisiin tietoihin, tietty puskuri ja numero. Ilmeisesti funktion kutsumisen jälkeen puskurissa on purettu rivi, koska se välitetään funktiolle FindClass, joka ottaa luokan nimen toiseksi parametriksi. Siksi numero on puskurin koko tai rivin pituus. Yritetään tulkita luokan nimi, sen pitäisi kertoa, olemmeko menossa oikeaan suuntaan. Katsotaanpa tarkemmin, mitä siinä tapahtuu 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;
}

Toiminto sub_7AF78 luo säilön esiintymän tietyn kokoisille tavuryhmille (emme käsittele näitä säilöjä yksityiskohtaisesti). Tässä luodaan kaksi tällaista säilöä: yksi sisältää rivin "DcO/lcK+h?m3c*q@" (on helppo arvata, että tämä on avain), toinen sisältää salattua tietoa. Seuraavaksi molemmat objektit sijoitetaan tiettyyn rakenteeseen, joka välitetään funktiolle sub_6115C. Merkitään tähän rakenteeseen myös kenttä, jonka arvo on 3. Katsotaan mitä tälle rakenteelle tapahtuu seuraavaksi.

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

Kytkinparametri on rakennekenttä, jolle on aiemmin määritetty arvo 3. Katso tapaus 3: funktiolle sub_6364C parametrit välitetään rakenteesta, joka on lisätty sinne edellisessä funktiossa, eli avaimesta ja salatusta tiedosta. Jos katsot tarkkaan sub_6364C, voit tunnistaa siitä RC4-algoritmin.

Meillä on algoritmi ja avain. Yritetään tulkita luokan nimi. Tässä on mitä tapahtui: com/taobao/wireless/security/adapter/JNICLibrary. Loistava! Olemme oikeilla jäljillä.

Komentopuu

Nyt on löydettävä haaste Rekisteröidy Alkuperäiset, joka osoittaa meidät funktioon doCommandNative. Katsotaanpa funktioita, joista on kutsuttu JNI_OnLoad, ja löydämme sen 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 todellakin, natiivi menetelmä, jolla on nimi, on rekisteröity täällä doCommandNative. Nyt tiedämme hänen osoitteensa. Katsotaan mitä hän tekee.

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

Nimen perusteella voit arvata, että tässä on kaikkien niiden toimintojen sisääntulopiste, jotka kehittäjät päättivät siirtää alkuperäiseen kirjastoon. Olemme kiinnostuneita toiminnosta numero 10601.

Näet koodista, että komentonumero tuottaa kolme numeroa: komento/10000, komento % 10000/100 и komento % 10, eli meidän tapauksessamme 1, 6 ja 1. Nämä kolme numeroa sekä osoitin JNIEnv ja funktiolle välitetyt argumentit lisätään rakenteeseen ja välitetään eteenpäin. Käyttäen kolmea saatua numeroa (merkitkäämme niitä N1, N2 ja N3) rakennetaan komentopuu.

Jotain tällaista:

Etsitään haavoittuvuuksia UC-selaimesta

Puu täyttyy dynaamisesti JNI_OnLoad.
Kolme numeroa koodaa polun puussa. Jokainen puun lehti sisältää vastaavan funktion pocked-osoitteen. Avain on pääsolmussa. Sen paikan löytäminen koodista, johon tarvitsemamme funktio lisätään puuhun, ei ole vaikeaa, jos ymmärrät kaikki käytetyt rakenteet (emme kuvaile niitä, jotta ei turvottaisi jo ennestään melko suurta artikkelia).

Lisää hämmennystä

Saimme toiminnon osoitteen, jonka pitäisi purkaa liikenteen salaus: 0x5F1AC. Mutta on liian aikaista iloita: UC Browserin kehittäjät ovat valmistaneet meille toisen yllätyksen.

Saatuamme parametrit Java-koodissa muodostetusta taulukosta, saamme
funktioon osoitteessa 0x4D070. Ja tässä meitä odottaa toisenlainen koodin hämärtäminen.

Laitamme kaksi indeksiä R7:ään ja R4:ään:

Etsitään haavoittuvuuksia UC-selaimesta

Siirrämme ensimmäisen indeksin R11:een:

Etsitään haavoittuvuuksia UC-selaimesta

Jos haluat saada osoitteen taulukosta, käytä indeksiä:

Etsitään haavoittuvuuksia UC-selaimesta

Ensimmäiseen osoitteeseen siirtymisen jälkeen käytetään toista indeksiä, joka on R4:ssä. Taulukossa on 230 elementtiä.

Mitä tehdä asialle? Voit kertoa IDA:lle, että tämä on kytkin: Muokkaa -> Muu -> Määritä kytkimen idiomi.

Etsitään haavoittuvuuksia UC-selaimesta

Tuloksena oleva koodi on pelottava. Mutta kun kuljet sen viidakon läpi, voit huomata kutsun meille jo tutulle toiminnolle sub_6115C:

Etsitään haavoittuvuuksia UC-selaimesta

Siellä oli kytkin, jossa tapauksessa 3 tapahtui salauksen purku RC4-algoritmilla. Ja tässä tapauksessa funktiolle välitetty rakenne täytetään parametreista, jotka on välitetty doCommandNative. Muistetaan, mitä meillä siellä oli magicInt arvolla 16. Tarkastellaan vastaavaa tapausta - ja useiden siirtymien jälkeen löydämme koodin, jolla algoritmi voidaan tunnistaa.

Etsitään haavoittuvuuksia UC-selaimesta

Tämä on AES!

Algoritmi on olemassa, jäljellä on vain saada sen parametrit: tila, avain ja mahdollisesti alustusvektori (sen läsnäolo riippuu AES-algoritmin toimintatilasta). Niiden kanssa oleva rakenne on muodostettava jonnekin ennen funktiokutsua sub_6115C, mutta tämä koodin osa on erityisen hyvin hämärtynyt, joten herää ajatus korjata koodi niin, että kaikki salauksenpurkutoiminnon parametrit upotetaan tiedostoon.

Patch

Jotta et kirjoita kaikkea korjauskoodia kokoonpanokielellä manuaalisesti, voit käynnistää Android Studion, kirjoittaa sinne toiminnon, joka vastaanottaa samat syöttöparametrit kuin salauksenpurkutoimintomme ja kirjoittaa tiedostoon, ja sitten kopioi ja liitä koodin, jonka kääntäjä Tuottaa.

Ystävämme UC Browser -tiimistä huolehtivat myös koodin lisäämisen mukavuudesta. Muistakaamme, että jokaisen funktion alussa meillä on roskakoodi, joka voidaan helposti korvata millä tahansa muulla. Todella kätevää 🙂 Kohdefunktion alussa ei kuitenkaan ole tarpeeksi tilaa koodille, joka tallentaa kaikki parametrit tiedostoon. Minun piti jakaa se osiin ja käyttää viereisten toimintojen roskalohkoja. Osia oli yhteensä neljä.

Ensimmäinen osa:

Etsitään haavoittuvuuksia UC-selaimesta

ARM-arkkitehtuurissa neljä ensimmäistä toimintoparametria välitetään rekistereiden R0-R3 kautta, loput, jos sellaisia ​​on, pinon kautta. LR-rekisteri kuljettaa palautusosoitteen. Kaikki tämä on tallennettava, jotta toiminto voi toimia sen parametrien tyhjennyksen jälkeen. Meidän on myös tallennettava kaikki prosessissa käytettävät rekisterit, joten teemme PUSH.W {R0-R10,LR}. R7:ssä saamme funktiolle pinon kautta välitetyn parametriluettelon osoitteen.

Toiminnon käyttäminen avata avataan tiedosto /data/local/tmp/aes "ab"-tilassa
eli lisäystä varten. R0:ssa lataamme tiedostonimen osoitteen, R1:ssä - tilaa osoittavan rivin osoitteen. Ja tähän roskakoodi päättyy, joten siirrymme seuraavaan toimintoon. Jotta se toimisi edelleen, laitamme alussa siirtymisen funktion todelliseen koodiin ohittaen roskat ja lisäämme roskat sijasta korjaustiedoston jatkon.

Etsitään haavoittuvuuksia UC-selaimesta

Kutsumus avata.

Toiminnon kolme ensimmäistä parametria aes on tyyppi int. Koska tallensimme rekisterit pinoon alussa, voimme yksinkertaisesti välittää funktion fwrite heidän osoitteensa pinossa.

Etsitään haavoittuvuuksia UC-selaimesta

Seuraavaksi meillä on kolme rakennetta, jotka sisältävät datan koon ja osoittimen avaimelle, alustusvektorille ja salatuille tiedoille.

Etsitään haavoittuvuuksia UC-selaimesta

Lopuksi sulje tiedosto, palauta rekisterit ja siirrä ohjaus todelliseen toimintoon aes.

Keräämme APK:n, jossa on korjattu kirjasto, allekirjoitamme sen, lataamme sen laitteeseen/emulaattoriin ja käynnistämme sen. Näemme, että kaatopaikkaamme luodaan ja sinne kirjoitetaan paljon dataa. Selain käyttää salausta paitsi liikenteessä, ja kaikki salaus kulkee kyseisen toiminnon kautta. Mutta jostain syystä tarvittavat tiedot eivät ole siellä, eikä vaadittu pyyntö näy liikenteessä. Jotta ei jäädä odottamaan, kunnes UC Browser tekee tarvittavan pyynnön, otetaan palvelimelta aiemmin saatu salattu vastaus ja korjataan sovellus uudelleen: lisää salauksen purku päätoiminnon onCreateen.

    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

Kokoamme, allekirjoitamme, asennamme, käynnistämme. Saamme NullPointerExceptionin, koska menetelmä palautti null.

Koodin lisäanalyysin aikana löydettiin funktio, joka purkaa mielenkiintoisia rivejä: "META-INF/" ja ".RSA". Näyttää siltä, ​​​​että sovellus vahvistaa sertifikaattiaan. Tai jopa luo avaimia siitä. En todellakaan halua käsitellä sitä, mitä varmenteen kanssa tapahtuu, joten laitamme sen vain oikean todistuksen. Korjataan salattua riviä niin, että "META-INF/":n sijaan saamme "BLABLINF/", luodaan APK:hen samanniminen kansio ja lisätään siihen oravaselaimen varmenne.

Kokoamme, allekirjoitamme, asennamme, käynnistämme. Bingo! Meillä on avain!

MITM

Saimme avaimen ja avainta vastaavan alustusvektorin. Yritetään purkaa palvelimen vastauksen salaus CBC-tilassa.

Etsitään haavoittuvuuksia UC-selaimesta

Näemme arkiston URL-osoitteen, joka on samanlainen kuin MD5, "extract_unzipsize" ja numero. Tarkistamme: arkiston MD5 on sama, pakkaamattoman kirjaston koko on sama. Yritämme korjata tämän kirjaston ja antaa sen selaimelle. Osoittaaksemme, että korjattu kirjastomme on latautunut, käynnistämme aikomuksen luoda tekstiviesti, jossa on teksti "PWNED!" Korvaamme kaksi vastausta palvelimelta: puds.ucweb.com/upgrade/index.xhtml ja ladata arkiston. Ensimmäisessä korvaamme MD5:n (koko ei muutu pakkauksen purkamisen jälkeen), toisessa annamme arkiston korjatulla kirjastolla.

Selain yrittää ladata arkiston useita kertoja, minkä jälkeen se antaa virheilmoituksen. Ilmeisesti jotain
hän ei pidä. Tämän hämärän muodon analysoinnin tuloksena kävi ilmi, että palvelin välittää myös arkiston koon:

Etsitään haavoittuvuuksia UC-selaimesta

Se on koodattu LEB128:aan. Korjauksen jälkeen arkiston koko kirjaston kanssa muuttui hieman, joten selain katsoi, että arkisto oli ladattu väärin, ja usean yrityksen jälkeen se heitti virheen.

Säädämme arkiston kokoa... Ja – voitto! 🙂 Tulos näkyy videossa.

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

Seuraukset ja kehittäjän reaktio

Samalla tavalla hakkerit voisivat käyttää UC Browserin suojaamatonta ominaisuutta haitallisten kirjastojen levittämiseen ja käyttämiseen. Nämä kirjastot toimivat selaimen yhteydessä, joten ne saavat kaikki sen järjestelmäoikeudet. Tuloksena on mahdollisuus näyttää phishing-ikkunoita sekä pääsy oranssin kiinalaisen oravan työtiedostoihin, mukaan lukien tietokantaan tallennetut kirjautumistunnukset, salasanat ja evästeet.

Otimme yhteyttä UC Browserin kehittäjiin ja kerroimme heille löytämämme ongelman, yritimme tuoda esiin haavoittuvuuden ja sen vaaran, mutta he eivät puhuneet kanssamme mistään. Sillä välin selain jatkoi vaarallisen ominaisuutensa esittämistä näkyvästi. Mutta kun paljastimme haavoittuvuuden yksityiskohdat, sitä ei enää voitu sivuuttaa kuten ennen. 27. maaliskuuta oli
julkaistiin uusi versio UC Browserista 12.10.9.1193, joka pääsi palvelimeen HTTPS:n kautta: puds.ucweb.com/upgrade/index.xhtml.

Lisäksi "korjauksen" jälkeen ja tämän artikkelin kirjoittamiseen asti PDF-tiedoston avaaminen selaimessa johti virheilmoitukseen, jossa oli teksti "Hups, jotain meni pieleen!" Pyyntöä palvelimelle ei tehty PDF-tiedoston avaamisen yhteydessä, mutta pyyntö tehtiin selaimen käynnistyksen yhteydessä, mikä vihjaa jatkuvaan mahdollisuuteen ladata suoritettavaa koodia Google Playn sääntöjen vastaisesti.

Lähde: will.com

Lisää kommentti