
Analyse des causes profondes et validation du correctif de CVE-2023-52356 dans libtiff à l'aide d'AddressSanitizer et GDB.
Ce laboratoire analyse la CVE-2023-52356 dans libtiff, en se concentrant sur le comportement de
TIFFReadRGBATileExt lorsqu'il reçoit des coordonnées d'image en dehors des limites
valides de l'image.
L'analyse comprend la reproduction du crash d'origine, le débogage avec AddressSanitizer et GDB, l'analyse de la cause racine, l'examen du correctif en amont, l'analyse du reproducteur d'origine et la vérification de la version corrigée.
4d0329a451558511triger_input_47Le reproducteur d'origine a été compilé contre la version vulnérable de libtiff avec AddressSanitizer activé.
Le reproducteur a été exécuté avec le fichier déclencheur fourni :
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
L'exécution a reproduit une erreur de segmentation causée par une lecture mémoire
invalide. La trace de pile AddressSanitizer a identifié la défaillance lors d'une
opération memmove appelée depuis TIFFReadRGBATileExt.
À l'aide de GDB, la condition du crash a été reproduite avec les valeurs d'exécution suivantes :
row = 34
img.height = 33
tile_ysize = 1
Le code vulnérable exécutait ensuite :
read_ysize = img.height - row;
Avec les valeurs observées, ce calcul donne :
33 - 34 = -1
Comme read_ysize est non signé, le résultat a été enveloppé à :
read_ysize = 4294967295
ce qui correspond à UINT32_MAX.
La valeur de read_ysize a ensuite été utilisée dans le calcul du pointeur source
pour memmove.
Immédiatement avant le memmove défaillant, GDB a montré :
read_ysize = 4294967295
read_xsize = 1
tile_ysize = 1
tile_xsize = 1
i_row = 0
raster = 0x7d0ff67e2d40
Le pointeur de destination a été évalué à :
0x7d0ff67e2d40
ce qui correspondait au début du tampon raster.
Le pointeur source a été évalué à :
0x7d13f67e2d38
Le pointeur source calculé était à 17179869176 octets, soit environ
16 Gio, au-delà du début du tampon raster.
L'exécution du memmove dans GDB a entraîné :
SIGSEGV, Segmentation fault
La trace arrière a montré le chemin de crash suivant :
__sanitizer_internal_memmove
__asan_memmove
TIFFReadRGBATileExt at tif_getimage.c:3345
LLVMFuzzerTestOneInput at poc.cc:61
Cela confirme que le dépassement inférieur non signé dans read_ysize a produit
un décalage source hors limites. Le pointeur source invalide résultant a ensuite été
utilisé par memmove, provoquant une lecture mémoire invalide et une erreur de
segmentation.
Le correctif en amont a ajouté une vérification explicite des limites avant le
calcul vulnérable de read_ysize :
if (col >= img.width || row >= img.height)
{
TIFFErrorExtR(tif, TIFFFileName(tif),
"Invalid row/col passed to TIFFReadRGBATile().");
TIFFRGBAImageEnd(&img);
return (0);
}
À l'aide de GDB sur la version corrigée, les valeurs d'exécution suivantes ont été observées :
row = 34
img.height = 33
col = 0
img.width = 2047
Pour ces valeurs, la nouvelle condition de validation est vraie car :
row >= img.height
34 >= 33
La fonction a donc signalé :
Invalid row/col passed to TIFFReadRGBATile()
et a renvoyé 0.
Par conséquent, l'exécution n'a pas atteint le calcul vulnérable :
read_ysize = img.height - row;
Cela empêche le dépassement inférieur non signé observé dans la version vulnérable et
empêche la valeur invalide d'être utilisée dans le calcul ultérieur du pointeur source
de memmove.
Le reproducteur d'origine calcule incorrectement le nombre de tuiles le long de l'axe Y.
Le code concerné transmet la valeur de retour de TIFFGetField() directement comme
coordonnée Y à TIFFComputeTile() :
TIFFComputeTile(
in_tif,
0,
TIFFGetField(in_tif, TIFFTAG_IMAGELENGTH, &tile_height),
0,
0)
À l'aide de GDB, tile_height a été observé comme contenant :
tile_height = 33
Cependant, TIFFGetField() a renvoyé :
1
La valeur de retour indique le succès ; ce n'est pas la hauteur de l'image. Par conséquent, l'appel devient effectivement :
TIFFComputeTile(in_tif, 0, 1, 0, 0)
Pour ce TIFF, GDB a montré que cet appel renvoie :
2047
Cette valeur est un index de tuile, pas le nombre de tuiles le long de l'axe Y.
Le reproducteur d'origine a ensuite utilisé cette valeur dans son calcul de
num_tiles_y, ce qui a donné :
num_tiles_y = 63
Cependant, les dimensions de l'image et des tuiles sont :
image_width = 2047
image_height = 33
tile_width = 1
tile_height = 1
Par conséquent, le nombre correct de tuiles le long de l'axe Y est :
num_tiles_y = 33
La valeur incorrecte de 63 fait itérer la boucle avec des valeurs Y de
0 à 62, même si la hauteur de l'image est 33 et que les coordonnées Y
valides sont uniquement 0 à 32.
Cela permet à une valeur invalide telle que :
row = 34
d'être transmise à TIFFReadRGBATileExt.
La version vulnérable de libtiff n'a pas rejeté cette coordonnée hors limites
avant d'effectuer le calcul non signé img.height - row. Cela a permis à l'entrée
API invalide générée par le reproducteur de devenir une défaillance de sécurité
mémoire.
Le reproducteur corrigé fourni par le mainteneur calcule le nombre de tuiles directement à partir des dimensions de l'image et des tuiles.
Pour le même TIFF, le calcul corrigé produit :
num_tiles_x = 2047
num_tiles_y = 33
Le reproducteur corrigé a été testé contre la même version vulnérable de libtiff et le même fichier déclencheur.
Le programme s'est terminé avec le statut 0, et aucun rapport AddressSanitizer ou
SIGSEGV n'a été produit.
Cela confirme l'observation du mainteneur selon laquelle le reproducteur d'origine contient un calcul incorrect du nombre de tuiles. Cependant, la version vulnérable de libtiff manquait toujours de validation défensive des lignes et colonnes, permettant à des coordonnées API invalides d'entraîner une défaillance de sécurité mémoire.
| Reproducteur | Version de libtiff | Résultat |
|---|---|---|
| PoC d'origine | Vulnérable (4d0329a4) | Lecture invalide et SIGSEGV dans memmove |
| Reproducteur corrigé | Vulnérable (4d0329a4) | Statut de sortie 0, pas d'ASan/SEGV |
| PoC d'origine | Corrigé (51558511) | Ligne/colonne invalide rejetée, pas d'ASan/SEGV |
Le crash dans TIFFReadRGBATileExt se produit lorsqu'une coordonnée d'image
hors limites atteint l'implémentation vulnérable de libtiff.
Dans le reproducteur d'origine, un calcul incorrect du nombre de tuiles fait que
num_tiles_y est calculé comme 63 au lieu de la valeur correcte 33.
Par conséquent, le reproducteur peut transmettre une ligne en dehors de la plage
d'image valide à TIFFReadRGBATileExt.
Dans la version vulnérable de libtiff, les arguments de ligne et de colonne n'étaient pas validés avant le calcul :
read_ysize = img.height - row;
Pour le cas reproduit, row était 34 tandis que img.height était 33.
Comme read_ysize est non signé, la soustraction a dépassé vers le bas jusqu'à
UINT32_MAX. Cette valeur a ensuite été utilisée dans le calcul du pointeur source
pour memmove, produisant une lecture mémoire invalide et une erreur de
segmentation.
Le correctif en amont ajoute une validation explicite des limites des lignes et
colonnes avant ce calcul. Les tests de la version corrigée ont confirmé que la même
coordonnée invalide est rejetée avant que le dépassement inférieur et memmove
ne puissent se produire.
Le reproducteur corrigé fourni par le mainteneur a également été testé contre la version vulnérable en utilisant le même fichier TIFF. Il a calculé le nombre correct de tuiles et s'est terminé sans rapport AddressSanitizer ou SIGSEGV.
Par conséquent, cette analyse n'établit pas que le fichier TIFF seul déclenche la défaillance de sécurité mémoire lorsque l'API est utilisée correctement. Le reproducteur d'origine contient un calcul incorrect du nombre de tuiles, tandis que l'implémentation historique de libtiff manquait de validation défensive des limites et permettait à l'entrée API invalide résultante de devenir une défaillance de sécurité mémoire.