Giriş
Martın sonunda biz
Bir müddət əvvəl UC Browser çox aqressiv şəkildə reklam edildi və yayıldı: o, zərərli proqramlardan istifadə edərək istifadəçilərin cihazlarında quraşdırılıb, müxtəlif saytlardan videofayllar adı altında yayılıb (yəni, istifadəçilər elə bilirdilər ki, məsələn, porno videonu yükləyirlər, lakin əvəzinə bu brauzerlə APK əldə etdi), brauzerin köhnəldiyi, həssas olduğu və bu kimi şeylər mesajları olan qorxulu bannerlərdən istifadə etdi. VK-da rəsmi UC Browser qrupunda var
Yazı zamanı UC Brauzerində Google Play-də 500-dan çox quraşdırma var. Bu təsir edicidir - yalnız Google Chrome-da daha çox şey var. Rəylər arasında Google Play-də bəzi tətbiqlərə reklam və yönləndirmə ilə bağlı kifayət qədər çox şikayətlər görə bilərsiniz. Tədqiqatımızın səbəbi bu idi: UC Brauzerinin pis bir şey edib-etmədiyini görmək qərarına gəldik. Və məlum oldu ki, o edir!
Tətbiq kodunda icra edilə bilən kodu yükləmək və işlətmək imkanı aşkar edildi,
Aşağıda yazılan hər şey tədqiqat zamanı Google Play-də mövcud olan UC Brauzer versiyası üçün uyğundur:
package: com.UCMobile.intl
versionName: 12.10.8.1172
versionCode: 10598
sha1 APK-файла: f5edb2243413c777172f6362876041eb0c3a928c
Hücum vektoru
UC Brauzer manifestində siz özünü izah edən adı olan bir xidmət tapa bilərsiniz com.uc.deployment.UpgradeDeployService.
<service android_exported="false" android_name="com.uc.deployment.UpgradeDeployService" android_process=":deploy" />
Bu xidmət işə salındıqda, brauzer POST sorğusu göndərir
Beləliklə, istifadəçi birbaşa brauzerdə PDF açmaq istədikdə, trafikdə aşağıdakı sorğular görünə bilər:
Əvvəlcə POST sorğusu var
PDF və ofis formatlarına baxmaq üçün kitabxanası olan arxiv endirilir. Güman etmək məntiqlidir ki, ilk sorğu sistem haqqında məlumat ötürür (ən azı tələb olunan kitabxananı təmin etmək üçün arxitektura) və buna cavab olaraq brauzer yükləmək lazım olan kitabxana haqqında bəzi məlumatları alır: ünvan və bəlkə də. , başqa bir şey. Problem ondadır ki, bu sorğu şifrələnmişdir.
Sorğu fraqmenti
Cavab fraqmenti
Kitabxananın özü ZIP-də qablaşdırılıb və şifrələnmir.
Trafik deşifrə kodunu axtarın
Gəlin server cavabını deşifrə etməyə çalışaq. Gəlin sinif koduna baxaq com.uc.deployment.UpgradeDeployService: metoddan onStartCommand getmək com.uc.deployment.bx, və ondan 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);
}
Biz burada POST sorğusunun formalaşdığını görürük. 16 baytlıq massivin yaradılmasına və onun doldurulmasına diqqət yetiririk: 0x5F, 0, 0x1F, -50 (=0xCE). Yuxarıdakı sorğuda gördüklərimizlə üst-üstə düşür.
Eyni sinifdə başqa maraqlı metodu olan iç-içə sinfi görə bilərsiniz:
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");
}
}
Metod giriş kimi bayt massivi götürür və sıfır baytın 0x60 və ya üçüncü baytın 0xD0, ikinci baytın isə 1, 11 və ya 0x1F olduğunu yoxlayır. Serverdən gələn cavaba baxırıq: sıfır bayt 0x60, ikincisi 0x1F, üçüncüsü 0x60. Bizə lazım olan kimi səslənir. Sətirlərə əsasən (“yuxarı_decrypt”, məsələn), burada serverin cavabının şifrəsini açacaq bir üsul çağırılmalıdır.
Gəlin üsula keçək gj. Qeyd edək ki, birinci arqument ofset 2-dəki baytdır (yəni bizim vəziyyətimizdə 0x1F), ikincisi isə server cavabı olmadan.
ilk 16 bayt.
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;
}
Aydındır ki, burada biz bir deşifrə alqoritmini seçirik və bizdə olan eyni baytı
0x1F-ə bərabər olan halda, üç mümkün variantdan birini bildirir.
Kodu təhlil etməyə davam edirik. Bir neçə sıçrayışdan sonra özümüzü izahlı adı olan bir üsulla tapırıq decryptBytesByKey.
Burada cavabımızdan daha iki bayt ayrılır və onlardan bir sətir əldə edilir. Aydındır ki, bu şəkildə mesajın şifrəsini açmaq üçün açar seçilir.
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;
}
İrəliyə baxaraq qeyd edirik ki, bu mərhələdə biz hələ bir açar əldə etmirik, yalnız onun "identifikatorunu" alırıq. Açarı əldə etmək bir az daha mürəkkəbdir.
Növbəti üsulda mövcud olanlara daha iki parametr əlavə edilir, onlardan dördü düzəldilir: sehrli nömrə 16, açar identifikator, şifrələnmiş məlumatlar və anlaşılmaz bir sətir (bizim vəziyyətimizdə boş).
public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}
Bir sıra keçidlərdən sonra metoda çatırıq staticBinarySafeDecryptNoB64 interfeysi com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. Əsas proqram kodunda bu interfeysi həyata keçirən siniflər yoxdur. Faylda belə bir sinif var lib/armeabi-v7a/libsgmain.so, bu əslində .so deyil, .jar. Bizi maraqlandıran üsul aşağıdakı kimi həyata keçirilir:
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);
}
//...
}
Burada bizim parametrlər siyahısı daha iki tam ədədlə tamamlanır: 2 və 0.
hər şey, 2 metodda olduğu kimi şifrənin açılması deməkdir doFinal sistem sinfi javax.crypto.Cipher. Və bütün bunlar 10601 nömrəsi ilə müəyyən bir Routerə köçürülür - bu, yəqin ki, komanda nömrəsidir.
Növbəti keçid zəncirindən sonra interfeysi həyata keçirən bir sinif tapırıq IRouterComponent və üsul 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);
}
}
Həm də sinif JNICL Kitabxanası, burada doğma metodun elan edildiyi doCommandNative:
package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}
Bu o deməkdir ki, biz yerli kodda bir üsul tapmalıyıq doCommandNative. Və burada əyləncə başlayır.
Maşın kodunun gizlədilməsi
Faylda libsgmain.so (bu, əslində .jardır və biz yuxarıda bəzi şifrələmə ilə əlaqəli interfeyslərin tətbiqini tapdıq) bir yerli kitabxana var: libsgmainso-6.4.36.so. Biz onu IDA-da açırıq və səhvləri olan bir dəstə dialoq qutusu alırıq. Problem bölmə başlığı cədvəlinin etibarsız olmasıdır. Bu, təhlili çətinləşdirmək üçün məqsədyönlü şəkildə edilir.
Ancaq buna ehtiyac yoxdur: bir ELF faylını düzgün yükləmək və təhlil etmək üçün proqramın başlıq cədvəli kifayətdir. Buna görə də, başlıqdakı müvafiq sahələri sıfırlayaraq bölmə cədvəlini sadəcə silirik.
Faylı yenidən IDA-da açın.
Java virtual maşınına Java kodunda yerli olaraq elan edilmiş metodun tətbiqinin yerli kitabxanada tam olaraq harada yerləşdiyini söyləməyin iki yolu var. Birincisi, ona bir növ adı verməkdir Java_paket_adı_ClassName_methodName.
İkincisi, kitabxananı yükləyərkən onu qeydiyyatdan keçirməkdir (funksiyada JNI_OnLoad)
funksiya çağırışından istifadə etməklə Yerliləri qeydiyyatdan keçirin.
Bizim vəziyyətimizdə birinci üsuldan istifadə etsək, ad belə olmalıdır: Java_com_taobao_wireless_security_adapter_JNICLlibrary_doCommandNative.
İxrac edilən funksiyalar arasında belə bir funksiya yoxdur, yəni zəng axtarmaq lazımdır Yerliləri qeydiyyatdan keçirin.
Gəlin funksiyaya keçək JNI_OnLoad və bu şəkli görürük:
Burda nə baş verir? İlk baxışda funksiyanın başlanğıcı və sonu ARM arxitekturası üçün xarakterikdir. Stəkdəki ilk göstəriş funksiyanın öz işində istifadə edəcəyi registrlərin məzmununu (bu halda R0, R1 və R2), həmçinin funksiyadan qayıtma ünvanını ehtiva edən LR registrinin məzmununu saxlayır. . Son göstəriş saxlanılan registrləri bərpa edir və qaytarma ünvanı dərhal PC reyestrinə yerləşdirilir - beləliklə funksiyadan qayıdır. Ancaq diqqətlə baxsanız, sondan əvvəlki təlimatın yığında saxlanan qaytarma ünvanını dəyişdirdiyini görəcəksiniz. Bundan sonra necə olacağını hesablayaq
kodun icrası. Müəyyən bir ünvan 1xB0 R130-ə yüklənir, ondan 5 çıxarılır, sonra R0-a köçürülür və ona 0x10 əlavə olunur. 0xB13B çıxır. Beləliklə, IDA son göstərişin normal bir funksiyanın qaytarılması olduğunu düşünür, lakin əslində hesablanmış 0xB13B ünvanına gedir.
Burada xatırlatmaq lazımdır ki, ARM prosessorlarının iki rejimi və iki təlimat dəsti var: ARM və Thumb. Ünvanın ən az əhəmiyyətli biti prosessora hansı təlimat dəstinin istifadə olunduğunu bildirir. Yəni, ünvan əslində 0xB13A-dır və ən az əhəmiyyətli bitdən biri Thumb rejimini göstərir.
Bu kitabxanada hər bir funksiyanın əvvəlinə oxşar “adapter” əlavə edilmişdir və
zibil kodu. Onları daha ətraflı nəzərdən keçirməyəcəyik - sadəcə xatırlayırıq
demək olar ki, bütün funksiyaların əsl başlanğıcının bir az uzaqda olduğunu.
Kod açıq şəkildə 0xB13A-a keçmədiyi üçün IDA özü kodun bu yerdə olduğunu tanımırdı. Eyni səbəbdən o, kitabxanadakı kodun çoxunu kod kimi tanımır, bu da təhlili bir qədər çətinləşdirir. BİA-ya deyirik ki, bu koddur və belə olur:
Cədvəl aydın şəkildə 0xB144-dən başlayır. Sub_494C-də nə var?
LR registrində bu funksiyanı çağırarkən əvvəllər qeyd olunan cədvəlin ünvanını alırıq (0xB144). R0-da - bu cədvəldəki indeks. Yəni qiymət cədvəldən götürülür, LR-ə əlavə edilir və nəticə belə olur
getmək üçün ünvan. Gəlin onu hesablamağa çalışaq: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. Alınan ünvana gedirik və sözün əsl mənasında bir neçə faydalı təlimat görürük və yenidən 0xB140-a gedirik:
İndi cədvəldən 0x20 indeksi ilə ofsetdə keçid olacaq.
Cədvəlin ölçüsünə görə kodda belə keçidlər çox olacaq. Sual yaranır ki, ünvanları əl ilə hesablamadan bununla daha avtomatik məşğul olmaq mümkündürmü? Və skriptlər və IDA-da kodu yamaq qabiliyyəti bizə kömək edir:
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"
Kursoru 0xB26A xəttinə qoyun, skripti işə salın və 0xB4B0-a keçidə baxın:
BİA yenə də bu ərazini kod kimi tanımadı. Biz ona kömək edirik və orada başqa bir dizayn görürük:
BLX-dən sonra verilən təlimatlar çox mənalı görünmür, daha çox bir növ yerdəyişmə kimidir. sub_4964-ə baxaq:
Həqiqətən də, burada LR-də yerləşən ünvanda bir dword alınır, bu ünvana əlavə olunur, bundan sonra nəticədə yaranan ünvandakı dəyər götürülür və yığına qoyulur. Həmçinin, LR-ə 4 əlavə olunur ki, funksiyadan qayıtdıqdan sonra bu eyni ofset atlanır. Bundan sonra POP {R1} əmri yığından alınan dəyəri alır. 0xB4BA + 0xEA = 0xB5A4 ünvanında nə olduğuna baxsanız, ünvan cədvəlinə bənzər bir şey görəcəksiniz:
Bu dizaynı düzəltmək üçün koddan iki parametr əldə etməlisiniz: ofset və nəticəni qoymaq istədiyiniz registr nömrəsi. Hər bir mümkün qeydiyyat üçün əvvəlcədən bir kod parçası hazırlamalı olacaqsınız.
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"
Kursoru əvəz etmək istədiyimiz strukturun - 0xB4B2 - başlanğıcına qoyuruq və skripti işə salırıq:
Artıq qeyd olunan strukturlara əlavə olaraq, kodda aşağıdakılar da var:
Əvvəlki vəziyyətdə olduğu kimi, BLX təlimatından sonra bir ofset var:
LR-dən ünvana ofset götürürük, LR-ə əlavə edirik və ora gedirik. 0x72044 + 0xC = 0x72050. Bu dizayn üçün skript olduqca sadədir:
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"
Skriptin icrasının nəticəsi:
Funksiyada hər şey düzəldildikdən sonra IDA-nı onun əsl başlanğıcına yönəldə bilərsiniz. Bütün funksiya kodunu birləşdirəcək və HexRays istifadə edərək dekompilyasiya edilə bilər.
Deşifrə sətirləri
Biz kitabxanada maşın kodunun qarışması ilə məşğul olmağı öyrəndik libsgmainso-6.4.36.so UC Brauzerindən və funksiya kodunu aldı 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;
}
Aşağıdakı sətirlərə daha yaxından nəzər salaq:
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
Funksiyada sub_73E24 sinif adı aydın şəkildə deşifrə edilir. Bu funksiyanın parametrləri olaraq, şifrələnmiş məlumatlara bənzər məlumatlara bir göstərici, müəyyən bir bufer və bir sıra ötürülür. Aydındır ki, funksiya çağırıldıqdan sonra buferdə şifrəsi açılmış xətt olacaq, çünki o, funksiyaya ötürülür. FindClass, ikinci parametr kimi sinif adını qəbul edir. Buna görə də nömrə buferin ölçüsü və ya xəttin uzunluğudur. Gəlin sinif adını deşifrə etməyə çalışaq, o bizə düzgün istiqamətdə getdiyimizi bildirməlidir. Gəlin nə baş verdiyinə daha yaxından nəzər salaq 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;
}
Function sub_7AF78 müəyyən ölçülü bayt massivləri üçün konteyner nümunəsini yaradır (bu konteynerlər üzərində ətraflı dayanmayacağıq). Burada iki belə konteyner yaradılır: biri xətt ehtiva edir "DcO/lcK+h?m3c*q@" (bunun açar olduğunu təxmin etmək asandır), digərində şifrələnmiş məlumatlar var. Sonra, hər iki obyekt funksiyaya ötürülən müəyyən bir quruluşa yerləşdirilir sub_6115C. Gəlin bu strukturda dəyəri 3 olan sahəni də qeyd edək.Görək bu strukturun bundan sonra nə baş verdiyi.
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;
}
Keçid parametri əvvəllər 3 dəyəri təyin edilmiş struktur sahəsidir. 3-cü vəziyyətə baxın: funksiyaya sub_6364C parametrlər əvvəlki funksiyaya əlavə edilmiş strukturdan, yəni açardan və şifrələnmiş məlumatdan ötürülür. Diqqətlə baxsanız sub_6364C, onda siz RC4 alqoritmini tanıya bilərsiniz.
Bizim alqoritmimiz və açarımız var. Gəlin sinif adını deşifrə etməyə çalışaq. Budur, baş verənlər: com/taobao/wireless/security/adapter/JNICLlibrary. Əla! Biz düzgün yoldayıq.
Komanda ağacı
İndi bir problem tapmalıyıq Yerliləri qeydiyyatdan keçirin, bu bizi funksiyaya işarə edəcək doCommandNative. -dən çağırılan funksiyalara baxaq JNI_OnLoad, və biz onu tapırıq 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;
}
Və həqiqətən, adı ilə yerli üsul burada qeydiyyatdan keçmişdir doCommandNative. İndi biz onun ünvanını bilirik. Görək nə edir.
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;
}
Adı ilə təxmin edə bilərsiniz ki, burada tərtibatçıların doğma kitabxanaya köçürməyə qərar verdiyi bütün funksiyaların giriş nöqtəsi var. Bizi 10601 nömrəli funksiya maraqlandırır.
Koddan görə bilərsiniz ki, komanda nömrəsi üç rəqəm yaradır: komanda/10000, komanda % 10000 / 100 и komanda % 10, yəni bizim vəziyyətimizdə 1, 6 və 1. Bu üç ədəd, həmçinin göstərici JNIEnv və funksiyaya ötürülən arqumentlər struktura əlavə edilir və ötürülür. Alınan üç ədəddən (onları N1, N2 və N3 kimi qeyd edək) istifadə edərək komanda ağacı qurulur.
Bu kimi bir şey:
Ağac dinamik şəkildə doldurulur JNI_OnLoad.
Üç rəqəm ağacdakı yolu kodlayır. Ağacın hər yarpağında müvafiq funksiyanın cibli ünvanı var. Açar ana qovşaqdadır. İstifadə olunan bütün strukturları başa düşsəniz, kodda bizə lazım olan funksiyanın ağaca əlavə olunduğu yeri tapmaq çətin deyil (onsuz da kifayət qədər böyük bir məqaləni şişirtməmək üçün onları təsvir etmirik).
Daha çox çaşqınlıq
Trafikin şifrəsini açmalı olan funksiyanın ünvanını aldıq: 0x5F1AC. Ancaq sevinmək üçün hələ tezdir: UC Browser-in tərtibatçıları bizim üçün daha bir sürpriz hazırladılar.
Java kodunda formalaşmış massivdən parametrləri aldıqdan sonra alırıq
0x4D070 ünvanındakı funksiyaya. Və burada bizi başqa bir kod gizlətməsi gözləyir.
R7 və R4-ə iki indeks qoyuruq:
İlk indeksi R11-ə keçirik:
Cədvəldən ünvan əldə etmək üçün indeksdən istifadə edin:
Birinci ünvana getdikdən sonra R4-də olan ikinci indeks istifadə olunur. Cədvəldə 230 element var.
Bununla bağlı nə etməli? Siz IDA-ya bunun bir keçid olduğunu deyə bilərsiniz: Redaktə et -> Digər -> Keçid deyimini təyin et.
Nəticə kod dəhşətlidir. Lakin, onun cəngəlliyindən keçərək, artıq bizə tanış olan bir funksiyaya zəng görə bilərsiniz sub_6115C:
3-cü vəziyyətdə RC4 alqoritmindən istifadə edərək şifrənin açılması olan bir keçid var idi. Və bu zaman funksiyaya ötürülən struktur ötürülən parametrlərdən doldurulur doCommandNative. Gəlin orada nələrimiz olduğunu xatırlayaq magicInt dəyəri ilə 16. Biz müvafiq işə baxırıq - və bir neçə keçiddən sonra alqoritmin müəyyən edilə biləcəyi kodu tapırıq.
Bu AES!
Alqoritm mövcuddur, onun parametrlərini əldə etmək qalır: rejim, açar və ehtimal ki, başlatma vektoru (onun mövcudluğu AES alqoritminin iş rejimindən asılıdır). Onlarla struktur funksiya çağırışından əvvəl haradasa formalaşmalıdır sub_6115C, lakin kodun bu hissəsi xüsusilə yaxşı qarışıqdır, ona görə də şifrəni açmaq funksiyasının bütün parametrlərinin fayla atılması üçün kodu yamaq etmək ideyası yaranır.
Yamaq
Bütün yamaq kodunu montaj dilində əl ilə yazmamaq üçün siz Android Studio proqramını işə sala, şifrləmə funksiyamızla eyni giriş parametrlərini alan funksiyanı orada yaza və fayla yaza, sonra kompilyatorun göstərəcəyi kodu kopyalayıb yapışdıra bilərsiniz. yaratmaq.
UC Brauzer komandasındakı dostlarımız da kodun əlavə edilməsinin rahatlığının qayğısına qaldılar. Unutmayaq ki, hər bir funksiyanın əvvəlində asanlıqla hər hansı digəri ilə əvəz edilə bilən zibil kodumuz var. Çox rahatdır 🙂 Bununla belə, hədəf funksiyasının əvvəlində bütün parametrləri faylda saxlayan kod üçün kifayət qədər yer yoxdur. Mən onu hissələrə bölməli və qonşu funksiyaların zibil bloklarından istifadə etməli oldum. Ümumilikdə dörd hissədən ibarət idi.
Birinci hissə:
ARM arxitekturasında ilk dörd funksiya parametri R0-R3 registrlərindən, qalanları, əgər varsa, stekdən keçir. LR reyestri geri qayıtma ünvanını daşıyır. Bütün bunları yadda saxlamaq lazımdır ki, biz onun parametrlərini atdıqdan sonra funksiya işləyə bilsin. Biz həmçinin prosesdə istifadə edəcəyimiz bütün registrləri saxlamalıyıq, ona görə də PUSH.W {R0-R10,LR} edirik. R7-də stek vasitəsilə funksiyaya ötürülən parametrlər siyahısının ünvanını alırıq.
Funksiyadan istifadə etməklə fopen faylı açaq /data/local/tmp/aes "ab" rejimində
yəni əlavə üçün. R0-da fayl adının ünvanını, R1-də rejimi göstərən xəttin ünvanını yükləyirik. Və burada zibil kodu bitir, ona görə də növbəti funksiyaya keçirik. Onun işləməyə davam etməsi üçün biz başlanğıcda zibildən yan keçərək funksiyanın real koduna keçidi qoyuruq və zibil yerinə yamağın davamını əlavə edirik.
Zəng edirik fopen.
Funksiyanın ilk üç parametri aes növü var int. Başlanğıcda registrləri stekdə saxladığımız üçün funksiyanı sadəcə ötürə bilərik yaz onların ünvanları yığında.
Sonra, məlumat ölçüsünü və açar, başlatma vektoru və şifrələnmiş məlumat üçün məlumatlara işarə edən üç strukturumuz var.
Sonda faylı bağlayın, registrləri bərpa edin və idarəetməni real funksiyaya köçürün aes.
Yamaqlanmış kitabxanası olan APK toplayırıq, onu imzalayırıq, cihaza/emulatora yükləyirik və işə salırıq. Görürük ki, bizim zibilimiz yaradılır və orada çoxlu məlumatlar yazılır. Brauzer şifrələmədən təkcə trafik üçün istifadə etmir və bütün şifrələmə sözügedən funksiyadan keçir. Amma nədənsə lazımi məlumatlar yoxdur və tələb olunan sorğu trafikdə görünmür. UC Brauzerinin lazımi sorğu ilə çıxış etməsini gözləməmək üçün gəlin əvvəllər alınan serverdən şifrələnmiş cavabı götürək və tətbiqi yenidən yamaq edək: əsas fəaliyyətin onCreate-ə deşifrəni əlavə edin.
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
Biz yığırıq, imzalayırıq, quraşdırırıq, işə salırıq. Metod null qaytardığına görə biz NullPointerException alırıq.
Kodun sonrakı təhlili zamanı maraqlı sətirləri deşifrə edən funksiya aşkar edilmişdir: “META-INF/” və “.RSA”. Deyəsən, tətbiq öz sertifikatını yoxlayır. Və ya hətta ondan açarlar yaradır. Sertifikatla baş verənlərlə həqiqətən məşğul olmaq istəmirəm, ona görə də ona düzgün sertifikat verəcəyik. Şifrələnmiş xətti düzəldək ki, “META-INF/” əvəzinə “BLABLINF/” əldə edək, APK-da bu adda bir qovluq yaradın və ora dələ brauzeri sertifikatını əlavə edin.
Biz yığırıq, imzalayırıq, quraşdırırıq, işə salırıq. Bingo! Açar bizdə!
MitM
Açar və açara bərabər başlanğıc vektoru aldıq. CBC rejimində server cavabının şifrəsini açmağa çalışaq.
Arxiv URL-ni, MD5-ə bənzər bir şey, “extract_unzipsize” və bir nömrəni görürük. Yoxlayırıq: arxivin MD5-i eynidir, açılmamış kitabxananın ölçüsü eynidir. Bu kitabxananı yamaq və brauzerə verməyə çalışırıq. Yamaqlanmış kitabxanamızın yükləndiyini göstərmək üçün biz “PWNED!” mətni ilə SMS yaratmaq niyyətini işə salacağıq. Serverdən iki cavabı əvəz edəcəyik:
Brauzer arxivi bir neçə dəfə yükləməyə çalışır, bundan sonra xəta verir. Görünür nəsə
bəyənmir. Bu qaranlıq formatın təhlili nəticəsində məlum oldu ki, server arxivin ölçüsünü də ötürür:
LEB128-də kodlaşdırılmışdır. Yamadan sonra kitabxana ilə arxivin ölçüsü bir az dəyişdi, buna görə də brauzer arxivin əyri şəkildə yükləndiyini hesab etdi və bir neçə cəhddən sonra xəta verdi.
Arxivin ölçüsünü tənzimləyirik... Və – qələbə! 🙂 Nəticə videodadır.
Nəticələr və inkişaf etdirici reaksiyası
Eyni şəkildə, hakerlər zərərli kitabxanaları yaymaq və işə salmaq üçün UC Brauzerinin etibarsız xüsusiyyətindən istifadə edə bilər. Bu kitabxanalar brauzer kontekstində işləyəcək, ona görə də onun bütün sistem icazələrini alacaqlar. Nəticədə, fişinq pəncərələrini göstərmək imkanı, həmçinin məlumat bazasında saxlanılan loginlər, parollar və kukilər daxil olmaqla, narıncı Çin dələsinin iş fayllarına daxil olmaq imkanı.
Biz UC Browser-in tərtibatçıları ilə əlaqə saxladıq və tapdığımız problem barədə onlara məlumat verdik, zəifliyi və onun təhlükəsini qeyd etməyə çalışdıq, lakin onlar bizimlə heç nə müzakirə etmədilər. Bu arada, brauzer təhlükəli xüsusiyyətini açıq şəkildə nümayiş etdirməyə davam etdi. Ancaq zəifliyin təfərrüatlarını açıqladıqdan sonra, əvvəlki kimi buna məhəl qoymamaq artıq mümkün deyildi. 27 mart idi
HTTPS vasitəsilə serverə daxil olan UC Browser 12.10.9.1193-ün yeni versiyası buraxıldı:
Bundan əlavə, "düzeltmədən" sonra və bu məqalənin yazıldığı vaxta qədər brauzerdə PDF-i açmağa çalışmaq "Oops, bir şey səhv oldu!" Mətni ilə səhv mesajı ilə nəticələndi. PDF-i açmağa çalışarkən serverə sorğu göndərilmədi, lakin brauzer işə salındıqda sorğu verildi ki, bu da Google Play qaydalarını pozaraq icra edilə bilən kodu yükləmək imkanının davam etdiyinə işarə edir.
Mənbə: www.habr.com