Technical Article

Fortalecimiento de un firmador de PDF en Delphi contra archivos PKCS#12 maliciosos

Cuando firma un PDF, normalmente piensa en la clave de firma como algo que usted controla. Reside en un archivo .pfx que usted generó, protegido por una contraseña que eligió. El código que lee ese archivo parece fontanería, 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 a un 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 procese un solo byte de firma. Un lector de PKCS#12 es una superficie de ataque, en el mismo sentido en que lo es un decodificador de imágenes o un cargador de fuentes

Este artículo recorre 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 un recuento del archivo un paso más allá de lo debido. Uno conduce a una lectura fuera de límites, el otro a un proceso que se cuelga hasta que se le finaliza

Por dónde viajan los bytes

Importar un .pfx para firmar un documento no es una sola operación, es una tubería corta, y cada etapa analiza algo que un atacante puede haber escrito. El contenedor es una estructura PKCS#12 tal como se define en RFC 7292, un nido de bolsas AuthenticatedSafe envuelto al rededor de una cubierta cifrada que contiene la clave privada. Leerlo significa recorrer ASN.1, derivar una clave a partir de la contraseña, descifrar y luego entregar la clave RSA recuperada al código que crea la firma

En HotPDF, esas etapas se mapean en unidades distintas. La lógica del contenedor PKCS#12 reside en HPDFPFX. Cada etiqueta, longitud y valor que toca es decodificado por el lector 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 generador 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 y HPDFPFX antes de que ocurra cualquier criptografía. Si esas dos unidades no tienen cuidado con lo que el archivo afirma, la criptografía posterior nunca tendrá la oportunidad de importar

Defecto uno: una longitud ASN.1 que desborda el límite de 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 escrita 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 mayor, emplea un byte inicial cuyos siete bits bajos indican el recuento de bytes de longitud que siguen, y luego esa cantidad de bytes big-endian transportan el valor real. Por lo tanto, cuatro bytes de longitud pueden declarar un tamaño de contenido que se aproxima a los cuatro gigabytes

Después de decodificar dicho valor, el analizador tiene que comprobar que el contenido realmente cabe dentro del búfer antes de confiar en él. La comprobación natural es confirmar que la posición actual más la longitud del contenido no supera el final de los datos. Escrita de la manera obvia, con la posición, la longitud del contenido y el total contenidos en enteros con signo de 32 bits, esa protección no funciona

// 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 con signo 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 copia en él, un SetLength seguido de un Move que lee del origen. Al origen solo le quedan unos pocos cientos de bytes, por lo que la copia lee mucho más allá del final de la entrada, una lectura fuera de límites que, en el mejor de los casos, provoca una caída 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, por lo que la comparación ve el número real y rechaza la longitud falsificada. La comprobación de valor no negativo independiente en ContentLen cierra el caso coincidente en el que un valor decodificado resulta negativo por sí solo. 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 pasara una protección incorrecta habría envenenado cada lectura tomada de él. Corregir el límite en el punto de decodificación es lo que hace seguros a los asistentes por encima de él

Defecto dos: un recuento de iteraciones PBKDF2 utilizado como arma

El segundo fallo no es un error de memoria, es el archivo indicándole a su CPU qué tan duro debe 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, aquí PBKDF2 con HMAC-SHA-256, luego un cifrado, aquí AES-256-CBC. PBKDF2 toma un recuento de iteraciones, y ese recuento es un parámetro que viaja en el archivo. Su único propósito es ser lento: más iteraciones significan que cada intento de adivinar la contraseña cuesta más, lo cual es bueno contra un atacante fuera de línea. 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 superior

Esa apertura está bien cuando usted generó el archivo. Es un arma cuando la generó el atacante. El recuento de iteraciones es un factor de trabajo controlado por el atacante, y un factor de trabajo controlado por el atacante es una denegación de servicio por complejidad algorítmica. Un .pfx falsificado puede codificar un recuento de iteraciones de miles de millones; el analizador lo lee obedientemente y llama a PBKDF2 para esa cantidad de rondas de HMAC-SHA-256, y el proceso desaparece en un bucle que no regresará en minutos u horas con un solo archivo suministrado. En un servidor de firma que maneja una credencial por solicitud, una sola carga diseñada detiene a un trabajador

El recuento empeora el desbordamiento antes de hacer que la CPU gire. El valor de iteración reside en el archivo como un entero 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. Decodifique el INTEGER directamente en ese campo y un valor grande se truncará, y un valor diseñado para caer en el bit de signo regresará negativo o como algún número pequeño no relacionado, por lo que incluso el tamaño del trabajo ya no es el que el archivo parecía pedir. La solución lee el valor a 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

Leer en un Int64 significa que el valor decodificado es el real, no un fantasma truncado de él. El límite inferior rechaza recuentos de cero y negativos, que no tienen sentido para una derivación de clave. El límite superior, cien millones, se encuentra muy por encima de cualquier archivo PKCS#12 legítimo, que hoy utiliza desde decenas hasta unos pocos cientos de miles de iteraciones, al tiempo que limita el peor de los casos a una cantidad de trabajo acotada y sobrevivible. Solo después de que el valor ha pasado ese rango se reduce al campo de 32 bits, por lo que el truncamiento ya no puede sorprender a nadie. En HotPDF, esta limitación reside en ParsePBES2Params, donde se decodifican los parámetros PBKDF2 en camino a PBKDF2HMACSHA256

Por qué ambas soluciones son la misma solución

Los dos defectos parecen diferentes, uno es un desbordamiento de búfer y el otro un proceso colgado, pero son el mismo error. En cada caso, un número de un archivo no confiable se llevó a un tipo de ancho fijo un paso antes de tiempo, 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, verificar 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 límite superior no es un parámetro, es un regulador remoto de su propia CPU

Orientación práctica para un canal de firma

La lección particular es validar la entrada de certificados no confiables de la misma manera que validaría cualquier carga no confiable. Limite el tamaño del .pfx que acepta, ya que uno legítimo es de kilobytes, no de megabytes. Trate un fallo de análisis como una entrada rechazada de rutina, no como un error que merezca un rastreo de pila para el usuario. Si firma en un servidor, ejecute la importación donde un trabajador estancado no pueda derribar el servicio con él, y establezca un tiempo de espera para la operación de modo que un archivo inesperadamente costoso esté limitado por el reloj de pared, así como por el límite de iteración

La lección más amplia va más allá de los certificados. El endurecimiento del analizador 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 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 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 reforzadas HPDFASN1, HPDFPFX, HPDFCrypt y HPDFCMS descritas aquí, para que la credencial que le entregue, de dondequiera que provenga, sea analizada defensivamente antes de que se confíe en ella

El flujo de trabajo de firma que protegen estas comprobaciones se cubre de principio a fin en nuestra guía práctica sobre firmas digitales PAdES en Delphi, y la misma postura defensiva aplicada al cifrado de documentos, incluida la ruta de clave AES-256 que comparte este código base, se describe en el artículo sobre cifrado AES-256 y seguridad. Todo ello se distribuye como parte de HotPDF Component para Delphi y C++Builder, junto con las API de carga, edición, cifrado y firma cubiertas en otras secciones de este blog