Hľadajte chyby zabezpečenia v prehliadači UC

Hľadajte chyby zabezpečenia v prehliadači UC

Úvod

Koncom marca sme nahlásené, že objavili skrytú možnosť načítať a spustiť neoverený kód v prehliadači UC. Dnes sa podrobne pozrieme na to, ako k tomuto sťahovaniu dochádza a ako ho môžu hackeri využiť na svoje účely.

Pred časom bol UC Browser propagovaný a distribuovaný veľmi agresívne: bol nainštalovaný na zariadenia používateľov pomocou malvéru, distribuovaného z rôznych stránok pod zámienkou video súborov (t. j. používatelia si mysleli, že sťahujú napríklad porno video, ale namiesto toho dostali súbor APK s týmto prehliadačom), používali strašidelné bannery so správami, že prehliadač je zastaraný, zraniteľný a podobne. V oficiálnej skupine UC Browser na VK existuje téma, v ktorej sa môžu používatelia sťažovať na nekalú reklamu, príkladov je tam veľa. V roku 2016 došlo dokonca videoreklama v ruštine (áno, reklama na prehliadač blokujúci reklamy).

V čase písania tohto článku má prehliadač UC v službe Google Play viac ako 500 000 000 inštalácií. To je pôsobivé – iba Google Chrome má viac. Medzi recenziami môžete vidieť pomerne veľa sťažností na reklamu a presmerovania na niektoré aplikácie v Google Play. Toto bol dôvod nášho výskumu: rozhodli sme sa zistiť, či UC Browser nerobí niečo zlé. A ukázalo sa, že áno!

V kóde aplikácie bola objavená možnosť stiahnuť a spustiť spustiteľný kód, čo je v rozpore s pravidlami pre zverejňovanie aplikácií na Google Play. Okrem sťahovania spustiteľného kódu to UC Browser robí nezabezpečeným spôsobom, ktorý možno použiť na spustenie útoku MitM. Uvidíme, či dokážeme vykonať takýto útok.

Všetko napísané nižšie je relevantné pre verziu prehliadača UC, ktorá bola v čase štúdie dostupná na Google Play:

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

Vektor útoku

V manifeste prehliadača UC môžete nájsť službu so samovysvetľujúcim názvom com.uc.deployment.UpgradeDeployService.

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

Keď sa táto služba spustí, prehliadač odošle požiadavku POST puds.ucweb.com/upgrade/index.xhtml, čo je vidieť v premávke nejaký čas po štarte. Ako odpoveď môže dostať príkaz na stiahnutie aktualizácie alebo nového modulu. Počas analýzy server takéto príkazy nedával, ale všimli sme si, že keď sa pokúšame otvoriť PDF v prehliadači, odošle druhú požiadavku na vyššie uvedenú adresu, po ktorej stiahne natívnu knižnicu. Na uskutočnenie útoku sme sa rozhodli využiť túto funkciu prehliadača UC: možnosť otvárať PDF pomocou natívnej knižnice, ktorá nie je v APK a ktorú si v prípade potreby stiahne z internetu. Stojí za zmienku, že teoreticky môže byť UC Browser nútený stiahnuť niečo bez interakcie používateľa - ak poskytnete dobre formovanú odpoveď na požiadavku, ktorá sa vykoná po spustení prehliadača. Aby sme to však urobili, musíme si podrobnejšie preštudovať protokol interakcie so serverom, preto sme sa rozhodli, že bude jednoduchšie upraviť zachytenú odpoveď a nahradiť knižnicu pre prácu s PDF.

Ak teda chce používateľ otvoriť súbor PDF priamo v prehliadači, v prevádzke sa môžu zobraziť nasledujúce požiadavky:

Hľadajte chyby zabezpečenia v prehliadači UC

Najprv existuje požiadavka POST puds.ucweb.com/upgrade/index.xhtml, potom
Stiahne sa archív s knižnicou na prezeranie PDF a kancelárskych formátov. Je logické predpokladať, že prvá požiadavka odošle informácie o systéme (aspoň architektúre na poskytnutie požadovanej knižnice) a ako odpoveď na ňu prehliadač dostane nejaké informácie o knižnici, ktorú je potrebné stiahnuť: adresu a príp. , niečo iné. Problém je v tom, že táto požiadavka je šifrovaná.

Žiadosť o fragment

Fragment odpovede

Hľadajte chyby zabezpečenia v prehliadači UC

Hľadajte chyby zabezpečenia v prehliadači UC

Samotná knižnica je zabalená v ZIP a nie je šifrovaná.

Hľadajte chyby zabezpečenia v prehliadači UC

Vyhľadajte kód na dešifrovanie premávky

Pokúsme sa dešifrovať odpoveď servera. Pozrime sa na kód triedy com.uc.deployment.UpgradeDeployService: z metódy onStartCommand ísť do com.uc.deployment.bxa z neho do 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);
}

Vidíme tu vytvorenie požiadavky POST. Dávame pozor na vytvorenie poľa 16 bajtov a jeho vyplnenie: 0x5F, 0, 0x1F, -50 (=0xCE). Zhoduje sa s tým, čo sme videli v žiadosti vyššie.

V tej istej triede môžete vidieť vnorenú triedu, ktorá má inú zaujímavú metódu:

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

Metóda berie ako vstup pole bajtov a kontroluje, či nulový bajt je 0x60 alebo tretí bajt je 0xD0 a druhý bajt je 1, 11 alebo 0x1F. Pozeráme sa na odpoveď zo servera: nulový bajt je 0x60, druhý je 0x1F, tretí je 0x60. Znie to ako to, čo potrebujeme. Súdiac podľa riadkov (napríklad „up_decrypt“) by sa tu mala zavolať metóda, ktorá dešifruje odpoveď servera.
Prejdime k metóde gj. Všimnite si, že prvým argumentom je bajt s posunom 2 (t. j. 0x1F v našom prípade) a druhým je odpoveď servera bez
prvých 16 bajtov.

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

Je zrejmé, že tu vyberieme dešifrovací algoritmus a rovnaký bajt, aký je v našom
prípad rovný 0x1F, označuje jednu z troch možných možností.

Pokračujeme v analýze kódu. Po pár skokoch sa ocitáme v metóde so samovysvetľujúcim názvom decryptBytesByKey.

Tu sa od našej odpovede oddelia ďalšie dva bajty a z nich sa získa reťazec. Je zrejmé, že týmto spôsobom sa vyberie kľúč na dešifrovanie správy.

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

Pri pohľade do budúcnosti si všimneme, že v tejto fáze ešte nezískavame kľúč, ale iba jeho „identifikátor“. Získanie kľúča je trochu zložitejšie.

V ďalšej metóde sa k existujúcim parametrom pridajú ďalšie dva, čím sa vytvoria štyri z nich: magické číslo 16, identifikátor kľúča, zašifrované údaje a nezrozumiteľný reťazec (v našom prípade prázdny).

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

Po sérii prechodov sa dostávame k metóde staticBinarySafeDecryptNoB64 rozhranie com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. V hlavnom aplikačnom kóde nie sú žiadne triedy, ktoré implementujú toto rozhranie. V súbore je taká trieda lib/armeabi-v7a/libsgmain.so, čo v skutočnosti nie je .so, ale .jar. Metóda, ktorá nás zaujíma, je implementovaná takto:

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

Tu je náš zoznam parametrov doplnený o ďalšie dve celé čísla: 2 a 0. Súdiac podľa
všetko, 2 znamená dešifrovanie, ako v metóde doFinal systémová trieda javax.crypto.Cipher. A toto všetko sa prenesie do istého smerovača s číslom 10601 - to je zrejme číslo príkazu.

Po ďalšom reťazci prechodov nájdeme triedu, ktorá implementuje rozhranie IRouterComponent a metóda 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);
}
}

A tiež trieda JNICLibrary, v ktorom je deklarovaná natívna metóda doCommandNative:

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

To znamená, že musíme nájsť metódu v natívnom kóde doCommandNative. A tu začína zábava.

Zahmlievanie strojového kódu

V súbore libsgmain.so (čo je vlastne .jar a v ktorom sme našli implementáciu niektorých rozhraní súvisiacich so šifrovaním vyššie) existuje jedna natívna knižnica: libsgmainso-6.4.36.so. Otvoríme ho v IDA a dostaneme veľa dialógových okien s chybami. Problém je v tom, že tabuľka hlavičiek sekcií je neplatná. Toto sa robí zámerne, aby sa skomplikovala analýza.

Hľadajte chyby zabezpečenia v prehliadači UC

Nie je to však potrebné: na správne načítanie súboru ELF a jeho analýzu stačí tabuľka hlavičiek programu. Preto jednoducho vymažeme tabuľku sekcií a vynulujeme príslušné polia v hlavičke.

Hľadajte chyby zabezpečenia v prehliadači UC

Znova otvorte súbor v IDA.

Existujú dva spôsoby, ako povedať virtuálnemu stroju Java, kde presne sa v natívnej knižnici nachádza implementácia metódy deklarovanej v kóde Java ako natívne. Prvým je dať mu druhové meno Java_package_name_ClassName_MethodName.

Druhým je zaregistrovať ho pri načítaní knižnice (vo funkcii JNI_OnLoad)
pomocou volania funkcie RegisterNatives.

V našom prípade, ak použijeme prvú metódu, názov by mal byť takýto: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Medzi exportovanými funkciami takáto funkcia nie je, čo znamená, že musíte vyhľadať hovor RegisterNatives.
Poďme k funkcii JNI_OnLoad a vidíme tento obrázok:

Hľadajte chyby zabezpečenia v prehliadači UC

Čo sa tu deje? Pre architektúru ARM je na prvý pohľad typický začiatok a koniec funkcie. Prvá inštrukcia na zásobníku ukladá obsah registrov, ktoré funkcia použije pri svojej činnosti (v tomto prípade R0, R1 a R2), ako aj obsah registra LR, ktorý obsahuje návratovú adresu z funkcie. . Posledná inštrukcia obnoví uložené registre a návratová adresa je okamžite umiestnená do registra PC - teda návrat z funkcie. Ak sa však pozriete pozorne, všimnete si, že predposledná inštrukcia mení návratovú adresu uloženú v zásobníku. Poďme si spočítať, aké to bude potom
vykonávanie kódu. Do R1 sa nahrá určitá adresa 0xB130, od nej sa odčíta 5, potom sa prenesie do R0 a k nej sa pridá 0x10. Ukázalo sa, že 0xB13B. IDA si teda myslí, že posledná inštrukcia je normálny návrat funkcie, ale v skutočnosti smeruje na vypočítanú adresu 0xB13B.

Tu je potrebné pripomenúť, že procesory ARM majú dva režimy a dve sady pokynov: ARM a Thumb. Najmenej významný bit adresy informuje procesor, ktorá inštrukčná sada sa používa. To znamená, že adresa je v skutočnosti 0xB13A a jeden z najmenej významných bitov označuje režim Thumb.

Podobný „adaptér“ bol pridaný na začiatok každej funkcie v tejto knižnici a
odpadkový kód. Nebudeme sa nimi ďalej podrobne zaoberať - len si pamätáme
že skutočný začiatok takmer všetkých funkcií je o niečo ďalej.

Keďže kód výslovne nepreskočí na 0xB13A, samotná IDA nerozpoznala, že sa kód nachádza na tomto mieste. Z rovnakého dôvodu nepozná väčšinu kódu v knižnici ako kód, čo trochu sťažuje analýzu. Hovoríme IDA, že toto je kód a stane sa toto:

Hľadajte chyby zabezpečenia v prehliadači UC

Tabuľka jasne začína na 0xB144. Čo je v sub_494C?

Hľadajte chyby zabezpečenia v prehliadači UC

Pri volaní tejto funkcie v registri LR získame adresu vyššie uvedenej tabuľky (0xB144). V R0 - index v tejto tabuľke. To znamená, že hodnota sa vezme z tabuľky, pripočíta sa k LR a výsledok je
adresu, na ktorú chcete ísť. Skúsme to vypočítať: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Ideme na prijatú adresu a uvidíme doslova pár užitočných pokynov a znova prejdeme na 0xB140:

Hľadajte chyby zabezpečenia v prehliadači UC

Teraz dôjde k prechodu na offset s indexom 0x20 z tabuľky.

Súdiac podľa veľkosti tabuľky, takýchto prechodov bude v kóde veľa. Vynára sa otázka, či sa to dá nejako riešiť automaticky, bez manuálneho výpočtu adries. A skripty a schopnosť opravovať kód v IDA nám pomáhajú:

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"

Umiestnite kurzor na riadok 0xB26A, spustite skript a uvidíte prechod na 0xB4B0:

Hľadajte chyby zabezpečenia v prehliadači UC

IDA opäť nerozpoznala túto oblasť ako kód. Pomáhame jej a vidíme tam ďalší dizajn:

Hľadajte chyby zabezpečenia v prehliadači UC

Návod po BLX vraj nedáva veľký zmysel, skôr ide o nejaký výtlak. Pozrime sa na sub_4964:

Hľadajte chyby zabezpečenia v prehliadači UC

A skutočne, tu sa vezme dword na adrese ležiacej v LR, pridá sa k tejto adrese, potom sa vezme hodnota na výslednej adrese a vloží sa do zásobníka. K LR sa tiež pridá 4, takže po návrate z funkcie sa rovnaký posun preskočí. Potom príkaz POP {R1} prevezme výslednú hodnotu zo zásobníka. Ak sa pozriete na to, čo sa nachádza na adrese 0xB4BA + 0xEA = 0xB5A4, uvidíte niečo podobné ako tabuľka adries:

Hľadajte chyby zabezpečenia v prehliadači UC

Na opravu tohto dizajnu budete musieť z kódu získať dva parametre: offset a číslo registra, do ktorého chcete vložiť výsledok. Pre každú možnú registráciu si budete musieť vopred pripraviť kúsok kódu.

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"

Umiestnime kurzor na začiatok štruktúry, ktorú chceme nahradiť - 0xB4B2 - a spustíme skript:

Hľadajte chyby zabezpečenia v prehliadači UC

Okrem už spomínaných štruktúr kód obsahuje aj nasledovné:

Hľadajte chyby zabezpečenia v prehliadači UC

Rovnako ako v predchádzajúcom prípade, po inštrukcii BLX je offset:

Hľadajte chyby zabezpečenia v prehliadači UC

Vezmeme offset na adresu z LR, pridáme ho do LR a ideme tam. 0x72044 + 0xC = 0x72050. Skript pre tento dizajn je pomerne jednoduchý:

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"

Výsledok spustenia skriptu:

Hľadajte chyby zabezpečenia v prehliadači UC

Keď je všetko vo funkcii opravené, môžete IDA nasmerovať na jej skutočný začiatok. Dá dohromady celý funkčný kód a dá sa dekompilovať pomocou HexRays.

Dekódovacie reťazce

Naučili sme sa riešiť zahmlievanie strojového kódu v knižnici libsgmainso-6.4.36.so z prehliadača UC a prijali kód funkcie 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;
}

Pozrime sa bližšie na nasledujúce riadky:

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

Vo funkcii sub_73E24 názov triedy sa jednoznačne dešifruje. Ako parametre tejto funkcie sa odovzdáva ukazovateľ na údaje podobné šifrovaným údajom, určitá vyrovnávacia pamäť a číslo. Je zrejmé, že po zavolaní funkcie bude vo vyrovnávacej pamäti dešifrovaný riadok, pretože je odovzdaný funkcii FindClass, ktorý má názov triedy ako druhý parameter. Preto je číslo veľkosť vyrovnávacej pamäte alebo dĺžka riadku. Skúsme rozlúštiť názov triedy, mal by nám povedať, či ideme správnym smerom. Pozrime sa bližšie na to, čo sa deje v 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;
}

Funkcia sub_7AF78 vytvorí inštanciu kontajnera pre bajtové polia zadanej veľkosti (nebudeme sa podrobne venovať týmto kontajnerom). Tu sú vytvorené dva takéto kontajnery: jeden obsahuje riadok "DcO/lcK+h?m3c*q@" (je ľahké uhádnuť, že ide o kľúč), druhý obsahuje šifrované údaje. Ďalej sa oba objekty umiestnia do určitej štruktúry, ktorá sa odovzdá funkcii sub_6115C. Označme v tejto štruktúre aj pole s hodnotou 3. Pozrime sa, čo sa stane s touto štruktúrou ďalej.

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

Parameter prepínača je pole štruktúry, ktorému bola predtým priradená hodnota 3. Pozrite sa na prípad 3: funkcie sub_6364C parametre sa odovzdávajú zo štruktúry, ktorá tam bola pridaná v predchádzajúcej funkcii, t. j. kľúč a zašifrované dáta. Ak sa pozriete pozorne sub_6364C, spoznáte v ňom algoritmus RC4.

Máme algoritmus a kľúč. Skúsme rozlúštiť názov triedy. Tu je to, čo sa stalo: com/taobao/wireless/security/adapter/JNICLibrary. Skvelé! Sme na správnej ceste.

Príkazový strom

Teraz musíme nájsť výzvu RegisterNatives, ktorý nás nasmeruje na funkciu doCommandNative. Pozrime sa na funkcie volané z JNI_OnLoad, a nájdeme to v 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;
}

A skutočne je tu zaregistrovaná natívna metóda s názvom doCommandNative. Teraz poznáme jeho adresu. Pozrime sa, čo robí.

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

Podľa názvu môžete uhádnuť, že tu je vstupný bod všetkých funkcií, ktoré sa vývojári rozhodli preniesť do natívnej knižnice. Máme záujem o funkciu číslo 10601.

Z kódu môžete vidieť, že číslo príkazu vytvára tri čísla: príkaz/10000, príkaz % 10000 100 / XNUMX и príkaz % 10, teda v našom prípade 1, 6 a 1. Tieto tri čísla, ako aj ukazovateľ na JNIEnv a argumenty odovzdané funkcii sa pridajú do štruktúry a prenesú sa ďalej. Pomocou troch získaných čísel (označme ich N1, N2 a N3) sa zostaví strom príkazov.

Niečo také:

Hľadajte chyby zabezpečenia v prehliadači UC

Strom sa vypĺňa dynamicky JNI_OnLoad.
Tri čísla kódujú cestu v strome. Každý list stromu obsahuje vybranú adresu príslušnej funkcie. Kľúč je v nadradenom uzle. Nájsť miesto v kóde, kde je do stromu pridaná funkcia, ktorú potrebujeme, nie je ťažké, ak rozumiete všetkým použitým štruktúram (neopisujeme ich, aby sme nenafúkli už aj tak dosť rozsiahly článok).

Ďalšie zahmlievanie

Dostali sme adresu funkcie, ktorá by mala dešifrovať prevádzku: 0x5F1AC. Ale na radosť je priskoro: vývojári prehliadača UC pre nás pripravili ďalšie prekvapenie.

Po prijatí parametrov z poľa, ktoré bolo vytvorené v kóde Java, dostaneme
na funkciu na adrese 0x4D070. A tu nás čaká ďalší typ zahmlievania kódu.

Do R7 a R4 sme vložili dva indexy:

Hľadajte chyby zabezpečenia v prehliadači UC

Prvý index presunieme na R11:

Hľadajte chyby zabezpečenia v prehliadači UC

Ak chcete získať adresu z tabuľky, použite index:

Hľadajte chyby zabezpečenia v prehliadači UC

Po prechode na prvú adresu sa použije druhý index, ktorý je v R4. V tabuľke je 230 prvkov.

Čo s tým robiť? IDA môžete povedať, že ide o prepínač: Edit -> Other -> Specify switch idiom.

Hľadajte chyby zabezpečenia v prehliadači UC

Výsledný kód je strašidelný. Keď sa však predierate jeho džungľou, môžete si všimnúť volanie funkcie, ktorá je nám už známa sub_6115C:

Hľadajte chyby zabezpečenia v prehliadači UC

Bol tam prepínač, v ktorom v prípade 3 došlo k dešifrovaniu pomocou algoritmu RC4. A v tomto prípade sa štruktúra odovzdaná funkcii vyplní z parametrov odovzdaných do doCommandNative. Pripomeňme si, čo sme tam mali magicInt s hodnotou 16. Pozrieme sa na príslušný prípad - a po niekoľkých prechodoch nájdeme kód, podľa ktorého sa dá algoritmus identifikovať.

Hľadajte chyby zabezpečenia v prehliadači UC

Toto je AES!

Algoritmus existuje, zostáva len získať jeho parametre: režim, kľúč a prípadne inicializačný vektor (jeho prítomnosť závisí od prevádzkového režimu algoritmu AES). Štruktúra s nimi musí byť vytvorená niekde pred volaním funkcie sub_6115C, ale táto časť kódu je obzvlášť dobre zahmlená, takže vzniká nápad opraviť kód tak, aby sa všetky parametre dešifrovacej funkcie uložili do súboru.

Náplasť

Aby ste nepísali celý opravný kód v assembleri ručne, môžete spustiť Android Studio, napísať tam funkciu, ktorá dostane rovnaké vstupné parametre ako naša dešifrovacia funkcia a zapíše do súboru, potom skopírujte a prilepte kód, ktorý kompilátor vytvorí generovať.

O pohodlie pri pridávaní kódu sa postarali aj naši priatelia z tímu UC Browser. Pripomeňme si, že na začiatku každej funkcie máme odpadkový kód, ktorý možno ľahko nahradiť ktorýmkoľvek iným. Veľmi pohodlné 🙂 Na začiatku cieľovej funkcie však nie je dostatok miesta pre kód, ktorý ukladá všetky parametre do súboru. Musel som ho rozdeliť na časti a použiť bloky odpadu zo susedných funkcií. Celkovo boli štyri časti.

Prvá časť:

Hľadajte chyby zabezpečenia v prehliadači UC

V architektúre ARM sa prvé štyri parametre funkcie prenášajú cez registre R0-R3, ostatné, ak nejaké existujú, sa prenášajú cez zásobník. Register LR nesie spätnú adresu. Toto všetko je potrebné uložiť, aby funkcia mohla fungovať po tom, ako vyklopíme jej parametre. Potrebujeme tiež uložiť všetky registre, ktoré budeme v procese používať, takže urobíme PUSH.W {R0-R10,LR}. V R7 dostaneme adresu zoznamu parametrov odovzdaných funkcii cez zásobník.

Pomocou funkcie otvorené otvoríme súbor /data/local/tmp/aes v režime „ab“.
teda na doplnenie. V R0 načítame adresu názvu súboru, v R1 - adresu riadku označujúci režim. A tu odpadkový kód končí, takže prejdeme k ďalšej funkcii. Aby to fungovalo aj naďalej, dáme na začiatok prechod do reálneho kódu funkcie, obídenie smetia a namiesto smetia pridáme pokračovanie patchu.

Hľadajte chyby zabezpečenia v prehliadači UC

Voláme otvorené.

Prvé tri parametre funkcie aes mať typ int. Keďže sme na začiatku uložili registre do zásobníka, funkciu môžeme jednoducho odovzdať fwrite ich adresy v zásobníku.

Hľadajte chyby zabezpečenia v prehliadači UC

Ďalej máme tri štruktúry, ktoré obsahujú veľkosť údajov a ukazovateľ na údaje pre kľúč, inicializačný vektor a šifrované údaje.

Hľadajte chyby zabezpečenia v prehliadači UC

Na konci súbor zatvorte, obnovte registre a preneste riadenie do skutočnej funkcie aes.

Zhromažďujeme súbor APK s opravenou knižnicou, podpíšeme ho, nahráme do zariadenia/emulátora a spustíme. Vidíme, že sa vytvára náš výpis a zapisuje sa tam množstvo údajov. Prehliadač používa šifrovanie nielen pre návštevnosť a celé šifrovanie prechádza cez príslušnú funkciu. Ale z nejakého dôvodu tam nie sú potrebné údaje a požadovaná požiadavka nie je viditeľná v prevádzke. Aby sme nečakali, kým UC Browser odošle potrebnú požiadavku, zoberme si zašifrovanú odpoveď zo servera prijatú skôr a znova aplikáciu opravíme: pridajte dešifrovanie do onCreate hlavnej aktivity.

    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

Zmontujeme, podpíšeme, nainštalujeme, spustíme. Výnimku NullPointerException dostaneme, pretože metóda vrátila hodnotu null.

Pri ďalšej analýze kódu bola objavená funkcia, ktorá dešifruje zaujímavé riadky: „META-INF/“ a „.RSA“. Zdá sa, že aplikácia overuje svoj certifikát. Alebo z neho dokonca generuje kľúče. Naozaj sa nechcem zaoberať tým, čo sa deje s certifikátom, takže mu jednoducho dáme správny certifikát. Opravme zašifrovaný riadok tak, že namiesto „META-INF/“ dostaneme „BLABLINF/“, vytvoríme priečinok s týmto názvom v APK a pridáme tam certifikát prehliadača veverička.

Zmontujeme, podpíšeme, nainštalujeme, spustíme. Bingo! My máme kľúč!

MitM

Dostali sme kľúč a inicializačný vektor rovný kľúču. Skúsme dešifrovať odpoveď servera v režime CBC.

Hľadajte chyby zabezpečenia v prehliadači UC

Vidíme URL archívu, niečo podobné ako MD5, „extract_unzipsize“ a číslo. Skontrolujeme: MD5 archívu je rovnaké, veľkosť rozbalenej knižnice je rovnaká. Snažíme sa opraviť túto knižnicu a dať ju prehliadaču. Aby sme ukázali, že sa naša opravená knižnica načítala, spustíme zámer vytvoriť SMS s textom „PWNED!“ Nahradíme dve odpovede zo servera: puds.ucweb.com/upgrade/index.xhtml a stiahnuť archív. V prvom nahradíme MD5 (veľkosť sa po rozbalení nemení), v druhom dáme archív s oplatenou knižnicou.

Prehliadač sa niekoľkokrát pokúsi stiahnuť archív, potom zobrazí chybu. Zrejme niečo
nemá rád. V dôsledku analýzy tohto temného formátu sa ukázalo, že server prenáša aj veľkosť archívu:

Hľadajte chyby zabezpečenia v prehliadači UC

Je zakódovaný v LEB128. Po oprave sa veľkosť archívu s knižnicou trochu zmenila, takže prehliadač usúdil, že archív je stiahnutý krivo a po niekoľkých pokusoch vyhodil chybu.

Prispôsobujeme veľkosť archívu... A – víťazstvo! 🙂 Výsledok je vo videu.

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

Dôsledky a reakcia vývojára

Rovnakým spôsobom by hackeri mohli použiť nezabezpečenú funkciu prehliadača UC na distribúciu a spúšťanie škodlivých knižníc. Tieto knižnice budú fungovať v kontexte prehliadača, takže získajú všetky jeho systémové povolenia. Výsledkom je možnosť zobrazenia phishingových okien, ako aj prístup k pracovným súborom oranžovej čínskej veveričky vrátane prihlasovacích údajov, hesiel a cookies uložených v databáze.

Oslovili sme vývojárov prehliadača UC a informovali sme ich o probléme, ktorý sme našli, snažili sme sa poukázať na zraniteľnosť a jej nebezpečenstvo, no o ničom sa s nami nebavili. Medzitým prehliadač naďalej predvádzal svoju nebezpečnú funkciu na očiach. Ale akonáhle sme odhalili podrobnosti o zraniteľnosti, už ju nebolo možné ignorovať ako predtým. Bol 27. marec
bola vydaná nová verzia prehliadača UC 12.10.9.1193, ktorý pristupoval na server cez HTTPS: puds.ucweb.com/upgrade/index.xhtml.

Okrem toho po „oprave“ a až do času písania tohto článku sa pri pokuse o otvorenie súboru PDF v prehliadači zobrazilo chybové hlásenie s textom „Ojoj, niečo sa pokazilo!“ Pri pokuse o otvorenie súboru PDF nebola odoslaná požiadavka na server, ale žiadosť bola zadaná pri spustení prehliadača, čo naznačuje, že je možné naďalej sťahovať spustiteľný kód v rozpore s pravidlami Google Play.

Zdroj: hab.com

Pridať komentár