本实验室分析了 libtiff 中的 CVE-2023-52356,重点关注 TIFFReadRGBATileExt 在接收到超出有效图像边界的图像坐标时的行为。
分析内容包括原始崩溃的复现、使用 AddressSanitizer 和 GDB 进行调试、根本原因分析、上游补丁的检查、原始复现程序的分析以及修复版本的验证。
4d0329a451558511triger_input_47原始复现程序是针对启用了 AddressSanitizer 的漏洞 libtiff 构建进行编译的。
复现程序使用提供的触发文件执行:
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
执行过程复现了由无效内存读取引起的段错误。AddressSanitizer 堆栈跟踪识别出失败发生在从 TIFFReadRGBATileExt 调用的 操作期间。
memmove使用 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
计算出的源指针位于 raster 缓冲区起始位置之后 17179869176 字节处,约为 16 GiB。
在 GDB 中执行 memmove 导致:
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
对于这些值,新的验证条件计算结果为真,因为:
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 导致循环使用从 0 到 62 的 Y 值进行迭代,尽管图像高度为 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) | memmove 中无效读取和 SIGSEGV |
| 修正复现程序 | 易受攻击(4d0329a4) | 退出状态 0,无 ASan/SEGV |
| 原始 PoC | 已修复(51558511) | 无效行/列被拒绝,无 ASan/SEGV |
当越界图像坐标到达易受攻击的 libtiff 实现时,TIFFReadRGBATileExt 中会发生崩溃。
在原始复现程序中,错误的瓦片计数计算导致 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 输入成为内存安全故障。