
Pendahuluan
Pada akhir Maret kami
melaporkan potensi tersembunyi untuk mengunduh dan menjalankan kode yang tidak diverifikasi di UC Browser. Hari ini kita akan memeriksa secara terperinci bagaimana itu terjadi dan bagaimana peretas dapat menggunakannya.
Beberapa waktu yang lalu, UC Browser dipromosikan dan didistribusikan dengan cukup agresif. Itu diinstal pada perangkat oleh malware, didistribusikan melalui situs web dengan kedok file video (mis., Pengguna berpikir mereka mengunduh pornografi atau sesuatu, tetapi sebaliknya mendapatkan file APK dengan browser ini), diiklankan menggunakan spanduk mengkhawatirkan tentang browser pengguna yang ketinggalan zaman atau rentan. Grup UC Browser VK resmi memiliki
topik di mana pengguna dapat mengeluh tentang iklan palsu dan banyak pengguna memberikan contoh. Pada tahun 2016, bahkan ada
iklan di Rusia (ya, iklan browser yang memblokir iklan).
Saat kami menulis artikel ini, UC Browser diinstal 500.000.000 kali dari Google Play. Ini mengesankan karena hanya Google Chrome yang berhasil melampaui itu. Di antara ulasan tersebut, Anda dapat melihat banyak keluhan pengguna tentang iklan dan dialihkan ke aplikasi lain di Google Play. Ini adalah alasan untuk penelitian kami: kami ingin melihat apakah UC Browser melakukan sesuatu yang salah. Dan itu! Aplikasi ini dapat mengunduh dan menjalankan kode yang dapat dieksekusi, yang
melanggar kebijakan Google Play untuk penerbitan aplikasi . Dan UC Browser tidak hanya mengunduh kode yang dapat dieksekusi; ini tidak aman, yang dapat digunakan untuk serangan MitM. Mari kita lihat apakah kita bisa menggunakannya dengan cara ini.
Segala sesuatu yang mengikuti berlaku untuk versi UC Browser yang didistribusikan melalui Google Play pada saat penelitian kami:
package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-file: f5edb2243413c777172f6362876041eb0c3a928c
Vektor serangan
Manifes UC Browser berisi layanan dengan nama
com.uc.deployment.UpgradeDeployService .
<service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" />
Ketika layanan ini diluncurkan, browser membuat permintaan POST ke
puds.ucweb.com/upgrade/index.xhtml yang dapat dilihat dalam lalu lintas untuk beberapa waktu setelah peluncuran. Sebagai tanggapan, browser dapat menerima perintah untuk mengunduh pembaruan apa pun atau modul baru. Selama analisis kami, kami tidak pernah menerima perintah seperti itu dari server, tetapi kami perhatikan bahwa ketika mencoba membuka file PDF di browser, itu mengulangi permintaan ke alamat di atas, kemudian mengunduh perpustakaan asli. Untuk mensimulasikan serangan, kami memutuskan untuk menggunakan fitur UC Browser ini - kemampuan untuk membuka file PDF menggunakan pustaka asli - tidak ada dalam file APK, tetapi dapat diunduh dari Internet. Secara teknis, UC Browser dapat mengunduh sesuatu tanpa izin pengguna ketika diberikan respons yang sesuai dengan permintaan yang dikirim saat startup. Tetapi untuk ini kita perlu mempelajari protokol interaksi dengan server secara lebih rinci, jadi kami pikir lebih mudah untuk hanya menghubungkan dan mengedit respons dan kemudian mengganti pustaka yang diperlukan untuk membuka file PDF dengan sesuatu yang berbeda.
Jadi, ketika pengguna ingin membuka file PDF langsung di browser, lalu lintas dapat berisi permintaan berikut:

Pertama, ada permintaan POST ke
puds.ucweb.com/upgrade/index.xhtml , kemudian pustaka terkompresi untuk melihat file PDF dan dokumen kantor diunduh. Secara logis, kita dapat mengasumsikan bahwa permintaan pertama mengirim informasi tentang sistem (setidaknya arsitektur, karena server perlu memilih perpustakaan yang sesuai), dan server merespons dengan beberapa informasi tentang perpustakaan yang perlu diunduh, seperti alamatnya dan mungkin sesuatu yang lain. Masalahnya adalah bahwa permintaan ini dienkripsi.
Perpustakaan dikompresi dalam file ZIP dan tidak dienkripsi.

Mencari kode dekripsi lalu lintas
Mari kita coba dan dekripsi respons server. Lihatlah kode
kelas com.uc.deployment.UpgradeDeployService : dari
metode onStartCommand , kami menavigasi ke
com.uc.deployment.bx , dan kemudian ke
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(); mb(v3_4); h v4_2 = new h(); mb(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.baacgetCpuArch())); v3_2.add(g.fs("cpu_arch", com.uc.baacPb())); String v4_3 = com.uc.baacPd(); 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.baacPc())); 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.baacJl()))); v3_2.add(g.fs("ram_1", String.valueOf(com.uc.baahPo()))); v3_2.add(g.fs("totalram", String.valueOf(com.uc.baahOL()))); 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 = gi(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.dci 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.dci$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); mb(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); }
Kita bisa melihat di sinilah permintaan POST dibuat. Lihat array 16-byte yang berisi: 0x5F, 0, 0x1F, -50 (= 0xCE). Nilai-nilainya sama seperti dalam permintaan di atas.
Kelas yang sama berisi kelas bersarang dengan metode lain yang menarik:
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 = gj(arg11[2], v7); } if(v7 == null) { goto label_57; } if(v5 != 0) { v1_1 = gP(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 = gb(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"); } }
Metode ini menerima input array byte dan memeriksa apakah byte nol 0x60, atau byte ketiga 0xD0 dan jika byte kedua adalah 1, 11, atau 0x1F. Periksa respons server: nol byte adalah 0x60, byte kedua adalah 0x1F, byte ketiga adalah 0x60. Sepertinya yang kita butuhkan. Dilihat oleh string ("up_decrypt", misalnya), suatu metode seharusnya dipanggil di sini untuk mendekripsi respons server. Sekarang mari kita lihat metode gj. Perhatikan bahwa argumen pertama adalah byte pada offset 2 (yaitu, 0x1F dalam kasus kami), dan yang kedua adalah respons server tanpa 16 byte pertama.
public static byte[] j(int arg1, byte[] arg2) { if(arg1 == 1) { arg2 = cc(arg2, c.adu); } else if(arg1 == 11) { arg2 = m.aF(arg2); } else if(arg1 != 0x1F) { } else { arg2 = EncryptHelper.decrypt(arg2); } return arg2; }
Jelas, itu adalah memilih algoritma dekripsi, dan byte yang dalam kasus kami sama dengan 0x1F menunjukkan satu dari tiga opsi yang mungkin.
Mari kita kembali ke analisis kode. Setelah beberapa lompatan, kita sampai pada metode dengan nama tanda,
decryptBytesByKey . Sekarang, dua byte lagi dipisahkan dari respons kami dan membentuk string. Jelas ini adalah bagaimana kunci dipilih untuk mendekripsi pesan.
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];
Melompat sedikit ke depan, perhatikan bahwa pada tahap ini, itu hanya pengidentifikasi kunci, bukan kunci itu sendiri. Pemilihan kunci akan menjadi sedikit lebih rumit.
Pada metode selanjutnya, dua parameter lagi ditambahkan ke yang sudah ada, jadi kami mendapatkan total empat. Angka ajaib 16, pengidentifikasi kunci, data terenkripsi, dan string ditambahkan di sana untuk beberapa alasan (kosong dalam kasus kami).
public final byte[] l(String keyId, byte[] encrypted) throws SecException { return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, ""); }
Setelah serangkaian lompatan, kita melihat metode
staticBinarySafeDecryptNoB64 dari
com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent interface. Kode aplikasi utama tidak memiliki kelas yang mengimplementasikan antarmuka ini. Kelas ini terkandung dalam file
lib / armeabi-v7a / libsgmain.so , yang sebenarnya bukan .SO, melainkan .JAR. Metode yang kami minati diimplementasikan sebagai berikut:
package com.alibaba.wireless.security.ai;
Di sini, daftar parameter kami dilengkapi dengan dua bilangan bulat lagi: 2 dan 0. Rupanya, 2 berarti dekripsi, seperti pada
metode doFinal dari kelas
sistem javax.crypto.Cipher . Kemudian, informasi ini ditransmisikan ke Router tertentu bersama dengan nomor 10601, yang tampaknya adalah nomor perintah.
Setelah rantai lompatan berikutnya, kami menemukan kelas yang mengimplementasikan antarmuka
RouterComponent dan metode
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); } }
Ada juga kelas
Perpustakaan JNIC , di mana metode
doCommandNative asli dinyatakan:
package com.taobao.wireless.security.adapter; public class JNICLibrary { public static native Object doCommandNative(int arg0, Object[] arg1); }
Jadi, kita perlu menemukan metode
doCommandNative dalam kode asli. Di situlah kesenangan dimulai.
Kebingungan kode mesin
Ada satu pustaka asli di file
libsgmain.so (yang sebenarnya adalah file .JAR dan, seperti yang kami katakan di atas, mengimplementasikan beberapa antarmuka terkait enkripsi):
libsgmainso-6.4.36.so . Kami memuatnya di IDA dan mendapatkan banyak dialog dengan pesan kesalahan. Masalahnya adalah bahwa tabel header bagian tidak valid. Ini dilakukan dengan sengaja untuk mempersulit analisis.

Tapi bagaimanapun juga kita tidak benar-benar membutuhkannya. Tabel tajuk program sudah cukup untuk memuat file ELF dengan benar dan menganalisisnya. Jadi kami cukup menghapus tabel header bagian, membatalkan bidang yang sesuai di header.

Kemudian kita buka file di IDA lagi.
Kami memiliki dua cara untuk memberi tahu mesin virtual Java persis di mana pustaka asli berisi implementasi metode yang dinyatakan sebagai asli dalam kode Java. Yang pertama adalah untuk memberikan nama seperti ini:
Java_package_name_ClassName_methodName . Yang kedua adalah mendaftarkannya saat memuat perpustakaan (dalam fungsi JNI_OnLoad) dengan memanggil fungsi RegisterNatives. Dalam kasus kami, jika Anda menggunakan metode pertama, namanya harus seperti ini:
Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . Daftar fungsi yang diekspor tidak mengandung nama ini, yang berarti kita perlu mencari RegisterNatives. Jadi, kita pergi ke fungsi JNI_OnLoad dan melihat yang berikut:

Apa yang terjadi di sini Sekilas, awal dan akhir fungsi adalah tipikal arsitektur ARM. Instruksi pertama mendorong isi register yang akan digunakan fungsi ke stack (dalam hal ini, R0, R1, dan R2), serta isi register LR dengan alamat pengembalian fungsi. Instruksi terakhir mengembalikan register yang disimpan dan menempatkan alamat pengirim ke register PC, sehingga kembali dari fungsinya. Tetapi jika kita melihat lebih dekat, kita mungkin memperhatikan bahwa instruksi kedua dari belakang mengubah alamat pengirim, disimpan di tumpukan. Mari menghitung apa yang akan terjadi ketika kode dieksekusi. Alamat 0xB130 memuat dalam R1, memiliki 5 dikurangi darinya, kemudian dipindahkan ke R0 dan menerima tambahan 0x10. Pada akhirnya, itu sama dengan 0xB13B. Dengan demikian, IDA berpikir bahwa instruksi terakhir melakukan pengembalian fungsi normal, sementara pada kenyataannya, ia melakukan transfer ke alamat terhitung 0xB13B.
Sekarang mari kita ingatkan Anda bahwa prosesor ARM memiliki dua mode dan dua set instruksi - ARM dan Thumb. Bit orde rendah dari alamat menentukan instruksi yang ditetapkan oleh prosesor yang akan digunakan. Yaitu, alamatnya sebenarnya 0xB13A, sedangkan nilai dalam bit orde rendah menunjukkan mode Jempol.
"Adaptor" yang serupa dan beberapa sampah semantik ditambahkan ke awal setiap fungsi di perpustakaan ini. Tapi kami tidak akan membahasnya secara rinci. Ingatlah bahwa awal sebenarnya dari hampir semua fungsi sedikit lebih jauh.
Karena tidak ada transisi eksplisit ke 0xB13A dalam kode, IDA tidak dapat mengenali bahwa ada kode di sana. Untuk alasan yang sama, itu tidak mengenali sebagian besar kode di perpustakaan sebagai kode, yang membuat menganalisa sedikit lebih rumit. Jadi, kami memberi tahu IDA bahwa ada kode, dan inilah yang terjadi:

Mulai dari 0xB144, kami memiliki tabel yang jelas. Tapi bagaimana dengan sub_494C?

Saat memanggil fungsi ini dalam register LR, kami mendapatkan alamat tabel yang disebutkan di atas (0xB144). R0 berisi indeks dalam tabel ini. Artinya, kita mengambil nilai dari tabel, menambahkannya ke LR, dan mendapatkan alamat yang harus kita tuju. Mari kita coba hitung: 0xB144 + [0xB144 + 8 * 4] = 0xB144 + 0x120 = 0xB264. Kami menavigasi ke alamat ini dan melihat beberapa instruksi yang berguna, kemudian pergi ke 0xB140:

Sekarang akan ada transisi diimbangi dengan indeks 0x20 dari tabel.
Dilihat dari ukuran tabel, akan ada banyak transisi dalam kode. Jadi kami ingin menangani ini secara otomatis dan menghindari penghitungan alamat secara manual. Dengan demikian, skrip dan kemampuan untuk menambal kode di IDA datang untuk menyelamatkan kami:
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:
Kami meletakkan kursor pada string 0xB26A, menjalankan skrip, dan melihat transisi ke 0xB4B0:

Sekali lagi, IDA tidak mengenali tempat ini sebagai kode. Kami membantu dan melihat struktur lain di sana:

Instruksi yang mengikuti BLX tidak tampak sangat berarti; mereka lebih seperti semacam offset. Kami melihat sub_4964:

Memang, dibutuhkan DWORD di alamat dari LR, menambahkannya ke alamat ini, lalu mengambil nilai di alamat yang dihasilkan dan menyimpannya di tumpukan. Selain itu, ia menambahkan 4 ke LR untuk melompati offset yang sama ini setelah kembali dari fungsi. Kemudian perintah POP {R1} mengambil nilai yang dihasilkan dari stack. Melihat apa yang terletak di alamat 0xB4BA + 0xEA = 0xB5A4, kita dapat melihat sesuatu yang mirip dengan tabel alamat:

Untuk menambal struktur ini, kita perlu mendapatkan dua parameter dari kode: offset dan nomor register tempat kita ingin mendorong hasilnya. Kami harus menyiapkan sepotong kode terlebih dahulu untuk setiap kemungkinan pendaftaran.
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
Kami meletakkan kursor di awal struktur yang ingin kami ganti (yaitu 0xB4B2) dan menjalankan skrip:

Selain struktur yang telah disebutkan, kode ini meliputi:

Seperti pada kasus sebelumnya, ada offset setelah instruksi BLX:

Kami mengambil offset di alamat dari LR, menambahkannya ke LR, dan menavigasi di sana. 0x72044 + 0xC = 0x72050. Script untuk struktur ini cukup sederhana:
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:
Hasil menjalankan skrip:

Setelah kita menambal semua yang ada di fungsi ini, kita bisa mengarahkan IDA ke awal sebenarnya. Ini akan mengumpulkan seluruh kode fungsi sepotong demi sepotong dan kita akan dapat mendekompilasi menggunakan HexRays.
Mendekripsi string
Kami telah belajar cara menangani kebingungan kode mesin di
libsgmainso-6.4.36.so perpustakaan dari UC Browser dan memperoleh kode fungsi
JNI_OnLoad .
int __fastcall real_JNI_OnLoad(JavaVM *vm) { int result;
Mari kita lihat string berikut:
sub_73E24(&unk_83EA6, &v6, 49); clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
Cukup jelas bahwa fungsi
sub_73E24 mendekripsi nama kelas. Parameter fungsi ini berisi pointer ke data yang terlihat mirip dengan yang dienkripsi, semacam buffer, dan angka. Jelas, string yang didekripsi akan berada di buffer setelah panggilan ke fungsi, karena buffer pergi ke fungsi
FindClass , yang menerima nama kelas yang sama dengan parameter kedua. Jadi angkanya adalah ukuran buffer atau panjang string. Mari kita coba mendekripsi nama kelas. Itu harus menunjukkan jika kita pergi ke arah yang benar. Mari kita lihat lebih dekat apa yang terjadi di
sub_73E24 .
int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size) { int v4;
Fungsi sub_7AF78 membuat instance wadah untuk array byte dengan ukuran yang ditentukan (kami tidak akan fokus pada mereka secara detail). Dua wadah tersebut dibuat di sini: satu berisi string "
DcO / lcK + h? M3c * q @ " (mudah ditebak bahwa ini adalah kuncinya), yang lain memiliki data terenkripsi. Kedua objek kemudian ditempatkan dalam struktur tertentu, yang mentransfer ke fungsi
sub_6115C . Kami juga dapat mencatat bahwa struktur ini berisi bidang dengan nilai 3. Mari kita lihat apa yang terjadi selanjutnya.
int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2) { int v3;
Bidang dengan nilai yang sebelumnya ditetapkan 3 ditransisikan sebagai parameter sakelar. Mari kita lihat kasus 3 - parameter yang ditambahkan fungsi sebelumnya ke struktur (yaitu, kunci dan data terenkripsi) ditransisikan ke fungsi sub_6364C. Jika kita melihat lebih dekat pada sub_6364C, kita dapat mengenali algoritma RC4.
Jadi kami memiliki algoritma dan kunci. Mari kita coba mendekripsi nama kelas. Inilah yang kami punya: com / taobao / nirkabel / keamanan / adaptor / JNICLibrary. Cemerlang! Kami berada di jalur yang benar.
Pohon perintah
Sekarang kita perlu menemukan panggilan ke
RegisterNatives , yang akan mengarahkan kita ke fungsi
doCommandNative . Jadi kita melihat fungsi-fungsi yang dipanggil dari
JNI_OnLoad , dan menemukannya di
sub_B7B0 :
int __fastcall sub_B7F6(JNIEnv *env, jclass clazz) { char signature[41];
Dan memang, metode asli dengan nama
doCommandNative terdaftar di sini. Sekarang kita tahu alamatnya. Mari kita lihat apa fungsinya.
int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args) { int v5;
Nama ini menunjukkan bahwa itu adalah titik masuk untuk semua fungsi yang ditransfer pengembang ke perpustakaan asli. Kami secara khusus tertarik pada nomor fungsi 10601.
Dari kode, kita dapat melihat bahwa nomor perintah memberi kita tiga angka: perintah / 10000, perintah% 10000/100, dan perintah% 10 (dalam kasus kami, 1, 6, dan 1). Tiga angka ini, serta penunjuk ke JNIEnv dan argumen yang ditransfer ke fungsi, membentuk struktur dan diteruskan. Dengan tiga angka ini (kami akan menyatakannya N1, N2, dan N3), pohon perintah dibangun. Sesuatu seperti ini:

Pohon dibuat secara dinamis di JNI_OnLoad. Tiga angka mengkodekan jalur di pohon. Setiap daun pohon berisi alamat xorred fungsi yang sesuai. Kuncinya ada di simpul induk. Sangat mudah untuk menemukan tempat dalam kode di mana fungsi yang kita butuhkan ditambahkan ke pohon jika kita memahami semua struktur di sana (kita tidak akan menghabiskan waktu menggambarkannya dalam artikel ini).
Lebih banyak kebingungan
Kami mendapat alamat fungsi yang seharusnya mendekripsi lalu lintas: 0x5F1AC. Tapi masih terlalu dini untuk bersantai - pengembang UC Browser punya kejutan lain untuk kita.
Setelah menerima parameter dari array dalam kode Java, kita pergi ke fungsi di 0x4D070. Jenis lain dari kebingungan kode sudah menunggu.
Kami kemudian mendorong dua indeks di R7 dan R4:

Indeks pertama bergerak ke R11:

Kami menggunakan indeks ini untuk mendapatkan alamat dari tabel:

Setelah mentransfer ke alamat pertama, kami menggunakan indeks kedua dari R4. Tabel ini berisi 230 elemen.
Apa yang kita lakukan dengannya? Kita bisa memberi tahu IDA bahwa itu adalah semacam saklar: Edit -> Lainnya -> Tentukan idiom sakelar.

Kode yang dihasilkan sangat menghebohkan.
Namun, kita dapat melihat panggilan ke fungsi sub_6115C yang familier dalam kusutnya :
Ada parameter sakelar dengan dekripsi RC4 dalam kasus 3. Dalam kasus ini, struktur yang mentransfer ke fungsi diisi dengan parameter yang ditransfer ke doCommandNative. Kami ingat bahwa kami memiliki magicInt di sana dengan nilai 16. Kami melihat kasus yang sesuai dan setelah beberapa transisi, menemukan kode yang membantu kami mengidentifikasi algoritma.
Itu AES!Kami memiliki algoritme dan hanya perlu mendapatkan parameternya, seperti mode, kunci, dan (mungkin) vektor inisialisasi (keberadaannya tergantung pada mode operasi algoritme AES). Struktur yang memuatnya harus dibuat di suatu tempat sebelum memanggil fungsi sub_6115C. Tetapi karena bagian kode ini dikaburkan dengan sangat baik, kami memutuskan untuk menambal kode tersebut sehingga semua parameter fungsi dekripsi dapat dibuang ke file.Tambalan
Jika Anda tidak ingin menulis secara manual semua kode tambalan dalam bahasa rakitan, Anda dapat menjalankan Android Studio, memberi kode fungsi yang menerima parameter yang sama dengan fungsi dekripsi kami dan menulis ke file, lalu menyalin kode yang dihasilkan yang dihasilkan oleh kompilerTeman baik kami dari tim UC Browser juga "memastikan" kenyamanan menambahkan kode. Kami ingat bahwa kami memiliki kode sampah di awal setiap fungsi, yang dapat dengan mudah diganti dengan kode lain. Sangat mudah :) Namun, tidak ada cukup ruang di awal fungsi target untuk kode yang menyimpan semua parameter ke file. Kami harus membaginya menjadi beberapa bagian dan menggunakan blok sampah dari fungsi yang berdekatan. Kami mendapat empat bagian secara total.Bagian satu:
Empat parameter fungsi pertama dalam arsitektur ARM ditunjukkan dalam register R0-R3, sedangkan sisanya, jika ada, melewati stack. Register LR menunjukkan alamat kembali. Kita perlu menyimpan semua data ini agar fungsinya dapat berfungsi setelah kita membuang parameternya. Kita juga perlu menyimpan semua register yang kita gunakan dalam proses, jadi kita menggunakan PUSH.W {R0-R10, LR}. Di R7, kami mendapatkan alamat daftar parameter yang ditransfer ke fungsi melalui stack.Dengan menggunakan fungsi fopen, kita membuka file / data / local / tmp / aes dalam mode "ab" (sehingga kita dapat menambahkan sesuatu). Kami kemudian memuat alamat nama file di R0 dan alamat string yang menunjukkan mode di R1. Di sinilah kode sampah berakhir, jadi kami menavigasi ke fungsi berikutnya. Karena kami ingin terus bekerja, kami menempatkan transisi ke kode fungsi yang sebenarnya di awal, sebelum sampah, dan mengganti sampah dengan sisa tambalan.
Kami kemudian memanggil fopen.Tiga parameter pertama dari fungsi aes adalah dari tipe int. Karena kami mendorong register ke stack di awal, kami dapat dengan mudah mentransfer alamat mereka di stack ke fungsi fwrite.
Selanjutnya, kami memiliki tiga struktur yang menunjukkan ukuran data dan berisi pointer ke data untuk kunci, vektor inisialisasi, dan data terenkripsi.
Pada akhirnya, kami menutup file, mengembalikan register, dan memberikan kontrol kembali ke fungsi aes yang sebenarnya.Kami mengkompilasi file APK dengan pustaka yang ditambal, menandatanganinya, mengunduhnya ke perangkat atau emulator, dan menjalankannya. Sekarang kita melihat dump telah dibuat dengan banyak data. Browser tidak hanya mendekripsi lalu lintas, tetapi data lain juga, dan semua dekripsi dilakukan melalui fungsi ini. Untuk beberapa alasan, kami tidak melihat data yang kami butuhkan, dan permintaan yang kami harapkan tidak terlihat dalam lalu lintas. Mari kita lewati menunggu sampai UC Browser memiliki kesempatan untuk membuat permintaan ini dan mengambil respons terenkripsi yang diperoleh sebelumnya dari server untuk menambal aplikasi lagi. Kami akan menambahkan dekripsi ke onCreate dari aktivitas utama. 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
Kami mengkompilasinya, menandatangani, menginstal, dan menjalankan. Jadi, kami mendapatkan NullPointerException karena metode ini mengembalikan nilai nol.Setelah menganalisis kode lebih lanjut, kami menemukan fungsi dengan string yang agak menarik: "META-INF /" dan ".RSA". Sepertinya aplikasi memverifikasi sertifikatnya, atau bahkan menghasilkan kunci darinya. Kami tidak benar-benar ingin menggali apa yang terjadi dengan sertifikat, jadi mari kita berikan sertifikat yang benar. Kami akan menambal string terenkripsi, jadi alih-alih “META-INF /” kami mendapatkan “BLABLINF /”, kami membuat folder dengan nama ini di file APK, dan menyimpan sertifikat browser di dalamnya.Kami mengkompilasinya, menandatangani, menginstal, dan menjalankan. Bingo! Kami punya kuncinya!Mitm
Sekarang kita punya kunci dan vektor inisialisasi yang sama. Mari kita coba mendekripsi respons server dalam mode CBC.
Kami melihat URL arsip, sesuatu seperti MD5, "extract_unzipsize", dan sebuah angka. Mari kita periksa. MD5 dari arsip adalah sama; ukuran pustaka unzip adalah sama. Sekarang kita akan mencoba untuk menambal pustaka ini dan mengirimkannya ke browser. Untuk menunjukkan bahwa pustaka yang ditambal kami telah dimuat, kami akan membangun Intent untuk membuat pesan teks "PWNED!" Kami akan mengganti dua respons dari server: puds.ucweb.com/upgrade/index.xhtml dan yang meminta unduhan arsip. Pada yang pertama, kami mengganti MD5 (ukurannya tetap sama setelah membuka ritsleting); di yang kedua, kami mengirim arsip dengan pustaka yang ditambal.Peramban melakukan beberapa upaya untuk mengunduh arsip, menghasilkan kesalahan. Rupanya, sesuatu yang mencurigakan sedang terjadi di sana. Menganalisis format aneh ini, kami menemukan bahwa server juga mentransmisikan ukuran arsip:
Ini dikodekan LEB128. Patch sedikit mengubah ukuran perpustakaan terkompresi, sehingga browser memutuskan bahwa arsip rusak saat diunduh dan menampilkan kesalahan setelah beberapa upaya.Jadi kami memperbaiki ukuran arsip dan ... voila! :) Lihat hasilnya di video.Konsekuensi dan respons pengembang
Dengan cara yang sama, peretas dapat menggunakan fitur UC Browser yang tidak aman ini untuk mendistribusikan dan meluncurkan perpustakaan jahat. Pustaka ini akan berfungsi dalam konteks browser, menghasilkan hak istimewa sistem penuh yang dimiliki browser. Ini memberi mereka pemerintahan bebas untuk menampilkan jendela phishing, serta akses ke file browser yang berfungsi termasuk login, kata sandi, dan cookie di database.Kami menghubungi pengembang UC Browser dan memberi tahu mereka tentang masalah yang kami temukan, mencoba menunjukkan kerentanan dan bahayanya, tetapi mereka menolak untuk membahas masalah tersebut. Sementara itu, browser dengan fungsi berbahaya tetap terlihat jelas. Meskipun begitu kami mengungkapkan rincian kerentanan, tidak mungkin untuk mengabaikannya seperti sebelumnya. Versi baru dari UC Browser 10.10.9.1193 dirilis pada tanggal 27 Maret, yang mengakses server melalui HTTPS puds.ucweb.com/upgrade/index.xhtml. Selain itu, antara "perbaikan bug" dan saat kami menulis artikel ini, upaya untuk membuka PDF di browser menghasilkan pesan kesalahan dengan teks, "Ups, ada sesuatu yang salah." Tidak ada permintaan ke server ketika mencoba membuka file PDF. Ini dilakukan saat startup, yang merupakan pertanda bahwa kemampuan untuk mengunduh kode yang dapat dieksekusi yang melanggar kebijakan Google Play masih ada.