البحث عن نقاط الضعف في متصفح UC

البحث عن نقاط الضعف في متصفح UC

مقدمة

في نهاية شهر مارس نحن ذكرتأنهم اكتشفوا قدرة مخفية على تحميل وتشغيل تعليمات برمجية لم يتم التحقق منها في متصفح UC. سننظر اليوم بالتفصيل في كيفية حدوث هذا التنزيل وكيف يمكن للمتسللين استخدامه لأغراضهم الخاصة.

منذ بعض الوقت، تم الإعلان عن متصفح UC وتوزيعه بقوة شديدة: فقد تم تثبيته على أجهزة المستخدمين باستخدام برامج ضارة، وتم توزيعه من مواقع مختلفة تحت ستار ملفات الفيديو (أي، اعتقد المستخدمون أنهم يقومون بتنزيل مقطع فيديو إباحي، على سبيل المثال، ولكن بدلاً من تلقي ملف APK مع هذا المتصفح)، استخدم لافتات مخيفة تحتوي على رسائل تفيد بأن المتصفح قديم ومعرض للخطر وأشياء من هذا القبيل. يوجد في مجموعة متصفح UC الرسمية على VK موضوع، حيث يمكن للمستخدمين تقديم شكوى من الإعلانات غير العادلة، وهناك العديد من الأمثلة هناك. في عام 2016 كان هناك حتى إعلانات الفيديو باللغة الروسية (نعم، الإعلان عن متصفح حظر الإعلانات).

في وقت كتابة هذا التقرير، كان متصفح UC لديه أكثر من 500 عملية تثبيت على Google Play. وهذا أمر مثير للإعجاب - فقط Google Chrome لديه المزيد. من بين المراجعات، يمكنك رؤية الكثير من الشكاوى حول الإعلانات وعمليات إعادة التوجيه إلى بعض التطبيقات على Google Play. كان هذا هو سبب بحثنا: قررنا معرفة ما إذا كان متصفح UC يفعل شيئًا سيئًا. واتضح أنه يفعل!

وفي كود التطبيق تم اكتشاف إمكانية تنزيل وتشغيل كود قابل للتنفيذ، وهو ما يخالف قواعد نشر التطبيقات على جوجل بلاي. بالإضافة إلى تنزيل التعليمات البرمجية القابلة للتنفيذ، يقوم متصفح UC بذلك بطريقة غير آمنة، والتي يمكن استخدامها لشن هجوم MitM. دعونا نرى ما إذا كان بإمكاننا تنفيذ مثل هذا الهجوم.

كل ما هو مكتوب أدناه ذو صلة بإصدار متصفح UC الذي كان متاحًا على 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 إلى Puds.ucweb.com/upgrade/index.xhtml، والتي يمكن رؤيتها في حركة المرور بعد وقت من البداية. ردًا على ذلك، قد يتلقى أمرًا بتنزيل بعض التحديثات أو الوحدات النمطية الجديدة. أثناء التحليل، لم يقم الخادم بإعطاء مثل هذه الأوامر، لكننا لاحظنا أنه عندما نحاول فتح ملف PDF في المتصفح، فإنه يقدم طلبًا ثانيًا إلى العنوان المحدد أعلاه، وبعد ذلك يقوم بتنزيل المكتبة الأصلية. لتنفيذ الهجوم، قررنا استخدام هذه الميزة في متصفح UC: القدرة على فتح ملف PDF باستخدام مكتبة أصلية، وهي ليست موجودة في APK ويمكن تنزيلها من الإنترنت إذا لزم الأمر. تجدر الإشارة إلى أنه من الناحية النظرية، يمكن إجبار متصفح UC على تنزيل شيء ما دون تدخل المستخدم - إذا قدمت استجابة جيدة التنظيم لطلب يتم تنفيذه بعد بدء تشغيل المتصفح. ولكن للقيام بذلك، نحتاج إلى دراسة بروتوكول التفاعل مع الخادم بمزيد من التفصيل، لذلك قررنا أنه سيكون من الأسهل تحرير الاستجابة المعترضة واستبدال المكتبة للعمل مع PDF.

لذلك، عندما يريد المستخدم فتح ملف PDF مباشرة في المتصفح، يمكن رؤية الطلبات التالية في حركة المرور:

البحث عن نقاط الضعف في متصفح UC

أولا هناك طلب POST ل Puds.ucweb.com/upgrade/index.xhtmlبعد ذلك
يتم تنزيل أرشيف به مكتبة لعرض تنسيقات PDF وOffice. من المنطقي أن نفترض أن الطلب الأول ينقل معلومات حول النظام (على الأقل بنية توفير المكتبة المطلوبة)، واستجابة له يتلقى المتصفح بعض المعلومات حول المكتبة التي يجب تنزيلها: العنوان وربما ، شيء آخر. المشكلة هي أن هذا الطلب مشفر.

طلب جزء

جزء الإجابة

البحث عن نقاط الضعف في متصفح UC

البحث عن نقاط الضعف في متصفح UC

المكتبة نفسها معبأة في ملف ZIP وغير مشفرة.

البحث عن نقاط الضعف في متصفح UC

البحث عن رمز فك تشفير حركة المرور

دعونا نحاول فك تشفير استجابة الخادم. دعونا نلقي نظرة على رمز الفصل com.uc.deployment.UpgradeDeployService: من الطريقة onStartCommand اذهب إلى com.uc.deployment.bx، ومنه إلى com.uc.browser.core.dcfe:

    public final void e(l arg9) {
int v4_5;
String v3_1;
byte[] v3;
byte[] v1 = null;
if(arg9 == null) {
v3 = v1;
}
else {
v3_1 = arg9.iGX.ipR;
StringBuilder v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]product:");
v4.append(arg9.iGX.ipR);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]version:");
v4.append(arg9.iGX.iEn);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]upgrade_type:");
v4.append(arg9.iGX.mMode);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]force_flag:");
v4.append(arg9.iGX.iEo);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_mode:");
v4.append(arg9.iGX.iDQ);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_type:");
v4.append(arg9.iGX.iEr);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_state:");
v4.append(arg9.iGX.iEp);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]silent_file:");
v4.append(arg9.iGX.iEq);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apk_md5:");
v4.append(arg9.iGX.iEl);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_type:");
v4.append(arg9.mDownloadType);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_group:");
v4.append(arg9.mDownloadGroup);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]download_path:");
v4.append(arg9.iGH);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_child_version:");
v4.append(arg9.iGX.iEx);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_series:");
v4.append(arg9.iGX.iEw);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_arch:");
v4.append(arg9.iGX.iEt);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp3:");
v4.append(arg9.iGX.iEv);
v4 = new StringBuilder("[");
v4.append(v3_1);
v4.append("]apollo_cpu_vfp:");
v4.append(arg9.iGX.iEu);
ArrayList v3_2 = arg9.iGX.iEz;
if(v3_2 != null && v3_2.size() != 0) {
Iterator v3_3 = v3_2.iterator();
while(v3_3.hasNext()) {
Object v4_1 = v3_3.next();
StringBuilder v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_name:");
v5.append(((au)v4_1).getName());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_name:");
v5.append(((au)v4_1).aDA());
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_ver_code:");
v5.append(((au)v4_1).gBl);
v5 = new StringBuilder("[");
v5.append(((au)v4_1).getName());
v5.append("]component_req_type:");
v5.append(((au)v4_1).gBq);
}
}
j v3_4 = new j();
m.b(v3_4);
h v4_2 = new h();
m.b(v4_2);
ay v5_1 = new ay();
v3_4.hS("");
v3_4.setImsi("");
v3_4.hV("");
v5_1.bPQ = v3_4;
v5_1.bPP = v4_2;
v5_1.yr(arg9.iGX.ipR);
v5_1.gBF = arg9.iGX.mMode;
v5_1.gBI = arg9.iGX.iEz;
v3_2 = v5_1.gAr;
c.aBh();
v3_2.add(g.fs("os_ver", c.getRomInfo()));
v3_2.add(g.fs("processor_arch", com.uc.b.a.a.c.getCpuArch()));
v3_2.add(g.fs("cpu_arch", com.uc.b.a.a.c.Pb()));
String v4_3 = com.uc.b.a.a.c.Pd();
v3_2.add(g.fs("cpu_vfp", v4_3));
v3_2.add(g.fs("net_type", String.valueOf(com.uc.base.system.a.Jo())));
v3_2.add(g.fs("fromhost", arg9.iGX.iEm));
v3_2.add(g.fs("plugin_ver", arg9.iGX.iEn));
v3_2.add(g.fs("target_lang", arg9.iGX.iEs));
v3_2.add(g.fs("vitamio_cpu_arch", arg9.iGX.iEt));
v3_2.add(g.fs("vitamio_vfp", arg9.iGX.iEu));
v3_2.add(g.fs("vitamio_vfp3", arg9.iGX.iEv));
v3_2.add(g.fs("plugin_child_ver", arg9.iGX.iEx));
v3_2.add(g.fs("ver_series", arg9.iGX.iEw));
v3_2.add(g.fs("child_ver", r.aVw()));
v3_2.add(g.fs("cur_ver_md5", arg9.iGX.iEl));
v3_2.add(g.fs("cur_ver_signature", SystemHelper.getUCMSignature()));
v3_2.add(g.fs("upgrade_log", i.bjt()));
v3_2.add(g.fs("silent_install", String.valueOf(arg9.iGX.iDQ)));
v3_2.add(g.fs("silent_state", String.valueOf(arg9.iGX.iEp)));
v3_2.add(g.fs("silent_file", arg9.iGX.iEq));
v3_2.add(g.fs("silent_type", String.valueOf(arg9.iGX.iEr)));
v3_2.add(g.fs("cpu_archit", com.uc.b.a.a.c.Pc()));
v3_2.add(g.fs("cpu_set", SystemHelper.getCpuInstruction()));
boolean v4_4 = v4_3 == null || !v4_3.contains("neon") ? false : true;
v3_2.add(g.fs("neon", String.valueOf(v4_4)));
v3_2.add(g.fs("cpu_cores", String.valueOf(com.uc.b.a.a.c.Jl())));
v3_2.add(g.fs("ram_1", String.valueOf(com.uc.b.a.a.h.Po())));
v3_2.add(g.fs("totalram", String.valueOf(com.uc.b.a.a.h.OL())));
c.aBh();
v3_2.add(g.fs("rom_1", c.getRomInfo()));
v4_5 = e.getScreenWidth();
int v6 = e.getScreenHeight();
StringBuilder v7 = new StringBuilder();
v7.append(v4_5);
v7.append("*");
v7.append(v6);
v3_2.add(g.fs("ss", v7.toString()));
v3_2.add(g.fs("api_level", String.valueOf(Build$VERSION.SDK_INT)));
v3_2.add(g.fs("uc_apk_list", SystemHelper.getUCMobileApks()));
Iterator v4_6 = arg9.iGX.iEA.entrySet().iterator();
while(v4_6.hasNext()) {
Object v6_1 = v4_6.next();
v3_2.add(g.fs(((Map$Entry)v6_1).getKey(), ((Map$Entry)v6_1).getValue()));
}
v3 = v5_1.toByteArray();
}
if(v3 == null) {
this.iGY.iGI.a(arg9, "up_encode", "yes", "fail");
return;
}
v4_5 = this.iGY.iGw ? 0x1F : 0;
if(v3 == null) {
}
else {
v3 = g.i(v4_5, v3);
if(v3 == null) {
}
else {
v1 = new byte[v3.length + 16];
byte[] v6_2 = new byte[16];
Arrays.fill(v6_2, 0);
v6_2[0] = 0x5F;
v6_2[1] = 0;
v6_2[2] = ((byte)v4_5);
v6_2[3] = -50;
System.arraycopy(v6_2, 0, v1, 0, 16);
System.arraycopy(v3, 0, v1, 16, v3.length);
}
}
if(v1 == null) {
this.iGY.iGI.a(arg9, "up_encrypt", "yes", "fail");
return;
}
if(TextUtils.isEmpty(this.iGY.mUpgradeUrl)) {
this.iGY.iGI.a(arg9, "up_url", "yes", "fail");
return;
}
StringBuilder v0 = new StringBuilder("[");
v0.append(arg9.iGX.ipR);
v0.append("]url:");
v0.append(this.iGY.mUpgradeUrl);
com.uc.browser.core.d.c.i v0_1 = this.iGY.iGI;
v3_1 = this.iGY.mUpgradeUrl;
com.uc.base.net.e v0_2 = new com.uc.base.net.e(new com.uc.browser.core.d.c.i$a(v0_1, arg9));
v3_1 = v3_1.contains("?") ? v3_1 + "&dataver=pb" : v3_1 + "?dataver=pb";
n v3_5 = v0_2.uc(v3_1);
m.b(v3_5, false);
v3_5.setMethod("POST");
v3_5.setBodyProvider(v1);
v0_2.b(v3_5);
this.iGY.iGI.a(arg9, "up_null", "yes", "success");
this.iGY.iGI.b(arg9);
}

نرى تشكيل طلب POST هنا. نحن ننتبه إلى إنشاء مصفوفة مكونة من 16 بايت وملؤها: 0x5F, 0, 0x1F, -50 (=0xCE). ويتزامن مع ما رأيناه في الطلب أعلاه.

في نفس الفصل يمكنك رؤية فصل متداخل له طريقة أخرى مثيرة للاهتمام:

        public final void a(l arg10, byte[] arg11) {
f v0 = this.iGQ;
StringBuilder v1 = new StringBuilder("[");
v1.append(arg10.iGX.ipR);
v1.append("]:UpgradeSuccess");
byte[] v1_1 = null;
if(arg11 == null) {
}
else if(arg11.length < 16) {
}
else {
if(arg11[0] != 0x60 && arg11[3] != 0xFFFFFFD0) {
goto label_57;
}
int v3 = 1;
int v5 = arg11[1] == 1 ? 1 : 0;
if(arg11[2] != 1 && arg11[2] != 11) {
if(arg11[2] == 0x1F) {
}
else {
v3 = 0;
}
}
byte[] v7 = new byte[arg11.length - 16];
System.arraycopy(arg11, 16, v7, 0, v7.length);
if(v3 != 0) {
v7 = g.j(arg11[2], v7);
}
if(v7 == null) {
goto label_57;
}
if(v5 != 0) {
v1_1 = g.P(v7);
goto label_57;
}
v1_1 = v7;
}
label_57:
if(v1_1 == null) {
v0.iGY.iGI.a(arg10, "up_decrypt", "yes", "fail");
return;
}
q v11 = g.b(arg10, v1_1);
if(v11 == null) {
v0.iGY.iGI.a(arg10, "up_decode", "yes", "fail");
return;
}
if(v0.iGY.iGt) {
v0.d(arg10);
}
if(v0.iGY.iGo != null) {
v0.iGY.iGo.a(0, ((o)v11));
}
if(v0.iGY.iGs) {
v0.iGY.a(((o)v11));
v0.iGY.iGI.a(v11, "up_silent", "yes", "success");
v0.iGY.iGI.a(v11);
return;
}
v0.iGY.iGI.a(v11, "up_silent", "no", "success");
}
}

تأخذ الطريقة مصفوفة من البايتات كمدخلات وتتحقق من أن البايت الصفري هو 0x60 أو البايت الثالث هو 0xD0 والبايت الثاني هو 1 أو 11 أو 0x1F. نحن ننظر إلى استجابة الخادم: البايت الصفري هو 0x60، والثاني هو 0x1F، والثالث هو 0x60. يبدو مثل ما نحتاجه. إذا حكمنا من خلال السطور ("up_decrypt"، على سبيل المثال)، يجب استدعاء طريقة هنا لفك تشفير استجابة الخادم.
دعنا ننتقل إلى الطريقة gj. لاحظ أن الوسيط الأول هو البايت عند الإزاحة 2 (أي 0x1F في حالتنا)، والثاني هو استجابة الخادم بدون
أول 16 بايت.

     public static byte[] j(int arg1, byte[] arg2) {
if(arg1 == 1) {
arg2 = c.c(arg2, c.adu);
}
else if(arg1 == 11) {
arg2 = m.aF(arg2);
}
else if(arg1 != 0x1F) {
}
else {
arg2 = EncryptHelper.decrypt(arg2);
}
return arg2;
}

من الواضح أننا هنا نختار خوارزمية فك التشفير، ونفس البايت الموجود لدينا
تشير حالة الأحرف التي تساوي 0x1F إلى أحد الخيارات الثلاثة الممكنة.

نواصل تحليل الكود. بعد بضع قفزات نجد أنفسنا في طريقة تحمل اسمًا واضحًا decryptBytesByKey.

هنا يتم فصل بايتين آخرين عن استجابتنا، ويتم الحصول على سلسلة منهم. من الواضح أنه بهذه الطريقة يتم تحديد مفتاح فك تشفير الرسالة.

    private static byte[] decryptBytesByKey(byte[] bytes) {
byte[] v0 = null;
if(bytes != null) {
try {
if(bytes.length < EncryptHelper.PREFIX_BYTES_SIZE) {
}
else if(bytes.length == EncryptHelper.PREFIX_BYTES_SIZE) {
return v0;
}
else {
byte[] prefix = new byte[EncryptHelper.PREFIX_BYTES_SIZE];  // 2 байта
System.arraycopy(bytes, 0, prefix, 0, prefix.length);
String keyId = c.ayR().d(ByteBuffer.wrap(prefix).getShort()); // Выбор ключа
if(keyId == null) {
return v0;
}
else {
a v2 = EncryptHelper.ayL();
if(v2 == null) {
return v0;
}
else {
byte[] enrypted = new byte[bytes.length - EncryptHelper.PREFIX_BYTES_SIZE];
System.arraycopy(bytes, EncryptHelper.PREFIX_BYTES_SIZE, enrypted, 0, enrypted.length);
return v2.l(keyId, enrypted);
}
}
}
}
catch(SecException v7_1) {
EncryptHelper.handleDecryptException(((Throwable)v7_1), v7_1.getErrorCode());
return v0;
}
catch(Throwable v7) {
EncryptHelper.handleDecryptException(v7, 2);
return v0;
}
}
return v0;
}

وبالنظر إلى المستقبل، نلاحظ أننا في هذه المرحلة لم نحصل بعد على المفتاح، ولكن فقط "المعرف" الخاص به. الحصول على المفتاح أكثر تعقيدًا بعض الشيء.

في الطريقة التالية، تتم إضافة معلمتين إضافيتين إلى المعلمات الموجودة، مما يجعل أربعة منها: الرقم السحري 16، ومعرف المفتاح، والبيانات المشفرة، وسلسلة غير مفهومة (في حالتنا، فارغة).

    public final byte[] l(String keyId, byte[] encrypted) throws SecException {
return this.ayJ().staticBinarySafeDecryptNoB64(16, keyId, encrypted, "");
}

وبعد سلسلة من التحولات نصل إلى الطريقة staticBinarySafeDecryptNoB64 السطح البيني com.alibaba.wireless.security.open.staticdataencrypt.IStaticDataEncryptComponent. لا توجد فئات في كود التطبيق الرئيسي تنفذ هذه الواجهة. هناك مثل هذه الفئة في الملف lib/armeabi-v7a/libsgmain.so، وهو ليس في الواقع .so، ولكنه .jar. يتم تنفيذ الطريقة التي تهمنا على النحو التالي:

package com.alibaba.wireless.security.a.i;
// ...
public class a implements IStaticDataEncryptComponent {
private ISecurityGuardPlugin a;
// ...
private byte[] a(int mode, int magicInt, int xzInt, String keyId, byte[] encrypted, String magicString) {
return this.a.getRouter().doCommand(10601, new Object[]{Integer.valueOf(mode), Integer.valueOf(magicInt), Integer.valueOf(xzInt), keyId, encrypted, magicString});
}
// ...
private byte[] b(int magicInt, String keyId, byte[] encrypted, String magicString) {
return this.a(2, magicInt, 0, keyId, encrypted, magicString);
}
// ...
public byte[] staticBinarySafeDecryptNoB64(int magicInt, String keyId, byte[] encrypted, String magicString) throws SecException {
if(keyId != null && keyId.length() > 0 && magicInt >= 0 && magicInt < 19 && encrypted != null && encrypted.length > 0) {
return this.b(magicInt, keyId, encrypted, magicString);
}
throw new SecException("", 301);
}
//...
}

هنا يتم استكمال قائمة المعلمات الخاصة بنا بعددين صحيحين آخرين: 2 و0
كل شيء، 2 يعني فك التشفير، كما في الطريقة doFinal فئة النظام javax.crypto.Cipher. ويتم نقل كل هذا إلى جهاز توجيه معين برقم 10601 - ويبدو أن هذا هو رقم الأمر.

بعد السلسلة التالية من التحولات نجد فئة تقوم بتنفيذ الواجهة IRouterComponent والطريقة doCommand:

package com.alibaba.wireless.security.mainplugin;
import com.alibaba.wireless.security.framework.IRouterComponent;
import com.taobao.wireless.security.adapter.JNICLibrary;
public class a implements IRouterComponent {
public a() {
super();
}
public Object doCommand(int arg2, Object[] arg3) {
return JNICLibrary.doCommandNative(arg2, arg3);
}
}

وأيضا الطبقة JNICLibrary، حيث يتم الإعلان عن الطريقة الأصلية doCommandNative:

package com.taobao.wireless.security.adapter;
public class JNICLibrary {
public static native Object doCommandNative(int arg0, Object[] arg1);
}

هذا يعني أننا بحاجة إلى إيجاد طريقة في الكود الأصلي doCommandNative. وهذا هو المكان الذي تبدأ فيه المتعة.

تشويش كود الآلة

في ملف libsgmain.so (وهو في الواقع ملف .jar والذي وجدنا فيه تنفيذ بعض الواجهات المتعلقة بالتشفير أعلاه مباشرة) توجد مكتبة أصلية واحدة: libsgmainso-6.4.36.so. نفتحه في IDA ونحصل على مجموعة من مربعات الحوار التي بها أخطاء. المشكلة هي أن جدول رؤوس الأقسام غير صالح. ويتم ذلك عمدا لتعقيد التحليل.

البحث عن نقاط الضعف في متصفح UC

ولكن ليس هناك حاجة لذلك: لتحميل ملف ELF بشكل صحيح وتحليله، يكفي جدول رأس البرنامج. لذلك، نقوم ببساطة بحذف جدول الأقسام، مع تصفية الحقول المقابلة في الرأس.

البحث عن نقاط الضعف في متصفح UC

افتح الملف في IDA مرة أخرى.

هناك طريقتان لإخبار جهاز Java الظاهري بالمكان الذي يوجد فيه بالضبط تنفيذ الطريقة المعلن عنها في كود Java على أنها أصلية في المكتبة الأصلية. الأول هو إعطائها اسم النوع Java_package_name_ClassName_MethodName.

والثاني هو تسجيله عند تحميل المكتبة (في الوظيفة JNI_OnLoad)
باستخدام استدعاء دالة سجل السكان الأصليين.

وفي حالتنا إذا استخدمنا الطريقة الأولى فيجب أن يكون الاسم هكذا: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative.

لا توجد مثل هذه الوظيفة بين الوظائف المصدرة، مما يعني أنك بحاجة إلى البحث عن مكالمة سجل السكان الأصليين.
دعنا نذهب إلى الوظيفة JNI_OnLoad ونرى هذه الصورة:

البحث عن نقاط الضعف في متصفح UC

ما الذي يحدث هنا؟ للوهلة الأولى، تعتبر بداية الوظيفة ونهايتها نموذجية لهندسة ARM. تقوم التعليمات الأولى في المكدس بتخزين محتويات السجلات التي ستستخدمها الوظيفة في عملها (في هذه الحالة، R0 وR1 وR2)، بالإضافة إلى محتويات السجل LR، الذي يحتوي على عنوان الإرجاع من الوظيفة . تعمل التعليمات الأخيرة على استعادة السجلات المحفوظة، ويتم وضع عنوان الإرجاع على الفور في سجل الكمبيوتر - وبالتالي العودة من الوظيفة. ولكن إذا نظرت عن كثب، ستلاحظ أن التعليمات قبل الأخيرة تغير عنوان الإرجاع المخزن في المكدس. دعونا نحسب كيف سيكون الأمر بعد ذلك
تنفيذ التعليمات البرمجية. يتم تحميل عنوان معين 1xB0 إلى R130، ويتم طرح 5 منه، ثم يتم نقله إلى R0 وإضافة 0x10 إليه. اتضح 0xB13B. وبالتالي، تعتقد IDA أن التعليمة الأخيرة هي إرجاع دالة عادية، ولكنها في الواقع تتجه إلى العنوان المحسوب 0xB13B.

تجدر الإشارة هنا إلى أن معالجات ARM لها وضعان ومجموعتان من التعليمات: ARM وThumb. يخبر الجزء الأقل أهمية من العنوان المعالج بمجموعة التعليمات المستخدمة. أي أن العنوان هو في الواقع 0xB13A، ويشير أحد البتات الأقل أهمية إلى وضع الإبهام.

تمت إضافة "محول" مشابه إلى بداية كل وظيفة في هذه المكتبة و
رمز القمامة. لن نتناولها بمزيد من التفصيل - نحن نتذكر فقط
أن البداية الحقيقية لجميع الوظائف تقريبًا بعيدة قليلاً.

نظرًا لأن الكود لا ينتقل بشكل صريح إلى 0xB13A، فإن IDA نفسها لم تتعرف على وجود الكود في هذا الموقع. لنفس السبب، لا يتعرف على معظم التعليمات البرمجية الموجودة في المكتبة على أنها تعليمات برمجية، مما يجعل التحليل صعبًا إلى حد ما. نقول للمؤسسة الدولية للتنمية أن هذا هو الرمز، وهذا ما يحدث:

البحث عن نقاط الضعف في متصفح UC

يبدأ الجدول بوضوح عند 0xB144. ماذا يوجد في sub_494C؟

البحث عن نقاط الضعف في متصفح UC

عند استدعاء هذه الدالة في السجل LR نحصل على عنوان الجدول المذكور سابقا (0xB144). في R0 - الفهرس في هذا الجدول. أي أن القيمة مأخوذة من الجدول وتضاف إلى LR وتكون النتيجة
العنوان الذي ستذهب إليه. لنحاول حسابها: 0xB144 + [0xB144 + 8* 4] = 0xB144 + 0x120 = 0xB264. نذهب إلى العنوان المستلم ونرى حرفيًا بعض الإرشادات المفيدة وننتقل مرة أخرى إلى 0xB140:

البحث عن نقاط الضعف في متصفح UC

الآن سيكون هناك انتقال عند الإزاحة مع الفهرس 0x20 من الجدول.

انطلاقا من حجم الجدول، سيكون هناك العديد من هذه التحولات في التعليمات البرمجية. السؤال الذي يطرح نفسه هو ما إذا كان من الممكن التعامل مع هذا بطريقة أو بأخرى بشكل تلقائي، دون حساب العناوين يدويًا. وتأتي البرامج النصية والقدرة على تصحيح التعليمات البرمجية في IDA لمساعدتنا:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 2
if get_wide_word(ea1) == 0xbf00: #NOP
ea1 += 2
if get_operand_type(ea1, 0) == 1 and get_operand_value(ea1, 0) == 0 and get_operand_type(ea1, 1) == 2:
index = get_wide_dword(get_operand_value(ea1, 1))
print "index =", hex(index)
ea1 += 2
if get_operand_type(ea1, 0) == 7:
table = get_operand_value(ea1, 0) + 4
elif get_operand_type(ea1, 1) == 2:
table = get_operand_value(ea1, 1) + 4
else:
print "Wrong operand type on", hex(ea1), "-", get_operand_type(ea1, 0), get_operand_type(ea1, 1)
table = None
if table is None:
print "Unable to find table"
else:
print "table =", hex(table)
offset = get_wide_dword(table + (index << 2))
put_unconditional_branch(ea, table + offset)
else:
print "Unknown code", get_operand_type(ea1, 0), get_operand_value(ea1, 0), get_operand_type(ea1, 1) == 2
else:
print "Unable to detect first instruction"

ضع المؤشر على السطر 0xB26A، وقم بتشغيل البرنامج النصي وشاهد الانتقال إلى 0xB4B0:

البحث عن نقاط الضعف في متصفح UC

لم تتعرف المؤسسة الدولية للتنمية مرة أخرى على هذه المنطقة كرمز. نحن نساعدها ونرى تصميمًا آخر هناك:

البحث عن نقاط الضعف في متصفح UC

لا يبدو أن التعليمات التي تأتي بعد BLX منطقية، فهي أشبه بنوع من الإزاحة. دعونا نلقي نظرة على sub_4964:

البحث عن نقاط الضعف في متصفح UC

وبالفعل، هنا يتم أخذ كلمة مزدوجة على العنوان الموجود في LR، وإضافتها إلى هذا العنوان، وبعد ذلك يتم أخذ القيمة الموجودة في العنوان الناتج ووضعها على المكدس. أيضًا، تتم إضافة 4 إلى LR بحيث يتم تخطي نفس الإزاحة بعد العودة من الوظيفة. وبعد ذلك يأخذ أمر POP {R1} القيمة الناتجة من المكدس. إذا نظرت إلى ما هو موجود في العنوان 0xB4BA + 0xEA = 0xB5A4، فسترى شيئًا مشابهًا لجدول العناوين:

البحث عن نقاط الضعف في متصفح UC

لتصحيح هذا التصميم، ستحتاج إلى الحصول على معلمتين من الكود: الإزاحة ورقم التسجيل الذي تريد وضع النتيجة فيه. لكل تسجيل محتمل، سيتعين عليك إعداد جزء من التعليمات البرمجية مسبقًا.

patches = {}
patches[0] = (0x00, 0xbf, 0x01, 0x48, 0x00, 0x68, 0x02, 0xe0)
patches[1] = (0x00, 0xbf, 0x01, 0x49, 0x09, 0x68, 0x02, 0xe0)
patches[2] = (0x00, 0xbf, 0x01, 0x4a, 0x12, 0x68, 0x02, 0xe0)
patches[3] = (0x00, 0xbf, 0x01, 0x4b, 0x1b, 0x68, 0x02, 0xe0)
patches[4] = (0x00, 0xbf, 0x01, 0x4c, 0x24, 0x68, 0x02, 0xe0)
patches[5] = (0x00, 0xbf, 0x01, 0x4d, 0x2d, 0x68, 0x02, 0xe0)
patches[8] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x80, 0xd8, 0xf8, 0x00, 0x80, 0x01, 0xe0)
patches[9] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0x90, 0xd9, 0xf8, 0x00, 0x90, 0x01, 0xe0)
patches[10] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xa0, 0xda, 0xf8, 0x00, 0xa0, 0x01, 0xe0)
patches[11] = (0x00, 0xbf, 0xdf, 0xf8, 0x06, 0xb0, 0xdb, 0xf8, 0x00, 0xb0, 0x01, 0xe0)
ea = here()
if (get_wide_word(ea) == 0xb082 #SUB SP, SP, #8
and get_wide_word(ea + 2) == 0xb503): #PUSH {R0,R1,LR}
if get_operand_type(ea + 4, 0) == 7:
pop = get_bytes(ea + 12, 4, 0)
if pop[1] == 'xbc':
register = -1
r = get_wide_byte(ea + 12)
for i in range(8):
if r == (1 << i):
register = i
break
if register == -1:
print "Unable to detect register"
else:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
if ea % 4 != 0:
ea += 2
patch_dword(ea, address)
elif pop[:3] == 'x5dxf8x04':
register = ord(pop[3]) >> 4
if register in patches:
address = get_wide_dword(ea + 8) + ea + 8
for b in patches[register]:
patch_byte(ea, b)
ea += 1
patch_dword(ea, address)
else:
print "POP instruction not found"
else:
print "Wrong operand type on +4:", get_operand_type(ea + 4, 0)
else:
print "Unable to detect first instructions"

نضع المؤشر في بداية البنية التي نريد استبدالها - 0xB4B2 - ونقوم بتشغيل البرنامج النصي:

البحث عن نقاط الضعف في متصفح UC

بالإضافة إلى الهياكل المذكورة بالفعل، يحتوي الكود أيضًا على ما يلي:

البحث عن نقاط الضعف في متصفح UC

كما في الحالة السابقة، بعد تعليمة BLX هناك إزاحة:

البحث عن نقاط الضعف في متصفح UC

نأخذ الإزاحة إلى العنوان من LR، ونضيفها إلى LR ونذهب إلى هناك. 0x72044 + 0xC = 0x72050. البرنامج النصي لهذا التصميم بسيط للغاية:

def put_unconditional_branch(source, destination):
offset = (destination - source - 4) >> 1
if offset > 2097151 or offset < -2097152:
raise RuntimeError("Invalid offset")
if offset > 1023 or offset < -1024:
instruction1 = 0xf000 | ((offset >> 11) & 0x7ff)
instruction2 = 0xb800 | (offset & 0x7ff)
patch_word(source, instruction1)
patch_word(source + 2, instruction2)
else:
instruction = 0xe000 | (offset & 0x7ff)
patch_word(source, instruction)
ea = here()
if get_wide_word(ea) == 0xb503: #PUSH {R0,R1,LR}
ea1 = ea + 6
if get_wide_word(ea + 2) == 0xbf00: #NOP
ea1 += 2
offset = get_wide_dword(ea1)
put_unconditional_branch(ea, (ea1 + offset) & 0xffffffff)
else:
print "Unable to detect first instruction"

نتيجة تنفيذ البرنامج النصي:

البحث عن نقاط الضعف في متصفح UC

بمجرد تصحيح كل شيء في الوظيفة، يمكنك توجيه IDA إلى بدايتها الحقيقية. سيقوم بتجميع كل كود الوظيفة معًا، ويمكن فك ترجمته باستخدام HexRays.

سلاسل فك التشفير

لقد تعلمنا كيفية التعامل مع تشويش كود الآلة في المكتبة libsgmainso-6.4.36.so من متصفح UC واستلمت رمز الوظيفة JNI_OnLoad.

int __fastcall real_JNI_OnLoad(JavaVM *vm)
{
int result; // r0
jclass clazz; // r0 MAPDST
int v4; // r0
JNIEnv *env; // r4
int v6; // [sp-40h] [bp-5Ch]
int v7; // [sp+Ch] [bp-10h]
v7 = *(_DWORD *)off_8AC00;
if ( !vm )
goto LABEL_39;
sub_7C4F4();
env = (JNIEnv *)sub_7C5B0(0);
if ( !env )
goto LABEL_39;
v4 = sub_72CCC();
sub_73634(v4);
sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);
if ( clazz
&& (sub_9EE4(),
sub_71D68(env),
sub_E7DC(env) >= 0
&& sub_69D68(env) >= 0
&& sub_197B4(env, clazz) >= 0
&& sub_E240(env, clazz) >= 0
&& sub_B8B0(env, clazz) >= 0
&& sub_5F0F4(env, clazz) >= 0
&& sub_70640(env, clazz) >= 0
&& sub_11F3C(env) >= 0
&& sub_21C3C(env, clazz) >= 0
&& sub_2148C(env, clazz) >= 0
&& sub_210E0(env, clazz) >= 0
&& sub_41B58(env, clazz) >= 0
&& sub_27920(env, clazz) >= 0
&& sub_293E8(env, clazz) >= 0
&& sub_208F4(env, clazz) >= 0) )
{
result = (sub_B7B0(env, clazz) >> 31) | 0x10004;
}
else
{
LABEL_39:
result = -1;
}
return result;
}

دعونا نلقي نظرة فاحصة على السطور التالية:

  sub_73E24(&unk_83EA6, &v6, 49);
clazz = (jclass)((int (__fastcall *)(JNIEnv *, int *))(*env)->FindClass)(env, &v6);

فى مهمة sub_73E24 من الواضح أنه يتم فك تشفير اسم الفئة. كمعلمات لهذه الوظيفة، يتم تمرير مؤشر للبيانات المشابهة للبيانات المشفرة ومخزن مؤقت معين ورقم. من الواضح أنه بعد استدعاء الوظيفة، سيكون هناك خط مفك التشفير في المخزن المؤقت، حيث يتم تمريره إلى الوظيفة FindClass، والذي يأخذ اسم الفئة كمعلمة ثانية. لذلك، الرقم هو حجم المخزن المؤقت أو طول السطر. دعونا نحاول فك تشفير اسم الفصل، يجب أن يخبرنا ما إذا كنا نسير في الاتجاه الصحيح. دعونا نلقي نظرة فاحصة على ما يحدث في sub_73E24.

int __fastcall sub_73E56(unsigned __int8 *in, unsigned __int8 *out, size_t size)
{
int v4; // r6
int v7; // r11
int v8; // r9
int v9; // r4
size_t v10; // r5
int v11; // r0
struc_1 v13; // [sp+0h] [bp-30h]
int v14; // [sp+1Ch] [bp-14h]
int v15; // [sp+20h] [bp-10h]
v4 = 0;
v15 = *(_DWORD *)off_8AC00;
v14 = 0;
v7 = sub_7AF78(17);
v8 = sub_7AF78(size);
if ( !v7 )
{
v9 = 0;
goto LABEL_12;
}
(*(void (__fastcall **)(int, const char *, int))(v7 + 12))(v7, "DcO/lcK+h?m3c*q@", 16);
if ( !v8 )
{
LABEL_9:
v4 = 0;
goto LABEL_10;
}
v4 = 0;
if ( !in )
{
LABEL_10:
v9 = 0;
goto LABEL_11;
}
v9 = 0;
if ( out )
{
memset(out, 0, size);
v10 = size - 1;
(*(void (__fastcall **)(int, unsigned __int8 *, size_t))(v8 + 12))(v8, in, v10);
memset(&v13, 0, 0x14u);
v13.field_4 = 3;
v13.field_10 = v7;
v13.field_14 = v8;
v11 = sub_6115C(&v13, &v14);
v9 = v11;
if ( v11 )
{
if ( *(_DWORD *)(v11 + 4) == v10 )
{
qmemcpy(out, *(const void **)v11, v10);
v4 = *(_DWORD *)(v9 + 4);
}
else
{
v4 = 0;
}
goto LABEL_11;
}
goto LABEL_9;
}
LABEL_11:
sub_7B148(v7);
LABEL_12:
if ( v8 )
sub_7B148(v8);
if ( v9 )
sub_7B148(v9);
return v4;
}

وظيفة sub_7AF78 ينشئ مثيلًا لحاوية لمصفوفات البايت بالحجم المحدد (لن نتناول هذه الحاويات بالتفصيل). هنا يتم إنشاء حاويتين: إحداهما تحتوي على السطر "DcO/lcK+h?m3c*q@" (من السهل تخمين أن هذا مفتاح)، والآخر يحتوي على بيانات مشفرة. بعد ذلك، يتم وضع كلا الكائنين في بنية معينة، والتي يتم تمريرها إلى الوظيفة sub_6115C. دعونا أيضًا نضع علامة على الحقل بالقيمة 3 في هذه البنية، دعونا نرى ما سيحدث لهذه البنية بعد ذلك.

int __fastcall sub_611B4(struc_1 *a1, _DWORD *a2)
{
int v3; // lr
unsigned int v4; // r1
int v5; // r0
int v6; // r1
int result; // r0
int v8; // r0
*a2 = 820000;
if ( a1 )
{
v3 = a1->field_14;
if ( v3 )
{
v4 = a1->field_4;
if ( v4 < 0x19 )
{
switch ( v4 )
{
case 0u:
v8 = sub_6419C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 3u:
v8 = sub_6364C(a1->field_0, a1->field_10, v3);
goto LABEL_17;
case 0x10u:
case 0x11u:
case 0x12u:
v8 = sub_612F4(
a1->field_0,
v4,
*(_QWORD *)&a1->field_8,
*(_QWORD *)&a1->field_8 >> 32,
a1->field_10,
v3,
a2);
goto LABEL_17;
case 0x14u:
v8 = sub_63A28(a1->field_0, v3);
goto LABEL_17;
case 0x15u:
sub_61A60(a1->field_0, v3, a2);
return result;
case 0x16u:
v8 = sub_62440(a1->field_14);
goto LABEL_17;
case 0x17u:
v8 = sub_6226C(a1->field_10, v3);
goto LABEL_17;
case 0x18u:
v8 = sub_63530(a1->field_14);
LABEL_17:
v6 = 0;
if ( v8 )
{
*a2 = 0;
v6 = v8;
}
return v6;
default:
LOWORD(v5) = 28032;
goto LABEL_5;
}
}
}
}
LOWORD(v5) = -27504;
LABEL_5:
HIWORD(v5) = 13;
v6 = 0;
*a2 = v5;
return v6;
}

معلمة التبديل هي حقل بنية تم تعيين القيمة 3 مسبقًا له. انظر إلى الحالة 3: للوظيفة sub_6364C يتم تمرير المعلمات من البنية التي تمت إضافتها هناك في الوظيفة السابقة، أي المفتاح والبيانات المشفرة. إذا نظرت عن كثب sub_6364Cيمكنك التعرف على خوارزمية RC4 فيه.

لدينا خوارزمية ومفتاح. دعونا نحاول فك اسم الفصل. وهنا ما حدث: com/taobao/wireless/security/adapter/JNICLibrary. عظيم! نحن نسير على الطريق الصحيح.

شجرة الأوامر

الآن نحن بحاجة إلى إيجاد التحدي سجل السكان الأصليين، والتي سوف توجهنا إلى الوظيفة doCommandNative. دعونا نلقي نظرة على الوظائف التي تم استدعاؤها من JNI_OnLoad، ونجده في sub_B7B0:

int __fastcall sub_B7F6(JNIEnv *env, jclass clazz)
{
char signature[41]; // [sp+7h] [bp-55h]
char name[16]; // [sp+30h] [bp-2Ch]
JNINativeMethod method; // [sp+40h] [bp-1Ch]
int v8; // [sp+4Ch] [bp-10h]
v8 = *(_DWORD *)off_8AC00;
decryptString((unsigned __int8 *)&unk_83ED9, (unsigned __int8 *)name, 0x10u);// doCommandNative
decryptString((unsigned __int8 *)&unk_83EEA, (unsigned __int8 *)signature, 0x29u);// (I[Ljava/lang/Object;)Ljava/lang/Object;
method.name = name;
method.signature = signature;
method.fnPtr = sub_B69C;
return ((int (__fastcall *)(JNIEnv *, jclass, JNINativeMethod *, int))(*env)->RegisterNatives)(env, clazz, &method, 1) >> 31;
}

وبالفعل، تم تسجيل طريقة أصلية بالاسم هنا doCommandNative. والآن نعرف عنوانه. دعونا نرى ماذا يفعل.

int __fastcall doCommandNative(JNIEnv *env, jobject obj, int command, jarray args)
{
int v5; // r5
struc_2 *a5; // r6
int v9; // r1
int v11; // [sp+Ch] [bp-14h]
int v12; // [sp+10h] [bp-10h]
v5 = 0;
v12 = *(_DWORD *)off_8AC00;
v11 = 0;
a5 = (struc_2 *)malloc(0x14u);
if ( a5 )
{
a5->field_0 = 0;
a5->field_4 = 0;
a5->field_8 = 0;
a5->field_C = 0;
v9 = command % 10000 / 100;
a5->field_0 = command / 10000;
a5->field_4 = v9;
a5->field_8 = command % 100;
a5->field_C = env;
a5->field_10 = args;
v5 = sub_9D60(command / 10000, v9, command % 100, 1, (int)a5, &v11);
}
free(a5);
if ( !v5 && v11 )
sub_7CF34(env, v11, &byte_83ED7);
return v5;
}

بالاسم يمكنك تخمين أن هذه هي نقطة الدخول لجميع الوظائف التي قرر المطورون نقلها إلى المكتبة الأصلية. نحن مهتمون بالوظيفة رقم 10601.

يمكنك أن ترى من الكود أن رقم الأمر ينتج ثلاثة أرقام: الأمر/10000, الأمر٪ 10000/100 и الأمر %10، أي في حالتنا، 1 و6 و1. هذه الأرقام الثلاثة، بالإضافة إلى مؤشر JNIEnv ويتم إضافة الوسائط التي تم تمريرها إلى الوظيفة إلى البنية وتمريرها. باستخدام الأرقام الثلاثة التي تم الحصول عليها (دعنا نشير إليها بـ N1 وN2 وN3)، يتم إنشاء شجرة أوامر.

شيء من هذا القبيل:

البحث عن نقاط الضعف في متصفح UC

يتم ملء الشجرة ديناميكيًا JNI_OnLoad.
ثلاثة أرقام تشفر المسار في الشجرة. تحتوي كل ورقة من الشجرة على العنوان المثقوب للوظيفة المقابلة. المفتاح موجود في العقدة الأم. ليس من الصعب العثور على المكان في الكود حيث تتم إضافة الوظيفة التي نحتاجها إلى الشجرة إذا فهمت جميع الهياكل المستخدمة (نحن لا نصفها حتى لا نضخم مقالة كبيرة بالفعل).

المزيد من التشويش

لقد تلقينا عنوان الوظيفة التي يجب أن تقوم بفك تشفير حركة المرور: 0x5F1AC. ولكن من السابق لأوانه أن نبتهج: لقد أعد لنا مطورو UC Browser مفاجأة أخرى.

بعد تلقي المعلمات من المصفوفة التي تم تشكيلها في كود Java، نحصل عليها
إلى الوظيفة على العنوان 0x4D070. وهنا ينتظرنا نوع آخر من تشويش التعليمات البرمجية.

نضع مؤشرين في R7 و R4:

البحث عن نقاط الضعف في متصفح UC

نحول الفهرس الأول إلى R11:

البحث عن نقاط الضعف في متصفح UC

للحصول على عنوان من جدول، استخدم الفهرس:

البحث عن نقاط الضعف في متصفح UC

بعد الذهاب إلى العنوان الأول، يتم استخدام الفهرس الثاني، وهو في R4. هناك 230 عنصرا في الجدول.

ما يجب فعله حيال ذلك؟ يمكنك إخبار IDA أن هذا هو التبديل: تحرير -> أخرى -> تحديد لغة التبديل.

البحث عن نقاط الضعف في متصفح UC

الكود الناتج فظيع. ولكن، أثناء شق طريقك عبر الغابة، يمكنك ملاحظة استدعاء وظيفة مألوفة لنا بالفعل sub_6115C:

البحث عن نقاط الضعف في متصفح UC

كان هناك مفتاح تم فيه فك التشفير في الحالة 3 باستخدام خوارزمية RC4. وفي هذه الحالة، يتم ملء البنية التي تم تمريرها إلى الوظيفة من المعلمات التي تم تمريرها إليها doCommandNative. دعونا نتذكر ما كان لدينا هناك com.magicInt بالقيمة 16. ننظر إلى الحالة المقابلة - وبعد عدة انتقالات نجد الكود الذي يمكن من خلاله التعرف على الخوارزمية.

البحث عن نقاط الضعف في متصفح UC

هذا هو الخدمات المعمارية والهندسية!

الخوارزمية موجودة، كل ما تبقى هو الحصول على معلماتها: الوضع والمفتاح وربما متجه التهيئة (يعتمد وجودها على وضع تشغيل خوارزمية AES). يجب أن يتم تشكيل البنية معهم في مكان ما قبل استدعاء الوظيفة sub_6115C، ولكن هذا الجزء من الكود غامض بشكل خاص، لذلك نشأت فكرة تصحيح الكود بحيث يتم تفريغ جميع معلمات وظيفة فك التشفير في ملف.

رقعة

لكي لا تكتب كل أكواد التصحيح بلغة التجميع يدويًا، يمكنك تشغيل Android Studio، وكتابة وظيفة هناك تتلقى نفس معلمات الإدخال مثل وظيفة فك التشفير الخاصة بنا وتكتب في ملف، ثم انسخ وألصق الكود الذي سيفعله المترجم يولد.

اهتم أصدقاؤنا من فريق UC Browser أيضًا بسهولة إضافة التعليمات البرمجية. دعونا نتذكر أنه في بداية كل وظيفة لدينا رمز غير مهم يمكن استبداله بسهولة بأي رمز آخر. مريح للغاية 🙂 ومع ذلك، في بداية الوظيفة المستهدفة، لا توجد مساحة كافية للكود الذي يحفظ جميع المعلمات في ملف. اضطررت إلى تقسيمها إلى أجزاء واستخدام كتل البيانات المهملة من الوظائف المجاورة. كان هناك أربعة أجزاء في المجموع.

الجزء الأول:

البحث عن نقاط الضعف في متصفح UC

في بنية ARM، يتم تمرير معلمات الوظيفة الأربعة الأولى عبر السجلات R0-R3، ويتم تمرير الباقي، إن وجد، عبر المكدس. يحمل سجل LR عنوان المرسل. يجب حفظ كل هذا حتى تتمكن الوظيفة من العمل بعد تفريغ معلماتها. نحتاج أيضًا إلى حفظ كافة السجلات التي سنستخدمها في العملية، لذلك نقوم بإجراء PUSH.W {R0-R10,LR}. في R7 نحصل على عنوان قائمة المعلمات التي تم تمريرها إلى الوظيفة عبر المكدس.

استخدام الوظيفة الدالة fopen دعونا نفتح الملف /data/local/tmp/aes في وضع "أب".
أي للإضافة. في R0 نقوم بتحميل عنوان اسم الملف، في R1 - عنوان السطر الذي يشير إلى الوضع. وهنا تنتهي التعليمات البرمجية المهملة، لذلك ننتقل إلى الوظيفة التالية. لكي تستمر في العمل، نضع في البداية الانتقال إلى الكود الحقيقي للوظيفة، متجاوزًا القمامة، وبدلاً من القمامة نضيف استمرارًا للتصحيح.

البحث عن نقاط الضعف في متصفح UC

نحن نتصل الدالة fopen.

المعلمات الثلاثة الأولى للوظيفة AES لديك نوع مادبا. نظرًا لأننا حفظنا السجلات في المكدس في البداية، فيمكننا ببساطة تمرير الوظيفة com.fwrite عناوينهم على المكدس.

البحث عن نقاط الضعف في متصفح UC

بعد ذلك لدينا ثلاث هياكل تحتوي على حجم البيانات ومؤشر للبيانات الخاصة بالمفتاح ومتجه التهيئة والبيانات المشفرة.

البحث عن نقاط الضعف في متصفح UC

في النهاية، قم بإغلاق الملف واستعادة السجلات ونقل التحكم إلى الوظيفة الحقيقية AES.

نقوم بتجميع ملف APK مع مكتبة مصححة، وتوقيعه، وتحميله على الجهاز/المحاكي، وتشغيله. نرى أنه يتم إنشاء ملف التفريغ الخاص بنا، ويتم كتابة الكثير من البيانات هناك. يستخدم المتصفح التشفير ليس فقط لحركة المرور، وكل التشفير يمر عبر الوظيفة المعنية. ولكن لسبب ما، لا توجد البيانات اللازمة، والطلب المطلوب غير مرئي في حركة المرور. لكي لا ننتظر حتى يتنازل متصفح UC لتقديم الطلب اللازم، فلنأخذ الاستجابة المشفرة من الخادم الذي تلقيناه سابقًا ونقوم بتصحيح التطبيق مرة أخرى: أضف فك التشفير إلى 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/"، أنشئ مجلدًا بهذا الاسم في ملف APK وأضف شهادة متصفح السنجاب هناك.

نحن نجمع، نوقع، نثبت، نطلق. البنغو! لدينا المفتاح!

MITM

لقد تلقينا مفتاحًا ومتجه تهيئة مساوٍ للمفتاح. دعنا نحاول فك تشفير استجابة الخادم في وضع CBC.

البحث عن نقاط الضعف في متصفح UC

نرى عنوان URL للأرشيف، وهو شيء مشابه لـ MD5، و"extract_unzipsize" ورقم. نحن نتحقق: MD5 للأرشيف هو نفسه، وحجم المكتبة التي تم فكها هو نفسه. نحن نحاول تصحيح هذه المكتبة وإعطائها للمتصفح. لإظهار أن مكتبتنا المصححة قد تم تحميلها، سنطلق نية لإنشاء رسالة نصية قصيرة تحتوي على النص "PWNED!" سنقوم باستبدال استجابتين من الخادم: Puds.ucweb.com/upgrade/index.xhtml ولتنزيل الأرشيف. في الأول نستبدل MD5 (لا يتغير الحجم بعد التفريغ)، وفي الثانية نعطي الأرشيف بالمكتبة المصححة.

يحاول المتصفح تنزيل الأرشيف عدة مرات، وبعد ذلك يعطي خطأ. على ما يبدو شيئا
لا يحب. نتيجة لتحليل هذا التنسيق الغامض، اتضح أن الخادم ينقل أيضًا حجم الأرشيف:

البحث عن نقاط الضعف في متصفح UC

تم ترميزه في LEB128. بعد التصحيح، تغير حجم الأرشيف مع المكتبة قليلاً، لذلك اعتبر المتصفح أن الأرشيف تم تنزيله بشكل ملتوي، وبعد عدة محاولات ألقى خطأ.

نعدل حجم الأرشيف... و- النصر! 🙂 النتيجة في الفيديو.

https://www.youtube.com/watch?v=Nfns7uH03J8

العواقب ورد فعل المطور

وبنفس الطريقة، يمكن للمتسللين استخدام الميزة غير الآمنة لمتصفح UC لتوزيع المكتبات الضارة وتشغيلها. ستعمل هذه المكتبات في سياق المتصفح، وبالتالي ستتلقى جميع أذونات النظام الخاصة به. ونتيجة لذلك، القدرة على عرض نوافذ التصيد، وكذلك الوصول إلى ملفات العمل الخاصة بالسنجاب الصيني البرتقالي، بما في ذلك تسجيلات الدخول وكلمات المرور وملفات تعريف الارتباط المخزنة في قاعدة البيانات.

لقد تواصلنا مع مطوري متصفح UC وأبلغناهم بالمشكلة التي وجدناها، وحاولنا الإشارة إلى الثغرة وخطورتها، لكنهم لم يناقشوا معنا أي شيء. وفي الوقت نفسه، استمر المتصفح في التباهي بميزاته الخطيرة على مرأى من الجميع. لكن بمجرد كشفنا عن تفاصيل الثغرة، لم يعد من الممكن تجاهلها كما كان من قبل. كان 27 مارس
تم إطلاق إصدار جديد من متصفح UC 12.10.9.1193، والذي يصل إلى الخادم عبر HTTPS: Puds.ucweb.com/upgrade/index.xhtml.

بالإضافة إلى ذلك، بعد "الإصلاح" وحتى وقت كتابة هذا المقال، أدت محاولة فتح ملف PDF في المتصفح إلى ظهور رسالة خطأ تحتوي على النص "عفوًا، حدث خطأ ما!" ولم يتم تقديم طلب إلى الخادم عند محاولة فتح ملف PDF، ولكن تم تقديم طلب عند تشغيل المتصفح، مما يشير إلى استمرار القدرة على تنزيل التعليمات البرمجية القابلة للتنفيذ في انتهاك لقواعد Google Play.

المصدر: www.habr.com

إضافة تعليق