Este artículo explica las herramientas y los comandos que se pueden usar para aplicar ingeniería inversa a un ejecutable en un entorno Linux.
La ingeniería inversa es el acto de descubrir qué hace un software, para lo cual no hay un código fuente disponible. Es posible que la ingeniería inversa no le proporcione los detalles exactos del software. Pero puede comprender bastante bien cómo se implementó un software.
La ingeniería inversa implica los siguientes tres pasos básicos:
- Recopilación de información
- Determinación del comportamiento del programa
- Interceptar las llamadas de la biblioteca
Yo. Recopilación de información
El primer paso es recopilar la información sobre el programa de destino y lo que hace. Para nuestro ejemplo, tomaremos el comando 'quién'. El comando 'quién' imprime la lista de usuarios conectados actualmente.
1. Comando de cadenas
Strings es un comando que imprime las cadenas de caracteres imprimibles en los archivos. Así que ahora usemos esto contra nuestro comando objetivo (quién).
# strings /usr/bin/who
Algunas de las cadenas importantes son,
users=%lu EXIT COMMENT IDLE TIME LINE NAME /dev/ /var/log/wtmp /var/run/utmp /usr/share/locale Michael Stone David MacKenzie Joseph Arceneaux
A partir de la salida about, podemos saber que "quién" está usando unos 3 archivos (/var/log/wtmp, /var/log/utmp, /usr/share/locale).
Leer más:Ejemplos de comandos de cadenas de Linux (buscar texto en archivos binarios de UNIX)
2. Comando nm
comando nm, se utiliza para enumerar los símbolos del programa de destino. Al usar nm, podemos conocer las funciones locales y de biblioteca y también las variables globales utilizadas. nm no puede funcionar en un programa que está rayado usando el comando 'strip'.
Nota:Por defecto, el comando 'quién' está eliminado. Para este ejemplo, compilé el comando "quién" una vez más.
# nm /usr/bin/who
Esto enumerará lo siguiente:
08049110 t print_line 08049320 t time_string 08049390 t print_user 08049820 t make_id_equals_comment 080498b0 t who 0804a170 T usage 0804a4e0 T main 0804a900 T set_program_name 08051ddc b need_runlevel 08051ddd b need_users 08051dde b my_line_only 08051de0 b time_format 08051de4 b time_format_width 08051de8 B program_name 08051d24 D Version 08051d28 D exit_failure
En la salida anterior:
- t|T:el símbolo está presente en la sección de código .text
- b|B:el símbolo está en la sección .data inicializada por la ONU
- D|d:el símbolo está en la sección de datos inicializados.
La Mayúscula o Minúscula, determina si el símbolo es local o global.
De la salida about, podemos saber lo siguiente,
- Tiene la función global (main,set_program_name,usage,etc..)
- Tiene algunas funciones locales (print_user, time_string, etc.)
- Tiene variables inicializadas globales (Versión, exit_failure)
- Tiene las variables inicializadas por la ONU (time_format, time_format_width, etc.)
A veces, al usar los nombres de las funciones, podemos adivinar qué harán las funciones.
Leer más:10 ejemplos prácticos de comandos de Linux nm
Los otros comandos que se pueden usar para obtener información son
- comando ldd
- comando del fusor
- comando lsof
- /sistema de archivos proc
II. Determinación del comportamiento del programa
3. Comando ltrace
Rastrea las llamadas a la función de biblioteca. Ejecuta el programa en ese proceso.
# ltrace /usr/bin/who
El resultado se muestra a continuación.
utmpxname(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0 setutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 1 getutxent(0x8050c6c, 0xb77068f8, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(NULL, 384) = 0x09ed59e8 getutxent(0, 384, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 768) = 0x09ed59e8 getutxent(0x9ed59e8, 768, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1152) = 0x09ed59e8 getutxent(0x9ed59e8, 1152, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 1920) = 0x09ed59e8 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 1920, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 realloc(0x09ed59e8, 3072) = 0x09ed59e8 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78) = 0x9ed5860 getutxent(0x9ed59e8, 3072, 0, 0xbfc5cdc0, 0xbfc5cd78)
Puede observar que hay un conjunto de llamadas a getutxent y su familia de funciones de biblioteca. También puede notar que ltrace da los resultados en el orden en que se llama a las funciones en el programa.
Ahora sabemos que el comando "quién" funciona llamando a getutxent y su familia de funciones para obtener los usuarios registrados.
4. Comando de rastreo
El comando strace se usa para rastrear las llamadas al sistema realizadas por el programa. Si un programa no usa ninguna función de biblioteca y solo usa llamadas al sistema, entonces usando ltrace simple, no podemos rastrear la ejecución del programa.
# strace /usr/bin/who
[b76e7424] brk(0x887d000) = 0x887d000 [b76e7424] access("/var/run/utmpx", F_OK) = -1 ENOENT (No such file or directory) [b76e7424] open("/var/run/utmp", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3 . . . [b76e7424] fcntl64(3, F_SETLKW, {type=F_RDLCK, whence=SEEK_SET, start=0, len=0}) = 0 [b76e7424] read(3, "\10\325"..., 384) = 384 [b76e7424] fcntl64(3, F_SETLKW, {type=F_UNLCK, whence=SEEK_SET, start=0, len=0}) = 0
Puede observar que cada vez que se llama a la función malloc, llama al sistema brk(). La función de la biblioteca getutxent en realidad llama a la llamada del sistema 'abrir' para abrir '/var/run/utmp' y coloca un bloqueo de lectura y lee el contenido y luego libera los bloqueos.
Ahora confirmamos que el comando who lee el archivo utmp para mostrar el resultado.
Tanto 'strace' como 'ltrace' tienen un conjunto de buenas opciones que se pueden usar.
- -p pid:se adjunta al pid especificado. Útil si el programa ya se está ejecutando y desea conocer su comportamiento.
- -n 2:aplica una sangría de 2 espacios a cada llamada anidada.
- -f – Seguir bifurcación
Leer más:7 ejemplos de Strace para depurar la ejecución de un programa en Linux
III. Interceptando las llamadas de la biblioteca
5. LD_PRELOAD Y LD_LIBRARY_PATH
LD_PRELOAD nos permite agregar una biblioteca a una ejecución particular del programa. La función de esta biblioteca sobrescribirá la función de biblioteca real.
Nota:No podemos usar esto con programas configurados con el bit 'suid'.
Tomemos el siguiente programa.
#include <stdio.h> int main() { char str1[]="TGS"; char str2[]="tgs"; if(strcmp(str1,str2)) { printf("String are not matched\n"); } else { printf("Strings are matched\n"); } }
Compile y ejecute el programa.
# cc -o my_prg my_prg.c # ./my_prg
Imprimirá "Las cadenas no coinciden".
Ahora escribiremos nuestra propia biblioteca y veremos cómo podemos interceptar la función de la biblioteca.
#include <stdio.h> int strcmp(const char *s1, const char *s2) { // Always return 0. return 0; }
Compile y establezca la variable LD_LIBRARY_PATH en el directorio actual.
# cc -o mylibrary.so -shared library.c -ldl # LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
Ahora se creará un archivo llamado 'library.so'.
Establezca la variable LD_PRELOAD en este archivo y ejecute el programa de comparación de cadenas.
# LD_PRELOAD=mylibrary.so ./my_prg
Ahora imprimirá "Las cadenas coinciden" porque usa nuestra versión de la función strcmp.
Nota:si desea interceptar cualquier función de biblioteca, entonces su propia función de biblioteca debe tener el mismo prototipo que la función de biblioteca original.
Acabamos de cubrir las cosas muy básicas necesarias para aplicar ingeniería inversa a un programa.
Para aquellos que deseen dar el siguiente paso en la ingeniería inversa, comprender el formato de archivo ELF y el programa de lenguaje ensamblador ayudará en mayor medida.