Maior do que diz: encontrando uma bomba de descompressão no cabeçalho de um PNG
O desafio
Toda noite às 02h00 o gerador de miniaturas cai em um único arquivo e derruba o pool de workers inteiro. O arquivo tem 46 KB em disco, é um PNG válido, abre no navegador, e o log de upload o registra como um ícone de 64 por 64. Nada no tamanho em disco explica uma máquina ficando sem memória. Um PNG carrega as dimensões em pixels no cabeçalho, à vista de todos, muito antes de um decodificador tocar nos dados comprimidos. Leia as dimensões no dump e informe a largura real. Os oito primeiros bytes estão destacados, passar o cursor em qualquer byte mostra o offset e o caractere ASCII, e a referência de assinaturas abaixo do dump nomeia cada chunk.
O que você vai aprender
- Localizar um chunk dentro de um PNG pelo seu tipo ASCII de quatro letras
- Ler um inteiro big-endian de 4 bytes em um hex dump
- Explicar por que o tamanho do arquivo em disco não diz nada sobre o tamanho decodificado
- Reconhecer uma bomba de descompressão como um ataque de recursos, não um arquivo malformado
- Separar metadados escritos por uma ferramenta da estrutura sobre a qual um decodificador atua
Habilidades testadas
Pré-requisitos
- Conversão de hexadecimal para decimal
- O que é um cabeçalho de arquivo
Como funciona
Um PNG é uma assinatura seguida por uma sequência de chunks, e todo chunk tem o mesmo formato: um comprimento de 4 bytes, um tipo ASCII de 4 bytes, os dados, e depois um checksum de 4 bytes. Essa regularidade é o que permite navegar por um dump sem um parser. Ache as quatro letras que nomeiam um chunk e você sabe exatamente onde os dados dele começam.
O primeiro chunk é sempre IHDR, e seus primeiros oito bytes são a largura e a altura, cada uma um inteiro big-endian de 32 bits. Big-endian significa que o byte mais significativo é escrito primeiro, então 00 00 75 30 é 0x7530 e não 0x3075. Todo decodificador lê esses oito bytes antes de qualquer outra coisa, porque precisa alocar o buffer de saída antes de poder descomprimir os dados nele.
Essa alocação é o ataque. Tamanho comprimido e tamanho decodificado não têm relação: uma única cor chapada em uma tela enorme comprime para poucos kilobytes e se expande para gigabytes. Qualquer serviço que aceita uma imagem, verifica que o upload é pequeno, e depois a entrega a uma biblioteca já perdeu, porque a biblioteca dimensiona seu buffer a partir do cabeçalho, e o cabeçalho é controlado pelo atacante.
Erros comuns
- Responder 64. O
64x64visível na coluna ASCII vive em um chunk de comentáriotEXt. É uma anotação, e o log de upload a copiou sem olhar o cabeçalho. - Responder 20000. Essa é a altura. A largura vem primeiro no
IHDR. - Ler os bytes em little-endian. Inteiros de PNG são big-endian, então
00 00 75 30é 30000, não 807534592. - Começar a contar no offset 8. Os offsets 8 a 11 são o comprimento do chunk e 12 a 15 são o tipo. A largura começa no offset 16.
- Ler o chunk pHYs. Ele guarda pixels por metro, que é resolução, não tamanho.
Como se proteger
Decida se deve decodificar a partir do cabeçalho, antes de decodificar.
- Faça o parse do
IHDRvocê mesmo e rejeite qualquer coisa acima de um orçamento de pixels. O total de pixels importa mais do que qualquer dimensão isolada: 30000 por 20000 são 600 megapixels, e um limite de 50 megapixels teria barrado isso na porta. - Configure os limites que sua biblioteca de imagens já oferece. A maioria delas expõe uma contagem máxima de pixels e recusa arquivos grandes demais se você ativar isso.
- Faça o trabalho em um processo com um limite rígido de memória, para que um arquivo ruim mate um worker em vez do pool inteiro, e nunca na thread da requisição.
- Trate todo chunk de texto como decoração não confiável. As dimensões registradas no momento do upload devem vir do seu próprio parse, não de um comentário no arquivo.