Шукаємо вразливості у 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, позначає один із трьох можливих варіантів.

Продовжуємо аналіз коду. Після пари стрибків потрапляємо в метод з назвою, що говорить. decryptBytesByKey.

Тут від нашої відповіді відокремлюється ще два байти, і з них виходить рядок. Зрозуміло, що у такий спосіб вибирається ключ для розшифрування повідомлення.

    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. І все це передається в якийсь Router з числом 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 і отримуємо купу діалогових вікон із помилками. Проблема у цьому, що таблиця секцій (section header table) – невалідна. Це зроблено спеціально для ускладнення аналізу.

Шукаємо вразливості у UC Browser

Але вона не потрібна: щоб коректно завантажити ELF-файл і проаналізувати його, цілком достатньо таблиці сегментів (program header table). Тому просто видаляємо таблицю секцій, занулюючи відповідні поля заголовку.

Шукаємо вразливості у UC Browser

Знову відкриваємо файл у IDA.

Є два способи повідомити віртуальну Java-машину, де саме в нативній бібліотеці знаходиться реалізація методу, оголошеного в Java-коді як native. Перший - дати йому ім'я виду Java_ім'я_пакета_Ім'яКласа_ім'яМетоду.

Другий — зареєструвати його під час завантаження бібліотеки (у функції JNI_OnLoad)
за допомогою виклику функції Реєстрація тубільців.

У нашому випадку, якщо використовувати перший спосіб, ім'я має бути таким: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Серед функцій, що експортуються, такої немає, отже, потрібно шукати виклик Реєстрація тубільців.
Ідемо у функцію JNI_OnLoad і бачимо таку картину:

Шукаємо вразливості у UC Browser

Що тут відбувається? На перший погляд, початок та кінець функції типові для архітектури ARM. Першою інструкцією у стек зберігається вміст регістрів, які функція буде використовувати у своїй роботі (в даному випадку R0, R1 і R2), а також вміст регістра LR, в якому знаходиться адреса повернення з функції. Останньою інструкцією збережені регістри відновлюються, причому адреса повернення відразу поміщається в регістр PC - таким чином відбувається повернення з функції. Але якщо придивитися, можна помітити, що передостання інструкція змінює адресу повернення, збережену в стеку. Обчислимо, яким він буде після
виконання коду. У R1 завантажується якийсь адресу 0xB130, від нього віднімається 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, додається до цієї адреси, після чого береться значення за отриманою адресою і кладеться в стек. Також до LR додається 4, щоб після повернення з функції перескочити це зсув. Після цього команда 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;
}

Як параметр switch передається поле структури, якому раніше було надано значення 3. Дивимося case 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.

За кодом можна побачити, що з номера команди виходить три числа: command / 10000, команда % 10000 / 100 и command % 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, що це такий switch: Edit -> Other -> Specify switch idiom.

Шукаємо вразливості у UC Browser

Код страшний. Але, пробираючись через його нетрі, можна побачити виклик вже знайомої нам функції sub_6115C:

Шукаємо вразливості у UC Browser

Там був switch, у якому в case 3 знаходилася розшифровка з використанням алгоритму RC4. А в цьому випадку структура, що передається в функцію, заповнюється з параметрів, переданих у doCommandNative. Згадуємо, що у нас там був magicInt зі значенням 16. Дивимося відповідний case – і після кількох переходів знаходимо код, яким можна впізнати алгоритм.

Шукаємо вразливості у UC Browser

Це AES!

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

патч

Щоб не писати весь код патча на мові асемблера вручну, можна запустити Android Studio, написати там функцію, яка отримує на вхід такі самі параметри, як у нашої функції розшифровки, і пише у файл, після чого скопіпастить код, який згенерує компілятор.

Про зручність додавання коду наші друзі з команди UC Browser теж подбали. Згадуємо, що на початку кожної функції ми маємо сміттєвий код, який легко можна замінити на будь-який інший. Дуже зручно 🙂 Правда, на початку цільової функції місця для коду, який зберігає всі параметри у файл, обмаль. Довелося розділити його на частини та використовувати сміттєві блоки сусідніх функцій. Усього вийшло чотири частини.

Перша частина:

Шукаємо вразливості у UC Browser

В архітектурі ARM перші чотири параметри функції передаються через регістри R0-R3, інші, якщо вони є через стек. У регістрі LR передається адреса повернення. Все це потрібно зберегти для того, щоб функція могла відпрацювати після того, як ми стискаємо її параметри. Також потрібно зберегти всі регістри, які ми використовуватимемо в процесі, тому робимо PUSH.W {R0-R10,LR}. У R7 ми отримуємо адресу списку параметрів, переданих функції через стек.

За допомогою функції fopen відкриємо файл /data/local/tmp/aes в режимі "ab",
тобто на додавання. У R0 завантажуємо адресу імені файлу, у R1 - адресу рядка із зазначенням режиму. І тут сміттєвий код закінчується, тому переходимо до наступної функції. Щоб вона продовжувала працювати, ставимо спочатку перехід на справжній код функції в обхід сміття, а замість сміття додаємо продовження патча.

Шукаємо вразливості у UC Browser

Викликаємо fopen.

Перші три параметри функції АЕС мають тип Int. Так як ми на початку зберегли регістри у стек, можна просто передати функції fwrite їх адреси у стеку.

Шукаємо вразливості у UC Browser

Далі у нас є три структури, які містять розмір даних та покажчик на дані для ключа, вектора ініціалізації та зашифрованих даних.

Шукаємо вразливості у UC Browser

Наприкінці закриваємо файл, відновлюємо регістри та передаємо керування справжньою функцією АЕС.

Збираємо 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, оскільки метод повернув null.

У ході подальшого аналізу коду було виявлено функцію, в якій розшифровуються цікаві рядки: «META-INF/» та «.RSA». Схоже, програма перевіряє свій сертифікат. Або навіть генерує ключі із нього. Розбиратися з тим, що відбувається з сертифікатом, зовсім не хочеться, тому просто підсунемо йому правильний сертифікат. Пропатчимо зашифрований рядок таким чином, щоб замість «META-INF/» вийшло «BLABLINF/», створимо папку з таким ім'ям в APK і підкинемо туди сертифікат білкобраузера.

Збираємо, підписуємо, встановлюємо, запускаємо. Бінґо! Ключ у нас!

MitM

Ми отримали ключ і вектор ініціалізації, що дорівнює ключу. Спробуймо розшифрувати відповідь сервера в режимі CBC.

Шукаємо вразливості у UC Browser

Бачимо URL архіву, щось схоже на MD5, extract_unzipsize і число. Перевіряємо: MD5 архіву збігається, розмір розпакованої бібліотеки збігається. Пробуємо пропатчити цю бібліотеку та віддати її браузеру. Щоб показати, що наша пропатчена бібліотека завантажилася, запускатимемо Intent на створення 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.

Джерело: habr.com

Додати коментар або відгук