lunes, 13 de abril de 2009

Utilizando dd en Linux

Sin duda uno de los programas open source más utilizados y nombrados en el mundo del análisis forense sea el binario dd. Esta pequeña utilidad nos vá a permitir realizar un duplicado exacto bit a bit de un medio de almacenamiento cualquiera. A lo largo de este artículo vamos a analizar las funcionalidades que nos ofrece y veremos algunos trucos básicos que pueden resultarnos de utilidad.

Introducción

Durante el desarrollo de una investigación forense podemos diferenciar varias fases entre las que se encontraría la de preservación de las evidencias. En esta etapa será preciso registrar todas las pruebas que se vayan encontrando y, la adquisición o duplicado de los medios de almacenamiento objeto de análisis es sin duda uno de los pasos más importantes; de esta forma el investigador podrá trabajar con los duplicados y garantizar así la integridad de las pruebas.

La norma más ampliamente aceptada para el proceso de duplicación de los datos no volátiles estipula que éstos deberán adquirirse en la capa de abstracción más baja, dado que con cada una de ellas (disco, fichero, volumen y aplicación tal y como precisa Brian Carrier) corremos el riesgo de ir perdiendo información.

Sin duda el nivel más bajo en que resultará posible realizar el duplicado de la información sera la copia sector a sector de los discos conectados a los sistemas objeto de análisis. Y aquí es donde resultará de enorme utilidad el uso del binario dd. Su funcionamiento es muy simple: lee un bloque de datos de un tamaño determinado del dispositivo de origen y lo escribe en el dispositivo destino, y así sucesivamente hasta agotar el contenido del dispositivo fuente; y todo ello sin alterar el original ni preocuparse de la naturaleza de los datos copiados.

Breves apuntes sobre dispositivos en Linux

La mayoría de las veces nos encontraremos ejecutando el comando dd desde un sistema Linux, ya sea mediante un LiveCD en el sistema objeto de análisis como desde la máquina del investigador, habiendo conectado previamente el disco que contiene las evidencias. Y para facilitar este proceso a aquellos que no estén muy familiarizados con Linux vamos a detenernos un momento para entender la nomenclatura utilizada a la hora de acceder a los discos y particiones.

Los identificadores para los discos IDE variarán en función del canal en que se encuentren, y también lo harán dependiendo de la posición que ocupen dentro de dicho canal. Resumiendo, quedaría algo así:

  • Canal IDE primario, disco maestro: /dev/hda
  • Canal IDE primario, disco esclavo: /dev/hdb
  • Canal IDE secundario, disco maestro: /dev/hdc
  • Canal IDE secundario, disco esclavo: /dev/hdd

Si ahora añadimos la posibilidad de que estos discos estén particionados tendremos que la primera partición del disco maestro dentro del canal IDE primario sería /dev/hda1, la segunda /dev/hda2, y así sucesivamente. Sólo incidir en que la primera partición lógica dentro de una partición extendida siempre será la número 5.

Por otra parte, si los discos son de tipo SCSI (p.e. un disco SATA o un dispositivo USB) sustituiremos el prefijo 'hd' por 'sd'. Ejemplos:
  • Primera partición del primer disco SCSI: /dev/sda1
  • Primera partición logica del tercer disco SCSI: /dev/sdc5

Parámetros básicos

Un ejemplo muy simple, ejecutado desde un sistema Linux, y en el cual volcaremos el contenido completo del disco maestro del canal IDE primario en un fichero dentro del directorio de trabajo actual, sería:
$ dd if=/dev/hda of=hda.img bs=512

El flag 'bs' permite indicar el tamaño de bloque en bytes utilizado por dd para realizar el proceso de lectura/escritura, y por defecto será de 512. Los parámetros 'if' y 'of' determinan, respectivamente, los ficheros de origen y destino utilizados en el proceso de copia.

Si el tamaño de la partición origen de la copia es múltiplo del tamaño de bloque indicado, el número de registros leídos/escritos irá seguido de '+0'. De otra forma dd rellenará el buffer de lectura con el número de bytes que resten, realizará la escritura de dichos bytes en el destino y en el resumen del resultado el número de registros leídos/escritos irá seguido de '+1'. Con un par de ejemplos será más fácil entenderlo:
$ dd if=fichero1.bin of=fichero2.bin bs=512
2+0 records in
2+0 records out
1024 bytes (1,0 kB) copied, 0,00417857 s, 245 kB/s

En el ejemplo anterior hemos duplicado un fichero de 1024 bytes. Dado que el tamaño del fichero es múltiplo del tamaño de bloque todos los registros leídos/escritos llenarán por completo el buffer utilizado por dd.
$ dd if=fichero1.bin of=fichero2.bin bs=512
2+1 records in
2+1 records out
1500 bytes (1,5 kB) copied, 0,000364299 s, 4,1 MB/s

Ahora hemos duplicado un fichero de 1500 bytes. Evidentemente el tamaño del fichero no es múltiplo del tamaño de bloque indicado, por lo que será necesario leer/escribir 2 registros completos de 512 bytes y 1 último registro de tan sólo 476 bytes.

El tamaño de bloque utilizado para la lectura/escritura e indicado mediante el flag 'bs' es una potencia de 2 con un valor mínimo igual a 512, e influirá notablemente en el rendimiento y velocidad del proceso. Para la mayoría de los casos un valor entre los 2 y los 8 kb proporcionará los mejores resultados. Si lo aplicamos, el primer comando mostrado quedaría como sigue:
$ dd if=/dev/hda of=hda.img bs=4k

Los parámetros 'ibs' y 'obs' nos permitirán especificar, respectivamente, el tamaño de bloque utilizado en el proceso de lectura y escritura.

Asegurando la integridad de los datos

La duplicación de los datos no garantiza por sí sola la integridad. Un método ampliamente aceptado consistiría en calcular un hash criptográfico para el disco que contiene las evidencias, el cual podría ser utilizado posteriormente para comprobar que no haya sufrido alteraciones. Por otra parte dicho hash también serviría para garantizar la validez de las copias realizadas, eliminando así cualquier sospecha de un posible error cometido durante el proceso de duplicación.

Para realizar el cálculo de dicho hash criptográfico deberemos utilizar otras herramientas dado que el binario dd original no incluye dicha funcionalidad. No obstante, y debido a su relación directa con el proceso de duplicación he pensado que sería adecuado mencionarlo.

Hasta el momento la suma MD5 era un valor de hash ampliamente aceptado como valor de comprobación, y aún hoy lo sigue siendo. No obstante, y debido a las últimas investigaciones relacionadas con la existencia de colisiones inherentes al algoritmo de suma MD5 mi recomendación particular sería la de obtener otro tipo de hash, o incluso calcular varios tipos diferentes.

Para obtener el hash SHA1 correspondiente al contenido del disco maestro del canal IDE primario, y realizar simultáneamente el duplicado de su contenido en un fichero dentro del directorio actual, ejecutaremos:
$ dd if=/dev/hda bs=4k | tee hda.img | shasum

Ahora sólo restaría comparar el hash obtenido como resultado del comando anterior con el correspondiente al fichero de volcado, para garantizar así que no se hayan producido errores en el proceso de generación del duplicado.

Y seguimos con los parámetros

En ocasiones quizás nos resulte útil dividir la imagen del disco duplicado en diferentes ficheros que se correspondan con las diferentes particiones que lo componen. Este proceso también puede realizarse con dd, pero primero necesitaremos de la ayuda del comando fdisk. Utilizaremos fdisk con los flags '-lu' para obtener un mapa de la estructura de las diferentes particiones, utilizando sectores como unidad predeterminada. Por ejemplo:
$ fdisk -lu disk.img
 
Disk hda.img: 0 heads, 0 sectors, 0 cylinders
Units = sectors of 1 * 512 bytes
 
Device Boot Start End Blocks Id System
disk.img1 * 63 16064999 8032468+ 83 Linux
disk.img2 16065000 32129999 8032500 7 NTFS
disk.img3 32130000 40162499 4016250 83 Linux
disk.img4 40162500 58621184 9229342+ 5 Extended
disk.img5 40162563 48194999 4016218 83 Linux

Tenemos 3 particiones primarias y 1 partición extendida la cual, a su vez, contiene una partición lógica. Para este caso nos valdremos de los flags:
  • skip=sectorinicial
    Permite indicar el sector que se utilizará como marca de inicio para realizar el proceso de duplicación, de forma que todos los anteriores sean omitidos.

  • count=numsectores
    Especifica el número de sectores que serán incluidos en el fichero destino obtenido como resultado del proceso de copia.

Sabiendo ésto, y teniendo en cuenta la salida de fdisk incluida anteriormente, el comando necesario para extraer el contenido de la primera partición sería:
$ dd if=disk.img of=part1.img bs=512 skip=63 count=16064937

El valor indicado para 'skip' permite saltarnos el contenido de la tabla de particiones del disco, ubicada en el primer sector, y los sectores no asignados que existen antes de la primera partición. El valor indicado para 'count' se corresponderá con el resultado de la siguiente fórmula:
último sector - 63 + 1  -->  1606499 - 63 + 1 = 16064937

Si tenemos en cuenta que deberemos saltarnos la partición extendida, y que dentro de ésta, a su vez, también existirá una tabla de particiones y varios sectores no asignados, el resto de comandos quedarían como sigue:
$ dd if=disk.img of=part2.img bs=512 skip=16065000 count=16065000 
$ dd if=disk.img of=part3.img bs=512 skip=32130000 count=8032500
$ dd if=disk.img of=part4.img bs=512 skip=40162563 count=8032437

Dividiendo y comprimiendo las imágenes

Otra posibilidad a tener en cuenta será la necesidad de dividir la imagen resultante en trozos más pequeños. Se me ocurre, por ejemplo, el caso de tener que almacenar el duplicado a analizar en un sistema de ficheros FAT32, como el que viene por defecto en la mayoría de discos USB externos de 2,5'', y que tiene la particularidad de no poder almacenar archivos cuyo tamaño exceda los 4 GB.

Vamos a generar 3 ficheros con un tamaño de unos 3,9GB cada uno, por lo que no se verán afectados por las limitaciones impuestas por FAT32:
$ dd if=/dev/hda of=/mnt/sda1/hda.dd.00 bs=1k count=4000000
$ dd if=/dev/hda of=/mnt/sda1/hda.dd.01 bs=1k count=4000000 skip=4096000000
$ dd if=/dev/hda of=/mnt/sda1/hda.dd.02 bs=1k count=4000000 skip=8192000000

Y si nos gustan más los scripts:
#!/bin/bash
#########################################
source=/dev/hda
target=/mnt/sda1/hda.dd
outsize=4000000
numfile=3
#########################################
for ((a=0; a <= numfile ; a++))
do
dd if=$source of=$target.0$a bs=1k \
count=$outsize skip=$((outsize*1024*a));
done
#########################################

El proceso anterior puede mejorarse, si por mejorar entendemos la posibilidad de generar el resultado con un único comando. Para ello vamos a utilizar el comando split:
$ dd if=/dev/hda | split -d -b 4096000000 - /mnt/sda1/hda.dd.

Independientemente del método utilizado para generar las imágenes resultantes, podremos juntar todos los trozos en un único fichero para poder analizarlo a posteriori mediante el siguiente comando:
$ cat /mnt/sda1/hda.dd.* > /home/neofito/hda.dd

Supongamos ahora que el problema no lo tenemos tanto en cuanto a una limitación claramente definida para el tamaño del fichero, sino más bién en cuanto al espacio libre de que disponemos para almacenar las imágenes. En estos casos nos será tremendamente útil contar con la posibilidad de comprimir el resultado:
$ dd if=/dev/hda bs=4k | gzip -c > /home/neofito/hda.dd.gz

Opciones para el manejo de errores

De forma predeterminada, si dd encuentra algún error en el proceso de lectura de los datos en el dispositivo de origen finalizará su ejecución inmediatamente. Para modificar este comportamiento puede utilizarse la opción 'conv=noerror', de forma que dd mostrará un mensaje advirtiendo del error pero no detendrá su ejecución. El problema de utilizar esta opción es que dd ignorará los bloques defectuosos de forma que, en la imagen resultante, se alterarán las posiciones de los datos, además de que el dispositivo de origen y la imagen no tendrán el mismo tamaño.

Para evitar este problema podemos utilizar la opción 'conv=noerror,sync'. De esta forma dd siempre escribirá los bloques con el tamaño indicado, rellenando el buffer de lectura con ceros como consecuencia del uso de 'sync'. Pero como puede advertirse a simple vista ésto provocará problemas si el tamaño del dispositivo de origen no es múltiplo del tamaño de bloque indicado. Veámoslo con un par de ejemplos:
$ dd if=fichero1.bin of=fichero2.bin bs=512
2+1 records in
2+1 records out
1500 bytes (1,5 kB) copied, 0,000364299 s, 4,1 MB/s

Tal y como hicimos anteriormente hemos duplicado un fichero de 1500 bytes. Dado que el tamaño del fichero no es múltiplo del tamaño de bloque indicado será necesario leer/escribir 2 registros completos de 512 bytes y 1 último registro de tan sólo 476 bytes.
$ dd if=fichero1.bin of=fichero2.bin bs=512 conv=sync
2+1 records in
3+0 records out
1536 bytes (1,5 kB) copied, 0,000224331 s, 6,8 MB/s

De forma similar hemos duplicado un fichero de 1500 bytes, pero en este caso y dado que hemos utilizado la opción 'conv=sync' se han leído 2 registros enteros de 512 bytes y un último registro de tan sólo 476 bytes, pero por el contrario se han escrito 3 registros completos de 512 bytes.

Será preciso tener muy en cuenta ésta particularidad durante el proceso de duplicación de las evidencias.

Duplicación a través de la red

Supongamos ahora que hemos iniciado el sistema sospechoso con un LiveCD y que, por algún motivo, no podemos almacenar el duplicado del disco en el mismo equipo. En estos casos podemos enviar la imagen a través de la red hacia el sistema donde realizaremos el análisis de las evidencias.

Imaginemos ahora que hemos conectado nuestro sistema utilizado para el análisis, llamémosle B, al sistema sospechoso, llamémosle A, mediante un cable ethernet cruzado. El sistema B tiene la IP 192.168.1.2/24 y un sistema Linux instalado. El sistema A tiene la IP 192.168.1.1/24. En este ejemplo utilizaremos el binario de netcat como medio de transporte para la imagen y también realizaremos la compresión de los datos enviados y su posterior descompresión en el destino.

Primero ejecutaremos un netcat a la escucha en el sistema B, y redirigiremos la salida capturada hacia un fichero:
$ nc -l -p 9000 | gzip -d -c > hda.dd

Ahora en el sistema A realizaremos la captura del contenido del disco y, también mediante netcat, la redirigiremos al sistema B:
$ dd if=/dev/hda bs=512 | gzip -c | nc 192.168.1.2 9000

Si en lugar de conectar ambos equipos mediante un cable ethernet cruzado tenemos que utilizar un medio inseguro como puede ser la propia red sospechosa, podemos garantizar la integridad de los datos utilizando un tunel cifrado. En este caso, y suponiendo que exista un servidor ssh en el sistema B (destino) del ejemplo anterior, bastará con ejecutar el siguiente comando en el sistema A (origen):
$ dd if=/dev/hda | ssh neofito@192.168.1.2 "(cat > hda.dd)"

Simple, seguro, efectivo y elegante. ¿Alguien dá mas? :-)

Bibliografía

File System Forensics Analysis
Brian Carrier
Adisson Wesley Professional

Windows Forensics: The field guide for conducting computer investigations
Chad Steel
John Wiley & Sons

Incident Response & Computer Forensics, Second Edition
Chris Prosise and Kevin Mandia
McGraw-Hill/Osborne

The Sleuth Kit Informer: Issue 2

The Sleuth Kit Informer: Issue 11

Learn The DD Command

LinuxQuestions.org: Linux Wiki - Dd

DD and Computer Forensics

11 comentarios:

Giorgio Grappa dijo...

Gracias por una descripción tan detallada de "dd". Estaba buscando información sobre si podría usarlo para obtener una imagen completa de un lápiz USB dividido en cuatro particiones, y parece ser que sí. Todavía no he probado a copiar la imagen en otro lápiz, pero, en principio, tendria que funcionar.

Me ha gustado mucho el nivel de tu bloc, lo añado a mi Akregator.

Saludos.

neofito dijo...

Me alegro de que el articulo te sea de utilidad, y gracias por los comentarios.

Ultimamente ando bastante liado pero espero tener tiempo en breve para seguir publicando con mas frecuencia.

Ssaludos

Anónimo dijo...

Impresionante, simplemente impresionante.

Es la primera vez que veo esta utilidad explicada con ejemplos muy prácticos.

Mi enhorabuena, y como dijo Giorgio, ya tienes otro lector fijo.

Saludos.

Anónimo dijo...

Para quien no la conozca, yo suelo usar dc3dd, una version mejorada de dd que esta creada y sustentada por una agencia estadounidense.
Incluye mejoras, asi que denle un vistazo :)

Pablo dijo...

La verdad que muy valioso el post sobre el comando dd. Lo conosco por haberlo utilizado no hace mucho en un trabajo que tuve que hacer para una escuela. En donde tuvimos que clonar varias maquinas, y la verdad que nos sirvio muy bien. Ya que nos ahorro muchisimo trabajo y quedo todo muy bien. Igual es muy practico conocer todo estos detalles nuevos. Se ve que es una gran herramienta.

Anónimo dijo...

Desearia saber si este comando lo pudo utilizar en c como una función, sin utilizar el system, exec y su derivados, si es así me podrian decir las funciones y libreria que se necesitan

neofito dijo...

No conozco ninguna libreria que te permita utilizar las funcionalidades del comando dd directamente en tus programas, lo siento.

No obstante si conozco algunas alternativas que te permitiran generar volcados en otros formatos que casi pueden considerarse standards de facto:

afflib
libewf

Saludos y suerte, ya nos contaras como te ha ido

gcorrea7 dijo...

Muchas felicidades por la aportación, me ha quedado muy claro. Saludos

elperrito dijo...

Magnífico artículo. Interesante, claro y con ejemplos. Y elegante, sí señor.

Enhorabuena

elperrito dijo...

Gracias por el artículo. Muy ilustrativo.

Enhorabuena

Wow dijo...

Muy útil y bien explicado