كسر متصفح UC



مقدمة


في نهاية شهر مارس ، أبلغنا عن الإمكانات الخفية لتنزيل وتشغيل شفرة لم يتم التحقق منها في متصفح UC. اليوم سنبحث بالتفصيل كيف يحدث وكيف يمكن للمتسللين استخدامه.

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

أثناء كتابة هذا المقال ، تم تثبيت UC Browser 500،000،000 مرة من Google Play. هذا مثير للإعجاب نظرًا لأن Google Chrome فقط هو الذي تمكن من تحقيق ذلك. من بين المراجعات ، يمكنك رؤية الكثير من شكاوى المستخدمين بشأن الإعلانات وإعادة توجيهها إلى تطبيقات أخرى على Google Play. كان هذا هو سبب دراستنا: لقد أردنا معرفة ما إذا كان متصفح UC يقوم بشيء خاطئ. وهذا هو! التطبيق قادر على تنزيل وتشغيل التعليمات البرمجية القابلة للتنفيذ ، والتي تنتهك سياسة Google Play لنشر التطبيق . ولا يقوم UC Browser بتنزيل التعليمات البرمجية القابلة للتنفيذ فقط ؛ يفعل هذا بشكل غير آمن ، والتي يمكن استخدامها لهجوم MitM. دعونا نرى ما اذا كنا نستطيع استخدامها بهذه الطريقة.

ينطبق كل ما يلي على إصدار متصفح UC الذي تم توزيعه عبر Google Play في وقت دراستنا:

package: com.UCMobile.intl versionName: 12.10.8.1172 versionCode: 10598 sha1 APK-file: f5edb2243413c777172f6362876041eb0c3a928c 

ناقل الهجوم


يحتوي بيان UC Browser على خدمة لها اسم منبهة com.uc.deployment.UpgradeDeployService .

  <service android:exported="false" android:name="com.uc.deployment.UpgradeDeployService" android:process=":deploy" /> 

عند بدء تشغيل هذه الخدمة ، يقدم المستعرض طلبًا POST puds.ucweb.com/upgrade/index.xhtml يمكن رؤيته في حركة المرور لبعض الوقت بعد الإطلاق. استجابة لذلك ، قد يتلقى المستعرض أمرًا لتنزيل أي تحديث أو وحدة نمطية جديدة. أثناء تحليلنا ، لم نتلق مطلقًا مثل هذه الأوامر من الخادم ، لكننا لاحظنا أنه عند محاولة فتح ملف PDF في المتصفح ، فإنه يكرر الطلب على العنوان أعلاه ، ثم يقوم بتنزيل مكتبة أصلية. لمحاكاة أي هجوم ، قررنا استخدام هذه الميزة في متصفح UC - القدرة على فتح ملفات PDF باستخدام مكتبة أصلية - غير موجودة في ملف APK ، ولكن يمكن تنزيلها من الإنترنت. من الناحية الفنية ، يمكن لـ UC Browser تنزيل شيء ما دون إذن المستخدم عند إعطاء استجابة مناسبة لطلب تم إرساله عند بدء التشغيل. لكن لهذا نحتاج إلى دراسة بروتوكول التفاعل مع الخادم بمزيد من التفصيل ، لذلك اعتقدنا أنه من الأسهل ربط الاستجابة وتحريرها ثم استبدال المكتبة اللازمة لفتح ملفات PDF بشيء مختلف.

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



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

طلب جزء

جزء الاستجابة






المكتبة مضغوطة في ملف ZIP ولا يتم تشفيرها.



البحث عن كود فك شفرة المرور


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

  public final void e(l arg9) { int v4_5; String v3_1; byte[] v3; byte[] v1 = null; if(arg9 == null) { v3 = v1; } else { v3_1 = arg9.iGX.ipR; StringBuilder v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]product:"); v4.append(arg9.iGX.ipR); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]version:"); v4.append(arg9.iGX.iEn); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]upgrade_type:"); v4.append(arg9.iGX.mMode); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]force_flag:"); v4.append(arg9.iGX.iEo); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_mode:"); v4.append(arg9.iGX.iDQ); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_type:"); v4.append(arg9.iGX.iEr); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_state:"); v4.append(arg9.iGX.iEp); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]silent_file:"); v4.append(arg9.iGX.iEq); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apk_md5:"); v4.append(arg9.iGX.iEl); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_type:"); v4.append(arg9.mDownloadType); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_group:"); v4.append(arg9.mDownloadGroup); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]download_path:"); v4.append(arg9.iGH); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_child_version:"); v4.append(arg9.iGX.iEx); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_series:"); v4.append(arg9.iGX.iEw); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_arch:"); v4.append(arg9.iGX.iEt); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp3:"); v4.append(arg9.iGX.iEv); v4 = new StringBuilder("["); v4.append(v3_1); v4.append("]apollo_cpu_vfp:"); v4.append(arg9.iGX.iEu); ArrayList v3_2 = arg9.iGX.iEz; if(v3_2 != null && v3_2.size() != 0) { Iterator v3_3 = v3_2.iterator(); while(v3_3.hasNext()) { Object v4_1 = v3_3.next(); StringBuilder v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_name:"); v5.append(((au)v4_1).getName()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_name:"); v5.append(((au)v4_1).aDA()); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_ver_code:"); v5.append(((au)v4_1).gBl); v5 = new StringBuilder("["); v5.append(((au)v4_1).getName()); v5.append("]component_req_type:"); v5.append(((au)v4_1).gBq); } } j v3_4 = new j(); 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); } 

يمكننا أن نرى هذا هو المكان الذي يتم فيه تقديم طلب 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 = 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"); } } 

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

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

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

دعنا نعود إلى تحليل الرمز. بعد بضع القفزات ، وصلنا إلى الأسلوب باستخدام اسم telltale ، 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.ai; // ... 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 ، وهو رقم الأمر على ما يبدو.

بعد سلسلة القفزات التالية ، نجد فئة تنفذ واجهة RouterComponent وطريقة 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); } } 

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



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



ثم نفتح الملف في المؤسسة الدولية للتنمية مرة أخرى.

لدينا طريقتان لإخبار جهاز Java الظاهري بالضبط أين تحتوي المكتبة الأصلية على تنفيذ الطريقة المعلنة على أنها لغة أصلية في كود Java. الأول هو منحها اسمًا مثل هذا: Java_package_name_ClassName_methodName . الثانية لتسجيلها عند تحميل المكتبة (في وظيفة JNI_OnLoad) عن طريق استدعاء وظيفة RegisterNatives. في حالتنا ، إذا استخدمت الطريقة الأولى ، يجب أن يكون الاسم كما يلي: Java_com_taobao_wireless_security_adapter_JNICLibrary_doCommandNative . لا تحتوي قائمة الوظائف التي تم تصديرها على هذا الاسم ، مما يعني أننا بحاجة إلى البحث عن RegisterNatives. وبالتالي ، نذهب إلى وظيفة JNI_OnLoad ونرى ما يلي:



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

الآن دعنا نذكرك أن معالجات ARM لها وضعان ومجموعتان من التعليمات - ARM و Thumb. تحدد بت ذات الترتيب المنخفض من العنوان مجموعة الإرشادات التي سيستخدمها المعالج ، أي أن العنوان هو في الواقع 0xB13A ، بينما تشير القيمة في بت ذات الترتيب المنخفض إلى وضع الإبهام.
تتم إضافة "محول" مشابه وبعض البيانات المهملة الدلالية إلى بداية كل وظيفة في هذه المكتبة. لكننا لن نتناولها بالتفصيل. فقط تذكر أن البداية الحقيقية لجميع الوظائف تقريبًا هي أبعد قليلاً.

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



بدءًا من 0xB144 ، لدينا بوضوح الجدول. ولكن ماذا عن sub_494C؟



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



الآن سيكون هناك انتقال في الإزاحة مع الفهرس 0x20 من الجدول.
استنادا إلى حجم الجدول ، سيكون هناك العديد من هذه التحولات في التعليمات البرمجية. لذلك نود التعامل مع هذا تلقائيًا وتجنب حساب العناوين يدويًا. وبالتالي ، البرامج النصية والقدرة على تصحيح الكود في المؤسسة الدولية للتنمية تأتي لإنقاذنا:

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

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



مرة أخرى ، المؤسسة الدولية للتنمية لا تتعرف على هذا المكان كرمز. نحن نساعدها ونرى بنية أخرى هناك:



لا تظهر التعليمات التي تتبع BLX ذات مغزى كبير ؛ هم أشبه نوعا من الإزاحة. ننظر إلى sub_4964:



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



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

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

نضع المؤشر في بداية الهيكل الذي نريد استبداله (على سبيل المثال 0xB4B2) وتشغيل البرنامج النصي:



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



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



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

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

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



بعد تصحيح كل شيء في هذه الوظيفة ، يمكننا توجيه المؤسسة الدولية للتنمية إلى بدايتها الحقيقية. ستقوم بجمع رمز الوظيفة بالكامل قطعة قطعة وسنكون قادرين على فك تشفيرها باستخدام 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. ! الرائعة نحن على الطريق الصحيح.

شجرة القيادة


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

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

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

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

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

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



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

مزيد من التعتيم


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

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

نضغط بعد ذلك على مؤشرين في R7 و R4:



ينتقل المؤشر الأول إلى R11:



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



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

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



الكود الناتج مروع.ومع ذلك ، يمكننا أن نرى استدعاء الدالة sub_6115C المألوفة في التشابكات الخاصة بها:



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



إنه AES!

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

بقعة


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

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



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

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



ثم ندعو fopen.

المعلمات الثلاثة الأولى للدالة aes هي من النوع int. نظرًا لأننا دفعنا السجلات إلى المكدس في البداية ، يمكننا ببساطة نقل عناوينها في المكدس إلى دالة fwrite.



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



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

  const/16 v1, 0x62 new-array v1, v1, [B fill-array-data v1, :encrypted_data const/16 v0, 0x1f invoke-static {v0, v1}, Lcom/uc/browser/core/d/c/g;->j(I[B)[B move-result-object v1 array-length v2, v1 invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String; move-result-object v2 const-string v0, "ololo" invoke-static {v0, v2}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I 

نقوم بتجميعه وتوقيعه وتثبيته وتشغيله. وبالتالي ، نحصل على NullPointerException منذ ترجع الطريقة قيمة فارغة.

بعد تحليل الشفرة بشكل أكبر ، وجدنا وظيفة ذات سلاسل مثيرة إلى حد ما: "META-INF /" و ".RSA". يبدو أن التطبيق يتحقق من شهادته ، أو حتى ينشئ مفاتيح منه. لا نريد حقًا البحث في ما يحدث مع الشهادة ، لذلك دعونا نعطيها الشهادة الصحيحة. سنقوم بتصحيح السلسلة المشفرة ، لذا بدلاً من "META-INF /" نحصل على "BLABLINF /" ، نقوم بإنشاء مجلد بهذا الاسم في ملف APK ، وحفظ شهادة المتصفح فيه.

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

MITM


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



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

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



يتم ترميز LEB128. يغير التصحيح حجم المكتبة المضغوطة قليلاً ، لذا قرر المستعرض كسر الأرشيف عند التنزيل وعرض خطأ بعد عدة محاولات.
لذلك نحن إصلاح حجم الأرشيف و ... فويلا! :) انظر النتيجة في الفيديو.


العواقب واستجابة المطور


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

لقد اتصلنا بمطوري UC Browser وأبلغنا بالمشكلة التي وجدناها ، وحاولنا توضيح مشكلة عدم الحصانة وخطرها ، لكنهم رفضوا مناقشة الأمر. وفي الوقت نفسه ، ظل المستعرض ذو الوظيفة الخطيرة في مرأى من الجميع. رغم أننا كشفنا تفاصيل الثغرة الأمنية ، إلا أنه كان من المستحيل تجاهلها كما كان من قبل. تم إصدار نسخة جديدة من متصفح UC Browser 10.10.9.1193 في 27 مارس ، والتي وصلت إلى الخادم عبر HTTPS puds.ucweb.com/upgrade/index.xhtml. بالإضافة إلى ذلك ، بين "إصلاح الأخطاء" والوقت الذي كتبنا فيه هذه المقالة ، أدت محاولة فتح ملف PDF في المستعرض إلى ظهور رسالة خطأ تحتوي على نص "عفوًا ، هناك خطأ ما". لم يكن هناك طلب على الخادم عند محاولة فتح ملف PDF. تم تنفيذ ذلك عند بدء التشغيل ، وهو ما يشير إلى أن القدرة على تنزيل الكود القابل للتنفيذ بما يخالف سياسات Google Play لا يزال موجودًا.

Source: https://habr.com/ru/post/ar452076/


All Articles