Por qué no veo pr0n en PDFs
En los últimos años multitud de 0days para Adobe Acrobat han visto la luz. Mucha gente comenta que la implementación del «estándar» ISO-32000 de Adobe está llena de vulnerabilidades pero poca gente veo que critique de por sí el formato (aunque también la hay). En este artículo os comento las cosas maravillosas del formato, las cosas también maravillosas de la implementación de Adobe y porqué todos los lectores de PDFs tienen y tendrán vulnerabilidades ya que, en mi opinión, es el formato el que está roto.
DISCLAIMER: La opinión mostrada aquí es la mía personal y no la de 48bits (que ahora que iroz está preparando la salida a bolsa definitiva tenemos que tener contentos a los posibles compradores).
El formato PDF
El formato está parcialmente documentado en la especificación ISO-32000 que solo está disponible previo pago (o tras búsqueda avanzada en Google…). De todos modos, Adobe nos deja gratuitamente descargar la especificación del formato PDF 1.7. Me voy a ceñir, de momento, a las características que vienen solo en este documento para mostrar porqué las propias especificaciones indican que es él formato el que está roto y no la implementación.
¿Qué es un PDF?
Si alguien nos pregunta que es un documento PDF lo más probable es que respondamos: «Un formato para ver documentos como si los hubiésemos impreso». Vale, pero ¿Es sólo eso? Pues va a ser que no, ya que tienen un montón de características fascinantes que, a título personal, ni de lejos hubiera pensado que estuvieran ahí o que hiciesen falta como por ejemplo:
- Soporte para múltiples formatos de vídeo no especificando la lista (página 764).
- Soporte para animaciones Flash (con una versión de Flash siempre anticuada en la implementación de Adobe).
- Un motor de renderizado 3D (página 789) para insertar, por ejemplo, documentos CAD.
- Soporte para conexión con bases de datos vía ADBC y ODBC (notas curiosas).
- Soporte para ejecución de comandos del sistema operativo (página 660).
- Soporte para JavaScript y FormCalc.
Este tipo de características que, en mi opinión, solo las utilizan un 0,5% de los usuarios del mundo-mundial ya han causado problemas en el pasado y, seguro, causarán en el futuro:
- PDF Backdoors Discovered (un artículo bastante sensacionalista sobre «problemas» con ADBC)
- Escape from PDF: «This is a special PDF hack: I managed to make a PoC PDF to execute an embedded executable without exploiting any vulnerability!».
- Adobe Acrobat, Reader, and Flash CVE-2010-3654 Remote Code Execution Vulnerability.
- Adobe Reader and Acrobat JavaScript methods buffer overflow vulnerabilities
De todos modos, queda de la mano del implementador elegir que características del estándar PDF se desean agregar así que vayamos a la base del formato y veamos porque todos los parsers están rotos…
Estructura de un PDF
La estructura típica de un archivo PDF es la siguiente:
%PDF-1.?
<num> <rev> obj
<</Properties /And /Values N>>
stream
stream data (typically binary)
endstream
endobj
trailer
<</More /Modifiers /And /RootObjectSpecification>>
%%EOF
Así pues, un documento PDF se compone de una cabecera (%PDF-1.<versión>), objetos (con un número y revisión), streams de datos, propiedades de los streams y de un trailer. Hasta aquí todo bien, «¡Qué fácil!», podemos decir a la hora de hacer nostros un parser de dicho formato pero no es todo tan bonito…
Diversión con los objetos
Los objetos en un documento PDF contienen streams. Un stream de datos puede ser, por ejemplo, una fuente embebida, una imagen, un archivo flash, un código JavaScript, etc… pero, además, un objeto puede referenciar a otros objetos. Por ejemplo, el siguiente código:
1 0 obj<</Type/Catalog/Pages 2 0 R /Names 3 0 R >>endobj
2 0 obj<</Type/Pages/Count 1/Kids[ 4 0 R ]>>endobj
3 0 obj<</JavaScript 5 0 R >>endobj
…
…referencia en el objeto «1» con revisión «0» al objeto 2 como contenido de la página y al objeto 3 como otros contenidos. El objeto 2 a su vez, referencia al objeto 4; el objeto 3, a su vez, referencia al objeto 5, y así podríamos seguir con un montón de referencias más, haciendo un tanto complejo el desarrollo de un parser decente.
Bueno, vale, ya tienes en cuenta que un objeto puede referenciar otro y luego, ¡Vaya! Te das cuenta que pueden darse problemas con referencias circulares y las controlas pero además ves que Adobe Acrobat controla las referencias circulares y simplemente las ignora en caso de encontrarlas, sin cascar ningún error. Vale, lo implementas «a la imagen y semejanza» del lector de PDFs más usado y continúas con el resto de «cositas» básicas para dar soporte al formato PDF y te encuentras lo siguiente:
1 0 obj<</Type/Catalog/Pages 2 0 R /Names 3 0 R >>endobj
2 0 obj<</Type/Pages/Count 1/Kids[ 4 0 R ]>>endobj
3 0 obj<</JavaScript 5 0 R >>endobj
…
1 0 obj<</Type/Catalog/Pages 100 0 R /Names 101 0 R >>endobj
…
¿Y aquí que hago? Tenemos el objeto número 1 duplicado pero Adobe Acrobat no casca ningún error y permite abrir el documento como si tal cosa. Miras la «docu» y te das cuenta que eso no viene documentado y tal (o al menos yo no me lo he encontrado). Al final te das cuenta que el último objeto es el que utiliza Adobe Acrobat y continúas implementando… con todos los fallos de la implementación de Adobe Acrobat, eso sí.
Reparación de objetos dañados
¿Es correcto un PDF como el siguiente?
1 0 obj <</Filter /FlateDecode >>
stream
…data…
2 0 obj
…
endobj
Así a priori podríamos decir que no ¿Verdad? No se ha cerrado el objeto «1 0» ni el stream correspondiente a ese objeto y se ha abierto otro. Bueno, pues Adobe Acrobat se lo come y si quieres hacer un lector de PDFs compatible con lo que soporta este pues te tocará comerte también este tipo de mariconadas…
Streams
Un stream es el contenido real de datos de un objeto. Esto es: si queremos mostrar una imagen esta se encontrará dentro de un stream. Los streams pueden estar codificados y/o comprimidos con los siguientes algoritmos:
- ZLib (/FlateCode)
- Contenido hexadecimal con formato ASCII (/AsciiHexDecode)
- Base 85 (/Ascii85Decode)
- Run Length Encode (/RunLengthDecode)
- LZW (/LZWDecode)
- JBIG2 (/Jbig2Decode)
- ….
Ya de primeras tenemos unos cuantos algoritmos que implementar… o que copiar de otros códigos. Si elegimos la opción más fácil habrá que tener en cuidado en no copiar además vulnerabilidades…
- JBIG2 Symbol Dictionary Processing in JPEG2000/JBIG Decoder add-on of Foxit Reader 2.3 and 3.0
- Adobe Acrobat and Reader PDF File Handling JBIG2 Image Remote Code Execution Vulnerability
El contenido de un stream puede estar además codificado y/o comprimido múltiples veces con cualquiera de los filtros aquí comentados haciendo cualquier mezcla que se te ocurra, incluso aplicando varias veces el mismo filtro. Por ejemplo, este pequeño stream que pongo aquí abajo:
<</Filter /AsciiHexDecode /FlateDecode /FlateDecode /FlateDecode /FlateDecode >>
stream
789cab98f3f68e629e708144fbc3facd9c46865d0e896a139c13b36635382ab7c55930c86d57e59ec79c7071c5afb385cdb979ec0a2d13585dc32e79d55c5ef2fef39c0797f7d754dad7fd
2c349dd96378cedebee6f7cf17090c4060fdeecfb7a47c53b69ec54fbfcedefe1e28d210fbfddfc787ffaa447e54ff7af3755b3f2350ccecdde51ab3d87a8e3f76bf37ec7f9b0c52d55bfd
ebf9bbab55dc3ff6c5d858defc660a143b70ec2e071b9076e8021bbd05c2e906738e20734665a82e5333f7fcbcf5db1a5efe2dfaf8a98281e1cff34f47d71baafd67609ceebb1700153f9a
9dendstream
Causará que tu máquina consuma 256MB de memoria solo para descomprimir un stream que no tiene nada más que un montón de bytes 0x00 comprimido varias veces con ZLib (/FlateDecode). Si pones, por ejemplo, 8 streams de este tipo provocarás que Acrobat Reader (o Foxit Reader o SumatraPDF o XPdf o Pepito Reader(TM)…) consuma 2 GB de memoria.
Ofuscación
Otra característica curiosa de los streams es que las propiedades del mismo pueden estar codificadas de varios modos. Por ejemplo, el stream siguiente:
<</Filter /AsciiHexDecode /FlateDecode >>
stream
…
endstream
Se podría codificar también de este modo utilizando carácteres hexadecimales:
<</Filter /#41sciiHexDecode /Fl#61teDecode >>
stream
…
endstream
O se podría codificar también de este modo utilizando codificación octal:
<</Filter /\101sciiHexDecode /Fl\141teDecode >>
stream
…
endstream
O también de este otro modo:
<</Filter /\101sciiH#65xD#65cod#65 /Fl\141t#65D#65cod#65 >>
stream
…
endstream
Y todas estas codificaciones (y las mezclas que se te ocurran) serán válidas. De hecho, es un tipo de ofuscación muy habitual en malware.
Conclusiones
El post ya se hace un poco largo así que creo que lo voy a dejar aquí, aunque podría seguir poniendo características y funcionamientos no documentados hasta que vomitéis. Eso sí, txapo el post pero no sin antes hacer una pequeña pregunta a quien se haya leído todo este truño: ¿No os parece un formato de archivo lo suficientemente complicado como para que meter la pata sea demasiado simple? En mi humilde opinión, sí. Así que todos los parsers y visores de PDFs del mundo estarán rotos por uno u otro sitio… O al menos si quieren ser compatibles con Adobe Acrobat.