Artículo técnico

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

Cuando firma un PDF, generalmente piensa en la clave de firma como algo que usted controla. Vive en un archivo .pfx que generó, protegido por una contraseña que eligió. El código que lee ese archivo se siente como plomería, no como 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, un firmante por lotes alimentado con 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 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 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 todos los analizadores binarios escritos en un lenguaje con enteros de ancho fijo: se confía en una longitud o un recuento del archivo un paso más de lo que se debería. Uno conduce a una lectura fuera de los límites, el otro a un proceso que se bloquea hasta que usted lo elimina

Por dónde viajan los bytes

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

En HotPDF, esas etapas se asignan a unidades distintas. La lógica del contenedor PKCS#12 vive 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 asientan en HPDFCrypt junto a PBKDF2HMACSHA256. Cuando se recupera la clave, HPDFRSA y el constructor CMS SignedData en HPDFCMS la convierten en la firma separada incrustada en el PDF. El punto de entrada público que impulsa 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 afirma el archivo, la criptografía que viene después nunca tiene la oportunidad de importar

Defecto uno: una longitud de ASN.1 que desborda más allá del guardia

El 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 que se debe confiar pero verificar, porque le dice al analizador (parser) qué tan lejos leer, y fue escrito por quien produjo el archivo. 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 cosa mayor, gasta un byte principal cuyos siete bits bajos dan el recuento de bytes de longitud que le 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 cercano a los cuatro gigabytes

Después de decodificar tal valor, el analizador tiene que verificar que el contenido realmente quepa dentro del búfer antes de confiar en él. La verificación natural es confirmar que la posición actual más la longitud del contenido no pase del final de los datos. Escrita de la manera obvia, con la posición, la longitud del contenido y el total mantenidos todos en enteros con signo de 32 bits, esa guardia está rota:

// 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 de 32 bits con signo y da la vuelta (wraps around) a un número negativo. Una suma negativa nunca es mayor que Total, por lo que el guardia informa 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 reclamada y copia en él, un SetLength seguido de un Move leyendo desde la fuente. A la fuente le quedan solo unos pocos cientos de bytes, 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, se bloquea y, en el peor, filtra la memoria de procesos adyacentes en el análisis

La única guardia correcta amplía la suma intermedia antes de la comparación, por lo que la suma no puede 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 verificación separada no negativa en ContentLen cierra el caso coincidente donde un valor decodificado aterriza negativo por sí solo. En HotPDF, este guardia vive en HPDFASN1ParseNode, la función que produce el nodo sobre el que se basan los demás ayudantes. Debido a que HPDFASN1Content dimensiona su SetLength y Move directamente a partir de la longitud del contenido del nodo, un nodo que pasara un guardia defectuoso habría envenenado cada lectura tomada de él. Arreglar el límite en el punto de decodificación es lo que hace que los ayudantes por encima de él sean seguros

Defecto dos: un recuento de iteraciones de PBKDF2 usado como un arma

La segunda falla no es un error de memoria, es el archivo que le dice 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 el RFC 8018. PBES2 ejecuta una función de derivación de claves, aquí PBKDF2 con HMAC-SHA-256, y luego un cifrador, aquí AES-256-CBC. PBKDF2 toma un recuento de iteraciones, y ese recuento es un parámetro que se lleva en el archivo. Su único propósito es ser lento: más iteraciones significa que cada conjetura de contraseña cuesta más, lo cual es bueno contra un atacante fuera de línea (offline). El RFC 8018 §4.2 es explícito en que un recuento mayor es mejor para la seguridad, y deliberadamente no establece un techo

Esa apertura está bien cuando usted generó el archivo. Es un arma cuando lo hizo 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 diligentemente y llama a PBKDF2 para tantas rondas de HMAC-SHA-256, y el proceso desaparece en un bucle que no regresará durante minutos u horas para un archivo proporcionado. En un servidor de firmas que maneja una credencial por solicitud, una sola carga maliciosa estanca a un trabajador

El recuento empeora el desbordamiento circular antes de hacer que la CPU gire. El valor de iteración vive en el archivo como un INTEGER ASN.1, que no tiene un ancho fijo, mientras que el campo que PBKDF2 consume finalmente es un Integer de 32 bits. Si decodifica el INTEGER directamente en ese campo, un valor grande se trunca, y un valor diseñado para aterrizar en el bit de signo vuelve 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 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

Leer en un Int64 significa que el valor decodificado es el real, no un fantasma truncado de él. El límite inferior rechaza los recuentos cero y negativos, que no tienen sentido para una derivación de clave. El límite superior, cien millones, se sitúa muy por encima de cualquier archivo PKCS#12 legítimo, que hoy en día utiliza desde decenas hasta los pocos cientos de miles de iteraciones, al tiempo que limita el peor de los casos a una cantidad de trabajo limitada y sobrevivible. Solo después de que el valor ha pasado esa banda se reduce al campo de 32 bits, de modo que el truncamiento ya no puede sorprender a nadie. En HotPDF, este límite vive en ParsePBES2Params, donde los parámetros PBKDF2 se decodifican en el 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 bloqueado, 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 demasiado pronto, antes de que se verificara con 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 ceden a la misma disciplina: decodificar con el ancho completo, comprobar con el límite real y solo entonces reducir. El Int64 intermedio no es una elección de estilo, es el único ancho en el que el guardia puede ver el valor que el atacante escribió en realidad. Un límite que se desborda no es un límite, y un recuento sin un techo no es un parámetro, es un acelerador remoto en su propia CPU

Guía práctica para una tubería de firma

La lección específica es validar la entrada de un certificado no confiable de la misma manera que validaría cualquier carga (upload) no confiable. Limite el tamaño de un .pfx que acepte, ya que uno legítimo es de kilobytes, no de megabytes. Trate una falla de análisis como una entrada rechazada de rutina, no como un error que merezca mostrarle un seguimiento de pila al usuario. Si firma en un servidor, ejecute la importación donde un trabajador atascado no pueda derribar el servicio con él, y ponga un tiempo de espera en la operación para que un archivo inesperadamente costoso esté limitado tanto por el reloj de pared como por el límite de iteraciones

La lección más amplia va más allá de los certificados. El fortalecimiento de analizadores no es una auditoría de una sola vez para una sola unidad, es una propiedad de cada lugar donde su biblioteca lee bytes que no escribió. Una biblioteca de PDF analiza mucho contenido de fuentes no confiables: fuentes incrustadas en un documento, imágenes en media docena de códecs, filtros de flujo de datos 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 construye la ruta de importación y de firma sobre las unidades fortalecidas HPDFASN1, HPDFPFX, HPDFCrypt y HPDFCMS descritas aquí, de modo que la credencial que se le entrega, de donde provenga, se analiza defensivamente antes de que jamás se le confíe

El flujo de trabajo de firma que protegen estas comprobaciones está cubierto de principio a fin en nuestro recorrido por las firmas digitales PAdES en Delphi, y la misma postura defensiva aplicada al cifrado de documentos, incluida la ruta de la clave AES-256 que comparte este código base, se describe en el artículo sobre el cifrado AES-256 y seguridad. Todo ello se incluye como parte del Componente HotPDF para Delphi y C++Builder, junto a las API de carga, edición, cifrado y firmas cubiertas en otras partes de este blog