مقدمة
في منتصف شهر أبريل ،
نشرنا أخبارًا عن نظام تروجان
Android.InfectionAds.1 ، الذي استغل العديد من نقاط الضعف الحرجة في نظام Android. واحد منهم ، CVE-2017-13156 (المعروف أيضًا باسم
Janus ) ، يتيح للبرامج الضارة إصابة ملفات APK دون الإضرار بالتوقيع الرقمي. والآخر هو CVE-2017-13315. إنه يمنح امتيازات طروادة الممتدة ، بحيث يمكنه تثبيت التطبيقات وإلغاء تثبيتها بشكل مستقل عن المستخدم. يتوفر تحليل مفصل لنظام
Android.InfectionAds.1 في
مكتبة الفيروسات الخاصة بنا ؛ بينما نحن هنا سوف نتطرق إلى ثغرة CVE-2017-13315 ونرى ما الذي تفعله.
ينتمي CVE-2017-13315 إلى مجموعة الثغرات التي يطلق عليها EvilParcel. تم العثور عليها في فئات نظام أندرويد المختلفة. تتيح الأخطاء في هذه الفئات إمكانية استبدال المعلومات أثناء تبادل البيانات بين التطبيقات والنظام. وبالتالي يتم منح البرامج الضارة التي تستغل نقاط الضعف في EvilParcel امتيازات أعلى وتصبح قادرة على ما يلي:
- تثبيت وإزالة التطبيقات مع أي أذونات دون تأكيد من المستخدمين ؛
- إصابة البرامج المثبتة على الجهاز واستبدال النسخ الأصلية بنسخ مصابة عند استخدامها مع نقاط الضعف الأخرى ؛
- إعادة تعيين رقم التعريف الشخصي لشاشة القفل على أجهزة Android.
اعتبارا من الآن ، نحن نعرف حوالي 7 نقاط ضعف من هذا النوع:
- CVE-2017-0806 (خطأ في فئة GateKeeperResponse) ، نُشر في أكتوبر 2017 ؛
- CVE-2017-13286 (خطأ في فئة OutputConfiguration ، نُشر في أبريل 2018 ؛
- CVE-2017-13287 (خطأ في فئة VerifyCredentialResponse) ، نُشر في أبريل 2018 ؛
- CVE-2017-13288 (خطأ في فئة PeriodicAdvertizingReport) ، نُشر في أبريل 2018 ؛
- CVE-2017-13289 (خطأ في فئة ParcelableRttResults) ، نُشر في أبريل 2018 ؛
- CVE-2017-13311 (خطأ في فئة SparseMappingTable) ، نُشر في مايو 2018 ؛
- CVE-2017-13315 (خطأ في فئة DcParamObject) ، نُشر في مايو 2018.
تشكل جميعها تهديدًا للأجهزة التي تعمل بنظام Android 5.0 - 8.1 دون تثبيت التحديث الأمني مايو 2018 (أو الأحدث).
المتطلبات الأساسية لضعف EvilParcel
دعونا نرى كيف يمكن أن تظهر نقاط الضعف EvilParcel. بادئ ذي بدء ، نحن بحاجة إلى النظر في بعض ميزات تطبيقات Android. تتفاعل جميع برامج Android مع بعضها البعض ، وكذلك مع نظام التشغيل ، عن طريق إرسال واستقبال الكائنات Intent. يمكن أن تحتوي هذه الكائنات على عدد اعتباطي من أزواج قيمة المفتاح داخل كائن Bundle.
عند نقل نية ، يتم تحويل كائن Bundle (متسلسل) إلى صفيف بايت ملفوف في Parcel ، ثم يتم إلغاء تسلسله تلقائيًا بعد قراءة المفاتيح والقيم من Bundle التسلسلي.
في Bundle ، المفتاح هو السلسلة ، ويمكن أن تكون القيمة أي شيء تقريبًا. على سبيل المثال ، يمكن أن يكون نوعًا بدائيًا أو سلسلة أو حاوية مع أنواع أو سلاسل بدائية. يمكن أن يكون أيضا كائن لا يتجزأ.
وبالتالي ، يمكن أن تحتوي الحزمة على كائن من أي نوع يقوم بتنفيذ واجهة Parcelable. لهذا ، نحتاج إلى تطبيق أساليب writeToParcel () وإنشاءFromFromParcel () لتسلسل الكائن وإلغاء تسلسله.
لتوضيح وجهة نظرنا ، فلنقم بإنشاء حزمة متسلسلة بسيطة. سنكتب رمزًا يضع ثلاثة أزواج ذات قيمة في الحزمة وتسلسلها:

الشكل 1. هيكل كائن حزمة متسلسلة
لاحظ الميزات المحددة لتسلسل الحزمة:
- تتم كتابة جميع أزواج القيمة الرئيسية بالتسلسل ؛
- تتم الإشارة إلى نوع القيمة قبل كل قيمة (13 للصفيف البايت ، 1 للعدد الصحيح ، 0 للسلسلة ، إلخ) ؛
- يشار إلى حجم البيانات متغير الطول قبل البيانات (طول السلسلة ، عدد بايت للصفيف) ؛
- جميع القيم هي 4 بايت محاذاة.
تتم كتابة جميع المفاتيح والقيم في Bundle بالتسلسل بحيث أنه عند الوصول إلى أي مفتاح أو قيمة لكائن Bundle المتسلسل ، يتم إلغاء تسلسل الأخير كليًا ، مع تهيئة جميع الكائنات Parcelable المضمنة أيضًا.
لذلك ، ما يمكن أن يكون مشكلة؟ المشكلة هي أن بعض فئات النظام التي تنفذ Parcelable قد تحتوي على أخطاء في أساليب createFromParcel () و writeToParcel (). في هذه الفئات ، سيختلف عدد البايتات المقروءة في createFromParcel () عن عدد البايتات المكتوبة في writeToParcel (). إذا وضعت كائنًا من هذه الفئة داخل Bundle ، فستتغير حدود الكائن داخل Bundle بعد إعادة الترسيم. هذا يخلق الظروف لاستغلال مشكلة عدم حصانة EvilParcel.
دعونا نرى مثالا للفئة التي تحتوي على هذا الخطأ:
class Demo implements Parcelable { byte[] data; public Demo() { this.data = new byte[0]; } protected Demo(Parcel in) { int length = in.readInt(); data = new byte[length]; if (length > 0) { in.readByteArray(data); } } public static final Creator<Demo> CREATOR = new Creator<Demo>() { @Override public Demo createFromParcel(Parcel in) { return new Demo(in); } }; @Override public void writeToParcel(Parcel parcel, int i) { parcel.writeInt(data.length); parcel.writeByteArray(data); } }
إذا كان حجم صفيف البيانات هو 0 ، فعند إنشاء كائن ، ستتم قراءة int (4 بايت) في createFromParcel () وسيتم كتابة اثنين int (8 بايت) في writeToParcel (). سيتم كتابة int الأول عن طريق استدعاء writeInt بشكل صريح. سيتم كتابة int الثاني عند استدعاء writeByteArray () ، حيث يتم دائمًا كتابة طول المصفوفة قبل المصفوفة في الطرود (انظر الشكل 1).
الحالات التي يكون فيها حجم صفيف البيانات يساوي 0 نادرة جدًا. لكن حتى عند حدوث ذلك ، يستمر البرنامج في العمل ، إذا قمت بنقل كائن متسلسل واحد فقط في وقت واحد (في المثال الخاص بنا ، الكائن Demo). لذلك ، تميل هذه الأخطاء إلى أن تمر مرور الكرام.
سنحاول الآن وضع كائن تجريبي بطول صفيف صفري في الحزمة:

الشكل 2. نتيجة إضافة كائن تجريبي ذو طول صفري إلى الحزمة
نحن تسلسل الكائن:

الشكل 3. كائن حزمة بعد التسلسل
الآن دعونا نحاول إلغاء تسلسلها:

الشكل 4. كائن حزمة بعد إلغاء التسلسل
ماذا نحصل؟ دعونا نلقي نظرة على جزء الطرود:

الشكل 5. هيكل الطرود بعد إلغاء الحزمة
في الشكلين 4 و 5 ، نرى أنه بدلاً من اثنين int ، تمت قراءة int واحدة في طريقة createFromParcel أثناء إلغاء التسلسل. لذلك ، تم قراءة جميع القيم اللاحقة من Bundle بشكل غير صحيح. تمت قراءة قيمة 0x0 عند 0x60 كطول المفتاح التالي. تمت قراءة القيمة 0x1 عند 0x64 كمفتاح. تمت قراءة القيمة 0x31 عند 0x68 كنوع قيمة. الطرود لا تحتوي على قيم بالنوع 0x31 ، لذلك تقرأ readFromParcel () بشق الأنفس استثناء.
كيف يمكن استخدام هذا في الحياة الحقيقية وتصبح نقطة ضعف؟ لنرى! يمكّن الخطأ أعلاه في فئات نظام Parcelable من إنشاء حزم قد تختلف خلال عمليات إلغاء التسلسل الأولى والمتكررة. لإثبات ذلك ، سنقوم بتعديل المثال السابق:
Parcel data = Parcel.obtain(); data.writeInt(3);
ينشئ هذا الرمز حزمة متسلسلة تحتوي على فئة مستضعفة. الآن دعونا نرى ما نحصل عليه بعد تنفيذ هذا الرمز:

الشكل 6. إنشاء حزمة مع فئة الضعيفة
بعد إلغاء التسلسل الأول ، ستحتوي هذه الحزمة على المفاتيح التالية:

الشكل 7. بعد إلغاء تسلسل حزمة مع فئة الضعيفة
الآن سنقوم بتسلسل الحزمة مرة أخرى ، ثم إلغاء تسلسلها مرة أخرى ، وإلقاء نظرة على قائمة المفاتيح:

الشكل 8. نتيجة لإعادة تنظيم وإلغاء التسلسل لحزمة مع فئة الضعيفة
ماذا نرى؟ تحتوي Bundle الآن على المفتاح Hidden (مع قيمة السلسلة "Hi there!") ، والتي لم تكن موجودة من قبل. دعونا نلقي نظرة على جزء Parcel من هذه الحزمة لمعرفة سبب حدوث ذلك:

الشكل 9. بنية الطرود لكائن Bundle مع فئة ضعيفة بعد دورتين من التسلسل وإلغاء التسلسل
هذا هو المكان الذي يمكننا أن نرى فيه نقطة الضعف في EvilParcel. يمكننا على وجه التحديد إنشاء حزمة تحتوي على فئة ضعيفة. سيؤدي تغيير حدود هذه الفئة إلى وضع أي كائن في هذه الحزمة ؛ على سبيل المثال ، نية ، والتي سوف تظهر فقط في الباقة بعد إلغاء التسلسل الثاني. هذا يساعد على إخفاء نية من آليات الأمن OS.
استغلال EvilParcel
قام Android.InfectionAds.1 باستغلال CVE-2017-13315 لتثبيت البرامج وإزالتها بشكل مستقل عن مالكي الأجهزة. لكن كيف؟
في عام 2013 ، تم اكتشاف
الخطأ 7699048 ، المعروف أيضًا باسم Launch AnyWhere. سمحت لتطبيقات الطرف الثالث ببدء أنشطة عشوائية نيابة عن مستخدم نظام أكثر امتيازًا. انظر الرسم البياني أدناه لمعرفة آلية العمل:

الشكل 10. تشغيل الخطأ 7699048
يمكن أن يستخدم تطبيق الاستغلال مشكلة عدم الحصانة هذه لتطبيق خدمة مصادقة الحساب ، المصممة لإضافة حسابات جديدة إلى نظام التشغيل. يساعد الخطأ 7699048 أنشطة بدء التشغيل في تثبيت التطبيقات وإزالتها واستبدالها ، وكذلك إعادة تعيين رمز PIN أو قفل النمط ويسبب المزيد من المتاعب.
شركة جوجل ألغى هذا الانتهاك عن طريق حظر إطلاق نشاط تعسفي من AccountManager. الآن ، يسمح AccountManager فقط بإطلاق الأنشطة الناشئة من نفس التطبيق. لهذا الغرض ، يقوم بفحص ومطابقة التوقيع الرقمي للبرنامج الذي بدأ النشاط مع توقيع التطبيق الذي يوجد به النشاط. يبدو مثل هذا:
if (result != null && (intent = result.getParcelable(AccountManager.KEY_INTENT)) != null) { int authenticatorUid = Binder.getCallingUid(); long bid = Binder.clearCallingIdentity(); try { PackageManager pm = mContext.getPackageManager(); ResolveInfo resolveInfo = pm.resolveActivityAsUser(intent, 0, mAccounts.userId); int targetUid = resolveInfo.activityInfo.applicationInfo.uid; if (PackageManager.SIGNATURE_MATCH != pm.checkSignatures(authenticatorUid, targetUid)) { throw new SecurityException( "Activity to be started with KEY_INTENT must " + "share Authenticator's signatures"); } } finally { Binder.restoreCallingIdentity(bid); } }
يبدو أن المشكلة قد تم حلها ، ولكنها ليست سهلة مثل كل ذلك. اتضح أن مشكلة عدم الحصانة المعروفة ، EvilParcel CVE-2017-13315 ، توفر حلاً! كما نعلم بالفعل ، بعد إصلاح Launch AnyWhere ، يتحقق النظام من التوقيع الرقمي للتطبيق. إذا تم التحقق بنجاح ، يتم نقل الحزمة إلى IAccountManagerResponse.onResult (). في الوقت نفسه ، يتم استدعاء onResult () عبر آلية IPC ، لذلك يتم إجراء تسلسل Bundle مرة أخرى. أثناء تطبيق onResult () ، يحدث ما يلي:
private class Response extends IAccountManagerResponse.Stub { public void onResult(Bundle bundle) { Intent intent = bundle.getParcelable(KEY_INTENT); if (intent != null && mActivity != null) {
ثم ، يقوم Bundle باستخراج مفتاح القصد ، ويتم تشغيل النشاط دون أي تحقق.
وبالتالي ، لبدء نشاط تعسفي مع امتيازات النظام ، ما عليك سوى إنشاء حزمة مع حقل النوايا مخبأة عند إلغاء التسلسل الأول والظهور أثناء إلغاء التسلسل المتكرر.
كما نعلم بالفعل ، يمكن لثغرات EvilParcel القيام بهذه المهمة بالفعل.
في الوقت الحالي ، تم إصلاح جميع الثغرات المعروفة من هذا النوع ضمن فئات Parcelable الضعيفة. ومع ذلك ، قد تظهر فئات مستضعفة جديدة في المستقبل. تطبيق الحزمة وآلية إضافة حسابات جديدة لا تزال كما كانت من قبل. لا يزالون يسمحون لنا بإنشاء هذا الاستغلال الدقيق عند اكتشاف فصول Parcelable القديمة أو الجديدة الضعيفة. علاوة على ذلك ، لا تزال هذه الفئات تنفذ يدويًا ، ويتعين على المبرمج التأكد من أن طول كائن Parcelable المتسلسل يبقى كما هو ، وهو عامل بشري مع كل ما يوحي به. ومع ذلك ، نأمل أن يكون هناك عدد قليل من هذه الأخطاء قدر الإمكان ، وأن ثغرات EvilParcel لن تشكل تهديدًا لمستخدمي Android.
يمكنك التحقق من جهازك المحمول بحثًا عن ثغرات EvilParcel باستخدام
Dr.Web Security Space لنظام Android. سيقوم مدقق الأمان المدمج بالإبلاغ عن المشكلات المكتشفة والتوصية بطرق للتخلص منها.