Un PDF no es un documento que usted abre. Es un pequeño programa que usted ejecuta. Cada fuente incrustada es un intérprete basado en pila esperando charstrings, cada imagen es un decodificador alimentado con campos de ancho, alto y profundidad de bits que el archivo eligió, y cada flujo de datos llega envuelto en filtros cuyos parámetros estableció el archivo. Ninguno de esos números es suyo. Provinieron de quien 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 una falla o de algo peor
PDF Library for Delphi trata a toda la ruta de decodificación como hostil, a lo largo de los programas de fuentes (TrueType, Type1, CFF y las tablas CMap), los decodificadores de imágenes (PNG, GIF, TIFF, JBIG2, y CCITT Group 3 y Group 4) y los filtros de flujo (LZW, ASCII85 y los predictores Flate). Lo que sigue son cinco trampas de seguridad de memoria que esa ruta tiene que cerrar, cada una basada en el comportamiento específico de Delphi que la hace posible, y las mismas formas se repiten en cualquier código en Pascal que analice entradas no confiables
Un desbordamiento de enteros que le entrega un búfer de tamaño insuficiente
El clásico error de seguridad de memoria en un decodificador de imágenes es un producto de dimensiones que se desborda. Un decodificador lee el ancho, el alto, la cantidad 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 se encuentra dentro de un rango razonable, de modo que la asignación tiene éxito, resulta ser demasiado pequeña y la decodificación termina escribiendo más allá de su límite final. Esto es CWE-190, desbordamiento de enteros, lo que lleva a una escritura fuera de límites en el montón (CWE-787) un paso después
Limitar cada dimensión a 65535 en una ruta de imagen compartida no basta cuando un decodificador independiente calcula sus propios tamaños. Una expresión de bytes por fila multiplicada por alto 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, sin importar cuán ancha sea la variable a la que se asigne el resultado. Un ancho y un alto de 60000 son plausibles para un escaneo grande, pero su producto en bytes excede un rango de 32 bits con signo y la longitud resulta pequeña. La misma trampa reside en un paso de predictor como BitsPerComponent * Colors * Columns
La defensa es hacer que al menos un operando sea Int64 para que toda la expresión se evalúe en 64 bits, luego comparar con MaxInt y rechazar el archivo antes de reducir el tamaño de nuevo para llamar a SetLength
// Rechace antes de asignar, no después de escribir.
// Evalúe el producto en Int64 para que no se desborde a 32 bits.
RowBytes := (Int64(FWidth) * Components * BitDepth + 7) div 8;
if (RowBytes <= 0) or (RowBytes * FHeight > MaxInt) then
Exit; // dimensiones hostiles o no admitidas; rechazar la imagen
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. Asignar una expresión demasiado grande en un destino de 32 bits es una conversión legal sobre la cual el compilador no advertirá de manera predeterminada, y la comprobación de rangos no detecta un desbordamiento que ocurre antes de que el valor se use alguna vez como índice. Deje el producto en 32 bits y el lenguaje le dará discretamente una longitud que miente sobre cuánta memoria la decodificación está a punto de tocar
Un tipo de campo que hace imposible que se dispare una protección
Un archivo TIFF es una cadena de directorios de archivos de imagen (IFD), 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 recorre sin una condición de detención se ejecuta para siempre. 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
Ahora declare el contador de páginas como Word, que en Delphi contiene valores de 0 a 65535, y agregue al bucle una protección de terminación con la forma "detenerse cuando el recuento de páginas exceda 65535". Se lee como correcto hasta que se observa 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 es estructuralmente siempre 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 y una cadena IFD en bucle mantiene al lector iterando
La cura es ampliar el campo para que la protección pueda expresar un valor que el contador realmente pueda contener. TPDFTIFF.FPageCount está declarado como Integer, así que la comparación FPageCount > 65535 es alcanzable y el bucle termina. Siempre que una comprobación de límites tenga la forma Valor > ValorMaximoDelTipo(Valor) y el operando ya esté tipado exactamente en ese máximo, la condición es una constante falsa: amplíe el tipo, o evalúe la igualdad contra el máximo para que pueda activarse
Comprobación de rangos desactivada en una ruta crítica
Con la comprobación de rangos activada, Delphi inserta una comprobación de límites en cada índice de matriz y cadena, que es la diferencia entre un índice fuera de rango que genera un ERangeError atrapable, y ese mismo índice leyendo o escribiendo memoria que no pertenece a la estructura. Las rutas críticas a veces la desactivan con una directiva local {$R-}, lo cual es justificable hasta que los índices dejan de ser confiables
El accesor de lista en el que se apoyan los intérpretes de fuentes, TPDFlibStringList.Get, es exactamente una de esas rutas. En Windows, se compila con la comprobación de rangos desactivada e indexa su almacenamiento de respaldo directamente, por lo que un índice fuera de rango sería un acceso a memoria en bruto en lugar de un error. Eso está bien cuando el índice siempre es válido, y exige cuidado dentro de un intérprete de charstrings CFF o Type2, donde el índice puede provenir del archivo. Un charstring que saca un operando de una pila vacía produce un índice de menos uno; un identificador de glifo desfasado por uno con respecto a la cantidad de glifos indexa una ranura más allá del final. Si no se controla, ambos serían un verdadero acceso fuera de límites en lugar de una excepción atrapable, y debido a que las ranuras contienen valores AnsiString con recuento de referencias, una lectura extraviada también puede corromper el recuento de referencias de una cadena
PDF Library for Delphi mantiene la comprobación de rangos desactivada en esa ruta crítica y en cambio hace que los índices sean demostrablemente válidos: antes de tomar el tope de la pila de operandos, el intérprete comprueba que la pila no esté vacía, y cada protección de índice es un estricto "menor que" contra la cantidad en lugar de un "menor o igual que" que admite el desfase por uno. La directiva traslada la responsabilidad de los límites desde el compilador hacia usted, y la validación que eliminó debe volver a colocarse a mano en cada punto de entrada
Recursión no controlada en un intérprete de charstrings
Un charstring de Type2 puede llamar a una subrutina, y una subrutina es en sí misma un charstring que puede llamar a otra, por lo que los operadores de llamadas de subrutina local y global permiten que el archivo decida qué tan profundo llega. Una subrutina que se llama a sí misma, directamente o a través de un ciclo, se repite sin fin hasta que se agota la pila nativa y el proceso muere. Eso es CWE-674, recursión no controlada
El intérprete de Type1 se protege contra esto con un contador de profundidad de llamadas y un techo, PLType1MaxCallDepth, y se niega a descender más allá de él, lo que refleja el límite de profundidad que la propia especificación de Type1 menciona. La trampa es que un intérprete estructuralmente similar escrito más tarde, como el de Type2, no hereda esa protección por sí solo, y una fuente construida a mano con una subrutina que llama a su propio número atraviesa directamente la comprobación faltante hacia un desbordamiento de pila
// La forma de la protección de profundidad que ambos intérpretes de charstrings necesitan.
// Rastree la profundidad a través de llamadas anidadas y rechace recursar más allá de ella.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // subrutina autorreferencial hostil; deje de descender
// ... interpret the subroutine, then Dec(CallDepth) on the way out
PDF Library for Delphi le da a la ruta de Type2 la misma profundidad limitada que su hermano de Type1. Cualquier descenso recursivo sobre una estructura controlada por un atacante, ya sean subrutinas de fuentes, una matriz anidada o una cadena de referencias cruzadas, necesita un techo de profundidad que la entrada no pueda elevar
Memoria no inicializada que se filtra en la salida
La trampa más sutil filtra los contenidos del montón hacia la salida descifrada, y la causa es una propiedad de SetLength que es fácil de olvidar. Cuando hace crecer un AnsiString con SetLength, Delphi asigna los bytes pero no los pone a cero, por lo que la nueva región retiene cualquier cosa que estuviera previamente en esa memoria del montón. Si cada byte se escribe subsecuentemente, 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 fuga de información
Una ruta de descifrado AES-CBC es exactamente donde esto muerde. Dimensione el búfer de salida con SetLength, procese el texto cifrado de a un bloque de 16 bytes a la vez, y cuando la longitud del texto cifrado no es un múltiplo de 16, una longitud que un atacante puede elegir, el bloque parcial final nunca se escribe, por lo que esos bytes finales mantienen los contenidos del montón que SetLength dejó atrás y el búfer se entrega como el texto plano descifrado de un objeto de documento. El remedio consta de dos protecciones, y ninguna por sí sola es suficiente: el punto de entrada de descifrado de PDF Library for Delphi 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 usarla, por lo que cualquier ruta que no logre escribir en una región devuelve ceros en lugar de residuos del montón
Lo que las cinco trampas tienen en común
Las cinco trampas son errores diferentes, pero riman. Un ancho de entero que desborda un producto, un tipo de campo que fija una protección en un falso constante, una comprobación de rango desactivada donde los índices dejaron de ser seguros, una recursión sin límite y un búfer que el lenguaje se negó a poner a cero. En cada una de ellas, Delphi hace exactamente lo que define, porque el lenguaje le brinda aritmética que se desborda, reducciones que son silenciosas, comprobaciones de rango que usted puede desactivar, recursión sin límite incorporado y asignación que no se inicializa. Ese es el contrato, y un analizador en Pascal lo cumple al controlar cuatro cosas de forma manual en cada límite que el archivo controla: el ancho de enteros, la comprobación de rangos, la profundidad de recursión y la inicialización de los búferes
PDF Library for Delphi, el motor para Delphi y C++Builder, protege su ruta de decodificación contra las cinco. Si su trabajo también llega hasta la forma en que un archivo afirma estar protegido, las notas complementarias sobre la auditoría de cifrado y permisos y sobre la comprobación previa de PDF/A y PDF/UA cubren el lado del análisis del mismo analizador, y todo ello se incluye dentro de la PDF Library for Delphi Delphi PDF Library junto a las API de carga, renderizado y firma cubiertas en otras partes de este blog