
اختبار Fuzzing لأجهزة إنترنت الأشياء باستخدام الراوتر TL-WR902AC كمثال
هذه هي نسخة HTML من ورقة الفصل الدراسي الخاصة بي والتي يمكن تنزيلها كملف PDF هنا.
أصبحت التغذية العشوائية (Fuzzing) "واحدة من أكثر الطرق فعالية" لاكتشاف الأخطاء في البرمجيات. وبهذا الادعاء أو بأدعاءات مشابهة، تبدأ العديد من الأوراق البحثية الحالية المتعلقة بالتغذية العشوائية [google-scholar]. كان الهدف الرئيسي من ورقة الفصل الدراسي السابقة حول موضوع "إنترنت الأشياء الهشّة" هو إيجاد خطأ متعلق بالذاكرة ثم كتابة استغلال لهذه الثغرة. وقد تمكّنّا من إيجاد ثغرة عبر عكس هندسة البرنامج الثابت (firmware)، لكن لم يتم العثور على أخطاء متعلقة بالذاكرة. إن إيجاد تجاوز سعة المخزن المؤقت عن طريق عكس هندسة ملف ثنائي يدويًا ليس فقط مستهلكًا للوقت، بل يتطلب أيضًا الكثير من الخبرة. وفي الوقت نفسه، تهدف التغذية العشوائية إلى أن تكون "الطريقة الأكثر فعالية" لإيجاد مثل هذه الثغرات المرتبطة بالذاكرة فعلى سبيل المثال، قدّمت Google أداة OSS-Fuzz التي تقوم باستمرار بالتغذية العشوائية للبرمجيات مفتوحة المصدر وقد وجدت بالفعل أكثر من 10,000 ثغرة عبر 1,000 مشروع [oss-fuzz].
الهدف من ورقة الفصل الدراسي هذه هو مرة أخرى إيجاد ثغرة متعلقة بالذاكرة، لكن هذه المرة باستخدام التغذية العشوائية. يجب أن تكون الثغرة المستهدفة قابلة للاستغلال عبر الشبكة دون معرفة بيانات اعتماد المسؤول. تصف هذه الورقة الطريقة لتحقيق هذا الهدف. ولهذا، تنقسم الورقة إلى جزأين. يركز الجزء الأول على كيفية إيجاد هدف واعد، وما الأدوات التي يمكن استخدامها، ومما يجب أن يتكون الهدف الجيد للتغذية العشوائية. ثم يصف الجزء الثاني كيفية تطوير وتصحيح أخطاء harness قادر على اختبار وظيفة معينة في ملف ثنائي بالتغذية العشوائية. ثم يُستخدم الـ harness المطوَّر بواسطة AFL++ للتغذية العشوائية على الوظيفة المستهدفة. في ما يلي، يتم تقديم خلفية مختصرة وعن الوضع الحالي لأحدث التقنيات عندما يتعلق الأمر بالتغذية العشوائية لأجهزة إنترنت الأشياء.
جميع الملفات التي أُنشئت في سياق ورقة الفصل الدراسي هذه منشورة بالكامل أيضًا على GitHub ويمكن الوصول إليها باستخدام العنوان التالي: otsmr/blackbox-fuzzing.
إن التغذية العشوائية لأجهزة إنترنت الأشياء ليست بالأمر السهل مثل التغذية العشوائية لمشروع مفتوح المصدر. غالبًا ما يكون الكود المصدري مملوكًا، مما يجعل التغذية العشوائية بالصندوق الرمادي، التي تُجهّز الكود المصدري لتحقيق أفضل أداء للتغذية العشوائية، مستحيلة [afl-persistent]. أيضًا، غالبًا لا تدعم أدوات التغذية العشوائية بنية المعالج (CPU) بشكل أصلي، مما يتطلب استخدام محاكٍ مثل QEMU [qemu] وهو ما يبطئ أيضًا سرعة التغذية العشوائية [afl-persistent]. وهناك مشكلة أخرى تتمثل في الأجهزة الطرفية، مما يعقّد تطوير نهج عام. الورقة البحثية "التغذية العشوائية للأنظمة المدمجة: مراجعة للتحديات والأدوات والحلول" [embedded-fuzzing] تقدم نظرة عامة على استراتيجيات التغذية العشوائية المختلفة، مثل التغذية العشوائية للأنظمة المدمجة القائمة على العتاد. معظم هذه الاستراتيجيات تحتاج إلى الكود المصدري للبرنامج المستهدف، مثل نقل الكود المصدري لأدوات التغذية العشوائية مثل AFL إلى أجهزة إنترنت الأشياء المبنية على ARM لتشغيل الأداة على عتاد إنترنت الأشياء. تشغيل الأداة على عتاد الجهاز ينطوي أيضًا على مشاكل في الأداء لأنها غالبًا تحتوي على معالجات منخفضة المستوى، وهي أبطأ من معالجات أجهزة الكمبيوتر المكتبية العادية. وهناك نهج آخر قدّمته هذه الورقة وهو التغذية العشوائية المدمجة القائمة على المحاكاة حيث يتم تنفيذ برنامج مستهدف واحد داخل محاكٍ لإجراء تغذية عشوائية موجّهة بالتغطية، أو يتم محاكاة النظام الكامل.
النهج المذكورة أعلاه جميعها تستهدف ملفًا ثنائيًا مباشرةً باستخدام محاكٍ أو عن طريق تجهيز
الكود المصدري. تتطلب هذه النهج إعداد تغذية عشوائية يجب غالبًا أن يكون مصممًا خصيصًا
لجهاز إنترنت أشياء واحد ويصعب تعميمه. لهذا، أنشأ الباحثون برنامجًا
IoTFuzzer يهدف إلى أن يكون إطار تغذية عشوائية آليًا يسعى إلى "إيجاد ثغرات تلف الذاكرة
دون الوصول إلى صور البرامج الثابتة الخاصة بها
[iotfuzzer]." IoTFuzzers
ويستند إلى ملاحظة أن معظم أجهزة إنترنت الأشياء لديها تطبيق جوال للتحكم بها، وأن هذه التطبيقات
تحتوي على معلومات حول البروتوكول المستخدم للتواصل مع الجهاز. ثم يقوم البرنامج
بتحديد المنطق الخاص بالبرنامج وإعادة استخدامه لتحوير حالات الاختبار لاختبار أجهزة إنترنت الأشياء
المستهدفة بفعالية [iotfuzzer].
يصف الـ harness تسلسل استدعاءات API التي تعالج المدخلات المقدمة من أداة التغذية العشوائية. وعلى العكس من التطبيق العادي الذي لا يحتاج غالبًا إلى harness، فإن المكتبة التي تنفذ دوالًا قابلة لإعادة الاستخدام يجب استدعاؤها باستخدام المعاملات الصحيحة وبالتسلسل الصحيح أيضًا، بحيث يمكن استدعاء الحالة بين استدعاءات متعددة للدوال المشتركة. إن إخضاع المكتبة للتغذية العشوائية دون بناء آلة الحالة من غير المرجح أن ينجح، وستُنشئ بدلًا من ذلك الكثير من حالات التعطل الإيجابية الكاذبة عند عدم فرض تبعيات المكتبة. يمكن أن يحدث هذا عندما يتم، على سبيل المثال، تخطّي فحص حجم المخزن المؤقت بواسطة أداة التغذية العشوائية مما يؤدي إلى تجاوز سعة مخزن مؤقت زائف.
في هذه الورقة، سيتم اختبار التطبيقات العادية بالتغذية العشوائية، ولكن بسبب التبعيات العتادية لاستخدام المآخذ (sockets) وتعدد الخيوط، نحتاج إلى إنشاء harness لها أيضًا. يتم تحميل الـ harness في سياق الملف الثنائي ويمكنه استدعاء الدوال الداخلية للبرنامج المستهدف، كما هو موضح في الكود 10.
يصف مصطلح "corpus" عينات الإدخال الصالحة أو حالات الاختبار ويعمل كمرجع أساسي لتوليد بيانات إدخال جديدة أثناء عملية التغذية العشوائية. في الكود 10 سيكون هذا، على سبيل المثال، طلب HTTP. ثم تستفيد أدوات التغذية العشوائية من هذه المجموعة لإنشاء حالات اختبار محوَّرة أو متنوعة مما يساعد في اكتشاف الثغرات البرمجية من خلال استكشاف مختلف سيناريوهات الإدخال.
الجزء الأكثر استهلاكًا للوقت في التغذية العشوائية بالصندوق الأسود هو إيجاد
دالة قابلة للاستغلال في البرنامج الثابت. الخطوة الأولى هي العثور على
ملفات ثنائية مثيرة للاهتمام، على سبيل المثال، تكون متاحة عبر الشبكة،
أو تستخدم دوالًا غير آمنة، أو لا تحتوي على ميزات أمان مثل
تفعيل كناري المكدس (stack canary)، وهو حماية من تجاوز سعة المخزن المؤقت. ورقتنا السابقة
([iovt]) وصفت بالفعل كيفية استخراج البرنامج الثابت
من الموجّه المستهدف وكيفية العثور على ملف ثنائي قد يكون خطيرًا.
ولهذا، تم استخدام أداة EMBA [emba]. تقوم EMBA بترتيب جميع
الملفات الثنائية الموجودة في البرنامج الثابت حسب عدد الدوال غير الآمنة مثل
strcpy، والوصول إلى الشبكة، والحماية الأمنية مثل كناري المكدس أو
بت NX (NX-Bit) التي تصبح مثيرة للاهتمام عند استغلال تجاوز سعة المخزن المؤقت،
وهو ما يمكن العثور عليه في الكود 1.
الكود 1: نتيجة EMBAs للاستخدامات غير الآمنة للدالة strcpy.
لأن الهدف من هذه الورقة هو إيجاد ثغرة في الذاكرة يمكن استغلالها عبر الشبكة دون معرفة بيانات اعتماد المسؤول، يجب أن تكون الدالة الضعيفة قابلة للاستدعاء عبر الشبكة وأن تتفاعل مباشرة مع إدخال المستخدم المقدم. لكن وجود تفاعل شبكي لا يعني أن الملف الثنائي يمكن الوصول إليه مباشرة عبر الشبكة أيضًا. لمعرفة أي الملفات الثنائية تستمع، يمكننا استخدام قشرة جذر UART، التي تم إنشاؤها بالفعل في [iovt].
```txt ~ # netstat -tulpn Active Internet connections (only servers) Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name tcp 0 0 127.0.0.1:20002 0.0.0.0:* LISTEN 1045/tmpd tcp 0 0 0.0.0.0:1900 0.0.0.0:* LISTEN 1034/upnpd tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1027/httpd tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1224/dropbear udp 0 0 0.0.0.0:20002 0.0.0.0:* 1048/tdpd [...] ```الكود 2: استخدام صدفة الجذر UART لتنفيذ netstat
أول ثنائي يبدو واعدًا هو wscd. يحتوي الثنائي على أكثر استدعاءات strcpy غير الآمنة (باستثناء مكتبة libcmm.so) وتفاعل شبكي، وهو في حالة wscd يعني أنه يتصل بجهاز UPnP ولا يستمع على منفذ محدد. يحتوي، كما سيظهر لاحقًا، على دالة سهلة للـ fuzzing، ولهذا تم اختيار هذا الثنائي كمثال في هذه الورقة لشرح الإجراء العام. قبل إجراء الهندسة العكسية، يمكننا استخدام صدفة الجذر UART لمعرفة ما إذا كان الثنائي قيد التشغيل وكيف تم تشغيله.
الكود 3: استخدام الأمر ps لعرض جميع البرامج قيد التشغيل.
مع ps لا نرى فقط أن الملف الثنائي يعمل، بل نرى أيضًا الوسائط، وهي
مهمة للتحقق مما إذا كانت وظيفة محتملة تُستدعى على الإطلاق. يمكن الحصول على معنى هذه الوسائط
من مساعدة سطر الأوامر (CLI)، والتي تُعرض عند استدعاء الملف الثنائي دون أي وسائط.
الرمز 4: خيارات الملف الثنائي wscd.
كما هو موضّح في الرمز 4، يبدأ تشغيل wscd مع "Enabled UPnP Device service" وهو ما يبدو واعدًا. بعد التحقق من أن الملف الثنائي يعمل فعلًا على الموجّه، يمكن تحليل الملف الثنائي باستخدام Ghidra للبحث عن الدوال المشبوهة. بالنسبة للتضمين العشوائي (fuzzing)، تُعد دوال التحليل (parsing functions) مثيرة للاهتمام بشكل خاص لأنها عادةً ما تكون معقّدة، وكثيرًا ما يحتوي الإدخال الذي يتم تحليله على حقول طول للبيانات التي يتضمنها، مثلما تحتوي حزمة TCP على طول الحمولة (payload).

الشكل 1: استخدام Ghidra للبحث عن دوال التحليل.
من الفوائد الأخرى لدوال التحليل أنها غالبًا لا تتفاعل مع أجزاء أخرى من الكود ولا تنطوي على تفاعل مع المستخدم عبر الشبكة. لذا يمكن استدعاء دالة التحليل مباشرةً مع الإدخال دون تعديل الملف الثنائي أو استبدال دوال أخرى، وبالتالي يمكن اختبار الدالة بالتضمين العشوائي.
قبل البدء في اختبار الدالة بالتضمين العشوائي، ينبغي التحقق مما إذا كانت الدالة تُستدعى أصلًا، لأن الدالة تكون مثيرة للاهتمام فقط عندما تُستدعى بإدخال يتحكم فيه المستخدم. وللقيام بذلك، يمكن استخدام Ghidra للبحث عن المراجع إلى الدالة المستهدفة. وفي حالة دالة parser_parse، توجد عدة طرق. ولأننا نعرف كيفية بدء تشغيل البرنامج، يمكن اختزال الاستدعاءات إلى شجرة استدعاءات واحدة، كما هو موضّح في الرمز 5.
الكود 5: شجرة استدعاء الدالة parser_parse
بعد العثور على دالة هدف، يمكننا الآن إنشاء إعداد fuzzing لاختبار الدالة، وهو موصوف في الجزء التالي. لكن أولاً، يتم تقديم دوال أخرى محتملة.
في هذه الورقة، تم تحليل عدة ملفات ثنائية محتملة يدويًا بحثًا عن دوال مشبوهة. فيما يلي ملخص قصير للأهداف المحتملة الأخرى التي تم العثور عليها.
الملف الثنائي httpd هو الواجهة الخلفية لواجهة الإدارة عبر الويب. يمكن الوصول إلى الملف الثنائي
عبر الشبكة على المنفذ 80. إحدى الدوال المثيرة للاهتمام في httpd هي الدالة httpd_parser_main.
أثناء تصفح تنفيذ المحلل باستخدام Ghidra، تم تحديد عدة أجزاء مشبوهة مختلفة من الكود.
أحد الأجزاء المشبوهة هو تحليل Content-Type. فيما يلي، يمكن العثور على طلب
HTTP أساسي.```txt
POST / HTTP/1.1\r\n
Content-Type: multipart/form-data; boundary=X;\r\n
Host: example.com\r\n
\r\n
\r\n
DATA\r\n
فيما يلي مقتطف من الدالة `httpd_parser_main` التي تقوم بتحليل `Content-Type` من طلب http المقدم من المستخدم.
<div id="c6"></div>```c
// user_input_ptr points to
// "Content-Type: multipart/form-data; boundary=X;\r\nHost: example.com\r\n..."
cursor = strstr(user_input_ptr,"multipart/form-data");
if (user_input_ptr == cursor) {
cursor = strstr(user_input_ptr,"boundary=");
user_input_ptr = cursor + 9;
// user_input_ptr points now to "X;\r\nHost: example.com\r\n..."
if (cursor != (char *)0x0) {
do {
while (cursor = user_input_ptr, *cursor == " ") {
user_input_ptr = cursor + 1;
}
user_input_ptr = cursor + 1;
} while (*cursor == "\t");
// cursor points now to "X;\r\nHost: example.com\r\n..."
// strchr returns a pointer to the first occurrence of ";" in the user request.
// If ";" is not found, the function returns a null pointer.
user_input_ptr = strchr(cursor, ";");
if (user_input_ptr != (char *)0x0) {
// The character ";" is replaced by an null byte to terminate the string
*user_input_ptr = "\0";
// cursor points now to "X\0\r\nHost: example.com\r\n..."
}
// DAT_00444050 global array from 0x00444050 to 0x0044414f (255 Bytes)
strcpy(&DAT_00444050, cursor);
// DAT_00444050 contains now "X"
}
}
الكود 6: شجرة استدعاء الدالة parser_parse
الثغرة في هذا الكود هي استدعاء الدالة strcpy والافتراض بأن Content-Type ينتهي بفاصلة منقوطة. لأن strcpy تنسخ المخزن المؤقت حتى الوصول إلى البايت الصفري التالي، وكما هو موضح في الكود 6 فإن البايت الصفري يُضاف فقط عند العثور على فاصلة منقوطة. وبإزالة الفاصلة المنقوطة، يصبح البايت الصفري التالي في نهاية مخزن الإدخال، أي على سبيل المثال نهاية طلب HTTP. وبالتالي يمكن تجاوز سعة المتغير العام DAT_00444050، مما يؤدي إلى استبدال البيانات الواقعة بعد العنوان 0x0044414f. الجزء الصعب لا يقتصر فقط على إيجاد متغير عام مهم بعد هذا العنوان يمكن استبداله، بل أيضاً على عدم إمكانية استخدام أي بايتات صفرية بسبب strcpy. لكن عندما يوجد خطأ واحد من هذا القبيل، فمن المرجح أن يكون هناك المزيد لاكتشافه.
الملف الثنائي tdpd يُستخدم من قبل تطبيق الجوال ويمكن الوصول إليه عبر UDP على الشبكة المحلية. يحتوي tdpd على نفس وظائف tmpd تقريباً، لكن معظمها لا يُستدعى أبداً. الدالة الرئيسية تكتفي بالاستماع للرسائل عبر منفذ UDP وترد دائماً بمعلومات أساسية عن الموجّه، مثل الاسم أو الطراز. بالكاد يوجد أي تفاعل مع المدخلات التي يقدمها المستخدم، وبالتالي فهي ليست مثيرة للاهتمام لأغراض الفازينغ.
هناك زوج آخر مثير للاهتمام من الملفات الثنائية وهما upnpd وushare. يتعامل كلا الملفين مع رسائل UPnP، الأمر الذي يستلزم تحليل XML. وبما أنه يمكن العثور على نص حقوق نشر في الملف الثنائي، فمن الممكن افتراض أن هذه البرامج لم تطورها TP-Link.```sh
$ strings usr/bin/ushare | grep "(C)"
Benjamin Zores (C) 2005-2007, for GeeXboX Team.
كلا الملفين الثنائيين يقومان بتحميل المكتبات المشتركة `libupnp.so` و `libixml.so` التي تحتوي على نفس
الدوال الموجودة في مشروع المصدر المفتوح `pupnp` [\[pupnp\]](https://github.com/pupnp/pupnp/). ولأن
تركيز هذه الورقة ينصب على اختبار الصندوق الأسود (black box fuzzing)، يتم تجاهل هذين الملفين الثنائيين. لكن اختبار الصندوق الرمادي (gray box fuzzing) لهذه
المكتبة قد يكون له إمكانات لأنه في عام 2021 تم العثور على تسرب للذاكرة في `libixml.so`
[\[pupnp-mem-leak\]](https://github.com/pupnp/pupnp/issues/249).
الملف الثنائي **tmpd** هو الواجهة الخلفية لتطبيق الجوال. الجزء المثير للاهتمام هو أن الموجّه و
تطبيق الجوال يتواصلان عبر بروتوكول ثنائي مخصص. في ما يلي، يتم عرض رسالة من
العميل إلى الخادم.
<div id="c7"></div>```txt
00000000 01 00 05 00 00 08 00 00 00 00 00 17 50 7b 6e fe |............P{n.|
00000010 01 01 02 00 00 00 00 00 |........ |
الكود 7: رسالة من تطبيق الجوال إلى الراوتر.
لفهم البروتوكول الثنائي، تمت الهندسة العكسية للملف الثنائي tmpd باستخدام Ghidra. وبهذه
المعلومات، يمكن تفكيك الرسالة في الكود 7 إلى ما يلي:```txt
01 00 05 00 : Version
00 08 00 00 : Size (8 Bytes)
00 00 00 17 : Datatype
50 7b 6e fe : Checksum (CRC32)
01 01 : Options
02 00 : Function id
00 00 00 00 : Function parameters
<p class="text-align: center">الكود 8: تفكيك البروتوكول الثنائي المخصص.</p>
هذا يبدو واعدًا لأن مثل هذه البروتوكولات الثنائية يجب تحليلها. لكن الجزء الأكثر اشتباهًا في البروتوكول الثنائي ليس حقل الطول، بل استخدام معرّف الدالة ومعاملاتها.
<figure id="f2">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/43cb089d7f85d30ba036234ff0fdb29b6a54dd4a3382b2bd68e065f9f6c75369.png" style="width:90.0%" /></p>
<figcaption>
<p style="text-align: center">الشكل 2: الدالة المسترجعة من tmpd التي تحلل معرّف الدالة ومعاملاتها.</p>
</figcaption>
</figure>
يوضح [الشكل 2](#f2) جزءًا من دالة التحليل المترجمة عكسيًا للبروتوكول المخصص. في السطر 16، يتم استخراج معرّف الدالة، ثم يتم استدعاء الدالة المقابلة في السطر 29. السلوك المشبوه هو أن الدالة تُستدعى بمعاملات يتم استخراجها دون أي فحص من مخزن الإدخال المتحكم فيه من قبل المستخدم. يمكننا الآن محاولة العثور على دالة في جدول القفز الموضح في [الشكل 3](#f3) حيث قد يكون هذا خطيرًا، مثل عندما يُستخدم المعامل لفهرسة مخزن مؤقت أو يُفسَّر كسلسلة نصية. بدلاً من التفكيك والبحث اليدوي في أكثر من 100 دالة، وهو أمر يستغرق وقتًا طويلاً، يمكننا استخدام أداة اختبار عشوائي تفعل ذلك تلقائيًا.
<figure id="f3">
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/708cf59aee459b840ca6dbcac32948d2b80fb426c3d4cc745157eac4f9b7c28e.png"
style="width:90.0%" /></p>
<figcaption><p style="text-align: center">الشكل 3: الدالة المسترجعة من tmpd التي تحلل معرّف الدالة ومعاملاتها.</p></figcaption>
</figure>
لسوء الحظ، فإن ملف `tmpd` الثنائي لا يمكن الوصول إليه عبر الشبكة إلا محليًا، كما هو موضح في [الكود 2](#c2). للاتصال بهذا الملف الثنائي، يتصل التطبيق أولاً بالموجّه عبر SSH في وضع `direct-tcpip` الذي يقوم فقط بإعادة توجيه الحزم إلى العملية المحلية. وتكون اتصالات SSH محمية ببيانات اعتماد المسؤول. ولكن كما هو موضح في [\[iovt\]](https://raw.githubusercontent.com/otsmr/internet-of-vulnerable-things/main/Internet_of_Vulnerable_Things.pdf) يمكن اختراق اتصال SSH بسهولة لأن التطبيق لا يتحقق أبدًا من المفتاح المضيف للخادم. ومن خلال إسقاط كل حزمة موجّهة إلى الإنترنت، يمكن خداع المسؤول لتسجيل الدخول إلى الموجّه أثناء تنفيذ هجوم رجل في المنتصف لسرقة بيانات الاعتماد.
## الاختبار العشوائي باستخدام AFL++ وQEMU
في هذا القسم، يتم تطوير أداة ربط تستهدف إحدى الدوال المكتشفة سابقًا. بعد تطوير أداة الربط، يُستخدم أداة الاختبار العشوائي المتطورة AFL++ [\[aflpp\]](https://github.com/AFLplusplus/AFLplusplus) لاختبار الدالة المستهدفة عشوائيًا. ولأن الملفات الثنائية مترجمة لمعمارية `mipsel`، يُستخدم المحاكي QEMU لتنفيذ الملف الثنائي. إن إعداد الاختبار العشوائي الأساسي المستخدم في هذه الورقة مستوحى في الغالب من تدوينة "Firmware Fuzzing 101" بقلم Adam Van Prooyen [\[b101\]](https://www.mayhem.security/blog/firmware-fuzzing-101).
### بيئة الاختبار العشوائي
لإنشاء بيئة اختبار عشوائي قابلة للتكرار بسهولة، يُعد Docker الخيار الأفضل. لقد أنشأنا ملف Dockerfile يقوم بتثبيت كل أداة ضرورية، مثل مترجم متصالب لمعمارية CPU `mipsel` أو `gdb-multiarch` الذي يمكن استخدامه لتصحيح أخطاء أداة الربط.
علاوة على ذلك، يتم تنزيل AFLplusplus وترجمته مع QEMU الذي يُبنى بإصدار يتضمن تعديلات طفيفة للسماح بتشغيل الملفات الثنائية غير المزوّدة بأدوات القياس تحت afl-fuzz.```docker
FROM debian:latest
RUN apt update && apt install -y \
curl \
vim \
gcc-mipsel-linux-gnu \
openssh-server \
qemu-user-static \
gdb-multiarch
# Qemu statics are installed at /usr/bin/qemu-mipsel-static
# Compiling AFL++
RUN apt install -y git make build-essential clang ninja-build pkg-config libglib2.0-dev libpixman-1-dev
RUN git clone https://github.com/AFLplusplus/AFLplusplus /AFLplusplus
WORKDIR /AFLplusplus
RUN make all
WORKDIR /AFLplusplus/qemu_mode
RUN CPU_TARGET=mipsel ./build_qemu_support.sh
RUN echo "#!/bin/bash\n\nsleep infinity" >> /entry.sh
RUN chmod +x /entry.sh
WORKDIR /share
ENTRYPOINT [ "/entry.sh" ]
Dockerfile الذي يثبّت الأدوات اللازمة.
يمكن بعد ذلك بناء الصورة باستخدام docker build.```sh
docker build -t fuzz .
عند بناء الصورة، يمكن استخدامها بسهولة مع `docker run` والذي
يبدأ بعد ذلك تشغيل الحاوية.```sh
docker run -d --rm -v $PWD/:/share --name fuzz fuzz
سيؤدي استخدام الخيار -d إلى تشغيل الحاوية في الخلفية. باستخدام docker exec يمكن بدء تشغيل عدة أصداف
داخل الحاوية، وهو أمر مفيد لبدء تشغيل الملف التنفيذي في جلسة واحدة باستخدام
QEMU وفي الجلسة الأخرى gdb-multiarch.```sh
docker exec -it fuzz /bin/bash
### تجاوز الدالة الرئيسية
في القسم السابق، تم تحديد هدف فازينغ قوي. المشكلة هي أنه عند تنفيذ
البرنامج الثنائي، لن نصل أبدًا إلى استدعاء الدالة لأن الدالة `parser_parse` تُستدعى فقط إذا
تم استلام حزمة TCP عبر مأخذ توصيل. هذا لا يضر بالأداء فحسب، بل يجعل من الصعب أيضًا
الإعداد. لهذا السبب، يجب أن تكون نقطة دخول أداة الفازينغ في موقع مختلف عن الدالة الرئيسية
العادية. لذلك، يمكن استخدام متغير البيئة `LD_PRELOAD` الذي يتيح حقن أداة ربط لديها
وصول إلى الدوال الداخلية. كما يصف دليل `ld.so`، وهو المسؤول عن
ربط المكتبات المشتركة المطلوبة لبرنامج قابل للتنفيذ في وقت التشغيل، يمكن استخدام `LD_PRELOAD`
"لتجاوز الدوال بشكل انتقائي في الكائنات المشتركة الأخرى
[\[man-pages\]](https://www.man7.org/linux/man-pages/man8/ld.so.8.html)."
الدالة `__uClibc_main` هي الأنسب لهذا الغرض. لتجاوز هذه الدالة، يجب إنشاء ملف C
يحتوي على دالة بنفس الاسم.```c
void __uClibc_main(void *main, int argc, char** argv) {
// Harness code, e.g. call the function parser_append
printf("My custom __uClibc_main was called!");
}
يمكن بعد ذلك تجميع ملف C بشكل متقاطع إلى كائن مشترك في بنية mipsel باستخدام
mipsel-linux-gnu-gcc. يفعّل الخيار -fPIC "Position Independent Code" (الكود المستقل عن العنوان) مما يعني أن
الكود الآلي لا يعتمد على وجوده في عنوان محدد من خلال استخدام العنونة النسبية
بدلاً من المطلقة.```txt
$ mipsel-linux-gnu-gcc parser_parse_hook.c -o parser_parse_hook.o -shared -fPIC
يمكن بعد ذلك تحميل المكتبة المشتركة المنشأة حديثًا عن طريق إضافة متغير البيئة `LD_PRELOAD`
إلى أمر QEMU.```txt
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o /usr/bin/wscd
My custom __uClibc_main was called!
باستخدام الأمر chroot، يمكن تغيير الدليل الحالي والدليل الجذر للأمر المقدَّم.
هذا مفيد لأن الملف التنفيذي wscd يفتح ملفات أخرى، مثل المكتبات المشتركة من
البرنامج الثابت. يمكننا ملاحظة هذا السلوك بإضافة الوسيط -strace إلى QEMU.```txt
chroot root /qemu-mipsel-static -E LD_PRELOAD=/parser_parse_hook.o -strace /usr/bin/wscd /corpus/notify.txt
38180 mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANONYMOUS|0x4000000,-1,0) = 0x7f7e7000
38180 stat("/etc/ld.so.cache",0x7ffffa48) = -1 errno=2 (No such file or directory)
38180 open("/parser_parse_hook.o",O_RDONLY) = 3
38180 fstat(3,0x7ffff920) = 0
38180 close(3) = 0
38180 munmap(0x7f7e6000,4096) = 0
38180 open("/lib/libpthread.so.0",O_RDONLY) = 3
38180 open("/lib/libc.so.0",O_RDONLY) = 3
[...]
كما نرى، يفتح الملف التنفيذي عدة مكتبات في مجلد `/lib/` داخل البرنامج الثابت وليس على المضيف.
### تطوير وتصحيح الـ harness
بعد إنشاء الإعداد، يمكننا الآن البدء في تطوير harness. كما هو موضح في قسم الخلفية، فإن الـ harness هو المُوجِّه بين الـ fuzzer والدالة المستهدفة. يقوم الـ harness بتحميل مدخلات التشويش (fuzz input)، التي يخزّنها AFL++ في ملف. باستخدام مسار الملف كوسيط، يستدعي الـ harness الهدف المراد اختباره؛ وفي هذه الحالة، سيكون ذلك `parser_append`. يمكن استدعاء الدوال باستخدام العنوان.
<div id="c10"></div>```c
void __uClibc_main(void *main, int argc, char** argv)
{
// Verify that a filename is provided
if (argc != 2) exit(1);
// Create function pointer to the fuzz target
int (*parser_request_init)(void *, int) = (void *) 0x00412564;
int (*parser_append)(void *, void *, int) = (void *) 0x00412e98;
// Open the fuzz input file
int fd = open(argv[1], O_RDONLY);
char fuzz_buf[2048 + 1];
int fuzz_buf_len = read(fd, fuzz_buf, sizeof(fuzz_buf) - 1);
if (fuzz_buf_len < 0) exit(1);
fuzz_buf[fuzz_buf_len] = 0;
// Call the target functions
uint8_t parsed_data[220];
parser_request_init(parsed_data, 8);
int status = parser_append(parsed_data, fuzz_buf, fuzz_buf_len);
printf("Response is %d\n", status);
exit(0);
}
الكود 10: كود التهيئة مع هدف التزويد العشوائي `parser_append` في الثنائي wscd.
كما هو موضح في الكود 10، فإن الدالة parser_parse لا تُستدعى مباشرةً
بل عبر استخدام الدالة parser_append. وقبل استدعاء هذه الدالة، يجب
استدعاء دالة التهيئة parser_request_init، التي تقوم بتهيئة البنية الناتجة
عن دالة parser_parse.
وبينما تكون التهيئة في حالة parser_parse سهلة الإعداد إلى حد كبير، فإن الأهداف الأخرى تتطلب
تهيئات أكثر تعقيدًا مثل دالة httpd_parser_main. على سبيل المثال، قبل استدعاء
الهدف، يجب استدعاء الدالة http_init_main، التي ينتهي تنفيذها بخطأ تجزئة SIGSEGV. لمعرفة مكان
حدوث هذا الخطأ، من المفيد تنقيح الكود باستخدام مصحح أخطاء مثل gdb. وللقيام
بذلك، يمكن تشغيل QEMU مع الخيار -g الذي يستدعي خادم gdb-server على المنفذ المحدد.```sh
chroot root /qemu-mipsel-static -strace -g 1234 -E LD_PRELOAD="/httpd_parser_main.o" /usr/bin/httpd
corpus/httpd/simple.txt
لأن الملف الثنائي بمعمارية `mipsel`، يجب استخدام `gdb-multiarch`. بعد بدء تشغيل gdb، يمكن تحميل سكربت التهيئة التالي باستخدام gdb عبر `sources <path to script>`.```sh
set solib-absolute-prefix /share/root/
file /share/root/usr/bin/httpd
target remote :1234
# break bevor fuzz target is called
# break __uClibc_main
break http_parser_main
display/4i $pc
بسبب chroot، غيّر السكربت أولاً مسار البادئة المطلق بحيث عندما يُحمّل الثنائي
كائنًا مشتركًا، سيجد gdb الملف. ثم يتم تعيين الملف المستهدف، لأن gdb-server
الخاص بـ QEMU لا يدعم نقل الملفات، لذلك يحاول gdb تحميل الملفات من القرص بدلاً من ذلك.
بعد تكوين gdb، يتصل السكربت بخادم gdb باستخدام target remote وينشئ
نقطة توقف عند بداية الدالة المستهدفة. باستخدام display، يتحسن المخرج فقط، فعند
التنقل خطوة بخطوة، ستظهر الأسطر الأربعة التالية من لغة التجميع. باستخدام si يمكننا تنفيذ تعليمة
واحدة، وهو مفيد عندما يحدث في الهارنس خطأ تجزئة عند استخدام المجموعة الافتراضية،
والتي يجب أن تعمل دائمًا. كما هو موضح في الكود 11، يحدث في الثنائي خطأ تجزئة في
الدالة fprintf.
Program received signal SIGSEGV, Segmentation fault.
<p style="text-align: center">الكود 11: خطأ تجزئة في printf.</p>
للتحقيق في الخطأ، يمكن استخدام Ghidra لمعرفة المعاملات التي تُستدعى بها الدالة.```c
fprintf(
*(FILE **)(iVar1 + 0x101c),
"HTTP/1.1 %d %s\r\n",
*(undefined4 *)(&DAT_0042ee68 + (uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 8),
(&PTR_DAT_0042ee6c)[(uint)(byte)(&DAT_00414570)[statuscode & 0x3f] * 2]
);
من المحتمل أن يكون سبب SIGSEGV هو أن المعامل الأول ليس واصف ملف بل مؤشر فارغ. حيث أن iVar1 هو مجرد إشارة إلى مدخلات دالة httpd_parser_main. هذا يعني أن مدخلات fuzzing يجب أن تحتوي على واصف ملف في الموضع 0x101c. لذا يجب تعديل المدخلات إلى البنية التالية.```c
typedef struct {
int _a; // 4 Bytes
int _b; // 4 Bytes
int socket; // 4 Bytes
int ip; // 4 Bytes
int mac; // 4 Bytes
unsigned char body[0x1008]; // 0x101c - 4*5 = 0x1008 Bytes
FILE * fd_out; // expected to be a valid file descriptor
} HttpMainT;
نظرًا لأن `fd_out` يجب أن يكون مجرد مؤشر واصف ملف صالح، فيمكن ضبطه بسهولة على `stdout`.
سيؤدي تنفيذ `httpd_parser_main` مرة أخرى إلى إنتاج مخرجات HTTP صالحة الآن.```c
$ chroot root /qemu-mipsel-static -E LD_PRELOAD=/httpd_parser_main.o \
/usr/bin/httpd /httpd_corpus.txt
bind: No such file or directory
[ dm_shmInit ] 086: shmget to exitst shared memory failed. Could not create shared memory.
rdp_getObj is called with: 4274932gdpr_getSystemGDPREntry Error
gdpr_getNewSystemGDPREntry OK
#Msg: getsockname error
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 24257
Set-Cookie: JSESSIONID=deleted; Expires=Thu, 01 Jan 1970 00:00:01 GMT; Path=/; HttpOnly
Connection: close
<!DOCTYPE html>
[...]
تعمل المنصة الآن ويمكن استخدامها لتشويش الدالة باستخدام AFL++، وهو ما سيتم شرحه في القسم التالي.
كما ذُكر في الخلفية، تصف مجموعة البيانات الأولية (seed corpus) عينات الإدخال الصالحة، والتي تعمل كمرجع أساسي لتوليد بيانات إدخال جديدة أثناء عملية التشويش.
عادةً ما تُختار هذه المدخلات لتمثيل جوانب مختلفة من البرامج المستهدفة. تستخدم أداة التشويش (fuzzer) مجموعة البيانات الأولية لتوليد حالات اختبار متحوِّلة أو متطوِّرة تُنفَّذ بعد ذلك على البرنامج المستهدف لكشف الأخطاء أو الانهيارات أو المشكلات الأخرى. تلعب هذه المجموعة دورًا مهمًا في توجيه أداة التشويش إلى المناطق ذات الصلة من البرنامج وزيادة احتمالية اكتشاف الثغرات أو السلوكيات غير المتوقعة. ومن خلال توفير مجموعة متنوعة وتمثيلية من المدخلات الأولية، تساعد مجموعة البيانات الأولية أداة التشويش على استكشاف مسارات مختلفة في الهدف بشكل أسرع، وبالتالي تزيد من التغطية.
عندما يتعلق الأمر بالدوال التي تحلل بيانات الشبكة، يمكن إنشاء هذه المدخلات باستخدام Wireshark لتسجيل حزم مختلفة.
بالنسبة للدالة httpd_parse_main، تم إنشاء أربع مجموعات بيانات مختلفة. تستهدف كل واحدة مسارات مختلفة في الملف الثنائي. أحد الأمثلة هو طلب تسجيل الدخول، الذي يحتوي على اسم المستخدم وكلمة المرور. لهذه المجموعة، كان لا بد من تعديل المنصة لأن TP-Link يستخدم تشفيرًا (ضعيفًا) "لحماية" كلمة المرور. لهذا، يتم تشفير كلمة المرور في المتصفح باستخدام AES ثم فك تشفيرها في الواجهة الخلفية (backend). حيث يتم توليد كلمة المرور في المتصفح ثم تشفيرها باستخدام RSA. بعد ذلك يتم توقيع البيانات المشفرة. ونظرًا لأن أداة التشويش لا يمكنها إنشاء توقيع أو تشفير البيانات، تم استبدال بعض الدوال وأصبحت الآن فقط تفك ترميز البيانات من base64. لهذا، تم أولاً استخراج البيانات كنص صريح من المتصفح باستخدام مصحح الأخطاء الموضح في الشكل 4.

الشكل 4: استخراج البيانات قبل التشفير.
في الهدف، تم بعد ذلك استبدال الدالة rsa_tmp_decrypt_bypart لتغيير المنطق من فك تشفير البيانات إلى مجرد فك الترميز من base64.```c
// Replacing the logic with b64_decode
int rsa_tmp_decrypt_bypart(uint8_t *input, int input_len, uint8_t *output) { // other params just key data
int (*b64_decode)(uint8_t *, int, uint8_t *, int) = (void *) 0x0040bf00;
b64_decode(output, 0x1000, input, input_len);
int * seqnumber = (int *) 0x00444db0;
*seqnumber = 0x3ac28e29-input_len+12;
return 0; // says it was okay
}
<p style="text-align: center">الكود 12: الدالة rsa_tmp_decrypt_bypart الآن تقوم فقط بفك ترميز base64
بدلاً من فك تشفير البيانات.</p>
أثناء تنفيذ مجموعة الاختبارات، تعيد الدالة المستهدفة دائمًا مستند HTML يحتوي على الخطأ "408
Request Timeout". وباستخدام Ghidra وGDB يمكن تحديد المشكلة. يحدث الخطأ دائمًا
بعد استدعاء الدالة `http_stream_fgets`. كان السطر الإشكالي هو التحقق من
حرف نهاية السطر `\n`.```c
if (((cVar1 == '\n') && (param_3 < pcVar4)) && (pcVar4[-1] == '\r')) {
يفرض هذا الشرط أنه بعد كل فاصل أسطر يجب أن يأتي حرف إرجاع. بعد إضافة حرف الإرجاع، عملت جميع الـ corpora التي تم إنشاؤها.
في القسم الأخير، قمنا بتطوير عدة harnesses وتنفيذها باستخدام QEMU. في هذا القسم، يتم استبدال QEMU بـ AFL++ الذي يستلم الـ corpora المُولّدة كمدخلات بذرية لتشويش الدالة الهدف. في قسم "Fuzzing environment"، تم إنشاء صورة Docker تقوم بالفعل بسحب AFL++ من GitHub ثم تستخدم برنامجًا نصيًا مقدمًا من AFL++ لبناء نسخة معدلة من QEMU. وبالتالي يمكن الآن تشغيل AFL++ باستخدام الأمر التالي الذي يستقبل معاملات مختلفة، مثل -Q الذي يخبر AFL++ باستخدام النسخة المعدلة من QEMU.```sh
QEMU_LD_PREFIX=/share/root AFL_PRELOAD=/share/root/httpd_parser_main.o
/AFLplusplus/afl-fuzz -Q
-i /share/root/corpus/httpd/ -o /share/afl-out/httpd/
-- /share/root/usr/bin/httpd @@
<p style="text-align: center">الكود 13: تضمين الثنائي <code>httpd</code> باستخدام الحزام
و <code>afl-fuzz</code>.</p>
على عكس ما سبق، لم يعد الأمر `chroot` ضروريًا ويُستبدل بالمتغير
`QEMU_LD_PREFIX`. وهو يخبر QEMU أين يبحث عن المكتبات المشتركة. أيضًا، يُستبدل المتغير `LD_PRELOAD`
بالنسخة الخاصة بـ AFL وهي `AFL_PRELOAD`. الوسيط الأخير في الأمر هو
الحرفان `@`. سيستبدلهما AFL++ بمسار ملف يحمل مدخلات التضمين.
عند التشغيل، يعرض AFL++ التقدم باستخدام واجهة الطرفية الموضحة في [الشكل 5](#f5).
<div id="f5"></div>
<figure>
<p><img src="https://assets.kitploit.com/production/public/readmes/48851/ac4b12fcf84c2041c9edc2a0871c5bb89b576d976b2eba0a2d0f843e7e708795.png" style="width:95.0%" /></p>
<figcaption><p style="text-align: center">الشكل 5: شاشة الحالة من AFL++.</p></figcaption>
</figure>
توفر شاشة حالة `AFL++` رؤى أساسية حول عملية التضمين الحالية. توثيقات
`AFL++` تحتوي على نظرة عامة جيدة للمصطلحات المستخدمة في شاشة الحالة
[\[afl-screen\]](https://aflplus.plus/docs/status_screen/). عند تصحيح أخطاء مجموعة المدخلات باستخدام
متغيرات البيئة التالية، يمكن تعطيل الواجهة، ومع `AFL_DEBUG` يتم تمكين تسجيل مفصّل،
يعرض مدخلات المُضمِّن الحالية و `stdout` من البرنامج الهدف.```sh
export AFL_DEBUG=1 && export AFL_NO_UI=1
unset AFL_DEBUG && unset AFL_NO_UI
كما هو موضح في الشكل 5، يمكن أن يستغرق اختبار التغذية العشوائية (fuzzing) لملف تنفيذي وقتًا طويلاً. وفقًا للوثائق، «يُتوقع أن يستغرق التشغيل أيامًا أو أسابيع» و«سيُسمح لبعض المهام بالعمل لأشهر». لتحسين الوقت المطلوب، يجب أن تكون سرعة التنفيذ أعلى من 100 عملية تنفيذ/ثانية. على سبيل المثال، عند اختبار الهدف httpd_main_parser بالتغذية العشوائية، كانت سرعة التنفيذ في البداية حوالي 30/ثانية. لتحسين السرعة، تم البحث في الملف التنفيذي الهدف عن دوال مشتبه بها، والتي من المحتمل أن تكون سبب التباطؤ. كانت إحدى الدوال المشتبه بها هي rsa_gdpr_generate_key لأنه من المعروف أن توليد مفتاح RSA بطيء. بعد استبدال الدالة، تحسنت السرعة إلى 600 عملية تنفيذ في الثانية.
أحد المؤشرات التي تساعد في تحديد موعد إيقاف التغذية العشوائية هو عداد الدورة (cycle counter). سيقوم AFL++ بتمييز الرقم باللون الأخضر عندما «لا يرى المُختبر أي نشاط لفترة أطول»، مما يساعد في اتخاذ قرار إيقاف المُختبر.
لكن الرقم الأكثر إثارة للاهتمام هو على الأرجح «إجمالي الانهيارات». يُظهر هذا الوقت الذي ينهار فيه البرنامج بسبب مدخلات التغذية العشوائية الحالية، ومن المحتمل أن يكون خطأً متعلقًا بالذاكرة. للتحقق من أن هذا خطأ حقيقي، يمكن استخدام gdb مرة أخرى لتحديد موقع الخطأ.
قد تكون التغذية العشوائية (Fuzzing) الطريقة الأكثر فعالية لاكتشاف الثغرات الأمنية. في هذه الورقة البحثية، تم اختبار ثلاث دوال مختلفة بالتغذية العشوائية، لكن لم يتم العثور على أي ثغرة. في حين أن إعداد التغذية العشوائية بالصندوق الأسود (black box fuzzing) بحد ذاته ليس معقدًا أو مستهلكًا للوقت، إلا أن العثور على هدف فعّال وتطوير أداة ربط (harness) تعمل هو الأمر المعقد والمستهلك للوقت. في معظم الأوقات، يجب تصحيح أخطاء أداة الربط، ثم يجب إجراء هندسة عكسية للمنطق الأساسي في الملف التنفيذي، وهو ما يستهلك وقتًا طويلاً مرة أخرى.