Шукаем уразлівасці ў 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)
з дапамогай выкліку функцыі RegisterNatives.

У нашым выпадку, калі выкарыстоўваць першы спосаб, імя павінна быць такім: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

Сярод экспартуемых функцый такой няма, значыць, трэба шукаць выклік RegisterNatives.
Ідзем у функцыю 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. Выдатна! Мы на правільным шляху.

Дрэва каманд

Цяпер трэба знайсці выклік RegisterNatives, які пакажа нам на функцыю 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, command% 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 і падкінем туды сертыфікат белкобраўзера.

Збіраны, падпісваем, усталёўваны, запускаем. Бінга! Ключ у нас!

МітМ

Мы атрымалі ключ і вектар ініцыялізацыі, роўны ключу. Паспрабуем расшыфраваць адказ сэрвера ў рэжыме CBC.

Шукаем уразлівасці ў UC Browser

Бачым URL архіва, нешта падобнае на MD5, "extract_unzipsize" і лік. Правяраем: MD5 архіва супадае, памер распакаванай бібліятэкі супадае. Спрабуем прапапатчыць гэтую бібліятэку і аддаць яе браўзэру. Каб паказаць, што нашая прапатчаная бібліятэка загрузілася, будзем запускаць Intent на стварэнне СМС з тэкстам «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

Дадаць каментар