Quando assina um PDF, pensa normalmente na chave de assinatura como algo que controla. Ela reside num arquivo .pfx gerado por si, protegido por uma palavra-passe que escolheu. O código que lê esse arquivo assemelha-se a canalização interna, e não a uma fronteira de segurança. Essa intuição falha assim que o certificado deixa de ser seu. Uma ferramenta de desktop que permita ao usuário escolher qualquer .pfx, um servidor que aceite credenciais enviadas por upload ou um assinador em lote que receba certificados através da rede: todos entregam bytes influenciados por atacantes a um analisador (parser) antes que um único byte de assinatura seja produzido. Um leitor PKCS#12 é uma superfície de ataque, no mesmo sentido em que um descodificador de imagem ou um carregador de tipos de letra o são
Este artigo percorre duas armadilhas contra as quais todo leitor de PKCS#12 precisa se proteger, ambas no caminho que importa uma credencial de assinatura, e mostra como o HotPDF as fecha. Nenhuma delas é exótica. As duas vêm da mesma causa raiz que afeta quase todo parser binário escrito em uma linguagem com inteiros de largura fixa: um comprimento ou uma contagem vinda do arquivo é confiada um passo além do que deveria. Uma leva a uma leitura fora de limites (out-of-bounds read), a outra a um processo que trava até você matá-lo
Por onde viajam os bytes
A importação de um .pfx para assinar um documento não é uma operação única; trata-se de um pequeno pipeline, e cada etapa analisa algo que um atacante pode ter escrito. O contentor é uma estrutura PKCS#12 conforme definida na norma RFC 7292, um conjunto de blocos AuthenticatedSafe que envolvem um invólucro encriptado contendo a chave privada. Lê-lo implica percorrer a estrutura ASN.1, derivar uma chave a partir da palavra-passe, desencriptar e, em seguida, entregar a chave RSA recuperada ao código que constrói a assinatura
No HotPDF, estas etapas estão mapeadas em unidades distintas. A lógica do contentor PKCS#12 reside em HPDFPFX. Cada etiqueta (tag), comprimento e valor em que toca é descodificado pelo leitor ASN.1 em HPDFASN1. A derivação de chaves e a desencriptação PBES2 situam-se em HPDFCrypt, juntamente com PBKDF2HMACSHA256. Assim que a chave é recuperada, a HPDFRSA e o construtor CMS SignedData em HPDFCMS convertem-na na assinatura destacada incorporada no PDF. O ponto de entrada público que gere toda a cadeia é uma única chamada
// Conduz o pipeline completo: carrega o PDF placeholder, analisa o PFX,
// deriva a chave, monta o CMS SignedData, escreve a saída assinada.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// assinatura incorporada
else
// a assinatura não foi concluída
;
Cada byte de signer.pfx passa por HPDFASN1 e HPDFPFX antes de ocorrer qualquer operação criptográfica. Se estas duas unidades não forem cautelosas com o que o arquivo declara, a criptografia a jusante nunca terá oportunidade de fazer a diferença
Armadilha um: um comprimento ASN.1 que contorna a guarda
A estrutura ASN.1 em DER e BER codifica cada elemento como uma etiqueta (tag), um comprimento e essa quantidade de bytes de conteúdo. O comprimento é o campo no qual deve confiar mas verificar, pois indica ao analisador o quanto deve ler, tendo sido escrito por quem produziu o arquivo. A norma X.690 §8.1.3 define duas codificações. A forma curta agrupa um comprimento de 0 a 127 num único byte. A forma longa, utilizada para tudo o que for maior, consome um byte inicial cujos sete bits inferiores indicam a contagem de bytes de comprimento que se seguem, e depois essa quantidade de bytes em big-endian transporta o valor real. Quatro bytes de comprimento podem, portanto, declarar um tamanho de conteúdo próximo de quatro gigabytes
Depois de descodificar um valor deste tipo, o analisador tem de verificar se o conteúdo cabe realmente no buffer antes de confiar nele. A verificação natural consiste em confirmar que a posição atual mais o comprimento do conteúdo não ultrapassam o fim dos dados. Escrita de forma intuitiva, com a posição, o comprimento do conteúdo e o total armazenados em inteiros com sinal de 32 bits, essa validação falha:
// A armadilha: aritmética de 32 bits com sinal. Com ContentLen perto de MaxInt,
// Pos + ContentLen transborda para um valor NEGATIVO, então a comparação
// é falsa e um comprimento forjado de ~2 GB passa direto.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
O problema está na adição e não na comparação. Quando o ContentLen está próximo de MaxInt (2147483647), Pos + ContentLen excede o limite do tipo de dados de 32 bits com sinal e converte-se num número negativo. Uma soma negativa nunca será superior a Total, pelo que a validação indica que está tudo bem e permite ao analisador avançar com um comprimento de conteúdo de aproximadamente dois gigabytes que o buffer não contém. O dano ocorre de seguida: o leitor aloca um buffer para esse comprimento alegado e copia para ele, efetuando um SetLength seguido de um Move que lê a partir da origem. Sendo que a origem tem apenas algumas centenas de bytes disponíveis, a cópia lê muito além do fim da entrada, uma leitura fora de limites (out-of-bounds read) que, no melhor dos cenários, provoca um crash e, no pior, expõe memória adjacente do processo à análise
A única verificação correta amplia a soma intermediária antes da comparação, para que a adição não possa transbordar o tipo em que é calculada. O HotPDF promove ambos os operandos a Int64:
// Correto: ambos os operandos ampliados para Int64 antes da soma, de modo que
// a soma não pode transbordar. Um comprimento forjado de 2 GB agora falha na verificação de limites.
if ContentLen < 0 then
raise EHPDFASN1Error.Create('negative content length after decoding.');
if Int64(Pos) + Int64(ContentLen) > Int64(Total) then
raise EHPDFASN1Error.Create('content overruns buffer');
Um Int64 guarda a soma de dois valores de 32 bits sem perda, então a comparação vê o número real e rejeita o comprimento forjado. A verificação separada de não negatividade em ContentLen fecha o caso equivalente em que um valor decodificado chega negativo por si só. No HotPDF essa guarda vive em HPDFASN1ParseNode, a função que produz o nó sobre o qual todos os outros auxiliares se apoiam. Como o HPDFASN1Content dimensiona seu SetLength e seu Move direto do comprimento de conteúdo do nó, um nó que passasse por uma guarda fraca envenenaria toda leitura feita a partir dele. Impor o limite no ponto de decodificação é o que torna seguros os auxiliares acima dele
Armadilha dois: uma contagem de iterações PBKDF2 usada como arma
A segunda armadilha não é um erro de memória, é o arquivo dizendo ao seu CPU o quanto trabalhar. O PKCS#12 protege o material de chave com PBES2, o esquema baseado em senha do PKCS#5, especificado no RFC 8018. O PBES2 roda uma função de derivação de chave, aqui PBKDF2 com HMAC-SHA-256, e depois uma cifra, aqui AES-256-CBC. A PBKDF2 recebe uma contagem de iterações, e essa contagem é um parâmetro carregado no arquivo. O propósito dela é ser lenta: mais iterações significam que cada tentativa de adivinhar a senha custa mais, o que é bom contra um atacante offline. A seção §4.2 do RFC 8018 diz explicitamente que uma contagem maior é melhor para a segurança e deliberadamente não define teto
Essa abertura é ótima quando o arquivo foi você quem gerou. Vira uma arma quando foi o atacante. A contagem de iterações é um fator de trabalho controlado pelo atacante, e um fator de trabalho controlado pelo atacante é uma negação de serviço por complexidade algorítmica. Um .pfx forjado pode codificar uma contagem de iterações na casa dos bilhões; um parser que a lê diligentemente e chama a PBKDF2 por tantas rodadas de HMAC-SHA-256 faz o processo desaparecer em um loop que não retorna por minutos ou horas com um único arquivo. Em um servidor de assinatura que processa uma credencial por requisição, um único upload manipulado travaria um worker
A contagem torna o wraparound pior antes de fazer o CPU girar. O valor da iteração vive no arquivo como um INTEGER ASN.1, que não tem largura fixa, enquanto o campo que a PBKDF2 consome no final é um Integer de 32 bits. Decodifique o INTEGER direto para esse campo e um valor grande truncará, e um valor desenhado para cair no bit de sinal volta negativo ou como algum número pequeno sem relação, de modo que nem o tamanho do trabalho é mais o que o arquivo parecia pedir. O HotPDF lê o valor em largura total e o limita antes de estreitar:
// Leia a contagem de iterações como Int64 primeiro, depois limite a uma faixa sensata
// ANTES que ela seja estreitada para o campo Iterations de 32 bits que o PBKDF2 usa.
LIter := HPDFASN1ToInteger(Data, Node); // retorna Int64
if (LIter < 1) or (LIter > 100000000) then
raise EHPDFPFXError.CreateFmt(
'PBKDF2 iteration count %d is outside the accepted range 1..100000000',
[LIter]);
Iterations := Integer(LIter); // seguro: já está limitado
Ler para um Int64 significa que o valor descodificado é o valor real, e não um fantasma truncado dele. O limite inferior rejeita contagens nulas e negativas, que não fazem sentido numa derivação de chave. O limite superior, cem milhões, fica bem acima de qualquer arquivo PKCS#12 legítimo, que hoje usa dezenas a poucas centenas de milhares de iterações, e ao mesmo tempo limita o pior caso a uma quantidade de trabalho delimitada e suportável. Só depois de o valor passar por essa faixa é que ele é reduzido ao campo de 32 bits, de modo que o truncamento já não surpreende ninguém. No HotPDF este limite vive em ParsePBES2Params, onde os parâmetros PBKDF2 são descodificados a caminho de PBKDF2HMACSHA256
Por que as duas guardas são a mesma guarda
As duas armadilhas parecem diferentes, uma um transbordo de buffer e outra um processo travado, mas são o mesmo erro. Em ambos os casos, um número de um arquivo não confiável é levado para um tipo de largura fixa um passo antes do devido, antes de ser conferido contra a realidade. O comprimento é somado em 32 bits antes do teste de limites; a contagem de iterações é estreitada para 32 bits antes do teste de faixa. Ambos cedem à mesma disciplina: decodificar em largura total, conferir contra o limite real e só então estreitar. O Int64 intermediário não é uma escolha de estilo, é a única largura em que a guarda consegue ver o valor que o atacante realmente escreveu. Um limite que transborda não é um limite, e uma contagem sem teto não é um parâmetro, é um throttle remoto no seu próprio CPU
Orientações práticas para um pipeline de assinatura
A lição imediata é validar entradas de certificados não confiáveis da mesma forma que validaria qualquer upload inseguro. Limite o tamanho de arquivos .pfx que aceita, uma vez que um arquivo legítimo tem kilobytes e não megabytes. Trate uma falha de análise como uma rejeição rotineira de dados, e não como um erro a apresentar com stack trace ao usuário. Se assina num servidor, corra a importação num contexto em que um processo bloqueado não deite abaixo o serviço, e defina um timeout para a operação, para que um arquivo inesperadamente exigente seja limitado pelo tempo de relógio, para além do teto de iterações
A lição mais ampla vai além dos certificados. O reforço da segurança de analisadores não é uma auditoria única a uma unidade; é uma propriedade de cada ponto em que a sua biblioteca lê bytes que não escreveu. Uma biblioteca de PDF analisa muitos dados de origens não confiáveis: tipos de letra incorporados num documento, imagens em meia dúzia de codecs, filtros de fluxos e, no caminho de assinatura, certificados. Cada um deles é uma superfície de ataque e todos merecem a mesma desconfiança em relação a comprimentos e contagens. O HotPDF constrói o caminho de importação e de assinatura sobre as unidades seguras HPDFASN1, HPDFPFX, HPDFCrypt e HPDFCMS aqui descritas, assegurando que a credencial que lhe fornece, independentemente da sua origem, seja analisada defensivamente antes de se confiar nela
O fluxo de trabalho de assinatura protegido por estas validações é abordado de ponta a ponta no nosso guia detalhado de assinaturas digitais PAdES em Delphi, e a mesma postura defensiva aplicada à encriptação de documentos, incluindo o caminho de chaves AES-256 que partilha esta base de código, está descrita no artigo sobre encriptação e segurança AES-256. Tudo isto é disponibilizado como parte do HotPDF Delphi Component para Delphi e C++Builder, juntamente com as APIs de carregamento, edição, encriptação e assinatura documentadas noutras secções deste blogue