الرابط في مقال اليوم يختلف عن المعتاد. هذا ليس مشروعًا تم تحليل الشفرة المصدرية من أجله ، بل سلسلة من الاستجابات لنفس القاعدة التشخيصية في عدة مشاريع مختلفة. ما هي الفائدة هنا؟ حقيقة أن بعض أجزاء الكود المدروسة تحتوي على أخطاء قابلة للتكرار عند العمل مع التطبيق ، بينما يحتوي البعض الآخر على نقاط ضعف كلية (CVE). بالإضافة إلى ذلك ، في نهاية المقال سنناقش قليلاً حول موضوع العيوب الأمنية.
مقدمة موجزة
جميع الأخطاء التي سيتم النظر فيها اليوم في المقال لها نمط مماثل:
- يتلقى البرنامج البيانات من دفق stdin ؛
- يتحقق مما إذا كانت قراءة البيانات ناجحة.
- إذا تمت قراءة البيانات بنجاح ، تتم إزالة حرف الحمل من السطر.
ومع ذلك ، فإن جميع الأجزاء التي سيتم النظر فيها تحتوي على أخطاء وتكون عرضة للإدخال المزيف. نظرًا لاستلام البيانات من مستخدم يمكنه انتهاك منطق تنفيذ التطبيق ، كان هناك إغراء كبير لمحاولة كسر شيء ما. ما فعلت.
تم اكتشاف كل المشاكل أدناه بواسطة
محلل ثابت PVS-Studio ، الذي يبحث عن أخطاء في الكود ليس فقط لـ C و C ++ ، ولكن أيضًا عن C # و Java.
بالطبع ، يعد العثور على مشكلة مع محلل ثابت أمرًا جيدًا ، إلا أن إيجاد وتكاثر مستوى مختلف تمامًا من المتعة. :)
Freeswitch
تم العثور على الجزء الأول من التعليمات البرمجية المشبوهة في رمز الوحدة النمطية
fs_cli.exe ، والذي يعد جزءًا من مجموعة توزيع FreeSWITCH:
static const char *basic_gets(int *cnt) { .... int c = getchar(); if (c < 0) { if (fgets(command_buf, sizeof(command_buf) - 1, stdin) != command_buf) { break; } command_buf[strlen(command_buf)-1] = '\0'; break; } .... }
تحذير PVS-Studio :
V1010 CWE-20 يتم استخدام البيانات الملوثة غير المحددة في الفهرس: 'strlen (command_buf)'.
يحذر المحلل من استدعاء مشبوه بواسطة الفهرس إلى صفيف
command_buf . يعتبر مشبوهًا بسبب استخدام البيانات الخارجية التي لم يتم التحقق منها كفهرس. خارجي - لأنه يتم الحصول عليها من خلال وظيفة
fgets من تيار
stdin . لم يتم التحقق منه - نظرًا لعدم إجراء التحقق قبل الاستخدام. التعبير
fgets (command_buf ، ....)! = لا يتم حساب
Command_buf ، لأننا بهذه الطريقة نتحقق فقط من حقيقة استلام البيانات ، ولكن ليس محتوياتها.
المشكلة في هذا الرمز هي أنه ، في ظل ظروف معينة ، سيتم كتابة '\ 0' خارج الصفيف ، مما سيؤدي إلى سلوك غير محدد. للقيام بذلك ، ما عليك سوى إدخال سلسلة ذات طول صفري (سلسلة ذات طول صفري من وجهة نظر اللغة C ، أي ، حيث يكون الحرف الأول هو '\ 0').
دعونا نقدر ما يحدث إذا قمت بتمرير سلسلة ذات طول صفري إلى الإدخال:
- fgets (command_buf ، ....) -> command_buf ؛
- fgets (....)! = command_buf -> false ( ثم يتم تجاهل فرع العبارة if ) ؛
- strlen (command_buf) -> 0 ؛
- command_buf [strlen (command_buf) - 1] -> command_buf [-1] .
عفوًا!
الشيء المثير للاهتمام هنا هو أن هذا التحلل محلل يمكن أن "يشعر بيديك". لتكرار المشكلة ، تحتاج إلى:
- إحضار البرنامج إلى هذه الوظيفة ؛
- قم بضبط الإدخال بحيث تقوم استدعاء getchar () بإرجاع قيمة سالبة ؛
- تمرير ل fgets وظيفة خط مع محطة الصفر في البداية ، والتي ينبغي قراءتها بنجاح.
بالتفتيش قليلاً في المصدر ، قمت بتكوين تسلسل محدد لإعادة إنتاج المشكلة:
- قم بتشغيل fs_cli.exe في وضع الدُفعات ( fs_cli.exe -b ). لاحظ أنه لتنفيذ خطوات إضافية ، يجب أن يكون الاتصال fs_cli.exe بالخادم ناجحًا. للقيام بذلك ، يكفي تشغيل FreeSwitchConsole.exe محليًا كمسؤول على سبيل المثال.
- ننفذ المدخلات بحيث تعيد استدعاء getchar () قيمة سالبة.
- أدخل سطرًا به نقطة الصفر في البداية (على سبيل المثال ، '\ 0Oooops').
- ....
- الربح!
ما يلي هو تشغيل الفيديو من المشكلة:
Ncftp
تم اكتشاف مشكلة مماثلة في مشروع NcFTP ، لكنها واجهت بالفعل في مكانين. نظرًا لأن الرمز يبدو مشابهاً ، ففكر في مكان إشكالي واحد فقط:
static int NcFTPConfirmResumeDownloadProc(....) { .... if (fgets(newname, sizeof(newname) - 1, stdin) == NULL) newname[0] = '\0'; newname[strlen(newname) - 1] = '\0'; .... }
تحذير PVS-Studio :
V1010 CWE-20 يتم استخدام البيانات الملوثة غير المحددة في الفهرس: 'strlen (newname)'.
هنا ، على عكس المثال من FreeSWITCH ، يتم كتابة الرمز بشكل أسوأ وأكثر عرضة للمشاكل. على سبيل المثال ، تحدث الكتابة "\ 0" بغض النظر عما إذا كانت القراءة ناجحة باستخدام
fgets أم لا. أي أن هناك إمكانيات أكثر لكيفية كسر منطق التنفيذ الطبيعي. دعنا نذهب بطريقة مثبتة - من خلال خطوط طول صفري.
المشكلة المستنسخة أكثر تعقيدًا قليلاً من FreeSWITCH. سلسلة الخطوات الموضحة أدناه:
- بدء الاتصال بالملقم والذي يمكنك من خلاله تنزيل الملف. على سبيل المثال ، لقد استخدمت speedtest.tele2.net (في النهاية ، يبدو الأمر Launch application ): ncftp.exe speedtest.tele2.net )؛
- تنزيل ملف من الخادم محليا ، يجب أن يكون هناك ملف بنفس الاسم ولكن مع خصائص مختلفة. يمكنك ، على سبيل المثال ، تنزيل ملف من الخادم وتغييره ومحاولة تنفيذ أمر التنزيل مرة أخرى (على سبيل المثال ، الحصول على 512 كيلوبايت ).
- أجب عن السؤال حول اختيار إجراء باستخدام سطر يبدأ بالحرف "N" (على سبيل المثال ، الآن ، لنحصل على بعض المتعة ) ؛
- أدخل '\ 0' (أو شيء أكثر إثارة للاهتمام) ؛
- ....
- الربح!
يتم تسجيل إعادة إنتاج المشكلة أيضًا على الفيديو:
Openldap
في مشروع OpenLDAP (بتعبير أدق ، في واحدة من المرافق المصاحبة) صعدوا على نفس الخليع كما في FreeSWITCH. تحدث محاولة حذف حرف فاصل الأسطر فقط إذا تمت قراءة السطر بنجاح ، ولكن لا توجد حماية ضد خطوط الطول الصفري أيضًا.
مقتطف الشفرة:
int main( int argc, char **argv ) { char buf[ 4096 ]; FILE *fp = NULL; .... if (....) { fp = stdin; } .... if ( fp == NULL ) { .... } else { while ((rc == 0 || contoper) && fgets(buf, sizeof(buf), fp) != NULL) { buf[ strlen( buf ) - 1 ] = '\0'; if ( *buf != '\0' ) { rc = dodelete( ld, buf ); if ( rc != 0 ) retval = rc; } } } .... }
تحذير PVS-Studio :
V1010 CWE-20 يتم استخدام البيانات الملوثة غير المحددة في الفهرس: 'strlen (buf)'.
نطرح الفائض حتى يصبح جوهر المشكلة أكثر وضوحًا:
while (.... && fgets(buf, sizeof(buf), fp) != NULL) { buf[ strlen( buf ) - 1 ] = '\0'; .... }
هذا الرمز أفضل من NcFTP ، لكنه لا يزال عرضة للتأثر. إذا طلب بناءً على طلب تمرير سلسلة من الطول الصفري إلى الإدخال:
- fgets (buf، ....) -> buf ؛
- fgets (....)! = NULL -> صواب (يبدأ نص حلقة التنفيذ) ؛
- strlen (buf) - 1 -> 0 - 1 -> -1 ؛
- buf [-1] = '\ 0' .
تشهير
على الرغم من أن الأخطاء التي تمت مناقشتها أعلاه مثيرة للاهتمام للغاية (يتم إعادة إنتاجها بشكل ثابت ، ويمكن "لمسها" (إلا إذا لم أتمكن من الوصول إلى يدي على مشكلة OpenLDAP)) ، لا يمكن تسميتها نقاط الضعف ، إذا كان ذلك فقط لسبب لا يتم تعيين مشاكل معرفات CVE.
ومع ذلك ، فإن بعض نقاط الضعف الحقيقية لديها نفس نمط المشكلة. ينطبق كلا قصتي الشفرة أدناه على مشروع libidn.
مقتطف الشفرة:
int main (int argc, char *argv[]) { .... else if (fgets (readbuf, BUFSIZ, stdin) == NULL) { if (feof (stdin)) break; error (EXIT_FAILURE, errno, _("input error")); } if (readbuf[strlen (readbuf) - 1] == '\n') readbuf[strlen (readbuf) - 1] = '\0'; .... }
تحذير PVS-Studio :
V1010 CWE-20 يتم استخدام البيانات الملوثة غير المحددة في الفهرس: 'strlen (readbuf)'.
الموقف مشابه ، باستثناء أنه ، على عكس الأمثلة السابقة ، حيث تم التسجيل في الفهرس
-1 ، تتم القراءة هنا. ومع ذلك ، لا يزال هذا السلوك غير محدد. تم
تعيين هذا الخطأ لمعرف CVE الخاص به (
CVE-2015-8948 ).
بعد اكتشاف مشكلة ، تم تغيير هذا الرمز على النحو التالي:
int main (int argc, char *argv[]) { .... else if (getline (&line, &linelen, stdin) == -1) { if (feof (stdin)) break; error (EXIT_FAILURE, errno, _("input error")); } if (line[strlen (line) - 1] == '\n') line[strlen (line) - 1] = '\0'; .... }
فوجئت قليلا؟ هذا يحدث. ثغرة جديدة ، معرّف CVE المقابل:
CVE-2016-6262 .
تحذير PVS-Studio :
V1010 CWE-20 يتم استخدام البيانات الملوثة غير المحددة في الفهرس: 'strlen (line)'.
في محاولة أخرى ، تم إصلاح المشكلة عن طريق إضافة فحص لطول سلسلة الإدخال:
if (strlen (line) > 0) if (line[strlen (line) - 1] == '\n') line[strlen (line) - 1] = '\0';
دعنا نلقي نظرة على التواريخ. الالتزام "إغلاق" CVE-2015-8948 - 08/10/2015. إغلاق الالتزام CVE-2016-62-62 - 01/14/2016. وهذا هو ، الفرق بين التصحيحات أعلاه هو
5 أشهر ! تتذكر هنا ميزة التحليل الثابت مثل اكتشاف الأخطاء في المراحل الأولى من كتابة التعليمات البرمجية ...
تحليل ثابت والأمن
لن يكون هناك أمثلة أخرى على الكود ؛ بدلاً من ذلك ، الإحصائيات والمنطق. في هذا القسم ، قد لا يتطابق رأي المؤلف مع رأي القارئ أكثر من ذي قبل في هذه المقالة.
ملاحظة أوصي بأن تقرأ مقالًا آخر حول موضوع مشابه - "
كيف يمكن لـ PVS-Studio المساعدة في العثور على الثغرات الأمنية؟ ". هناك أمثلة مثيرة للاهتمام من نقاط الضعف التي تبدو وكأنها أخطاء بسيطة. بالإضافة إلى ذلك ، في هذه المقالة تحدثت قليلاً عن المصطلحات ولماذا يجب أن يكون التحليل الثابت إذا كنت مهتمًا بموضوع الأمان.
دعونا نلقي نظرة على الإحصاءات المتعلقة بعدد الثغرات الأمنية المكتشفة خلال السنوات العشر الماضية لتقييم الوضع. أخذت البيانات من موقع
CVE Details .

حالة مثيرة للاهتمام يلوح في الأفق. حتى عام 2014 ، لم يتجاوز عدد CVEs المسجلة علامة 6000 وحدة ، وابتداء من - لم ينخفض أدناه. لكن الأكثر إثارة للاهتمام هنا ، بالطبع ، هو إحصائيات عام 2017 - القائد المطلق (14،714 وحدة). أما بالنسبة للسنة الحالية - 2018 ، فهي لم تنته بعد ، لكنها بالفعل تحطّم الأرقام القياسية - 151010 وحدة.
هل هذا يعني أن جميع البرامج الجديدة مليئة بالثقوب مثل الغربال؟ لا أفكر ، وإليكم السبب:
- زيادة الاهتمام بموضوع الثغرات الأمنية. بالتأكيد ، حتى لو لم تكن قريبًا جدًا من موضوع الأمان ، فقد صادفت مرارًا مقالات ومذكرات وتقارير ومقاطع فيديو حول موضوع الأمان. بمعنى آخر ، تم إنشاء نوع من "الضجيج". هل هو سيء؟ ربما لا. في النهاية ، يعود الأمر كله إلى حقيقة أن المطورين مهتمون أكثر بأمان التطبيق ، وهو أمر جيد.
- زيادة في عدد التطبيقات المتقدمة. المزيد من التعليمات البرمجية - من المرجح أن تحدث أي ثغرة أمنية ستؤدي إلى تجديد الإحصاءات.
- تحسين أدوات البحث عن الضعف وضمان جودة الرمز. المزيد من الطلب -> المزيد من العرض. أصبحت الأجهزة التحليلية وأجهزة fuzzers وأدوات أخرى أكثر تقدمًا ، الأمر الذي يلبي احتياجات أولئك الذين يرغبون في البحث عن نقاط الضعف (بغض النظر عن أي جانب من المتاريس التي يشغلونها).
لذلك لا يمكن تسمية الاتجاه الناشئ بالسالب بشكل حصري - فالناشرون أكثر قلقًا بشأن أمن المعلومات ، ويجري تحسين أدوات العثور على المشكلات ، وكل هذا إيجابي بلا شك.
هل هذا يعني أنه يمكنك الاسترخاء وليس "الاستحمام"؟ لا أعتقد ذلك. إذا كنت مهتمًا بموضوع الأمان الخاص بالتطبيقات ، فيجب أن تتخذ أكبر عدد ممكن من تدابير الأمان. هذا صحيح بشكل خاص إذا كانت شفرة المصدر في المجال العام ، لأنها:
- أكثر عرضة لتضمين نقاط الضعف الخارجية ؛
- أكثر عرضة "للتحقيق" من قبل "السادة" المهتمين بالثقوب في طلبك لغرض استغلالهم. على الرغم من أن المهنئين في هذه الحالة سيكونون قادرين على مساعدتك أكثر.
لا أريد أن أقول أنك لست بحاجة إلى ترجمة مشاريعك في إطار المصادر المفتوحة. فقط ضع في اعتبارك ضوابط الجودة / السلامة المناسبة.
هل التحليل الثابت مثل هذا التدبير الإضافي؟ نعم يقوم التحليل الثابت بعمل جيد لاكتشاف نقاط الضعف المحتملة التي قد تصبح حقيقية في المستقبل.
يبدو لي (أعترف أنني مخطئ) أن الكثيرين يعتبرون نقاط الضعف ظاهرة عالية المستوى إلى حد ما. نعم ولا. يمكن أن تكون مشاكل التعليمات البرمجية التي تبدو وكأنها أخطاء برمجة بسيطة نقاط ضعف خطيرة. مرة أخرى ، يتم توفير بعض أمثلة هذه الثغرات الأمنية في
المقالة المذكورة سابقًا . لا تقلل من شأن الأخطاء البسيطة.
الخاتمة
لا تنس أن بيانات الإدخال يمكن أن تكون ذات طول صفري ، ويجب أيضًا أخذ ذلك في الاعتبار.
استنتاجات حول ما إذا كان كل الضجيج مع نقاط الضعف مجرد الضجيج ، أو إذا كانت المشكلة موجودة ، تفعل ذلك بنفسك.
من ناحيتي ، ما لم أقترح تجربة مشروعك
PVS-Studio ، إذا لم تكن قد قمت بذلك بالفعل.
كل التوفيق!

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