Quando você assina um PDF, geralmente pensa na chave de assinatura como algo que você controla. Ela reside em um arquivo .pfx que você gerou, protegida por uma senha que você escolheu. O código que lê esse arquivo parece ser apenas um canal, e não uma fronteira de segurança. Essa intuição está errada no momento em que o certificado deixa de ser seu. Uma ferramenta desktop que permite ao usuário escolher qualquer .pfx, um servidor que aceita uma credencial enviada por upload ou um assinador em lote que recebe certificados pela rede, todos entregam bytes influenciados por invasores a um analisador antes que um único byte de assinatura seja gerado. Um leitor PKCS#12 é uma superfície de ataque, no mesmo sentido que um decodificador de imagem ou um carregador de fonte.
Este artigo examina dois defeitos reais que existiam nesse leitor, ambos no caminho que importa uma credencial de assinatura. Nenhum deles é exótico. Ambos derivam da mesma causa raiz que afeta quase todo analisador binário escrito em uma linguagem com inteiros de largura fixa: um comprimento ou uma contagem do arquivo recebe mais confiança do que deveria. Um leva a uma leitura fora dos limites, e o outro a um processo que trava até que seja encerrado.
Por onde os bytes trafegam
Importar um .pfx para assinar um documento não é uma única operação, é um pipeline curto, e cada estágio analisa algo que um invasor pode ter gravado. O contêiner é uma estrutura PKCS#12 como definido na RFC 7292, um conjunto de pacotes AuthenticatedSafe envoltos em um envelope criptografado que contém a chave privada. Lê-lo significa percorrer ASN.1, derivar uma chave a partir da senha, descriptografar e, em seguida, entregar a chave RSA recuperada ao código que gera a assinatura.
No HotPDF, esses estágios são mapeados para unidades distintas. A lógica do contêiner PKCS#12 reside em HPDFPFX. Cada tag, comprimento e valor que ela toca é decodificado pelo leitor ASN.1 em HPDFASN1. A derivação de chaves e a descriptografia PBES2 ficam em HPDFCrypt junto com o PBKDF2HMACSHA256. Quando a chave é recuperada, o HPDFRSA e o gerador CMS SignedData em HPDFCMS transformam na assinatura destacada incorporada no PDF. O ponto de entrada público que direciona toda a cadeia é uma única chamada.
// Drives the full pipeline: load the placeholder PDF, parse the PFX,
// derive the key, build CMS SignedData, write the signed output.
if THotPDF.SignPDFWithPFX('Prepared.pdf', 'Signed.pdf',
'signer.pfx', 'p@ssw0rd') then
// signature embedded
else
// signing did not complete
;
Cada byte do signer.pfx passa por HPDFASN1 e HPDFPFX antes que qualquer criptografia ocorra. Se essas duas unidades não forem cuidadosas em relação ao que o arquivo alega, a criptografia subsequente nunca terá a oportunidade de fazer diferença.
Defeito um: um comprimento ASN.1 que ultrapassa a proteção
O ASN.1 em DER e BER codifica cada elemento como uma tag, um comprimento e a respectiva quantidade de bytes de conteúdo. O comprimento é o campo que você deve confiar, mas verificar, porque ele informa ao analisador até onde ler, e foi gravado por quem produziu o arquivo. A norma X.690 §8.1.3 define duas codificações. A forma curta compacta um comprimento de 0 a 127 em um único byte. A forma longa, usada para qualquer tamanho superior, utiliza um byte inicial cujos sete bits inferiores fornecem a contagem de bytes de comprimento que se seguem, e então essa quantidade de bytes em big-endian carrega o valor real. Quatro bytes de comprimento podem, portanto, declarar um tamanho de conteúdo próximo a quatro gigabytes.
Após decodificar esse valor, o analisador deve verificar se o conteúdo realmente cabe no buffer antes de confiar nele. A verificação natural é confirmar se a posição atual mais o comprimento do conteúdo não ultrapassa o fim dos dados. Escrito da maneira óbvia, com a posição, o comprimento do conteúdo e o total armazenados em inteiros com sinal de 32 bits, essa proteção falha:
// The trap: signed 32-bit arithmetic. With ContentLen near MaxInt,
// Pos + ContentLen overflows to a NEGATIVE value, so the comparison
// is false and a forged ~2 GB length sails straight through.
if Pos + ContentLen > Total then
raise EHPDFASN1Error.Create('content overruns buffer');
O problema é a adição, não a comparação. Quando o ContentLen está próximo de MaxInt (2147483647), Pos + ContentLen ultrapassa o limite do intervalo de 32 bits com sinal e resulta em um número negativo. Uma soma negativa nunca é maior que Total, portanto a proteção relata que tudo está bem e permite que o analisador prossiga com um comprimento de conteúdo de aproximadamente dois gigabytes que o buffer não possui. O que acontece a seguir é o dano: o leitor aloca um buffer para esse comprimento alegado e realiza a cópia, um SetLength seguido por um Move lendo da origem. A origem possui apenas algumas centenas de bytes restantes, de modo que a cópia lê muito além do final da entrada, uma leitura fora dos limites que na melhor das hipóteses causa um travamento e na pior vaza memória de processos adjacentes para a análise.
A única proteção correta expande a soma intermediária antes da comparação, para que a adição não estoure o tipo no qual é calculada. A correção promove ambos os operandos para Int64:
// Correct: both operands widened to Int64 before the add, so the sum
// cannot wrap. A forged 2 GB length now fails the bounds check.
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 armazena a soma de dois valores de 32 bits sem perda, de modo que a comparação visualiza o número real e rejeita o comprimento forjado. A verificação separada de valor não negativo no ContentLen cobre o caso correspondente em que um valor decodificado acaba sendo negativo por si só. No HotPDF, essa proteção reside em HPDFASN1ParseNode, a função que gera o nó sobre o qual todos os outros auxiliares são construídos. Como o HPDFASN1Content dimensiona seu SetLength e Move diretamente a partir do comprimento do conteúdo do nó, um nó que passasse por uma proteção defeituosa contaminaria cada leitura realizada a partir dele. Corrigir o limite no ponto de decodificação é o que torna os auxiliares acima dele seguros.
Defeito dois: uma contagem de iterações PBKDF2 usada como arma
A segunda falha não é um erro de memória, é o arquivo informando à sua CPU o quanto ela deve trabalhar. O PKCS#12 protege seu material de chave com PBES2, o esquema baseado em senha do PKCS#5, especificado na RFC 8018. O PBES2 executa uma função de derivação de chaves, neste caso PBKDF2 com HMAC-SHA-256, e depois uma cifra, aqui AES-256-CBC. O PBKDF2 recebe uma contagem de iterações, e essa contagem é um parâmetro contido no arquivo. Seu propósito principal é ser lento: mais iterações significam que cada tentativa de adivinhação de senha custa mais, o que é bom contra um invasor offline. A RFC 8018 §4.2 é explícita ao dizer que uma contagem maior é melhor para a segurança, e deliberadamente não define um limite máximo.
Essa flexibilidade é aceitável quando você gerou o arquivo. É uma arma quando o invasor a gerou. A contagem de iterações é um fator de trabalho controlado pelo invasor, e um fator de trabalho controlado pelo invasor representa 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; o analisador lê obedientemente esse valor e chama o PBKDF2 para essa quantidade de rodadas de HMAC-SHA-256, e o processo desaparece em um loop que não retornará por minutos ou horas com um único arquivo fornecido. Em um servidor de assinatura que processa uma credencial por solicitação, um único envio modificado paralisa um worker.
A contagem torna o estouro ainda pior antes de fazer a CPU girar em falso. O valor da iteração existe no arquivo como um INTEGER do ASN.1, que não possui largura fixa, enquanto o campo que o PBKDF2 consome em última análise é um Integer de 32 bits. Decodifique o INTEGER diretamente nesse campo e um valor grande será truncado, e um valor projetado para atingir o bit de sinal retornará negativo ou como algum número pequeno não relacionado, de modo que até mesmo a magnitude do trabalho não corresponde mais ao que o arquivo parecia solicitar. A correção lê o valor em sua largura total e o limita antes de reduzi-lo:
// Read the iteration count as Int64 first, then clamp to a sane band
// BEFORE it is narrowed into the 32-bit Iterations field PBKDF2 uses.
LIter := HPDFASN1ToInteger(Data, Node); // returns 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); // safe: already bounded
Por que ambas as correções são a mesma correção
Os dois defeitos parecem diferentes, um sendo um estouro de buffer e o outro um processo travado, mas são o mesmo erro. Em cada caso, um número de um arquivo não confiável foi transferido para um tipo de largura fixa um passo cedo demais, antes de ter sido verificado em relação à realidade. O comprimento foi adicionado em 32 bits antes do teste de limites; a contagem de iterações foi reduzida para 32 bits antes do teste de intervalo. Ambos se submetem à mesma disciplina: decodificar na largura total, verificar em relação ao limite real e somente então reduzir. O Int64 intermediário não é uma escolha de estilo, é a única largura na qual a proteção pode visualizar o valor que o invasor realmente gravou. Um limite que transborda não é um limite, e uma contagem sem teto não é um parâmetro, é um controle remoto sobre sua própria CPU.
Orientação prática para um pipeline de assinatura
A lição imediata é validar a entrada de certificados não confiáveis da mesma forma que você validaria qualquer upload não confiável. Limite o tamanho do .pfx que você aceita, já que um legítimo tem kilobytes, não megabytes. Trate uma falha de análise como entrada rotineiramente rejeitada, e não como um erro que justifique um rastreamento de pilha (stack trace) para o usuário. Se você assina em um servidor, execute a importação onde um worker paralisado não possa derrubar o serviço junto com ele, e coloque um tempo limite (timeout) em torno da operação para que um arquivo inesperadamente complexo seja limitado pelo tempo de relógio, bem como pelo limite de iterações.
A rota mais ampla vai além dos certificados. O fortalecimento do analisador não é uma auditoria única de uma unidade, é uma propriedade de cada local onde sua biblioteca lê bytes que ela não gravou. Uma biblioteca de PDF analisa muita coisa de fontes não confiáveis: fontes incorporadas em um documento, imagens em meia dúzia de codecs, filtros de fluxo e, no caminho de assinatura, certificados. Cada um deles é uma superfície de ataque, e cada um merece a mesma suspeita sobre cada comprimento e cada contagem. O HotPDF constrói o caminho de importação e assinatura nas unidades fortalecidas HPDFASN1, HPDFPFX, HPDFCrypt e HPDFCMS descritas aqui, de modo que a credencial fornecida, não importa de onde venha, seja analisada defensivamente antes de receber qualquer confiança.
O fluxo de trabalho de assinatura que essas verificações protegem é coberto de ponta a ponta em nosso tutorial de assinaturas digitais PAdES no Delphi, e a mesma postura defensiva aplicada à criptografia de documentos, incluindo o caminho da chave AES-256 que compartilha essa base de código, é descrita no artigo sobre criptografia e segurança AES-256. Tudo isso é fornecido como parte do Componente HotPDF para Delphi e C++Builder, junto com as APIs de carregamento, edição, criptografia e assinatura descritas em outras seções deste blog.