
تحليل السبب الجذري والتحقق من التصحيح لثغرة CVE-2023-52356 في libtiff باستخدام AddressSanitizer وGDB.
يحلل هذا المختبر ثغرة CVE-2023-52356 في libtiff، مع التركيز على سلوك
TIFFReadRGBATileExt عندما يستقبل إحداثيات صور خارج حدود الصورة الصالحة.
يتضمن التحليل إعادة إنتاج الانهيار الأصلي، والتصحيح باستخدام AddressSanitizer وGDB، وتحليل السبب الجذري، وفحص التصحيح المقدم من upstream، وتحليل أداة إعادة الإنتاج الأصلية، والتحقق من الإصدار المُصحح.
4d0329a451558511triger_input_47تم تجميع أداة إعادة الإنتاج الأصلية مقابل بناء libtiff القابل للاستغلال مع تفعيل AddressSanitizer.
تم تنفيذ أداة إعادة الإنتاج مع ملف التشغيل المقدم:
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
أعاد التنفيذ إنتاج خطأ تجزئة ناتج عن قراءة ذاكرة غير صالحة. حدد تتبع مكدس AddressSanitizer الفشل أثناء
عملية memmove تم استدعاؤها من TIFFReadRGBATileExt.
باستخدام GDB، تم إعادة إنتاج حالة الانهيار مع القيم التالية في وقت التشغيل:
row = 34
img.height = 33
tile_ysize = 1
ثم نفذ الكود القابل للاستغلال:
read_ysize = img.height - row;
مع القيم المرصودة، تكون هذه العملية الحسابية:
33 - 34 = -1
لأن read_ysize غير موقعة، التفّت النتيجة إلى:
read_ysize = 4294967295
وهي UINT32_MAX.
تم استخدام قيمة read_ysize لاحقًا في حساب مؤشر المصدر
لعملية memmove.
مباشرة قبل memmove الفاشلة، أظهر GDB:
read_ysize = 4294967295
read_xsize = 1
tile_ysize = 1
tile_xsize = 1
i_row = 0
raster = 0x7d0ff67e2d40
تم تقييم مؤشر الوجهة إلى:
0x7d0ff67e2d40
وهي بداية مخزن raster.
تم تقييم مؤشر المصدر إلى:
0x7d13f67e2d38
كان مؤشر المصدر المحسوب 17179869176 بايت، أي حوالي
16 جيجابايت، بعد بداية مخزن raster.
أدى تنفيذ memmove في GDB إلى:
SIGSEGV, Segmentation fault
أظهر تتبع المكدس مسار الانهيار التالي:
__sanitizer_internal_memmove
__asan_memmove
TIFFReadRGBATileExt at tif_getimage.c:3345
LLVMFuzzerTestOneInput at poc.cc:61
يؤكد هذا أن التدفق السفلي غير الموقع في read_ysize أنتج
إزاحة مصدر خارج الحدود. تم استخدام مؤشر المصدر غير الصالح الناتج بعد ذلك بواسطة
memmove، مما تسبب في قراءة ذاكرة غير صالحة وخطأ تجزئة.
أضاف الإصلاح المقدم من upstream فحصًا صريحًا للحدود قبل حساب
read_ysize القابل للاستغلال:
if (col >= img.width || row >= img.height)
{
TIFFErrorExtR(tif, TIFFFileName(tif),
"Invalid row/col passed to TIFFReadRGBATile().");
TIFFRGBAImageEnd(&img);
return (0);
}
باستخدام GDB على الإصدار المُصحح، تم رصد القيم التالية في وقت التشغيل:
row = 34
img.height = 33
col = 0
img.width = 2047
بالنسبة لهذه القيم، يكون شرط التحقق الجديد صحيحًا لأن:
row >= img.height
34 >= 33
لذلك أبلغت الدالة عن:
Invalid row/col passed to TIFFReadRGBATile()
وأرجعت 0.
نتيجة لذلك، لم يصل التنفيذ إلى الحساب القابل للاستغلال:
read_ysize = img.height - row;
يمنع هذا التدفق السفلي غير الموقع الذي لوحظ في الإصدار القابل للاستغلال و
يمنع استخدام القيمة غير الصالحة في حساب مؤشر مصدر memmove لاحقًا.
تحسب أداة إعادة الإنتاج الأصلية بشكل غير صحيح عدد البلاطات على طول محور Y.
يمرر الكود ذو الصلة القيمة المرجعة من TIFFGetField() مباشرة كإحداثي Y إلى TIFFComputeTile():
TIFFComputeTile(
in_tif,
0,
TIFFGetField(in_tif, TIFFTAG_IMAGELENGTH, &tile_height),
0,
0)
باستخدام GDB، لوحظ أن tile_height يحتوي على:
tile_height = 33
ومع ذلك، أعادت TIFFGetField():
1
تشير القيمة المرجعة إلى النجاح؛ وهي ليست ارتفاع الصورة. لذلك، يصبح الاستدعاء فعليًا:
TIFFComputeTile(in_tif, 0, 1, 0, 0)
بالنسبة لملف TIFF هذا، أظهر GDB أن هذا الاستدعاء يُرجع:
2047
هذه القيمة هي فهرس بلاطة، وليست عدد البلاطات على طول محور Y.
استخدمت أداة إعادة الإنتاج الأصلية بعد ذلك هذه القيمة في حسابها لـ
num_tiles_y، مما أدى إلى:
num_tiles_y = 63
ومع ذلك، فإن أبعاد الصورة وأبعاد البلاطة هي:
image_width = 2047
image_height = 33
tile_width = 1
tile_height = 1
لذلك، فإن العدد الصحيح للبلاطات على طول محور Y هو:
num_tiles_y = 33
تتسبب القيمة غير الصحيحة 63 في تكرار الحلقة بقيم Y من
0 إلى 62، على الرغم من أن ارتفاع الصورة هو 33 وإحداثيات Y الصالحة
هي فقط من 0 إلى 32.
يسمح هذا بتمرير قيمة غير صالحة مثل:
row = 34
إلى TIFFReadRGBATileExt.
لم يرفض إصدار libtiff القابل للاستغلال هذا الإحداثي خارج النطاق
قبل إجراء حساب img.height - row غير الموقع. سمح هذا لمدخلات API غير الصالحة
التي تم إنشاؤها بواسطة أداة إعادة الإنتاج بأن تتحول إلى فشل في سلامة الذاكرة.
تحسب أداة إعادة الإنتاج المصححة المقدمة من المشرف عدد البلاطات مباشرة من أبعاد الصورة والبلاطة.
بالنسبة لنفس ملف TIFF، ينتج الحساب المصحح:
num_tiles_x = 2047
num_tiles_y = 33
تم اختبار أداة إعادة الإنتاج المصححة ضد نفس بناء libtiff القابل للاستغلال ونفس ملف التشغيل.
خرج البرنامج بالحالة 0، ولم يتم إنتاج أي تقرير AddressSanitizer أو SIGSEGV.
يدعم هذا ملاحظة المشرف بأن أداة إعادة الإنتاج الأصلية تحتوي على حساب غير صحيح لعدد البلاطات. ومع ذلك، لا يزال إصدار libtiff القابل للاستغلال يفتقر إلى التحقق الدفاعي من الصفوف والأعمدة، مما سمح لإحداثيات API غير الصالحة بأن تؤدي إلى فشل في سلامة الذاكرة.
| أداة إعادة الإنتاج | إصدار libtiff | النتيجة |
|---|---|---|
| PoC الأصلي | قابل للاستغلال (4d0329a4) | قراءة غير صالحة وSIGSEGV في memmove |
| أداة إعادة الإنتاج المصححة | قابل للاستغلال (4d0329a4) | حالة خروج 0، بدون ASan/SEGV |
| PoC الأصلي | مُصحح (51558511) | رفض صف/عمود غير صالح، بدون ASan/SEGV |
يحدث الانهيار في TIFFReadRGBATileExt عندما يصل إحداثي صورة خارج النطاق
إلى تنفيذ libtiff القابل للاستغلال.
في أداة إعادة الإنتاج الأصلية، يتسبب حساب غير صحيح لعدد البلاطات في
حساب num_tiles_y كـ 63 بدلاً من القيمة الصحيحة 33.
نتيجة لذلك، يمكن لأداة إعادة الإنتاج تمرير صف خارج نطاق الصورة الصالح إلى
TIFFReadRGBATileExt.
في إصدار libtiff القابل للاستغلال، لم يتم التحقق من وسيطات الصف والعمود قبل الحساب:
read_ysize = img.height - row;
بالنسبة للحالة المعاد إنتاجها، كانت row تساوي 34 بينما كانت img.height تساوي 33.
لأن read_ysize غير موقعة، تفّلت عملية الطرح إلى
UINT32_MAX. تم استخدام هذه القيمة لاحقًا في حساب مؤشر المصدر
لعملية memmove، مما أنتج قراءة ذاكرة غير صالحة وخطأ تجزئة.
يضيف الإصلاح المقدم من upstream تحققًا صريحًا من حدود الصفوف والأعمدة قبل
هذا الحساب. أكد اختبار البناء المُصحح أن نفس الإحداثي غير الصالح
يتم رفضه قبل حدوث التدفق السفلي وmemmove.
تم أيضًا اختبار أداة إعادة الإنتاج المصححة المقدمة من المشرف ضد البناء القابل للاستغلال باستخدام نفس ملف TIFF. حسبت عدد البلاطات الصحيح واكتملت دون أي تقرير AddressSanitizer أو SIGSEGV.
لذلك، لا يثبت هذا التحليل أن ملف TIFF وحده يُطلق فشل سلامة الذاكرة عند استخدام API بشكل صحيح. تحتوي أداة إعادة الإنتاج الأصلية على حساب غير صحيح لعدد البلاطات، بينما افتقر تنفيذ libtiff التاريخي إلى التحقق الدفاعي من الحدود وسمح لمدخلات API غير الصالحة الناتجة بأن تتحول إلى فشل في سلامة الذاكرة.