Ú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 / 14

Macros, uniones y campos de bits

LoberaFormación técnica14 / 33 · 5 min de lectura · Linux · x86 / x86-64
14 / 33

Distinguir lo que resuelve el preprocesador de lo que existe en el ejecutable.

Consultar conceptos ↗

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:

C / 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:

CÓDIGO / SALIDA
$ ./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.

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
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:

C / EJEMPLO
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           ;-- 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:

ENSAMBLADOR / REFERENCIA
                                                                      ; [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.

CÓDIGO / SALIDA
[0x7fffe11a8280]> ps @ 0x7fffe11a8270
hello world
[0x7fffe11a8280]> 

Pero entonces tenemos esto:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[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:

C / 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;
}
CÓDIGO / SALIDA
$ ./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:

C / EJEMPLO
#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:

C / EJEMPLO
[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.

CÓDIGO / SALIDA
│           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:

ENSAMBLADOR / REFERENCIA
           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?

ENSAMBLADOR / REFERENCIA
           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

C / EJEMPLO
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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.

ENSAMBLADOR / REFERENCIA
           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.

Práctica manual

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.

TERMINAL / COMPANION
./coursectl start 12 --example union
./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.