Ingeniería inversa e informática forense

Listar datos de la cabecera ELF de un binario

readelf -h <binario>

Entre la información disponible se encuentra el punto de entrada, la arquitectura, la candidad de secciones, el sistema operativo, el formato de datos (little endian, big endian), el tipo de binario, y otros datos posiblemente útiles dependiendo del contexto.

Dump hexadecimal de un archivo

xxd <binario>

Podemos limitar la cantidad de líneas que vemos enviando la salida a head . Por ejemplo, para ver las cinco primeras:

$ xxd main | head -5
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 3e00 0100 0000 4005 0000 0000 0000  ..>.....@.......
00000020: 4000 0000 0000 0000 2819 0000 0000 0000  @.......(.......
00000030: 0000 0000 4000 3800 0900 4000 1d00 1c00  ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000  ........@.......

Los primeros bytes hacen usualmente la firma del formato del archivo. Esta secuencia de bytes suele ser conocida como números mágicos. Por ejemplo, la secuencia de bytes 7F 45 4C 46 identifica al formato ELF, los ejecutables típicos de Linux. La secuencia de bytes 89 50 4E 47 0D 0A 1A 0A , por otro lado, identifica al formato PNG.

Averiguar tipo de archivo

file <archivo>

El programa file reconoce el tipo de datos contenido en un archivo. Para ello una de las cosas que evalua es la secuencia de bytes iniciales del archivo. Si el archivo es una imagen PNG, obtendremos algo como:

$ file imagen.png
imagen.png: PNG image data, 181 x 172, 8-bit/color RGB, non-interlaced

Si lo probamos con un binario ejecutable el resultado será:

$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4cb0d8f9a67589a83b33ea0a2631841c7ae26fe4, not stripped

Sabemos que el formato de este archivo ejecutable es el ELF, un ejecutable para Linux y arquitectura Intel x86 de 64 bits.

Escritura de bytes arbitrarios

printf "\xAA\xBB\xCC\xDD"

Este ejemplo utiliza printf para generar una secuencia de cuatro bytes, concretamente: 0xAA , 0xBB , 0xCC , 0xDD . Esta técnica puede ser utilizada para generar exploits. El siguiente ejemplo genera una secuencia de 256 caracteres A , seguida de una secuencia de cuatro caracteres B = 0x42 :

echo -ne $(printf "A%.0s" {1..256})$(printf "\x42\x42\x42\x42")

Parchear un binario en un offset específico

https://stackoverflow.com/questions/4783657/cli-write-byte-at-address-hexedit-modify-binary-from-the-command-line

El siguiente ejemplo utiliza dd para sobrescribir un binario. Esto puede servir para realizar parches, sobrescribir una dirección, reemplazar alguna instrucción, etc. El comando sobrescribe el binario, ¡realizar una copia de seguridad!

printf "\x42\x43\x44\x45" | dd of=binario bs=1 seek=24 count=4 conv=notrunc

Si analizaramos el binario con readelf , notaríamos algo como lo siguiente:

$ readelf -h binario
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                        ELF64
  Data:                         2's complement, little endian
  [...]
  Entry point address:          0x45444342
  [...]

Nótese que, por ser una arquitectura little endian, los bytes más significativos están en posiciones más altas de memoria.

También es posible observar el cambio con un dump hexadecimal del archivo:

$ xxd main | head -5
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
00000010: 0300 3e00 0100 0000 4243 4445 0000 0000  ..>.....BCDE....
00000020: 4000 0000 0000 0000 5019 0000 0000 0000  @.......P.......
00000030: 0000 0000 4000 3800 0900 4000 1d00 1c00  ....@.8...@.....
00000040: 0600 0000 0400 0000 4000 0000 0000 0000  ........@.......

Listar símbolos de un ejecutable

nm <binario>

Los símbolos de un binario ejecutable son producto del proceso de compilación / enlace, y usualmente están presentes salvo que se haya explicitado que deben ser eliminados durante el proceso de generación del binario. Un listado de símbolos de un programa sencillo, un hello world escrito en C y compilado en Ubuntu con gcc , puede verse de la siguiente forma:

$ nm main
0000000000201010 B __bss_start
0000000000201010 b completed.7696
                 w __cxa_finalize@@GLIBC_2.2.5
0000000000201000 D __data_start
0000000000201000 W data_start
0000000000000570 t deregister_tm_clones
0000000000000600 t __do_global_dtors_aux
0000000000200dc0 t __do_global_dtors_aux_fini_array_entry
0000000000201008 D __dso_handle
0000000000200dc8 d _DYNAMIC
0000000000201010 D _edata
0000000000201018 B _end
0000000000000704 T _fini
0000000000000640 t frame_dummy
0000000000200db8 t __frame_dummy_init_array_entry
000000000000088c r __FRAME_END__
0000000000200fb8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
0000000000000724 r __GNU_EH_FRAME_HDR
00000000000004f0 T _init
0000000000200dc0 t __init_array_end
0000000000200db8 t __init_array_start
0000000000000710 R _IO_stdin_used
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
0000000000000700 T __libc_csu_fini
0000000000000690 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000000662 T main
                 U printf@@GLIBC_2.2.5
00000000000005b0 t register_tm_clones
000000000000064a T say_hello_world
0000000000000540 T _start
0000000000201010 D __TMC_END__

Vemos que hay muchos símbolos, muchos introducidos por el proceso de compilación y enlace. Usualmente sirven para darnos una idea de las funciones que realiza el programa. Por ejemplo, en este caso vemos una función main , y adicionalmente vemos una función say_hello_world , la cuál podemos suponer que efectivamente imprime el mensaje en pantalla; adicionalmente, podemos suponer que dicha función utiliza printf , ya que ésta última se encuentra referenciada en la lista de símbolos.

Podemos eliminar los símbolos pasando los flags adecuados al compilador. Alternativamente, podemos utilizar la herramienta strip desde la terminal:

strip <binario>

Si ejecutamos strip main y luego volvemos a ejecutar nm main , observaremos el siguiente mensaje:

nm: main: no symbols

Listar strings de un binario

strings -t x <binario>

Los strings legibles no son más que secuencias de bytes, cada uno dentro de un cierto rango en la tabla ASCII; en el caso puntual de C, son secuencias de byes terminadas con un byte nulo. El comando strings escanea el binario en busca de secuencias de bytes embebidas, y las muestra en pantalla. Por ejemplo, para el hello world del ejemplo anterior (todavía con símbolos) podemos ver algo como lo siguiente:

$ strings -t x main
    238 /lib64/ld-linux-x86-64.so.2
    361 libc.so.6
    36b printf
    372 __cxa_finalize
    381 __libc_start_main
    393 GLIBC_2.2.5
    39f _ITM_deregisterTMCloneTable
    3bb __gmon_start__
    3ca _ITM_registerTMCloneTable
    690 AWAVI
    697 AUATL
    6ea []A\A]A^A_
    714 Hello, world!
    7cf ;*3$"
   1010 GCC: (Ubuntu 7.3.0-16ubuntu3) 7.3.0
   1639 crtstuff.c
   1644 deregister_tm_clones
   1659 __do_global_dtors_aux
   166f completed.7696
   167e __do_global_dtors_aux_fini_array_entry
   16a5 frame_dummy
   16b1 __frame_dummy_init_array_entry
   16d0 main.c
   16d7 __FRAME_END__
   16e5 __init_array_end
   16f6 _DYNAMIC
   16ff __init_array_start
   1712 __GNU_EH_FRAME_HDR
   1725 _GLOBAL_OFFSET_TABLE_
   173b __libc_csu_fini
   174b _ITM_deregisterTMCloneTable
   1767 _edata
   176e printf@@GLIBC_2.2.5
   1782 __libc_start_main@@GLIBC_2.2.5
   17a1 __data_start
   17ae __gmon_start__
   17bd __dso_handle
   17ca _IO_stdin_used
   17d9 __libc_csu_init
   17e9 say_hello_world
   17f9 __bss_start
   1805 main
   180a __TMC_END__
   1816 _ITM_registerTMCloneTable
   1830 __cxa_finalize@@GLIBC_2.2.5
   184d .symtab
   1855 .strtab
   185d .shstrtab
   1867 .interp
   186f .note.ABI-tag
   187d .note.gnu.build-id
   1890 .gnu.hash
   189a .dynsym
   18a2 .dynstr
   18aa .gnu.version
   18b7 .gnu.version_r
   18c6 .rela.dyn
   18d0 .rela.plt
   18da .init
   18e0 .plt.got
   18e9 .text
   18ef .fini
   18f5 .rodata
   18fd .eh_frame_hdr
   190b .eh_frame
   1915 .init_array
   1921 .fini_array
   192d .dynamic
   1936 .data
   193c .bss
   1941 .comment

Observamos que hay una cantidad bastante grande, y entre ellas se encuentra efectivamente el mensaje "Hello, world!"; muchas de estas cadenas son introducidas por el proceso de compilación y enlace. Eliminando los símbolos, sin embargo, podemos observar lo siguiente:

$ strip main && strings -t x main
    238 /lib64/ld-linux-x86-64.so.2
    361 libc.so.6
    36b printf
    372 __cxa_finalize
    381 __libc_start_main
    393 GLIBC_2.2.5
    39f _ITM_deregisterTMCloneTable
    3bb __gmon_start__
    3ca _ITM_registerTMCloneTable
    690 AWAVI
    697 AUATL
    6ea []A\A]A^A_
    714 Hello, world!
    7cf ;*3$"
   1010 GCC: (Ubuntu 7.3.0-16ubuntu3) 7.3.0
   1035 .shstrtab
   103f .interp
   1047 .note.ABI-tag
   1055 .note.gnu.build-id
   1068 .gnu.hash
   1072 .dynsym
   107a .dynstr
   1082 .gnu.version
   108f .gnu.version_r
   109e .rela.dyn
   10a8 .rela.plt
   10b2 .init
   10b8 .plt.got
   10c1 .text
   10c7 .fini
   10cd .rodata
   10d5 .eh_frame_hdr
   10e3 .eh_frame
   10ed .init_array
   10f9 .fini_array
   1105 .dynamic
   110e .data
   1114 .bss
   1119 .comment

Observamos que es una cantidad más reducida, aunque hay todavía una cantidad considerable de strings que no son precisamente símbolos y que también son introducidos por el proceso de generación del binario. Entre estos strings se encuentran los nombres de sección (e.g. .data , .bss , .text ) y los strings estáticos utilizados por el programa (e.g. Hello, world! ), y otros strings que no son símbolos usados por las herramientas de build pero que igualmente están embebidos en el binario (e.g. GLIBC_2.2.5).

Listar contenido de la sección de datos de un binario

objdump -s -j .rodata <binario>

Previamente se hizo referencia a las secciones propias de un binario. Los binarios ejecutables están usualmente organizados en secciones, cada una con un fin específico. Convencionalmente, la sección .data usualmente contiene datos mutables globales preinicializados, mientras que la sección .text contiene el código del programa. La sección .rodata contiene datos globales inmutables, como cadenas de caracteres constantes o literales. Cada sección tiene distintos permisos; una sección puede tener alguna combinación de permisos de lectura, escritura y ejecución. Por ejemplo las secciones que permiten ejecución usualmente no permiten escritura para evitar que un atacante ejecute código arbitrario si logra explotar alguna vulnerabilidad que le permita sobrescribir la memoria.

Para el caso de un hello world, podríamos observar lo siguiente:

$ objdump -s -j .rodata main
main:     file format elf64-x86-64

Contents of section .rodata:
 0710 01000200 48656c6c 6f2c2077 6f726c64  ....Hello, world
 0720 2100

Adicionalmente, podemos listar las secciones de un ELF mediante el siguiente comando:

readelf --sections <binario>

Para el ejemplo en cuestión, observaremos algo como lo siguiente:

$readelf --sections main
[... secciones omitidas]
  [14] .text             PROGBITS         0000000000000540  00000540
       00000000000001c2  0000000000000000  AX       0     0     16
  [15] .fini             PROGBITS         0000000000000704  00000704
       0000000000000009  0000000000000000  AX       0     0     4
  [16] .rodata           PROGBITS         0000000000000710  00000710
       0000000000000012  0000000000000000   A       0     0     4
[... secciones omitidas]
  [24] .bss              NOBITS           0000000000201010  00001010
       0000000000000008  0000000000000000  WA       0     0     1
[... secciones omitidas]

Notamos que la sección .text tiene permisos de lectura y ejecución ( AX , Access y eXecution), mientras que la sección .rodata tiene solo permisos de lectura. Notamos, por otro lado, que la sección .bss tiene permisos de lectura y escritura (WA , write access).

Desensamblar un Binario, Sintaxis Intel

objdump -M intel -d <binario>

Desensambar un binario nos permite ver el código assembly de las secciones ejecutables . Para el caso del binario de los ejemplos anteriores, veremos una cantidad de código bastante grande debido a que el compilador inyecta código de inicialización y enlace dinámico, entre otras cosas. Para un programa simple escrito en assembly puro podríamos observar algo como lo siguiente:

mainasm:     file format elf64-x86-64

Disassembly of section .text:

0000000000400080 <_start>:
  400080:   b8 3c 00 00 00          mov    eax,0x3c
  400085:   bf 00 00 00 00          mov    edi,0x0
  40008a:   0f 05                   syscall 

Este programa simplemente realiza un syscall sys_exit , finalizando la ejecución con código 0 .

Lanzar una shell nueva con ASLR deshabilitado

setarch `uname -m` -R /bin/bash

Cuando se realiza análisis dinámico de ejecutables (es decir, análisis en tiempo de ejecución), puede resultar conveniente deshabilitar ASLR, un mecanismo de mitigación de ataques que, de estar habilitado, le indica al sistema operativo que debe armar el espacio virtual de memoria del proceso cargando ciertas regiones como el stack o como la sección de código a partir de posiciones aleatorias. Esto fuerza a un atacante que cuente con la posibilidad de sobrescribir la memoria de un proceso a encontrar las direcciones de los elementos que deben ser efectivamente sobrescritos para ejecutar un ataque efectivo que le de control del sistema objetivo.

Monitorear system calls que realiza un proceso en ejecución

strace -i <binario>   # Para iniciar la ejecución del programa
strace -i -p <pid>    # Para acoplar a un proceso existente

Los programas que se ejecutan en el espacio del usuario deben usualmente recurrir a los servicios provistos por el sistema operativo. Esto se hace mediante system calls (llamadas al sistema). El programa strace se adosa a un proceso en ejecución y registra las llamadas al sistema ejecutadas por el mismo. Esto nos da información sobre el comportamiento del proceso. Por ejemplo, el hello world de los ejemplos anteriores mostraría, entre otras cosas, las siguientes líneas:

$ strace main
[... llamadas omitidas]
write(1, "Hello, world!", 13Hello, world!) = 13
exit_group(0) = ?
+++ exited with 0 +++

Allí vemos la llamada a write , utilizada por la función printf , y una llamada a exit_group introducida por el compilador. Las llamadas omitidas son también introducidas por el compilador, y realizan tareas como reservar y mapear memoria, configurar permisos de páginas, y cargar bibliotecas dinámicas, entre otras cosas.

Análisis de firmware y binarios compuestos con binwalk

binwalk <binario>

Esta herramienta puede utilizarse para obtener información sobre binarios compuestos como firmware de dispositivos de red o IoT; estos binarios están compuestos por múltiples secciones concatenadas, cada una de las cuáles puede tener algún formato conocido (e.g. algunas pueden ser file systems, otras pueden ser secciones de datos, archivos zip, etc.). Esta herramienta permite listar las distintas secciones y extraerlas. Para extraer las secciones a un subdirectorio local solo basta pasar el flag -e :

binwalk -e <binario>

Listar metadatos de un archivo

exiftool <binario>

El programa lista los metadatos de un archivo, y también permite editarlos.

$ exiftool imagen.png

ExifTool Version Number         : 10.10
File Name                       : imagen.png
Directory                       : .
File Size                       : 7.4 kB
File Modification Date/Time     : 2018:07:06 14:28:18-03:00
File Access Date/Time           : 2018:07:06 14:28:18-03:00
File Inode Change Date/Time     : 2018:07:06 14:28:18-03:00
File Permissions                : rw-rw-r--
File Type                       : PNG
File Type Extension             : png
MIME Type                       : image/png
Image Width                     : 181
Image Height                    : 172
Bit Depth                       : 8
Color Type                      : RGB
Compression                     : Deflate/Inflate
Filter                          : Adaptive
Interlace                       : Noninterlaced
Significant Bits                : 8 8 8
Image Size                      : 181x172
Megapixels                      : 0.031

También es posible borrar o editar los metadatos de un archivo.

$ exiftool -title="Nombre de la imagen" imagen.png


ExifTool Version Number         : 10.10
File Name                       : imagen.png
[... metadatos]
Title                           : Nombre de la imagen
Image Size                      : 181x172
Megapixels                      : 0.031

Redes

Establecer una conexión TCP y enviar datos

Suponiendo que tenemos un servidor TCP escuchando en 192.168.0.100:8000, podemos utilizar netcat para establecer una conexión y enviar datos. Por ejemplo, podemos hacer lo siguiente:

echo "Hello, world!" | netcat 192.168.0.100 8000

La herramienta es interactiva, y permanecerá abierta mientras dure la conexión, mostrándonos lo que envíe el servidor. Podemos seguir enviando mensajes si así lo quisiéramos, o podemos finalizar la conexión con Ctrl+C.

Si queremos enviar datos binarios también podemos hacerlo. Podemos pasar un archivo por la entrada estándar, o pasar los datos por un pipe:

printf "\x00\x00\x00\x00Hello" | netcat 192.168.0.100 8000

El comando anterior enviará 4 bytes nulos seguidos de la secuencia ASCII "Hello". Como se mencionó, también podemos levantar los datos de un archivo:

printf "\x00\x00\x00\x00Hello" > data
netcat 192.168.0.100 8000 < data

Criptografía y codificación

Generar una clave simétrica de 256 bits

openssl rand 32 > key

Este comando genera 32 bytes de datos (256 bits) mediante un mecanismo apto para fines criptográficos. Para generar una versión codificada como un string de caracteres hexadecimales se puede utilizar el siguiente comando:

cat key | hexdump -v -e '/1 "%02X"' > key.hex

Esto genera una representación legible de la secuencia de bytes, donde cada par de caracteres son un byte. Por ejemplo, una clave de 32 bytes representada en codificación hexadecimal podría verse como la siguiente:

DA1F9F56B9797EF2C21DE0BD499E941053642462695B31F15EDA907C3CE39C10

Cifrar y decifrar un archivo con AES CBC

Habiendo generado una clave de 256 bits y habiendola guardado codificada en hexadecimal en un archivo key.hex , podemos utilizar OpenSSL para cifrar un archivo existente. Si vamos a utilizar AES en modo CBC debemos primero generar un vector de inicialización de 16 bytes:

openssl rand 16 | hexdump -v -e '/1 "%02X"' > iv

Ahora ejecutamos el siguiente comando para cifrar un archivo file , generando un archivo cifrado file.enc .

openssl enc -aes-256-cbc -in file -out file.enc -K $(cat key.hex) -iv $(cat iv)

Si adicionalmente queremos que la salida se guarde codificada en base64 (para emisión mediante algún protocolo de texto plano como HTTP, por ejemplo), podemos pasar también la opción -a .

Para decifrar el archivo cifrado debemos contar con la clave y con el vector de inicialización. Luego ejecutamos el siguiente comando:

openssl enc -aes-256-cbc -d -in file.enc -out file.clean -K $(cat key.hex) -iv $(cat iv)

Si se hubiese utilizado la opción -a , también debemos pasarla en esta instancia para deshacer la codificación antes de proceder a decifrar el archivo.