
Introducere
La sfârșitul lunii martie, noi , am descoperit o vulnerabilitate ascunsă prin care se poate încărca și executa cod neverificat în UC Browser. Astăzi vom analiza în detaliu cum se desfășoară această încărcare și cum pot hackerii să o utilizeze în scopurile lor.
Cu ceva timp în urmă, UC Browser a fost promovat și distribuit foarte agresiv: era instalat pe dispozitivele utilizatorilor prin intermediul programelor malițioase, distribuit de pe diferite site-uri sub formă de fișiere video (adică utilizatorii credeau că descarcă, de exemplu, un videoclip pentru adulți, dar primeau în schimb un APK cu acest browser), iar bannerele menționau în mod alarmant că browserul este învechit, vulnerabil și tot așa mai departe. În grupul oficial UC Browser de pe VK există , unde utilizatorii pot reclama reclame înșelătoare, acolo sunt multe exemple. În 2016, a existat chiar în limba rusă (da, publicitate pentru un browser care blochează reclamele).
La momentul redactării acestui articol, UC Browser a acumulat peste 500.000.000 de instalări pe Google Play. Este impresionant — doar Google Chrome are mai multe. În recenzii se pot vedea destule plângeri referitoare la reclame și redirecționări către anumite aplicații din Google Play. Acest lucru a fost motivul pentru care am decis să investigăm: am vrut să vedem dacă UC Browser face ceva în neregulă. Și s-a dovedit că chiar face!
În codul aplicației a fost descoperită o posibilitate de a încărca și executa cod executabil, pe Google Play. Pe lângă faptul că UC Browser încarcă cod executabil, o face într-un mod nesigur, ceea ce poate fi utilizat pentru a desfășura un atac MitM. Să vedem dacă reușim să realizăm un astfel de atac.
Tot ceea ce este scris mai departe este valabil pentru versiunea UC Browser care era disponibilă pe Google Play în momentul desfășurării cercetării:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-ului: f5edb2243413c777172f6362876041eb0c3a928cVectorul de atac
În manifestul UC Browser se poate găsi un serviciu cu un nume sugestiv com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" \/>
Când se lansează acest serviciu, browserul efectuează o solicitare POST către , pe care îl poți observa în trafic la scurt timp după ce a început. În răspuns, el poate primi comanda de a descărca o actualizare sau un nou modul. În timpul analizei, serverul nu a dat astfel de comenzi, dar am observat că, atunci când încercam să deschidem un PDF în browser, acesta efectuează o cerere repetată la adresa menționată anterior, după care descarcă biblioteca nativă. Pentru a efectua atacul, am decis să folosim această particularitate a UC Browser: capacitatea de a deschide PDF-uri folosind o bibliotecă nativă, care nu se află în APK și pe care o încarcă din Internet, dacă este necesar. Merită menționat că, teoretic, UC Browser poate fi determinat să descarce ceva și fără interacțiunea utilizatorului – dacă se returnează un răspuns corect formatat la cererea care se execută după lansarea browserului. Dar pentru aceasta trebuie studiat mai detaliat protocolul de interacțiune cu serverul, așa că am decis că este mai simplu să edităm răspunsul interceptat și să înlocuim biblioteca pentru a lucra cu PDF-uri.
Așadar, când utilizatorul dorește să deschidă un PDF direct în browser, în trafic pot fi observate următoarele cereri:

Mai întâi, apare o cerere POST către , după care
se descarcă un arhivă cu biblioteca pentru vizualizarea PDF-urilor și a formatelor Office. Logica sugerează că în prima cerere este transmisă informația despre sistem (cel puțin, arhitectura, pentru a oferi biblioteca corectă), iar ca răspuns la aceasta, browserul primește anumite informații despre biblioteca care trebuie descărcată: adresa și, posibil, ceva în plus. Problema este că această cerere este criptată.
Fragmentul cererii
Fragmentul răspunsului


Însăși biblioteca este ambalată într-un ZIP și nu este criptată.

Căutarea codului pentru decriptarea traficului
Vom încerca să decriptăm răspunsul serverului. Ne uităm la codul clasei com.uc.deployment.UpgradeDeployService: din metoda onStartCommand ne îndreptăm către com.uc.deployment.b.x, iar din acesta către 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("]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);
}Aici observăm formarea unei cereri POST. Atragem atenția asupra creării unui tablou de 16 bytes și completarea acestuia: 0x5F, 0, 0x1F, -50 (=0xCE). Se potrivește cu ceea ce am văzut în cererea de mai sus.
În aceeași clasă, putem observa o clasă înglobată, care conține o altă metodă interesantă:
public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
} Metoda primește un tablou de bytes și verifică dacă byte-ul zero este egal cu 0x60 sau byte-ul trei este egal cu 0xD0, iar byte-ul doi este 1, 11 sau 0x1F. Observăm răspunsul de la server: byte-ul zero — 0x60, byte-ul doi — 0x1F, byte-ul trei — 0x60. Pare că este ceea ce avem nevoie. Judecând după liniile („up_decrypt”, de exemplu), aici ar trebui să fie apelată o metodă care va decripta răspunsul serverului.
Să trecem la metoda g.j. Observăm că, ca prim argument, i se transmite byte-ul de la offset 2 (adică 0x1F în cazul nostru), iar ca al doilea — răspunsul serverului fără
primele 16 bytes.
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;
} Se pare că aici se aleg algoritmii de decriptare, iar acel byte, care în cazul nostru
în cazul în care este 0x1F, indică una dintre cele trei opțiuni posibile.
Continuăm analiza codului. După câteva salturi, ajungem la metoda cu un nume sugestiv decryptBytesByKey.
Aici se separă încă doi biți de răspunsul nostru, iar din ei se obține un șir. Este clar că astfel se alege cheia pentru decriptarea mesajului.
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 biți
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Alegerea cheii
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;
}Pentru a anticipa, menționăm că în acest stadiu nu obținem cheia, ci doar "identificatorul" ei. Obținerea cheii este un pic mai complicată.
În următoarea metodă, la parametrii existenți se adaugă încă doi, ajungându-se la patru: numărul magic 16, identificatorul cheii, datele criptate și un șir neclar (în cazul nostru, gol).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}După o serie de tranziții, ajungem la metoda staticBinarySafeDecryptNoB64 interfață com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. În codul principal al aplicației nu există clase care să implementeze această interfață. O astfel de clasă se află în fișierul lib/armeabi-v7a/libsgmain.so, care de fapt nu este .so, ci .jar. Metoda care ne interesează este implementată astfel:
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 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
} Aici, lista noastră de parametri este completată cu încă două numere întregi: 2 și 0. Judecând după
tot, 2 înseamnă decriptare, așa cum este în metoda doFinal a clasei sistemului javax.crypto.Cipher. Și toate acestea sunt transmise unui Router cu numărul 10601 — acesta este, se pare, numărul comenzii.
După o nouă succesiune de tranziții, găsim o clasă care implementează interfața IRouterComponent și metoda doCommand:
package com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
public class a implements IRouterComponent {
public a() {
super();
}
public Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}Și de asemenea clasa JNICLibrary, în care este declarat metoda nativă doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}Înseamnă că trebuie să găsim metoda în codul nativ doCommandNative. Și aici începe distracția.
Obfuscația codului mașină
În fișierul libsgmain.so (care este, de fapt, un .jar și în care am găsit anterior implementări ale unor interfețe legate de criptare) există o bibliotecă nativă: libsgmainso-6.4.36.so. O deschidem în IDA și primim o mulțime de feronete de eroare. Problema este că tabela secțiunilor (section header table) este invalidă. Acest lucru a fost făcut intenționat pentru a complicat analiza.

Dar nu este necesară: pentru a încărca corect fișierul ELF și a-l analiza, este suficient tabela segmentelor (program header table). Prin urmare, pur și simplu ștergem tabela secțiunilor, nulificând câmpurile corespunzătoare din antet.

Deschidem din nou fișierul în IDA.
Există două modalități de a informa mașina Java virtuală despre unde se află implementarea metodei, declarate în codul Java ca nativ. Prima este să-i dăm un nume de tip Java_numele_pachetului_NumeleClasei_numeleMetodei.
Al doilea - să-l înregistrez la încărcarea bibliotecii (în funcția JNI_OnLoad)
prin apelul funcției RegisterNatives.
În cazul nostru, dacă folosim prima metodă, numele ar trebui să fie astfel: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.
Printre funcțiile exportate nu există așa ceva, așa că trebuie să căutăm apelul RegisterNatives.
Mergem în funcția JNI_OnLoad și vedem următoarea scenă:

Ce se întâmplă aici? La prima vedere, începutul și sfârșitul funcției sunt tipice pentru arhitectura ARM. Prima instrucțiune salvează în stivă conținutul registrelor pe care funcția le va folosi (în acest caz R0, R1 și R2), precum și conținutul registrului LR, în care se află adresa de întoarcere din funcție. Ultima instrucțiune restaurează registrele salvate, iar adresa de întoarcere este imediat plasată în registrul PC - astfel se întâmplă întoarcerea din funcție. Dar dacă ne uităm mai atent, putem observa că instrucțiunea penultimă modifică adresa de întoarcere, salvată în stivă. Să calculăm care va fi aceasta după
executarea codului. În R1 se încarcă o anumită adresă 0xB130, din care se scade 5, apoi este reîncărcată în R0 și i se adaugă 0x10. Rezultatul este 0xB13B. Așadar, IDA crede că în ultima instrucțiune are loc o întoarcere obișnuită din funcție, când de fapt se face o tranziție la adresa calculată 0xB13B.
Aici trebuie să amintim că procesoarele ARM au două moduri și două seturi de instrucțiuni: ARM și Thumb. Bitul de ordin inferior al adresei spune procesorului care set de instrucțiuni este utilizat. Adică, adresa este de fapt 0xB13A, iar unitatea din bitul de ordin inferior semnifică modul Thumb.
La începutul fiecărei funcții din această bibliotecă a fost adăugat un asemenea "adaptor" și
cod de gunoi. Nu vom insista asupra lor în detaliu - să ne amintim doar că
adevăratul început al aproape tuturor funcțiilor se află puțin mai departe.
Deoarece în cod nu există o tranziție explicită la 0xB13A, IDA nu a recunoscut de la sine că în acest loc se află cod. Din aceeași cauză, o mare parte din codul din bibliotecă nu este recunoscut ca fiind cod, ceea ce îngreunează analiza. Spunem IDA că aici este cod, și iată ce obținem:

La 0xB144 începe evident o tabelă. Și ce este în sub_494C?

La apelul acestei funcții, în registrul LR vom obține adresa tabelei menționate anterior (0xB144). În R0 - indexul în această tabelă. Adică se ia valoarea din tabelă, se adaugă la LR și se obține
adresa la care trebuie să mergem. Să încercăm să o calculăm: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Accesăm adresa obținută și vedem literal câteva instrucțiuni utile și din nou un salt la 0xB140:

Acum va exista un salt pe baza offset-ului cu indexul 0x20 din tabel.
Judecând după dimensiunea tabelului, asemenea salturi vor apărea frecvent în cod. Se pune întrebarea, putem oare lupta cu asta mai automatizat, fără a calcula manual adresele. Și ne vin în ajutor scripturile și posibilitatea de a modifica codul în 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 "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 "Tip de operand greșit pe", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Imposibil de găsit tabelul"
else:
print "tabel =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Cod necunoscut", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Imposibil de detectat prima instrucțiune"Pune cursorul pe linia 0xB26A, rulează scriptul și vedem un salt la 0xB4B0:

IDA din nou nu a recunoscut această secțiune ca fiind cod. O ajutăm și vedem o altă construcție acolo:

Instrucțiunile după BLX nu par foarte semnificative, mai mult seamănă cu un fel de offset. Să ne uităm în sub_4964:

Și într-adevăr, aici se ia un dword de la adresa care se află în LR, se adaugă acelei adrese, după care se ia valoarea de la adresa obținută și se pune în stivă. De asemenea, la LR se adaugă 4, pentru a sări peste acest offset după întoarcerea din funcție. După aceea, comanda POP {R1} extrage valoarea obținută din stivă. Dacă ne uităm la ce se află la adresa 0xB4BA + 0xEA = 0xB5A4, putem vedea ceva asemănător cu un tabel de adrese:

Pentru a aplica acest patch, va trebui să obținem doi parametri din cod: deplasarea și numărul registrului în care trebuie să plasăm rezultatul. Pentru fiecare registru posibil, va fi necesar să pregătim dinainte o bucată de cod.
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 "Instrucțiunea POP nu a fost găsită"
else:
print "Tip de operand greșit pe +4:", get_operand_type(ea + 4, 0)
else:
print "Imposibil de detectat primele instrucțiuni"Plasăm cursorul la începutul construcției pe care dorim să o înlocuim — 0xB4B2 — și lansăm scriptul:

Pe lângă construcțiile deja menționate, în cod mai apar și acestea:

Ca și în cazul anterior, după instrucțiunea BLX urmează o deplasare:

Luăm deplasarea de la adresa din LR, o adunăm la LR și ne indreptăm acolo. 0x72044 + 0xC = 0x72050. Scriptul pentru această construcție este foarte simplu:
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 "Imposibil de detectat prima instrucțiune"Rezultatul execuției scriptului:

După ce funcția este complet patch-uită, putem indica IDA către începutul ei real. Aceasta va aduna codul funcției în bucăți, iar acesta poate fi decompilat folosind HexRays.
Dezvăluirea string-urilor
Am învățat să combatem obscurarea codului mașină în bibliotecă libsgmainso-6.4.36.so din UC Browser și am obținut codul funcției 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;
}Să analizăm mai atent următoarele linii:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);În funcție sub_73E24 se desfășoară clar decriptarea numelui clasei. Ca parametrii ai acestei funcții se transmite un pointer către date asemănătoare cu cele criptate, un anumit buffer și un număr. Este evident că după apelul funcției, în buffer va fi un șir de caractere decriptat, deoarece acesta este transmis funcției FindClass, care acceptă ca al doilea parametru numele clasei. Așadar, numărul reprezintă dimensiunea buffer-ului sau lungimea șirului. Să încercăm să decriptăm numele clasei, acesta ar trebui să ne indice dacă ne îndreptăm în direcția corectă. Să analizăm mai detaliat ce se întâmplă în sub_73E24.
int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4;
int v7;
int v8;
int v9;
size_t v10;
int v11;
struc_1 v13;
int v14;
int v15;
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;
}Funcția sub_7AF78 creează o instanță a unui container pentru matricele de byți de dimensiunea specificată (nu vom intra în detalii despre aceste recipiente). Aici se creează două astfel de recipiente: într-unul se introduce un șir „DcO/lcK+h?m3c*q@” (nu e greu de ghicit că acesta este cheia), în celălalt - datele criptate. Apoi ambele obiecte sunt plasate într-o anumită structură care este transmisă funcției sub_6115C. De asemenea, să remarcăm în această structură un câmp cu valoarea 3. Să vedem ce se întâmplă cu această structură mai departe.
int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3;
unsigned int v4;
int v5;
int v6;
int result;
int v8;
*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;
}Ca parametru switch este transmis un câmp al structurii, căruia anterior i s-a atribuit valoarea 3. Să ne uităm la cazul 3: în funcție sub_6364C sunt transmise parametrii din structură, care au fost adăugați acolo în funcția anterioară, adică cheia și datele criptate. Dacă ne uităm cu atenție la sub_6364C, putem afla în ea algoritmul RC4.
Avem algoritmul și cheia. Să încercăm să decriptăm numele clasei. Iată ce a ieșit: com/taobao/wireless/security/adapter/JNICLibrary. Excelent! Suntem pe drumul cel bun.
Arborele comenzilor
Acum trebuie să găsim apelul RegisterNatives, care ne va indica funcția doCommandNative. Să vedem funcțiile apelate din JNI_OnLoad, și o găsim în sub_B7B0:
int __fastcall sub_B7F6(JNIEnv *env, jclass clazz)
{
char signature[41]; // [sp+7h] [bp-55h]
char name[16]; // [sp+30h] [bp-2Ch]
JNINativeMethod method; // [sp+40h] [bp-1Ch]
int v8; // [sp+4Ch] [bp-10h]
v8 = *(_DWORD *)off_8AC00;
decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u); // doCommandNative
decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u); // (I[Ljava/lang/Object;)Ljava/lang/Object;
method.name = name;
method.signature = signature;
method.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31;
}Și într-adevăr, aici se înregistrează metoda nativă cu numele doCommandNative. Acum știm adresa sa. Să vedem ce face.
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;
}După numele său, se poate deduce că aici se află punctul de intrare pentru toate funcțiile pe care dezvoltatorii au decis să le mute în biblioteca nativă. Ne interesează funcția cu numărul 10601.
Din cod se poate observa că din numărul comenzii se obțin trei numere: command / 10000, command % 10000 / 100 și command % 10, adică, în cazul nostru, 1, 6 și 1. Aceste trei numere, precum și pointerul către JNIEnv și argumentele transmise funcției, sunt adunate într-o structură și sunt transmise mai departe. Folosind cele trei numere obținute (să le denumim N1, N2 și N3), se construiește arborele comenzilor.
Aproximativ așa:

Arborele se completează dinamic în JNI_OnLoad.
Trei numere codifică calea într-un arbore. Fiecare frunză a arborelui conține adresa funcției corespunzătoare, codificată. Cheia se află în nodul părinte. A găsi locul în cod unde se adaugă funcția de care avem nevoie nu este o sarcină dificilă, dacă înțelegem toate structurile utilizate (nu vom oferi descrierea acestora pentru a nu umfla un articol deja destul de lung).
Încă obfuscare
Am obținut adresa funcției care ar trebui să decripteze traficul: 0x5F1AC. Dar nu ne bucurăm prea devreme: dezvoltatorii UC Browser ne-au pregătit o altă surpriză.
După ce am obținut parametrii dintr-un array, care a fost format în codul Java, ajungem
în funcția de la adresa 0x4D070. Și aici ne așteaptă o altă formă de obfuscare a codului.
Punem în R7 și R4 două indici:

Transferăm primul indice în R11:

Pentru a obține adresa din tabelă, folosim indicele:

După ce se face saltul la prima adresă, se folosește al doilea indice, care se află în R4. În tabel sunt 230 de elemente.
Ce să facem cu asta? Putem spune IDA că este un switch: Edit -> Other -> Specify switch idiom.

Codul rezultat este înfricoșător. Dar, străbătând prin labirintul său, putem observa apelul unei funcții deja cunoscute. sub_6115C:

Acolo a fost un switch, în care în case 3 se afla decriptarea folosind algoritmul RC4. Iar în acest caz, structura transmisă funcției se completează din parametrii transmiși în doCommandNative. Ne amintim că aveam magicInt cu valoarea 16. Vizionăm case-ul corespunzător – și după câteva salturi găsim codul prin care putem identifica algoritmul.

Este AES!
Algoritmul există, trebuie să obținem parametrii săi: modul, cheia și, posibil, vectorul de inițializare (prezența acestuia depinde de modul de funcționare al algoritmului AES). Structura cu acestea ar trebui să fie formată undeva înainte de apelul funcției sub_6115C, dar această parte a codului este obfuscată foarte bine, așa că apare ideea de a patch-ui codul pentru a face ca toți parametrii funcției de decriptare să fie salvate într-un fișier.
Patch
Pentru a nu scrie tot codul patch-ului în limbaj de asamblare manual, putem lansa Android Studio, să scriem acolo o funcție care primește aceleași parametrii ca funcția noastră de decriptare și scrie într-un fișier, după care să copiem codul pe care îl va genera compilatorul.
Privind la comoditatea de a adăuga cod, prietenii noștri din echipa UC Browser s-au gândit și la asta. Ne amintim că la începutul fiecărei funcții avem un cod de tip junk, care poate fi ușor înlocuit cu altceva. Foarte convenabil 🙂 Totuși, la începutul funcției țintă nu avem prea mult loc pentru codul care salvează toate parametrii într-un fișier, așa că a trebuit să îl împărțim în părți și să folosim blocurile de tip junk din funcțiile vecine. În total, au rezultat patru părți.
Prima parte:

În arhitectura ARM, primele patru parametrii ai funcției sunt transmiși prin registrele R0-R3, restul, dacă există — prin stivă. În registrul LR se transmite adresa de întoarcere. Toate acestea trebuie salvate pentru ca funcția să poată să își continue execuția după ce am dump-uit parametrii săi. De asemenea, trebuie să salvăm toate registrele pe care le vom folosi în proces, așa că facem PUSH.W {R0-R10,LR}. În R7 obținem adresa listei de parametrii, transmiși funcției prin stivă.
Folosind funcția fopen vom deschide fișierul /data/local/tmp/aes în modul „ab”,
adică pentru adăugare. În R0 încărcăm adresa numelui fișierului, în R1 — adresa șirului cu indicarea modului. Și aici codul de tip junk se termină, așa că trecem la următoarea funcție. Ca aceasta să continue să funcționeze, plasăm la început un salt către codul real al funcției, ocolind junk-ul, iar în loc de junk adăugăm continuarea patch-ului.

Invocăm fopen.
primele trei parametrii ai funcției aes au tipul int. Deoarece am salvat registrele în stivă la început, putem transmite pur și simplu funcției fwrite adresele lor din stivă.

Apoi avem trei structuri, care conțin dimensiunea datelor și un pointer către date pentru cheie, vectorul de inițializare și datele criptate.

La final, închidem fișierul, restabilim registrele și predăm controlul funcției reale aes.
Adunăm APK-ul cu biblioteca patch-ată, îl semnăm, îl punem pe dispozitiv/emulator, îl lansăm. Vedem că dump-ul nostru se creează și acolo se scriu multe date. Browserul folosește criptarea nu doar pentru trafic, iar toată criptarea trece prin funcția analizată. Însă datele necesare lipsesc dintr-un motiv necunoscut, iar în trafic nu se vede cererea dorită. Pentru a nu aștepta ca UC Browser să facă cererea necesară, vom lua răspunsul criptat de la server, obținut anterior, și vom patch-ui aplicația din nou: vom adăuga decriptarea în onCreate a activității principale.
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;)IAdunăm, semnăm, instalăm, lansăm. Obținem NullPointerException, deoarece metoda a returnat null.
În urma unei analize suplimentare a codului, a fost descoperită o funcție care decriptează linii interesante: „META-INF/” și „.RSA”. Se pare că aplicația își verifică certificatul. Sau chiar generează chei din acesta. Nu vrem să ne ocupăm de ceea ce se întâmplă cu certificatul, așa că pur și simplu îi vom da certificatul corect. Vom patch-ui linia criptată astfel încât să obținem „BLABLINF/” în loc de „META-INF/”, vom crea un folder cu acest nume în APK și vom adăuga acolo certificatul browserului belc.
Adunăm, semnăm, instalăm, lansăm. Bingo! Avem cheia!
MitM
Am obținut cheia și vectorul de inițializare, egal cu cheia. Să încercăm să decriptăm răspunsul serverului în modul CBC.

Vedem URL-ul arhivei, ceva care pare a fi MD5, „extract_unzipsize” și un număr. Verificăm: MD5-ul arhivei corespunde, dimensiunea bibliotecii dezarhivate este corectă. Încercăm să patch-uim această bibliotecă și să o oferim browserului. Pentru a arăta că biblioteca noastră patch-uită s-a încărcat, vom lansa un Intent pentru a crea un SMS cu textul „PWNED!”. Vom modifica două răspunsuri de la server: și pentru descărcarea arhivei. În primul, modificăm MD5-ul (dimensiunea după dezarhivare nu se modifică), iar în al doilea, oferim arhiva cu biblioteca patch-uită.
Browserul încearcă de câteva ori să descarce arhiva, după care afișează o eroare. Se pare că ceva
nu-i place. În urma analizei acestui format ciudat, am aflat că serverul transmite de asemenea dimensiunea arhivei:

Este codificat în LEB128. După patch, dimensiunea arhivei cu biblioteca s-a schimbat ușor, așa că browserul a considerat că arhiva a fost descărcată într-un mod greșit și după câteva încercări a emis o eroare.
Corectăm dimensiunea arhivei… Și – victorie! 🙂 Rezultatul în video.
Consecințe și reacția dezvoltatorului
În același mod, hackerii ar putea folosi funcția nesigură UC Browser pentru a răspândi și a rula biblioteci malware. Aceste biblioteci vor funcționa în contextul browserului, astfel că vor obține toate permisiunile de sistem. Ca urmare, vor putea arăta feronii de phishing, precum și să obțină acces la fișierele de lucru ale veveriței chineze portocalii, inclusiv la datele de autentificare, parolele și cookie-urile stocate în baza de date.
Am contactat dezvoltatorii UC Browser și le-am raportat problema găsită, încercând să le semnalăm vulnerabilitatea și pericolul acesteia, dar nu au dorit să discute nimic cu noi. Între timp, browserul continua să se laude cu funcția periculoasă la vedere. Însă, odată ce am dezvăluit detalii despre vulnerabilitate, nu mai putea fi ignorată ca înainte. Pe 27 martie, a fost
lansată o nouă versiune UC Browser 12.10.9.1193, care se conecta la server prin HTTPS: .
În plus, după "remediere" și până la momentul scrierii articolului, încercarea de a deschide un PDF în browser ducea la apariția unui mesaj de eroare cu textul „Ups, ceva nu a mers bine!”. Cererea către server în încercarea de a deschide PDF-ul nu era executată, dar cererea la deschiderea browserului era, ceea ce sugerează că a rămas posibilitatea de a încărca cod executabil în încălcarea regulilor Google Play.
Sursa: habr.com
