
Análise de causa-raiz e validação de patch do CVE-2023-52356 no libtiff usando AddressSanitizer e GDB.
Este laboratório analisa a CVE-2023-52356 no libtiff, com foco no comportamento de
TIFFReadRGBATileExt quando recebe coordenadas de imagem fora dos limites válidos
da imagem.
A análise inclui a reprodução da falha original, depuração com AddressSanitizer e GDB, análise da causa raiz, exame do patch upstream, análise do reprodutor original e verificação da versão corrigida.
4d0329a451558511triger_input_47O reprodutor original foi compilado contra a versão vulnerável do libtiff com AddressSanitizer habilitado.
O reprodutor foi executado com o arquivo de disparo fornecido:
LD_LIBRARY_PATH="$PWD/libtiff/build-asan/libtiff" ./poc triger_input_47
A execução reproduziu uma falha de segmentação causada por uma leitura de memória
inválida. O rastreamento de pilha do AddressSanitizer identificou a falha durante
uma operação memmove chamada a partir de TIFFReadRGBATileExt.
Usando GDB, a condição da falha foi reproduzida com os seguintes valores em tempo de execução:
row = 34
img.height = 33
tile_ysize = 1
O código vulnerável então executou:
read_ysize = img.height - row;
Com os valores observados, este cálculo é:
33 - 34 = -1
Como read_ysize não tem sinal, o resultado foi envolvido para:
read_ysize = 4294967295
que é UINT32_MAX.
O valor de read_ysize foi posteriormente usado no cálculo do ponteiro de origem
para memmove.
Imediatamente antes do memmove que falhou, o GDB mostrou:
read_ysize = 4294967295
read_xsize = 1
tile_ysize = 1
tile_xsize = 1
i_row = 0
raster = 0x7d0ff67e2d40
O ponteiro de destino avaliado para:
0x7d0ff67e2d40
que era o início do buffer raster.
O ponteiro de origem avaliado para:
0x7d13f67e2d38
O ponteiro de origem calculado estava 17179869176 bytes, aproximadamente
16 GiB, além do início do buffer raster.
A execução do memmove no GDB resultou em:
SIGSEGV, Segmentation fault
O backtrace mostrou o seguinte caminho da falha:
__sanitizer_internal_memmove
__asan_memmove
TIFFReadRGBATileExt at tif_getimage.c:3345
LLVMFuzzerTestOneInput at poc.cc:61
Isso confirma que o underflow sem sinal em read_ysize produziu um
deslocamento de origem fora dos limites. O ponteiro de origem inválido resultante
foi então usado por memmove, causando uma leitura de memória inválida e uma
falha de segmentação.
A correção upstream adicionou uma verificação explícita de limites antes do
cálculo vulnerável 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 na versão corrigida, os seguintes valores em tempo de execução foram observados:
row = 34
img.height = 33
col = 0
img.width = 2047
Para esses valores, a nova condição de validação é avaliada como verdadeira porque:
row >= img.height
34 >= 33
A função portanto relatou:
Invalid row/col passed to TIFFReadRGBATile()
e retornou 0.
Como resultado, a execução não alcançou o cálculo vulnerável:
read_ysize = img.height - row;
Isso impede o underflow sem sinal observado na versão vulnerável e
impede que o valor inválido seja usado no cálculo posterior do ponteiro de origem
do memmove.
O reprodutor original calcula incorretamente o número de tiles ao longo do eixo Y.
O código relevante passa o valor de retorno de TIFFGetField() diretamente como
a coordenada Y para TIFFComputeTile():
TIFFComputeTile(
in_tif,
0,
TIFFGetField(in_tif, TIFFTAG_IMAGELENGTH, &tile_height),
0,
0)
Usando GDB, observou-se que tile_height continha:
tile_height = 33
No entanto, TIFFGetField() retornou:
1
O valor de retorno indica sucesso; não é a altura da imagem. Portanto, a chamada efetivamente se torna:
TIFFComputeTile(in_tif, 0, 1, 0, 0)
Para este TIFF, o GDB mostrou que esta chamada retorna:
2047
Este valor é um índice de tile, não o número de tiles ao longo do eixo Y.
O reprodutor original então usou este valor em seu cálculo de
num_tiles_y, resultando em:
num_tiles_y = 63
No entanto, as dimensões da imagem e as dimensões dos tiles são:
image_width = 2047
image_height = 33
tile_width = 1
tile_height = 1
Portanto, o número correto de tiles ao longo do eixo Y é:
num_tiles_y = 33
O valor incorreto de 63 faz com que o loop itere com valores Y de
0 a 62, mesmo que a altura da imagem seja 33 e as coordenadas Y válidas
sejam apenas 0 a 32.
Isso permite que um valor inválido como:
row = 34
seja passado para TIFFReadRGBATileExt.
A versão vulnerável do libtiff não rejeitou esta coordenada fora do intervalo
antes de realizar o cálculo sem sinal img.height - row. Isso permitiu
que a entrada de API inválida gerada pelo reprodutor se tornasse uma falha de
segurança de memória.
O reprodutor corrigido fornecido pelo mantenedor calcula o número de tiles diretamente a partir das dimensões da imagem e dos tiles.
Para o mesmo TIFF, o cálculo corrigido produz:
num_tiles_x = 2047
num_tiles_y = 33
O reprodutor corrigido foi testado contra a mesma versão vulnerável do libtiff e o mesmo arquivo de disparo.
O programa saiu com status 0, e nenhum relatório de AddressSanitizer ou SIGSEGV
foi produzido.
Isso apoia a observação do mantenedor de que o reprodutor original contém um cálculo incorreto de contagem de tiles. No entanto, a versão vulnerável do libtiff ainda carecia de validação defensiva de linha e coluna, permitindo que coordenadas de API inválidas resultassem em uma falha de segurança de memória.
| Reproduzir | Versão do libtiff | Resultado |
|---|---|---|
| PoC original | Vulnerável (4d0329a4) | Leitura inválida e SIGSEGV em memmove |
| Reproduzir corrigido | Vulnerável (4d0329a4) | Status de saída 0, sem ASan/SEGV |
| PoC original | Corrigido (51558511) | Linha/coluna inválida rejeitada, sem ASan/SEGV |
A falha em TIFFReadRGBATileExt ocorre quando uma coordenada de imagem fora
do intervalo alcança a implementação vulnerável do libtiff.
No reprodutor original, um cálculo incorreto de contagem de tiles faz com que
num_tiles_y seja calculado como 63 em vez do valor correto 33.
Como resultado, o reprodutor pode passar uma linha fora do intervalo válido da
imagem para TIFFReadRGBATileExt.
Na versão vulnerável do libtiff, os argumentos de linha e coluna não eram validados antes do cálculo:
read_ysize = img.height - row;
Para o caso reproduzido, row era 34 enquanto img.height era 33.
Como read_ysize não tem sinal, a subtração sofreu underflow para
UINT32_MAX. Este valor foi subsequentemente usado no cálculo do ponteiro de
origem para memmove, produzindo uma leitura de memória inválida e uma
falha de segmentação.
A correção upstream adiciona validação explícita de limites de linha e coluna antes
deste cálculo. O teste da versão corrigida confirmou que a mesma coordenada
inválida é rejeitada antes que o underflow e o memmove possam ocorrer.
O reprodutor corrigido fornecido pelo mantenedor também foi testado contra a versão vulnerável usando o mesmo arquivo TIFF. Ele calculou a contagem correta de tiles e foi concluído sem um relatório de AddressSanitizer ou SIGSEGV.
Portanto, esta análise não estabelece que o arquivo TIFF sozinho dispara a falha de segurança de memória quando a API é usada corretamente. O reprodutor original contém um cálculo incorreto de contagem de tiles, enquanto a implementação histórica do libtiff carecia de validação defensiva de limites e permitia que a entrada de API inválida resultante se tornasse uma falha de segurança de memória.