Um PDF não é um mero documento que se abre. Trata-se de um pequeno programa que corre. Cada tipo de letra incorporado é um intérprete baseado em stack à espera de charstrings, cada imagem é um descodificador alimentado com campos de largura, altura e profundidade de bits escolhidos pelo arquivo, e cada fluxo (stream) chega envolto em filtros cujos parâmetros foram definidos no arquivo. Nenhum desses números é seu. Provêm de quem produziu o arquivo, o que num cenário real pode ser a fatura de um cliente ou um anexo de um remetente desconhecido. Os descodificadores que convertem esses bytes em píxeis e glifos constituem a superfície de ataque, e um analisador que confie nos dados aí contidos está à distância de um arquivo malformado para sofrer um crash ou pior
O PDF Library for Delphi trata todo o caminho de decodificação como hostil, através dos programas de fonte (TrueType, Type1, CFF e as tabelas CMap), dos decodificadores de imagem (PNG, GIF, TIFF, JBIG2 e CCITT Group 3 e Group 4) e dos filtros de stream (LZW, ASCII85 e os preditores Flate). O que segue são cinco armadilhas de memory safety que esse caminho precisa fechar, cada uma fundamentada no comportamento específico do Delphi que a torna possível, e as mesmas formas reaparecem em qualquer código Pascal que analise entrada não confiável
Um estouro de inteiros que resulta num buffer subdimensionado
O bug clássico de segurança de memória num descodificador de imagem é a multiplicação das dimensões cujo resultado dá a volta (wrap). Um descodificador lê a largura, a altura, a contagem de componentes e a profundidade de bits, multiplica-os para dimensionar o seu resultado, aloca essa quantidade de bytes e depois escreve a imagem com as suas dimensões reais. Se a multiplicação for efetuada em aritmética de 32 bits, o produto pode dar a volta para um valor pequeno, mesmo que cada fator individual esteja num intervalo aceitável, pelo que a alocação é bem-sucedida, resulta num buffer demasiado pequeno e a descodificação ultrapassa os limites da memória alocada. Trata-se do erro CWE-190, estouro de inteiros (integer overflow), que resulta numa escrita fora de limites no heap (CWE-787) uma etapa à frente
Limitar cada dimensão a 65535 em um caminho de imagem compartilhado não é suficiente quando um decodificador autônomo calcula seus próprios tamanhos. Uma expressão de bytes por linha vezes altura como ByteCount * FHeight, ou uma expressão por pixel como FWidth * Components * BitDepth, é um produto de 32 bits no Delphi quando ambos os operandos são inteiros de 32 bits, independente de quão larga seja a variável à qual você atribui o resultado. Uma largura e uma altura de 60000 são plausíveis para uma digitalização grande, mas o produto delas em bytes estoura o intervalo de 32 bits com sinal e o comprimento sai pequeno. A mesma armadilha está em um stride de preditor como BitsPerComponent * Colors * Columns
A defesa é tornar pelo menos um operando Int64 para que a expressão inteira seja avaliada em 64 bits, depois comparar com MaxInt e recusar o arquivo antes de estreitar o tipo de volta para chamar SetLength
// Rejeitar antes de alocar, não depois de escrever.
// Avaliar o produto em Int64 para que não possa dar a volta em 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // dimensões hostis ou inviáveis; recusa a imagem
SetLength(Buffer, RowBytes * FHeight);
O que torna este problema específico do Delphi, e não um cenário genérico, é a redução silenciosa do tipo (narrowing). Atribuir uma expressão demasiado larga a um destino de 32 bits constitui uma conversão permitida sobre a qual o compilador não alerta por predefinição, e a verificação de limites (range checking) não deteta uma inversão que ocorra antes de o valor ser usado como índice. Se deixar o produto em 32 bits, a linguagem fornece silenciosamente um comprimento incorreto em relação à memória que a descodificação vai aceder
Um tipo de campo que impede o disparo de uma validação de segurança
Um arquivo TIFF é uma cadeia de diretórios de arquivos de imagem (IFDs), cada um contendo o deslocamento (offset) de bytes do seguinte. Um arquivo malicioso pode apontar essa cadeia de volta para si mesma, e um leitor que a percorra sem uma condição de paragem ficará em execução contínua. Trata-se do erro CWE-835, um ciclo infinito induzido por dados controlados pelo atacante, sendo a defesa um contador que para assim que ultrapassa um limite que nenhum arquivo legítimo alcançaria
Agora declare o contador de páginas como Word, que no Delphi guarda valores de 0 a 65535, e dê ao loop uma condição de parada do tipo "parar quando a contagem de páginas exceder 65535". Parece correto até você notar que o operando e o limite compartilham o mesmo teto. Um Word nunca pode ser maior que 65535, então a comparação é estruturalmente sempre falsa: quando o contador chega a 65535, o próximo incremento o traz de volta a 0, a guarda nunca vê um valor acima do teto, e uma cadeia de IFDs em loop mantém o leitor girando
O remédio é alargar o campo para que a guarda possa expressar um valor que o contador de fato consegue guardar. TPDFTIFF.FPageCount é declarado como Integer, então a comparação FPageCount > 65535 é alcançável e o loop termina. Sempre que uma checagem de limite tem a forma Value > MaxValueOfType(Value) e o operando já é tipado exatamente nesse máximo, a condição é uma constante falsa: alargue o tipo, ou teste igualdade contra o máximo para que ela possa disparar
Verificação de limites desativada num caminho crítico (hot path)
Com a verificação de limites (range checking) ativada, o Delphi insere uma verificação de limites em cada índice de array e string, o que representa a diferença entre um índice fora de limites gerar uma exceção capturável ERangeError e esse mesmo índice ler ou escrever em memória que não pertence à estrutura. Os caminhos críticos (hot paths) por vezes desativam-na com uma diretiva local {$R-}, o que é justificável até ao momento em que os índices deixam de ser confiáveis
O acessor de lista do qual os intérpretes de fonte dependem, TPDFlibStringList.Get, é exatamente um caminho assim. No Windows ele é compilado com range checking desligado e indexa seu armazenamento de suporte diretamente, então um índice fora do intervalo seria um acesso cru à memória em vez de um erro. Isso é aceitável quando o índice é sempre válido, e exige cuidado dentro de um intérprete de charstrings CFF ou Type2, onde o índice pode vir do arquivo. Uma charstring que desempilha um operando de uma stack vazia produz um índice de menos um; um identificador de glifo deslocado em um em relação à contagem de glifos indexa uma posição além do fim. Sem checagem, ambos seriam um acesso genuíno fora dos limites em vez de uma exceção capturável, e como as posições guardam valores AnsiString com contagem de referências, uma leitura perdida também pode corromper a contagem de referências de uma string
O PDF Library for Delphi mantém o range checking desligado nesse hot path e torna os índices comprovadamente válidos: antes de tomar o topo da stack de operandos, o intérprete verifica que a stack não está vazia, e toda guarda de índice é um menor-que estrito contra a contagem em vez de um menor-ou-igual que admite o off-by-one. A diretiva transfere a responsabilidade pelos limites do compilador para você, e a validação que ela removeu precisa ser recolocada à mão em todo ponto de entrada
Recursividade sem limites num intérprete de charstrings
Uma charstring Type2 pode chamar uma sub-rotina, e uma sub-rotina é ela própria uma charstring que pode chamar outra, pelo que os operadores de chamada de sub-rotinas locais e globais permitem ao arquivo ditar a profundidade do processo. Uma sub-rotina que se chame a si mesma, diretamente ou através de um ciclo, entra em recursividade contínua até esgotar a stack nativa e encerrar o processo. Trata-se da falha CWE-674, recursividade descontrolada (uncontrolled recursion)
O intérprete Type1 se protege disso com um contador de profundidade de chamadas e um teto, PLType1MaxCallDepth, e se recusa a descer além dele, o que reflete o limite de profundidade que a própria especificação Type1 nomeia. A armadilha é que um intérprete estruturalmente semelhante escrito depois, como o Type2, não herda essa guarda por conta própria, e uma fonte construída à mão com uma sub-rotina que chama o próprio número atravessa direto uma checagem ausente até um stack overflow
// A forma da guarda de profundidade que ambos os intérpretes de charstring precisam.
// Acompanha a profundidade entre chamadas aninhadas e se recusa a recursar além dela.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // sub-rotina hostil autorreferente; para de descer
// ... interpreta a sub-rotina, depois Dec(CallDepth) na saída
O PDF Library for Delphi dá ao caminho Type2 a mesma profundidade limitada do seu irmão Type1. Qualquer descida recursiva sobre estrutura controlada pelo atacante, sejam sub-rotinas de fonte, um array aninhado ou uma cadeia de cross-reference, precisa de um teto de profundidade que a entrada não consiga levantar
Fuga de memória não inicializada para o resultado
A armadilha mais sutil vaza conteúdo do heap para a saída descriptografada, e a causa é uma propriedade do SetLength fácil de esquecer. Quando você cresce uma AnsiString com SetLength, o Delphi aloca os bytes mas não os zera, então a região nova guarda o que estava antes naquela memória do heap. Se todo byte é escrito depois, isso nunca importa; se um caminho deixa parte do buffer sem escrever e depois o retorna como dados, esses bytes obsoletos saem junto com o resultado. Isso é CWE-457, uso de memória não inicializada, e quando o resultado cruza uma fronteira de confiança vira um vazamento de informação
Um caminho de descriptografia AES-CBC é exatamente onde isso morde. Dimensione o buffer de saída com SetLength, processe o ciphertext um bloco de 16 bytes por vez, e quando o comprimento do ciphertext não é múltiplo de 16, um comprimento que o atacante pode escolher, o bloco parcial final nunca é escrito, então esses bytes finais guardam o conteúdo do heap que o SetLength deixou e o buffer volta como o plaintext descriptografado de um objeto de documento. O remédio são duas guardas, e nenhuma delas sozinha é suficiente: o ponto de entrada de descriptografia do PDF Library for Delphi rejeita qualquer ciphertext cujo comprimento não é múltiplo do tamanho de bloco, e como rede de segurança a saída é limpa com FillChar antes do uso, para que qualquer caminho que falhe em escrever uma região retorne zeros em vez de resíduo de heap
O que as cinco armadilhas têm em comum
As cinco armadilhas são bugs diferentes, mas rimam. Uma largura de inteiro que faz um produto dar a volta, um tipo de campo que prende uma guarda a uma constante falsa, um range check desligado onde os índices deixaram de ser seguros, uma recursão sem piso, e um buffer que a linguagem se recusou a zerar. Em cada uma o Delphi faz exatamente o que define, porque a linguagem te dá aritmética que dá a volta, estreitamento silencioso, range checks que você pode desligar, recursão sem limite embutido, e alocação que não inicializa. Esse é o contrato, e um parser Pascal o cumpre assumindo quatro coisas à mão em cada fronteira que o arquivo controla: largura de inteiros, range checking, profundidade de recursão e inicialização de buffer
O PDF Library for Delphi, o motor para Delphi e C++Builder, protege seu caminho de decodificação contra as cinco. Se o seu trabalho também alcança como um arquivo declara estar protegido, as notas complementares sobre auditoria de criptografia e permissões e sobre preflight de PDF/A e PDF/UA cobrem o lado de análise do mesmo parser, e tudo isso vem dentro da PDF Library for Delphi Delphi PDF Library junto com as APIs de carregamento, renderização e assinatura cobertas em outros textos deste blog