Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Fundamentos de reversing / 01

Primer análisis con radare2

LoberaFormación técnica01 / 33 · 6 min de lectura · Linux · x86 / x86-64
01 / 33

Reconocer el formato del ejecutable, localizar main y seguir una llamada a la biblioteca de C.

Consultar conceptos ↗
Notas de la práctica

El laboratorio actual utiliza GCC, símbolos de depuración y -O0. El artículo compara salidas históricas de 32 y 64 bits. Sigue las funciones y las operaciones; las direcciones concretas pueden cambiar.

Sobre este curso

El curso parte de programas escritos en C para estudiar cómo se representan en un ejecutable sus funciones, instrucciones y estructuras de datos. Los primeros ejemplos permiten comparar la fuente con el desensamblado. Ese trabajo prepara la lectura de programas cuyo código fuente no está disponible, incluidos los casos de malware de los últimos módulos.

A lo largo de estas entradas partiremos de un fichero fuente en C, lo compilaremos tanto en 32 bits como en 64 bits y luego haremos reversing con radare2, para que puedas apreciar las diferencias entre ambos tipos de binarios. Una vez cubiertos los fundamentos del reversing, nos centraremos en x64.

Conviene conocer los elementos básicos de C y de una arquitectura de computadores. Las primeras lecciones introducen los comandos de radare2 y su relación con instrucciones como mov, push, call y ret. Puedes consultar las definiciones en el glosario.

Obtener radare2

La mayoría de quienes usan radare2 lo hacen en sistemas Linux, desde donde se pueden analizar todo tipo de binarios y, si se quiere depurarlos, conectarse a una sesión de depuración remota o usar gdb en Linux. Aquí usaremos r2 en Linux la mayor parte del tiempo y pasaremos a Windows cuando sea necesario.

Radare2 soporta una gran cantidad de arquitecturas, desde x64 hasta Arduinos o Tamagotchis, y puedes analizar binarios de esas arquitecturas desde tu sistema Ubuntu o el que uses.

Dicho esto, la mejor forma de obtener radare2, tal como sugiere su propia página web, es clonarlo desde el repositorio:

TERMINAL
git clone https://github.com/radare/radare2
cd radare2
sys/install.sh   # just run this script to update r2 from git

También puedes instalarlo desde el repositorio apt/rpm/yaourt o el que corresponda en la mayoría de distribuciones con algo como:

TERMINAL
sudo apt-get install radare2

Y si quieres usarlo en Windows puedes descargar el instalador para Windows desde su sitio web.

Hello world

Empezaremos con un programa que imprime una cadena. Su tamaño permite seguir la función main, la llamada de salida y el retorno:

C / EJEMPLO
#include <stdio.h>
int main() {
   printf("Hello, World!");
   return 0;
}

Un programa así se puede compilar fácilmente con el compilador GCC de la siguiente forma:

TERMINAL
gcc -w hello_world.c -o hello_word

Al ejecutarlo, la salida contiene Hello, World!. Ahora compararemos ese comportamiento con las instrucciones del ejecutable.

Una vez compilado el programa, podemos abrirlo con r2 mediante "radare2 programa", donde programa es el binario que quieres analizar. r2 es también un alias de radare2, así que puedes usarlo indistintamente. Una vez cargado el binario, hay que indicarle a radare que lo analice; ejecutar aaaa analizará el binario y detectará estructuras de datos, llamadas a funciones y elementos similares. Si quieres entender mejor cómo funciona esto, esta respuesta en Stack Overflow es bastante clarificadora.

CÓDIGO / SALIDA
lab@lab-VirtualBox:~$ radare2 c_examples/bin/hello_world 
 -- Use V! to enter into the visual panels mode (dwm style)
[0x08048310]> aaaa
[Cannot analyze at 0x08048300g with sym. and entry0 (aa)
[x] Analyze all flags starting with sym. and entry0 (aa)
[Cannot analyze at 0x08048300ac)
[x] Analyze function calls (aac)
[x] Analyze len bytes of instructions for references (aar)
[x] Check for objc references
[x] Check for vtables
[x] Type matching analysis for all functions (aaft)
[x] Propagate noreturn information
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Finding function preludes
[x] Enable constraint types analysis for variables
[0x08048310]> 

Una vez cargado y analizado el programa, se pueden realizar muchas acciones sobre él: listar cadenas, desensamblar bloques de código y más. Una de las primeras cosas que querrás hacer cuando te encuentras con un binario desconocido es obtener la lista de sus funciones. Puedes hacerlo con el comando afl.

CÓDIGO / SALIDA
[0x08048310]> afl
0x08048310    1 33           entry0
0x080482f0    1 6            sym.imp.__libc_start_main
0x08048350    4 43           sym.deregister_tm_clones
0x08048380    4 53           sym.register_tm_clones
0x080483c0    3 30           entry.fini0
0x080483e0    4 43   -> 40   entry.init0
0x080484a0    1 2            sym.__libc_csu_fini
0x08048340    1 4            sym.__x86.get_pc_thunk.bx
0x080484a4    1 20           sym._fini
0x08048440    4 93           sym.__libc_csu_init
0x0804840b    1 46           main
0x080482e0    1 6            sym.imp.printf
0x080482ac    3 35           sym._init
[0x08048310]> 

En ejemplos como este, la función más interesante es "main". Las funciones Entry* también pueden ser interesantes, pero en un programa hello world como este probablemente encontrarás en ellas bastante código que de momento no entenderás (código interno añadido por el compilador), así que las dejamos para más adelante. Las demás funciones que aparecen en este ejemplo están relacionadas con las bibliotecas y llamadas de C que usa el programa (como la llamada a printf que seguramente identificarás).

Otra acción inicial interesante es obtener información general sobre el binario con iI:

CÓDIGO / SALIDA
[0x08048310]> iI
arch     x86
baddr    0x8048000
binsz    6115
bintype  elf
bits     32
canary   false
class    ELF32
compiler GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.12) 5.4.0 20160609
crypto   false
endian   little
havecode true
intrp    /lib/ld-linux.so.2
laddr    0x0
lang     c
linenum  true
lsyms    true
machine  Intel 80386
maxopsz  16
minopsz  1
nx       true
os       linux
pcalign  0
pic      false
relocs   true
relro    partial
rpath    NONE
sanitiz  false
static   false
stripped false
subsys   linux
va       true

[0x08048310]> 

Esto mostrará valores como la arquitectura o el compilador. Esos datos son siempre útiles para saber con qué estamos tratando y empezar a descartar estrategias, centrándose en lo que importa.

También podemos listar las importaciones con ii

CÓDIGO / SALIDA
[0x08048310]> ii
[Imports]
nth                vaddr              bind   type              name
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
577730550794551297 0xb6f9994600000000 FUNC   printf            
2                  0xb6f9994d00000000 NOTYPE __gmon_start__    
577730619514028035 0xb6f9994600000000 FUNC   __libc_start_main 

En este caso concreto, a estas alturas deberíamos tener claro que estamos ante un programa sencillo escrito en C.

Otro comando de interés es iz, que lista todas las cadenas contenidas en la sección de datos del programa (izz lista las cadenas en todo el fichero). Este comando es especialmente útil cuando se trabaja con crackmes sencillos, ya que permite identificar contraseñas hardcodeadas.

En términos generales, conocer las cadenas de un programa ayuda a hacerse una idea general de qué hace ese programa.

CÓDIGO / SALIDA
[0x08048310]> iz
[Strings]
nth           paddr             vaddr       len size     section type          string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
5222680231936 0x80484c000000000 0xd00000000 14  34461128 ascii   Hello, World! bin.strings

[0x08048310]> 

iz identifica la cadena Hello, World!, su dirección y su tipo. La etiqueta ascii describe la codificación reconocida; la sección que contiene la cadena se consulta en la información del binario.

Ahora que conocemos algunos fundamentos básicos, vamos a hacer reversing del programa e intentar entender qué hace y cómo lo hace.

C / EJEMPLO
[0x08048310]> sf main
[0x0804840b]> pdb
            ; DATA XREF from entry0 @ 0x8048327
 46: int main (int32_t arg_4h, char **argv, char **envp);
           ; var int32_t var_4h @ ebp-0x4
           ; arg int32_t arg_4h @ esp+0x24
           0x0804840b      8d4c2404       lea ecx, [arg_4h]
           0x0804840f      83e4f0         and esp, 0xfffffff0
           0x08048412      ff71fc         push dword [ecx - 4]
           0x08048415      55             push ebp
           0x08048416      89e5           mov ebp, esp
           0x08048418      51             push ecx
           0x08048419      83ec04         sub esp, 4
           0x0804841c      83ec0c         sub esp, 0xc
           0x0804841f      68c0840408     push str.Hello__World       ; 0x80484c0 ; "Hello, World!" ; const char *format
           0x08048424      e8b7feffff     call sym.imp.printf         ; int printf(const char *format)
           0x08048429      83c410         add esp, 0x10
           0x0804842c      b800000000     mov eax, 0
           0x08048431      8b4dfc         mov ecx, dword [var_4h]
           0x08048434      c9             leave
           0x08048435      8d61fc         lea esp, [ecx - 4]
           0x08048438      c3             ret
[0x0804840b]> 

Lo primero que hay que entender es que estamos inspeccionando main, y main es una función, por lo que tiene que retornar a algún sitio y puede recibir argumentos o necesitar espacio para variables locales. Las operaciones relacionadas con esos aspectos se realizan al principio y al final del código.

La parte en la que hay que fijarse aquí comienza con push str.Hello__World y luego la llamada a printf. En sistemas de 32 bits, la forma de pasar parámetros a las funciones consiste en hacer push de esos parámetros en la pila y luego ejecutar una operación call sobre la dirección de la función. Así, algo como "printf(a);" en C equivale a "push a; call printf" en asm. ¿Y el resto del código? La primera parte está relacionada con el establecimiento de un nuevo stack frame, que es una sección del código que contendrá las variables locales de la función, los argumentos pasados y valores similares. El final del código

Binarios x64 y sus particularidades

Repitamos el proceso ahora compilando el mismo programa y abriéndolo en r2 en un sistema x64.

ENSAMBLADOR / REFERENCIA
[0x00000540]> sf sym.main
[0x0000064a]> pdb
            ;-- main:
/ (fcn) sym.main 28
|   sym.main ();
|              ; DATA XREF from 0x0000055d (entry0)
|           0x0000064a      55             push rbp
|           0x0000064b      4889e5         mov rbp, rsp
|           0x0000064e      488d3d9f0000.  lea rdi, qword str.Hello__World ; 0x6f4 ; "Hello, World!" ; const char * format
|           0x00000655      b800000000     mov eax, 0
|           0x0000065a      e8c1feffff     call sym.imp.printf         ; int printf(const char *format)
|           0x0000065f      b800000000     mov eax, 0
|           0x00000664      5d             pop rbp
\           0x00000665      c3             ret
[0x0000064a]> 

En el ejemplo Linux x86-64 se utiliza la ABI System V: los primeros seis argumentos de clase entera o puntero se pasan por rdi, rsi, rdx, rcx, r8 y r9. Otros argumentos pueden utilizar registros vectoriales o la pila. La convención depende de la ABI; Windows x64 utiliza otra distribución.

El prólogo push rbp; mov rbp, rsp conserva el frame pointer anterior y establece el de esta función. Después, lea carga en rdi la dirección de Hello, World!. El mov eax, 0 anterior a printf deja al a cero: en una llamada variádica de esta ABI indica que no se han utilizado registros vectoriales para sus argumentos. Es una operación distinta del mov eax, 0 final que prepara el valor de retorno.

Al final del programa vemos un mov eax, 0, luego pop rbp y finalmente ret. El primero establece eax a 0 preparando el programa para ejecutar un "return 0"; el segundo restaura la pila a su estado original recuperando el valor original del stack frame en rbp. Después, ret simplemente retorna de la función, saliendo de ella con el valor 0.

Comandos utilizados

En esta entrada hemos utilizado los siguientes comandos:

Comando Descripción
aaaa Analiza completamente el binario
afl Lista todas las funciones del binario
ii Lista las importaciones
iI Información sobre el binario
iz Lista las cadenas del binario
sf function Desplazarse a una función
pdb Imprime el desensamblado del bloque básico
Sesión en vídeo / Companion0:46

Localizar main con el companion.

Hermes identifica la cadena del programa y la llamada que la imprime. Después comprobamos ambos datos desde la consola de la misma sesión.

Captura de una sesión real de Hermes y radare2. Sin audio; esperas abreviadas.Descargar vídeo ↙
Leer la explicación y los comandos

La consulta

Localiza main y la cadena Hello, World! en hello. ¿Qué llamada imprime el mensaje? Muéstrame los comandos y las instrucciones que lo confirman.

Qué muestra la sesión

El binario contiene la cadena «Hello, World!» en 0x402004 y la función main en 0x401136. Hermes consulta las herramientas de radare2 para localizar ambas y examinar las instrucciones.

En 0x40113e se carga la dirección de la cadena. La llamada a printf aparece en 0x401148. La consola devuelve el mismo desensamblado y la misma cadena: el lector puede revisar directamente los datos utilizados por el agente.

Hermes etiqueta la consulta de puntos de entrada como ie en su respuesta. La herramienta MCP ejecuta ies, que incluye main en el listado. Para reproducir esa consulta desde la consola utiliza ies.

Comprobación desde la consola

Abre ./coursectl console en otra terminal del companion y ejecuta:

CÓDIGO / SALIDA
pdf @ main
iz

Estas direcciones pertenecen al ejecutable grabado. En tu compilación, localiza primero main y las referencias a la cadena. Preparar el companion ↗

Práctica manual

Vuelve al binario

Reconocer el formato del ejecutable, localizar main y seguir una llamada a la biblioteca de C. Identifica al menos una instrucción y el dato que utiliza; conserva la compilación con la que lo has observado.

Fuente de esta práctica: hello.c ↓.

Descargar los 13 ejemplos de fundamentos ↙

Puedes utilizar la consola del companion sin conversar con el agente, o abrir el ejecutable en tu desensamblador habitual.

TERMINAL / COMPANION
./coursectl start 1 --example hello
./coursectl console

Cierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.

Comprobación personal

Antes de continuar

Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.

El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.