Distinguir lo que resuelve el preprocesador de lo que existe en el ejecutable.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 12 --example unionEstoy siguiendo «Macros, uniones y campos de bits» del curso de Lobera. Distinguir lo que resuelve el preprocesador de lo que existe en el ejecutable. 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.
Las uniones permiten compartir almacenamiento y los campos de bits distribuyen varios valores dentro de una unidad. Las macros actúan antes de compilar. Estudiaremos cómo se refleja cada mecanismo en las instrucciones y en los accesos a memoria.
Uniones
En C, las uniones son estructuras de datos especiales que permiten almacenar distintos tipos de datos en la misma posición de memoria. Se puede definir una unión con varios miembros, pero en un momento dado solo uno de ellos puede contener un valor. Las uniones ofrecen una forma eficiente de reutilizar la misma posición de memoria para varios propósitos. Estas estructuras aparecen con frecuencia en sistemas con poco espacio disponible para el programa, como los sistemas embebidos.
Empezaremos revisando este ejemplo:
#include <stdio.h>
int main() {
union {
char ichar; /* 1 byte */
int num; /* 4 bytes */
} sample;
int n1, n2;
printf("Size of 'sample' union = %d \n", sizeof(sample));
sample.num = 25;
sample.ichar = 50;
printf("%d", sample.num);
getchar();
getchar();
return 0;
}
Si compilamos y ejecutamos este programa, veremos lo siguiente:
$ ./union1
Size of 'sample' union = 4
50
Como se puede observar, la unión tomó la variable más grande —el int en este caso— para definir el tamaño: 4 bytes que pueden albergar tanto un int como un char.
[0x7fb4c26cb090]> s main
[0x55b90da1b145]> pdf
; DATA XREF from entry0 @ 0x55b90da1b07d
┌ 80: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_4h @ rbp-0x4
│ 0x55b90da1b145 55 push rbp
│ 0x55b90da1b146 4889e5 mov rbp, rsp
│ 0x55b90da1b149 4883ec10 sub rsp, 0x10
│ 0x55b90da1b14d be04000000 mov esi, 4
│ 0x55b90da1b152 488d3dab0e00. lea rdi, str.Size_of__sample__union____d ; 0x55b90da1c004 ; "Size of 'sample' union = %d \n"
│ 0x55b90da1b159 b800000000 mov eax, 0
│ 0x55b90da1b15e e8cdfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55b90da1b163 c745fc190000. mov dword [var_4h], 0x19 ; 25
│ 0x55b90da1b16a c645fc32 mov byte [var_4h], 0x32 ; '2' ; 50
│ 0x55b90da1b16e 8b45fc mov eax, dword [var_4h]
│ 0x55b90da1b171 89c6 mov esi, eax
│ 0x55b90da1b173 488d3da80e00. lea rdi, [0x55b90da1c022] ; "%d"
│ 0x55b90da1b17a b800000000 mov eax, 0
│ 0x55b90da1b17f e8acfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55b90da1b184 e8b7feffff call sym.imp.getchar ; int getchar(void)
│ 0x55b90da1b189 e8b2feffff call sym.imp.getchar ; int getchar(void)
│ 0x55b90da1b18e b800000000 mov eax, 0
│ 0x55b90da1b193 c9 leave
└ 0x55b90da1b194 c3 ret
[0x55b90da1b145]>
Este concepto resulta muy fácil de entender al desensamblar el código. Primero se carga 0x19 = 25dec en var_4h; después se carga 0x32 = 50dec exactamente en el mismo lugar.
Para la primera carga:
│ 0x55b90da1b163 c745fc190000. mov dword [var_4h], 0x19 ; 25
│ ;-- rip:
│ 0x55b90da1b16a b c645fc32 mov byte [var_4h], 0x32 ; '2' ; 50
[0x55b90da1b16a]> afvd
var var_4h = 0x7ffd1dd3133c = (qword)0x0da1b1a000000019
[0x55b90da1b16a]>
Para la segunda carga:
dc
[0x55b90da1b16e]> afvd
var var_4h = 0x7ffd1dd3133c = (qword)0x0da1b1a000000032
El mismo espacio se actualiza con el nuevo valor, sin más.
Veamos un último ejemplo sobre esto:
#include <stdio.h>
int main() {
union {
char ichar; /* 1 byte */
int num; /* 4 bytes */
char arr[20];
} sample;
int n1, n2;
printf("Size of 'sample' union = %d \n", sizeof(sample));
sample.num = 25;
sample.ichar = 50;
printf("value= %d \n", sample.num);
strcpy(sample.arr,"hello world");
printf("value= %s \n",sample.arr);
sample.num = 65;
printf("value= %s \n",sample.arr);
getchar();
getchar();
return 0;
}
En este ejemplo se ha añadido un array de char, así que ¿cuál será el tamaño ahora? Exacto, 20 bytes. Como tenemos espacio para un array (arr = dirección base) y también referencias a char e int... seguramente ya intuyes que pueden ocurrir cosas extrañas. Veámoslo:
[0x55e376e55155]> pdf
; DATA XREF from entry0 @ 0x55e376e5508d
┌ 194: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55e376e55155 55 push rbp
│ 0x55e376e55156 4889e5 mov rbp, rsp
│ 0x55e376e55159 4883ec20 sub rsp, 0x20
│ 0x55e376e5515d 64488b042528. mov rax, qword fs:[0x28]
│ 0x55e376e55166 488945f8 mov qword [var_8h], rax
│ 0x55e376e5516a 31c0 xor eax, eax
│ 0x55e376e5516c be14000000 mov esi, 0x14 ; 20
│ 0x55e376e55171 488d3d8c0e00. lea rdi, str.Size_of__sample__union____d ; 0x55e376e56004 ; "Size of 'sample' union = %d \n"
│ 0x55e376e55178 b800000000 mov eax, 0
│ 0x55e376e5517d e8befeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55e376e55182 c745e0190000. mov dword [var_20h], 0x19 ; 25
│ 0x55e376e55189 c645e032 mov byte [var_20h], 0x32 ; '2' ; 50
│ 0x55e376e5518d 8b45e0 mov eax, dword [var_20h]
│ 0x55e376e55190 89c6 mov esi, eax
│ 0x55e376e55192 488d3d890e00. lea rdi, str.value___d ; 0x55e376e56022 ; "value= %d \n"
│ 0x55e376e55199 b800000000 mov eax, 0
│ 0x55e376e5519e e89dfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55e376e551a3 488d45e0 lea rax, [var_20h]
│ 0x55e376e551a7 48ba68656c6c. movabs rdx, 0x6f77206f6c6c6568 ; 'hello wo'
│ 0x55e376e551b1 488910 mov qword [rax], rdx
│ 0x55e376e551b4 c74008726c64. mov dword [rax + 8], 0x646c72 ; 'rld'
│ ; [0x646c72:4]=-1
│ 0x55e376e551bb 488d45e0 lea rax, [var_20h]
│ 0x55e376e551bf 4889c6 mov rsi, rax
│ 0x55e376e551c2 488d3d650e00. lea rdi, str.value___s ; 0x55e376e5602e ; "value= %s \n"
│ 0x55e376e551c9 b800000000 mov eax, 0
│ 0x55e376e551ce e86dfeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55e376e551d3 c745e0410000. mov dword [var_20h], 0x41 ; 'A' ; 65
│ 0x55e376e551da 488d45e0 lea rax, [var_20h]
│ 0x55e376e551de 4889c6 mov rsi, rax
│ 0x55e376e551e1 488d3d460e00. lea rdi, str.value___s ; 0x55e376e5602e ; "value= %s \n"
│ 0x55e376e551e8 b800000000 mov eax, 0
│ 0x55e376e551ed e84efeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55e376e551f2 e859feffff call sym.imp.getchar ; int getchar(void)
│ 0x55e376e551f7 e854feffff call sym.imp.getchar ; int getchar(void)
│ 0x55e376e551fc b800000000 mov eax, 0
│ 0x55e376e55201 488b4df8 mov rcx, qword [var_8h]
│ 0x55e376e55205 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x55e376e5520e 7405 je 0x55e376e55215
│ │ 0x55e376e55210 e81bfeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55e376e55215 c9 leave
└ 0x55e376e55216 c3 ret
[0x55e376e55155]>
En este punto del programa:
│ ;-- rip:
│ 0x55e376e551a7 b 48ba68656c6c. movabs rdx, 0x6f77206f6c6c6568 ; 'hello wo'
│ 0x55e376e551b1 488910 mov qword [rax], rdx
│ 0x55e376e551b4 c74008726c64. mov dword [rax + 8], 0x646c72 ; 'rld'
var var_20h = 0x7fffe11a8270 = (qword)0x000055e300000032
La unión contendrá el valor 0x32 en este momento; después se usarán movabs y mov para almacenar la cadena "Hello world" dentro de la unión. Veamos:
│ ; [0x646c72:4]=-1
│ ;-- rip:
│ 0x55e376e551bb b 488d45e0 lea rax, [var_20h]
│ 0x55e376e551bf 4889c6 mov rsi, rax
var var_20h = 0x7fffe11a8270 = (qword)0x6f77206f6c6c6568
[0x55e376e551bb]> pxw @ 0x7fffe11a8270
0x7fffe11a8270 0x6c6c6568 0x6f77206f 0x00646c72 0x000055e3 hello world..U..
0x7fffe11a8280 0xe11a8370 0x00007fff 0xd33c3600 0x910f1334 p........6<.4...
Si solo queremos imprimir la cadena apuntada por esa dirección de memoria, podemos usar ps para ello.
[0x7fffe11a8280]> ps @ 0x7fffe11a8270
hello world
[0x7fffe11a8280]>
Pero entonces tenemos esto:
│ 0x55e376e551d3 c745e0410000. mov dword [var_20h], 0x41 ; 'A' ; 65
│ 0x55e376e551da 488d45e0 lea rax, [var_20h]
│ 0x55e376e551de 4889c6 mov rsi, rax
0x00000041 se mueve a la dirección base del array. ¿Qué crees que ocurrirá? Podría pensarse que la primera letra del array, 'H', se convertirá en 'A' y la cadena quedará como 'Aello World', pero se ha usado un mov completo en lugar de un mov byte... veamos:
[0x55e376e551da]> pxw @ 0x7fffe11a8270
0x7fffe11a8270 0x00000041 0x6f77206f 0x00646c72 0x000055e3 A...o world..U..
[0x55e376e551da]> ps @ 0x7fffe11a8270
A
La escritura de 32 bits almacena 0x00000041. En este sistema little-endian los primeros bytes quedan como 41 00 00 00: una lectura de cadena encuentra A seguida de un terminador nulo. Los bytes posteriores siguen en el objeto, pero quedan fuera de la cadena delimitada por ese terminador.
El ancho de una escritura permite explicar cambios que una lectura de texto oculta. Al reconstruir un buffer, compara sus bytes completos con la interpretación que realiza la función que lo consume.
Como ejercicio, modifica una copia del programa de práctica para escribir únicamente el primer carácter. Compara el ancho de la instrucción y los bytes que permanecen en la unión.
Campos de bits
Los campos de bits guardan cierta similitud con las uniones. Son fáciles de entender: permiten definir structs con variables de un tamaño específico de N bits, reduciendo así el espacio necesario. Por ejemplo, un struct típico con dos int ocupa 8 bytes; si esos int sirven como flags de verdadero/falso, solo se necesitan los valores 0 y 1, por lo que no tiene sentido reservar espacio para números tan grandes como 0xFFFFFFF. Con un solo bit basta, y aquí es donde los campos de bits resultan muy útiles.
Empecemos con este ejemplo:
#include <stdio.h>
#include <string.h>
/* define simple structure */
struct {
unsigned int widthValidated;
unsigned int heightValidated;
} status1;
/* define a structure with bit fields */
struct {
unsigned int widthValidated : 1;
unsigned int heightValidated : 1;
} status2;
int main( ) {
printf( "Memory size occupied by status1 : %d\n", sizeof(status1));
printf( "Memory size occupied by status2 : %d\n", sizeof(status2));
return 0;
}
$ ./bitfield1
Memory size occupied by status1 : 8
Memory size occupied by status2 : 4
Como se puede ver, el segundo struct ocupa la mitad de espacio que el primero. Con esa idea clara, vamos a desensamblar el siguiente:
#include <stdio.h>
#include <string.h>
struct {
unsigned int age : 3;
} Age;
#include <stdio.h>
#include <string.h>
struct {
unsigned int age : 3;
} Age;
int main( ) {
Age.age = 4;
printf( "Sizeof( Age ) : %d\n", sizeof(Age) );
printf( "Age.age : %d\n", Age.age );
Age.age = 7;
printf( "Age.age : %d\n", Age.age );
Age.age = 8;
printf( "Age.age : %d\n", Age.age );
return 0;
}
El desensamblado tendrá este aspecto:
[0x564052630135]> pdf
; DATA XREF from entry0 @ 0x56405263006d
┌ 180: int main (int argc, char **argv, char **envp);
│ 0x564052630135 55 push rbp
│ 0x564052630136 4889e5 mov rbp, rsp
│ 0x564052630139 0fb605d42e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x564052630140 83e0f8 and eax, 0xfffffff8 ; 4294967288
│ 0x564052630143 83c804 or eax, 4
│ 0x564052630146 8805c82e0000 mov byte [0x564052633014], al ; [0x564052633014:1]=0
│ 0x56405263014c be04000000 mov esi, 4
│ 0x564052630151 488d3dac0e00. lea rdi, str.Sizeof__Age___:__d ; 0x564052631004 ; "Sizeof( Age ) : %d\n"
│ 0x564052630158 b800000000 mov eax, 0
│ 0x56405263015d e8cefeffff call sym.imp.printf ; int printf(const char *format)
│ 0x564052630162 0fb605ab2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x564052630169 83e007 and eax, 7
│ 0x56405263016c 0fb6c0 movzx eax, al
│ 0x56405263016f 89c6 mov esi, eax
│ 0x564052630171 488d3da00e00. lea rdi, str.Age.age_:__d ; 0x564052631018 ; "Age.age : %d\n"
│ 0x564052630178 b800000000 mov eax, 0
│ 0x56405263017d e8aefeffff call sym.imp.printf ; int printf(const char *format)
│ 0x564052630182 0fb6058b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x564052630189 83c807 or eax, 7
│ 0x56405263018c 8805822e0000 mov byte [0x564052633014], al ; [0x564052633014:1]=0
│ 0x564052630192 0fb6057b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x564052630199 83e007 and eax, 7
│ 0x56405263019c 0fb6c0 movzx eax, al
│ 0x56405263019f 89c6 mov esi, eax
│ 0x5640526301a1 488d3d700e00. lea rdi, str.Age.age_:__d ; 0x564052631018 ; "Age.age : %d\n"
│ 0x5640526301a8 b800000000 mov eax, 0
│ 0x5640526301ad e87efeffff call sym.imp.printf ; int printf(const char *format)
│ 0x5640526301b2 0fb6055b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x5640526301b9 83e0f8 and eax, 0xfffffff8 ; 4294967288
│ 0x5640526301bc 8805522e0000 mov byte [0x564052633014], al ; [0x564052633014:1]=0
│ 0x5640526301c2 0fb6054b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x5640526301c9 83e007 and eax, 7
│ 0x5640526301cc 0fb6c0 movzx eax, al
│ 0x5640526301cf 89c6 mov esi, eax
│ 0x5640526301d1 488d3d400e00. lea rdi, str.Age.age_:__d ; 0x564052631018 ; "Age.age : %d\n"
│ 0x5640526301d8 b800000000 mov eax, 0
│ 0x5640526301dd e84efeffff call sym.imp.printf ; int printf(const char *format)
│ 0x5640526301e2 b800000000 mov eax, 0
│ 0x5640526301e7 5d pop rbp
└ 0x5640526301e8 c3 ret
La carga obtiene la unidad que contiene age. La máscara 0xfffffff8 pone a cero sus tres bits inferiores y conserva los demás. El or posterior incorpora el valor 4 en ese campo. Esta secuencia permite cambiar un miembro sin sobrescribir los bits que pertenecen a otros.
│ 0x564052630139 0fb605d42e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=0
│ 0x564052630140 b 83e0f8 and eax, 0xfffffff8 ; 4294967288
│ 0x564052630143 83c804 or eax, 4
[0x564052630146]> dr
rax = 0x00000004
A continuación la variable se actualiza con 4; más adelante, al cargar un 7, se realiza otro OR:
│ 0x564052630182 b 0fb6058b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=4
│ 0x564052630189 83c807 or eax, 7
│ 0x56405263018c 8805822e0000 mov byte [0x564052633014], al ; [0x564052633014:1]=4
[0x564052630182]> ds
[0x564052630189]> dr eax
0x00000004
[0x564052630189]> ds
[0x56405263018c]> dr eax
0x00000007
4 | 7 produce 7 porque combina los bits que están a uno en cualquiera de los operandos. OR no calcula el máximo: 4 | 3, por ejemplo, también produce 7. La secuencia del listado debe interpretarse como máscara y actualización del campo.
¿Qué ocurre al final de este programa cuando intentamos cargar 8dec dentro del mapa de bits?
│ 0x5640526301b2 0fb6055b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=4
│ 0x5640526301b9 83e0f8 and eax, 0xfffffff8 ; 4294967288
│ 0x5640526301bc 8805522e0000 mov byte [0x564052633014], al ; [0x564052633014:1]=4
│ 0x5640526301c2 0fb6054b2e00. movzx eax, byte [0x564052633014] ; [0x564052633014:1]=4
│ 0x5640526301c9 83e007 and eax, 7
│ 0x5640526301cc 0fb6c0 movzx eax, al
│ 0x5640526301cf 89c6 mov esi, eax
│ 0x5640526301d1 488d3d400e00. lea rdi, str.Age.age_:__d ; 0x564052631018 ; "Age.age : %d\n"
│ 0x5640526301d8 b800000000 mov eax, 0
El campo sin signo de 3 bits puede representar valores de 0 a 7. En este ejemplo, asignar 8 conserva sus tres bits inferiores, 000, y se observa cero. Otros valores producen otros residuos; el ancho del campo determina qué parte de la representación queda almacenada.
Defines
Estamos llegando al final de esta primera parte del curso. En C, la directiva #define permite definir macros dentro del código fuente. Estas definiciones de macro permiten declarar valores constantes para usarlos a lo largo del código.
Las definiciones de macro no son variables y el código del programa no puede modificarlas como sí haría con variables.
Esta respuesta de Stack Overflow es muy clara sobre el tema: https://stackoverflow.com/questions/4024318/why-do-most-c-developers-use-define-instead-of-const
#include <stdio.h>
#define SUM(x,y) x+y
#define MAX 10
int main() {
int n1, n2;
printf("VAL 1 = ");
scanf("%d", &n1);
printf("VAL 2 = ");
scanf("%d", &n2);
printf("SUM = %d\n", SUM(n1,n2));
if(SUM(n1,n2)>MAX){
printf("> MAX\n");
}
getchar();
getchar();
return 0;
}
Veamos el interior:
[0x55817740f175]> pdf
; DATA XREF from entry0 @ 0x55817740f0ad
┌ 194: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_ch @ rbp-0xc
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55817740f175 55 push rbp
│ 0x55817740f176 4889e5 mov rbp, rsp
│ 0x55817740f179 4883ec10 sub rsp, 0x10
│ 0x55817740f17d 64488b042528. mov rax, qword fs:[0x28]
│ 0x55817740f186 488945f8 mov qword [var_8h], rax
│ 0x55817740f18a 31c0 xor eax, eax
│ 0x55817740f18c 488d3d710e00. lea rdi, str.VAL_1 ; 0x558177410004 ; "VAL 1 = "
│ 0x55817740f193 b800000000 mov eax, 0
│ 0x55817740f198 e8b3feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55817740f19d 488d45f0 lea rax, [var_10h]
│ 0x55817740f1a1 4889c6 mov rsi, rax
│ 0x55817740f1a4 488d3d620e00. lea rdi, [0x55817741000d] ; "%d"
│ 0x55817740f1ab b800000000 mov eax, 0
│ 0x55817740f1b0 e8bbfeffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x55817740f1b5 488d3d540e00. lea rdi, str.VAL_2 ; 0x558177410010 ; "VAL 2 = "
│ 0x55817740f1bc b800000000 mov eax, 0
│ 0x55817740f1c1 e88afeffff call sym.imp.printf ; int printf(const char *format)
│ 0x55817740f1c6 488d45f4 lea rax, [var_ch]
│ 0x55817740f1ca 4889c6 mov rsi, rax
│ 0x55817740f1cd 488d3d390e00. lea rdi, [0x55817741000d] ; "%d"
│ 0x55817740f1d4 b800000000 mov eax, 0
│ 0x55817740f1d9 e892feffff call sym.imp.__isoc99_scanf ; int scanf(const char *format)
│ 0x55817740f1de 8b55f0 mov edx, dword [var_10h]
│ 0x55817740f1e1 8b45f4 mov eax, dword [var_ch]
│ 0x55817740f1e4 01d0 add eax, edx
│ 0x55817740f1e6 89c6 mov esi, eax
│ 0x55817740f1e8 488d3d2a0e00. lea rdi, str.SUM____d ; 0x558177410019 ; "SUM = %d\n"
│ 0x55817740f1ef b800000000 mov eax, 0
│ 0x55817740f1f4 e857feffff call sym.imp.printf ; int printf(const char *format)
│ 0x55817740f1f9 8b55f0 mov edx, dword [var_10h]
│ 0x55817740f1fc 8b45f4 mov eax, dword [var_ch]
│ 0x55817740f1ff 01d0 add eax, edx
│ 0x55817740f201 83f80a cmp eax, 0xa ; 10
│ ┌─< 0x55817740f204 7e0c jle 0x55817740f212
│ │ 0x55817740f206 488d3d160e00. lea rdi, str.MAX ; 0x558177410023 ; "> MAX"
│ │ 0x55817740f20d e81efeffff call sym.imp.puts ; int puts(const char *s)
│ └─> 0x55817740f212 e849feffff call sym.imp.getchar ; int getchar(void)
│ 0x55817740f217 e844feffff call sym.imp.getchar ; int getchar(void)
│ 0x55817740f21c b800000000 mov eax, 0
│ 0x55817740f221 488b4df8 mov rcx, qword [var_8h]
│ 0x55817740f225 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x55817740f22e 7405 je 0x55817740f235
│ │ 0x55817740f230 e80bfeffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55817740f235 c9 leave
└ 0x55817740f236 c3 ret
[0x55817740f175]>
No hace falta depurar el código. Como se puede ver, los defines son simplemente directrices para el compilador: este insertará el equivalente de esa operación allá donde se invoque. Al final, la suma se realiza de la misma manera; no se llama a ninguna función, solo hay un add en ensamblador:
│ 0x55817740f1de 8b55f0 mov edx, dword [var_10h]
│ 0x55817740f1e1 8b45f4 mov eax, dword [var_ch]
│ 0x55817740f1e4 01d0 add eax, edx
│ 0x55817740f1e6 89c6 mov esi, eax
│ 0x55817740f1e8 488d3d2a0e00. lea rdi, str.SUM____d ; 0x558177410019 ; "SUM = %d\n"
Lo mismo ocurre con la constante: se insertará un 0xA en cada lugar donde se referencie MAX.
│ 0x55817740f1ff 01d0 add eax, edx
│ 0x55817740f201 83f80a cmp eax, 0xa ; 10
│ ┌─< 0x55817740f204 7e0c jle 0x55817740f212
Otra forma de trabajar con macros del compilador es incluir código de otros ficheros, pero no profundizaré en eso porque no aparece nada especial en el desensamblado. Pruébalo tú mismo.
Con esto terminamos la parte más básica del curso. A partir de aquí, presentaré algunos crackmes como examen y práctica, y después pasaremos a la siguiente parte, donde abordaré conceptos más avanzados: pipes, procesos (forks), hilos y sockets, diferenciando claramente el código de Windows y Unix, ya que algunas cosas como los sockets funcionan de forma ligeramente distinta en cada sistema operativo. También veremos funcionalidades de r2 que aún no hemos mostrado, como ESIL.
Vuelve al binario
Distinguir lo que resuelve el preprocesador de lo que existe en el ejecutable. 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: union.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 12 --example union
./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.