Análisis estático del código ejecutable ELF – tutorial

El siguiente documento es la representación práctica de las acciones descritas en el artículo La ingeniería inversa del código ejecutable ELF en el análisis de post intrusión (Hakin9 01/2005).
Objeto analizado: netc. Antes de empezar a analizar el archivo tenemos que copiarlo a la carpeta principal.

Preparativos

Está claro que Hakin9 Live posee todas las herramientas necesarias para realizar el análisis. Sin embargo, cuando no las usamos, para realizar todo lo descrito es necesario el sistema Linux y las siguientes aplicaciones:

Aplicaciones del paquete binutils Scripts y aplicaciones del paquete fenris Las demás herramientas:

Diagnóstico previo del objeto analizado

[01] Por medio de la herramienta file conseguimos la información básica sobre el archivo analizado

# file netc
netc: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.2.5, statically linked, stripped

Como podemos observar el archivo analizado ha sido compilado estáticamente y ha pasado el proceso de stripping.

[02] Buscamos en el archivo las cadenas de caracteres que nos interesen

# strings -a netc > strings.out

Al analizar el contenido recibido vamos a encontrar la siguiente información que nos ayudará en las siguientes operaciones:

Otra forma de encontrar las cadenas de caracteres que nos interesen puede ser la búsqueda en el contenido de las secciones del archivo analizado. Por ejemplo, cuando queremos recibir información sobre el sistema y sobre la versión del compilador que se empleó para compilar el archivo, se guardan de forma estándar en la sección .comment.

Podemos leer el contenido de la sección indicada del archivo al aplicar el comando:

# objdump -j .comment -s nombre_de_la_aplicación > comment.out

o

# objdump -h nombre_de_la_aplicación > sections.out

y previsualizar con cualquier navegador el contenido del archivo con offset indicado.

Prueba de restaurar el array de símbolos

[03] Buscamos y descargamos de Internet la librería que se pueda emplear durante la compilación

Del resultado de la acción del comando strings en el paso [02] veremos que el archivo ha sido compilado en el sistema Mandrake Linux 10.0 con el empleo del compilador GCC 3.3.2. En nuestro caso nos vamos a limitar a la prueba de restaurar los símbolos relacionados con la librería libc procedente del sistema Mandrake 10.0. Teniendo en cuenta el uso de la librería libc podemos suponer con mucha probabilidad que vamos a emplearla en el archivo analizado. Otra librería que también puede emplearse en el proceso de restaurar el array de símbolos es la librería libgcc.a. Sin embargo, nos vamos a limitar tan sólo a la librería libc ya que los elementos de la librería libgcc.a los vamos a reconocer a continuación, al comparar con una aplicación compilada de forma correcta.

Guardamos la librería libc.a en la carpeta ~/analysis/libc_components/

¿Qué vamos a hacer si no poseemos la información que podría ayudarnos a la hora de fijar la versión de las librerías empleadas durante la compilación? En la misma situación podemos descargar diferentes versiones de librerías incluso para diferentes distribuciones del sistema y efectuar los siguientes pasos del proceso de la restauración del array de símbolos y evaluar los resultados recibidos teniendo en cuenta la eficacia y el número de alcances.

[04] Desempaquetamos los objetos de la librería

# ar x libc.a

[05] Comprobamos que en la aplicación analizada está incluido el código de los respectivos objetos de la librería

# search_static netc ~/analysis/libc_components > obj_file

En este paso tenemos que tener en cuenta las colisiones que pueden aparecer durante la verificación realizada. Las colisiones detectadas se encuentran en la parte final del archivo obj_file. ¿Qué importancia práctica y qué influencia en el análisis tienen las colisiones? El problema es que sin el análisis detallado del código de las funciones en las cuales aparecieron las colisiones no se puede determinar claramente cuál de las funciones se empleó realmente en la aplicación. Los ejemplos de la colisión detectada:

# Possible conflict below requiring manual resolution:
# ----------------------------------------------------
# /analysis/libc_components/getsrvbynm.o - match at 0x08057580 (0x000000ea bytes)
# /analysis/libc_components/getsrvbypt.o - match at 0x08057580 (0x000000ea bytes)

[06] Generamos el listado de las referencias simbólicas encontradas de los respectivos objetos de la librería

# gensymbols obj_file > symbols_db

El resultado de la acción del script es el listado de símbolos junto con las direcciones en las cuales podemos encontrar su código.

[07] Realizamos el desensamblaje de la aplicación analizada

# gendump netc > out1

[08] Eliminamos el código de las funciones encontradas de la librería del archivo out1

# decomp_strip obj_file < out1 > out2

[09] Para facilitar el análisis añadimos los nombres de las funciones encontradas en los lugares de sus llamadas

# decomp_insert_symbols symbols_db < out2 > out3

[10] Para aumentar la legibilidad del código en los lugares de referencia a las cadenas de caracteres introducimos su contenido

# decomp_xref_data netc < out3 > out4

Para recuperar el contenido del array de símbolos también podemos aplicar las herramientas del paquete fenris.

Los siguientes pasos:
[a] Abrimos para editar el script getfprints
[b] En el parámetro TRYLIBS introducimos las rutas y los nombres de librerías de las cuales queremos crear las bases de signaturas

# getfprints

[c] Sustituimos el nombre del archivo de destino con el nombre predeterminado del archivo de signaturas

# mv NEW-fnprints.dat fnprints.dat

[d] Aprovechamos la aplicación dress y restauramos los símbolos eliminados

# dress -F ./fnprints.dat nombre_de_la_aplicación > listado_de_símbolos_recuperados

o

# dress -F ./fnprints.dat nombre_de_la_aplicación_con_el_listado_de_símbolos_añadido

Fijamos las funciones añadidas por el compilador

Cuando conocemos la versión del compilador empleado en la compilación de la aplicación analizada (y nosotros sí la conocemos) o bien podemos sobreentender esta información, podemos empezar a tratar de encontrar la localización de las funciones añadidas por el compilador ( deberíamos obtener un resultado parecido empleando la librería libgcc.a en el proceso de restauración del array de símbolos). Para realizar esta acción vamos a comparar una aplicación modelo compilada con el mismo compilador que el archivo analizado.

[11] Creamos nuestra propia aplicación modelo sample.c

int main (int argc, char **argv[])
{
return 0;
}

[12] Compilamos una aplicación modelo

# gcc -static -o sample sample.c

[13] Comparamos los elementos de la aplicación compilada con el código del archivo analizado - out4

Como consecuencia somos capaces de fijar las siguientes funciones:

La función _start() - Dirección = 08048100


La función call_gmon_start() - Dirección = 08048124


La función __do_global_dtors_aux() - Dirección = 08048150


La función frame_dummy() - Dirección = 080481b0


Conseguimos también la localización de la función _start() al leer el valor entrypoint de la cabecera ELF

Fijamos la localización de la función main()

[14] Cuando comparamos la estructura de la función _start() de la aplicación modelo sample con el código de esta función encontrada en el archivo encontrado fijamos la localización de la función main().

08048100: xor %ebp,%ebp
08048102: pop %esi
08048103: mov %esp,%ecx
08048105: and $0xfffffff0,%esp
08048108: push %eax
08048109: push %esp
0804810a: push %edx
0804810b: push $0x804aa90
08048110: push $0x804aa30
08048115: push %ecx
08048116: push %esi
08048117: push $0x804994f
0804811c: call 0x0804a3b0 &lt;__libc_start_main&gt;
08048121: hlt

Fijamos la función del usuario

[15] Fijamos las funciones del usuario (más concretamente - las funciones que no han sido reconocidas como objetos de la librería)

# grep 'call 0x' out4 | grep -v '<' > user_f.out

[16] Como en el código analizado muchas funciones llamadas se repiten, vamos a tratar de conseguir tan sólo las direcciones únicas de funciones

# grep 'call 0x' out4 | grep -v '<' | awk '{print $3}' | sort -u

0x0804812d
0x080481bd
0x08048204
0x080482a5
0x08048303
0x0804834b
0x080483b5
0x080483fd
0x080486b8
0x080488ab
0x08048951
0x080489b3
0x08048a3b
0x08048d9d
0x08049235
0x08049311
0x0804950b
0x0804aed0
0x0804bf70
0x0804bf90
0x08057370
0x08057580


Como vemos en el resultado, una parte de las direcciones conseguidas no existen en nuestro código out4. Su falta resulta de la existencia de las signaturas de funciones que fueron eliminadas en el paso [8].

Análisis real de las acciones realizadas por la aplicación

[17] En los siguientes pasos cuando empezamos por la función main(), deberíamos realizar el análisis del flujo del control entre las funciones del usuario y las acciones realizadas por éste. Cuando efectuemos este paso, deberíamos conseguir la respuesta a las preguntas de tipo: ¿qué papel desempeño en el sistema el objeto analizado y qué mecanismos empleó? Para la interpretación real de las acciones realizadas por las respectivas funciones es necesario el conocimiento del lenguaje Assembler al nivel por lo menos básico.

Función main()
0x0804812d
0x080481bd
0x08048204
0x080482a5
0x08048303
0x0804834b
0x080483b5
0x080483fd
0x080486b8
0x080488ab
0x08048951
0x080489b3
0x08048a3b
0x08048d9d
0x08049235
0x08049311
0x0804950b