
Sissejuhatus
Märtsi lõpus teatasime, Mõni aeg tagasi reklaamiti ja levitati UC Browserit väga agressiivselt: seda installeeriti kasutajate seadmetesse pahavara abil, levitati erinevatelt saitidelt videofailide näol (st kasutajad arvasid, et laadivad näiteks pornofilmi, aga said selle asemel APK selle brauseriga), kasutati hirmutavaid bannereid teadetega, et brauser on aegunud, haavatav ja nii edasi. UC Browseri ametlikus VK grupis on
, kus kasutajad saavad kahtlase reklaami kohta kaebusi esitada, seal on palju näiteid. 2016. aastal oli isegi videoreklaam Artikli kirjutamise hetkeks oli UC Browseri Google Play's rohkem kui 500 000 000 allalaadimist. See on muljetavaldav — ainult Google Chrome'il on rohkem. Arvustuste seas on piisavalt palju kaebusi reklaamide ja suunamiste kohta mingitesse rakendustesse Google Play's. Just see andis meile inspiratsiooni uurimiseks: otsustasime vaadata, kas UC Browser teeb midagi kahtlast. Ja selgus, et teebki!
Rakenduse koodis avastati võimalus laadida ja käivitada käivaid koode,
mis on vastuolus Google Play rakenduste avaldamise reeglitega. Lisaks sellele, et UC Browser laadib käituvat koodi, teeb ta seda ebaturvaliselt, mida saab kasutada Man-in-the-Middle (MitM) rünnaku läbiviimiseks. Vaatame, kas suudame sellist rünnakut teostada. package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-faili: f5edb2243413c777172f6362876041eb0c3a928c
Rünnakute vektor
UC Browseri manifestis võib leida teenuse, mille nimetus oncom.uc.deployment.UpgradeDeployService
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" /> Selle teenuse käivitamise ajal teeb brauser POST-päringu aadressile.
puds.ucweb.com/upgrade/index.xhtml
Käivitades seda teenust, saadab brauser POST-päringu aadressile , mida saab liikluses märgata mõne aja pärast pärast käivitamist. Vastuseks võib ta saada käsu laadida alla mõni uuendus või uus moodul. Server ei andnud selliseid käske analüüsiprotsessi käigus, kuid tähelepanekud näitasid, et PDF-i avamisel brauseris tegi ta korduva päringu ülaltoodud aadressile, pärast mida laadis alla originaalkirjastuse teegi. Rünnaku läbiviimiseks otsustasime kasutada UC Brauseri spetsiifikat: võimet avada PDF-i läbi originaalkirjastuse, mida APK-s ei ole ja mille ta vajadusel laadib Internetist alla. Tuleb märkida, et teoreetiliselt on võimalik UC Brauserit sundida midagi alla laadima ka ilma kasutaja kaasamiseta – kui esitada õigesti vormindatud vastus päringule, mis toimub pärast brauseri käivitamist. Kuid selleks tuleb detailsemalt uurida serveriga suhtlemise protokolli. Seetõttu otsustasime, et on lihtsam redigeerida püütud vastust ja asendada PDF-i avamiseks mõeldud teek.
Nii et kui kasutaja tahab PDF-i brauseris otse avada, siis liikluses võib näha järgmisi päringuid:

Esmalt tuleb POST-päring aadressile , pärast seda
laaditakse alla teek PDF-ide ja kontoriformaatide vaatamiseks. On loogiline eeldada, et esimeses päringus edastatakse teave süsteemi kohta (vähemalt arhitektuur, et anda vajalik teek), ja sellele vastuseks saab brauser teavet teegi kohta, mida tuleb alla laadida: aadress ja võib-olla veel midagi. Probleem on selles, et see päring on krüptitud.
Päringu fragment
Vastuse fragment


Ise teek on pakitud ZIP-faili ja ei ole krüptitud.

Liikluse dekrüpteerimise koodi otsimine
Proovime dekrüpteerida serveri vastust. Vaadake klassi koodi Selle teenuse käivitamise ajal teeb brauser POST-päringu aadressile: meetodist onStartCommand liigume com.uc.deployment.b.x, sealt edasi com.uc.browser.core.d.c.f.e:
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("]toode:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]versioon:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]uuendustüüp:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]sundimine:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vaikerežiim:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vaikerežiimi tüüp:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vaikerežiimi olek:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]vaikerežiimi fail:");
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("]laadimistüüp:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]laadimiste grupp:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]laadimise tee:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_lapse_versioon:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_seeria:");
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("]komponendi_nimi:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponendi_versiooni_nimi:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponendi_versiooni_kood:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]komponendi_nõudlustüüp:");
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_versioon", c.getRomInfo()));
v3_2.add(g.fs("protsessori_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_tüüp", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_versioon", arg9.iGX.iEn));
v3_2.add(g.fs("sihtkeel", 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_lapse_versioon", arg9.iGX.iEx));
v3_2.add(g.fs("versioon_seeria", arg9.iGX.iEw));
v3_2.add(g.fs("lapse_versioon", r.aVw()));
v3_2.add(g.fs("praegune_versioon_md5", arg9.iGX.iEl));
v3_2.add(g.fs("praegune_versiooni_allkiri", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("uuendamise_logi", i.bjt()));
v3_2.add(g.fs("vaikime_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("vaikerežiimi_olek", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("vaikerežiimi_fail", arg9.iGX.iEq));
v3_2.add(g.fs("vaikerežiimi_tüüp", 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("kokku_ram", 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_tase", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_loend", 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, "uuenda_encode", "jah", "ebaõnnestus");
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, "uuenda_encrypt", "jah", "ebaõnnestus");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "uuenda_url", "jah", "ebaõnnestus");
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, "uuenda_tühi", "jah", "edukalt");
this.iGY.iGI.b(arg9);
}Siin näeme POST-päringu loomist. Pöörake tähelepanu 16-baidise massiivi loomisele ja selle täitmisele: 0x5F, 0, 0x1F, -50 (=0xCE). See sobib kokku sellega, mida nägime eelnevas päringus.
Samas klassis on näha ka sisemine klass, kus 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 võtab sisendiks byte massiivi ja kontrollib, et nulls byte oleks 0x60 või kolmas byte 0xD0 ning teine byte oleks 1, 11 või 0x1F. Vaatame serveri vastust: nulls byte — 0x60, teine — 0x1F, kolmas — 0x60. Tundub, et see on see, mida vajame. Arvestades rea („up_decrypt“, näiteks), peaks siin olema meetod, mis dekodeerib serveri vastuse.
Liigume meetodi juurde g.j. Tähelepanu, et esimese argumendina antakse edasi byte positsioonil 2 (st 0x1F meie puhul), ja teise argumendina — serveri vastus ilma
esimeste 16 baitideta.
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;
} Ilmselt toimub siin dekrüpteerimise algoritmi valik, ja just see byte, mis meie kontekstis on.
juhtum on 0x1F, mis tähistab ühte kolmest võimalikust variandist.
Jätkame koodi analüüsi. Pärast paarit hüpet jõuame meetodi juurde, millel on kõnekujuline nimi decryptBytesByKey.
Siin lahutatakse meie vastusest veel kaks bait, ja nende põhjal saadakse string. Selge on, et selle meetodiga valitakse võtme jaoks krüpteeritud sõnumi dekrüpteerimise võtme.
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 baiti
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Võtme valik
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;
}Eelnevalt märgime, et antud etapis ei saadud veel võtme, vaid ainult selle "identifikaator". Võtme saamine on veidi keerulisem.
Järgmises meetodis lisatakse olemasolevatele parameetritele veel kaks, muutes nende arvu neljaks: maagiline number 16, võtme identifikaator, krüpteeritud andmed ja arusaamatu string (meie juhul tühi).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}Pärast seeriat üleminekuid jõuame meetodi juurde staticBinarySafeDecryptNoB64 liidese com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Rakenduse põhikoodis pole klassid, mis rakendavad seda liidest. Selline klass on failis lib/armeabi-v7a/libsgmain.so, mis tegelikult ei ole .so, vaid .jar. Meid huvitav meetod on rakendatud järgmiselt:
pakett com.alibaba.wireless.security.a.i;
// ...
publiku klass a rakendab IStaticDataEncryptComponent {
privaatne ISecurityGuardPlugin a;
// ...
privaatne 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});
}
// ...
privaatne byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
publik byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) viskab SecException {
if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
viska new SecException("", 301);
}
//...
} Siin meie parameetrite loetelu täiendatakse veel kahe tervete arvuga: 2 ja 0. Oletades, et
2 tähendab dekrüpteerimist, nagu meetodis doFinal süsteemne klass javax.crypto.Cipher. Ja kõik see edastatakse mingisse Routerisse numbriga 10601 — see on ilmselt käskluse number.
Pärast järgmise ülemineku ahelat leiame klassi, mis rakendab liidest IRouterComponent ja meetod doCommand:
pakett com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
publiku klass a rakendab IRouterComponent {
publiku a() {
super();
}
publik Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}Ja ka klass JNICLibrary, kus on deklareeritud põhimõtteline meetod doCommandNative:
pakett com.taobao.wireless.security.adapter;
publiku klass JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}See tähendab, et peame leidma põhimõttelise meetodi doCommandNative. Ja siin algab tõeline lõbu.
Masinkoodi obfuskatsioon
Failis libsgmain.so (mis on tegelikult .jar ja milles me leidsime mõned varem rakendatud liidesed, mis on seotud krüpteerimisega) on üks põhimõtteline teek: libsgmainso-6.4.36.so. Avame selle IDA-s ja saame hulga dialoogikaste vigadega. Probleem seisneb selles, et sektsioonide tabel (section header table) – ei ole kehtiv. See on tehtud spetsiaalselt analüüsi raskendamiseks.

Aga see pole vajalik: et õigesti laadida ELF-faili ja analüüsida seda, piisab sektsioonide tabelist (program header table). Seetõttu lihtsalt kustutame sektsioonide tabeli, nullides vastavad väljad päisesse.

Avame faili uuesti IDA-s.
On kaks viisi, kuidas teavitada virtuaalset Java-masinat, kus täpselt põhimõttelise teegi rakendus asub, mis on Java-koodis deklareeritud kui native. Esimene — anda sellele nimi vormis Java_paketi_nime_klass_nimi_metood.
Teine — registreerida see, kui laaditakse teeki (funktsioonis JNI_OnLoad)
funktsiooni kutsumisega RegisterNatives.
Meie puhul, kui kasutada esimest meetodit, peab nimi olema järgmine: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Eksporditud funktsioonide seas sellist pole, seega tuleb otsida kutset RegisterNatives.
Läheme funktsiooni JNI_OnLoad ja näeme sellist pilti:

Mis siin toimub? Esmapilgul on funktsiooni algus ja lõpp tüüpilised ARM-arhitektuurile. Esimese käsu korral salvestatakse stacki register, mida funktsioon oma töös kasutab (antud juhul R0, R1 ja R2), samuti LR registreeritud väärtus, milles asub funktsioonist tagasipöördumise aadress. Viimane käsk taastab salvestatud registrid, tuues tagasipöördumise aadressi kohe registreeritud PC-sse — nii toimub funktsioonist tagasipöördumine. Kuid kui tähelepanelikult vaadata, võib märgata, et eelviimane käsk muudab stackis hoitavat tagasipöördumise aadressi. Arvutame, mis see aadress pärast
koodi täitmist olema saab. R1-sse laaditakse aadress 0xB130, millest lahutatakse 5, seejärel paigutatakse see R0-sse ja sellele lisatakse 0x10. Nähtavasti saame 0xB13B. Nii arvab IDA, et viimases käsus toimub tavaline tagasipöördumine funktsioonist, kuid tegelikult toimub üleminek arvutatud aadressile 0xB13B.
Siinkohal tasub meenutada, et ARM-protsessoritel on kaks režiimi ja kaks käsukogumit: ARM ja Thumb. Aadressi madalam bit ütleb protsessorile, millist käsukogumit kasutatakse. Ehk aadress on tegelikult 0xB13A, ja madalama biti üks näitab Thumb režiimi.
Iga funktsiooni alguses selles teegis on sarnane "adapter" ja
prügikood. Edasi ei peatume neile üksikasjalikult – lihtsalt meenutame,
et peaaegu kõikide funktsioonide tõeline algus on veidi edasi.
Kuna koodis pole selget üleminekut aadressile 0xB13A, ei tuvastanud IDA ise, et sellesse kohta asub kood. Samuti ei tuvastanud ta suures osas teekis olevat koodi, mis mõnevõrra raskendab analüüsi. Ütleme IDA-le, et siin on kood, ja siin on tulemuseks:

Aadressil 0xB144 algab selgelt tabel. Aga mis on sub_494C-s?

Selle funktsiooni kutsumisel LR registris saame mainitud tabeli aadressi (0xB144). R0-s on indeksi väärtus selles tabelis. Ehk tabelist võetakse väärtus, lisatakse LR-ile ja saadakse
aadress, kuhu minna. Proovime selle välja arvutada: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Liigume saadud aadressile ja näeme sõna otseses mõttes mõnda kasulikku juhendit ja jälle üleminekut 0xB140:

Nüüd toimub üleminek nihke järgi, mille indeks on 0x20 tabelis.
Tabeli suuruse põhjal esineb selliseid üleminekuid koodis palju. Tõuseb küsimus, kas seda kuidagi automaatsemalt lahendada, ilma aadresside käsitsi arvutamiseta. Ja siin tulevad meile appi skriptid ja võimalus koodi IDA-s patšida:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or 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 "indeks =", 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 "Vale operanditüüp", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Tabelit ei leitud"
else:
print "tabel =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Tundmatu kood", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Esimese käsu tuvastamine ebaõnnestus"Paneme kursori reale 0xB26A, käivitame skripti ja näeme üleminekut 0xB4B0:

IDA ei tuvastanud jälle seda osa koodina. Aitame tal mõista ja näeme seal muud konstruktsiooni:

BLX-i järel olevad käsud ei tundu kuigi mõistlikud, need rohkem meenutavad mingit nihket. Vaatame sub_4964:

Ja tõesti, siin võetakse dword aadressilt, mis asub LR-is, sellele aadressile lisatakse ning pärast seda võetakse saadud aadressilt väärtus ja pannakse see kuhugi. Samuti lisatakse LR-ile 4, et pärast funktsioonist tagasiminekut vahele jätta see nihke. Pärast seda võtab käsk POP {R1} saadud väärtuse kuhjast. Kui vaadata, mis asub aadressil 0xB4BA + 0xEA = 0xB5A4, siis võib näha midagi sarnast aadresside tabeliga:

Ette konstruktsiooni patcheerimiseks tuleb koodist hankida kaks parameetrit: nihke ja registri numbri, kuhu tulemus paigutada. Iga võimaliku registri jaoks tuleb eelnevalt ette valmistada koodilõik.
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 <> 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 käsku ei leitud"
else:
print "Vale operanditüüp +4-l:", get_operand_type(ea + 4, 0)
else:
print "Esimese käsku ei suutnud tuvastada"Seame kursori konstruktsiooni algusesse, mida soovime asendada — 0xB4B2 — ja käivitame skripti:

Lisaks juba mainitud konstruktsioonidele leidub koodis ka järgmisi:

Nagu eelmisel juhul, järgneb käsklusele BLX nihke:

Hankime nihke LR aadressilt, lisame selle LR-ile ja liigume sinna. 0x72044 + 0xC = 0x72050. Skript selle konstruktsiooni jaoks on üsna lihtne:
def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset 1023 or 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 "Esimese käsu tuvastamine ebaõnnestus"Skripti täitmise tulemus:

Pärast seda, kui funktsioon on kõik patšitakse, saab IDA näidata selle tõelist algust. See kogub kogu funktsiooni koodi tükkhaaval, ja seda saab dekompileerida HexRays'i abil.
Sõnumite dekodeerimine
Oleme õppinud võitlema masinkoodi obfuskaatorite vastu raamatukogus. libsgmainso-6.4.36.so UC Browserist ja saime funktsiooni koodi. 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;
}Vaadakem järgmisi ridu lähemalt:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);Funktsioonis sub_73E24 toimub ilmselt klassinime dekrüpteerimine. Selle funktsiooni parameetritena edastatakse viide andmetele, mis näivad olema krüpteeritud, mingi puhvers ja number. On selge, et funktsiooni kutsumise järel on puhvris dekrüpteeritud string, kuna see edastatakse funktsioonile FindClass, mis võtab teise parameetrina klassi nime. Seega on number puhvri suurus või stringi pikkus. Proovime klassi nime dekrüpteerida, see peaks meile näitama, 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 kaks säilituskonteinerit, mis on määratud antud suurusega (ära süvene nende konteinerite funktsiooni). Siin luuakse kaks sellist konteinerit: ühte paigutatakse string „DcO/lcK+h?m3c*q@“ (ei ole raske arvata, et see on võti), teise — krüpteeritud andmed. Seejärel lisatakse mõlemad objektid mingisse struktuuri, mis edastatakse funktsioonile sub_6115C. Samuti tasub märkida, et selle struktuuri väli on väärtusega 3. Vaatame, mis sellega edasi juhtub.
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 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;
}Switchi parameetrina edastatakse struktuuri väli, millele on eelnevalt omistatud väärtus 3. Vaadake case 3: funktsiooni sub_6364C edastatakse struktuurist parameetrid, mis olid sinna eelnevas funktsioonis salvestatud, s.t. võti ja krüpteeritud andmed. Kui vaadata tähelepanelikult sub_6364C, võib seal leida algoritmi RC4.
Meil on algoritm ja võti. Proovime dekrüpteerida klassi nime. Siin on, mis me saime: com/taobao/wireless/security/adapter/JNICLibrary. Suurepärane! Me oleme õigel teel.
Käskude puu
Nüüd tuleb leida kutsung RegisterNatives, mis viib meid funktsiooni doCommandNative. Läheme läbi funktsioonid, mida kutsutakse JNI_OnLoad, ja leiame selle 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 meetod; // [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;
meetod.name = name;
meetod.signature = signature;
meetod.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &meetod, 1) >> 31;
}Ja tõesti, siin registreeritakse natiivne meetod nimega doCommandNative. Nüüd teame selle aadressi. Vaadake, mida see 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õib arvata, et siin on kõigi funktsioonide sisenemiskoht, mille arendajad otsustasid viia natiivsetesse raamatukogudesse. Meid huvitab funktsioon numbriga 10601.
Koodist võib näha, et käskude numbrist saadakse kolm numbrit: command / 10000, command % 10000 / 100 ja command % 10, s.t., meie puhul 1, 6 ja 1. Need kolm numbrit, samuti viitamine JNIEnv ja funktsiooni edastatud argumendid, koondatakse struktuuri ja edastatakse edasi. Saadud kolme numbri (nimetame need N1, N2 ja N3) abil ehitatakse käskude puu.
Umbes selline:

Puu täidetakse dünaamiliselt JNI_OnLoad.
Kolm numbrit kodeerivad teed puus. Iga puu leht sisaldab vastava funktsiooni pangas aadressi. Võti on vanemas sõlmes. Leiame koha koodis, kus puusse lisatakse meie soovitud funktsioon, ei ole suurt vaeva, kui mõista kõiki kasutatavaid struktuure (nende kirjeldust ei tooda, et mitte paisutada juba niigi suurt artiklit).
Veel obfuskatsioonist
Oleme saanud funktsiooni aadressi, mis peaks dekrüpteerima liiklust: 0x5F1AC. Kuid rõõmustamine on veel vara: UC Browseri arendajad on meile valmistunud veel üheks üllatuseks.
Pärast parameetrite saamist massiivist, mis oli loodud Java-koodis, jõuame
funktsiooni aadressile 0x4D070. Ja siin ootab meid ees veel üks obfuskatsiooni vorm.
Paneme R7 ja R4 kaks indeksit:

Asetame esimese indeksi R11:se.

Aadressi saamiseks tabelist kasutame indeksit:

Pärast esimest aadressile üleminekut kasutatakse teist indeksit, mis on R4. Tabelis on 230 elementi.
Mida sellega peale hakata? Võime IDA-le öelda, et see on selline switch: Muuda -> Muu -> Täpsusta switch idiomit.

Saatud kood on hirmutav. Kuid selle läbi labürindi liikudes võib märkida, et kutsume juba tuttavat funktsiooni. sub_6115C:

Seal oli switch, kus case 3-s oli dekrüpteerimine, kasutades algoritmi RC4. Ja selles juhusis täidetakse funktsioonile edastatav struktuur parameetritest, mis edastati doCommandNative. Tuletame meelde, et meil oli seal magicInt väärtusega 16. Vaatame vastavat case'i – ja pärast mitmeid üleminekuid leiame koodi, mille kaudu saab algoritmi tuvastada.

See on AES!
Algoritm on olemas, jääb vaid saada selle parameetrid: režiim, võti ja võib-olla algväärtuse vektor (selle olemasolu sõltub AES-i algoritmi töörežiimist). Struktuur, mis neid sisaldab, peaks suutma vormida kuskil enne funktsiooni sub_6115C, kuid selle koodi osa on eriti hästi obfutseeritud, seega tekib idee koodi patšida, et kõik dekrüpteerimisfunktsiooni parameetrid salvestataks faili.
Patš
Kuna ei soovi kogu patši koodi käsitsi assembleri keeles kirjutada, saab avada Android Studio, kirjutada seal funktsiooni, mis võtab sisendiks samad parameetrid nagu meie dekrüpteerimisfunktsioon, ja kirjutab faili, seejärel kopeerida-kleebida kood, mille genereerib kompilaator.
Meie sõbrad UC Browseri meeskonnast on ka kodu koodi lisamise mugavuse üle hoolitsenud. Tuletame meelde, et iga funktsiooni alguses on meil 'prügikood', mille saab lihtsalt asendada mõne teisega. See on väga mugav 🙂 Tõsi, sihtfunktsiooni alguses on koodi jaoks, mis salvestab kõik parameetrid faili, ruumi liiga vähe. Pidin selle kolmeks osaks jagama ja kasutama kõrval asuvate funktsioonide prügiblokke. Kokku sai neli osa.
Esimene osa:

ARM arhitektuuris edastatakse funktsiooni esimesed neli parameetrit registerite R0-R3 kaudu ning ülejäänud, kui need on olemas, via virna. Register LR edastab tagasipöördumise aadressi. Kõik see tuleb salvestada, et funktsioon saaks töötada pärast seda, kui oleme selle parameetrid 'dumpinud'. Samuti tuleb salvestada kõik registerid, mida me protsessis kasutame, seega teeme PUSH.W {R0-R10,LR}. R7-s saame parameetrite nimekirja aadressi, mis edastati funktsioonile via virna.
Funktsiooni kaudu fopen avafaili /data/local/tmp/aes „ab“ režiimis,
st. lisamiseks. R0-s laadime faili nime aadressi, R1-s režiimi määratleva stringi aadressi. Ja siin lõpeb prügikood, seega liigume järgmisse funktsiooni. Selle jätkamiseks paneme algusesse ülemineku tegelikku funktsiooni koodi, mööda prügist, ning prügile lisame patši jätku.

Kutse fopen.
esimesele kolmele parameetrile funktsioonis aes on tüüp int. Kuna me alguses salvestasime registerid virna, võime lihtsalt edastada funktsioonile fwrite nende aadresse virnas.

Edasi on meil kolm struktuuri, mis sisaldavad andmete suurust ja andmeedastuse osutajat võtme, alguse vektori ja krüpteeritud andmete jaoks.

Lõpus sulgeme faili, taastame registerid ja edastame juhtimise tõelisele funktsioonile. aes.
Kogume APK koos patšitud teegiga, signeerime selle, laadime seadmesse/emulaatorisse, käivitame. Näeme, et meie dump luuakse ja sinna kirjutatakse palju andmeid. Brauser kasutab krüpteerimist mitte ainult liikluse jaoks, ja kogu krüpteerimine käib läbi arutatava funktsiooni. Kuid vajalikud andmed on mingil põhjusel puuduvad ja liikluses ei ole vajalikku päringut näha. Et mitte oodata, kuni UC Browser korraldab vajaliku päringu, võtame varasema serverilt saadud krüpteeritud vastuse ja patšime rakenduse uuesti: lisame dekrüpteerimise peamise tegevuse onCreate funktsiooni.
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;)IKogume, allkirjastame, installime, käivitame. Saame NullPointerException, kuna meetod tagastas null.
Koodi edasisel analüüsimisel leiti funktsioon, mis dekrüpteerib huvitavad read: «META-INF/» ja «.RSA». Tundub, et rakendus kontrollib oma sertifikaati. Või isegi genereerib võtmeid sellest. Tegeleda sertifikaadiga on tülikas, nii et lihtsalt anname sellele õige sertifikaadi. Patchime krüpteeritud rea nii, et «META-INF/» asendatakse «BLABLINF/», loome APK-s kausta sellise nimega ja paneme sinna be浏览器 sertifikaadi.
Kogume, allkirjastame, installime, käivitame. Bingo! Meil on võtmed!
MitM
Me saime võtme ja initsialiseerimisvektori, mis on võrdne võtmega. Proovime serveri vastust CBC režiimis dekrüpteerida.

Näeme arhivi URL-i, mis meenutab midagi MD5-d, «extract_unzipsize» ja numbrit. Kontrollime: arhiiv MD5 vastab, lahtipakitud teegi suurus vastab. Proovime seda teeki patchida ja anda selle brauserile. Näiteks, et näidata, et meie patchitud teek on laaditud, käivitame Intent SMSi loomiseks tekstiga «PWNED!». Asendame kaks serveri vastust: ja arhiivi allalaadimisega. Esimeses asendame MD5 (lahtipakitud suurus ei muutu), teises anname arhiivi patchitud teegiga.
Brauser proovib korduvalt arhiivi alla laadida, pärast mida annab vea. Tundub, et midagi
ei meeldi talle. Selle kahtlase formaadi analüüsimisel selgus, et server edastab ka arhiivi suuruse:

See on kodeeritud LEB128 formaadis. Pärast patchimist muutus arhiivi suurus teegiga veidi, seetõttu pidas brauser, et arhiiv laaditi valesti, ja pärast mitut katset andis vea.
Parandame arhiivi suurust... Ja – võit! 🙂 Tulemused videos.
Tagajärjed ja arendaja reaktsioon
Sarnasena kas hakkere oleks võinud kasutada UC Browser'i turvaaukot, et levitada ja käivitada pahavara teeke. Need teekid töötavad brauseri kontekstis, mistõttu nad saavad kõik selle süsteemi õigused. Selle tulemusena on võimalus kuvada libahäkkimisaken ja pääseda ligi tööfailidele, sealhulgas oranzhi Hiina orava andmebaasis salvestatud sisselogimistele, paroolidele ja küpsistele.
Me võtsime ühendust UC Browser'i arendajatega ja teavitasi neid leitud probleemist, püüdes osutada haavatavusele ja selle ohtudele, kuid nad ei soovinud meiega millestki arutada. Vahepeal jätkas brauser ohtliku funktsiooniga uhkustamist. Kuid niipea, kui avaldasime haavatavuse detailid, ei olnud võimalik seda enam nagu varem ignoreerida. 27. märtsil
välja lastud uus versioon UC Browser 12.10.9.1193, mis kutsus serverit HTTPS kaudu: .
Lisaks sellele, pärast "parandamist" ja kuni artikli kirjutamise hetkeni põhjustas brauseris PDF avamise katse veaaken, mille tekst oli "Ups, midagi läks valesti!". Serveri päring PDF avamisest ei toimunud, kuid brauseri käivitamisel toimus päring, mis vihjab jätkuvale võimalusele laadida täitmisfaili, rikkuvalt Google Play reegleid.
Allikas: habr.com
