PDFs "a pelo" for fun and profit (Parte I)

Publicado por Pedro C. el 06-02-2013

Vamos a publicar una serie de artículos, para que podais crear vuestros propios ficheros PDFs desde cero y "a mano" para poder comprender cómo funciona el formato y poder embeber código en ellos para ejecutar ciertas acciones y algunas de ellas muy interesantes como veremos.

En éste primer artículo, veremos el formato de un fichero PDF para poder crearlo con un simple editor de texto. Soy consciente que no es un artículo de agradable lectura por su extensión y tecnicidad, pero es necesario sentarse a estudiarlo con "papel y boli" para comprenderlo y poder posteriormente jugar con el formato a nuestro antojo. Las risas están aseguradas tras el esfuerzo inicial...

El formato PDF

PDF es acrónimo de Portable Document Format y fue creado para ver el mismo contenido en diferentes plataformas y medios como la pantalla, impresora, etc. De sobra es conocido por todos y sobretodo, por las vulnerabilidades que tiene Adobe Acrobat y Adobe Reader tal como podemos leer en el blog Inteco entre otros. Originalmente el formato era propietario de Adobe pero desde el año 2008, forma parte del estándar 32000-1:2008; para aquell@s que quieran profundizar en la documentación oficial existente, lo tienen en la sección PDF Reference de Adobe, concretamente en la versión 1.7 (sexta edición)

De forma general, un fichero PDF se compone de objetos y por regla general se encuentra estructurado en cuatro partes:

  • Cabecera del fichero: Nos indicará la especificación que seguirá el fichero
  • Cuerpo: Contiene todos los objetos que forman el documento
  • Tabla de referencias cruzadas: Enumera dónde se encuentran los objetos indirectos en el documento
  • Trailer: Contiene el número de entradas de la tabla de referencias cruzadas y otros objetos especiales
  • /--------------\
    |   CABECERA   |
    |--------------|
    |              |
    |    CUERPO    |
    |              |
    |--------------|
    |  TABLA XREF  |
    |--------------|
    |   TRAILER    |
    \--------------/
    				  

Comentarios

Como en cualquier lenguaje de programación, podemos incluir comentarios precedidos del símbolo % comentario...

La cabecera

Nos sirve para especificar la versión que seguirá el formato del fichero. Es de la forma %PDF-v1.7 (o cualquier ocurrencia del desarrollador). También puede contener una línea opcional para especificar si el fichero PDF contiene datos binarios.

El cuerpo

Hace referencia a todos los objetos indirectos con los que vamos a trabajar en el documento. Un objeto se representa de la forma:

1 0 obj
...
endobj
2 0 obj
...
endobj
...				
				

El primer número representa el número de objeto y el segundo número la revisión. No tienen porque ser consecutivos, sino que podemos numerarlos conforme queramos y nos sea más cómodo para luego seguir un cierto orden y poder insertar nuevos objetos intermedios (de diez en diez, por centenas, etc... ¿Recordais la líneas del BASIC?). Con respecto a las revisiones, recordar que un PDF modificado conserva los datos originales aunque se muestren los nuevos y de lo que hablaremos en un futuro para jugar al despiste como jugaron con las tropas americanas en Iraq al desclasificar información...

Existen 9 tipos de objetos:

  • Número: Ejemplo 1
  • Referencia indirecta: Ejemplo 1 0 R y si el objeto no existe, sería igual a un objeto Null
  • Nombre: Ejemplo /Name que es un identificador
  • Diccionario: Ejemplo <<...>> que es una lista no ordenada de pares del tipo (Nombre,Objeto) donde el objeto puede ser también otro nombre. Por ejemplo /Type /Font
  • Array: Ejemplo [ item1 item2 item3 ... ] y representa una lista ordenada de objetos
  • Cadena: Ejemplo (texto) empleado para representar cadenas
  • Stream: Ejemplo << /Length ... >> stream ... endstream empleado para representar datos embebidos y puede ser comprimido con diferentes algoritmos como veremos para reducir el tamaño del fichero
  • Boolean: Ejemplo true
  • Null Object: Ejemplo null

El cuerpo debe especificar al menos los objetos Catálogo, Nodo Raíz del árbol de páginas y una Página del árbol. Es decir, un mínimo de tres objetos, aunque como veremos en un ejemplo posterior, serán necesarios muchos más para poder crear un PDF con un simple "Hola MADESYP!!!". Es decir, un objeto de fuente, un objeto con el texto, etc...

  • Objeto Catálogo: Lo mínimo que debe incluir es un elemento /Type con el valor /Catalog junto con una referencia indirecta al nodo raíz del árbol de páginas:
  • 1 0 obj
    <</Type /Catalog
    /Pages 2 0 R
    >>
    endobj				  
    				  
  • Objeto Nodo Raíz del Arbol de Páginas: Lo mínimo que tenemos que especificar, es el identificador /Type con el valor /Pages, un objeto array con el valor /Kids y las referencias indirectas a las páginas hijas y un elemento /Count con el número de páginas hijas que dependen de éste nodo:
  • 2 0 obj
    <</Type /Pages
    /Kids [ 3 0 R ]
    /Count 1
    >>
    endobj
    				  
  • Objeto Página del árbol: Debe al menos contener el elemento /Type con el valor /Page (singular!), una referencia indirecta al "nodo padre", un array denominado /MediaBox con las coordenadas esquina inferior izquierda (0,0) y esquina superior derecha (595,842) para un DIN-A4 donde visualizaremos los datos, y el identificador /Resources con un diccionario vacío:
  • 3 0 obj
    <</Type /Page
    /Parent 2 0 R
    /MediaBox [ 0 0 595 842 ]
    /Resources <<>>
    >>
    endobj				  
    				  

La Tabla de Referencias Cruzadas

Es la tabla donde tenemos una lista secuencial de los objetos que componen el documento. Comienza con la palabra xref seguida de una o varias subsecciones. Cada una de ellas, referencia el primer objeto de la sección y el número de objetos que contiene. Para especificar dónde se encuentra el objeto en el PDF, se emplean 10 dígitos con el offset en bytes desde el comienzo hasta el objeto, un espacio, 5 dígitos con el número de revisión y el carácter "f" si el objeto ha sido liberado o "n" si se trata de un nuevo objeto.

xref
0 4
0000000000 65535 f 
0000000010 00000 n 
0000000100 00000 n 
0000000182 00000 n				
				

Básicamente en la tabla de referencias cruzadas del ejemplo, comenzamos siempre el primer objeto (#1) con 65535 f, nos indica que el "primer objeto nuestro" (#2) y segundo para el fichero, se encuentra en un offset de 10 bytes desde el comienzo del documento, el segundo (#3) a 100 bytes y el tercero (#4) a 182 bytes.

Siempre que modifiquemos un fichero PDF es necesario actualizar la tabla de referencias cruzadas así como el trailer como veremos a continuación.

El Trailer

Basta con comenzar con la palabra clave trailer y un diccionario con el número de entradas que tiene la tabla de referencias cruzadas. También es necesario escribir una referencia indirecta al objeto que tiene el catálogo. Finaliza con una línea en blanco, y la palabra startxref seguida del offset donde comienza la tabla de referencias cruzadas finalizando con la etiqueta %%EOF para indicar el final del fichero:

trailer
<</Size 4
/Root 1 0 R
>>

startxref
213
%%EOF				
				

En concreto, el bloque Cuerpo + Tabla XREF + Trailer puede ser repetido más de una vez si se han realizado modificaciones en el documento original. Con eso podemos jugar al despiste como haremos en posteriores entregas.

Leer e interpretar un fichero PDF

Un lector de PDFs no tiene porqué leer el documento secuencialmente de arriba a abajo, sino que lo hace de una forma más compleja:

  • Lee la primera línea para obtener la versión del PDF para poder interpretar ciertas palabras clave empleadas
  • Va al final del documento para comprobar la etiqueta %%EOF y lee la línea anterior para saber el offset de la tabla de referencias cruzadas. Por eso, cuando descargamos un PDF "voluminoso" hasta que no se ha descargado no podemos visualizarlo. Si la tabla se encontrase corrupta, algunos lectores intentan reconstruirla a base de todos los objetos encontrados, lo que hace que sea muy lento el proceso.
  • Construye una tabla con los offsets de los objetos a los que hace referencia la tabla de referencias cruzadas.
  • Lee el diccionario del trailer para saber el nombre que contiene el catálogo que será el comienzo del documento.
  • Entonces comienza una lectura aleatoria del documento para leer la estructura global. Por ejemplo, del Catálogo -> Pages -> Page -> Objeto 1 -> etc...

Nuestro primer PDF

Tan sólo necesitamos un editor de texto (bloc de notas, vi, joe, nano o cualquier otro de vuestro gusto). También será necesario un editor hexadecimal para saber dónde tenemos los offsets, a no ser que seamos masocas y contemos los caracteres a mano también incluyendo el CR + LF y seamos capaces de saber si hay algún espacio, tabulador o caracteres que no se visualizan en el editor por ahí perdidos como suele pasar y que trastocarán todo nuestro fichero...

Copiaremos el siguiente texto en el editor de texto (ojo que los espacios en blanco son 1 byte y cuentan los finales de línea, líneas en blanco, etc...) y lo guardaremos con el nombre first.pdf (debe ocupar 435 bytes)

%PDF-v1.7
% Mi primer PDF hecho a mano por Academia MADESYP

1 0 obj
<</Type /Catalog
/Pages 2 0 R
>>
endobj

2 0 obj
<</Type /Pages
/Kids [ 3 0 R ]
/Count 1
>>
endobj

3 0 obj
<</Type /Page
/Parent 2 0 R
/MediaBox [ 0 0 595 842 ]
/Resources <<>>
>>
endobj

xref
0 4
0000000000 65535 f
0000000001 00000 n
0000000002 00000 n
0000000003 00000 n

trailer
<</Size 4
/Root 1 0 R
>>

startxref
123
%%EOF
				

Para aquell@s que lo quieran descargar, os lo dejamos en nuestro repositorio maligno. Como podemos observar, disponemos del encabezado, el cuerpo con tan sólo de los 3 objetos mínimos necesarios, la tabla de referencias cruzadas y el trailer. Vamos a completar ahora la tabla de referencias cruzadas de forma correcta para obtener los offsets de los objetos. Para ello, hoy vamos a usar hexedit en Windows para no os quejeis de tanto Debian ;-D

Abrimos el fichero creado con hexedit. Estaremos en la parte hexadecimal. Pulsamos la tecla del tabulador (->) e iremos a la parte "cristiana". Ahora, nos vamos a situar justo en el 1 del primer objeto (1 0 obj...) y apuntamos el offset que tiene (Cursor: 00000040 en Hexadecimal). Apuntamos 64 (su valor en decimal). Hacemos lo mismo con el objeto 2 0 obj... y apuntamos 119 (0x77), con el objeto 3 0 obj... y apuntamos 185 (0xB9) y con el objeto xref apuntando 0x11A (282). Ya sabemos los offsets de todo lo que nos interesa actualizar. Y como dicen que una imágen vale más que mil palabras, os lo dejo de forma gráfica:

Volvemos al editor de texto (aunque podemos hacerlo directamente también desde el hexedit) y vamos a actualizar la tabla de referencias cruzadas para indicar los offsets de los 3 objetos que hemos creado. El primero lo dejaremos siempre conforme se encuentra:

xref
0 4
0000000000 65535 f
0000000064 00000 n
0000000119 00000 n
0000000185 00000 n				
				

Ahora, vamos a modificar el trailer para indicarle a startxref el offset de dónde se encuentra (0x11A):

startxref
282
%%EOF				
				

Ya podemos guardar nuestro fichero y abrirlo en un visor de PDFs para ver el resultado. Os dejamos el resultado final en nuestro repositorio maligno. Bonito, no??? Excelente DIN-A4 vertical sin nada!!! En particular, vamos a intentar abrirlo con la última versión de Adobe Reader (en éstos momentos la 11.0.2); observar que si algún offset estuviera mal calculado (podeis cambiarlo ahora) al cerrar el fichero, dice que si queremos guardar los cambios. Respondemos afirmativamente y lo guardamos con otro nombre. Abrimos en un editor de textos el fichero y veremos que ha añadido "caracteres chinos" que en futuras entradas veremos cómo interpretarlos. En general, básicamente es una optimización de algunos objetos, y además, añade metadatos que pueden leerse en cristiano junto con la tabla de referencias cruzadas recalculada!!!

Mi segundo PDF

Animados por el excelente resultado conseguido XD, vamos a intentar poner algo de texto en el DIN-A4 que hemos creado antes. Partiremos del PDF básico que tenemos en el editor de texto.

Vamos a modificar el objeto 3 0 para añadir unos recursos y un contenido al mismo:

3 0 obj
<</Type /Page
/Parent 2 0 R
/MediaBox [ 0 0 595 842 ]
/Resources << /Font << /F1 5 0 R >> >>
/Contents 4 0 R
>>
endobj
				

Hemos añadido un elemento /Font con un diccionario que contiene una etiqueta F1 que se definirá en el objeto 5 0 por haber sido referenciado indirectamente. También contamos con el elemento /Contents que tendrá su contenido en el objeto 4 0 por haber sido referenciado igualmente.

Vamos ahora a añadir el nuevo objeto 5 0 y comentar cada entrada:

5 0 obj
<<
 /Type /Font
 /Subtype /Type1
 /BaseFont /Arial
>>
endobj				
				

Observamos que se trata de un diccionario con un elemento /Type con el valor /Font, a continuación vemos el subtipo especificado de los 7 disponibles en la especificación /Subtype /Type1 y por último, definimos la fuente base /BaseFont del tipo Arial.

Vamos con el objeto 4 0 que aunque parece el más sencillo por contener "sólo el texto" a mostrar, es necesario definir muchos más parámetros para el mismo:

4 0 obj
<< /Length 56 >>
stream
 BT
  /F1 50 Tf
  590 790 Td (Hola MADESYP!!!) Tj
 ET
endstream
endobj
				

Lo primero que vemos en el diccionario es un elemento /Length con el valor en bytes del contenido del stream que viene a continuación. La etiqueta BT significa "Begin Text" y la etiqueta ET "End Text" para definir el bloque de texto que vamos a mostrar. Hacemos referencia al nombre con el que hemos etiqueta el tipo de fuente /F1 seguido del tamaño y finalizado por la etiqueta Tf (consultar la documentación para ver cómo trabaja la pila). Por último, se especifican las coordenadas desde la esquina inferior izquierda del documento para mostrar el texto que hay entre las etiquetas Td y Tj que como es un string, requiere que el objeto se encuentre delimitado por los paréntesis.

Denotar que si el objeto es anterior, debe ir antes del objeto 5 0 por lógica (aunque no necesario como luego veremos) e igualmente podemos dejar espacios, tabulaciones, etc. para facilitar al principio la creación de bloques. Pero si ponemos dichos caracteres, el tamaño del stream será mayor y es necesario incluirlos en la longitud total. Con hexedit se puede seleccionar un bloque para saber su longitud. Si lo haceis todo a mano, no olvideis el carácter CR (0x0D) y LF (0x0A) que también cuentan para la longitud siendo cada uno 1 byte.

Ahora, nos tocará añadir en la tabla de refencias cruzadas el número de objetos que contiene junto con sus dos entradas para los nuevos objetos que hemos añadido:

xref
0 6
0000000000 65535 f
0000000064 00000 n
0000000119 00000 n
0000000185 00000 n
0000000004 00000 n
0000000005 00000 n
				

En nuestro caso, el fichero base empleado (puede descargarse de nuestro repo) o que podeis copiar (cuidado con los finales de línea y espacios en blanco que luego no veis) es el siguiente:

%PDF-v1.7
% Mi segundo PDF hecho a mano por Academia MADESYP

1 0 obj
<</Type /Catalog
/Pages 2 0 R
>>
endobj

2 0 obj
<</Type /Pages
/Kids [ 3 0 R ]
/Count 1
>>
endobj

3 0 obj
<</Type /Page
/Parent 2 0 R
/MediaBox [ 0 0 595 842 ]
/Resources << /Font << /F1 5 0 R >> >>
/Contents 4 0 R
>>
endobj

4 0 obj
<< /Length 56 >>
stream
 BT
  /F1 50 Tf
  590 790 Td (Hola MADESYP!!!) Tj
 ET
endstream
endobj

5 0 obj
<<
 /Type /Font
 /Subtype /Type1
 /BaseFont /Arial
>>
endobj

xref
0 6
0000000000 65535 f
0000000001 00000 n
0000000002 00000 n
0000000003 00000 n
0000000004 00000 n
0000000005 00000 n

trailer
<</Size 6
/Root 1 0 R
>>

startxref
123
%%EOF				
				

Nuestro último paso, será actualizar el valor de las entradas de xref, volver a buscar los offsets de los objetos 1, 2, 3, 4, 5 y 6 y el valor de entradas del trailer asi como el offset de startxref. En la tabla xref indicamos 6 para reflejar el número de entradas, actualizamos la tabla de referencias cruzadas con los offsets 65, 120, 186, 322 y 436. En la sección del trailer, cambiamos el /Size por 6 que son las nuevamente las entradas, buscamos el offset de xref que se encuentra en 514 y lo actualizamos en la penúltima línea:

...
xref
0 6
0000000000 65535 f
0000000065 00000 n
0000000120 00000 n
0000000186 00000 n
0000000322 00000 n
0000000436 00000 n

trailer
<</Size 6
/Root 1 0 R
>>

startxref
514
...
				

Por último, guardaremos el documento con el nombre second.pdf o podeis descargarlo de nuestro repositorio maligno.

Lo abrimos con Adobe Reader y por fin vemos algo!!! ¿Dónde??? Si os fijais, se ve un poco en la parte derecha... ¿Qué ha ocurrido? Que hemos especificado mal las coordenadas donde visualizaremos el texto. Editamos de nuevo el fichero y modificamos las coordenadas en el objeto 4 0 como las siguientes:

...				
  100 790 Td (Hola MADESYP!!!) Tj
...  
				

Volvemos a guardar el fichero con el nombre third.pdf o podeis volver a descargarlo de nuestro repositorio maligno.

Ahora si!!! Probar a jugar con las coordenadas para acostumbraros a los ejes. Si poneis cifras con menos de 3 dígitos, completar con 0s por la izquierda para no tener que recalcular los offsets. Por ejemplo, 091, 002, etc.

En el siguiente...

Comenzaremos con los filtros como ASCIIHexDecode, ASCII85Decode, LZWDecode, FlateDecode, RunLengthDecode, CCITTFaxDecode, JBIG2Decode, DCTDecode, JPXDecode y Crypt junto con herramientas que nos permitiran extraer el contenido de un fichero comprimido o codificado y comenzaremos con nuestros primeros códigos embebidos en JavaScript para quien quiera ir repasando.

Recordaros que en los Cursos especializados de Seguridad Informática y Administración de Sistemas que ofrecemos en Academia MADESYP realizamos y establecemos las contramedidas con todo esto y mucho más...

Ser buenos y no hagáis maldades!