Вовед
На крајот на март ние
Пред извесно време, UC Browser беше рекламиран и дистрибуиран многу агресивно: тој беше инсталиран на уредите на корисниците користејќи малициозен софтвер, дистрибуиран од различни сајтови под маската на видео датотеки (т.е., корисниците мислеа дека преземаат, на пример, порно видео, но наместо тоа, доби АПК со овој прелистувач), користеше застрашувачки банери со пораки дека прелистувачот е застарен, ранлив и слични работи. Во официјалната група на UC Browser на VK постои
Во моментот на пишување, UC Browser има над 500 инсталации на Google Play. Ова е импресивно - само Google Chrome има повеќе. Меѓу прегледите можете да видите доста поплаки за рекламирање и пренасочувања кон некои апликации на Google Play. Ова беше причината за нашето истражување: решивме да видиме дали UC Browser прави нешто лошо. И испадна дека го прави тоа!
Во кодот на апликацијата, беше откриена можноста за преземање и извршување на извршниот код,
Сè што е напишано подолу е релевантно за верзијата на UC Browser што беше достапна на Google Play во времето на студијата:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c
Вектор на напад
Во манифестот на прелистувачот UC можете да најдете услуга со самообјасниво име com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Кога ќе се стартува оваа услуга, прелистувачот испраќа барање POST до
Значи, кога корисникот сака да отвори PDF директно во прелистувачот, следните барања може да се видат во сообраќајот:
Прво има барање POST да
Се презема архива со библиотека за прегледување на PDF и канцелариски формати. Логично е да се претпостави дека првото барање пренесува информации за системот (барем архитектурата за да ја обезбеди потребната библиотека), а како одговор на тоа прелистувачот добива некои информации за библиотеката што треба да се преземе: адресата и, можеби , нешто друго. Проблемот е што ова барање е шифрирано.
Побарајте фрагмент
Фрагмент од одговор
Самата библиотека е спакувана во ZIP и не е шифрирана.
Пребарајте код за дешифрирање на сообраќајот
Ајде да се обидеме да го дешифрираме одговорот на серверот. Ајде да го погледнеме кодот на класата 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“, на пример), овде треба да се повика метод што ќе го дешифрира одговорот на серверот.
Ајде да преминеме на методот ѓ. Забележете дека првиот аргумент е бајтот на поместување 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, туку .тегла. Методот за кој сме заинтересирани е имплементиран на следниов начин:
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 значи дешифрирање, како во методот доКонечно системска класа 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);
}
}
И, исто така, класа JNICL библиотека, во кој е деклариран мајчин метод doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}
Ова значи дека треба да најдеме метод во мајчин код doCommandNative. И тука започнува забавата.
Замаглување на машинскиот код
Во датотека libsgmain.така (што всушност е .тегла и во која најдовме имплементација на некои интерфејси поврзани со шифрирање веднаш погоре) има една домашна библиотека: libsgmainso-6.4.36.so. Го отвораме во IDA и добиваме еден куп дијалог-кутии со грешки. Проблемот е што табелата со заглавие на секцијата е неважечка. Ова е направено намерно за да се комплицира анализата.
Но, тоа не е потребно: за правилно да се вчита датотеката ELF и да се анализира, доволна е табела за заглавие на програмата. Затоа, ние едноставно ја бришеме табелата со секции, нулајќи ги соодветните полиња во заглавието.
Повторно отворете ја датотеката во IDA.
Постојат два начини да се каже на Java виртуелната машина каде точно во матичната библиотека се наоѓа имплементацијата на методот деклариран во Java кодот како мајчин. Првата е да му се даде име на видот Java_package_name_ClassName_MethodName.
Втората е да ја регистрирате при вчитување на библиотеката (во функцијата JNI_OnLoad)
користејќи функциски повик Регистрирај се Домородци.
Во нашиот случај, ако го користиме првиот метод, името треба да биде вака: Java_com_taobao_wireless_security_adapter_JNICLlibrary_doCommandNative.
Не постои таква функција меѓу извезените функции, што значи дека треба да барате повик Регистрирај се Домородци.
Ајде да одиме на функцијата JNI_OnLoad и ја гледаме оваа слика:
Што се случува овде? На прв поглед, почетокот и крајот на функцијата се типични за архитектурата на ARM. Првата инструкција на оџакот ја складира содржината на регистрите што функцијата ќе ги користи во своето работење (во овој случај, R0, R1 и R2), како и содржината на регистарот LR, кој ја содржи повратната адреса од функцијата . Последната инструкција ги обновува зачуваните регистри, а адресата за враќање веднаш се става во регистарот на компјутер - со што се враќа од функцијата. Но, ако погледнете внимателно, ќе забележите дека претпоследната инструкција ја менува повратната адреса зачувана на оџакот. Ајде да пресметаме како ќе биде после
извршување на кодот. Одредена адреса 1xB0 се вчитува во R130, од неа се одзема 5, потоа се пренесува на R0 и на неа се додава 0x10. Излегува 0xB13B. Така, IDA смета дека последната инструкција е нормално враќање на функцијата, но всушност таа оди на пресметаната адреса 0xB13B.
Овде вреди да се потсетиме дека ARM процесорите имаат два режима и две групи инструкции: ARM и Thumb. Најмалку значајниот дел од адресата му кажува на процесорот кој сет на инструкции се користи. Односно, адресата е всушност 0xB13A, а еден во најмалку значајниот бит го означува режимот Thumb.
Сличен „адаптер“ е додаден на почетокот на секоја функција во оваа библиотека и
код за ѓубре. Ние нема да се задржиме на нив подетално - само се сеќаваме
дека вистинскиот почеток на речиси сите функции е малку подалеку.
Бидејќи кодот не прескокнува експлицитно на 0xB13A, самата IDA не препозна дека кодот се наоѓа на оваа локација. Од истата причина, тој не го препознава најголемиот дел од кодот во библиотеката како код, што ја отежнува анализата донекаде. Им кажуваме на IDA дека ова е кодот, и еве што се случува:
Табелата јасно започнува на 0xB144. Што има во sub_494C?
При повикување на оваа функција во регистарот LR, ја добиваме адресата на претходно споменатата табела (0xB144). Во R0 - индекс во оваа табела. Односно, вредноста се зема од табелата, се додава на LR и резултатот е
адресата на која треба да се оди. Ајде да се обидеме да го пресметаме: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Одиме на добиената адреса и гледаме буквално неколку корисни инструкции и повторно одиме на 0xB140:
Сега ќе има транзиција при поместување со индекс 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:
ИДА повторно не ја препозна оваа област како шифра. Ние и помагаме и гледаме друг дизајн таму:
Инструкциите по BLX изгледа немаат многу смисла, тоа е повеќе како некакво поместување. Ајде да погледнеме во sub_4964:
И навистина, овде се зема dword на адресата што лежи во LR, додадена на оваа адреса, по што вредноста на добиената адреса се зема и се става на оџакот. Исто така, 4 се додава на LR, така што по враќањето од функцијата, истото поместување се прескокнува. По што командата POP {R1} ја зема добиената вредност од стекот. Ако погледнете што се наоѓа на адресата 0xB4BA + 0xEA = 0xB5A4, ќе видите нешто слично на табела со адреси:
За да го закрпите овој дизајн, ќе треба да добиете два параметри од кодот: поместувањето и бројот на регистарот во кој сакате да го ставите резултатот. За секој можен регистар, ќе треба однапред да подготвите парче код.
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 - и ја извршуваме скриптата:
Покрај веќе споменатите структури, кодот го содржи и следново:
Како и во претходниот случај, по инструкцијата BLX има поместување:
Го земаме поместувањето на адресата од 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"
Резултат од извршувањето на скриптата:
Откако сè ќе се закрпи во функцијата, можете да го насочите 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;
}
Функција под_7AF78 создава примерок од контејнер за бајтни низи со одредената големина (нема да се задржиме на овие контејнери во детали). Тука се создаваат два такви контејнери: едниот ја содржи линијата „DcO/lcK+h?m3c*q@“ (лесно е да се погоди дека ова е клуч), другиот содржи шифрирани податоци. Следно, двата објекти се ставаат во одредена структура, која се пренесува на функцијата под_6115С. Ајде да означиме и поле со вредност 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: на функцијата под_6364С параметрите се пренесуваат од структурата што беа додадени таму во претходната функција, односно клучот и шифрираните податоци. Ако погледнете внимателно на под_6364С, можете да го препознаете алгоритмот RC4 во него.
Имаме алгоритам и клуч. Ајде да се обидеме да го дешифрираме името на класата. Еве што се случи: com / taobao / безжична / безбедност / адаптер / JNICL библиотека. Одлично! Ние сме на вистинскиот пат.
Дрво на команди
Сега треба да најдеме предизвик Регистрирај се Домородци, што ќе ни укаже на функцијата doCommandNative. Ајде да ги погледнеме функциите повикани од JNI_OnLoad, и го наоѓаме во под_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), се гради командно дрво.
Нешто како ова:
Дрвото се пополнува динамично JNI_OnLoad.
Три броја ја кодираат патеката во дрвото. Секој лист од дрвото ја содржи ставената адреса на соодветната функција. Клучот е во родителскиот јазол. Пронаоѓањето на местото во кодот каде што функцијата што ни треба е додадена на дрвото не е тешко ако ги разбирате сите употребени структури (не ги опишуваме за да не се надуе веќе прилично голема статија).
Повеќе замаглување
Ја добивме адресата на функцијата што треба да го дешифрира сообраќајот: 0x5F1AC. Но, рано е да се радуваме: програмерите на UC Browser ни подготвија уште едно изненадување.
По добивањето на параметрите од низата што беше формирана во кодот Java, добиваме
на функцијата на адреса 0x4D070. И тука нè чека друг тип на замаглување на кодот.
Ставивме два индекси во R7 и R4:
Го префрламе првиот индекс на R11:
За да добиете адреса од табела, користете индекс:
По одењето на првата адреса се користи вториот индекс кој е во R4. Во табелата има 230 елементи.
Што да се прави во врска со тоа? Можете да му кажете на IDA дека ова е прекинувач: Уреди -> Друго -> Наведете идиом на прекинувачот.
Резултирачкиот код е застрашувачки. Но, пробивајќи се низ нејзината џунгла, можете да забележите повик до функција која веќе ни е позната под_6115С:
Имаше прекинувач во кој во случајот 3 имаше дешифрирање со помош на алгоритмот RC4. И во овој случај, структурата предадена на функцијата се пополнува од параметрите доставени до doCommandNative. Да се потсетиме што имавме таму magicInt со вредност 16. Го разгледуваме соодветниот случај - и по неколку транзиции го наоѓаме кодот со кој може да се идентификува алгоритмот.
Ова е AES!
Алгоритмот постои, останува само да се добијат неговите параметри: режим, клуч и, можеби, векторот за иницијализација (неговото присуство зависи од режимот на работа на алгоритмот AES). Структурата со нив мора да се формира некаде пред повикот на функцијата под_6115С, но овој дел од кодот е особено добро заматен, па се наметнува идејата да се закрпи кодот така што сите параметри на функцијата за дешифрирање се фрлаат во датотека.
Лепенка
За да не се пишува рачно целиот код за закрпи на асемблерски јазик, можете да го стартувате Android Studio, да напишете функција таму што ги прима истите влезни параметри како нашата функција за дешифрирање и запишува во датотека, а потоа копирајте-залепете го кодот што компајлерот ќе го генерира.
Нашите пријатели од тимот на UC Browser се погрижија и за практичноста на додавање код. Да се потсетиме дека на почетокот на секоја функција имаме ѓубре код кој лесно може да се замени со кој било друг. Многу погодно 🙂 Сепак, на почетокот на целната функција нема доволно простор за кодот што ги зачувува сите параметри во датотека. Морав да го поделам на делови и да користам блокови за ѓубре од соседните функции. Имаше вкупно четири дела.
Првиот дел:
Во архитектурата ARM, првите четири функционални параметри се пренесуваат низ регистрите R0-R3, а останатите, доколку ги има, се пренесуваат низ стекот. Регистарот LR ја носи повратната адреса. Сето ова треба да се зачува за да може функцијата да работи откако ќе ги исфрлиме нејзините параметри. Исто така, треба да ги зачуваме сите регистри што ќе ги користиме во процесот, па го правиме PUSH.W {R0-R10,LR}. Во R7 ја добиваме адресата на листата на параметри пренесени на функцијата преку стекот.
Користење на функцијата отворете ајде да ја отвориме датотеката /data/local/tmp/aes во режим „ab“.
односно за дополнување. Во R0 ја вчитуваме адресата на името на датотеката, во R1 - адресата на линијата што го означува режимот. И тука завршува кодот за ѓубре, па преминуваме на следната функција. За да продолжи да работи, на почетокот го ставаме преминот кон вистинскиот код на функцијата, заобиколувајќи го ѓубрето и наместо ѓубрето додаваме продолжение на закрпата.
Повикувајќи се отворете.
Првите три параметри на функцијата аес имаат тип int. Бидејќи на почетокот ги зачувавме регистрите во стекот, едноставно можеме да ја пренесеме функцијата fwrite нивните адреси на оџакот.
Следно, имаме три структури кои ја содржат големината на податоците и покажувач кон податоците за клучот, векторот за иницијализација и шифрирани податоци.
На крајот, затворете ја датотеката, вратете ги регистрите и префрлете ја контролата на вистинската функција аес.
Собираме АПК со закрпена библиотека, ја потпишуваме, ја поставуваме на уредот/емулаторот и ја стартуваме. Гледаме дека наша депонија се создава, а таму се пишуваат многу податоци. Прелистувачот користи шифрирање не само за сообраќај, и целото шифрирање поминува низ функцијата за која станува збор. Но, поради некоја причина потребните податоци ги нема, а бараното барање не е видливо во сообраќајот. За да не чекаме додека 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/“, да креираме папка со тоа име во АПК и таму да го додадеме сертификатот на прелистувачот squirrel.
Ние собираме, потпишуваме, инсталираме, стартуваме. Бинго! Ние го имаме клучот!
MitM
Добивме клуч и вектор за иницијализација еднаков на клучот. Ајде да се обидеме да го дешифрираме одговорот на серверот во CBC режим.
Го гледаме URL-то на архивата, нешто слично на MD5, „extract_unzipsize“ и број. Проверуваме: MD5 на архивата е иста, големината на неотпакуваната библиотека е иста. Се обидуваме да ја закрпиме оваа библиотека и да ја дадеме на прелистувачот. За да покажеме дека нашата закрпена библиотека е вчитана, ќе започнеме Intent да креираме SMS со текст „PWNED!“ Ќе замениме два одговори од серверот:
Прелистувачот се обидува да ја преземе архивата неколку пати, по што дава грешка. Очигледно нешто
тој не сака. Како резултат на анализата на овој матен формат, се покажа дека серверот ја пренесува и големината на архивата:
Тој е кодиран во LEB128. По закрпата, големината на архивата со библиотеката малку се промени, па прелистувачот сметаше дека архивата е симната криво и по неколку обиди фрли грешка.
Ја прилагодуваме големината на архивата... И – победа! 🙂 Резултатот е во видеото.
Последици и реакција на развивачот
На ист начин, хакерите би можеле да ја користат небезбедната карактеристика на UC Browser за да дистрибуираат и стартуваат малициозни библиотеки. Овие библиотеки ќе работат во контекст на прелистувачот, така што ќе ги добијат сите негови системски дозволи. Како резултат на тоа, можноста за прикажување на фишинг прозорци, како и пристап до работните датотеки на портокаловата кинеска верверица, вклучувајќи најавувања, лозинки и колачиња зачувани во базата на податоци.
Ги контактиравме програмерите на UC Browser и ги информиравме за проблемот што го најдовме, се обидовме да укажеме на ранливоста и нејзината опасност, но тие не разговараа за ништо со нас. Во меѓувреме, прелистувачот продолжи да ја покажува својата опасна карактеристика на очигледен поглед. Но, откако ги откривме деталите за ранливоста, веќе не беше можно да се игнорира како порано. 27 март беше
беше објавена нова верзија на UC Browser 12.10.9.1193, која пристапи до серверот преку HTTPS:
Дополнително, по „поправката“ и до моментот на пишување на овој напис, обидот да се отвори PDF во прелистувачот резултираше со порака за грешка со текстот „Упс, нешто тргна наопаку!“ Не беше поднесено барање до серверот при обидот да се отвори PDF, но беше поднесено барање кога беше стартуван прелистувачот, што укажува на континуираната можност за преземање на извршна шифра со кршење на правилата на Google Play.
Извор: www.habr.com