Un PDF no es un documento que simplemente se abre. Es un pequeño programa que se ejecuta. Cada fuente incrustada es un intérprete basado en pila que espera cadenas de caracteres (charstrings), cada imagen es un decodificador alimentado con campos de ancho, altura y profundidad de bits elegidos por el archivo, y cada flujo llega envuelto en filtros cuyos parámetros estableció el archivo. Ninguno de esos números es suyo. Provienen de quienquiera que haya producido el archivo, que en una carga de trabajo real es la factura de un cliente o un archivo adjunto de un remitente desconocido. Los decodificadores que convierten esos bytes en píxeles y glifos son la superficie de ataque, y un analizador que confía en su entrada allí está a un archivo malformado de distancia de una caída o algo peor
PDFlibPas pasó por una fase de endurecimiento que trató toda la ruta de decodificación como hostil, a través de los programas de fuentes (TrueType, Type1, CFF y las tablas CMap), los decodificadores de imágenes (PNG, GIF, TIFF, JBIG2 y CCITT Grupo 3 y Grupo 4) y los filtros de flujo (LZW, ASCII85 y los predictores Flate). A continuación se presentan cinco clases de defectos que cerró, cada uno basado en el comportamiento específico de Delphi que lo hizo posible. Están corregidos en las versiones actuales, y las mismas formas se repiten en cualquier código de Pascal que analice entradas no confiables
Un desbordamiento de enteros que le entrega un búfer de tamaño insuficiente
El error clásico de seguridad de memoria en un decodificador de imágenes es el producto de dimensiones que se desborda. Un decodificador lee el ancho, la altura, el recuento de componentes y la profundidad de bits, los multiplica para dimensionar su salida, asigna esa cantidad de bytes y luego escribe la imagen en sus dimensiones reales. Si la multiplicación se realiza en aritmética de 32 bits, el producto puede desbordarse a un valor pequeño incluso cuando cada factor individual está dentro de un rango sensato, por lo que la asignación tiene éxito, resulta demasiado pequeña y la decodificación se sale de sus límites. Esto es CWE-190, desbordamiento de enteros, que conduce a una escritura fuera de límites en el montículo (CWE-787) un paso después
La ruta de imagen compartida ya limitaba cada dimensión a 65535; los decodificadores independientes no heredaron todos ese límite. Una expresión de bytes de fila por altura como ByteCount * FHeight, o una expresión por píxel como FWidth * Components * BitDepth, es un producto de 32 bits en Delphi cuando ambos operandos son enteros de 32 bits, independientemente de qué tan ancha sea la variable a la que asigne el resultado. Un ancho y una altura de 60000 son plausibles para un escaneo grande, pero su producto en bytes supera el rango con signo de 32 bits y la longitud resulta pequeña. La misma trampa existía en el paso del predictor ZLib, BitsPerComponent * Colors * Columns
La solución es hacer que al menos un operando sea Int64 para que toda la expresión se evalúe en 64 bits, luego comparar contra MaxInt y rechazar el archivo antes de reducir el tamaño para llamar a SetLength
// Reject before allocating, not after writing.
// Evaluate the product in Int64 so it cannot wrap at 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // hostile or unsupportable dimensions; refuse the image
SetLength(Buffer, RowBytes * FHeight);
Lo que hace que esto sea un problema de Delphi en lugar de uno genérico es la reducción silenciosa de tipo. Asignar una expresión demasiado ancha a un destino de 32 bits es una conversión legal sobre la cual el compilador no advertirá por defecto, y la comprobación de rango no detecta un desbordamiento que ocurre antes de que el valor se use como índice. Deje el producto en 32 bits y el lenguaje le dará silenciosamente una longitud que miente sobre cuánta memoria está a punto de tocar la decodificación
Un tipo de campo que hace imposible activar una protección
Un archivo TIFF es una cadena de directorios de archivos de imagen, cada uno de los cuales lleva el desplazamiento de bytes del siguiente. Un archivo malicioso puede apuntar esa cadena hacia sí mismo, y un lector que la recorra sin una condición de parada se ejecutará indefinidamente. Eso es CWE-835, un bucle infinito impulsado por una entrada controlada por el atacante, y la defensa es un contador que se detiene una vez que supera un límite que ningún archivo legítimo alcanzaría
El contador de páginas se declaró como Word, que en Delphi contiene valores de 0 a 65535. El bucle llevaba una protección de terminación de la forma "detener cuando el recuento de páginas supere 65535", lo que se lee como correcto hasta que se da cuenta de que el operando y el umbral comparten un límite superior. Un Word nunca puede ser mayor que 65535, por lo que la comparación estructuralmente siempre es falsa: cuando el contador llega a 65535, el siguiente incremento lo devuelve a 0, la protección nunca ve un valor por encima del límite máximo y una cadena de IFD en bucle mantiene al lector girando
La solución fue ampliar el campo para que la protección pueda expresar un valor que el contador realmente pueda contener. Con TPDFTIFF.FPageCount declarado como Integer, la misma comparación FPageCount > 65535 se vuelve accesible, el bucle termina y la propiedad pública PageCount cambió de tipo para coincidir sin romper a ningún llamador. Siempre que una comprobación de límite tenga la forma Value > MaxValueOfType(Value) y el operando ya sea del tipo de ese máximo exacto, la condición es una constante falsa: amplíe el tipo, o pruebe la igualdad contra el máximo para que pueda activarse
Comprobación de rango desactivada en una ruta caliente
Con la comprobación de rango activada, Delphi inserta una comprobación de límites en cada índice de matriz y de cadena, lo que marca la diferencia entre que un índice fuera de rango genere un error capturable ERangeError o que ese mismo índice lea o escriba en memoria que no pertenece a la estructura. Las rutas calientes a veces la desactivan con una directiva local {$R-}, lo cual es defendible justo hasta que los índices dejan de ser confiables
El descriptor de acceso de lista en el que se apoyan los intérpretes de fuentes, TPDFlibStringList.Get, es exactamente esa ruta. En Windows, se compila con la comprobación de rango desactivada e indexa su almacenamiento de respaldo directamente, por lo que un índice fuera de rango no es un error sino un acceso a memoria no procesada. Eso está bien cuando el índice siempre es válido, y deja de estar bien dentro de un intérprete de cadenas de caracteres (charstrings) CFF o Type2, donde el índice puede provenir del archivo. Una cadena de caracteres que extrae un operando de una pila vacía produce un índice de menos uno; un identificador de glifo desviado por uno con respecto al recuento de glifos indexa una ranura más allá del final. Con la comprobación de rango desactivada, ambos se convierten en un acceso real fuera de límites en lugar de una excepción capturable, y debido a que las ranuras contienen valores AnsiString con recuento de referencias, una lectura desviada también puede corromper el recuento de referencias de una cadena
El endurecimiento no volvió a activar la comprobación de rango para la ruta caliente. Hizo que los índices fueran demostrablemente válidos primero: antes de tomar la parte superior de la pila de operandos, el intérprete comprueba que la pila no esté vacía, y cada protección de índice se escribió como un menor estricto que en comparación con el recuento en lugar de un menor o igual que admite el desvío por uno. La directiva traslada la responsabilidad de los límites del compilador a usted, y la validación que eliminó tiene que volver a colocarse a mano en cada punto de entrada
Recursividad ilimitada en un intérprete de cadenas de caracteres
Una cadena de caracteres Type2 puede llamar a una subrutina, y una subrutina es en sí misma una cadena de caracteres que puede llamar a otra, por lo que los operadores de llamada a subrutinas locales y globales permiten al archivo decidir qué tan profundo llega. Una subrutina que se llama a sí misma, directamente o a través de un ciclo, realiza una recursividad sin fin hasta que la pila nativa se agota y el proceso muere. Eso es CWE-674, recursividad descontrolada
El intérprete Type1 ya se protegía contra esto. Llevaba un contador de profundidad de llamada y un límite máximo, PLType1MaxCallDepth, y se negaba a descender más allá de él, lo que refleja el límite de profundidad que la propia especificación de Type1 establece. El intérprete Type2, añadido más tarde y estructuralmente similar, no llevaba la misma protección, y una fuente construida a mano con una subrutina que llama a su propio número pasa directamente a través de la comprobación ausente a un desbordamiento de pila
// The shape of the Type1 guard the Type2 path was missing.
// Track depth across nested calls and refuse to recurse past it.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // hostile self-referential subroutine; stop descending
// ... interpret the subroutine, then Dec(CallDepth) on the way out
Memoria no inicializada que se filtra en la salida
El defecto más sutil filtraba el contenido del montículo en la salida descifrada, y la causa es una propiedad de SetLength que es fácil de olvidar. Cuando hace crecer una cadena AnsiString con SetLength, Delphi asigna los bytes pero no los pone a cero, por lo que la nueva región contiene lo que estuviera previamente en esa memoria del montículo. Si posteriormente se escribe cada byte, esto nunca importa; si una ruta deja parte del búfer sin escribir y luego lo devuelve como datos, esos bytes obsoletos salen con el resultado. Eso es CWE-457, uso de memoria no inicializada, y cuando el resultado cruza un límite de confianza se convierte en una filtración de información
La ruta de descifrado AES-CBC experimentó exactamente esto. El búfer de salida se dimensionó con SetLength y el descifrador procesó el texto cifrado bloque por bloque de 16 bytes a la vez. Cuando la longitud del texto cifrado no era un múltiplo de 16, una longitud que un atacante puede elegir, el bloque parcial final nunca se escribía, por lo que esos bytes finales conservaban el contenido del montículo que SetLength dejó atrás y el búfer se devolvía como el texto plano descifrado de un objeto de documento. La solución consiste en dos protecciones, y ninguna de ellas por sí sola es suficiente: el punto de entrada de descifrado ahora rechaza cualquier texto cifrado cuya longitud no sea un múltiplo del tamaño del bloque, y como respaldo, la salida se limpia con FillChar antes de su uso para que cualquier ruta que no escriba en una región devuelva ceros en lugar de residuos del montículo
Con lo que le deja la fase de revisión
Los cinco defectos son errores diferentes, pero riman. Un ancho de entero que desborda un producto, un tipo de campo que fija una protección a una constante falsa, una comprobación de rango desactivada donde los índices dejaron de ser seguros, una recursividad sin fondo y un búfer que el lenguaje declinó poner a cero. En cada uno de ellos, Delphi hizo exactamente lo que define, porque el lenguaje le brinda aritmética que se desborda, reducción de tipo silenciosa, comprobaciones de rango que puede desactivar, recursividad sin límite incorporado y asignación que no inicializa. Ese es el contrato, y un analizador de Pascal lo cumple asumiendo cuatro tareas a mano en cada límite que el archivo controla: ancho de entero, comprobación de rango, profundidad de recursividad e inicialización del búfer
Estos defectos están cerrados en las versiones actuales de PDFlibPas, el motor para Delphi y C++Builder. Si su trabajo también abarca cómo un archivo afirma estar protegido, las notas complementarias sobre auditoría de cifrado y permisos y sobre preflight de PDF/A y PDF/UA cubren el lado del análisis del mismo analizador, y todo ello se distribuye dentro de la PDFlibPas Delphi PDF Library junto con las API de carga, procesamiento y firma cubiertas en otras secciones de este blog