Cuando firma un PDF, por lo general piensa en la clave de firma como algo que controla. Reside en un archivo .pfx que usted generó, protegido por una contraseña que eligió. El código que lee ese archivo parece una tubería de paso, no un límite. Esa intuición es incorrecta en el momento en que el certificado deja de ser suyo. Una herramienta de escritorio que permite al usuario elegir cualquier .pfx, un servidor que acepta una credencial cargada o un firmador por lotes que recibe certificados a través de la red, todos entregan bytes influenciados por un atacante a un analizador antes de que se produzca un solo byte de firma. Un lector de PKCS#12 es una superficie de ataque, en el mismo sentido que lo es un decodificador de imágenes o un cargador de fuentes.
Este artículo analiza dos defectos reales que existían en ese lector, ambos en la ruta que importa una credencial de firma. Ninguno es exótico. Ambos provienen de la misma causa raíz que afecta a casi cualquier analizador binario escrito en un lenguaje con enteros de ancho fijo: se confía en una longitud o en un recuento del archivo un paso más allá de lo que se debería. Uno conduce a una lectura fuera de los límites y el otro a un proceso que se bloquea hasta que lo finaliza.
Hacia dónde viajan los bytes
Importar un .pfx para firmar un documento no es una sola operación, es una canalización corta, y cada etapa analiza algo que un atacante puede haber escrito. El contenedor es una estructura PKCS#12 como se define en RFC 7292, un conjunto de contenedores AuthenticatedSafe envueltos alrededor de una cubierta cifrada que guarda la clave privada. Leerlo significa recorrer el formato ASN.1, derivar una clave a partir de la contraseña, descifrar y luego entregar la clave RSA recuperada al código que compila la firma.
En HotPDF, esas etapas se asignan a unidades distintas. La lógica del contenedor PKCS#12 reside en HPDFPFX. Cada etiqueta, longitud y valor que toca es decodificado por el lector de ASN.1 en HPDFASN1. La derivación de claves y el descifrado PBES2 se encuentran en HPDFCrypt junto con PBKDF2HMACSHA256. Cuando se recupera la clave, HPDFRSA y el compilador de CMS SignedData en HPDFCMS la convierten en la firma separada incrustada en el PDF. El punto de entrada público que dirige toda la cadena es una sola llamada.
// 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 de signer.pfx fluye a través de HPDFASN1 and HPDFPFX antes de que ocurra cualquier proceso criptográfico. Si estas dos unidades no son cuidadosas con lo que declara el archivo, la criptografía posterior nunca tendrá la oportunidad de actuar.
Defecto uno: una longitud ASN.1 que se desborda más allá de la protección
ASN.1 en DER y BER codifica cada elemento como una etiqueta, una longitud y esa cantidad de bytes de contenido. La longitud es el campo en el que se debe confiar pero verificar, porque le indica al analizador qué tan lejos debe leer y fue escrito por quien produjo el archivo. La norma X.690 §8.1.3 define dos codificaciones. La forma corta empaqueta una longitud de 0 a 127 en un solo byte. La forma larga, utilizada para cualquier tamaño superior, emplea un byte inicial cuyos siete bits inferiores indican el recuento de bytes de longitud que siguen, y luego esa cantidad de bytes en formato big-endian transporta el valor real. Por lo tanto, cuatro bytes de longitud pueden declarar un tamaño de contenido cercano a los cuatro gigabytes.
Después de decodificar dicho valor, el analizador debe comprobar que el contenido realmente quepa dentro del búfer antes de confiar en él. La comprobación natural consiste en confirmar que la posición actual más la longitud del contenido no sobrepasen el final de los datos. Si se escribe de la forma obvia, con la posición, la longitud del contenido y el total almacenados en enteros con signo de 32 bits, esa protección falla:
// 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');
El problema es la suma, no la comparación. Cuando ContentLen está cerca de MaxInt (2147483647), Pos + ContentLen desborda el rango firmado de 32 bits y se convierte en un número negativo. Una suma negativa nunca es mayor que Total, por lo que la protección reporta que todo está bien y permite que el analizador proceda con una longitud de contenido de aproximadamente dos gigabytes que el búfer no contiene. Lo que sucede a continuación es el daño: el lector asigna un búfer para esa longitud declarada y realiza una copia en él, un SetLength seguido de un Move que lee desde la fuente. La fuente solo tiene unos pocos cientos de bytes restantes, por lo que la copia lee mucho más allá del final de la entrada, una lectura fuera de los límites que, en el mejor de los casos, bloquea el sistema y, en el peor, filtra memoria del proceso adyacente en el análisis.
La única protección correcta amplía la suma intermedia antes de la comparación, de modo que la suma no pueda desbordar el tipo en el que se calcula. La solución promueve ambos operandos a 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');
Un Int64 contiene la suma de dos valores de 32 bits sin pérdida de datos, por lo que la comparación detecta el número real y rechaza la longitud falsificada. La comprobación independiente de valor no negativo en ContentLen cubre el caso coincidente en el que un valor decodificado resulta negativo por sí mismo. En HotPDF, esta protección reside en HPDFASN1ParseNode, la función que produce el nodo sobre el que se basan todos los demás asistentes. Debido a que HPDFASN1Content dimensiona su SetLength y Move directamente a partir de la longitud del contenido del nodo, un nodo que hubiera pasado una protección defectuosa habría envenenado cada lectura realizada a partir de él. Corregir el límite en el punto de decodificación es lo que hace seguros a los asistentes que se encuentran por encima de él.
Defecto dos: un recuento de iteraciones PBKDF2 utilizado como arma
La segunda falla no es un error de memoria, es el archivo indicándole a su CPU qué tan duro trabajar. PKCS#12 protege su material de clave con PBES2, el esquema basado en contraseña de PKCS#5, especificado en RFC 8018. PBES2 ejecuta una función de derivación de clave, en este caso PBKDF2 con HMAC-SHA-256, y luego un cifrado, en este caso AES-256-CBC. PBKDF2 toma un recuento de iteraciones, y ese recuento es un parámetro incluido en el archivo. Su único propósito es ser lento: más iteraciones significa que cada intento de adivinar la contraseña cuesta más, lo cual es bueno contra un atacante sin conexión. La norma RFC 8018 §4.2 es explícita al señalar que un recuento mayor es mejor para la seguridad y, deliberadamente, no establece un límite máximo.
Esa flexibilidad está bien cuando usted generó el archivo. Se convierte en un arma cuando lo hace el atacante. El recuento de iteraciones es un factor de trabajo controlado por el atacante, y un factor de trabajo controlado por el atacante representa una denegación de servicio por complejidad algorítmica. Un archivo .pfx falsificado puede codificar un recuento de iteraciones de miles de millones; el analizador lo lee obedientemente e invoca a PBKDF2 para esa cantidad de rondas de HMAC-SHA-256, y el proceso desaparece en un bucle que no responderá durante minutos u horas con un solo archivo suministrado. En un servidor de firma que maneja una credencial por solicitud, una sola carga diseñada con fines maliciosos bloquea a un subproceso de trabajo.
El recuento empeora el desbordamiento antes de hacer que la CPU gire sin control. El valor de iteración reside en el archivo como un ASN.1 INTEGER, que no tiene un ancho fijo, mientras que el campo que PBKDF2 consume en última instancia es un Integer de 32 bits. Si decodifica el INTEGER directamente en ese campo, un valor grande se truncará y un valor diseñado para aterrizar en el bit de signo regresará como negativo o como un número pequeño no relacionado, por lo que incluso el tamaño del trabajo ya no es lo que el archivo parecía solicitar. La solución lee el valor a su ancho completo y lo limita antes de reducirlo:
// 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 qué ambas soluciones representan la misma solución
Los dos defectos parecen diferentes (uno es un desbordamiento de búfer y el otro un proceso bloqueado), pero representan el mismo error. En cada caso, un número de un archivo no confiable se trasladó a un tipo de ancho fijo un paso demasiado pronto, antes de haber sido verificado contra la realidad. La longitud se sumó en 32 bits antes de la prueba de límites; el recuento de iteraciones se redujo a 32 bits antes de la prueba de rango. Ambos se rinden ante la misma disciplina: decodificar a ancho completo, comprobar contra el límite real y solo entonces reducir. El Int64 intermedio no es una opción de estilo, es el único ancho en el que la protección puede ver el valor que el atacante realmente escribió. Un límite que se desborda no es un límite, y un recuento sin un tope máximo no es un parámetro, sino un acelerador remoto para su propia CPU.
Orientación práctica para una canalización de firma
La lección específica es validar las entradas de certificados no confiables del mismo modo en que validaría cualquier archivo cargado no confiable. Limite el tamaño de los archivos .pfx que acepta, ya que uno legítimo es de kilobytes, no de megabytes. Trate una falla de análisis como un rechazo de entrada de rutina, no como un error que merezca mostrar un rastreo de pila (stack trace) al usuario. Si realiza firmas en un servidor, ejecute la importación donde un proceso bloqueado no pueda derribar el servicio y configure un tiempo de espera (timeout) alrededor de la operación para que un archivo inesperadamente costoso esté limitado por el reloj, así como por el tope de iteración.
La lección más amplia va más allá de los certificados. La protección de analizadores no es una auditoría única de una unidad, es una propiedad de cada lugar donde su biblioteca lee bytes que no escribió. Una biblioteca PDF analiza una gran cantidad de datos de fuentes no confiables: fuentes incrustadas en un documento, imágenes en media docena de códecs, filtros de flujo y, en la ruta de firma, certificados. Cada uno de ellos es una superficie de ataque y cada uno merece la misma sospecha en cada longitud y en cada recuento. HotPDF crea la ruta de importación y firma sobre las unidades protegidas HPDFASN1, HPDFPFX, HPDFCrypt y HPDFCMS descritas aquí, de modo que la credencial que le entregue, sea cual sea su origen, se analice de manera defensiva antes de confiar en ella.
El flujo de trabajo de firma que protegen estas comprobaciones se cubre de extremo a extremo en nuestra guía de firmas digitales PAdES en Delphi, y la misma postura defensiva aplicada al cifrado de documentos, incluida la ruta de clave AES-256 que comparte esta base de código, se describe en el artículo sobre cifrado y seguridad AES-256. Todo ello se incluye como parte de HotPDF Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras partes de este blog.