
Анализ первопричины и проверка патча для CVE-2023-52356 в libtiff с использованием AddressSanitizer и GDB.
В этом лабораторном анализе рассматривается CVE-2023-52356 в libtiff, с акцентом на поведение
TIFFReadRGBATileExt при получении координат изображения, выходящих за допустимые границы.
Анализ включает воспроизведение исходного сбоя, отладку с помощью AddressSanitizer и GDB, анализ первопричины, изучение исправления в апстриме, анализ исходного репро-файла и проверку исправленной версии.
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, что вызвало недопустимое чтение памяти и ошибку сегментации.
Исправление в апстриме добавило явную проверку границ перед уязвимым
вычислением 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
Для этих значений новое условие проверки принимает значение true, поскольку:
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) | Недопустимые row/col отклонены, нет 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, что привело к недопустимому чтению памяти и ошибке сегментации.
Исправление в апстриме добавляет явную проверку границ строк и столбцов перед этим
вычислением. Тестирование исправленной сборки подтвердило, что та же недопустимая
координата отклоняется до того, как могут произойти переполнение и memmove.
Предоставленный сопровождающим исправленный репро-файл также был протестирован против уязвимой сборки с использованием того же файла TIFF. Он вычислил правильное количество тайлов и завершился без отчёта AddressSanitizer или SIGSEGV.
Таким образом, данный анализ не устанавливает, что сам файл TIFF вызывает отказ безопасности памяти при корректном использовании API. Исходный репро-файл содержит некорректное вычисление количества тайлов, в то время как историческая реализация libtiff не имела защитной проверки границ и позволяла результирующему недопустимому входному значению API превращаться в отказ безопасности памяти.