
الإفصاح عن ثغرات كاميرات Accfly: CVE-2020-25782, CVE-2020-25783, CVE-2020-25784, CVE-2020-25785.
في بداية عام 2020، في مكان عملي السابق، أتيحت لي فرصة المشاركة في حدث داخلي بأسلوب pwn2own. كانت هناك عدة أهداف متاحة، لكن أكثر ما أثار اهتمامي هو كاميرا Accfly اللاسلكية الأمنية. للأسف لم أتمكن من إنهاء بحثي للحدث الفعلي، ولكن نظرًا لعدم وجود محاولات أخرى على هذا الجهاز، واصلت العمل عليه.
كان التركيز الرئيسي للبحث على الثغرات التي قد تؤدي إلى تنفيذ التعليمات البرمجية عن بُعد (RCE). هذا النوع من الثغرات يسمح للمهاجم بالسيطرة الكاملة على الجهاز، وفي حالة كاميرا الفيديو قد يؤدي إلى اختراق كامل لخصوصية المالك. للأسف، تبين أن البرنامج الثابت للجهاز مليء بمثل هذه المشكلات.
أولًا، لا يوفر الجهاز أي مصادقة. ونتيجة لذلك، يمكن للمهاجم القادر على الاتصال به الوصول إليه وإعادة ضبطه بحرية. في أبسط صوره، من الممكن إعادة تشغيل الجهاز باستمرار، مما يجعله غير قابل للاستخدام تمامًا للمستخدم الشرعي. نطاق هذا الهجوم محدود نوعًا ما، لأن الجهاز مصمم ليعمل داخل شبكة WiFi، عادةً خلف NAT، وبالتالي لا يمكن الوصول إليه مباشرة من الإنترنت. ومع ذلك، فإن غياب التشفير بين الجهاز وتطبيق الهاتف الذكي الخاص بمالكه، بالإضافة إلى استخدام خادم البائع كوسيط للاتصال، يخلق فرصة لهجمات الوسيط (MitM) أو التلاعب بنظام DNS، والتي يمكنها اختراق قيد NAT الخاص بشبكة WiFi.
علاوة على ذلك، يستخدم التطبيق بروتوكولًا ثنائيًا خاصًا للاتصالات. وقد تم تنفيذه بمزيج من C وC++، وتبين أنه مليء بوظائف معالجة السلاسل النصية غير الآمنة. يحتوي الملف التنفيذي الرئيسي على كمية هائلة من التعليمات البرمجية غير المستخدمة، مما يشير إلى أنه يُعاد استخدامه على أجهزة أخرى. هذا يجعل الصيانة أصعب ويزيد من سطح الهجوم. كما لا يقوم التطبيق بتمكين أي آليات حماية حديثة، والتي قد تحميه من العديد من تقنيات الاستغلال الشائعة. بالإضافة إلى ذلك، فهو لا يحد حتى من صلاحيات المستخدم، حيث يعمل بصلاحيات الجذر (root) - بأعلى الامتيازات المتاحة.
كنتيجة لهذا البحث، تم توثيق الثغرات الأربع التالية.
CNetClientManage::ServerIP_Proto_Set عند معالجة الرسائل الواردةCNetClientTalk::OprMsg عند معالجة الرسائل الواردةCNetClientGuard::SubOprMsg عند معالجة الرسائل الواردةCFtpProtocol::FtpLogin أثناء إجراء التحديثبالنسبة لثلاث منها، تم تطوير استغلالات تنفيذ التعليمات البرمجية عن بُعد (RCE)، والتي تسمح للمهاجم بالسيطرة الكاملة على الجهاز. ومع ذلك، نظرًا لعدم وجود استجابة من البائع لمحاولات الإبلاغ عن الثغرات، يحتوي هذا المستودع على استغلالات PoC محدودة فقط، والتي تقوم فقط بتعطيل التطبيق.
تم العثور على هذه المشكلات في إصدار البرنامج V3.10.73 وتم التحقق منها في إصدار البرنامج V4.15.77، وهو أحدث إصدار متاح وقت نشر هذا التقرير (26 يناير 2021).
إذا كانت لديك أي أسئلة، فلا تتردد في التواصل معي عبر البريد الإلكتروني (انظر التزام git) أو عبر مشكلات Github. إذا كان لديك جهاز إنترنت الأشياء (IoT) تعتقد أنه قد يكون مثيرًا للاختراق، أو كنت تبحث عن باحث أمني، أو تريد فقط إلقاء التحية، يسعدني التواصل معك. يمكنك أيضًا شراء فنجان قهوة لي!
الجهاز المستهدف هو كاميرا فيديو يتم التحكم فيها من التطبيق المحمول المصاحب. بدأ تحليلي بحركة مرور شبكة الكاميرا واستمر في البرنامج الثابت للكاميرا. يتم استخدام بروتوكول ثنائي خاص لجميع الاتصالات. تُرسل الأوامر إما مباشرة إلى الجهاز المحمول عندما يكون في نفس الشبكة، أو تمر عبر خادم الشركة المصنعة للجهاز. يستمع برنامج الكاميرا نفسه على منافذ TCP متعددة (23456،34567) وUDP (34568، 34569). لا يوجد تشفير ولا مصادقة لحركة مرور الشبكة، مما يسمح بهجمات الوسيط (MitM) أو الوصول المباشر عند تعرض الكاميرا عبر الشبكة. يبدو من المحتمل أن الوصول إلى بث الفيديو ممكن دون مصادقة أيضًا، لكنني لم أقم بعكس هندسة ما يكفي من البروتوكول الخاص لتجربة ذلك.
بعد نظرة عامة موجزة على الاتصالات، كانت الخطوة التالية هي محاولة الوصول إلى البرنامج الثابت للجهاز. كانت محاولتي الأولى تنزيله مباشرة عن طريق اختطاف عملية تحديث الجهاز، ولكن لم يحدث شيء من هذا القبيل في حركة مرور الشبكة. لكنت علقت في هذه المرحلة، لولا المساعدة الضرورية للغاية من زميل قام باستخراج البرنامج الثابت من ذاكرة الفلاش، مما سمح لي بمواصلة هذا البحث.
تبين أن البرنامج الثابت يعمل بنظام Linux على معالج MIPS صغير النمط (little-endian). هناك عملية واحدة مثيرة للاهتمام، تُسمى Alloca، وهي المسؤولة عن التقاط الفيديو وتتعامل أيضًا مع جميع اتصالات الشبكة. التطبيق مكتوب بلغة C++ ويحتوي على الكثير من التعليمات البرمجية غير المستخدمة على هذا الجهاز. يشير هذا إلى أن نفس البرنامج يُستخدم على أجهزة مختلفة أيضًا.
على الرغم من اكتشاف هذه المشكلة أخيرًا، إلا أنها حاسمة للاستغلال الفعلي لمعظم المشكلات الأخرى، لأنها تنبع من استخدام دوال سلاسل نصية غير آمنة في لغة C. بينما توجد عدة تقنيات يمكن استخدامها لتنفيذ التعليمات البرمجية بنجاح في سيناريوهات مماثلة، فإن التطبيق مُنشأ بطريقة تجعل معظمها عديم الفائدة. المشكلة الرئيسية هي أن كود وبيانات Alloca مخصصة بشكل ثابت على عناوين منخفضة ( < 0x01000000). وبالتالي فإن محاولات إعادة استخدام الكود الموجود (أي ROP وما شابه) ليست مفيدة، لأنها تتطلب القدرة على كتابة عناوين في ذاكرة البرنامج. نظرًا لأن سلاسل لغة C تستخدم \x00 كحرف إنهاء، وتنهي دوال السلاسل المعالجة عند أول بايت من هذا القبيل، فليس من الممكن استخدام أكثر من بايت NULL واحد. بالإضافة إلى ذلك، فإن موقع المكدس عشوائي والتطبيق متعدد الخيوط بشكل كبير، مما يجعل التقنيات الأخرى أقل موثوقية بكثير.
هذه الثغرة هي نتيجة لمشاركة البيانات بين عدة خيوط والاستخدام غير الآمن لـ strcpy. بينما قمت بتحليل هذه المشكلة بالذات لفترة طويلة، لم ألحظ فرصة استخدامها كمتجه لتسريب البيانات إلا قبل أسابيع قليلة من هذا النشر. ومن المثير للاهتمام، أنه بفضل تسريب عنوان كومة لكائن C++، تسمح هذه الثغرة أيضًا بتنفيذ التعليمات البرمجية عن بُعد. ومع ذلك، فإن هذا الهجوم غير مشمول في هذا التقرير.
يمكن لتطبيق Alloca تحديث نفسه عبر FTP. يمكن طلب هذه العملية من خادم، والذي يوفر أيضًا اسم المستخدم وكلمة المرور واسم الملف اللازمة. الدالة التي تبدأ التحديث موضحة هنا:

من الواضح أن الاستدعاءات الثلاثة لـ strcpy غير آمنة وتؤدي إلى تجاوز سعة الكومة، لأن كائن ftpUpgrade مُخصص ديناميكيًا. لسوء الحظ، فإن الترتيب الذي تُنفذ به النسخ وتخطيط بنية ftpUpgrade يجعل من المستحيل فعليًا بدء خيط يقوم بتسريب البيانات. عند إلقاء نظرة أقرب على الحزمة الواردة، تظهر البنية التالية:
struct ftp_upgrade_pkt {
struct pktHeader;
char username[16];
char password[16];
char filename[128];
}
بينما يبدو كائن ftpUpgrade شيئًا مثل:
struct CNetClientFtpUpgrade {
// ... something here
char filename[128];
char unknown[6];
char username[16];
char password[16];
CFtpDownlad *;
CNetClientConnect*;
int something[5];
bool threadRunning;
// ... and more
}
يمكن أن يحدث التسريب بعد أن يتم ملء أحد المؤشرات الداخلية (CFtpDownload*، CNetClientConnect*) بواسطة التطبيق. بالإضافة إلى ذلك، يتم نسخ اسم المستخدم وكلمة المرور (مرة أخرى، بشكل آمن هذه المرة) إلى الكائن المُنشأ حديثًا قبل تخزين مؤشره في الموقع القابل للتسريب، وبالتالي يمكن أن يحدث التسريب فقط مع filename. ونتيجة لذلك، يجب أن يكون اسم الملف طويلًا جدًا، ولكن نظرًا لترتيب وسلوك الإنهاء في strcpy، فإن اسم الملف الطويل بما يكفي سيؤدي إلى اسم مستخدم وكلمة مرور أطول، مما سيؤدي في النهاية إلى استبدال threadRunning وعدم بدء الخيط على الإطلاق.
لو كانت هذه التعليمات البرمجية أحادية الخيط، لما أمكن فعل الكثير. ولكن نظرًا لأن خيط FtpDownload الجديد يُنشأ ويقوم بتشغيل الدالة DownloadFile، فإنه يمثل فرصة مثيرة للاهتمام لأنه يشارك كائن CNetClientFtpUpgrade مع الخيط الذي يعالج الحزم الواردة. فهو لا يحتوي فقط على عمليات إدخال/إخراج متعددة يمكن التحكم بها خارجيًا (طلبات DNS، معالجة اتصال FTP)، بل يحاول أيضًا الاتصال بخادم FTP حتى 10 مرات (يتم ذلك في المتصل بـ DownloadFile). هذا يسمح بالتحكم في تنفيذ خيط FtpDownload (عن طريق حجبه في عمليات الإدخال/الإخراج)، مما يعطي وقتًا لخيط معالجة الرسائل لمعالجة الطلبات الأخرى.

باختصار، بمجرد إرسال طلبات تحديث متعددة، من الممكن تغيير filename (والبارامترات الأخرى) المستخدم من قبل خيط FtpDownload قيد التشغيل بالفعل واستقبال عنوان كومة مسرّب. كمكافأة إضافية، تستخدم الدالة FtpSize (المحددة باللون الأخضر) المخزن المؤقت داخل الكائن المشار إليه بالعنوان المسرّب لتخزين filename نفسه، مما يسمح بحقن تافه للمرحلة الأولى من شيلكود. القيد الوحيد هنا هو الطول وغياب بايتات NULL، بسبب استخدام strcpy. يتم توفير PoC نموذجي يقوم فقط بتسريب عنوان كومة من الجهاز.
تجاوز سعة مخزن مؤقت غير مصادق عليه على المكدس في الدالة CNetClientManage::ServerIP_Proto_Set
الغياب التام للمصادقة في معالجة حركة المرور الواردة وجهني للبحث عن معالجات الحزم. إحدى الدوال المثيرة للاهتمام هي ServerIP_Proto_Set. يبدو أنها تُستخدم لإنشاء استبدال ثابت لدقة DNS. لم أجد طريقة لإعادة توجيه حركة المرور بهذه الطريقة، ولكن هناك تجاوز سعة مخزن مؤقت آخر هنا (محدد باللون البرتقالي).

البيانات التي تُقرأ مباشرة من الحزمة تُستخدم داخل دالة sprintf. في هذه الحالة، يُفترض أن البيانات القادمة من الحزمة ستتناسب مع مخزن مؤقت بحجم 16 بايت، لكن استخدام تنسيق %s العادي يسمح بكتابة أي عدد كبير من البايتات، بشرط ألا تحتوي على NULL.
هذه الثغرة محدودة إلى حد ما. على الرغم من أنه من الممكن كتابة الكثير من البيانات على المكدس، بحيث يمكن أن يعمل استخدام NOP-sledge، إلا أنه ليس من الممكن كتابة بايتات NULL. حتى محاولة كتابة بايت NULL واحد ستفشل، لأن تنسيق دالة sprintf يسبقه بحرف \n. عقبة أخرى هي كائن CMutex المخزن بعد المخزن المؤقت. يجب أن تملأ أي محاولة لتجاوز السعة هذا الـ mutex بقيمة صحيحة (أو على الأقل قيمة ترضي مُدمّر CGuard - المحدد باللون الأحمر). هذا يمثل مشكلة، لأن المُدمّر يتبع المتغير المُمرر مرتين ثم يستخدم قيمته في استدعاء pthread_mutex_unlock. بعد بعض الاختبارات، اكتشفت أن مخزنًا مؤقتًا مملوءًا ببايتات NULL يكفي للعودة بشكل صحيح من pthread_mutex_unlock، لكنه لا يزال بحاجة إلى أن يتم إلغاء الإشارة إليه إلى عنوان ذاكرة صحيح.
التسريب يأتي للإنقاذ. الهجوم معقد بعض الشيء، لأننا نحتاج إلى عنوان كومة لا يحتوي على أي بايتات NULL. لحسن الحظ، البحث أصبح أسهل، لأن الجهاز يوفر لنا القدرة على إعادة تشغيل عن بُعد دون مصادقة. في كل مرة يتم توفير تخصيص مختلف لمساحة عنوان الكومة. لذلك من الممكن فقط إعادة ضبط الجهاز وتسريب عنوان حتى يتم العثور على عنوان مناسب. ومن الملائم أيضًا أن هذا يسمح بتخزين مرحلة أولى قصيرة من شيلكود. نظرًا لأننا بحاجة إلى تجاوز مشكلة الـ mutex (هناك حاجة إلى مؤشر إلى مؤشر)، فإننا نسرب عنوانًا آخر، هذه المرة بتمرير العنوان المسرب سابقًا كاسم ملف. يوضح المخطط التالي تخطيط الذاكرة المتوقع:

إذا سار كل شيء كما هو مخطط له، فمن الممكن تمرير عنوان ثانٍ كـ mutex والعنوان الأول كعنوان إرجاع. ومع ذلك، هذا ليس ضروريًا فقط لتعطيل التطبيق كما تفعل PoC.
تجاوز سعة مخزن مؤقت غير مصادق عليه على الكومة في الدالة CNetClientTalk::OprMsg
من المفترض أن يسمح الجهاز بالاتصال الصوتي ثنائي الاتجاه. يبدو أن معالجًا آخر للحزم الواردة مسؤول عن استقبال وتشغيل الصوت. حزمة الشبكة audio_pkt_hdr موصوفة بالبنية التالية:
struct audio_pkt_hdr {
struct pktHeader field_0x0;
int field_0x14
int field_0x18
int field_0x1c
char audioBuff[0x140];
}
أحد حقول بنية pktHeader هو طول الحزمة (كما يُنقل عبر الشبكة). يمكن تعيين هذا الحقل بحرية من قبل المرسل. الجزء المعرض للخطر هو نسخ البيانات مباشرة من الحزمة الواردة باستخدام قيمة الطول غير الموثوقة المقدمة في رأس الحزمة الواردة.

كما يتضح، يتم إنشاء كائن CNetClientTalk باستخدام المُنشئ التالي:

لذلك فإن الاستدعاء أعلاه للدالة memcpy يؤدي إلى تجاوز سعة المخزن المؤقت على الكومة. لسوء الحظ، الاستغلال الفعلي لهذه المشكلة صعب إلى حد ما. على الرغم من أنه من الممكن استبدال الكومة مرارًا وتكرارًا، إلا أنني لم أجد طريقة للتحكم في البيانات التي سيتم تخزينها على الكومة بعد المخزن المؤقت الممتلئ. نظرًا لأن التطبيق يحتوي على أكثر من 50 خيطًا نشطًا، بعضها مسؤول عن معالجة الصوت والفيديو، فهو يخصص/يحرر الذاكرة باستمرار. يؤدي هذا إلى تغير بيانات الكومة باستمرار، مما يجعل من الصعب التنبؤ بما يُخزن بعد المخزن المؤقت واستبداله بشكل صحيح.
تجاوز سعة مخزن مؤقت غير مصادق عليه على المكدس في الدالة CNetClientGuard::SubOprMsg
هنا يأتي معالج آخر للحزم الواردة. هذه المرة، حزمة الشبكة لها البنية التالية (مع حذف رأس الحزمة الشائع):
struct pkt_hdr_sub_cliGuard {
dword deviceId;
dword userId;
dword magic;
dword subCmd;
dword field_0x10;
dword field_0x14;
dword field_0x18;
dword field_0x1c;
dword guard_icommand;
dword moreThenRandomStackValue;
dword itemCnt;
char array_of_0x18[24];
};
مرة أخرى، الجزء المثير للاهتمام هو المصفوفة الأخيرة (لأنه يمكننا تكبير هذه الحزمة بقدر ما نريد)، والتي تحتوي على بنية داخلية بحجم 24. تنبع الثغرة من افتراض أن itemCnt المستلم لن يتجاوز 6، لأن المخزن المؤقت الوجهة للنسخ له الحجم 144 (=24*6)، وهو ما يظهر في القائمة التالية (باللون البرتقالي):

هذه المرة يتم النسخ باستخدام memcpy (باللون الأخضر)، لذلك لا يوجد حد على الأحرف المسموح بها. يتم النسخ على دفعات، بواسطة حلقة while (محددة باللون الأزرق). ومن الجدير بالملاحظة أن العداد cnt_v0 يتناقص داخل الحلقة، لذلك يتم نسخ الدفعات بترتيب عكسي. بما في ذلك المتغيرات التي تلي المخزن المؤقت المعرض للخطر buf، يجب أن يكون التجاوز 256 بايت، ثم 4 سجلات ($s0-$s3) و$ra. نظرًا لعدم معرفتنا بتخطيط الذاكرة، يستخدم كود PoC تقنية ROP. يتم استخدام أداة واحدة، والتي تشغل أحد الأصوات المدمجة بالجهاز (وتتعطل).
تجاوز سعة مخزن مؤقت غير مصادق عليه على المكدس في الدالة CFtpProtocol::FtpLogin
كان أحد الاتجاهات الأولية لتحليلي هو البحث في إجراء التحديث. كما اكتشفت، الجهاز لديه وظيفة تحديث عبر FTP، والتي يمكن بدؤها بإرسال طلب تحديث وتؤدي إلى تنزيل البرنامج الثابت من موقع FTP خارجي. كما هو الحال مع الثغرات الأخرى، ليست هناك حاجة للمصادقة قبل طلب تحديث الجهاز. كشف التحليل المتعمق لوظيفة FTP عن تجاوز سعة مخزن مؤقت على المكدس في الدالة CFtpProtocol::FtpLogin. كما نرى في القائمة المفككة أدناه، تقوم الدالة بتمرير مصفوفة char بحجم 256 إلى الدالة FtpPwd.

تُستخدم FtpPwd للحصول على دليل العمل الحالي من خادم FTP. تقوم بتحميل مخزنها الداخلي بما يصل إلى 1500 بايت من الاستجابة ثم نسخها إلى المخزن المؤقت المقدم. ينتج عن هذا التسلسل من الاستدعاءات تجاوز بمقدار 1242 بايت. في هذه الحالة، الأحرف المسموح بها محدودة للغاية، لأن استخدام " (علامة الاقتباس المزدوجة) سيؤدي إلى تقصير سلسلة الإدخال (يُستخدم strchr للبحث عن char في سلسلة C) ولن يتجاوز المخزن المؤقت. لحسن الحظ، من الضروري فقط تسليم عنوان واحد، سيتم توجيه تنفيذ التعليمات البرمجية إليه.

لاستغلال هذه الثغرة، من الضروري إما التحكم في DNS أو إعادة توجيه (أو اعتراض) الاتصال بخادم FTP. التطبيق لا يحتوي على أي حمايات حديثة، لذلك من الممكن تنفيذ التعليمات البرمجية مباشرة من المكدس. بدون تسريب عنوان، أفضل ما يمكن فعله هو إما تخمين موقع المكدس أو إعادة توجيه التنفيذ إلى دالة واحدة، والتي ستقوم بعد ذلك بتعطيل التطبيق. كانت محاولاتي الأولى هي فقط تشغيل أحد الأصوات المدمجة، وهو ما هو مقدم كـ PoC. باستخدام التسريب، من الممكن الحصول على سيطرة كاملة على الجهاز.