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

Punteros y memoria dinámica

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

Distinguir una dirección de su contenido y reconocer las reservas de pila y heap.

Consultar conceptos ↗

Un puntero almacena una dirección. Para seguirlo en el desensamblado hay que distinguir la lectura de esa dirección, su cálculo y el acceso al objeto apuntado. El primer ejemplo permite observar las tres operaciones; después las aplicaremos a un array reservado en el heap.

Dirección, puntero y contenido

Empecemos con este código

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

int main(){

    int i = 2;

    char c = 'c';

    char* pc = &c;

    printf("Value of i: %d \n",i);
    printf("Address of i: %p \n",&i);
    printf("Value of c: %c \n", c);
    printf("Address of c: %p \n",&c);

    printf("Updating the content of the mem address pointed by pc \n");

    *pc = 'b';

    printf("Value of c: %c \n", c);

return 0;
}

En C, &c indica la dirección de la variable c. *p declara un puntero. Pueden existir múltiples punteros apuntando a la misma dirección de memoria, de modo que si esa dirección se actualiza, el cambio afecta a todos los punteros. Si no tienes ningún conocimiento previo sobre punteros, lee primero esto: https://www.programiz.com/c-programming/c-pointers y luego continúa con este artículo.

Al compilar, el programa produce la siguiente salida:

CÓDIGO / SALIDA
Value of i: 2 
Address of i: 0x7ffe948d662c 
Value of c: c 
Address of c: 0x7ffe948d662b 
Updating the content of the mem address pointed by pc 
Value of c: b 

Y ahora el desensamblado:

C / EJEMPLO
[0x556609210155]> pdf
            ; DATA XREF from entry0 @ 0x55660921008d
 210: int main (int argc, char **argv, char **envp);
           ; var int64_t var_15h @ rbp-0x15
           ; var int64_t var_14h @ rbp-0x14
           ; var int64_t var_10h @ rbp-0x10
           ; var int64_t var_8h @ rbp-0x8
           0x556609210155      55             push rbp
           0x556609210156      4889e5         mov rbp, rsp
           0x556609210159      4883ec20       sub rsp, 0x20
           0x55660921015d      64488b042528.  mov rax, qword fs:[0x28]
           0x556609210166      488945f8       mov qword [var_8h], rax
           0x55660921016a      31c0           xor eax, eax
           0x55660921016c      c745ec020000.  mov dword [var_14h], 2
           0x556609210173      c645eb63       mov byte [var_15h], 0x63 ; 'c' ; 99
           0x556609210177      488d45eb       lea rax, [var_15h]
           0x55660921017b      488945f0       mov qword [var_10h], rax
           0x55660921017f      8b45ec         mov eax, dword [var_14h]
           0x556609210182      89c6           mov esi, eax
           0x556609210184      488d3d7d0e00.  lea rdi, str.Value_of_i:__d ; 0x556609211008 ; "Value of i: %d \n"
           0x55660921018b      b800000000     mov eax, 0
           0x556609210190      e8bbfeffff     call sym.imp.printf     ; int printf(const char *format)
           0x556609210195      488d45ec       lea rax, [var_14h]
           0x556609210199      4889c6         mov rsi, rax
           0x55660921019c      488d3d760e00.  lea rdi, str.Address_of_i:__p ; 0x556609211019 ; "Address of i: %p \n"
           0x5566092101a3      b800000000     mov eax, 0
           0x5566092101a8      e8a3feffff     call sym.imp.printf     ; int printf(const char *format)
           0x5566092101ad      0fb645eb       movzx eax, byte [var_15h]
           0x5566092101b1      0fbec0         movsx eax, al
           0x5566092101b4      89c6           mov esi, eax
           0x5566092101b6      488d3d6f0e00.  lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
           0x5566092101bd      b800000000     mov eax, 0
           0x5566092101c2      e889feffff     call sym.imp.printf     ; int printf(const char *format)
           0x5566092101c7      488d45eb       lea rax, [var_15h]
           0x5566092101cb      4889c6         mov rsi, rax
           0x5566092101ce      488d3d680e00.  lea rdi, str.Address_of_c:__p ; 0x55660921103d ; "Address of c: %p \n"
           0x5566092101d5      b800000000     mov eax, 0
           0x5566092101da      e871feffff     call sym.imp.printf     ; int printf(const char *format)
           0x5566092101df      488d3d6a0e00.  lea rdi, str.Updating_the_content_of_the_mem_address_pointed_by_pc ; 0x556609211050 ; "Updating the content of the mem address pointed by pc "
           0x5566092101e6      e845feffff     call sym.imp.puts       ; int puts(const char *s)
           0x5566092101eb      488b45f0       mov rax, qword [var_10h]
           0x5566092101ef      c60062         mov byte [rax], 0x62    ; 'b'
                                                                      ; [0x62:1]=255 ; 98
           0x5566092101f2      0fb645eb       movzx eax, byte [var_15h]
           0x5566092101f6      0fbec0         movsx eax, al
           0x5566092101f9      89c6           mov esi, eax
           0x5566092101fb      488d3d2a0e00.  lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
           0x556609210202      b800000000     mov eax, 0
           0x556609210207      e844feffff     call sym.imp.printf     ; int printf(const char *format)
           0x55660921020c      b800000000     mov eax, 0
           0x556609210211      488b55f8       mov rdx, qword [var_8h]
           0x556609210215      644833142528.  xor rdx, qword fs:[0x28]
       ┌─< 0x55660921021e      7405           je 0x556609210225
          0x556609210220      e81bfeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
       └─> 0x556609210225      c9             leave
           0x556609210226      c3             ret
[0x556609210155]> 

Vamos a diseccionarlo. Al principio el programa inicializa estas variables:

ENSAMBLADOR / REFERENCIA
           0x55660921016c      c745ec020000.  mov dword [var_14h], 2
           0x556609210173      c645eb63       mov byte [var_15h], 0x63 ; 'c' ; 99
           0x556609210177      488d45eb       lea rax, [var_15h]
           0x55660921017b      488945f0       mov qword [var_10h], rax

La primera variable se inicializa con 2, la segunda con 0x63, es decir, el carácter C, y a continuación la dirección de memoria de esa variable (lea = load effective address) —no su contenido— se almacena en var_10h. Como podemos deducir con bastante seguridad que esas primeras variables corresponden al entero i y al carácter c, las renombramos con afvn, con lo que el resultado queda así:

CÓDIGO / SALIDA
[0x556609210155]> afv
var int64_t char_c @ rbp-0x15
var int64_t int_i @ rbp-0x14
var int64_t var_10h @ rbp-0x10
var int64_t var_8h @ rbp-0x8 = stack cookie

Y como var_10h almacenará la DIRECCIÓN de c, la renombramos como pointer_c

CÓDIGO / SALIDA
[0x556609210155]> afvn pointer_c var_10h
[0x556609210155]> afv
var int64_t char_c @ rbp-0x15
var int64_t int_i @ rbp-0x14
var int64_t pointer_c @ rbp-0x10
var int64_t var_8h @ rbp-0x8

Inspeccionemos el primer printf

ENSAMBLADOR / REFERENCIA
           0x55660921017f      8b45ec         mov eax, dword [int_i]
           0x556609210182      89c6           mov esi, eax
           0x556609210184      488d3d7d0e00.  lea rdi, str.Value_of_i:__d ; 0x556609211008 ; "Value of i: %d \n"
           0x55660921018b      b800000000     mov eax, 0
           0x556609210190      e8bbfeffff     call sym.imp.printf     ; int printf(const char *format)

Nada nuevo aquí. Con [ref] indicamos que queremos el contenido de lo apuntado por int_i, es decir, el valor de la variable i; lo cargamos en eax (usamos eax en lugar de rax porque el espacio disponible es suficiente) y después cargamos la dirección base de la cadena "Value of..." como parámetro. La función printf toma esos dos parámetros y hace su trabajo, como ya deberíamos saber. Nótese que en este primer caso pasamos el valor de la variable, no su dirección. Continuemos:

ENSAMBLADOR / REFERENCIA
           0x556609210195      488d45ec       lea rax, [int_i]
           0x556609210199      4889c6         mov rsi, rax
           0x55660921019c      488d3d760e00.  lea rdi, str.Address_of_i:__p ; 0x556609211019 ; "Address of i: %p \n"
           0x5566092101a3      b800000000     mov eax, 0
           0x5566092101a8      e8a3feffff     call sym.imp.printf     ; int printf(const char *format)

Observa cómo en el siguiente bloque el programa usa lea en lugar de mov. Esta vez carga la DIRECCIÓN de int_i en lugar del valor almacenado en lo que apunta int_i. En términos generales, usar &var se traduce internamente en lea reg, [var]. Este es un concepto clave en reversing, desarrollo de exploits y, en general, en cualquier análisis de programas a bajo nivel; conviene tenerlo muy claro. Sigamos

ENSAMBLADOR / REFERENCIA
           0x5566092101ad      0fb645eb       movzx eax, byte [char_c]
           0x5566092101b1      0fbec0         movsx eax, al
           0x5566092101b4      89c6           mov esi, eax
           0x5566092101b6      488d3d6f0e00.  lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
           0x5566092101bd      b800000000     mov eax, 0
           0x5566092101c2      e889feffff     call sym.imp.printf     ; int printf(const char *format)

La instrucción carga un byte y lo extiende con ceros al ancho del registro de destino. movzx expresa esa extensión; su presencia no demuestra una optimización del consumo de memoria.

ENSAMBLADOR / REFERENCIA
           0x5566092101c7      488d45eb       lea rax, [char_c]
           0x5566092101cb      4889c6         mov rsi, rax
           0x5566092101ce      488d3d680e00.  lea rdi, str.Address_of_c:__p ; 0x55660921103d ; "Address of c: %p \n"
           0x5566092101d5      b800000000     mov eax, 0
           0x5566092101da      e871feffff     call sym.imp.printf     ; int printf(const char *format)

De nuevo, esta vez se carga la DIRECCIÓN.

Al final del programa tenemos esto:

ENSAMBLADOR / REFERENCIA
           0x5566092101eb      488b45f0       mov rax, qword [pointer_c]
           0x5566092101ef      c60062         mov byte [rax], 0x62    ; 'b'
                                                                      ; [0x62:1]=255 ; 98
           0x5566092101f2      0fb645eb       movzx eax, byte [char_c]
           0x5566092101f6      0fbec0         movsx eax, al
           0x5566092101f9      89c6           mov esi, eax
           0x5566092101fb      488d3d2a0e00.  lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
           0x556609210202      b800000000     mov eax, 0
           0x556609210207      e844feffff     call sym.imp.printf     ; int printf(const char *format)

La primera instrucción carga en rax el puntero guardado en pointer_c. La segunda escribe el byte 0x62, el carácter b, en la dirección que contiene rax. Así se modifica el objeto char_c.

Empezamos así:

ENSAMBLADOR / REFERENCIA
           0x5566092101eb b    488b45f0       mov rax, qword [pointer_c]
           0x5566092101ef      c60062         mov byte [rax], 0x62    ; 'b'
                                                                      ; [0x62:1]=255 ; 98
           0x5566092101f2      0fb645eb       movzx eax, byte [char_c]
           0x5566092101f6      0fbec0         movsx eax, al
           0x5566092101f9      89c6           mov esi, eax
           0x5566092101fb      488d3d2a0e00.  lea rdi, str.Value_of_c:__c ; 0x55660921102c ; "Value of c: %c \n"
           0x556609210202      b800000000     mov eax, 0
           0x556609210207      e844feffff     call sym.imp.printf     ; int printf(const char *format)
[...]
[0x5566092101eb]> afvd
var var_8h = 0x7ffd71635228 = (qword)0xa0af6487b4e4ef00
var int_i = 0x7ffd7163521c = (qword)0x7163521b00000002
var char_c = 0x7ffd7163521b = (qword)0x63521b0000000263
var pointer_c = 0x7ffd71635220 = (qword)0x00007ffd7163521b

Como se puede ver, el contenido de pointer_c (0x00007ffd7163521b) es la dirección de memoria de char_c (0x7ffd7163521b). Así funcionan los punteros: almacenan direcciones de memoria.

Tras ejecutar el primer mov, rax tendrá este aspecto:

CÓDIGO / SALIDA
[0x5566092101eb]> ds
[0x5566092101ef]> dr rax
0x7ffd7163521b

Ahora rax contiene el CONTENIDO de pointer_c, y ese contenido es la DIRECCIÓN de char_c, por lo que hacer [rax] equivale a hacer [char_c]. Y char_c contiene el carácter 'c':

CÓDIGO / SALIDA
[0x5566092101ef]> afvd
var var_8h = 0x7ffd71635228 = (qword)0xa0af6487b4e4ef00
var int_i = 0x7ffd7163521c = (qword)0x7163521b00000002
var char_c = 0x7ffd7163521b = (qword)0x63521b0000000263
var pointer_c = 0x7ffd71635220 = (qword)0x00007ffd7163521b

[0x5566092101ef]> pxw @ 0x7ffd7163521b
0x7ffd7163521b  0x00000263 0x63521b00 0x007ffd71 0xe4ef0000  c.....Rcq.......

Y tras ejecutar el segundo mov

CÓDIGO / SALIDA
[0x5566092101ef]> ds
[0x5566092101f2]> pxw @ 0x7ffd7163521b
0x7ffd7163521b  0x00000262 0x63521b00 0x007ffd71 0xe4ef0000  b.....Rcq.......

Ahora contiene el carácter 'b'.

La carga posterior de char_c obtiene el byte modificado. pointer_c conserva su dirección; [rax] designa el contenido situado en ella.

Con esto terminamos el primer ejemplo. Pasemos a algo más complejo, aunque igualmente accesible.

Un array local en la pila

¿Recuerdas el programa que vimos en uno de los tutoriales anteriores? El que usaba un array que el usuario debía rellenar en un bucle while. Inspeccionemos este:

C
#include <stdio.h>

main() {
  int data[100];      
  int entered;    
  int i;           
  long sum=0;     

  do {
    printf("How many numbers? ");
    scanf("%d", &entered);
    if (entered>100)  
      printf("Limit is 100");
  } while (entered>100);  


  for (i=0; i<entered; i++) {
    printf("Enter number %d: ", i+1);
    scanf("%d", &data[i]);
  }


  for (i=0; i<entered; i++) 
    sum += data[i];

  printf("SUM: %ld\n", sum);
}

Este ejemplo reserva el array como variable local y utiliza un índice para recorrerlo:

C / EJEMPLO
[0x55c2a9231155]> pdf
            ; DATA XREF from entry0 @ 0x55c2a923108d
 335: int main (int argc, char **argv, char **envp);
           ; var int64_t var_1b0h @ rbp-0x1b0
           ; var int64_t var_1ach @ rbp-0x1ac
           ; var int64_t var_1a8h @ rbp-0x1a8
           ; var int64_t var_1a0h @ rbp-0x1a0
           ; var int64_t var_8h @ rbp-0x8
           0x55c2a9231155      55             push rbp
           0x55c2a9231156      4889e5         mov rbp, rsp
           0x55c2a9231159      4881ecb00100.  sub rsp, 0x1b0
           0x55c2a9231160      64488b042528.  mov rax, qword fs:[0x28]
           0x55c2a9231169      488945f8       mov qword [var_8h], rax
           0x55c2a923116d      31c0           xor eax, eax
           0x55c2a923116f      48c78558feff.  mov qword [var_1a8h], 0
       ┌─> 0x55c2a923117a      488d3d830e00.  lea rdi, str.How_many_numbers ; 0x55c2a9232004 ; "How many numbers? "
          0x55c2a9231181      b800000000     mov eax, 0
          0x55c2a9231186      e8b5feffff     call sym.imp.printf     ; int printf(const char *format)
          0x55c2a923118b      488d8550feff.  lea rax, [var_1b0h]
          0x55c2a9231192      4889c6         mov rsi, rax
          0x55c2a9231195      488d3d7b0e00.  lea rdi, [0x55c2a9232017] ; "%d"
          0x55c2a923119c      b800000000     mov eax, 0
          0x55c2a92311a1      e8aafeffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
          0x55c2a92311a6      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a92311ac      83f864         cmp eax, 0x64           ; 100
      ┌──< 0x55c2a92311af      7e11           jle 0x55c2a92311c2
      │╎   0x55c2a92311b1      488d3d620e00.  lea rdi, str.Limit_is_100 ; 0x55c2a923201a ; "Limit is 100"
      │╎   0x55c2a92311b8      b800000000     mov eax, 0
      │╎   0x55c2a92311bd      e87efeffff     call sym.imp.printf     ; int printf(const char *format)
      └──> 0x55c2a92311c2      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a92311c8      83f864         cmp eax, 0x64           ; 100
       └─< 0x55c2a92311cb      7fad           jg 0x55c2a923117a
           0x55c2a92311cd      c78554feffff.  mov dword [var_1ach], 0
       ┌─< 0x55c2a92311d7      eb4e           jmp 0x55c2a9231227
      ┌──> 0x55c2a92311d9      8b8554feffff   mov eax, dword [var_1ach]
      ╎│   0x55c2a92311df      83c001         add eax, 1
      ╎│   0x55c2a92311e2      89c6           mov esi, eax
      ╎│   0x55c2a92311e4      488d3d3c0e00.  lea rdi, str.Enter_number__d: ; 0x55c2a9232027 ; "Enter number %d: "
      ╎│   0x55c2a92311eb      b800000000     mov eax, 0
      ╎│   0x55c2a92311f0      e84bfeffff     call sym.imp.printf     ; int printf(const char *format)
      ╎│   0x55c2a92311f5      488d8560feff.  lea rax, [var_1a0h]
      ╎│   0x55c2a92311fc      8b9554feffff   mov edx, dword [var_1ach]
      ╎│   0x55c2a9231202      4863d2         movsxd rdx, edx
      ╎│   0x55c2a9231205      48c1e202       shl rdx, 2
      ╎│   0x55c2a9231209      4801d0         add rax, rdx
      ╎│   0x55c2a923120c      4889c6         mov rsi, rax
      ╎│   0x55c2a923120f      488d3d010e00.  lea rdi, [0x55c2a9232017] ; "%d"
      ╎│   0x55c2a9231216      b800000000     mov eax, 0
      ╎│   0x55c2a923121b      e830feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
      ╎│   0x55c2a9231220      838554feffff.  add dword [var_1ach], 1
      ╎│   ; CODE XREF from main @ 0x55c2a92311d7
      ╎└─> 0x55c2a9231227      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a923122d      398554feffff   cmp dword [var_1ach], eax
      └──< 0x55c2a9231233      7ca4           jl 0x55c2a92311d9
           0x55c2a9231235      c78554feffff.  mov dword [var_1ach], 0
       ┌─< 0x55c2a923123f      eb1f           jmp 0x55c2a9231260
      ┌──> 0x55c2a9231241      8b8554feffff   mov eax, dword [var_1ach]
      ╎│   0x55c2a9231247      4898           cdqe
      ╎│   0x55c2a9231249      8b848560feff.  mov eax, dword [rbp + rax*4 - 0x1a0]
      ╎│   0x55c2a9231250      4898           cdqe
      ╎│   0x55c2a9231252      48018558feff.  add qword [var_1a8h], rax
      ╎│   0x55c2a9231259      838554feffff.  add dword [var_1ach], 1
      ╎│   ; CODE XREF from main @ 0x55c2a923123f
      ╎└─> 0x55c2a9231260      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a9231266      398554feffff   cmp dword [var_1ach], eax
      └──< 0x55c2a923126c      7cd3           jl 0x55c2a9231241
           0x55c2a923126e      488b8558feff.  mov rax, qword [var_1a8h]
           0x55c2a9231275      4889c6         mov rsi, rax
           0x55c2a9231278      488d3dba0d00.  lea rdi, str.SUM:__ld   ; 0x55c2a9232039 ; "SUM: %ld\n"
           0x55c2a923127f      b800000000     mov eax, 0
           0x55c2a9231284      e8b7fdffff     call sym.imp.printf     ; int printf(const char *format)
           0x55c2a9231289      b800000000     mov eax, 0
           0x55c2a923128e      488b4df8       mov rcx, qword [var_8h]
           0x55c2a9231292      6448330c2528.  xor rcx, qword fs:[0x28]
       ┌─< 0x55c2a923129b      7405           je 0x55c2a92312a2
          0x55c2a923129d      e88efdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
       └─> 0x55c2a92312a2      c9             leave
           0x55c2a92312a3      c3             ret
[0x55c2a9231155]> 

Lo que quiero que notes es lo siguiente: al comienzo del código aparece esto = 432:

CÓDIGO / SALIDA
│           0x55c2a9231159      4881ecb00100.  sub rsp, 

Habrás visto instrucciones como esta en muchos de los binarios que hemos analizado. Lo que hace es reservar espacio en la memoria local de la función (usando la pila) para almacenar variables locales. (consulta https://en.wikibooks.org/wiki/X86_Disassembly/Functions_and_Stack_Frames)

En este caso reserva algo más de 400 bytes. Tiene sentido: según el código, declaramos un array de 100 enteros y, asumiendo 4 bytes por entero, esos 400 bytes y algo más son coherentes. Es habitual que el programa reserve un poco más de espacio del estrictamente necesario. Observar este sub rbp al inicio de la función aporta información relevante.

No voy a entrar en muchos detalles sobre el código, ya que no hay prácticamente nada nuevo; deberías saber analizarlo sin problemas.

Lo que quiero destacar esta vez es lo siguiente. En este programa usamos un do-while en lugar de un while, y podemos verlo aquí:

ENSAMBLADOR / REFERENCIA
       ┌─> 0x55c2a923117a      488d3d830e00.  lea rdi, str.How_many_numbers ; 0x55c2a9232004 ; "How many numbers? "
          0x55c2a9231181      b800000000     mov eax, 0
          0x55c2a9231186      e8b5feffff     call sym.imp.printf     ; int printf(const char *format)
          0x55c2a923118b      488d8550feff.  lea rax, [var_1b0h]
          0x55c2a9231192      4889c6         mov rsi, rax
          0x55c2a9231195      488d3d7b0e00.  lea rdi, [0x55c2a9232017] ; "%d"
          0x55c2a923119c      b800000000     mov eax, 0
          0x55c2a92311a1      e8aafeffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
          0x55c2a92311a6      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a92311ac      83f864         cmp eax, 0x64           ; 100
      ┌──< 0x55c2a92311af      7e11           jle 0x55c2a92311c2
      │╎   0x55c2a92311b1      488d3d620e00.  lea rdi, str.Limit_is_100 ; 0x55c2a923201a ; "Limit is 100"
      │╎   0x55c2a92311b8      b800000000     mov eax, 0
      │╎   0x55c2a92311bd      e87efeffff     call sym.imp.printf     ; int printf(const char *format)
      └──> 0x55c2a92311c2      8b8550feffff   mov eax, dword [var_1b0h]
          0x55c2a92311c8      83f864         cmp eax, 0x64           ; 100
       └─< 0x55c2a92311cb      7fad           jg 0x55c2a923117a

El bloque de código se ejecuta primero y, después, se comprueba la condición (cmp 100); el programa salta de vuelta al inicio del bloque o continúa, pero el bloque se ejecuta al menos una vez. Con un while en lugar de un do-while, el programa salta primero a la comprobación y, en función del resultado, entra o no en el bloque.

Reserva dinámica del array

Pasemos a inspeccionar el mismo programa usando memoria dinámica:

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

main() {
  int* data;      
  int valnum;    
  int i;          
  long sum=0;     

  do {
    printf("How many vals you need to add? ");
    scanf("%d", &valnum);
    data = (int *) malloc (valnum * sizeof(int));
    if (data == NULL)  
      printf("NO SPACE AVAILABLE.");
  } while (data == NULL); 


  for (i=0; i<valnum; i++) {
    printf("ENTER NUM %d ", i+1);
    scanf("%d", data+i);
  }


  for (i=0; i<valnum; i++) 
    sum += *(data+i);

  printf("SUM: %ld\n", sum);
  free(data);
}

La versión con memoria dinámica pregunta cuántos valores se van a almacenar y calcula el tamaño de la reserva:

CÓDIGO / SALIDA
data = (int *) malloc (valnum * sizeof(int));

Podemos desglosarlo para entenderlo mejor. Primero se calcula valnum * sizeof(int): sizeof(int) devuelve el tamaño en bytes de un entero en nuestro sistema (por ejemplo, 4), que se multiplica por valnum, la entrada del usuario (cuántos valores quiere añadir). Si el usuario quiere 4 valores, el cálculo sería algo como 4*4 = 16 bytes, el espacio necesario para alojar 4 enteros. Ese resultado se pasa a malloc. Esta función se usa para reservar espacio en memoria. void *malloc(size_t size) reserva la memoria solicitada (en el heap) y devuelve un puntero a ella (en la pila), o NULL si la solicitud falla; por eso el resultado de malloc se almacena en un puntero a entero.

A continuación el programa hace lo siguiente:

CÓDIGO / SALIDA
scanf("%d", data+i);

Nótese que normalmente al usar scanf pasamos &var; aquí, en cambio, pasamos data+i, siendo i un entero (1, 2, 3...). Por eso data es en sí mismo un puntero, una dirección de memoria, y usar & no tendría sentido. El +i indica la siguiente dirección de memoria disponible, ya que data apunta a la dirección base de ese array dinámico.

La misma lógica se aplica al calcular la suma

CÓDIGO / SALIDA
sum += *(data+i);

Desensamblemos ahora este programa y analicémoslo:

C / EJEMPLO
[0x558471c62175]> pdf
            ; DATA XREF from entry0 @ 0x558471c620ad
 322: int main (int argc, char **argv, char **envp);
           ; var int64_t var_20h @ rbp-0x20
           ; var int64_t var_1ch @ rbp-0x1c
           ; var int64_t var_18h @ rbp-0x18
           ; var int64_t var_10h @ rbp-0x10
           ; var int64_t var_8h @ rbp-0x8
           0x558471c62175      55             push rbp
           0x558471c62176      4889e5         mov rbp, rsp
           0x558471c62179      4883ec20       sub rsp, 0x20
           0x558471c6217d      64488b042528.  mov rax, qword fs:[0x28]
           0x558471c62186      488945f8       mov qword [var_8h], rax
           0x558471c6218a      31c0           xor eax, eax
           0x558471c6218c      48c745e80000.  mov qword [var_18h], 0
       ┌─> 0x558471c62194      488d3d6d0e00.  lea rdi, str.How_many_vals_you_need_to_add ; 0x558471c63008 ; "How many vals you need to add? "
          0x558471c6219b      b800000000     mov eax, 0
          0x558471c621a0      e8abfeffff     call sym.imp.printf     ; int printf(const char *format)
          0x558471c621a5      488d45e0       lea rax, [var_20h]
          0x558471c621a9      4889c6         mov rsi, rax
          0x558471c621ac      488d3d750e00.  lea rdi, [0x558471c63028] ; "%d"
          0x558471c621b3      b800000000     mov eax, 0
          0x558471c621b8      e8b3feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
          0x558471c621bd      8b45e0         mov eax, dword [var_20h]
          0x558471c621c0      4898           cdqe
          0x558471c621c2      48c1e002       shl rax, 2
          0x558471c621c6      4889c7         mov rdi, rax
          0x558471c621c9      e892feffff     call sym.imp.malloc     ;  void *malloc(size_t size)
          0x558471c621ce      488945f0       mov qword [var_10h], rax
          0x558471c621d2      48837df000     cmp qword [var_10h], 0
      ┌──< 0x558471c621d7      7511           jne 0x558471c621ea
      │╎   0x558471c621d9      488d3d4b0e00.  lea rdi, str.NO_SPACE_AVAILABLE. ; 0x558471c6302b ; "NO SPACE AVAILABLE."
      │╎   0x558471c621e0      b800000000     mov eax, 0
      │╎   0x558471c621e5      e866feffff     call sym.imp.printf     ; int printf(const char *format)
      └──> 0x558471c621ea      48837df000     cmp qword [var_10h], 0
       └─< 0x558471c621ef      74a3           je 0x558471c62194
           0x558471c621f1      c745e4000000.  mov dword [var_1ch], 0
       ┌─< 0x558471c621f8      eb45           jmp 0x558471c6223f
      ┌──> 0x558471c621fa      8b45e4         mov eax, dword [var_1ch]
      ╎│   0x558471c621fd      83c001         add eax, 1
      ╎│   0x558471c62200      89c6           mov esi, eax
      ╎│   0x558471c62202      488d3d360e00.  lea rdi, str.ENTER_NUM__d ; 0x558471c6303f ; "ENTER NUM %d "
      ╎│   0x558471c62209      b800000000     mov eax, 0
      ╎│   0x558471c6220e      e83dfeffff     call sym.imp.printf     ; int printf(const char *format)
      ╎│   0x558471c62213      8b45e4         mov eax, dword [var_1ch]
      ╎│   0x558471c62216      4898           cdqe
      ╎│   0x558471c62218      488d14850000.  lea rdx, [rax*4]
      ╎│   0x558471c62220      488b45f0       mov rax, qword [var_10h]
      ╎│   0x558471c62224      4801d0         add rax, rdx
      ╎│   0x558471c62227      4889c6         mov rsi, rax
      ╎│   0x558471c6222a      488d3df70d00.  lea rdi, [0x558471c63028] ; "%d"
      ╎│   0x558471c62231      b800000000     mov eax, 0
      ╎│   0x558471c62236      e835feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
      ╎│   0x558471c6223b      8345e401       add dword [var_1ch], 1
      ╎│   ; CODE XREF from main @ 0x558471c621f8
      ╎└─> 0x558471c6223f      8b45e0         mov eax, dword [var_20h]
          0x558471c62242      3945e4         cmp dword [var_1ch], eax
      └──< 0x558471c62245      7cb3           jl 0x558471c621fa
           0x558471c62247      c745e4000000.  mov dword [var_1ch], 0
       ┌─< 0x558471c6224e      eb20           jmp 0x558471c62270
      ┌──> 0x558471c62250      8b45e4         mov eax, dword [var_1ch]
      ╎│   0x558471c62253      4898           cdqe
      ╎│   0x558471c62255      488d14850000.  lea rdx, [rax*4]
      ╎│   0x558471c6225d      488b45f0       mov rax, qword [var_10h]
      ╎│   0x558471c62261      4801d0         add rax, rdx
      ╎│   0x558471c62264      8b00           mov eax, dword [rax]
      ╎│   0x558471c62266      4898           cdqe
      ╎│   0x558471c62268      480145e8       add qword [var_18h], rax
      ╎│   0x558471c6226c      8345e401       add dword [var_1ch], 1
      ╎│   ; CODE XREF from main @ 0x558471c6224e
      ╎└─> 0x558471c62270      8b45e0         mov eax, dword [var_20h]
          0x558471c62273      3945e4         cmp dword [var_1ch], eax
      └──< 0x558471c62276      7cd8           jl 0x558471c62250
           0x558471c62278      488b45e8       mov rax, qword [var_18h]
           0x558471c6227c      4889c6         mov rsi, rax
           0x558471c6227f      488d3dc70d00.  lea rdi, str.SUM:__ld   ; 0x558471c6304d ; "SUM: %ld\n"
           0x558471c62286      b800000000     mov eax, 0
           0x558471c6228b      e8c0fdffff     call sym.imp.printf     ; int printf(const char *format)
           0x558471c62290      488b45f0       mov rax, qword [var_10h]
           0x558471c62294      4889c7         mov rdi, rax
           0x558471c62297      e894fdffff     call sym.imp.free       ; void free(void *ptr)
           0x558471c6229c      b800000000     mov eax, 0
           0x558471c622a1      488b4df8       mov rcx, qword [var_8h]
           0x558471c622a5      6448330c2528.  xor rcx, qword fs:[0x28]
       ┌─< 0x558471c622ae      7405           je 0x558471c622b5
          0x558471c622b0      e88bfdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
       └─> 0x558471c622b5      c9             leave
           0x558471c622b6      c3             ret
[0x558471c62175]> 

El malloc se realiza aquí:

ENSAMBLADOR / REFERENCIA
          0x558471c621b8      e8b3feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)
          0x558471c621bd      8b45e0         mov eax, dword [var_20h]
          0x558471c621c0      4898           cdqe
          0x558471c621c2      48c1e002       shl rax, 2
          0x558471c621c6      4889c7         mov rdi, rax
          0x558471c621c9      e892feffff     call sym.imp.malloc     ;  void *malloc(size_t size)

Cargamos la entrada del usuario (var_20h) en eax y la multiplicamos por 4 con shl rax, 2. Como se puede ver, sizeof no se llama aquí porque el compilador ya conoce el tamaño de un entero. Así, la entrada del usuario multiplicada por 4 se pasa a malloc. Ejecutemos esto.

ENSAMBLADOR / REFERENCIA
          0x558471c621ce b    488945f0       mov qword [var_10h], rax
          0x558471c621d2      48837df000     cmp qword [var_10h], 0
      ┌──< 0x558471c621d7      7511           jne 0x558471c621ea
      │╎   0x558471c621d9      488d3d4b0e00.  lea rdi, str.NO_SPACE_AVAILABLE. ; 0x558471c6302b ; "NO SPACE AVAILABLE."
      │╎   0x558471c621e0      b800000000     mov eax, 0
      │╎   0x558471c621e5      e866feffff     call sym.imp.printf     ; int printf(const char *format)


[0x558471c621ce]> dr rax
0x5584735cfa80

[0x558471c621ce]> dmhg

Heap Layout
┌────────────────────────────────────┐
    Malloc chunk @ 0x5584735cf250   
 size: 0x410 status: allocated      
└────────────────────────────────────┘
    v
    
    
┌────────────────────────────────────┐
    Malloc chunk @ 0x5584735cf660   
 size: 0x410 status: allocated      
└────────────────────────────────────┘
    v
    
    
┌────────────────────────────────────┐
    Malloc chunk @ 0x5584735cfa70   
 size: 0x20 status: allocated       
└────────────────────────────────────┘
    v
    
    └──┐
       
   ┌───────────────────────────────┐
     Top chunk @ 0x5584735cfa90   
   └───────────────────────────────┘

[0x558471c621ce]> 

La función devuelve un puntero y se ha creado espacio en el heap. El puntero se guarda en var_10h.

ENSAMBLADOR / REFERENCIA
          0x558471c621ce b    488945f0       mov qword [var_10h], rax
          0x558471c621d2      48837df000     cmp qword [var_10h], 0

Veamos ahora cómo se gestiona la memoria para la entrada del usuario:

ENSAMBLADOR / REFERENCIA
      ╎│   0x558471c62213      8b45e4         mov eax, dword [var_1ch]
      ╎│   0x558471c62216      4898           cdqe
      ╎│   0x558471c62218      488d14850000.  lea rdx, [rax*4]
      ╎│   0x558471c62220      488b45f0       mov rax, qword [var_10h]
      ╎│   0x558471c62224      4801d0         add rax, rdx
      ╎│   0x558471c62227      4889c6         mov rsi, rax
      ╎│   0x558471c6222a      488d3df70d00.  lea rdi, [0x558471c63028] ; "%d"
      ╎│   0x558471c62231      b800000000     mov eax, 0
      ╎│   0x558471c62236      e835feffff     call sym.imp.__isoc99_scanf ; int scanf(const char *format)

Asumiendo que var_1ch es el contador del bucle, vemos cómo se mueve a eax y luego el CONTENIDO de rax*4 se carga en rdx y se suma a la dirección base del array dinámico. Siendo 4 el tamaño del entero, tiene todo el sentido: el contador avanzará de 4 en 4 por el array dinámico y asignará los nuevos valores en cada posición. La dirección correspondiente se pasa a scanf.

El siguiente bloque calcula la suma con las operaciones que ya hemos estudiado:

ENSAMBLADOR / REFERENCIA
      ┌──> 0x558471c62250      8b45e4         mov eax, dword [var_1ch]
      ╎│   0x558471c62253      4898           cdqe
      ╎│   0x558471c62255      488d14850000.  lea rdx, [rax*4]
      ╎│   0x558471c6225d      488b45f0       mov rax, qword [var_10h]
      ╎│   0x558471c62261      4801d0         add rax, rdx
      ╎│   0x558471c62264      8b00           mov eax, dword [rax]
      ╎│   0x558471c62266      4898           cdqe
      ╎│   0x558471c62268      480145e8       add qword [var_18h], rax
      ╎│   0x558471c6226c      8345e401       add dword [var_1ch], 1

Se hace lo mismo pero esta vez var_18h almacena la suma.

Final de la vida del objeto

Al final del programa se llama a free. Me gusta esta respuesta de Stack Overflow: https://stackoverflow.com/questions/14986543/calling-free-in-c

free devuelve al asignador un bloque obtenido mediante una reserva compatible. A partir de esa llamada termina la vida del objeto: conservar el valor del puntero no permite seguir accediendo a él. La liberación tampoco garantiza que sus bytes se borren o que la región se devuelva inmediatamente al sistema operativo.

ENSAMBLADOR / REFERENCIA
           0x558471c62290      488b45f0       mov rax, qword [var_10h]
           0x558471c62294      4889c7         mov rdi, rax
           0x558471c62297      e894fdffff     call sym.imp.free       ; void free(void *ptr)

Nótese que pasamos la dirección base de nuestro array dinámico a free.

Sesión en vídeo / Companion1:05

Observar una escritura en memoria.

La sesión empieza en un breakpoint preparado antes de *pc = 'b'. Hermes lee el byte, ejecuta una sola instrucción y comprueba el cambio en la memoria del proceso.

Captura de una sesión real de Hermes y radare2. Sin audio; esperas abreviadas.Descargar vídeo ↙
Leer la explicación y los comandos

La consulta

Hemos detenido pointer-write justo antes de la asignación *pc = 'b'. Examina la instrucción actual y el byte al que apunta el registro. Ejecuta únicamente esa instrucción con ds y vuelve a leer el byte. Explica el cambio observado y conserva el proceso detenido.

Qué muestra la sesión

La instrucción mov byte [rax], 0x62 escribe en la dirección que contiene RAX. Antes del paso, esa posición contiene 0x63, el código ASCII de c.

Tras ejecutar ds, la misma dirección contiene 0x62, el código de b. RIP avanza a la instrucción siguiente y el proceso permanece detenido.

Al inicio, Hermes distingue la posición de análisis de radare2 del registro RIP del proceso. La comprobación utiliza los registros del depurador y la dirección numérica de RAX para consultar el byte correcto.

Comprobación desde la consola

Desde la consola del companion se revisan estas instrucciones y datos:

CÓDIGO / SALIDA
dr rip
dr rax
px 1 @ 0x7ffff0807fdb
pd 3 @ 0x40120a

El breakpoint lo prepara el docente antes de grabar. Para repetir la lectura, consulta dr rax y utiliza la dirección de tu proceso; la dirección de pila del vídeo pertenece a esa ejecución. Descargar el fuente del vídeo ↙ · Preparar el companion ↗

Práctica manual

Vuelve al binario

Distinguir una dirección de su contenido y reconocer las reservas de pila y 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-write.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 9 --example pointer-write
./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.