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

Aritmética de punteros y estructuras dinámicas

LoberaFormación técnica12 / 33 · 7 min de lectura · Linux · x86 / x86-64
12 / 33

Seguir el paso de direcciones a funciones y la construcción de estructuras en el heap.

Consultar conceptos ↗
Notas de la práctica

Avanzar un puntero sobre un objeto escalar no autoriza a acceder a otro objeto contiguo. Para practicar aritmética de punteros usa los ejemplos de arrays. El laboratorio pointer-argument permite seguir una modificación por referencia sin depender de accesos fuera del objeto.

Los punteros y la memoria dinámica en general son temas muy relevantes que desempeñan un papel fundamental en los programas modernos y merecen más de una entrada. Si quieres adentrarte en la investigación de vulnerabilidades o en la escritura de exploits, entender a la perfección cómo se gestiona la memoria en el software moderno es imprescindible. Si tu objetivo es la investigación en seguridad o el reversing, la capacidad de interpretar qué está haciendo el programa (o qué está haciendo la CPU) en un momento dado y en un contexto concreto es fundamental.

Avanzar un puntero

Empecemos con el siguiente programa:

C
#include <stdio.h>
#include <stdlib.h>

main() {

int * ipoint;


  ipoint  = (int *) malloc (sizeof(int));
  *ipoint = 3;


  printf ("%p \n",ipoint);


  printf ("%i\n",*ipoint);  

  ipoint ++;

  printf ("%p\n",ipoint);


  printf ("%i\n",*ipoint); 
  getchar();
}

El programa comienza reservando en el heap un espacio de memoria del tamaño de un int; se declara un puntero a ese espacio. Dicho espacio se inicializa con el valor 3 en decimal. Observa que * se usa para referenciar el espacio apuntado por el puntero: si no se usa *, ipoint = dirección. A continuación se imprime la dirección de memoria y luego el valor. Después el programa ejecuta ipoint++. ¿Qué ocurrirá? ¿3+1? ¿O dirección de memoria +1?

Ya deberías saberlo... pero veámoslo dentro de radare2:

En este ejemplo ya sabemos qué buscamos, pero radare2 ofrece formas muy útiles de presentar información interesante que permiten llegar antes al punto clave. Probemos con agc:

CÓDIGO / SALIDA
[0x55a93cbe56da]> agc
                          ┌────────────────────┐
                          │  main              │
                          └────────────────────┘
                                v
                                │
      ┌─────────────────────────│
      │                         └─────────────────────────┐
      │                         │                         │
┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐
│  sym.imp.malloc    │    │  sym.imp.printf    │    │  sym.imp.getchar   │
└────────────────────┘    └────────────────────┘    └────────────────────┘

agc analiza el grafo de llamadas, por lo que en este caso podemos identificar rápidamente que dentro de main se llaman malloc, printf y getchar. Esto resulta útil cuando existen «circuitos» o «caminos» interesantes entre funciones que queremos analizar.

Continuemos con pdf:

C / EJEMPLO
[0x55a93cbe56da]> pdf
            ; DATA XREF from entry0 @ 0x55a93cbe55ed
 147: int main (int argc, char **argv, char **envp);
           ; var int64_t var_8h @ rbp-0x8
           0x55a93cbe56da      55             push rbp
           0x55a93cbe56db      4889e5         mov rbp, rsp
           0x55a93cbe56de      4883ec10       sub rsp, 0x10
           0x55a93cbe56e2      bf04000000     mov edi, 4
           0x55a93cbe56e7      e8c4feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
           0x55a93cbe56ec      488945f8       mov qword [var_8h], rax
           0x55a93cbe56f0      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe56f4      c70003000000   mov dword [rax], 3
           0x55a93cbe56fa      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe56fe      4889c6         mov rsi, rax
           0x55a93cbe5701      488d3dec0000.  lea rdi, str.p          ; 0x55a93cbe57f4 ; "%p \n"
           0x55a93cbe5708      b800000000     mov eax, 0
           0x55a93cbe570d      e87efeffff     call sym.imp.printf     ; int printf(const char *format)
           0x55a93cbe5712      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe5716      8b00           mov eax, dword [rax]
           0x55a93cbe5718      89c6           mov esi, eax
           0x55a93cbe571a      488d3dd80000.  lea rdi, [0x55a93cbe57f9] ; "%i\n"
           0x55a93cbe5721      b800000000     mov eax, 0
           0x55a93cbe5726      e865feffff     call sym.imp.printf     ; int printf(const char *format)
           0x55a93cbe572b      488345f804     add qword [var_8h], 4
           0x55a93cbe5730      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe5734      4889c6         mov rsi, rax
           0x55a93cbe5737      488d3dbf0000.  lea rdi, [0x55a93cbe57fd] ; "%p\n"
           0x55a93cbe573e      b800000000     mov eax, 0
           0x55a93cbe5743      e848feffff     call sym.imp.printf     ; int printf(const char *format)
           0x55a93cbe5748      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe574c      8b00           mov eax, dword [rax]
           0x55a93cbe574e      89c6           mov esi, eax
           0x55a93cbe5750      488d3da20000.  lea rdi, [0x55a93cbe57f9] ; "%i\n"
           0x55a93cbe5757      b800000000     mov eax, 0
           0x55a93cbe575c      e82ffeffff     call sym.imp.printf     ; int printf(const char *format)
           0x55a93cbe5761      e83afeffff     call sym.imp.getchar    ; int getchar(void)
           0x55a93cbe5766      b800000000     mov eax, 0
           0x55a93cbe576b      c9             leave
           0x55a93cbe576c      c3             ret
[0x55a93cbe56da]> 

Como de costumbre, diseccionemos la función en varias partes; la primera llama a malloc:

ENSAMBLADOR / REFERENCIA
           0x55a93cbe56de      4883ec10       sub rsp, 0x10
           0x55a93cbe56e2      bf04000000     mov edi, 4
           0x55a93cbe56e7      e8c4feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
           0x55a93cbe56ec      488945f8       mov qword [var_8h], rax

Como vemos, el programa reserva espacio en la pila para variables estáticas y reserva 4 bytes mediante malloc, almacenando la dirección resultante (el puntero) en var_8h.

ENSAMBLADOR / REFERENCIA
           0x55a93cbe56fe      4889c6         mov rsi, rax
           0x55a93cbe5701      488d3dec0000.  lea rdi, str.p          ; 0x55a93cbe57f4 ; "%p \n"
           0x55a93cbe5708      b800000000     mov eax, 0
           0x55a93cbe570d      e87efeffff     call sym.imp.printf     ; int printf(const char *format)

A continuación se imprime el contenido de rax. Como rax contiene el puntero devuelto por malloc, lo que se imprimirá es un puntero:

ENSAMBLADOR / REFERENCIA
           0x55a93cbe5712      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe5716      8b00           mov eax, dword [rax]
           0x55a93cbe5718      89c6           mov esi, eax
           0x55a93cbe571a      488d3dd80000.  lea rdi, [0x55a93cbe57f9] ; "%i\n"
           0x55a93cbe5721      b800000000     mov eax, 0
           0x55a93cbe5726      e865feffff     call sym.imp.printf     ; int printf(const char *format)

Después se llama de nuevo a printf, pero esta vez, fíjate en la segunda línea: se pasa el CONTENIDO en lugar de una dirección. Veamos qué ocurre ahora:

ENSAMBLADOR / REFERENCIA
           0x55a93cbe572b      488345f804     add qword [var_8h], 4
           0x55a93cbe5730      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe5734      4889c6         mov rsi, rax
           0x55a93cbe5737      488d3dbf0000.  lea rdi, [0x55a93cbe57fd] ; "%p\n"
           0x55a93cbe573e      b800000000     mov eax, 0
           0x55a93cbe5743      e848feffff     call sym.imp.printf     ; int printf(const char *format)

Se suma 4 al contenido de var_8h. Como el contenido de var_8h es un puntero, el 3 no se convierte en 4 ni en 7 ni en ningún otro valor. Ten en cuenta que al hacer pointer++ con un puntero a int, ++ equivale a +4, ya que el tamaño de un int es 4 bytes en este caso. Tras eso, se imprime el puntero. El último bloque de código es el siguiente:

ENSAMBLADOR / REFERENCIA
           0x55a93cbe5748      488b45f8       mov rax, qword [var_8h]
           0x55a93cbe574c      8b00           mov eax, dword [rax]
           0x55a93cbe574e      89c6           mov esi, eax
           0x55a93cbe5750      488d3da20000.  lea rdi, [0x55a93cbe57f9] ; "%i\n"
           0x55a93cbe5757      b800000000     mov eax, 0
           0x55a93cbe575c      e82ffeffff     call sym.imp.printf     ; int printf(const char *format)

Por último, el programa imprime el contenido de lo apuntado por var_8h, pero como var_8h fue actualizado, ahora apunta a una dirección de memoria diferente, 4 bytes más adelante respecto a la original, por lo que se imprimirá cualquier valor aleatorio en lugar de nuestro 3. Depura el programa por tu cuenta y compruébalo como ejercicio.

Modificar el valor apuntado

Ese ejemplo era muy básico. Pasemos a otro igualmente sencillo para asegurarnos de que no se nos escapa nada:

C / EJEMPLO
#include <stdio.h>
#include <stdlib.h>

main() {

int * spoint;


  spoint  = (int *) malloc (sizeof(int));
  *spoint = 3;


  printf ("%p \n",spoint);

  printf ("%d\n",*spoint);

  (*spoint) ++;

  printf ("%d\n",*spoint);


  getchar();
}

La diferencia está aquí: (*spoint)++. Acostúmbrate a esta notación, ya que la verás con frecuencia al revisar código C/C++ o al trabajar con código de bajo nivel en general. (*pointer) se usa para referenciar el contenido real de un puntero, por lo que hacerlo aquí actualizará efectivamente spoint y convertirá ese 3 en un 4.

Es hora de depurarlo, pero espera: este programa es casi idéntico al anterior; parece una versión «parcheada». El parcheo es muy habitual en el software moderno: los desarrolladores parchean y redistribuyen su software, y en ocasiones esos parches corrigen vulnerabilidades de seguridad.

Podemos usar binary diffing para comparar los dos ejecutables así:

CÓDIGO / SALIDA
red@blue:~/c/chapter10$ radiff2 -A -a x86  -C ipoint secondpointer
[x] Analyze all flags starting with sym. and entry0 (aa)
[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] Analyze all flags starting with sym. and entry0 (aa)
[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.
                          sym._init  23 0x560 |   MATCH  (1.000000) | 0x560   23 sym._init
                     sym.imp.printf   6 0x590 |   MATCH  (1.000000) | 0x590    6 sym.imp.printf
                    sym.imp.getchar   6 0x5a0 |   MATCH  (1.000000) | 0x5a0    6 sym.imp.getchar
                     sym.imp.malloc   6 0x5b0 |   MATCH  (1.000000) | 0x5b0    6 sym.imp.malloc
                             entry0  42 0x5d0 |   MATCH  (0.952381) | 0x5d0   42 entry0
           sym.deregister_tm_clones  50 0x600 |   MATCH  (1.000000) | 0x600   50 sym.deregister_tm_clones
             sym.register_tm_clones  66 0x640 |   MATCH  (1.000000) | 0x640   66 sym.register_tm_clones
          sym.__do_global_dtors_aux  58 0x690 |   MATCH  (1.000000) | 0x690   58 sym.__do_global_dtors_aux
                        entry.init0  10 0x6d0 |   MATCH  (1.000000) | 0x6d0   10 entry.init0
                               main 147 0x6da |   MATCH  (0.802721) | 0x6da  133 main
                sym.__libc_csu_init 101 0x770 | UNMATCH  (0.970297) | 0x760  101 sym.__libc_csu_init
                sym.__libc_csu_fini   2 0x7e0 |   MATCH  (1.000000) | 0x7d0    2 sym.__libc_csu_fini
                          sym._fini   9 0x7e4 |   MATCH  (1.000000) | 0x7d4    9 sym._fini
loc.imp._ITM_deregisterTMCloneTable 292 0x0 | UNMATCH  (0.993711) | 0x0  292 loc.imp._ITM_deregisterTMCloneTable
red@blue:~/c/chapter10$ 

Hay muchas formas de usar radiff. Sin argumentos simplemente vuelca las diferencias, pero estas pueden ser enormes. La salida que acabamos de generar con -a nos muestra que las principales diferencias se encuentran en la función main. Esto es muy útil, especialmente en programas grandes con muchas funciones, donde el parche puede haberse aplicado a una única función; de este modo se ahorra mucho tiempo.

En este caso, la diferencia se encuentra aquí:

ENSAMBLADOR / REFERENCIA
           0x0000072f      8b00           mov eax, dword [rax]
           0x00000731      8d5001         lea edx, [rax + 1]
           0x00000734      488b45f8       mov rax, qword [var_8h]
           0x00000738      8910           mov dword [rax], edx
           0x0000073a      488b45f8       mov rax, qword [var_8h]
           0x0000073e      8b00           mov eax, dword [rax]
           0x00000740      89c6           mov esi, eax
           0x00000742      488d3da00000.  lea rdi, [0x000007e9]       ; "%d\n" ; const char *format
           0x00000749      b800000000     mov eax, 0

Aquí se mueve a eax el contenido de lo apuntado por RAX —un puntero en este caso—, y luego se carga en edx el contenido de lo apuntado por ese puntero (3) más 1. Como puedes ver, se usa un «juego de punteros» para evitar usar add directamente. El resultado final es el mismo.

Un puntero como argumento

Pasemos a un ejemplo más complejo. Como puedes deducir, los punteros pueden usarse junto con funciones. Cuando se pasa un array como argumento a una función, se pasa una referencia (dirección base) al array en lugar del array completo, por lo que si el array se modifica dentro de la función, esos cambios serán permanentes y efectivos tras el ret. Lo mismo puede hacerse con ints, chars o cualquier tipo de variable: basta con declarar esos parámetros como punteros con * (o declararlos como direcciones con & y operar dentro de la función).

C / EJEMPLO
#include <stdio.h>

void x2(int *x) {
   *x = *x * 2;
}

main() {
   int n = 5;   
   printf("value= %d\n", n);
   x2(&n);
   printf("updated_value= %d\n", n);



  getchar();
}

Como esto es algo más complejo, podemos usar agc para generar el grafo de llamadas en main:

CÓDIGO / SALIDA
[0x562a3106b714]> agc
                                        ┌────────────────────┐
                                        │  main              │
                                        └────────────────────┘
                                              v
                                              │
      ┌───────────────────────────────────────│
      │                         ┌─────────────│
      │                         │             │───────────┐
      │                         │             └─────────────────────────────────────┐
      │                         │                         │                         │
┌────────────────────┐    ┌────────────────────┐    ┌────────────────────┐    ┌────────────────────────────┐
│  sym.imp.printf    │    │  sym.x2            │    │  sym.imp.getchar   │    │  sym.imp.__stack_chk_fail  │
└────────────────────┘    └────────────────────┘    └────────────────────┘    └────────────────────────────┘
[0x562a3106b714]> 

Aquí vemos que el programa llama a sym.x2.

C / EJEMPLO
[0x562a3106b714]> pdf
            ; DATA XREF from entry0 @ 0x562a3106b60d
 118: int main (int argc, char **argv, char **envp);
           ; var int64_t var_ch @ rbp-0xc
           ; var int64_t var_8h @ rbp-0x8
           0x562a3106b714      55             push rbp
           0x562a3106b715      4889e5         mov rbp, rsp
           0x562a3106b718      4883ec10       sub rsp, 0x10
           0x562a3106b71c      64488b042528.  mov rax, qword fs:[0x28]
           0x562a3106b725      488945f8       mov qword [var_8h], rax
           0x562a3106b729      31c0           xor eax, eax
           0x562a3106b72b      c745f4050000.  mov dword [var_ch], 5
           0x562a3106b732      8b45f4         mov eax, dword [var_ch]
           0x562a3106b735      89c6           mov esi, eax
           0x562a3106b737      488d3dd60000.  lea rdi, str.value___d  ; 0x562a3106b814 ; "value= %d\n"
           0x562a3106b73e      b800000000     mov eax, 0
           0x562a3106b743      e878feffff     call sym.imp.printf     ; int printf(const char *format)
           0x562a3106b748      488d45f4       lea rax, [var_ch]
           0x562a3106b74c      4889c7         mov rdi, rax
           0x562a3106b74f      e8a6ffffff     call sym.x2
           0x562a3106b754      8b45f4         mov eax, dword [var_ch]
           0x562a3106b757      89c6           mov esi, eax
           0x562a3106b759      488d3dbf0000.  lea rdi, str.updated_value___d ; 0x562a3106b81f ; "updated_value= %d\n"
           0x562a3106b760      b800000000     mov eax, 0
           0x562a3106b765      e856feffff     call sym.imp.printf     ; int printf(const char *format)
           0x562a3106b76a      e861feffff     call sym.imp.getchar    ; int getchar(void)
           0x562a3106b76f      b800000000     mov eax, 0
           0x562a3106b774      488b55f8       mov rdx, qword [var_8h]
           0x562a3106b778      644833142528.  xor rdx, qword fs:[0x28]
       ┌─< 0x562a3106b781      7405           je 0x562a3106b788
          0x562a3106b783      e828feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
       └─> 0x562a3106b788      c9             leave
           0x562a3106b789      c3             ret
[0x562a3106b714]> 

Primero, var_ch se inicializa con 0x5:

ENSAMBLADOR / REFERENCIA
           0x562a3106b72b      c745f4050000.  mov dword [var_ch], 5
           0x562a3106b732      8b45f4         mov eax, dword [var_ch]

Luego se llama a x2 así:

ENSAMBLADOR / REFERENCIA
           0x562a3106b748      488d45f4       lea rax, [var_ch]
           0x562a3106b74c      4889c7         mov rdi, rax
           0x562a3106b74f      e8a6ffffff     call sym.x2

En lugar del valor (mov), se usa LEA (load effective address) para cargar var_ch en rdi, de modo que la dirección (un puntero) del valor almacenado en var_ch se pasa a x2.

Veamos ahora el interior de x2:

ENSAMBLADOR / REFERENCIA
[0x562a3106b714]> s sym.x2
[0x562a3106b6fa]> pdf
            ; CALL XREF from main @ 0x562a3106b74f
 26: sym.x2 (int64_t arg1);
           ; var int64_t var_8h @ rbp-0x8
           ; arg int64_t arg1 @ rdi
           0x562a3106b6fa      55             push rbp
           0x562a3106b6fb      4889e5         mov rbp, rsp
           0x562a3106b6fe      48897df8       mov qword [var_8h], rdi ; arg1
           0x562a3106b702      488b45f8       mov rax, qword [var_8h]
           0x562a3106b706      8b00           mov eax, dword [rax]
           0x562a3106b708      8d1400         lea edx, [rax + rax]
           0x562a3106b70b      488b45f8       mov rax, qword [var_8h]
           0x562a3106b70f      8910           mov dword [rax], edx
           0x562a3106b711      90             nop
           0x562a3106b712      5d             pop rbp
           0x562a3106b713      c3             ret
[0x562a3106b6fa]> 

Primero se carga desde rdi el valor, que queda almacenado en var_8h, un espacio local reservado dentro de la función.

ENSAMBLADOR / REFERENCIA
           0x562a3106b6fe      48897df8       mov qword [var_8h], rdi ; arg1
           0x562a3106b702      488b45f8       mov rax, qword [var_8h]

Luego el programa multiplica por dos, del mismo modo que en el ejemplo anterior.

ENSAMBLADOR / REFERENCIA
           0x562a3106b706      8b00           mov eax, dword [rax]
           0x562a3106b708      8d1400         lea edx, [rax + rax]

El resultado entero calculado en edx se escribe en la dirección recibida por la función. El puntero permite modificar el objeto del llamador; el valor almacenado es el resultado de la operación. Comprueba ambos accesos en pointer-argument.

Dirección base e índice

Con arrays se puede operar de la misma manera. Cuando declaramos un array estático, estamos declarando una dirección base y un espacio de tipo t (int, char, float...) en memoria.

Eso puede interpretarse usando punteros, como en este programa:

C / EJEMPLO
#include <stdio.h>

main() {
   int data[10];
   int i;

     printf ("%p\n", data);

    *(data)= 20;

     printf ("%d ", *(data));

     *(data+1)= 40;

     printf ("%d ", data[1]);
     getchar();
}

Aquí está:

C / EJEMPLO
[0x560d6a0d06fa]> pdf
            ; DATA XREF from entry0 @ 0x560d6a0d060d
 137: int main (int argc, char **argv, char **envp);
           ; var int64_t var_30h @ rbp-0x30
           ; var int64_t var_2ch @ rbp-0x2c
           ; var int64_t var_8h @ rbp-0x8
           0x560d6a0d06fa      55             push rbp
           0x560d6a0d06fb      4889e5         mov rbp, rsp
           0x560d6a0d06fe      4883ec30       sub rsp, 0x30
           0x560d6a0d0702      64488b042528.  mov rax, qword fs:[0x28]
           0x560d6a0d070b      488945f8       mov qword [var_8h], rax
           0x560d6a0d070f      31c0           xor eax, eax
           0x560d6a0d0711      488d45d0       lea rax, [var_30h]
           0x560d6a0d0715      4889c6         mov rsi, rax
           0x560d6a0d0718      488d3df50000.  lea rdi, [0x560d6a0d0814] ; "%p\n"
           0x560d6a0d071f      b800000000     mov eax, 0
           0x560d6a0d0724      e897feffff     call sym.imp.printf     ; int printf(const char *format)
           0x560d6a0d0729      c745d0140000.  mov dword [var_30h], 0x14 ; 20
           0x560d6a0d0730      8b45d0         mov eax, dword [var_30h]
           0x560d6a0d0733      89c6           mov esi, eax
           0x560d6a0d0735      488d3ddc0000.  lea rdi, [0x560d6a0d0818] ; "%d "
           0x560d6a0d073c      b800000000     mov eax, 0
           0x560d6a0d0741      e87afeffff     call sym.imp.printf     ; int printf(const char *format)
           0x560d6a0d0746      c745d4280000.  mov dword [var_2ch], 0x28 ; '(' ; 40
           0x560d6a0d074d      8b45d4         mov eax, dword [var_2ch]
           0x560d6a0d0750      89c6           mov esi, eax
           0x560d6a0d0752      488d3dbf0000.  lea rdi, [0x560d6a0d0818] ; "%d "
           0x560d6a0d0759      b800000000     mov eax, 0
           0x560d6a0d075e      e85dfeffff     call sym.imp.printf     ; int printf(const char *format)
           0x560d6a0d0763      e868feffff     call sym.imp.getchar    ; int getchar(void)
           0x560d6a0d0768      b800000000     mov eax, 0
           0x560d6a0d076d      488b55f8       mov rdx, qword [var_8h]
           0x560d6a0d0771      644833142528.  xor rdx, qword fs:[0x28]
       ┌─< 0x560d6a0d077a      7405           je 0x560d6a0d0781
          0x560d6a0d077c      e82ffeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
       └─> 0x560d6a0d0781      c9             leave
           0x560d6a0d0782      c3             ret
[0x560d6a0d06fa]> 

En este programa, en lugar de una dirección base y demás, el compilador trabaja con dos variables, ya que solo se inicializan dos posiciones. La primera posición se inicializa con 20:

ENSAMBLADOR / REFERENCIA
           0x560d6a0d0729 b    c745d0140000.  mov dword [var_30h], 0x14 ; 20
           0x560d6a0d0730 b    8b45d0         mov eax, dword [var_30h]

Podemos comprobarlo depurando el programa así:

CÓDIGO / SALIDA
[0x560d6a0d0729]> afvd
var var_8h = 0x7ffef51d96a8 = (qword)0x073629ee5f9d1100
var var_30h = 0x7ffef51d9680 = (qword)0x00007fed746809a0
var var_2ch = 0x7ffef51d9684 = (qword)0x0000000000007fed
[0x560d6a0d0729]> 

Esas son las «variables»; tras avanzar más allá de la inicialización vemos:

CÓDIGO / SALIDA
[0x560d6a0d0729]> ds
[0x560d6a0d0730]> afvd
var var_8h = 0x7ffef51d96a8 = (qword)0x073629ee5f9d1100
var var_30h = 0x7ffef51d9680 = (qword)0x00007fed00000014
var var_2ch = 0x7ffef51d9684 = (qword)0x0000000000007fed
[0x560d6a0d0730]> 

Debido a la lógica de los arrays: 0x7ffef51d9680 será la dirección base del array; var_2ch irá justo a continuación, como puedes observar:

ENSAMBLADOR / REFERENCIA
           0x560d6a0d0746      c745d4280000.  mov dword [var_2ch], 0x28 ; '(' ; 40
           0x560d6a0d074d      8b45d4         mov eax, dword [var_2ch]

Pasamos a la siguiente posición, donde se inicializa var_2ch. Como veremos, esas variables se suceden una tras otra así:

CÓDIGO / SALIDA
[0x560d6a0d0750]> pf 2i @ 0x7ffef51d9680
0x7ffef51d9680 [0] {
  0x7ffef51d9680 = 20
}
0x7ffef51d9684 [1] {
  0x7ffef51d9684 = 40
}

Una forma habitual de identificar arrays en memoria es observar muchas variables del mismo tipo que se suceden consecutivamente, como acabamos de ver.

Estructuras reservadas en el heap

Por último, trabajemos con este ejemplo final:

C / EJEMPLO
#include <stdio.h>

main() {    
   struct person {
     char name[30];
     char email[25];
     int age;
   };

   struct person *person1;


   person1 = (struct person*)
     malloc (sizeof(struct person));
   strcpy(person1->name, "Peter");
   strcpy(person1->email, "p@p.p");
   person1->age = 21;

   printf("Person data= %s, %s, and the age is: %d\n",
     person1->name, person1->email, person1->age);
   free(person1);


     getchar();
}

El último ejemplo reserva una estructura mediante malloc. sizeof incluye el espacio de sus miembros y el padding necesario para respetar su alineación. Para reconstruirla, compara el tamaño solicitado con los offsets de cada acceso.

p->campo accede a un miembro a través de un puntero y equivale a (*p).campo. Esta sintaxis también se utiliza con punteros a estructuras alojadas en la pila; no depende de que la reserva sea dinámica.

El desensamblado muestra la reserva y la inicialización de sus miembros:

C / EJEMPLO
[0x7fead046c090]> s main
[0x559db307071a]> pdf
            ; DATA XREF from entry0 @ 0x559db307062d
 137: int main (int argc, char **argv, char **envp);
           ; var int64_t var_8h @ rbp-0x8
           0x559db307071a      55             push rbp
           0x559db307071b      4889e5         mov rbp, rsp
           0x559db307071e      4883ec10       sub rsp, 0x10
           0x559db3070722      bf3c000000     mov edi, 0x3c           ; '<' ; 60
           0x559db3070727      e8c4feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
           0x559db307072c      488945f8       mov qword [var_8h], rax
           0x559db3070730      488b45f8       mov rax, qword [var_8h]
           0x559db3070734      c70050657465   mov dword [rax], 0x65746550 ; 'Pete'
                                                                      ; [0x65746550:4]=-1
           0x559db307073a      66c740047200   mov word [rax + 4], 0x72 ; 'r'
                                                                      ; [0x72:2]=0xffff ; 114
           0x559db3070740      488b45f8       mov rax, qword [var_8h]
           0x559db3070744      4883c01e       add rax, 0x1e           ; 30
           0x559db3070748      c7007040702e   mov dword [rax], 0x2e704070 ; 'p@p.'
                                                                      ; [0x2e704070:4]=-1
           0x559db307074e      66c740047000   mov word [rax + 4], 0x70 ; 'p'
                                                                      ; [0x70:2]=0xffff ; 112
           0x559db3070754      488b45f8       mov rax, qword [var_8h]
           0x559db3070758      c74038150000.  mov dword [rax + 0x38], 0x15 ; [0x15:4]=-1 ; 21
           0x559db307075f      488b45f8       mov rax, qword [var_8h]
           0x559db3070763      8b5038         mov edx, dword [rax + 0x38]
           0x559db3070766      488b45f8       mov rax, qword [var_8h]
           0x559db307076a      488d701e       lea rsi, [rax + 0x1e]
           0x559db307076e      488b45f8       mov rax, qword [var_8h]
           0x559db3070772      89d1           mov ecx, edx
           0x559db3070774      4889f2         mov rdx, rsi
           0x559db3070777      4889c6         mov rsi, rax
           0x559db307077a      488d3db70000.  lea rdi, str.Person_data___s___s__and_the_age_is:__d ; 0x559db3070838 ; "Person data= %s, %s, and the age is: %d\n"
           0x559db3070781      b800000000     mov eax, 0
           0x559db3070786      e845feffff     call sym.imp.printf     ; int printf(const char *format)
           0x559db307078b      488b45f8       mov rax, qword [var_8h]
           0x559db307078f      4889c7         mov rdi, rax
           0x559db3070792      e829feffff     call sym.imp.free       ; void free(void *ptr)
           0x559db3070797      e844feffff     call sym.imp.getchar    ; int getchar(void)
           0x559db307079c      b800000000     mov eax, 0
           0x559db30707a1      c9             leave
           0x559db30707a2      c3             ret
[0x559db307071a]> 

Como podemos ver en primer lugar, el programa declara var_8h como puntero a la dirección base de la estructura y reserva espacio en la pila (0x10):

ENSAMBLADOR / REFERENCIA
           ; var int64_t var_8h @ rbp-0x8
           0x559db307071a      55             push rbp
           0x559db307071b      4889e5         mov rbp, rsp
           0x559db307071e      4883ec10       sub rsp, 0x10

Luego reserva 60 bytes en el heap para la estructura con malloc:

ENSAMBLADOR / REFERENCIA
           0x559db3070722      bf3c000000     mov edi, 0x3c           ; '<' ; 60
           0x559db3070727      e8c4feffff     call sym.imp.malloc     ;  void *malloc(size_t size)

A continuación carga la dirección base en rax y copia «Peter» dentro.

ENSAMBLADOR / REFERENCIA
           0x559db3070730      488b45f8       mov rax, qword [var_8h]
           0x559db3070734      c70050657465   mov dword [rax], 0x65746550 ; 'Pete'
                                                                      ; [0x65746550:4]=-1
           0x559db307073a      66c740047200   mov word [rax + 4], 0x72 ; 'r'
                                                                      ; [0x72:2]=0xffff ; 114

Observa cómo la «r» final se carga después de «Pete» (rax+4), ya que «Pete» = 4 caracteres = 4 bytes.

ENSAMBLADOR / REFERENCIA
           0x559db3070740      488b45f8       mov rax, qword [var_8h]
           0x559db3070744      4883c01e       add rax, 0x1e           ; 30
           0x559db3070748      c7007040702e   mov dword [rax], 0x2e704070 ; 'p@p.'
                                                                      ; [0x2e704070:4]=-1
           0x559db307074e      66c740047000   mov word [rax + 4], 0x70 ; 'p'

Observa que add se suma a 0x1e, lo cual es importante: como rax contiene la dirección base de esa estructura de datos y el primer campo de la estructura es un array de 30 posiciones de tipo byte, hay que avanzar sobre él, ya que el siguiente array comenzará justo después.

ENSAMBLADOR / REFERENCIA
           0x559db3070754      488b45f8       mov rax, qword [var_8h]
           0x559db3070758      c74038150000.  mov dword [rax + 0x38], 0x15 ; [0x15:4]=-1 ; 21
           0x559db307075f      488b45f8       mov rax, qword [var_8h]

A continuación tenemos el int de la edad, que sigue la misma estrategia de inicialización.

Como práctica, compila estos programas y reviértelos por tu cuenta. La siguiente lección tratará sobre listas enlazadas y será la última sobre memoria dinámica; después abordaremos aspectos básicos como defines, uniones y operaciones a nivel de bit.

Práctica manual

Vuelve al binario

Seguir el paso de direcciones a funciones y la construcción de estructuras en el heap. 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: pointer-argument.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 10 --example pointer-argument
./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.