Reconocer el formato del ejecutable, localizar main y seguir una llamada a la biblioteca de C.
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.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 1 --example helloEstoy siguiendo «Primer análisis con radare2» del curso de Lobera. Reconocer el formato del ejecutable, localizar main y seguir una llamada a la biblioteca de C. Dame primero una pista. Después contrasta la explicación con las instrucciones y guarda las evidencias que hayamos comprobado.
El agente y la consola comparten el estado de radare2. Preparación y funcionamiento.
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:
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:
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:
#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:
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.
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.
[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:
[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
[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.
[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.
[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.
[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 |
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.
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:
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 ↗
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.
./coursectl start 1 --example hello
./coursectl consoleCierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.
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.