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
PDFlibPas pasó por una fase de endurecimiento que trató 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 clases de defectos que cerró, cada una basada en el comportamiento específico de Delphi que lo hizo posible. Están solucionados en las versiones actuales, 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
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 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 residía en el paso de 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 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
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 con la forma "detenerse cuando el recuento de páginas exceda 65535", que parece 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 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 alcanzable, el bucle termina, y la propiedad pública PageCount cambió de tipo para coincidir sin romper ninguna llamada. 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 no es un error sino un acceso a memoria en bruto. Eso está bien cuando el índice siempre es válido, y deja de estar bien 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. Con la comprobación de rangos desactivada, ambos se convierten en 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
El endurecimiento no volvió a activar la comprobación de rangos para la ruta crítica. Primero hizo que los índices fueran 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 se escribió como un "estrictamente menor que" en lugar de un "menor o igual que" contra la cantidad para evitar 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 ya se protegía contra esto. Llevaba un contador de profundidad de llamadas y un techo, 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 menciona. El intérprete de Type2, agregado 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 atraviesa directamente la comprobación faltante hacia un desbordamiento de pila
// La forma de la protección de Type1 que le faltaba a la ruta de Type2.
// Rastree la profundidad en llamadas anidadas y rechace la recursión más allá de esta.
Inc(CallDepth);
if CallDepth > PLType1MaxCallDepth then
Exit; // subrutina hostil autorreferencial; deje de descender
// ... interprete la subrutina, luego Dec(CallDepth) a la salida
La solución fue darle a la ruta de Type2 la misma profundidad limitada que ya tenía 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
El defecto más sutil filtraba 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
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 de a un 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 escribió, por lo que esos bytes finales mantuvieron los contenidos del montón que SetLength dejó atrás y el búfer se entregó 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 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 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 le deja esta fase
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 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 uno de ellos, Delphi hizo 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
Estos defectos están solucionados en las versiones actuales de PDFlibPas, el motor para Delphi y C++Builder. 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 PDFlibPas Delphi PDF Library junto a las API de carga, renderizado y firma cubiertas en otras partes de este blog