Cómo examinar archivos binarios desde la línea de comandos

¿Tienes un archivo misterioso? El comando file de Linux te dirá rápidamente qué tipo de archivo es. Sin embargo, si se trata de un archivo binario, puedes obtener más información al respecto.

file tiene toda una serie de compañeros estables que te ayudarán a analizarlo. Te mostraremos cómo usar algunas de estas herramientas.

Identificar tipos de archivo

Los archivos generalmente tienen características que permiten que los paquetes de software identifiquen qué tipo de archivo es y qué representan los datos que contiene. No tendría sentido intentar abrir un archivo PNG en un reproductor de música MP3. Por lo tanto, es útil y pragmático que un archivo lleve algún tipo de identificación.

Esto podría ser unos pocos bytes de firma al comienzo del archivo. Esto permite que un archivo sea explícito sobre su formato y contenido.

A veces, el tipo de archivo se infiere de un aspecto distintivo de la organización interna de los datos en sí. Esto es conocido como la arquitectura del archivo.

Algunos sistemas operativos, como Windows, están completamente guiados por la extensión de un archivo. Puedes llamarlo crédulo o de confianza, pero Windows asume que cualquier archivo con la extensión DOCX realmente es un archivo de procesamiento de texto DOCX. Linux no es así, como pronto verás. Quiere pruebas y busca dentro del archivo para encontrarlo.

Las herramientas descritas aquí ya estaban instaladas en las distribuciones Manjaro 20, Fedora 21 y Ubuntu 20.04 que utilizamos para investigar este artículo. Comencemos nuestra investigación usando el comando file.

Usando el comando file

Tenemos una colección de diferentes tipos de archivos en nuestro directorio actual. Son una mezcla de documentos, código fuente, ejecutables y archivos de texto.

El comando ls nos mostrará lo que hay en el directorio, y la opción –hl (tamaños legibles por humanos, listado largo) nos mostrará el tamaño de cada archivo:

Probemos file en algunos de estos y veamos qué obtenemos:

Los tres formatos de archivo están correctamente identificados. Siempre que sea posible, file nos da un poco más de información. Nos informa que el archivo PDF está en el formato de la versión 1.5.

Incluso si cambiamos el nombre del archivo ODT para que tenga una extensión con el valor arbitrario de XYZ, el archivo aún se identifica correctamente. Tanto en el explorador de archivos como en la línea de comandos con file.

Dentro del navegador de archivos Files, se le da el icono correcto. En la línea de comando, file ignora la extensión y mira dentro del archivo para determinar su tipo:

El uso file en archivos multimedia, como archivos de imagen y música, generalmente proporciona información sobre su formato, codificación, resolución, etc.

Curiosamente, incluso con archivos de texto sin formato, file no juzga el archivo por su extensión. Por ejemplo, si tienes un archivo con la extensión “.c”, que contiene texto plano estándar pero no código fuente, file no lo confunde. Este no lo identifica como un archivo de código fuente C genuino:

file identifica correctamente el encabezado del archivo (“.h”) como parte de una colección de archivos de código fuente C. Este sabe que el makefile es un script.

Usar file con archivos binarios

Los archivos binarios son más una “caja negra” que otros. Los archivos de imagen se pueden ver, los de sonido se pueden reproducir y los archivos de documentos se pueden abrir con el software adecuado. Sin embargo, los archivos binarios son más desafiantes.

Por ejemplo, los archivos “hello” y “wd” son ejecutables binarios. Son programas. El archivo llamado “wd.o” es un archivo objeto.

Cuando un compilador compila el código fuente, se crean uno o más archivos de objeto. Estos contienen el código de máquina que la computadora eventualmente ejecutará cuando se ejecute el programa terminado, junto con información para el enlazador.

El vinculador verifica cada archivo de objeto para llamadas de función a bibliotecas. Los vincula a cualquier biblioteca que use el programa. El resultado de este proceso es un archivo ejecutable.

El archivo “watch.exe” es un ejecutable binario que ha sido compilado para ejecutarse en Windows:

Tomando el último primero, file nos dice que el archivo “watch.exe” es un programa de consola ejecutable PE32+, para la familia de procesadores x86 en Microsoft Windows.

PE significa formato ejecutable portátil, que tiene versiones de 32 y 64 bits. El PE32 es la versión de 32 bits, y el PE32 + es la versión de 64 bits.

Los otros tres archivos están identificados como archivos de formato ejecutable y enlazable (ELF). Este es un estándar para archivos ejecutables y archivos de objetos compartidos, como las bibliotecas. En breve veremos el formato de encabezado ELF.

Lo que podría llamar tu atención es que los dos ejecutables (“wd” y “hello”) se identifican como objetos compartidos de Linux Standard Base (LSB). Por su parte, el archivo de objetos “wd.o” se identifica como un LSB reubicable. La palabra ejecutable es obvia en su ausencia.

Archivos reubicables

Los archivos de objetos son reubicables, lo que significa que el código dentro de ellos se puede cargar en la memoria en cualquier ubicación. Los ejecutables se enumeran como objetos compartidos porque han sido creados por el vinculador a partir de los archivos de objetos. Por lo tanto, heredan esta capacidad.

Esto permite que el   sistema de asignación aleatoria del diseño del espacio de direcciones (ASMR) cargue los ejecutables en la memoria en direcciones que elija. Los ejecutables estándar tienen una dirección de carga codificada en sus encabezados, que dictan dónde se cargan en la memoria.

ASMR es una técnica de seguridad. Cargar ejecutables en la memoria en direcciones predecibles los hace susceptibles de ataque. Esto se debe a que los atacantes siempre conocerán sus puntos de entrada y la ubicación de sus funciones. Las posiciones ejecutables independientes (PIE) posicionados en una dirección aleatoria supera esta susceptibilidad.

Si compilamos nuestro programa con el compilador gcc y brindamos la opción -no-pie, generaremos un ejecutable convencional.

La opción –o (archivo de salida) nos permite proporcionar un nombre para nuestro ejecutable:

Usaremos file en el nuevo ejecutable y veremos qué ha cambiado:

El tamaño del ejecutable es el mismo que antes (17 KB):

El binario ahora se identifica como un ejecutable estándar. Lo hacemos solo con fines de demostración. Si compilas aplicaciones de esta manera, perderá todas las ventajas del ASMR.

¿Por qué es un ejecutable tan grande?

Nuestro programa de ejemplo hello es de 17 KB, por lo que difícilmente podría llamarse grande, pero todo es relativo. El código fuente es de 120 bytes:

¿Qué aumenta el binario si todo lo que hace es imprimir una cadena en la ventana de la terminal? Sabemos que hay un encabezado ELF, pero solo tiene 64 bytes para un binario de 64 bits. Claramente, debe ser otra cosa:

Analicemos el binario con el comando strings como un simple primer paso para descubrir qué hay dentro de él. Lo canalizaremosless:

Hay muchas cadenas dentro del binario, además del “Hello, Geek world! de nuestro código fuente. La mayoría de ellos son etiquetas para regiones dentro del binario, y los nombres y la información de enlace de objetos compartidos. Estos incluyen las bibliotecas y funciones dentro de esas bibliotecas, de las cuales depende el binario.

Explorando

El comando ldd nos muestra las dependencias de objetos compartidos de un binario:

Hay tres entradas en la salida, y dos de ellas incluyen una ruta de directorio (la primera no):

  • linux-vdso.so: Virtual Dynamic Shared Object (VDSO) es un mecanismo de kernel. Este permite que un binario de espacio de usuario acceda a un conjunto de rutinas de espacio de kernel. Esto evita la sobrecarga de un cambio de contexto desde el modo de kernel de usuario. Los objetos compartidos VDSO se adhieren al formato de formato ejecutable y enlazable (ELF), esto les permite vincularse dinámicamente al binario en tiempo de ejecución. El VDSO se asigna dinámicamente y aprovecha ASMR. La capacidad de VDSO es proporcionada por la Biblioteca GNU C estándar si el kernel admite el esquema ASMR.
  • libc.so.6: El objeto compartido de la Biblioteca GNU C.
  • /lib64/ld-linux-x86-64.so.2: este es el enlazador dinámico que el binario quiere usar. El enlazador dinámico interroga al binario para descubrir qué dependencias tiene. Lanza esos objetos compartidos en la memoria. Prepara el binario para ejecutarse y poder encontrar y acceder a las dependencias en la memoria. Luego, inicia el programa.

El encabezado ELF

Podemos examinar y decodificar el encabezado ELF usando la utilidad readelf y la opción –h (encabezado de archivo):

El encabezado se interpreta para nosotros.

El primer byte de todos los archivos binarios ELF se establece en el valor hexadecimal 0x7F. Los siguientes tres bytes se establecen en 0x45, 0x4C y 0x46. El primer byte es un indicador que identifica el archivo como un binario ELF. Para aclarar esto, los siguientes tres bytes deletrean “ELF” en ASCII:

  • Class: indica si el binario es un ejecutable de 32 o 64 bits (1 = 32, 2 = 64).
  • Data: indica la endianness en uso. La codificación endian define la forma en que se almacenan los números multibyte. En la codificación big-endian, un número se almacena primero con sus bits más significativos. En la codificación little-endian, el número se almacena primero con sus bits menos significativos.
  • Version: La versión de ELF (actualmente, es 1).
  • OS/ABI: representa el tipo de interfaz binaria de la aplicación en uso. Esto define la interfaz entre dos módulos binarios, como un programa y una biblioteca compartida.
  • ABI Version: La versión de ABI.
  • Type: el tipo de binario ELF. Los valores comunes son ET_REL para un recurso reubicable (como un archivo de objeto). ET_EXEC para un ejecutable compilado con el indicador -no-pieET_DYN para un ejecutable compatible con ASMR.
  • Machine: La arquitectura del conjunto de instrucciones. Esto indica la plataforma de destino para la cual se creó el binario.
  • Version: siempre establecido en 1, para esta versión de ELF.
  • Entry Point Address: la dirección de memoria dentro del binario en el que comienza la ejecución.

Demás opciones

Las otras entradas son tamaños y números de regiones y secciones dentro del binario para que se puedan calcular sus ubicaciones.

Un vistazo rápido a los primeros ocho bytes del binario con hexdump mostrará el byte de firma. Asimismo, la cadena “ELF” en los primeros cuatro bytes del archivo. La opción –C (canónica) nos da la representación ASCII de los bytes junto con sus valores hexadecimales. Por otra parte, la opción –n (número) nos permite especificar cuántos bytes queremos ver:

objdump y la vista detallada

Si deseas ver el detalle esencial, puedes usar el comando objdump con la opción –d (desensamblar):

Esto desensambla el código de máquina ejecutable y lo muestra en bytes hexadecimales junto con el equivalente en lenguaje ensamblador. La ubicación de la dirección del primer byte en cada línea se muestra en el extremo izquierdo.

Esto solo es útil si puedes leer el lenguaje ensamblador, o tienes curiosidad por saber qué sucede en segundo plano. Hay mucha salida, por lo que la canalizamos con less.

Compilar y vincular

Hay muchas formas de compilar un binario. Por ejemplo, el desarrollador elige si incluir información de depuración. La forma en que se vincula el binario también juega un papel en su contenido y tamaño.

Si las referencias binarias comparten objetos como dependencias externas, será más pequeño que uno con el que las dependencias se vinculan estáticamente.

La mayoría de los desarrolladores ya conocen los comandos que hemos cubierto aquí. Para otros, sin embargo, ofrecen algunas formas fáciles de explorar y ver qué hay dentro de la caja negra binaria.