Seguir llamadas, distinguir ramas de un switch e identificar el retorno de un bucle.
Notas de la práctica
El vídeo utiliza positive, un ejemplo adicional del laboratorio 3. La práctica manual de esta página utiliza switch-fruit; ambos están disponibles en las descargas.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 3 --example switch-fruitEstoy siguiendo «Funciones, switch y bucles» del curso de Lobera. Seguir llamadas, distinguir ramas de un switch e identificar el retorno de un bucle. 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.
En el capítulo anterior de este curso completo de radare2, recorrimos la estructura básica de un binario ejecutable escrito en C, prestando atención a las llamadas a funciones de entrada/salida como printf y a estructuras de datos básicas como las variables. También se revisaron las medidas básicas de control del flujo de ejecución mediante sentencias if-else. Hoy profundizamos un poco más en esas medidas de control de flujo: presentaremos el uso básico de la sentencia switch-case, declararemos y usaremos funciones propias y, al final, analizaremos bucles con while y for.
Funciones y condiciones
Refrescamos algunos conceptos anteriores con el siguiente código, que declara una función para detectar números positivos y salta a ella.
#include <stdio.h>
func2(){
int num;
printf("Enter a number: ");
scanf("%d", &num);
if(num>0) printf("The number is positive.\n");
getchar();
}
main(){
func2();
getchar();
}
Podemos compilar ese código y abrirlo en radare2.
[0x000006a0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x000006a0]>
Tras ejecutar el afl inicial, esta vez detectamos un par de lugares interesantes. Tenemos el sym.main como de costumbre, pero también el sym.func2, que no parece tan habitual y merece atención. Otras pistas iniciales son la presencia de scanf, getchar, printf o puts, que dan una idea general de lo que hace el programa.
[0x000006a0]> afl
0x00000000 3 72 -> 73 sym.imp.__libc_start_main
0x00000618 3 23 sym._init
0x00000640 1 6 sym.imp.puts
0x00000650 1 6 sym.imp.__stack_chk_fail
0x00000660 1 6 sym.imp.printf
0x00000670 1 6 sym.imp.getchar
0x00000680 1 6 sym.imp.__isoc99_scanf
0x00000690 1 6 sub.__cxa_finalize_248_690
0x000006a0 1 43 entry0
0x000006d0 4 50 -> 40 sym.deregister_tm_clones
0x00000710 4 66 -> 57 sym.register_tm_clones
0x00000760 4 49 sym.__do_global_dtors_aux
0x000007a0 1 10 entry1.init
0x000007aa 5 111 sym.func2
0x00000819 1 26 sym.main
0x00000840 4 101 sym.__libc_csu_init
0x000008b0 1 2 sym.__libc_csu_fini
0x000008b4 1 9 sym._fini
[0x000006a0]>
Empezamos saltando a la función main:
[0x000006a0]> s main
[0x00000819]> pdb
;-- main:
/ (fcn) sym.main 26
| sym.main ();
| ; DATA XREF from 0x000006bd (entry0)
| 0x00000819 55 push rbp
| 0x0000081a 4889e5 mov rbp, rsp
| 0x0000081d b800000000 mov eax, 0
| 0x00000822 e883ffffff call sym.func2
| 0x00000827 e844feffff call sym.imp.getchar ; int getchar(void)
| 0x0000082c b800000000 mov eax, 0
| 0x00000831 5d pop rbp
\ 0x00000832 c3 ret
[0x00000819]>
Esta función main realiza las operaciones básicas de alineación de pila y salta rápidamente a la función func2; tras ejecutarla, llama a getchar(), probablemente para mantener la ventana abierta (si el programa se ejecuta, por ejemplo, en Windows).
En este caso nos centraremos en la función func2.
[0x00000819]> s sym.func2
[0x000007aa]> pdf
/ (fcn) sym.func2 111
| sym.func2 ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x00000822 (sym.main)
| 0x000007aa 55 push rbp
| 0x000007ab 4889e5 mov rbp, rsp
| 0x000007ae 4883ec10 sub rsp, 0x10
| 0x000007b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x1a08 ; '('
| 0x000007bb 488945f8 mov qword [local_8h], rax
| 0x000007bf 31c0 xor eax, eax
| 0x000007c1 488d3dfc0000. lea rdi, qword str.Enter_a_number: ; 0x8c4 ; "Enter a number: "
| 0x000007c8 b800000000 mov eax, 0
| 0x000007cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x000007d2 488d45f4 lea rax, qword [local_ch]
| 0x000007d6 4889c6 mov rsi, rax
| 0x000007d9 488d3df50000. lea rdi, qword [0x000008d5] ; "%d"
| 0x000007e0 b800000000 mov eax, 0
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0c jle 0x7fd
| | 0x000007f1 488d3de00000. lea rdi, qword str.The_number_is_positive. ; 0x8d8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007ef (sym.func2)
| `-> 0x000007fd e86efeffff call sym.imp.getchar ; int getchar(void)
| 0x00000802 90 nop
| 0x00000803 488b55f8 mov rdx, qword [local_8h]
| 0x00000807 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x00000810 7405 je 0x817
| | 0x00000812 e839feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| | ; JMP XREF from 0x00000810 (sym.func2)
| `-> 0x00000817 c9 leave
\ 0x00000818 c3 ret
[0x000007aa]>
| 0x000007ae 4883ec10 sub rsp, 0x10
| 0x000007b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=0x1a08 ; '('
| 0x000007bb 488945f8 mov qword [local_8h], rax
| 0x000007bf 31c0 xor eax, eax
Como ya conocemos el programa, sabemos que lo primero que hace en este punto es declarar la variable number, que almacenará la entrada del usuario. Las dos primeras líneas pueden resultar algo confusas y, en mi opinión, no están directamente relacionadas con el algoritmo que analizamos. La primera línea aparece en muchas funciones y básicamente reserva espacio en la pila para operar con variables y estructuras de datos; como aquí usamos variables, el programa «reserva» ese espacio. Profundizaremos en esto más adelante; por ahora es suficiente con entenderlo así.
A continuación encontramos esta instrucción mov rax, qword que va un poco más allá del alcance de este artículo. La inserta el compilador gcc y su función es establecer una comprobación de guarda de pila contra posibles vulnerabilidades de desbordamiento de búfer. Si miramos el código con más detalle, vemos una operación XOR en la misma ubicación al final de la rutina. En términos generales, el programa comprobará si la pila ha sido corrompida; si es así, lanzará __stack_chk_fail para gestionar el problema de forma segura. El mecanismo completo está explicado en detalle en https://stackoverflow.com/questions/10325713/why-does-this-memory-address-fs0x28-fs0x28-have-a-random-value. La variable etiquetada como local_8h se usará para almacenar ese «canario».
Continuamos.
| 0x000007c1 488d3dfc0000. lea rdi, qword str.Enter_a_number: ; 0x8c4 ; "Enter a number: "
| 0x000007c8 b800000000 mov eax, 0
| 0x000007cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x000007d2 488d45f4 lea rax, qword [local_ch]
| 0x000007d6 4889c6 mov rsi, rax
| 0x000007d9 488d3df50000. lea rdi, qword [0x000008d5] ; "%d"
| 0x000007e0 b800000000 mov eax, 0
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
Como ya sabemos, en sistemas x64 los parámetros o argumentos se pasan a las funciones a través de los registros, así que con lea rdi, "enter a number" cargamos la dirección efectiva donde está almacenada la cadena "Enter a number:" en memoria en el registro. No pasamos la cadena completa a la función, sino una referencia a su ubicación. Este concepto es importante: si la función modifica la cadena, el valor original también se modificará. Si en cambio pasáramos una copia, las operaciones realizadas dentro de la función sobre esa copia no afectarían al original.
Vemos que la cadena a imprimir se pasa por referencia como parámetro a la función printf y que el registro eax se pone a cero. En la ABI x86_64, si una función (como printf) tiene argumentos variables, se espera que AL (parte de EAX) contenga el número de registros vectoriales (SSE, AVX) usados para pasar argumentos a esa función; en nuestro caso ese número es cero. Más información al respecto en https://www.codingame.com/playgrounds/283/sse-avx-vectorization/what-is-sse-and-avx
Una vez pasados esos parámetros, se llama a la función printf, que debería mostrar la cadena por la salida estándar (pantalla).
Justo después de la llamada, vemos que la etiqueta local_ch se carga primero en rax y luego en rsi. local_ch referencia un espacio reservado, habitualmente una variable; en este caso representa la variable number. Después vemos que "%d" también se carga como parámetro, en este caso en rdi, eax se pone a cero y el programa llama a la función scanf.
En términos generales, ese bloque de código equivale a printf("Enter a number:") seguido de scanf("%d", &number).
La siguiente fase del programa corresponde a la comprobación del valor introducido por el usuario.
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0c jle 0x7fd
| | 0x000007f1 488d3de00000. lea rdi, qword str.The_number_is_positive. ; 0x8d8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007ef (sym.func2)
| `-> 0x000007fd e86efeffff call sym.imp.getchar ; int getchar(void)
Tras scanf, la entrada del usuario (local_ch) se mueve a eax y, como vimos antes, se realiza una comparación con 0 mediante test y jle. Si la entrada es >0, la ejecución continúa para imprimir "The number is positive"; si esa condición no se cumple, el programa salta directamente al final del bloque.
A partir de aquí podemos dar por concluido este análisis sencillo y pasar a un paso adicional.
Añadimos ahora una condición else para ver cómo la gestiona el compilador.
#include <stdio.h>
func2(){
int num;
printf("Enter a number: ");
scanf("%d", &num);
if(num>0) printf("The number is positive.\n");
else printf("The number is negative.\n");
getchar();
}
main(){
func2();
getchar();
}
Podemos ir directamente al punto relevante.
| 0x000007e5 e896feffff call sym.imp.__isoc99_scanf
| 0x000007ea 8b45f4 mov eax, dword [local_ch]
| 0x000007ed 85c0 test eax, eax
| ,=< 0x000007ef 7e0e jle 0x7ff
| | 0x000007f1 488d3df00000. lea rdi, qword str.The_number_is_positive. ; 0x8e8 ; "The number is positive."
| | 0x000007f8 e843feffff call sym.imp.puts ; int puts(const char *s)
| ,==< 0x000007fd eb0c jmp 0x80b
| || ; JMP XREF from 0x000007ef (sym.func2)
| |`-> 0x000007ff 488d3dfa0000. lea rdi, qword str.The_number_is_negative. ; 0x900 ; "The number is negative."
| | 0x00000806 e835feffff call sym.imp.puts ; int puts(const char *s)
| | ; JMP XREF from 0x000007fd (sym.func2)
| `--> 0x0000080b e860feffff call sym.imp.getchar ; int getchar(void)
La rama else se ejecuta para cero y para los valores negativos. El mensaje del ejemplo los describe como negativos, aunque cero pertenece también a esa ruta. Al analizar una condición, comprueba sus instrucciones además de la cadena que imprime.
Dentro de radare2 existe una forma más clara de inspeccionar bifurcaciones de código como la que acabamos de ver. Si escribes VV dentro de una función podrás inspeccionarla visualmente.
| test eax, eax |
| jle 0x7ff;[gc] |
`--------------------------------------------'
| |
| '--------------------.
.----------------------------' |
| |
| |
.------------------------------------------------. .------------------------------------------------.
| 0x7f1 ;[gg] | | 0x7ff ;[gc] |
| ; 0x8e8 | | ; JMP XREF from 0x000007ef (sym.func2) |
| ; "The number is positive." | | ; 0x900 |
| lea rdi, qword str.The_number_is_positive. | | ; "The number is negative." |
| call sym.imp.puts;[ge] | | lea rdi, qword str.The_number_is_negative. |
| jmp 0x80b;[gf] | | call sym.imp.puts;[ge] |
`------------------------------------------------' `------------------------------------------------'
| |
'----------------------------. |
.------------------------'
|
|
.---------------------------------------------.
SWITCH CASE
El switch-case plantea un escenario más avanzado que el if-else. La sentencia if-else funciona bien cuando hay dos o tres caminos posibles para redirigir el flujo de ejecución, pero si hay muchos casos distintos conviene usar algo más avanzado como switch-case.
Trabajaremos con el siguiente fragmento de código.
#include <stdio.h>
func2(){
printf("Enter a key and then press enter: ");
char key;
scanf("%c",&key);
switch(key){
case ' ':
printf("Space. \n");
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '0': printf("Digit.\n");
break;
default: printf("Neither space nor digit.\n");
}
}
main(){
func2();
getchar();
}
El código es bastante sencillo; como de costumbre, toda la lógica ocurre dentro de la función func2. El programa lee un carácter de la entrada estándar y lo pasa a la sentencia switch. El valor del carácter se evaluará contra cada uno de los case. Si el carácter introducido es un espacio, se llamará a printf y la comprobación terminará con la instrucción break, lo que significa que el programa saltará directamente fuera del bloque switch.
Los casos correspondientes a los caracteres 0 a 9 comparten el mismo bloque. Basta con que el carácter coincida con uno de ellos para imprimir Digit.. Los valores que no coinciden con el espacio ni con un dígito llegan a default.
Ahora que sabemos qué hace este programa, vamos directamente a radare2. Esta vez analizaremos el binario tanto de forma estática como dinámica.
red@blue:~/c/chapter3$ radare2 -d case
Process with PID 7901 started...
= attach 7901 7901
bin.baddr 0x560c23c18000
Using 0x560c23c18000
asm.bits 64
[0x7ff6a3b32090]> aaa
[ WARNING : block size exceeding max block size at 0x560c23e18fe0
[+] Try changing it with e anal.bb.maxsize
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
= attach 7901 7901
7901
[0x7ff6a3b32090]>
Iniciamos el programa con el flag -d para poder depurarlo. Lo que queremos analizar está dentro de la función func2, así que empezamos por ahí.
[0x7ff6a3b32090]> s sym.func2
[0x560c23c187aa]> pdf
/ (fcn) sym.func2 154
| sym.func2 ();
| ; var int local_9h @ rbp-0x9
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x560c23c1884d (sym.main)
| 0x560c23c187aa 55 push rbp
| 0x560c23c187ab 4889e5 mov rbp, rsp
| 0x560c23c187ae 4883ec10 sub rsp, 0x10
| 0x560c23c187b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x560c23c187bb 488945f8 mov qword [local_8h], rax
| 0x560c23c187bf 31c0 xor eax, eax
| 0x560c23c187c1 488d3d200100. lea rdi, qword str.Enter_a_key_and_then_press_enter: ; 0x560c23c188e8 ; "Enter a key and then press enter: "
| 0x560c23c187c8 b800000000 mov eax, 0
| 0x560c23c187cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
| | 0x560c23c187f6 83f820 cmp eax, 0x20 ; 32
| ,==< 0x560c23c187f9 7c26 jl 0x560c23c18821
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
| ||| 0x560c23c1880c e82ffeffff call sym.imp.puts ; int puts(const char *s)
| |||,=< 0x560c23c18811 eb1a jmp 0x560c23c1882d
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
| |``--> 0x560c23c18821 488d3df50000. lea rdi, qword str.Neither_space_nor_digit. ; 0x560c23c1891d ; "Neither space nor digit."
| | | 0x560c23c18828 e813feffff call sym.imp.puts ; int puts(const char *s)
| | | ; JMP XREF from 0x560c23c18811 (sym.func2)
| | | ; JMP XREF from 0x560c23c1881f (sym.func2)
| `--`-> 0x560c23c1882d 90 nop
| 0x560c23c1882e 488b55f8 mov rdx, qword [local_8h]
| 0x560c23c18832 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x560c23c1883b 7405 je 0x560c23c18842
| | 0x560c23c1883d e80efeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x560c23c18842 c9 leave
\ 0x560c23c18843 c3 ret
[0x560c23c187aa]>
Este bloque comienza con el mecanismo de protección de pila que presentamos antes. El control del flujo de ejecución aquí es algo más complejo. Esto ocurrirá a veces en CTFs o en proyectos reales de reversing. Hay dos soluciones habituales: saltar al modo visual o buscar cadenas o llamadas a funciones interesantes. Aquí podemos identificar fácilmente las cadenas Space, Digit y Neither space or digit, lo que dice mucho y básicamente resuelve el problema, ya que identificamos tres casos principales.
La lógica comienza con el scanf.
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
En este ejemplo, empezamos pasando un par de parámetros a la función scanf a través de los registros: con %c indicamos que vamos a leer un carácter, y local_9h será la ubicación de ese carácter en memoria.
Tras leer el valor del carácter desde la entrada del usuario, el programa lo prepara para compararlo con el conjunto de caracteres del primer case. Como un char en C ocupa un (1) byte, el programa solo necesita el contenido de AL (la parte baja de RAX/EAX). Tras cargar el valor, el programa lo compara con 0x20, que representa el valor de 'espacio' según la tabla ASCII.
| 0x560c23c187f1 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
| | 0x560c23c187f6 83f820 cmp eax, 0x20 ; 32
| ,==< 0x560c23c187f9 7c26 jl 0x560c23c18821
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
| ||| 0x560c23c1880c e82ffeffff call sym.imp.puts ; int puts(const char *s)
| |||,=< 0x560c23c18811 eb1a jmp 0x560c23c1882d
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
| |``--> 0x560c23c18821 488d3df50000. lea rdi, qword str.Neither_space_nor_digit. ; 0x560c23c1891d ; "Neither space nor digit."
| | | 0x560c23c18828 e813feffff call sym.imp.puts ; int puts(const char *s)
| | | ; JMP XREF from 0x560c23c18811 (sym.func2)
| | | ; JMP XREF from 0x560c23c1881f (sym.func2)
| `--`-> 0x560c23c1882d 90 nop
Si el contenido de eax (la entrada) es igual al valor de espacio, el programa saltará a 0x560c23c18805, donde imprimirá "space"; tras imprimirlo, saldrá del bloque saltando directamente a 0x560c23c1882d (nop).
Lo interesante viene justo después. Tras esta primera comparación, si la condición de salto no se cumple, el programa vuelve a comparar el valor con un espacio, esta vez comprobando si el valor es MENOR QUE un espacio; si lo es, salta a "Neither space nor digit" y sale del bloque. ¿Por qué? Es un truco sencillo que usan los compiladores para comprobar si un carácter está fuera de un rango. Como se puede verificar, todos los dígitos entre 0 y 9 tienen valores superiores a 0x20 en la tabla ASCII, por lo que cualquier valor por debajo de 0x20 no puede ser un dígito.
A continuación se realiza la última comparación:
| || 0x560c23c187fb 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
La resta eax - 0x30 transforma los códigos ASCII de 0 a 9 en el intervalo de 0 a 9. La comparación posterior con 9 y el salto ja descartan los valores que, interpretados sin signo, quedan por encima del intervalo. En este listado, ja conduce a Neither space nor digit; la otra ruta alcanza Digit..
Examinemos esto con más detalle depurando el programa e introduciendo un dígito.
Empezamos estableciendo un par de breakpoints.
[0x560c23c187aa]> db 0x560c23c187f1
[0x560c23c187aa]> db 0x560c23c187fb
[0x560c23c187aa]>
Lanzamos la ejecución hasta el primer breakpoint.
[0x560c23c187aa]> dc
Enter a key and then press enter: 5
hit breakpoint at: 560c23c187f1
[0x560c23c187f1]> pdb
/ (fcn) sym.func2 154
| sym.func2 ();
| ; var int local_9h @ rbp-0x9
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x560c23c1884d (sym.main)
| 0x560c23c187aa 55 push rbp
| 0x560c23c187ab 4889e5 mov rbp, rsp
| 0x560c23c187ae 4883ec10 sub rsp, 0x10
| 0x560c23c187b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x560c23c187bb 488945f8 mov qword [local_8h], rax
| 0x560c23c187bf 31c0 xor eax, eax
| 0x560c23c187c1 488d3d200100. lea rdi, qword str.Enter_a_key_and_then_press_enter: ; 0x560c23c188e8 ; "Enter a key and then press enter: "
| 0x560c23c187c8 b800000000 mov eax, 0
| 0x560c23c187cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x560c23c187d2 488d45f7 lea rax, qword [local_9h]
| 0x560c23c187d6 4889c6 mov rsi, rax
| 0x560c23c187d9 488d3d2b0100. lea rdi, qword [0x560c23c1890b] ; "%c"
| 0x560c23c187e0 b800000000 mov eax, 0z
| 0x560c23c187e5 e896feffff call sym.imp.__isoc99_scanf
| 0x560c23c187ea 0fb645f7 movzx eax, byte [local_9h]
| 0x560c23c187ee 0fbec0 movsx eax, al
| ;-- rip:
| 0x560c23c187f1 b 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
[0x560c23c187f1]>
El valor '5' se ha cargado en la variable y, por tanto, ahora está en AL (RAX).
| 0x560c23c187f1 b 83f820 cmp eax, 0x20 ; 32
| ,=< 0x560c23c187f4 740f je 0x560c23c18805
[0x560c23c187f1]> dr
rax = 0x00000035
rax = 0x35, que corresponde a '5' en ASCII. '5' se comparará con ' ' y, como no son el mismo número, el flag zero permanecerá a cero.
[0x560c23c187f1]> ds
[0x560c23c187f1]> dr 1
cf = 0x00000000
pf = 0x00000000
af = 0x00000000
zf = 0x00000000
sf = 0x00000000
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x560c23c187f1]>
El flujo de ejecución continuará y llegaremos a la otra instrucción cmp.
cmp funciona así con los flags:
Assume result = op1 - op2
CF - 1 if unsigned op2 > unsigned op1
OF - 1 if sign bit of OP1 != sign bit of result
SF - 1 if MSB (aka sign bit) of result = 1
ZF - 1 if Result = 0 (i.e. op1=op2)
AF - 1 if Carry in the low nibble of result
PF - 1 if Parity of Least significant byte is even
Para el carácter 5, la comparación con el espacio no toma la rama de igualdad. Después se calcula 0x35 - 0x30 = 5. Los flags que interesan a cada salto son los producidos por su comparación; no se puede asumir que todos estén a cero.
|| 0x560c23c187fb b 83e830 sub eax, 0x30 ; '0'
| || 0x560c23c187fe 83f809 cmp eax, 9 ; 9
| ,===< 0x560c23c18801 771e ja 0x560c23c18821
| ||| ;-- rip:
| ,====< 0x560c23c18803 eb0e jmp 0x560c23c18813
| |||`-> 0x560c23c18805 488d3d020100. lea rdi, qword str.Space. ; 0x560c23c1890e ; "Space. "
El valor se comparará entonces con 9, por lo que los flags quedarán así:
[0x560c23c187f1]> dr 1
cf = 0x00000001
pf = 0x00000001
af = 0x00000001
zf = 0x00000000
sf = 0x00000001
tf = 0x00000000
if = 0x00000001
df = 0x00000000
of = 0x00000000
[0x560c23c187f1]>
ja requiere CF=0 y ZF=0. Al comparar 5 con 9 aparece CF=1, como muestra el registro anterior, así que ese salto no se toma. La ejecución continúa por el jmp que conduce al bloque Digit..
| |||| ; JMP XREF from 0x560c23c18803 (sym.func2)
| `----> 0x560c23c18813 488d3dfc0000. lea rdi, qword str.Digit. ; 0x560c23c18916 ; "Digit."
| ||| 0x560c23c1881a e821feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x560c23c1881f eb0c jmp 0x560c23c1882d
En este punto ya sabemos cómo va a terminar el programa, así que pasamos a nuestro último ejemplo de switch-case.
#include <stdio.h>
func2(){
printf("Enter a key and then press enter: ");
int val;
printf("Select a fruit: \n");
printf("1: Apple\n");
printf("2: Orange\n");
printf("3: Banana\n");
printf("4: Pear\n");
scanf("%d",&val);
switch(val){
case 1:
printf("Apple. \n");
break;
case 2:
printf("Orange. \n");
break;
case 3:
printf("Banana. \n");
break;
case 4:
printf("Pear. \n");
break;
default: printf("Nothing selected.\n");
}
}
main(){
func2();
getchar();
}
Podemos inspeccionarlo fácilmente con el modo visual de radare2.
| cmp eax, 2 |
| je 0x854;[gd] |
`----------------------------------------------------------'
| |
| '--------------------------------------------.
.----------------------' |
| |
| |
.--------------------. .---------------------------------------------.
| 0x82e ;[gg] | | 0x854 ;[gd] |
| cmp eax, 2 | | ; JMP XREF from 0x0000082c (sym.func2) |
| jg 0x83a;[gf] | | ; 0x9ac |
`--------------------' | ; "Orange. " |
| | | lea rdi, qword str.Orange. |
| | | call sym.imp.puts;[gb] |
| | | jmp 0x88a;[gp] |
| | `---------------------------------------------'
| | |
| '---------------------. |
.-------------------------------------' | |
| | '------------------.
| | |
| | |
.--------------------. .---------------------------------------------. |
| 0x833 ;[gi] | | 0x83a ;[gf] | |
| cmp eax, 1 | | ; JMP XREF from 0x00000831 (sym.func2) | |
| je 0x846;[gh] | | cmp eax, 3 | |
`--------------------' | je 0x862;[gl] | |
| | `---------------------------------------------' |
| | | | |
| '--. | | |
.------------------' | | | |
| | | '--------------. |
| | .------' | |
| | | | |
| | | | |
.--------------------. .---------------------------------------------. .--------------------. .---------------------------------------------. |
| 0x838 ;[gk] | | 0x846 ;[gh] | | 0x83f ;[gn] | | 0x862 ;[gl] | |
| jmp 0x87e;[gj] | | ; JMP XREF from 0x00000836 (sym.func2) | | cmp eax, 4 | | ; JMP XREF from 0x0000083d (sym.func2) | |
`--------------------' | ; 0x9a4 | | je 0x870;[gm] | | ; 0x9b5 | |
| | ; "Apple. " | `--------------------' | ; "Banana. " | |
| | lea rdi, qword str.Apple. | | | | lea rdi, qword str.Banana. | |
| | call sym.imp.puts;[gb] | | | | call sym.imp.puts;[gb] | |
| | jmp 0x88a;[gp] | | | | jmp 0x88a;[gp] | |
| `---------------------------------------------' | | `---------------------------------------------' |
El programa lee la elección del usuario y compara el valor con cada una de las opciones; si no coincide, sigue comparando hasta evaluar todas las condiciones. Deberías poder hacer el análisis por tu cuenta sin mayor dificultad.
Bucles While y For
Los bucles repiten un bloque mientras se cumpla una condición. Tanto while como for pueden producir comparaciones y saltos hacia un bloque anterior. Para reconstruirlos hay que localizar la inicialización, la condición y la actualización; el número de iteraciones depende de esos elementos.
#include <stdio.h>
func2(){
int num;
printf("Enter a num, (exit with 0):");
scanf("%d", &num);
while(num != 0){
if(num > 0) printf("Positive num\n");
else printf("Negative num\n");
printf("Enter another num (exit with 0):");
scanf("%d", &num);
}
}
main(){
func2();
getchar();
}
Dentro de radare2, la función func2 tendrá este aspecto:
[0x7fba89d68090]> s sym.func2
[0x55b75b1ba7aa]> pdf
/ (fcn) sym.func2 170
| sym.func2 ();
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x55b75b1ba85d (sym.main)
| 0x55b75b1ba7aa 55 push rbp
| 0x55b75b1ba7ab 4889e5 mov rbp, rsp
| 0x55b75b1ba7ae 4883ec10 sub rsp, 0x10
| 0x55b75b1ba7b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x55b75b1ba7bb 488945f8 mov qword [local_8h], rax
| 0x55b75b1ba7bf 31c0 xor eax, eax
| 0x55b75b1ba7c1 488d3d300100. lea rdi, qword str.Enter_a_num___exit_with_0_: ; 0x55b75b1ba8f8 ; "Enter a num, (exit with 0):"
| 0x55b75b1ba7c8 b800000000 mov eax, 0
| 0x55b75b1ba7cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x55b75b1ba7d2 488d45f4 lea rax, qword [local_ch]
| 0x55b75b1ba7d6 4889c6 mov rsi, rax
| 0x55b75b1ba7d9 488d3d340100. lea rdi, qword [0x55b75b1ba914] ; "%d"
| 0x55b75b1ba7e0 b800000000 mov eax, 0
| 0x55b75b1ba7e5 e896feffff call sym.imp.__isoc99_scanf
| ,=< 0x55b75b1ba7ea eb4a jmp 0x55b75b1ba836
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [local_ch]
| :| 0x55b75b1ba7ef 85c0 test eax, eax
| ,===< 0x55b75b1ba7f1 7e0e jle 0x55b75b1ba801
| |:| 0x55b75b1ba7f3 488d3d1d0100. lea rdi, qword str.Positive_num ; 0x55b75b1ba917 ; "Positive num"
| |:| 0x55b75b1ba7fa e841feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x55b75b1ba7ff eb0c jmp 0x55b75b1ba80d
| |`---> 0x55b75b1ba801 488d3d1c0100. lea rdi, qword str.Negative_num ; 0x55b75b1ba924 ; "Negative num"
| | :| 0x55b75b1ba808 e833feffff call sym.imp.puts ; int puts(const char *s)
| | :| ; JMP XREF from 0x55b75b1ba7ff (sym.func2)
| `----> 0x55b75b1ba80d 488d3d240100. lea rdi, qword str.Enter_another_num__exit_with_0_: ; 0x55b75b1ba938 ; "Enter another num (exit with 0):"
| :| 0x55b75b1ba814 b800000000 mov eax, 0
| :| 0x55b75b1ba819 e842feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x55b75b1ba81e 488d45f4 lea rax, qword [local_ch]
| :| 0x55b75b1ba822 4889c6 mov rsi, rax
| :| 0x55b75b1ba825 488d3de80000. lea rdi, qword [0x55b75b1ba914] ; "%d"
| :| 0x55b75b1ba82c b800000000 mov eax, 0
| :| 0x55b75b1ba831 e84afeffff call sym.imp.__isoc99_scanf
| :| ; JMP XREF from 0x55b75b1ba7ea (sym.func2)
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [local_ch]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
| 0x55b75b1ba83d 90 nop
| 0x55b75b1ba83e 488b55f8 mov rdx, qword [local_8h]
| 0x55b75b1ba842 644833142528. xor rdx, qword fs:[0x28]
| ,=< 0x55b75b1ba84b 7405 je 0x55b75b1ba852
| | 0x55b75b1ba84d e8fefdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
| `-> 0x55b75b1ba852 c9 leave
\ 0x55b75b1ba853 c3 ret
[0x55b75b1ba7aa]>
La lógica del programa aquí es bastante similar a los bloques if-else anteriores, en términos generales. La diferencia es que vemos una flecha que vuelve hacia arriba desde la instrucción ubicada en 0x55b75b1ba83b, lo que captura la esencia de un bucle. Vamos paso a paso.
En primer lugar, el programa reserva espacio y, como se puede ver, usa un par de variables: una relacionada con el guarda de pila y la otra probablemente con la entrada del usuario.
| ; var int local_ch @ rbp-0xc
| ; var int local_8h @ rbp-0x8
| ; CALL XREF from 0x55b75b1ba85d (sym.main)
| 0x55b75b1ba7aa 55 push rbp
| 0x55b75b1ba7ab 4889e5 mov rbp, rsp
| 0x55b75b1ba7ae 4883ec10 sub rsp, 0x10
| 0x55b75b1ba7b2 64488b042528. mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
| 0x55b75b1ba7bb 488945f8 mov qword [local_8h], rax
| 0x55b75b1ba7bf 31c0 xor eax, eax
El programa solicita la entrada del usuario y la almacena en la variable local_ch.
| 0x55b75b1ba7cd e88efeffff call sym.imp.printf ; int printf(const char *format)
| 0x55b75b1ba7d2 488d45f4 lea rax, qword [local_ch]
| 0x55b75b1ba7d6 4889c6 mov rsi, rax
| 0x55b75b1ba7d9 488d3d340100. lea rdi, qword [0x55b75b1ba914] ; "%d"
| 0x55b75b1ba7e0 b800000000 mov eax, 0
| 0x55b75b1ba7e5 e896feffff call sym.imp.__isoc99_scanf
Podemos incluso renombrar esa variable para que todo sea más legible.
[0x55b75b1ba7aa]> afvn local_ch input
[0x55b75b1ba7aa]> afvn
local_8h
input
[0x55b75b1ba7aa]>
Continuamos. Justo después del scanf, realizamos un salto al final de este bloque de código, así:
| ,=< 0x55b75b1ba7ea eb4a jmp 0x55b75b1ba836
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [input]
Y si recordamos, la condición de salida del bucle era que el valor de entrada fuera igual a cero, así que en este caso, si la entrada no es cero, jne se activará y nos llevará de vuelta al inicio del bucle.
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [input]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
El siguiente bloque de código ya lo conocemos.
| .--> 0x55b75b1ba7ec 8b45f4 mov eax, dword [input]
| :| 0x55b75b1ba7ef 85c0 test eax, eax
| ,===< 0x55b75b1ba7f1 7e0e jle 0x55b75b1ba801
| |:| 0x55b75b1ba7f3 488d3d1d0100. lea rdi, qword str.Positive_num ; 0x55b75b1ba917 ; "Positive num"
| |:| 0x55b75b1ba7fa e841feffff call sym.imp.puts ; int puts(const char *s)
| ,====< 0x55b75b1ba7ff eb0c jmp 0x55b75b1ba80d
| |`---> 0x55b75b1ba801 488d3d1c0100. lea rdi, qword str.Negative_num ; 0x55b75b1ba924 ; "Negative num"
| | :| 0x55b75b1ba808 e833feffff call sym.imp.puts ; int puts(const char *s)
| | :| ; JMP XREF from 0x55b75b1ba7ff (sym.func2)
El programa comprueba si la entrada es positiva o negativa mediante test y el salto correspondiente.
Justo después, vuelve a pedir una entrada y comprueba la condición de salida al final del bloque.
| `----> 0x55b75b1ba80d 488d3d240100. lea rdi, qword str.Enter_another_num__exit_with_0_: ; 0x55b75b1ba938 ; "Enter another num (exit with 0):"
| :| 0x55b75b1ba814 b800000000 mov eax, 0
| :| 0x55b75b1ba819 e842feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x55b75b1ba81e 488d45f4 lea rax, qword [input]
| :| 0x55b75b1ba822 4889c6 mov rsi, rax
| :| 0x55b75b1ba825 488d3de80000. lea rdi, qword [0x55b75b1ba914] ; "%d"
| :| 0x55b75b1ba82c b800000000 mov eax, 0
| :| 0x55b75b1ba831 e84afeffff call sym.imp.__isoc99_scanf
| :| ; JMP XREF from 0x55b75b1ba7ea (sym.func2)
| :`-> 0x55b75b1ba836 8b45f4 mov eax, dword [input]
| : 0x55b75b1ba839 85c0 test eax, eax
| `==< 0x55b75b1ba83b 75af jne 0x55b75b1ba7ec
La otra forma habitual de crear bucles es mediante for, como se puede ver aquí:
#include <stdio.h>
func2(){
int counter = 0;
for(counter=1; counter <=10; counter++){
printf("%d ", counter);
}
}
main(){
func2();
getchar();
}
Y el desensamblado en radare2 tendrá este aspecto:
[0x0000068a]> pdf
/ (fcn) sym.func2 59
| sym.func2 ();
| ; var int input @ rbp-0x4
| ; CALL XREF from 0x000006ce (sym.main)
| 0x0000068a 55 push rbp
| 0x0000068b 4889e5 mov rbp, rsp
| 0x0000068e 4883ec10 sub rsp, 0x10
| 0x00000692 c745fc000000. mov dword [input], 0
| 0x00000699 c745fc010000. mov dword [input], 1
| ,=< 0x000006a0 eb1a jmp 0x6bc
| | ; JMP XREF from 0x000006c0 (sym.func2)
| .--> 0x000006a2 8b45fc mov eax, dword [input]
| :| 0x000006a5 89c6 mov esi, eax
| :| 0x000006a7 488d3db60000. lea rdi, qword [0x00000764] ; "%d "
| :| 0x000006ae b800000000 mov eax, 0
| :| 0x000006b3 e898feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x000006b8 8345fc01 add dword [input], 1
| :| ; JMP XREF from 0x000006a0 (sym.func2)
| :`-> 0x000006bc 837dfc0a cmp dword [input], 0xa ; [0xa:4]=0
| `==< 0x000006c0 7ee0 jle 0x6a2
| 0x000006c2 90 nop
| 0x000006c3 c9 leave
\ 0x000006c4 c3 ret
[0x0000068a]>
En esta compilación no aparece un canario en la función. La instrumentación depende de las opciones del compilador y de las características de cada función; la presencia de una entrada del usuario, por sí sola, no determina que se inserte una protección de pila.
El programa empieza poniendo el valor a cero (counter = 0) y luego lo pone a 1 para iniciar el for.
| 0x00000692 c745fc000000. mov dword [input], 0
| 0x00000699 c745fc010000. mov dword [input], 1
| ,=< 0x000006a0 eb1a jmp 0x6bc
A continuación compara el valor con 10 (0xA).
| :`-> 0x000006bc 837dfc0a cmp dword [input], 0xa ; [0xa:4]=0
| `==< 0x000006c0 7ee0 jle 0x6a2
Como el valor es menor, vuelve al inicio para ejecutar el código del bucle.
El código básicamente imprime el valor de la variable y le suma 1.
| .--> 0x000006a2 8b45fc mov eax, dword [input]
| :| 0x000006a5 89c6 mov esi, eax
| :| 0x000006a7 488d3db60000. lea rdi, qword [0x00000764] ; "%d "
| :| 0x000006ae b800000000 mov eax, 0
| :| 0x000006b3 e898feffff call sym.imp.printf ; int printf(const char *format)
| :| 0x000006b8 8345fc01 add dword [input], 1
En la siguiente lección empezaremos a explorar estructuras de datos como arrays y matrices.
Interpretar un condicional.
Hermes localiza la condición del ejemplo positive. La comprobación manual permite seguir el salto y las dos rutas dentro de func2.
Leer la explicación y los comandos
La consulta
En positive, localiza la comparación del entero con cero y el salto condicional. ¿Qué valores siguen cada rama? Apoya la explicación en el desensamblado.
Qué muestra la sesión
El ejemplo lee un entero y muestra un mensaje cuando es positivo. Hermes identifica la función func2 y examina la secuencia que decide si se imprime el mensaje.
En esta compilación, test eax, eax actualiza los flags a partir del entero. El salto jle de 0x4011fb conduce a 0x401207 cuando el valor es cero o negativo. Para un valor positivo, la ejecución llega a puts en 0x401202.
La consola muestra esa misma secuencia y permite comprobar el destino del salto. El vídeo recoge el análisis del flujo; no ejecuta pruebas con distintas entradas.
Comprobación desde la consola
En ./coursectl console, muestra la función que contiene el condicional:
pdf @ sym.func2
La segunda grabación utiliza positive, otro ejemplo del laboratorio 3. Puedes abrirlo con ./coursectl chat 3 --example positive. Las direcciones pueden variar al recompilar. Descargar el fuente del vídeo ↙ · Preparar el companion ↗
Vuelve al binario
Seguir llamadas, distinguir ramas de un switch e identificar el retorno de un bucle. 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: switch-fruit.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 3 --example switch-fruit
./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.