
Análisis de causa raíz y validación de parches de CVE-2023-52356 en libtiff mediante AddressSanitizer y GDB.
Este laboratorio analiza la CVE-2023-52356 en libtiff, centrándose en el comportamiento de
TIFFReadRGBATileExt cuando recibe coordenadas de imagen fuera de los límites válidos
de la imagen.
El análisis incluye la reproducción del fallo original, la depuración con AddressSanitizer y GDB, el análisis de la causa raíz, el examen del parche ascendente, el análisis del reproductor original y la verificación de la versión corregida.
4d0329a451558511triger_input_47El reproductor original fue compilado contra la compilación vulnerable de libtiff con AddressSanitizer habilitado.
El reproductor se ejecutó con el archivo desencadenante proporcionado:
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
La ejecución reprodujo un fallo de segmentación causado por una lectura de memoria
inválida. El seguimiento de pila de AddressSanitizer identificó el fallo durante una
operación memmove llamada desde TIFFReadRGBATileExt.
Usando GDB, la condición del fallo se reprodujo con los siguientes valores en tiempo de ejecución:
row = 34
img.height = 33
tile_ysize = 1
El código vulnerable luego ejecutó:
read_ysize = img.height - row;
Con los valores observados, este cálculo es:
33 - 34 = -1
Debido a que read_ysize no tiene signo, el resultado se envolvió a:
read_ysize = 4294967295
que es UINT32_MAX.
El valor de read_ysize se usó posteriormente en el cálculo del puntero de origen
para memmove.
Inmediatamente antes del memmove fallido, GDB mostró:
read_ysize = 4294967295
read_xsize = 1
tile_ysize = 1
tile_xsize = 1
i_row = 0
raster = 0x7d0ff67e2d40
El puntero de destino se evaluó como:
0x7d0ff67e2d40
que era el inicio del búfer raster.
El puntero de origen se evaluó como:
0x7d13f67e2d38
El puntero de origen calculado estaba 17179869176 bytes, aproximadamente
16 GiB, más allá del inicio del búfer raster.
La ejecución del memmove en GDB resultó en:
SIGSEGV, Segmentation fault
El backtrace mostró la siguiente ruta del fallo:
__sanitizer_internal_memmove
__asan_memmove
TIFFReadRGBATileExt at tif_getimage.c:3345
LLVMFuzzerTestOneInput at poc.cc:61
Esto confirma que el subdesbordamiento sin signo en read_ysize produjo un
desplazamiento de origen fuera de los límites. El puntero de origen inválido resultante
fue luego usado por memmove, causando una lectura de memoria inválida y un fallo
de segmentación.
La corrección ascendente añadió una comprobación explícita de límites antes del
cálculo vulnerable de read_ysize:
if (col >= img.width || row >= img.height)
{
TIFFErrorExtR(tif, TIFFFileName(tif),
"Invalid row/col passed to TIFFReadRGBATile().");
TIFFRGBAImageEnd(&img);
return (0);
}
Usando GDB en la versión corregida, se observaron los siguientes valores en tiempo de ejecución:
row = 34
img.height = 33
col = 0
img.width = 2047
Para estos valores, la nueva condición de validación se evalúa como verdadera porque:
row >= img.height
34 >= 33
La función por lo tanto informó:
Invalid row/col passed to TIFFReadRGBATile()
y devolvió 0.
Como resultado, la ejecución no alcanzó el cálculo vulnerable:
read_ysize = img.height - row;
Esto evita el subdesbordamiento sin signo observado en la versión vulnerable y
detiene que el valor inválido se use en el cálculo posterior del puntero de origen
de memmove.
El reproductor original calcula incorrectamente el número de mosaicos a lo largo del eje Y.
El código relevante pasa el valor de retorno de TIFFGetField() directamente como
la coordenada Y a TIFFComputeTile():
TIFFComputeTile(
in_tif,
0,
TIFFGetField(in_tif, TIFFTAG_IMAGELENGTH, &tile_height),
0,
0)
Usando GDB, se observó que tile_height contenía:
tile_height = 33
Sin embargo, TIFFGetField() devolvió:
1
El valor de retorno indica éxito; no es la altura de la imagen. Por lo tanto, la llamada efectivamente se convierte en:
TIFFComputeTile(in_tif, 0, 1, 0, 0)
Para este TIFF, GDB mostró que esta llamada devuelve:
2047
Este valor es un índice de mosaico, no el número de mosaicos a lo largo del eje Y.
El reproductor original luego usó este valor en su cálculo de
num_tiles_y, resultando en:
num_tiles_y = 63
Sin embargo, las dimensiones de la imagen y del mosaico son:
image_width = 2047
image_height = 33
tile_width = 1
tile_height = 1
Por lo tanto, el número correcto de mosaicos a lo largo del eje Y es:
num_tiles_y = 33
El valor incorrecto de 63 hace que el bucle itere con valores Y desde
0 hasta 62, aunque la altura de la imagen es 33 y las coordenadas Y
válidas son solo 0 hasta 32.
Esto permite que un valor inválido como:
row = 34
se pase a TIFFReadRGBATileExt.
La versión vulnerable de libtiff no rechazó esta coordenada fuera de rango
antes de realizar el cálculo sin signo img.height - row. Esto permitió que
la entrada de API inválida generada por el reproductor se convirtiera en un
fallo de seguridad de memoria.
El reproductor corregido proporcionado por el mantenedor calcula el número de mosaicos directamente a partir de las dimensiones de la imagen y del mosaico.
Para el mismo TIFF, el cálculo corregido produce:
num_tiles_x = 2047
num_tiles_y = 33
El reproductor corregido se probó contra la misma compilación vulnerable de libtiff y el mismo archivo desencadenante.
El programa salió con el estado 0, y no se produjo ningún informe de AddressSanitizer
ni SIGSEGV.
Esto respalda la observación del mantenedor de que el reproductor original contiene un cálculo incorrecto del número de mosaicos. Sin embargo, la versión vulnerable de libtiff aún carecía de validación defensiva de filas y columnas, lo que permitía que coordenadas de API inválidas resultaran en un fallo de seguridad de memoria.
| Reproductor | Versión de libtiff | Resultado |
|---|---|---|
| PoC original | Vulnerable (4d0329a4) | Lectura inválida y SIGSEGV en memmove |
| Reproductor corregido | Vulnerable (4d0329a4) | Estado de salida 0, sin ASan/SEGV |
| PoC original | Corregida (51558511) | Fila/columna inválida rechazada, sin ASan/SEGV |
El fallo en TIFFReadRGBATileExt ocurre cuando una coordenada de imagen fuera
de rango alcanza la implementación vulnerable de libtiff.
En el reproductor original, un cálculo incorrecto del número de mosaicos hace
que num_tiles_y se calcule como 63 en lugar del valor correcto 33.
Como resultado, el reproductor puede pasar una fila fuera del rango válido de la imagen a
TIFFReadRGBATileExt.
En la versión vulnerable de libtiff, los argumentos de fila y columna no se validaban antes del cálculo:
read_ysize = img.height - row;
Para el caso reproducido, row era 34 mientras que img.height era 33.
Debido a que read_ysize no tiene signo, la resta se subdesbordó a
UINT32_MAX. Este valor se usó posteriormente en el cálculo del puntero de origen
para memmove, produciendo una lectura de memoria inválida y un fallo de
segmentación.
La corrección ascendente añade validación explícita de límites de fila y columna antes de
este cálculo. Las pruebas con la compilación corregida confirmaron que la misma
coordenada inválida se rechaza antes de que puedan ocurrir el subdesbordamiento y el memmove.
El reproductor corregido proporcionado por el mantenedor también se probó contra la compilación vulnerable usando el mismo archivo TIFF. Calculó el número correcto de mosaicos y se completó sin un informe de AddressSanitizer o SIGSEGV.
Por lo tanto, este análisis no establece que el archivo TIFF por sí solo desencadene el fallo de seguridad de memoria cuando la API se usa correctamente. El reproductor original contiene un cálculo incorrecto del número de mosaicos, mientras que la implementación histórica de libtiff carecía de validación defensiva de límites y permitía que la entrada de API inválida resultante se convirtiera en un fallo de seguridad de memoria.