Търсят се уязвимости в UC Browser

Търсят се уязвимости в UC Browser

въведение

В края на март ние докладвани, че са открили скрита възможност за зареждане и изпълнение на непроверен код в UC Browser. Днес ще разгледаме подробно как става това изтегляне и как хакерите могат да го използват за свои собствени цели.

Преди известно време UC Browser беше рекламиран и разпространяван много агресивно: той беше инсталиран на потребителски устройства с помощта на зловреден софтуер, разпространяван от различни сайтове под прикритието на видео файлове (т.е. потребителите си мислеха, че изтеглят например порно видео, но вместо това получи APK с този браузър), използва страшни банери със съобщения, че браузърът е остарял, уязвим и подобни неща. В официалната група на UC Browser във VK има тема, в които потребителите могат да се оплакват от нелоялна реклама, има много примери там. През 2016 г. дори имаше видео реклама на руски (да, реклама за браузър за блокиране на реклами).

Към момента на писане UC Browser има над 500 000 000 инсталации в Google Play. Това е впечатляващо - само Google Chrome има повече. Сред рецензиите можете да видите доста оплаквания относно реклами и пренасочвания към някои приложения в Google Play. Това беше причината за нашето проучване: решихме да видим дали UC Browser прави нещо лошо. И се оказа, че го прави!

В кода на приложението беше открита възможност за изтегляне и стартиране на изпълним код, което противоречи на правилата за публикуване на приложения в Google Play. В допълнение към изтеглянето на изпълним код, UC Browser прави това по несигурен начин, който може да се използва за стартиране на MitM атака. Да видим дали можем да извършим такава атака.

Всичко написано по-долу е приложимо за версията на UC Browser, която беше налична в Google Play по време на проучването:

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

Вектор на атака

В манифеста на UC Browser можете да намерите услуга с разбираемо име com.uc.deployment.UpgradeDeployService.

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

Когато тази услуга стартира, браузърът прави POST заявка към puds.ucweb.com/upgrade/index.xhtml, което може да се види в трафика известно време след старта. В отговор той може да получи команда да изтегли някаква актуализация или нов модул. По време на анализа сървърът не даде такива команди, но забелязахме, че когато се опитаме да отворим PDF в браузъра, той прави втора заявка до посочения по-горе адрес, след което изтегля родната библиотека. За да извършим атаката, решихме да използваме тази функция на UC Browser: възможността за отваряне на PDF с помощта на родна библиотека, която не е в APK и която тя изтегля от Интернет, ако е необходимо. Струва си да се отбележи, че теоретично UC Browser може да бъде принуден да изтегли нещо без взаимодействие с потребителя - ако предоставите добре формиран отговор на заявка, която се изпълнява след стартиране на браузъра. Но за да направим това, трябва да проучим по-подробно протокола за взаимодействие със сървъра, затова решихме, че ще бъде по-лесно да редактираме прихванатия отговор и да заменим библиотеката за работа с PDF.

Така че, когато потребител иска да отвори PDF директно в браузъра, в трафика могат да се видят следните заявки:

Търсят се уязвимости в UC Browser

Първо има POST заявка към puds.ucweb.com/upgrade/index.xhtml, тогава
Изтегля се архив с библиотека за преглед на PDF и офис формати. Логично е да се предположи, че първата заявка предава информация за системата (най-малко архитектурата за предоставяне на необходимата библиотека), а в отговор на нея браузърът получава някаква информация за библиотеката, която трябва да бъде изтеглена: адреса и евентуално , нещо друго. Проблемът е, че тази заявка е криптирана.

Заявка за фрагмент

Фрагмент на отговора

Търсят се уязвимости в UC Browser

Търсят се уязвимости в UC Browser

Самата библиотека е пакетирана в ZIP и не е криптирана.

Търсят се уязвимости в UC Browser

Потърсете код за дешифриране на трафика

Нека се опитаме да дешифрираме отговора на сървъра. Нека да разгледаме кода на класа com.uc.deployment.UpgradeDeployService: от метод onStartCommand отидете на com.uc.deployment.bx, и от него към 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);
}

Виждаме формирането на POST заявка тук. Обръщаме внимание на създаването на масив от 16 байта и неговото попълване: 0x5F, 0, 0x1F, -50 (=0xCE). Съвпада с това, което видяхме в заявката по-горе.

В същия клас можете да видите вложен клас, който има друг интересен метод:

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

Методът приема масив от байтове като вход и проверява дали нулевият байт е 0x60 или третият байт е 0xD0, а вторият байт е 1, 11 или 0x1F. Разглеждаме отговора от сървъра: нулевият байт е 0x60, вторият е 0x1F, третият е 0x60. Звучи като това, от което се нуждаем. Съдейки по редовете („up_decrypt“, например), тук трябва да се извика метод, който ще дешифрира отговора на сървъра.
Да преминем към метода gj. Имайте предвид, че първият аргумент е байтът при отместване 2 (т.е. 0x1F в нашия случай), а вторият е отговорът на сървъра без
първите 16 байта.

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

Очевидно тук избираме алгоритъм за дешифриране и същия байт, който е в нашия
случай, равен на 0x1F, обозначава една от три възможни опции.

Продължаваме да анализираме кода. След няколко скока се озоваваме в метод с разбиращо се име декриптиранеBytesByKey.

Тук още два байта се отделят от нашия отговор и от тях се получава низ. Ясно е, че по този начин се избира ключът за дешифриране на съобщението.

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

Гледайки напред, отбелязваме, че на този етап все още не получаваме ключ, а само неговия „идентификатор“. Получаването на ключа е малко по-сложно.

В следващия метод към съществуващите се добавят още два параметъра, което ги прави четири: магическото число 16, идентификаторът на ключа, криптираните данни и неразбираем низ (в нашия случай празен).

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

След серия от преходи стигаме до метода staticBinarySafeDecryptNoB64 интерфейс com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Няма класове в основния код на приложението, които реализират този интерфейс. Във файла има такъв клас lib/armeabi-v7a/libsgmain.so, което всъщност не е .so, а .jar. Методът, който ни интересува, се изпълнява по следния начин:

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

Тук нашият списък от параметри се допълва с още две цели числа: 2 и 0. Съдейки по
всичко, 2 означава дешифриране, както в метода doFinal системен клас javax.crypto.Cipher. И всичко това се прехвърля на определен рутер с номер 10601 - това явно е номерът на командата.

След следващата верига от преходи намираме клас, който имплементира интерфейса IRouterComponent и метод 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);
}
}

А също и класа JNICLibrary, в който е деклариран нативният метод doCommandNative:

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

Това означава, че трябва да намерим метод в собствения код doCommandNative. И тук започва забавлението.

Обфускация на машинния код

Във файла libsgmain.so (което всъщност е .jar и в което открихме изпълнението на някои интерфейси, свързани с криптиране точно по-горе) има една собствена библиотека: libsgmainso-6.4.36.so. Отваряме го в IDA и получаваме куп диалогови прозорци с грешки. Проблемът е, че таблицата със заглавки на секции е невалидна. Това се прави нарочно, за да се усложни анализът.

Търсят се уязвимости в UC Browser

Но не е необходимо: за да заредите правилно ELF файл и да го анализирате, е достатъчна таблица със заглавки на програмата. Следователно ние просто изтриваме таблицата със секции, като нулираме съответните полета в заглавката.

Търсят се уязвимости в UC Browser

Отворете отново файла в IDA.

Има два начина да кажете на виртуалната машина на Java къде точно в собствената библиотека се намира изпълнението на метод, деклариран в кода на Java като естествен. Първият е да му се даде име на вида Java_package_name_ClassName_MethodName.

Второто е да го регистрирате при зареждане на библиотеката (във функцията JNI_OnLoad)
използвайки извикване на функция Регистрирайте се.

В нашия случай, ако използваме първия метод, името трябва да бъде така: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Сред експортираните функции няма такава функция, което означава, че трябва да потърсите повикване Регистрирайте се.
Да отидем на функцията JNI_OnLoad и виждаме тази снимка:

Търсят се уязвимости в UC Browser

Какво става тук? На пръв поглед началото и краят на функцията са типични за ARM архитектурата. Първата инструкция в стека съхранява съдържанието на регистрите, които функцията ще използва в своята работа (в този случай R0, R1 и R2), както и съдържанието на регистъра LR, който съдържа адреса за връщане от функцията . Последната инструкция възстановява записаните регистри, а адресът за връщане веднага се поставя в регистъра на компютъра - по този начин се връща от функцията. Но ако се вгледате внимателно, ще забележите, че предпоследната инструкция променя адреса за връщане, съхранен в стека. Нека изчислим какво ще бъде след това
изпълнение на код. Определен адрес 1xB0 се зарежда в R130, от него се изважда 5, след което се прехвърля към R0 и към него се добавя 0x10. Оказва се 0xB13B. По този начин IDA смята, че последната инструкция е нормално връщане на функция, но всъщност тя скача до изчисления адрес 0xB13B.

Тук си струва да припомним, че ARM процесорите имат два режима и два набора инструкции: ARM и Thumb. Най-малкият бит от адреса казва на процесора кой набор от инструкции се използва. Тоест адресът всъщност е 0xB13A, а един в най-младшият бит показва режима Thumb.

Подобен „адаптер“ е добавен в началото на всяка функция в тази библиотека и
код за боклук. По-нататък няма да се спираме на тях подробно - просто си спомняме
че истинското начало на почти всички функции е малко по-далече.

Тъй като кодът не прескача изрично до 0xB13A, самата IDA не разпозна, че кодът се намира на това място. По същата причина той не разпознава по-голямата част от кода в библиотеката като код, което прави анализа донякъде труден. Казваме на IDA, че това е кодът, и ето какво се случва:

Търсят се уязвимости в UC Browser

Таблицата очевидно започва от 0xB144. Какво има в sub_494C?

Търсят се уязвимости в UC Browser

Когато извикваме тази функция в регистъра LR, получаваме адреса на споменатата по-горе таблица (0xB144). В R0 - индекс в тази таблица. Тоест стойността се взема от таблицата, добавя се към LR и резултатът е
адреса, на който да отидете. Нека се опитаме да го изчислим: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Отиваме на получения адрес и виждаме буквално няколко полезни инструкции и отново отиваме на 0xB140:

Търсят се уязвимости в UC Browser

Сега ще има преход при отместване с индекс 0x20 от таблицата.

Съдейки по размера на таблицата, ще има много такива преходи в кода. Възниква въпросът дали е възможно по някакъв начин да се справим с това по-автоматично, без ръчно изчисляване на адреси. И скриптовете и възможността за корекция на код в IDA ни идват на помощ:

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"

Поставете курсора на ред 0xB26A, стартирайте скрипта и вижте прехода към 0xB4B0:

Търсят се уязвимости в UC Browser

IDA отново не разпозна тази област като код. Ние й помагаме и виждаме друг дизайн там:

Търсят се уязвимости в UC Browser

Инструкциите след BLX май нямат особен смисъл, по-скоро е някакво изместване. Нека да разгледаме sub_4964:

Търсят се уязвимости в UC Browser

И наистина, тук се взема dword на адреса, който се намира в LR, добавя се към този адрес, след което стойността на получения адрес се взема и се поставя в стека. Също така, 4 се добавя към LR, така че след връщане от функцията същото отместване се пропуска. След което командата POP {R1} взема получената стойност от стека. Ако погледнете какво се намира на адрес 0xB4BA + 0xEA = 0xB5A4, ще видите нещо подобно на адресна таблица:

Търсят се уязвимости в UC Browser

За да коригирате този дизайн, ще трябва да получите два параметъра от кода: отместването и номера на регистъра, в който искате да поставите резултата. За всеки възможен регистър ще трябва предварително да подготвите част от кода.

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"

Поставяме курсора в началото на структурата, която искаме да заменим - 0xB4B2 - и стартираме скрипта:

Търсят се уязвимости в UC Browser

В допълнение към вече споменатите структури, кодът съдържа и следното:

Търсят се уязвимости в UC Browser

Както в предишния случай, след инструкцията BLX има отместване:

Търсят се уязвимости в UC Browser

Взимаме отместването към адреса от LR, добавяме го към LR и отиваме там. 0x72044 + 0xC = 0x72050. Скриптът за този дизайн е доста прост:

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"

Резултат от изпълнението на скрипта:

Търсят се уязвимости в UC Browser

След като всичко е закърпено във функцията, можете да посочите IDA към нейното истинско начало. Той ще сглоби целия функционален код и може да бъде декомпилиран с помощта на HexRays.

Декодиране на низове

Научихме се да се справяме с обфускацията на машинния код в библиотеката libsgmainso-6.4.36.so от UC Browser и получи функционалния код 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;
}

Нека разгледаме по-подробно следните редове:

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

Във функция sub_73E24 името на класа очевидно се дешифрира. Като параметри на тази функция се предава указател към данни, подобни на криптирани данни, определен буфер и число. Очевидно след извикването на функцията ще има дешифриран ред в буфера, тъй като той се предава на функцията FindClass, който приема името на класа като втори параметър. Следователно числото е размерът на буфера или дължината на линията. Нека се опитаме да дешифрираме името на класа, то трябва да ни каже дали вървим в правилната посока. Нека да разгледаме по-отблизо какво се случва в 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;
}

Функция sub_7AF78 създава екземпляр на контейнер за байтови масиви с определен размер (няма да се спираме подробно на тези контейнери). Тук се създават два такива контейнера: единият съдържа линията „DcO/lcK+h?m3c*q@“ (лесно е да се досетите, че това е ключ), другият съдържа криптирани данни. След това двата обекта се поставят в определена структура, която се предава на функцията sub_6115C. Нека също да маркираме поле със стойност 3 в тази структура.Нека видим какво ще се случи с тази структура след това.

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

Параметърът за превключване е структурно поле, на което преди това е била присвоена стойност 3. Вижте случай 3: към функцията sub_6364C параметрите се предават от структурата, която е добавена там в предишната функция, т.е. ключът и криптираните данни. Ако се вгледате внимателно в sub_6364C, можете да разпознаете алгоритъма RC4 в него.

Имаме алгоритъм и ключ. Нека се опитаме да дешифрираме името на класа. Ето какво се случи: com/taobao/wireless/security/adapter/JNICLibrary. Страхотен! Ние сме на прав път.

Командно дърво

Сега трябва да намерим предизвикателство Регистрирайте се, което ще ни насочи към функцията doCommandNative. Нека разгледаме функциите, извикани от JNI_OnLoad, и го намираме в 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;
}

И наистина тук е регистриран естествен метод с името doCommandNative. Сега знаем адреса му. Да видим какво прави.

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

По името можете да познаете, че тук е входната точка на всички функции, които разработчиците решиха да прехвърлят в родната библиотека. Интересуваме се от функция номер 10601.

Можете да видите от кода, че номерът на командата произвежда три числа: команда/10000, команда % 10000 / 100 и команда % 10, т.е. в нашия случай 1, 6 и 1. Тези три числа, както и указател към JNIEnv и аргументите, предадени на функцията, се добавят към структура и се предават нататък. С помощта на получените три числа (нека ги обозначим с N1, N2 и N3) се изгражда командно дърво.

Нещо като това:

Търсят се уязвимости в UC Browser

Дървото се попълва динамично JNI_OnLoad.
Три числа кодират пътя в дървото. Всяко листо на дървото съдържа вградения адрес на съответната функция. Ключът е в родителския възел. Намирането на мястото в кода, където функцията, от която се нуждаем, е добавена към дървото, не е трудно, ако разбирате всички използвани структури (ние не ги описваме, за да не раздуваме вече доста голяма статия).

Още объркване

Получихме адреса на функцията, която трябва да дешифрира трафика: 0x5F1AC. Но е твърде рано да се радваме: разработчиците на UC Browser са подготвили още една изненада за нас.

След получаване на параметрите от масива, който е формиран в Java кода, получаваме
към функцията на адрес 0x4D070. И тук ни очаква друг тип обфускация на кода.

Поставяме два индекса в R7 и R4:

Търсят се уязвимости в UC Browser

Преместваме първия индекс към R11:

Търсят се уязвимости в UC Browser

За да получите адрес от таблица, използвайте индекс:

Търсят се уязвимости в UC Browser

След като отидете на първия адрес, се използва вторият индекс, който е в R4. В таблицата има 230 елемента.

Какво да правим по въпроса? Можете да кажете на IDA, че това е превключвател: Edit -> Other -> Specify switch idiom.

Търсят се уязвимости в UC Browser

Полученият код е ужасен. Но, проправяйки си път през джунглата му, можете да забележите извикване на функция, която вече ни е позната sub_6115C:

Търсят се уязвимости в UC Browser

Имаше превключвател, в който в случай 3 имаше декриптиране с помощта на алгоритъма RC4. И в този случай структурата, предадена на функцията, се запълва от параметрите, предадени на doCommandNative. Да си спомним какво имахме там magicInt със стойност 16. Разглеждаме съответния случай - и след няколко прехода намираме кода, по който алгоритъмът може да бъде идентифициран.

Търсят се уязвимости в UC Browser

Това е AES!

Алгоритъмът съществува, остава само да се получат неговите параметри: режим, ключ и, евентуално, вектор за инициализация (неговото присъствие зависи от режима на работа на алгоритъма AES). Структурата с тях трябва да се формира някъде преди извикването на функцията sub_6115C, но тази част от кода е особено добре обфусцирана, така че възниква идеята да се коригира кодът, така че всички параметри на функцията за дешифриране да бъдат изхвърлени във файл.

кръпка

За да не пишете целия код на корекцията на асемблер ръчно, можете да стартирате Android Studio, да напишете там функция, която получава същите входни параметри като нашата функция за декриптиране и записва във файл, след което копирайте и поставете кода, който компилаторът ще генерирам.

За удобството при добавяне на код се погрижиха и нашите приятели от екипа на UC Browser. Нека си припомним, че в началото на всяка функция имаме код за боклук, който лесно може да бъде заменен с всеки друг. Много удобно 🙂 В началото на целевата функция обаче няма достатъчно място за кода, който записва всички параметри във файл. Трябваше да го разделя на части и да използвам блокове боклук от съседни функции. Имаше общо четири части.

Първата част:

Търсят се уязвимости в UC Browser

В архитектурата ARM първите четири функционални параметъра се предават през регистрите R0-R3, останалите, ако има такива, се предават през стека. Регистърът LR носи обратния адрес. Всичко това трябва да бъде запазено, за да може функцията да работи, след като изхвърлим нейните параметри. Също така трябва да запазим всички регистри, които ще използваме в процеса, така че правим PUSH.W {R0-R10,LR}. В R7 получаваме адреса на списъка с параметри, предадени на функцията чрез стека.

Използване на функцията fopen да отворим файла /данни/локални/tmp/aes в режим "ab".
т.е. за добавяне. В R0 зареждаме адреса на името на файла, в R1 - адреса на реда, показващ режима. И тук кодът за боклук свършва, така че преминаваме към следващата функция. За да продължи да работи, поставяме в началото прехода към реалния код на функцията, заобикаляйки боклука, а вместо боклука добавяме продължение на корекцията.

Търсят се уязвимости в UC Browser

Обаждаме се fopen.

Първите три параметъра на функцията AES имат тип Int. Тъй като записахме регистрите в стека в началото, можем просто да предадем функцията fwrite техните адреси в стека.

Търсят се уязвимости в UC Browser

След това имаме три структури, които съдържат размера на данните и указател към данните за ключа, вектор за инициализация и криптирани данни.

Търсят се уязвимости в UC Browser

Накрая затворете файла, възстановете регистрите и прехвърлете управлението на реалната функция AES.

Събираме APK с пачвана библиотека, подписваме го, качваме го на устройството/емулатора и го стартираме. Виждаме, че нашият дъмп се създава и там се записват много данни. Браузърът използва криптиране не само за трафика, а цялото криптиране минава през въпросната функция. Но по някаква причина необходимите данни не са там и необходимата заявка не се вижда в трафика. За да не чакаме, докато UC Browser благоволи да направи необходимата заявка, нека вземем шифрования отговор от сървъра, получен по-рано, и отново да закърпим приложението: добавете дешифрирането към onCreate на основната дейност.

    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

Сглобяваме, подписваме, инсталираме, пускаме. Получаваме NullPointerException, защото методът върна нула.

По време на по-нататъшния анализ на кода беше открита функция, която дешифрира интересни редове: “META-INF/” и “.RSA”. Изглежда, че приложението проверява своя сертификат. Или дори генерира ключове от него. Наистина не искам да се занимавам с това, което се случва със сертификата, така че просто ще му предоставим правилния сертификат. Нека закърпим шифрования ред, така че вместо „META-INF/“ да получим „BLABLINF/“, създайте папка с това име в APK и добавете сертификата на браузъра на катерица там.

Сглобяваме, подписваме, монтираме, пускаме. Бинго! Ние имаме ключа!

MitM

Получихме ключ и вектор за инициализация, равен на ключа. Нека се опитаме да дешифрираме отговора на сървъра в режим CBC.

Търсят се уязвимости в UC Browser

Виждаме URL адреса на архива, нещо подобно на MD5, „extract_unzipsize“ и число. Проверяваме: MD5 на архива е същият, размерът на разопакованата библиотека е същият. Опитваме се да коригираме тази библиотека и да я дадем на браузъра. За да покажем, че нашата библиотека с корекции се е заредила, ще стартираме намерение за създаване на SMS с текст „PWNED!“ Ще заменим два отговора от сървъра: puds.ucweb.com/upgrade/index.xhtml и да изтеглите архива. В първия заменяме MD5 (размерът не се променя след разопаковане), във втория даваме архива с закърпената библиотека.

Браузърът се опитва да изтегли архива няколко пъти, след което дава грешка. Явно нещо
той не харесва. В резултат на анализа на този мътен формат се оказа, че сървърът предава и размера на архива:

Търсят се уязвимости в UC Browser

Той е кодиран в LEB128. След корекцията размерът на архива с библиотеката се промени малко, така че браузърът счете, че архивът е изтеглен неправилно и след няколко опита издаде грешка.

Коригираме размера на архива... И – победа! 🙂 Резултатът е във видеото.

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

Последици и реакция на разработчика

По същия начин хакерите могат да използват несигурната функция на UC Browser, за да разпространяват и изпълняват злонамерени библиотеки. Тези библиотеки ще работят в контекста на браузъра, така че ще получат всички негови системни разрешения. В резултат на това възможността за показване на фишинг прозорци, както и достъп до работните файлове на оранжевата китайска катерица, включително влизания, пароли и бисквитки, съхранявани в базата данни.

Свързахме се с разработчиците на UC Browser и ги информирахме за проблема, който открихме, опитахме се да посочим уязвимостта и нейната опасност, но те не обсъдиха нищо с нас. Междувременно браузърът продължи да показва опасната си функция пред очите. Но след като разкрихме подробностите за уязвимостта, вече не беше възможно да я игнорираме както преди. 27 март беше
беше пусната нова версия на UC Browser 12.10.9.1193, която осъществява достъп до сървъра чрез HTTPS: puds.ucweb.com/upgrade/index.xhtml.

В допълнение, след „поправката“ и до момента на написване на тази статия, опитът за отваряне на PDF в браузър доведе до съобщение за грешка с текст „Ами сега, нещо се обърка!“ Заявка към сървъра не е направена при опит за отваряне на PDF, но е направена заявка при стартиране на браузъра, което намеква за продължаващата възможност за изтегляне на изпълним код в нарушение на правилата на Google Play.

Източник: www.habr.com

Добавяне на нов коментар