استكشف خطأ iOS مع Hopper

مرحبا! اسمي ألكسندر نيكيشين ، أقوم بتطوير تطبيقات iOS في Badoo. في المقالة ، سأتحدث عن كيفية تحقيقنا في خطأ في UIKit ، والذي لم ترغب Apple في إصلاحه لمدة ستة أشهر.



بدأ كل شيء في أغسطس 2019 مع الإصدارات التجريبية الأولى من iOS 13. ثم واجهنا مشكلة لأول مرة. في تطبيقات Badoo و Bumble ، نعمل باستمرار على تحسين الواجهات ، على سبيل المثال ، نحاول تحسين عملية التسجيل المملة وغير المحبوبة قدر الإمكان. تعد تلميحات الأدوات التنبؤية المنهجية فوق لوحة المفاتيح طريقة رائعة لتقليل عدد نقرات المستخدم عند إدخال البيانات. ومع ذلك ، في الإصدار الجديد من iOS ، فوجئنا عندما وجدنا أن مطالبات إدخال رقم الهاتف قد اختفت.



عندما ظهرت نسخة GM ، أدركنا أنه لم يتم إصلاح المشكلة. يبدو لنا أن مثل هذا الخطأ الواضح لا يمكن تفويته ببساطة من خلال اختبارات الانحدار ، والتي ربما كانت لدى Apple في ترسانتها ، وبدأنا ننتظر تصحيحًا في أول حزمة تحديث كبيرة. أمل المطورين الآخرين لهذا . ومع ذلك ، مع إصدار الإصدار 13.1 ، لم يتغير شيء ، ولم يكن لدينا خيار سوى فتح الرادار ، وهو ما فعلناه في أوائل أكتوبر. مر الوقت ، خرج iOS 13.2 و 13.3 ، وظل الخطأ لا يزال غير مصحح.

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

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


iOS 12 مقابل iOS 13

أصبح من الواضح على الفور أن Apple في iOS 13 أعادت تصميم تطبيق لوحة المفاتيح وأبرزت المطالبات في وحدة تحكم منفصلة ( UIPredictionViewController). من الواضح أن الاتجاه نحو تطبيق نظام الوحدات والتحلل قد وصل إليه أيضًا (بالمناسبة ، تحدث Artyom Loenko مؤخرًا عن تجربتنا في تطبيقها ). على الأرجح ، بسبب هذا ، كان هناك تراجع في الوظائف. بدأت دائرة البحث في التضييق.

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



يبدو أن هناك فكرة ، من السهل التحقق منها ، باستخدام الأداة القديمة الجيدة التي تخدع وظائف التنفيذ (Swizzling). سنستخدمها بحيث تُرجع الدالة "المشبوهة" دائمًا القيمة الحقيقية:

+(void)swizzleIsVisibleForInputDelegate {
    SEL targetSelector = sel_getUid("isVisibleForInputDelegate:inputViews:");
    Class targetClass = NSClassFromString(@”UIPredictionViewController”);
    if (targetClass == nil) {
        return;
    }
    if (![targetClass instancesRespondToSelector:targetSelector]) {
        return;
    }

    Method method = class_getInstanceMethod(targetClass, targetSelector);
    if (method == NULL) {
        return;
    }

    IMP originalImplementation = method_getImplementation(method);
    IMP newImp = imp_implementationWithBlock(^BOOL(id me, id delegate, id views) {
        //   ,        . 
        BOOL result = ((bool (*)(id,SEL,id,id))originalImplementation)(me, targetSelector, delegate, views);
        if ([delegate isKindOfClass:[UITextField class]] && [delegate keyboardType] == UIKeyboardTypePhonePad) {
            return YES;
        }
        return result;
    });

    method_setImplementation(method, newImp);
}

عند إعادة تشغيل مشروع الاختبار ، اكتشفت أن مطالبات الهاتف عادت إلى iOS 13 وتعمل "في الوضع العادي". قد ينهي هذا التحقيق وربما يستخدم بعناية شديدة هذا الحل الإرشادي المحظور والمحتمل من Apple في تجميع الإصدار مع القدرة على تمكين / تعطيل بعض المستخدمين عن بُعد (للحصول على خيار إدارة الميزات عن بُعد ، يمكنك مشاهدة تسجيل تقرير زميلتي كاترينا تروفيمينكو). ومع ذلك ، لم أتوقف أبدًا عن التساؤل عن سبب إرجاع هذه الوظيفة بشكل خاطئ عند استخدام نوع لوحة مفاتيح الهاتف.

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

كل ذلك من نفسواجهة خاصة منشورة ، يمكنك معرفة ما UIPredictionViewControllerهو جزء من إطار عمل UIKitCore. كل ما تبقى هو العثور عليه وتحميله على Hopper. توجد ملفات إطار العمل الثنائي في أعماق Xcode. هنا ، على سبيل المثال ، هو المسار الكامل لـ UIKitCore الذي نحتاجه:

/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework/UIKitCore

نقوم بعمل ملف السحب والإفلات في Hopper ، ونؤكد الإجراء في مربع حوار النظام وننتظر اكتمال الفهرسة (في حالة إطار عمل كبير مثل UIKit ، يمكن أن يستغرق ذلك من أربع إلى ست دقائق).

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



يوجد في شريط المهام العلوي أزرار لتبديل أوضاع عرض التعليمات البرمجية. من اليسار الى اليمين:



  • وضع ASM - رمز المجمع ؛
  • وضع CFG - كود المجمع في شكل مخطط كتلة (شجرة) ، حيث يتم دمج أجزاء الرمز في كتل ، وتظهر التحولات كفروع ؛
  • وضع الكود الزائف - الكود الزائف الناتج (سنتحدث عنه أكثر أدناه) ؛
  • الوضع السداسي هو تمثيل سداسي عشري لملف ثنائي أو abracadabra ، وهو ما لا يساعدنا كثيرًا في بحثنا.

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



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

سياق صغير: في rbx التسجيل أعلى قليلاً في الكود (الذي حذفته من أجل البساطة) هو رابط للكائن الذي ينفذ البروتوكول UITextInputTraits_Private(هذه نسخة موسعة من البروتوكول العام UITextInputTraits).

لذا ، فإن أول الشروط هو التحقق من أن المطالبات لا تخفي من خلال تكوين حقل الإدخال. في التصحيح ، يمكنك أن ترى أنه يعمل: الخاصيةhidePredictionإرجاع كاذبة. الشرط الثاني هو التحقق من أن لوحة المفاتيح ليست في وضع "الانقسام" (على جهاز iPad ، اسحب الزر الأيمن السفلي لأعلى ، فقط 2-3٪ من المستخدمين يعرفون هذا الشيء). مع هذا أيضًا ، كل شيء في محله.

استمر. في المرحلة التالية ، يبدأ التلاعب keyboardType، مما يلمح إلينا أن الحقيقة في مكان ما قريب. يتم التحقق أولاً من أن التيار الحالي keyboardTypeأقل من أو يساوي 0xb (أو 11 بالتنسيق العشري). إذا فتحنا الإعلان في Xcode UIKeyboardType، فسنرى أن هناك 13 نوعًا من لوحات المفاتيح ، وواحد منهم (UIKeyboardTypeAlphabet) تم إهمالها وإعلانها كمرجع لنوع آخر. أي أن هناك 12 نوعًا في التعداد: إذا بدأت من 0 ، فإن الأخير سيكون له قيمة تساوي 11. وبعبارة أخرى ، يتحقق الرمز من القيمة في شكل فحص تجاوز ، ومرة ​​أخرى ، يمر بنجاح.

علاوة على ذلك ، نرى حالة غريبة جدًا if (!COND)، ولم أتمكن منذ فترة طويلة من فهم ما يتم فحصه ، نظرًا لأن متغير COND لم يتم الإعلان عنه في أي مكان آخر في الرمز. علاوة على ذلك ، أظهرت نقاط التوقف الرمزية الخاصة بي أن الفشل في تحقيق هذا الشرط هو بالضبط الذي يؤدي إلى الخروج المبكر من الوظيفة. وهنا ليس لدينا خيار سوى العودة إلى وضع ASM ودراسة هذا الجزء من التعليمات البرمجية في شكل المجمع.

للعثور على هذا الشرط في قائمة ASM ، يمكنك استخدام خيار "لا يوجد تكرار للشفرة" ، والذي سيعرض الرمز الزائف ليس في شكل رمز مصدر بشروط if-else ، ولكن في شكل كتل فريدة من التعليمات البرمجية والانتقالات في شكل goto. في هذه الحالة ، سنرى موضع بداية هذه الكتل في الملف الثنائي ونستخدم هذا المؤشر للبحث في وضع ASM. لذلك اكتشفت أن الكتلة التي نحن مهتمون بها موجودة في fe79f4:



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



نأتي إلى الجزء الأكثر صعوبة ، حيث سنقوم بتحليل الأسطر الثلاثة لرمز المجمع.

لقد تعرفت على السطر الأول من ذاكرتي (بفضل دروس المجمع في بلدي MIET الأصلي ، وأخيرًا جاءت اللحظة في حياتي عندما أصبح التعليم العالي في متناول اليد!). كل شيء بسيط للغاية هنا: يتم وضع ثابت 0x930 (100100110000 بتنسيق ثنائي) في سجل ecx.

في السطر الثاني، ونحن نرى تعليمات BT تنفيذها على excو السجلات eax. قيمة واحدة نعرفها بالفعل ، يمكن رؤية قيمة الثانية في لقطة الشاشة السابقة: rax = [rbx keyboardType]- تحتوي على نوع لوحة المفاتيح الحالي. rax- هذا هو السجل 64 بت بأكمله eax- هذا هو جزء 32 بت.

مع تحديد البيانات ، يبقى فهم منطق الفريق. تقودنا Google إلى هذا الوصف :
Selects the bit in a bit string (specified with the first operand, called the bit base) at the bit-position designated by the bit offset operand (second operand) and stores the value of the bit in the CF flag.

تستخرج التعليمات قليلاً من المعامل الأول (الثابت 0x930) في الموضع المحدد بواسطة المعامل الثاني (نوع لوحة المفاتيح) ، ويضعها في CF (إشارة الحمل ). أي ، نتيجة لذلك ، سيحتوي CF على 0 أو 1 ، اعتمادًا على نوع لوحة المفاتيح.

ننتقل إلى العملية الأخيرة jb، يحتوي على الوصف التالي : من

Jump short if below (CF=1)

السهل تخمين أن هناك انتقال إلى الوظيفة (الخروج المبكر) إذا كان CF هو 1.

في هذه المرحلة ، يبدأ اللغز في الإضافة. لدينا قناع بت 100100110000 (يحتوي على 12 بت وفقًا لعدد أنواع لوحة المفاتيح المتوفرة) ، ويحدد حالة الخروج المبكر. الآن ، إذا تحققنا من توافر تلميحات لجميع أنواع لوحات المفاتيح بترتيب تصاعدي rawValue، فسيكون كل شيء في مكانه.



في نظام التشغيل iOS 12 ، لن نجد مثل هذا المنطق - تعمل تلميحات هناك لأي نوع من لوحة المفاتيح. أظن أنه في iOS 13 ، قررت Apple تعطيل تلميحات لوحات المفاتيح الرقمية ، وهو أمر مفهوم من حيث المبدأ: لا يمكنني التوصل إلى سيناريو حيث يحتاج النظام إلى المطالبة بالأرقام. على ما يبدو ، عن طريق الخطأ ، ضربت أيضًا "اليد الساخنة" UIKeyboardTypePhonePad، والتي تشبه إلى حد كبير لوحة المفاتيح الرقمية العادية. في الوقت نفسه UIKeyboardTypeNamePhonePad، من خلال الجمع بين لوحة مفاتيح QWERTY للبحث عن جهات الاتصال الهاتفية ولوحة المفاتيح الرقمية المعطلة نفسها ، استمر في إظهار المطالبات.

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

All Articles