Căutăm vulnerabilități în UC Browser

Căutăm vulnerabilități în UC Browser

Introducere

La sfârşitul lunii martie noi raportat, că au descoperit o capacitate ascunsă de a încărca și rula cod neverificat în UC Browser. Astăzi vom analiza în detaliu modul în care are loc această descărcare și cum o pot folosi hackerii în propriile scopuri.

Cu ceva timp în urmă, UC Browser a fost promovat și distribuit foarte agresiv: a fost instalat pe dispozitivele utilizatorilor folosind programe malware, distribuite de pe diverse site-uri sub masca fișierelor video (adică, utilizatorii credeau că descarcă, de exemplu, un videoclip porno, dar a primit în schimb un APK cu acest browser), a folosit bannere înfricoșătoare cu mesaje că browserul era învechit, vulnerabil și chestii de genul ăsta. În grupul oficial UC Browser pe VK există temă, în care utilizatorii se pot plânge de publicitate neloială, există multe exemple acolo. În 2016 a existat chiar publicitate video în rusă (da, publicitate pentru un browser de blocare a reclamelor).

La momentul scrierii, UC Browser are peste 500 de instalări pe Google Play. Acest lucru este impresionant - doar Google Chrome are mai mult. Printre recenzii puteți vedea destul de multe reclamații despre publicitate și redirecționări către unele aplicații de pe Google Play. Acesta a fost motivul cercetării noastre: am decis să vedem dacă UC Browser face ceva rău. Și s-a dovedit că o face!

În codul aplicației, a fost descoperită capacitatea de a descărca și rula cod executabil, ceea ce este contrar regulilor de publicare a aplicaţiilor pe Google Play. Pe lângă descărcarea codului executabil, UC Browser face acest lucru într-un mod nesigur, care poate fi folosit pentru a lansa un atac MitM. Să vedem dacă putem face un asemenea atac.

Tot ceea ce scrie mai jos este relevant pentru versiunea UC Browser care era disponibilă pe Google Play la momentul studiului:

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

Vector de atac

În manifestul UC Browser puteți găsi un serviciu cu un nume care se explică de la sine com.uc.deployment.UpgradeDeployService.

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

Când pornește acest serviciu, browserul face o solicitare POST către puds.ucweb.com/upgrade/index.xhtml, care poate fi văzut în trafic la ceva timp după pornire. Ca răspuns, el poate primi o comandă pentru a descărca o actualizare sau un modul nou. În timpul analizei, serverul nu a dat astfel de comenzi, dar am observat că atunci când încercăm să deschidem un PDF în browser face o a doua solicitare la adresa specificată mai sus, după care descarcă biblioteca nativă. Pentru a realiza atacul, am decis să folosim această caracteristică a UC Browser: capacitatea de a deschide PDF folosind o bibliotecă nativă, care nu se află în APK și pe care o descarcă de pe Internet dacă este necesar. Este de remarcat faptul că, teoretic, UC Browser poate fi forțat să descarce ceva fără interacțiunea utilizatorului - dacă oferiți un răspuns bine format la o solicitare care este executată după lansarea browserului. Dar pentru a face acest lucru, trebuie să studiem mai detaliat protocolul de interacțiune cu serverul, așa că am decis că ar fi mai ușor să edităm răspunsul interceptat și să înlocuim biblioteca pentru lucrul cu PDF.

Deci, atunci când un utilizator dorește să deschidă un PDF direct în browser, următoarele solicitări pot fi văzute în trafic:

Căutăm vulnerabilități în UC Browser

Mai întâi există o solicitare POST către puds.ucweb.com/upgrade/index.xhtmldupă care
Este descărcată o arhivă cu o bibliotecă pentru vizualizarea PDF și formate office. Este logic să presupunem că prima solicitare transmite informații despre sistem (cel puțin arhitectura pentru a furniza biblioteca necesară), iar ca răspuns la aceasta browserul primește câteva informații despre biblioteca care trebuie descărcată: adresa și, eventual , altceva. Problema este că această solicitare este criptată.

Fragment de solicitare

Fragment de răspuns

Căutăm vulnerabilități în UC Browser

Căutăm vulnerabilități în UC Browser

Biblioteca în sine este ambalată în ZIP și nu este criptată.

Căutăm vulnerabilități în UC Browser

Căutați codul de decriptare a traficului

Să încercăm să descifrăm răspunsul serverului. Să ne uităm la codul clasei com.uc.deployment.UpgradeDeployService: din metoda onStartCommand mergi la com.uc.deployment.bx, iar de la ea la 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);
}

Vedem aici formarea unei cereri POST. Acordăm atenție creării unui tablou de 16 octeți și umplerii acestuia: 0x5F, 0, 0x1F, -50 (=0xCE). Coincide cu ceea ce am văzut în cererea de mai sus.

În aceeași clasă puteți vedea o clasă imbricată care are 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 ia ca intrare o matrice de octeți și verifică dacă octetul zero este 0x60 sau al treilea octet este 0xD0, iar al doilea octet este 1, 11 sau 0x1F. Ne uităm la răspunsul de la server: octetul zero este 0x60, al doilea este 0x1F, al treilea este 0x60. Sună a ceea ce avem nevoie. Judecând după linii („up_decrypt”, de exemplu), aici ar trebui apelată o metodă care va decripta răspunsul serverului.
Să trecem la metodă gj. Rețineți că primul argument este octetul de la offset 2 (adică 0x1F în cazul nostru), iar al doilea este răspunsul serverului fără
primii 16 octeți.

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

Evident, aici selectăm un algoritm de decriptare și același octet care se află în nostru
caz egal cu 0x1F, denotă una dintre cele trei opțiuni posibile.

Continuăm să analizăm codul. După câteva sărituri ne aflăm într-o metodă cu un nume care se explică de la sine decryptBytesByKey.

Aici încă doi octeți sunt separați de răspunsul nostru și se obține un șir din aceștia. Este clar că în acest fel este selectată cheia de decriptare a 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 байта
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;
}

Privind în perspectivă, observăm că în această etapă nu obținem încă o cheie, ci doar „identificatorul” acesteia. Obținerea cheii este puțin mai complicată.

În următoarea metodă, la cei existenți se adaugă încă doi parametri, făcând patru dintre ei: numărul magic 16, identificatorul cheii, datele criptate și un șir de neînțeles (î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 metodă staticBinarySafeDecryptNoB64 interfață com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Nu există clase în codul principal al aplicației care implementează această interfață. Există o astfel de clasă în dosar lib/armeabi-v7a/libsgmain.so, care nu este de fapt un .so, ci un .jar. Metoda care ne interesează este implementată după cum urmează:

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

Aici lista noastră de parametri este completată cu încă două numere întregi: 2 și 0. Judecând după
totul, 2 înseamnă decriptare, ca în metodă doFinal clasa de sistem javax.crypto.Cipher. Și toate acestea sunt transferate la un anumit Router cu numărul 10601 - acesta este aparent numărul de comandă.

După următorul lanț de tranziții găsim o clasă care implementează interfața IRouterComponent si 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, clasă Biblioteca JNIC, î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);
}

Aceasta înseamnă că trebuie să găsim o metodă în codul nativ doCommandNative. Și de aici începe distracția.

Ofucarea codului mașinii

În dosar libsgmain.so (care este de fapt un .jar și în care am găsit implementarea unor interfețe legate de criptare chiar mai sus) există o bibliotecă nativă: libsgmainso-6.4.36.so. Îl deschidem în IDA și obținem o grămadă de casete de dialog cu erori. Problema este că tabelul antet de secțiune este nevalid. Acest lucru se face intenționat pentru a complica analiza.

Căutăm vulnerabilități în UC Browser

Dar nu este necesar: pentru a încărca corect un fișier ELF și a-l analiza, este suficient un tabel de antet de program. Prin urmare, pur și simplu ștergem tabelul de secțiuni, reducând la zero câmpurile corespunzătoare din antet.

Căutăm vulnerabilități în UC Browser

Deschideți din nou fișierul în IDA.

Există două moduri de a spune mașinii virtuale Java unde exact se află în biblioteca nativă implementarea unei metode declarate în codul Java ca nativă. Primul este să îi dai un nume de specie Java_package_name_ClassName_MethodName.

Al doilea este să îl înregistrați la încărcarea bibliotecii (în funcția JNI_OnLoad)
folosind un apel de funcție Înregistrați nativii.

În cazul nostru, dacă folosim prima metodă, numele ar trebui să fie astfel: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Nu există o astfel de funcție printre funcțiile exportate, ceea ce înseamnă că trebuie să căutați un apel Înregistrați nativii.
Să trecem la funcție JNI_OnLoad si vedem aceasta poza:

Căutăm vulnerabilități în UC Browser

Ce se petrece aici? La prima vedere, începutul și sfârșitul funcției sunt tipice pentru arhitectura ARM. Prima instrucțiune de pe stivă stochează conținutul registrelor pe care funcția le va folosi în funcționarea sa (în acest caz, R0, R1 și R2), precum și conținutul registrului LR, care conține adresa de retur din funcție. . Ultima instrucțiune restabilește registrele salvate, iar adresa de retur este imediat plasată în registrul PC - revenind astfel din funcție. Dar dacă te uiți cu atenție, vei observa că penultima instrucțiune schimbă adresa de retur stocată pe stivă. Să calculăm cum va fi după
executarea codului. O anumită adresă 1xB0 este încărcată în R130, 5 este scăzut din ea, apoi este transferată în R0 și i se adaugă 0x10. Se dovedește 0xB13B. Astfel, IDA crede că ultima instrucțiune este o funcție normală returnată, dar de fapt merge la adresa calculată 0xB13B.

Merită să ne amintim că procesoarele ARM au două moduri și două seturi de instrucțiuni: ARM și Thumb. Bitul cel mai puțin semnificativ al adresei îi spune procesorului ce set de instrucțiuni este utilizat. Adică, adresa este de fapt 0xB13A, iar unul din bitul cel mai puțin semnificativ indică modul Thumb.

Un „adaptor” similar a fost adăugat la începutul fiecărei funcții din această bibliotecă și
cod de gunoi. Nu ne vom opri asupra lor în detaliu - doar ne amintim
că începutul real al aproape tuturor funcțiilor este puțin mai departe.

Deoarece codul nu sare în mod explicit la 0xB13A, IDA în sine nu a recunoscut că codul era localizat în această locație. Din același motiv, nu recunoaște majoritatea codului din bibliotecă ca cod, ceea ce face analiza oarecum dificilă. Îi spunem IDA că acesta este codul și iată ce se întâmplă:

Căutăm vulnerabilități în UC Browser

Tabelul începe clar de la 0xB144. Ce este în sub_494C?

Căutăm vulnerabilități în UC Browser

Când apelăm această funcție în registrul LR, obținem adresa tabelului menționat anterior (0xB144). În R0 - indice din acest tabel. Adică, valoarea este luată din tabel, adăugată la LR și rezultatul este
adresa la care să mergi. Să încercăm să-l calculăm: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Mergem la adresa primită și vedem literalmente câteva instrucțiuni utile și mergem din nou la 0xB140:

Căutăm vulnerabilități în UC Browser

Acum va exista o tranziție la offset cu indexul 0x20 din tabel.

Judecând după dimensiunea tabelului, vor exista multe astfel de tranziții în cod. Se pune întrebarea dacă este posibil să se ocupe cumva de asta mai automat, fără a calcula manual adresele. Și scripturile și capacitatea de a corela codul în IDA ne vin în ajutor:

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"

Plasați cursorul pe linia 0xB26A, rulați scriptul și vedeți tranziția la 0xB4B0:

Căutăm vulnerabilități în UC Browser

IDA din nou nu a recunoscut această zonă ca un cod. O ajutăm și vedem un alt design acolo:

Căutăm vulnerabilități în UC Browser

Instrucțiunile de după BLX nu par să aibă prea mult sens, este mai degrabă un fel de deplasare. Să ne uităm la sub_4964:

Căutăm vulnerabilități în UC Browser

Și într-adevăr, aici se ia un dword la adresa aflată în LR, adăugat la această adresă, după care se ia valoarea la adresa rezultată și se pune pe stivă. De asemenea, 4 este adăugat la LR, astfel încât, după revenirea din funcție, același offset să fie sărit. După care comanda POP {R1} preia valoarea rezultată din stivă. Dacă vă uitați la ceea ce se află la adresa 0xB4BA + 0xEA = 0xB5A4, veți vedea ceva similar cu un tabel de adrese:

Căutăm vulnerabilități în UC Browser

Pentru a corela acest design, va trebui să obțineți doi parametri din cod: offset-ul și numărul de registru în care doriți să puneți rezultatul. Pentru fiecare registru posibil, va trebui să pregătiți în prealabil 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 << 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"

Plasăm cursorul la începutul structurii pe care dorim să o înlocuim - 0xB4B2 - și rulăm scriptul:

Căutăm vulnerabilități în UC Browser

Pe lângă structurile deja menționate, codul conține și următoarele:

Căutăm vulnerabilități în UC Browser

Ca și în cazul precedent, după instrucțiunea BLX există un offset:

Căutăm vulnerabilități în UC Browser

Luăm offset-ul la adresa de la LR, îl adăugăm la LR și mergem acolo. 0x72044 + 0xC = 0x72050. Scriptul pentru acest design este destul de simplu:

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"

Rezultatul execuției scriptului:

Căutăm vulnerabilități în UC Browser

Odată ce totul este corectat în funcție, puteți indica IDA către începutul său real. Acesta va reuni întregul cod al funcției și poate fi decompilat folosind HexRays.

Decodificarea șirurilor

Am învățat să ne ocupăm de ofuscarea codului mașină din bibliotecă libsgmainso-6.4.36.so de la UC Browser și a primit 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ă aruncăm o privire mai atentă la următoarele rânduri:

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

În funcțiune sub_73E24 numele clasei este clar decriptat. Ca parametri ai acestei funcții, sunt transmise un pointer către date similare cu datele criptate, un anumit buffer și un număr. Evident, după apelarea funcției, va exista o linie decriptată în buffer, deoarece este transmisă funcției FindClass, care ia numele clasei ca al doilea parametru. Prin urmare, numărul este dimensiunea tamponului sau lungimea liniei. Să încercăm să descifrăm numele clasei, ar trebui să ne spună dacă mergem în direcția bună. Să aruncăm o privire mai atentă la ceea ce se întâmplă în 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;
}

Funcție sub_7AF78 creează o instanță a unui container pentru matrice de octeți de dimensiunea specificată (nu ne vom opri asupra acestor containere în detaliu). Aici sunt create două astfel de containere: unul conține linia „DcO/lcK+h?m3c*q@” (este ușor de ghicit că aceasta este o cheie), cealaltă conține date criptate. În continuare, ambele obiecte sunt plasate într-o anumită structură, care este transmisă funcției sub_6115C. Să marchem și un câmp în această structură cu valoarea 3. Să vedem mai departe ce se întâmplă cu această structură.

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

Parametrul switch este un câmp de structură căruia i s-a atribuit anterior valoarea 3. Uită-te la cazul 3: funcției sub_6364C parametrii sunt transferați din structura care au fost adăugate acolo în funcția anterioară, adică cheia și datele criptate. Dacă te uiți cu atenție sub_6364C, puteți recunoaște algoritmul RC4 în el.

Avem un algoritm și o cheie. Să încercăm să descifrăm numele clasei. Iată ce s-a întâmplat: com/taobao/wireless/security/adapter/JNICLibrary. Grozav! Suntem pe drumul cel bun.

Arborele de comenzi

Acum trebuie să găsim o provocare Înregistrați nativii, care ne va indica funcția doCommandNative. Să ne uităm la funcțiile numite de la JNI_OnLoad, și îl 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 este înregistrată o metodă nativă cu numele doCommandNative. Acum îi știm adresa. 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ă nume puteți ghici că aici este punctul de intrare al tuturor funcțiilor pe care dezvoltatorii au decis să le transfere în biblioteca nativă. Suntem interesați de funcția numărul 10601.

Puteți vedea din cod că numărul comenzii produce trei numere: comanda/10000, comanda % 10000 / 100 и comanda % 10, adică, în cazul nostru, 1, 6 și 1. Aceste trei numere, precum și un indicator către JNIEnv iar argumentele transmise funcției sunt adăugate unei structuri și transmise mai departe. Folosind cele trei numere obținute (să le notăm N1, N2 și N3), se construiește un arbore de comenzi.

Ceva de genul:

Căutăm vulnerabilități în UC Browser

Arborele este umplut dinamic JNI_OnLoad.
Trei numere codifică calea din arbore. Fiecare frunză a arborelui conține adresa pocked a funcției corespunzătoare. Cheia se află în nodul părinte. Găsirea locului din cod în care funcția de care avem nevoie este adăugată în arbore nu este dificilă dacă înțelegeți toate structurile folosite (nu le descriem pentru a nu umfla un articol deja destul de mare).

Mai multă ofuscare

Am primit adresa funcției care ar trebui să decripteze traficul: 0x5F1AC. Dar este prea devreme să ne bucurăm: dezvoltatorii UC Browser ne-au pregătit o altă surpriză.

După ce primim parametrii din matricea care s-a format în codul Java, obținem
la funcția de la adresa 0x4D070. Și aici ne așteaptă un alt tip de ofuscare a codului.

Am pus doi indici în R7 și R4:

Căutăm vulnerabilități în UC Browser

Mutăm primul indice la R11:

Căutăm vulnerabilități în UC Browser

Pentru a obține o adresă dintr-un tabel, utilizați un index:

Căutăm vulnerabilități în UC Browser

După ce mergeți la prima adresă, se folosește al doilea index, care este în R4. Există 230 de elemente în tabel.

Ce să faci în privința asta? Puteți spune IDA că acesta este un comutator: Editare -> Altele -> Specificați limbajul comutatorului.

Căutăm vulnerabilități în UC Browser

Codul rezultat este înfricoșător. Dar, făcându-ți drum prin jungla ei, poți observa un apel către o funcție deja familiară nouă sub_6115C:

Căutăm vulnerabilități în UC Browser

A existat un comutator în care în cazul 3 a existat o decriptare folosind algoritmul RC4. Și în acest caz, structura transmisă funcției este completată din parametrii trecuți la doCommandNative. Să ne amintim ce aveam acolo magicInt cu valoarea 16. Ne uităm la cazul corespunzător – iar după mai multe tranziții găsim codul prin care se poate identifica algoritmul.

Căutăm vulnerabilități în UC Browser

Acesta este AES!

Algoritmul există, nu rămâne decât să-i obținem parametrii: mod, cheie și, eventual, vectorul de inițializare (prezența acestuia depinde de modul de funcționare al algoritmului AES). Structura cu ele trebuie formată undeva înainte de apelul funcției sub_6115C, dar această parte a codului este deosebit de bine ofuscată, așa că apare ideea de a corecta codul, astfel încât toți parametrii funcției de decriptare să fie descărcați într-un fișier.

Plasture

Pentru a nu scrie manual tot codul de corecție în limbajul de asamblare, puteți lansa Android Studio, scrieți acolo o funcție care primește aceiași parametri de intrare ca și funcția noastră de decriptare și scrie într-un fișier, apoi copiați-lipiți codul pe care compilatorul îl va Genera.

Prietenii noștri din echipa UC Browser s-au ocupat și de comoditatea de a adăuga cod. Să ne amintim că la începutul fiecărei funcții avem un cod de gunoi care poate fi ușor înlocuit cu oricare altul. Foarte convenabil 🙂 Totuși, la începutul funcției țintă nu există suficient spațiu pentru codul care salvează toți parametrii într-un fișier. A trebuit să-l împart în părți și să folosesc blocuri de gunoi de la funcțiile învecinate. Au fost patru părți în total.

Prima parte:

Căutăm vulnerabilități în UC Browser

În arhitectura ARM, primii patru parametri ai funcției sunt trecuți prin registrele R0-R3, restul, dacă există, sunt trecuți prin stivă. Registrul LR poartă adresa de retur. Toate acestea trebuie să fie salvate, astfel încât funcția să poată funcționa după ce îi aruncăm parametrii. 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 parametri trecuți funcției prin intermediul stivei.

Folosind funcția deschis hai sa deschidem fisierul /data/local/tmp/aes în modul „ab”.
adică pentru adăugare. În R0 încărcăm adresa numelui fișierului, în R1 - adresa liniei care indică modul. Și aici se termină codul de gunoi, așa că trecem la următoarea funcție. Pentru ca acesta să funcționeze în continuare, punem la început trecerea la codul real al funcției, ocolind gunoiul, iar în loc de gunoi adăugăm o continuare a patch-ului.

Căutăm vulnerabilități în UC Browser

Apel deschis.

Primii trei parametri ai funcției aes au tip int. Deoarece am salvat registrele în stivă la început, putem pur și simplu să transmitem funcția fscrie adresele lor pe stivă.

Căutăm vulnerabilități în UC Browser

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

Căutăm vulnerabilități în UC Browser

La sfârșit, închideți fișierul, restaurați registrele și transferați controlul la funcția reală aes.

Colectăm un APK cu o bibliotecă corectată, îl semnăm, îl încărcăm pe dispozitiv/emulator și îl lansăm. Vedem că depozitul nostru este creat și o mulțime de date sunt scrise acolo. Browserul folosește criptarea nu numai pentru trafic, iar toată criptarea trece prin funcția în cauză. Dar, dintr-un motiv oarecare, datele necesare nu sunt acolo, iar solicitarea solicitată nu este vizibilă în trafic. Pentru a nu aștepta până când UC Browser se pregătește să facă cererea necesară, să luăm răspunsul criptat de la serverul primit mai devreme și să corectăm aplicația din nou: adăugați decriptarea la 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;)I

Asamblam, semnăm, instalăm, lansăm. Primim o NullPointerException deoarece metoda a returnat null.

În timpul analizei ulterioare a codului, a fost descoperită o funcție care descifrează linii interesante: „META-INF/” și „.RSA”. Se pare că aplicația își verifică certificatul. Sau chiar generează chei din el. Nu vreau să mă ocup de ceea ce se întâmplă cu certificatul, așa că îi vom introduce certificatul corect. Să corectăm linia criptată, astfel încât în ​​loc de „META-INF/” să obținem „BLABLINF/”, să creăm un folder cu acel nume în APK și să adăugăm acolo certificatul de browser veveriță.

Asamblam, semnăm, instalăm, lansăm. Bingo! Avem cheia!

MitM

Am primit o cheie și un vector de inițializare egal cu cheia. Să încercăm să decriptăm răspunsul serverului în modul CBC.

Căutăm vulnerabilități în UC Browser

Vedem adresa URL a arhivei, ceva similar cu MD5, „extract_unzipsize” și un număr. Verificăm: MD5-ul arhivei este același, dimensiunea bibliotecii dezambalate este aceeași. Încercăm să corectăm această bibliotecă și să o oferim browserului. Pentru a arăta că biblioteca noastră corectată s-a încărcat, vom lansa o Intenție de a crea un SMS cu textul „PWNED!” Vom înlocui două răspunsuri de la server: puds.ucweb.com/upgrade/index.xhtml și pentru a descărca arhiva. În primul înlocuim MD5 (dimensiunea nu se schimbă după despachetare), în al doilea dăm arhiva cu biblioteca patchată.

Browserul încearcă să descarce arhiva de mai multe ori, după care dă o eroare. Aparent ceva
lui nu-i place. Ca urmare a analizei acestui format tulbure, s-a dovedit că serverul transmite și dimensiunea arhivei:

Căutăm vulnerabilități în UC Browser

Este codificat în LEB128. După patch, dimensiunea arhivei cu biblioteca s-a schimbat puțin, așa că browserul a considerat că arhiva a fost descărcată strâmb, iar după mai multe încercări a aruncat o eroare.

Ajustăm dimensiunea arhivei... Și – victorie! 🙂 Rezultatul este în videoclip.

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

Consecințele și reacția dezvoltatorului

În același mod, hackerii ar putea folosi caracteristica nesigură a UC Browser pentru a distribui și a rula biblioteci rău intenționate. Aceste biblioteci vor funcționa în contextul browserului, deci vor primi toate permisiunile de sistem ale acestuia. Ca rezultat, capacitatea de a afișa ferestre de phishing, precum și accesul la fișierele de lucru ale veveriței chinezești portocalii, inclusiv autentificări, parole și cookie-uri stocate în baza de date.

Am contactat dezvoltatorii UC Browser și i-am informat despre problema pe care am găsit-o, am încercat să evidențiem vulnerabilitatea și pericolul acesteia, dar nu au discutat nimic cu noi. Între timp, browserul a continuat să-și etaleze caracteristica periculoasă la vedere. Dar odată ce am dezvăluit detaliile vulnerabilității, nu a mai fost posibil să o ignorăm ca înainte. 27 martie a fost
a fost lansată o nouă versiune a UC Browser 12.10.9.1193, care a accesat serverul prin HTTPS: puds.ucweb.com/upgrade/index.xhtml.

În plus, după „remediere” și până la momentul scrierii acestui articol, încercarea de a deschide un PDF într-un browser a dus la un mesaj de eroare cu textul „Hopa, ceva a mers prost!” O solicitare către server nu a fost făcută atunci când s-a încercat să deschidă un PDF, dar a fost făcută o solicitare la lansarea browserului, ceea ce sugerează posibilitatea continuă de a descărca codul executabil, încălcând regulile Google Play.

Sursa: www.habr.com

Adauga un comentariu