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

Arrays y cadenas

LoberaFormación técnica04 / 33 · 6 min de lectura · Linux · x86 / x86-64
04 / 33

Leer datos contiguos y relacionar el índice con la dirección de cada elemento.

Consultar conceptos ↗

En esta entrega veremos algunas estructuras de datos básicas: arrays unidimensionales y arrays de caracteres que pueden interpretarse como cadenas.

Iremos ejemplo por ejemplo, que es una de las formas más directas de aprender, y nos centraremos en entender el desensamblado más que en explicar funcionalidades de r2.

Arrays

Los arrays son estructuras de datos simples que contienen uno o más elementos de un tipo concreto. Permiten referenciar múltiples elementos con una sola etiqueta, en lugar de declarar una variable por cada uno.

Considera el siguiente código:

C
# include <stdio.h>

main(){
 func();
 getchar();     
}

func(){
    int num[5];       
    int sum;            
    num[0] = 200;      
    num[1] = 150;
    num[2] = 100;
    num[3] = -50;
    num[4] = 300;
    sum = num[0] +     num[1] + num[2] + num[3] + num[4];
    printf("SUM IS %d", sum);
}

Declaramos un array numérico de 5 enteros. Podemos referenciar cada posición por su índice numérico y tratar cada elemento como una variable independiente, así que a efectos prácticos no hay gran diferencia.

Veámoslo dentro de r2.

ENSAMBLADOR / REFERENCIA
[0x7f84fc8ab090]> afl
0x562febd98000    5 292  -> 293  sym.imp.__libc_start_main
0x562febd98588    3 23           sym._init
0x562febd985b0    1 6            sym.imp.__stack_chk_fail
0x562febd985c0    1 6            sym.imp.printf
0x562febd985d0    1 6            sym.imp.getchar
0x562febd985e0    1 6            sub.__cxa_finalize_248_5e0
0x562febd985f0    1 43           entry0
0x562febd98620    4 50   -> 40   sym.deregister_tm_clones
0x562febd98660    4 66   -> 57   sym.register_tm_clones
0x562febd986b0    4 49           sym.__do_global_dtors_aux
0x562febd986f0    1 10           entry1.init
0x562febd986fa    1 26           sym.main
0x562febd98714    3 129          sym.funcion
0x562febd987a0    4 101          sym.__libc_csu_init
0x562febd98810    1 2            sym.__libc_csu_fini
0x562febd98814    1 9            sym._fini
0x562febf98fe0    1 1020         reloc.__libc_start_main_224
[0x7f84fc8ab090]> s sym.funcion
[0x562febd98714]> pdf
/ (fcn) sym.funcion 129
|   sym.funcion ();
|           ; var int local_24h @ rbp-0x24
|           ; var int local_20h @ rbp-0x20
|           ; var int local_1ch @ rbp-0x1c
|           ; var int local_18h @ rbp-0x18
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x562febd98703 (sym.main)
|           0x562febd98714      55             push rbp
|           0x562febd98715      4889e5         mov rbp, rsp
|           0x562febd98718      4883ec30       sub rsp, 0x30           ; '0'
|           0x562febd9871c      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x562febd98725      488945f8       mov qword [local_8h], rax
|           0x562febd98729      31c0           xor eax, eax
|           0x562febd9872b      c745e0c80000.  mov dword [local_20h], 0xc8 ; 200
|           0x562febd98732      c745e4960000.  mov dword [local_1ch], 0x96 ; 150
|           0x562febd98739      c745e8640000.  mov dword [local_18h], 0x64 ; 'd' ; 100
|           0x562febd98740      c745ecceffff.  mov dword [local_14h], 0xffffffce ; 4294967246
|           0x562febd98747      c745f02c0100.  mov dword [local_10h], 0x12c ; 300
|           0x562febd9874e      8b55e0         mov edx, dword [local_20h]
|           0x562febd98751      8b45e4         mov eax, dword [local_1ch]
|           0x562febd98754      01c2           add edx, eax
|           0x562febd98756      8b45e8         mov eax, dword [local_18h]
|           0x562febd98759      01c2           add edx, eax
|           0x562febd9875b      8b45ec         mov eax, dword [local_14h]
|           0x562febd9875e      01c2           add edx, eax
|           0x562febd98760      8b45f0         mov eax, dword [local_10h]
|           0x562febd98763      01d0           add eax, edx
|           0x562febd98765      8945dc         mov dword [local_24h], eax
|           0x562febd98768      8b45dc         mov eax, dword [local_24h]
|           0x562febd9876b      89c6           mov esi, eax
|           0x562febd9876d      488d3db00000.  lea rdi, qword str.SUM_IS__d ; 0x562febd98824 ; "SUM IS %d" ; const char * format
|           0x562febd98774      b800000000     mov eax, 0
|           0x562febd98779      e842feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x562febd9877e      90             nop
|           0x562febd9877f      488b4df8       mov rcx, qword [local_8h]
|           0x562febd98783      6448330c2528.  xor rcx, qword fs:[0x28]
|       ,=< 0x562febd9878c      7405           je 0x562febd98793
|       |   0x562febd9878e      e81dfeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x562febd98793      c9             leave
\           0x562febd98794      c3             ret
[0x562febd98714]> 

Como antes, podemos dividir el análisis en partes. Si miramos con atención, vemos algunas «variables» declaradas dentro de la función:

ENSAMBLADOR / REFERENCIA
           ; var int local_24h @ rbp-0x24
|           ; var int local_20h @ rbp-0x20
|           ; var int local_1ch @ rbp-0x1c
|           ; var int local_18h @ rbp-0x18
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x562febd98703 (sym.main)
|           0x562febd98714      55             push rbp
|           0x562febd98715      4889e5         mov rbp, rsp
|           0x562febd98718      4883ec30       sub rsp, 0x30           ; '0'
|           0x562febd9871c      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x562febd98725      488945f8       mov qword [local_8h], rax

Ya sabemos que local_8h corresponde al stack guard, sin problema. Pero aparecen otras 6 variables, lo cual resulta extraño, dado que solo declaramos el array y la variable sum.

Si seguimos la traza de esas 6 variables en el código vemos:

ENSAMBLADOR / REFERENCIA
|           0x562febd9872b      c745e0c80000.  mov dword [local_20h], 0xc8 ; 200
|           0x562febd98732      c745e4960000.  mov dword [local_1ch], 0x96 ; 150
|           0x562febd98739      c745e8640000.  mov dword [local_18h], 0x64 ; 'd' ; 100
|           0x562febd98740      c745ecceffff.  mov dword [local_14h], 0xffffffce ; 4294967246
|           0x562febd98747      c745f02c0100.  mov dword [local_10h], 0x12c ; 300

Se inicializan a 200, 150, 100, un valor extraño y 300. ¿Qué es ese valor extraño? Debería ser -50.

Si usamos rax2…

CÓDIGO / SALIDA
red@blue:~/c/chapter4$ rax2 0x0ffffffffffffffce
-50
red@blue:~/c/chapter4$ 

red@blue:~/c/chapter4$ rax2 -50
0xffffffffffffffce

Problema resuelto. El compilador interpreta el array como un conjunto de variables asignadas una tras otra en memoria. Podemos comprobarlo fácilmente colocando un breakpoint tras la inicialización:

Y volcando la dirección de memoria inicial.

ENSAMBLADOR / REFERENCIA
|           0x562febd98747      c745f02c0100.  mov dword [local_10h], 0x12c ; 300
|           ;-- rip:
|           0x562febd9874e b    8b55e0         mov edx, dword [local_20h]

[0x562febd98714]> afvd
var local_8h = 0x7ffcf5d7ad88  0xebc835f94cbbc100   ...L.5..
var local_20h = 0x7ffcf5d7ad70  0x00000096000000c8   ........
var local_1ch = 0x7ffcf5d7ad74  0x0000006400000096   ....d...
var local_18h = 0x7ffcf5d7ad78  0xffffffce00000064   d.......
var local_14h = 0x7ffcf5d7ad7c  0x0000012cffffffce   ....,...
var local_10h = 0x7ffcf5d7ad80  0x0000562f0000012c   ,.../V..
var local_24h = 0x7ffcf5d7ad6c  0x000000c80000562f   /V......


[0x562febd98714]> pxw @ 0x7ffcf5d7ad70
0x7ffcf5d7ad70  0x000000c8 0x00000096 0x00000064 0xffffffce  ........d.......
0x7ffcf5d7ad80  0x0000012c 0x0000562f 0x4cbbc100 0xebc835f9  ,.../V.....L.5..
0x7ffcf5d7ad90  0xf5d7ada0 0x00007ffc 0xebd98708 0x0000562f  ............/V..

Todas esas variables aparecen juntas en memoria una tras otra, tal como funcionan los arrays.

A partir de ahí no hay mayor dificultad. El registro edx acumula la suma de todos esos números, la variable sum almacena el valor final y se llama a printf.

Veamos ahora exactamente lo mismo pero usando variables individuales:

C
# include <stdio.h>

main(){
 func();
 getchar();     
}

func(){
    int sum;

    int num0 = 200;
    int num1 = 150;
    int num2 = 100;
    int num3 = -50;
    int num4 = 300;
    sum = num0 + num1 + num2 + num3 + num4;
    printf("SUM is %d", sum);

}
ENSAMBLADOR / REFERENCIA
[0x5570354d96a4]> pdf
/ (fcn) sym.func 94
|   sym.func ();
|           ; var int local_18h @ rbp-0x18
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_ch @ rbp-0xc
|           ; var int local_8h @ rbp-0x8
|           ; var int local_4h @ rbp-0x4
|              ; CALL XREF from 0x5570354d9693 (sym.main)
|           0x5570354d96a4      55             push rbp
|           0x5570354d96a5      4889e5         mov rbp, rsp
|           0x5570354d96a8      4883ec20       sub rsp, 0x20
|           0x5570354d96ac      c745e8c80000.  mov dword [local_18h], 0xc8 ; 200
|           0x5570354d96b3      c745ec960000.  mov dword [local_14h], 0x96 ; 150
|           0x5570354d96ba      c745f0640000.  mov dword [local_10h], 0x64 ; 'd' ; 100
|           0x5570354d96c1      c745f4ceffff.  mov dword [local_ch], 0xffffffce ; 4294967246
|           0x5570354d96c8      c745f82c0100.  mov dword [local_8h], 0x12c ; 300
|           0x5570354d96cf      8b55e8         mov edx, dword [local_18h]
|           0x5570354d96d2      8b45ec         mov eax, dword [local_14h]
|           0x5570354d96d5      01c2           add edx, eax
|           0x5570354d96d7      8b45f0         mov eax, dword [local_10h]
|           0x5570354d96da      01c2           add edx, eax
|           0x5570354d96dc      8b45f4         mov eax, dword [local_ch]
|           0x5570354d96df      01c2           add edx, eax
|           0x5570354d96e1      8b45f8         mov eax, dword [local_8h]
|           0x5570354d96e4      01d0           add eax, edx
|           0x5570354d96e6      8945fc         mov dword [local_4h], eax
|           0x5570354d96e9      8b45fc         mov eax, dword [local_4h]
|           0x5570354d96ec      89c6           mov esi, eax
|           0x5570354d96ee      488d3d9f0000.  lea rdi, qword str.SUM_is__d ; 0x5570354d9794 ; "SUM is %d"
|           0x5570354d96f5      b800000000     mov eax, 0
|           0x5570354d96fa      e851feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5570354d96ff      90             nop
|           0x5570354d9700      c9             leave
\           0x5570354d9701      c3             ret
[0x5570354d96a4]> 

No hay mucho más que explicar: el resultado es idéntico. El compilador ha detectado que el código hace exactamente lo mismo y lo ha tratado de la misma manera.

Ahora bien, un array de 5 posiciones puede leerse como 5 variables independientes, pero ¿qué ocurre con un array de 100 elementos? ¿Tendrá el mismo aspecto?

Veámoslo.

C
# include <stdio.h>

main(){
 func();
 getchar();     
}

func(){

int arr[100];
for (int i=0;i<100;i++){
        arr[i]=i;
}
for (int i=0;i<100;i++){
        printf("val %d",arr[i]);
}
}

Primero se declara, luego se rellena y al final se imprime.

ENSAMBLADOR / REFERENCIA
:> pdf
/ (fcn) sym.func 160
|   sym.func ();
|           ; var int local_1a8h @ rbp-0x1a8
|           ; var int local_1a4h @ rbp-0x1a4
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x5567f0ed8703 (sym.main)
|           0x5567f0ed8714      55             push rbp
|           0x5567f0ed8715      4889e5         mov rbp, rsp
|           0x5567f0ed8718      4881ecb00100.  sub rsp, 0x1b0
|           0x5567f0ed871f      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x5567f0ed8728      488945f8       mov qword [local_8h], rax
|           0x5567f0ed872c      31c0           xor eax, eax
|           0x5567f0ed872e      c78558feffff.  mov dword [local_1a8h], 0
|       ,=< 0x5567f0ed8738      eb1c           jmp 0x5567f0ed8756
|      .--> 0x5567f0ed873a      8b8558feffff   mov eax, dword [local_1a8h]
|      :|   0x5567f0ed8740      4898           cdqe
|      :|   0x5567f0ed8742      8b9558feffff   mov edx, dword [local_1a8h]
|      :|   0x5567f0ed8748      89948560feff.  mov dword [rbp + rax*4 - 0x1a0], edx
|      :|   0x5567f0ed874f      838558feffff.  add dword [local_1a8h], 1
|      :|      ; JMP XREF from 0x5567f0ed8738 (sym.func)
|      :`-> 0x5567f0ed8756      83bd58feffff.  cmp dword [local_1a8h], 0x63 ; [0x63:4]=-1 ; 'c' ; 99
|      `==< 0x5567f0ed875d      7edb           jle 0x5567f0ed873a
|           0x5567f0ed875f      c7855cfeffff.  mov dword [local_1a4h], 0
|       ,=< 0x5567f0ed8769      eb29           jmp 0x5567f0ed8794
|      .--> 0x5567f0ed876b      8b855cfeffff   mov eax, dword [local_1a4h]
|      :|   0x5567f0ed8771      4898           cdqe
|      :|   0x5567f0ed8773      8b848560feff.  mov eax, dword [rbp + rax*4 - 0x1a0]
|      :|   0x5567f0ed877a      89c6           mov esi, eax
|      :|   0x5567f0ed877c      488d3dc10000.  lea rdi, qword str.val__d ; 0x5567f0ed8844 ; "val %d"
|      :|   0x5567f0ed8783      b800000000     mov eax, 0
|      :|   0x5567f0ed8788      e833feffff     call sym.imp.printf     ; int printf(const char *format)
|      :|   0x5567f0ed878d      83855cfeffff.  add dword [local_1a4h], 1
|      :|      ; JMP XREF from 0x5567f0ed8769 (sym.func)
|      :`-> 0x5567f0ed8794      83bd5cfeffff.  cmp dword [local_1a4h], 0x63 ; [0x63:4]=-1 ; 'c' ; 99
|      `==< 0x5567f0ed879b      7ece           jle 0x5567f0ed876b
|           0x5567f0ed879d      90             nop
|           0x5567f0ed879e      488b4df8       mov rcx, qword [local_8h]
|           0x5567f0ed87a2      6448330c2528.  xor rcx, qword fs:[0x28]
|       ,=< 0x5567f0ed87ab      7405           je 0x5567f0ed87b2
|       |   0x5567f0ed87ad      e8fefdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x5567f0ed87b2      c9             leave
\           0x5567f0ed87b3      c3             ret
:> 

Como era de esperar, no aparecen 100 variables independientes, sino tres: la del stack guard y un par más que probablemente corresponden a los índices de los dos bucles for.

El programa usará registros como índices. Inspeccionemos el primer bucle, el que inicializa el array:

ENSAMBLADOR / REFERENCIA
|           0x5567f0ed872e      c78558feffff.  mov dword [local_1a8h], 0
|       ,=< 0x5567f0ed8738      eb1c           jmp 0x5567f0ed8756
|      .--> 0x5567f0ed873a      8b8558feffff   mov eax, dword [local_1a8h]
|      :|   0x5567f0ed8740      4898           cdqe

Primero inicializa una variable, local_1a8h, y luego salta aquí:

ENSAMBLADOR / REFERENCIA
|      :`-> 0x5567f0ed8756      83bd58feffff.  cmp dword [local_1a8h], 0x63 ; [0x63:4]=-1 ; 'c' ; 99
|      `==< 0x5567f0ed875d      7edb           jle 0x5567f0ed873a
|           0x5567f0ed875f      c7855cfeffff.  mov dword [local_1a4h], 0

Tras el salto, el programa compara el valor inicializado con 99 (i < 100); si es menor, vuelve al inicio del bucle. Si es igual o mayor, continúa.

ENSAMBLADOR / REFERENCIA
|      .--> 0x5567f0ed873a      8b8558feffff   mov eax, dword [local_1a8h]
|      :|   0x5567f0ed8740      4898           cdqe
|      :|   0x5567f0ed8742      8b9558feffff   mov edx, dword [local_1a8h]
|      :|   0x5567f0ed8748      89948560feff.  mov dword [rbp + rax*4 - 0x1a0], edx
|      :|   0x5567f0ed874f      838558feffff.  add dword [local_1a8h], 1

El programa mueve el contenido de i a eax; como eax es la parte de 32 bits de RAX, usa cdqe para que el valor funcione correctamente en modo de 64 bits.

*Para saber más: en modo de 64 bits, el tamaño de operación por defecto es el del registro destino. El prefijo REX.W promueve esta instrucción (CDQE cuando se promueve) para operar con operandos de 64 bits. En ese caso, CDQE copia el signo (bit 31) del doble word en EAX a los 32 bits altos de RAX.

A continuación mueve el resultado a edx (usado aquí como registro temporal) y lo almacena en su posición dentro del array.

¿Cómo se calcula esa posición? Tenemos: [rbp + rax4 - 0x1a0], que puede representarse como: [(rbp - 0x1a0) + rax4]. Como se puede ver, (rbp - 0x1a0) es simplemente una zona de memoria reservada para el array.

A eso se le suma rax*4 en cada iteración. En ese punto rax contiene el valor de i; como un int ocupa 4 bytes, el valor de rax se multiplica por 4 para situar el entero en la posición correcta dentro del array.

En resumen: (rbp - 0x1a0) es la dirección base del array y rax*4 es el índice.

El resto del código funciona igual.

Inicialización de arrays

Los arrays pueden preinicializarse, pero el resultado es el mismo:

C
# include <stdio.h>

main(){
 func();
 getchar();     
}

func(){
    int sum=0;
    int i;

    int num[5] ={200, 150, 100, -50, 300};
        for(i=0;i<=4;i++) sum += num[i]; 
                printf("SUM is %d", sum);
        }
ENSAMBLADOR / REFERENCIA
[0x00000714]> pdf
/ (fcn) sym.func 141
|   sym.func ();
|           ; var int local_28h @ rbp-0x28
|           ; var int local_24h @ rbp-0x24
|           ; var int local_20h @ rbp-0x20
|           ; var int local_1ch @ rbp-0x1c
|           ; var int local_18h @ rbp-0x18
|           ; var int local_14h @ rbp-0x14
|           ; var int local_10h @ rbp-0x10
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x00000703 (sym.main)
|           0x00000714      55             push rbp
|           0x00000715      4889e5         mov rbp, rsp
|           0x00000718      4883ec30       sub rsp, 0x30               ; '0'
|           0x0000071c      64488b042528.  mov rax, qword fs:[0x28]    ; [0x28:8]=0x19b8 ; '('
|           0x00000725      488945f8       mov qword [local_8h], rax
|           0x00000729      31c0           xor eax, eax
|           0x0000072b      c745d8000000.  mov dword [local_28h], 0
|           0x00000732      c745e0c80000.  mov dword [local_20h], 0xc8
|           0x00000739      c745e4960000.  mov dword [local_1ch], 0x96
|           0x00000740      c745e8640000.  mov dword [local_18h], 0x64 ; 'd'
|           0x00000747      c745ecceffff.  mov dword [local_14h], 0xffffffce ; 4294967246
|           0x0000074e      c745f02c0100.  mov dword [local_10h], 0x12c
|           0x00000755      c745dc000000.  mov dword [local_24h], 0
[...]

Exactamente lo mismo.

Arrays de caracteres / cadenas

Aquí las cosas se vuelven algo más interesantes. Declararemos un array de caracteres que puede interpretarse como una cadena y usaremos scanf para leer la entrada del usuario:

C
# include <stdio.h>

main(){
 func();
 getchar();  
 getchar();   
}

func(){
    char text[40];        

    printf("Name?: ");
    scanf("%s", &text);
    printf("Hi, %s\n", text);

        }

En este caso solo hay una variable: local_30h.

ENSAMBLADOR / REFERENCIA
[0x55d494124789]> pdf
/ (fcn) sym.func 111
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x55d494124773 (sym.main)
|           0x55d494124789      55             push rbp
|           0x55d49412478a      4889e5         mov rbp, rsp
|           0x55d49412478d      4883ec30       sub rsp, 0x30           ; '0'
|           0x55d494124791      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x55d49412479a      488945f8       mov qword [local_8h], rax
|           0x55d49412479e      31c0           xor eax, eax
|           0x55d4941247a0      488d3ddd0000.  lea rdi, qword str.Name_: ; 0x55d494124884 ; "Name?: "
|           0x55d4941247a7      b800000000     mov eax, 0
|           0x55d4941247ac      e86ffeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d4941247b1      488d45d0       lea rax, qword [local_30h]
|           0x55d4941247b5      4889c6         mov rsi, rax
|           0x55d4941247b8      488d3dcd0000.  lea rdi, qword [0x55d49412488c] ; "%s"
|           0x55d4941247bf      b800000000     mov eax, 0
|           0x55d4941247c4      e877feffff     call sym.imp.__isoc99_scanf
|           0x55d4941247c9      488d45d0       lea rax, qword [local_30h]
|           0x55d4941247cd      4889c6         mov rsi, rax
|           0x55d4941247d0      488d3db80000.  lea rdi, qword str.Hi___s ; 0x55d49412488f ; "Hi, %s\n"
|           0x55d4941247d7      b800000000     mov eax, 0
|           0x55d4941247dc      e83ffeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d4941247e1      90             nop
|           0x55d4941247e2      488b55f8       mov rdx, qword [local_8h]
|           0x55d4941247e6      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x55d4941247ef      7405           je 0x55d4941247f6
|       |   0x55d4941247f1      e81afeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x55d4941247f6      c9             leave
\           0x55d4941247f7      c3             ret
[0x55d494124789]> 

local_30h está 0x30 (48 en decimal) bytes por debajo de rbp, lo que encaja perfectamente con que sea el array de caracteres donde se almacenará la cadena.

Podemos colocar un breakpoint en el punto donde se referencia por primera vez, justo antes de entrar en scanf.

CÓDIGO / SALIDA
[0x55d494124789]> db 0x55d4941247b5
[0x55d494124789]> dc
hit breakpoint at: 55d4941247b5
[0x55d494124789]> 

[0x55d494124789]> dr
rax = 0x7ffdfa5e7920

[0x55d494124789]> pxw @ 0x7ffdfa5e7920
0x7ffdfa5e7920  0x00000001 0x00000000 0x9412484d 0x000055d4  ........MH...U..
0x7ffdfa5e7930  0x032ee9a0 0x00007ff9 0x00000000 0x00000000  ................

Anotamos esta dirección: 0x7ffdfa5e7920, que es donde se almacenará la cadena.

CÓDIGO / SALIDA
[0x55d494124789]> db 0x55d4941247cd
[0x55d494124789]> dc
Name?: ARTIK
hit breakpoint at: 55d4941247cd
[0x55d4941247cd]> pxw @ 0x7ffdfa5e7920
0x7ffdfa5e7920  0x49545241 0x0000004b 0x9412484d 0x000055d4  ARTIK...MH...U..

El resto del código sigue el mismo esquema: local_30h se pasa de nuevo a printf y se imprime la salida.

Como de costumbre, se puede acceder a los elementos del array de caracteres individualmente mediante un índice. Considera este ejemplo:

C
# include <stdio.h>

main(){
 func();
 getchar();
  getchar();     
}

func(){

    char text[40];        

    printf("Name?: ");
    scanf("%s", text);
    printf("Hey, %s. First letter: %c\n", text, text[0]);
    printf("Ho, %s. Second letter: %c\n", text, text[1]);
}

El programa declara un array de caracteres, lee la entrada del usuario para almacenar el texto y luego accede al array tanto de forma global como por índice.

Dentro de radare2 tiene este aspecto.

ENSAMBLADOR / REFERENCIA
[0x55d2fe6d3789]> pdf
/ (fcn) sym.func 149
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_2fh @ rbp-0x2f
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x55d2fe6d3773 (sym.main)
|           0x55d2fe6d3789      55             push rbp
|           0x55d2fe6d378a      4889e5         mov rbp, rsp
|           0x55d2fe6d378d      4883ec30       sub rsp, 0x30           ; '0'
|           0x55d2fe6d3791      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x55d2fe6d379a      488945f8       mov qword [local_8h], rax
|           0x55d2fe6d379e      31c0           xor eax, eax
|           0x55d2fe6d37a0      488d3dfd0000.  lea rdi, qword str.Name_: ; 0x55d2fe6d38a4 ; "Name?: "
|           0x55d2fe6d37a7      b800000000     mov eax, 0
|           0x55d2fe6d37ac      e86ffeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d2fe6d37b1      488d45d0       lea rax, qword [local_30h]
|           0x55d2fe6d37b5      4889c6         mov rsi, rax
|           0x55d2fe6d37b8      488d3ded0000.  lea rdi, qword [0x55d2fe6d38ac] ; "%s"
|           0x55d2fe6d37bf      b800000000     mov eax, 0
|           0x55d2fe6d37c4      e877feffff     call sym.imp.__isoc99_scanf
|           0x55d2fe6d37c9      0fb645d0       movzx eax, byte [local_30h]
|           0x55d2fe6d37cd      0fbed0         movsx edx, al
|           0x55d2fe6d37d0      488d45d0       lea rax, qword [local_30h]
|           0x55d2fe6d37d4      4889c6         mov rsi, rax
|           0x55d2fe6d37d7      488d3dd10000.  lea rdi, qword str.Hey___s._First_letter:__c ; 0x55d2fe6d38af ; "Hey, %s. First letter: %c\n"
|           0x55d2fe6d37de      b800000000     mov eax, 0
|           0x55d2fe6d37e3      e838feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d2fe6d37e8      0fb645d1       movzx eax, byte [local_2fh]
|           0x55d2fe6d37ec      0fbed0         movsx edx, al
|           0x55d2fe6d37ef      488d45d0       lea rax, qword [local_30h]
|           0x55d2fe6d37f3      4889c6         mov rsi, rax
|           0x55d2fe6d37f6      488d3dcd0000.  lea rdi, qword str.Ho___s._Second_letter:__c ; 0x55d2fe6d38ca ; "Ho, %s. Second letter: %c\n"
|           0x55d2fe6d37fd      b800000000     mov eax, 0
|           0x55d2fe6d3802      e819feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55d2fe6d3807      90             nop
|           0x55d2fe6d3808      488b4df8       mov rcx, qword [local_8h]
|           0x55d2fe6d380c      6448330c2528.  xor rcx, qword fs:[0x28]
|       ,=< 0x55d2fe6d3815      7405           je 0x55d2fe6d381c
|       |   0x55d2fe6d3817      e8f4fdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x55d2fe6d381c      c9             leave
\           0x55d2fe6d381d      c3             ret
[0x55d2fe6d3789]> 

Detectamos dos variables:

CÓDIGO / SALIDA
/ (fcn) sym.func 149
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_2fh @ rbp-0x2f
|           ; var int local_8h @ rbp-0x8

La primera ya la conocemos: es la dirección base del array de caracteres. La segunda está justo un byte por encima de esa dirección base; lo tendremos en cuenta. Podemos renombrarlas para mayor claridad.

Ya conocemos cómo scanf lee la cadena:

ENSAMBLADOR / REFERENCIA
|           0x55d2fe6d37b1      488d45d0       lea rax, qword [text_array]
|           0x55d2fe6d37b5      4889c6         mov rsi, rax
|           0x55d2fe6d37b8      488d3ded0000.  lea rdi, qword [0x55d2fe6d38ac] ; "%s"
|           0x55d2fe6d37bf      b800000000     mov eax, 0
|           0x55d2fe6d37c4      e877feffff     call sym.imp.__isoc99_scanf

Pero luego aparece esto:

ENSAMBLADOR / REFERENCIA
|           0x55d2fe6d37c9      0fb645d0       movzx eax, byte [text_array]
|           0x55d2fe6d37cd      0fbed0         movsx edx, al
|           0x55d2fe6d37d0      488d45d0       lea rax, qword [text_array]
|           0x55d2fe6d37d4      4889c6         mov rsi, rax
|           0x55d2fe6d37d7      488d3dd10000.  lea rdi, qword str.Hey___s._First_letter:__c ; 0x55d2fe6d38af ; "Hey, %s. First letter: %c\n"
|           0x55d2fe6d37de      b800000000     mov eax, 0
|           0x55d2fe6d37e3      e838feffff     call sym.imp.printf     ; int printf(const char *format)

En la primera instrucción vemos movzx extrayendo el primer byte de text_array (copia el contenido del operando fuente —registro o posición de memoria— al operando destino —registro— y extiende el valor con ceros; el tamaño del valor convertido depende del atributo de tamaño de operando).

Ese byte (al) se mueve a edx con movsx (copia el contenido del operando fuente —registro o posición de memoria— al operando destino —registro— y extiende el valor con signo a 16 o 32 bits).

Podemos deducir que esto extrae el primer carácter del array; después el puntero a ese array se mueve a rsi a través de rax y se llama a printf.

El siguiente bloque de código hace lo mismo, pero usando la variable «pos1» que detectamos antes como índice. Lo depuramos para verlo con más claridad.

ENSAMBLADOR / REFERENCIA
|           0x55d2fe6d37e8      0fb645d1       movzx eax, byte [pos1]
|           0x55d2fe6d37ec      0fbed0         movsx edx, al
|           ;-- rip:
|           0x55d2fe6d37ef b    488d45d0       lea rax, qword [text_array]
|           0x55d2fe6d37f3      4889c6         mov rsi, rax
|           0x55d2fe6d37f6      488d3dcd0000.  lea rdi, qword str.Ho___s._Second_letter:__c ; 0x55d2fe6d38ca ; "Ho, %s. Second letter: %c\n"
|           0x55d2fe6d37fd      b800000000     mov eax, 0

Tras colocar un breakpoint justo antes del movsx e inspeccionar los registros, vemos que:

CÓDIGO / SALIDA
[0x55d2fe6d37d0]> dr
rax = 0x00000052
rbx = 0x00000000
rcx = 0x00000000
rdx = 0x00000052

rdx (y al) = 52 = código ASCII de R, siendo ARTIK la entrada.

Sesión en vídeo / Companion0:43

Reconocer un array en la pila.

Hermes relaciona cinco posiciones de memoria con las instrucciones que inicializan el array y calculan su suma. La consola permite revisar esa misma secuencia.

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

En array-sum, localiza el array de cinco enteros y las instrucciones que suman sus elementos. ¿Qué tamaño tiene cada elemento en esta compilación y qué valor se pasa a printf? Compruébalo en el desensamblado de func.

Qué muestra la sesión

Los accesos dword operan sobre enteros de 32 bits. Las direcciones de los elementos avanzan de cuatro en cuatro bytes; los cinco elementos ocupan 20 bytes.

Las inicializaciones corresponden a 200, 150, 100, −50 y 300. Las instrucciones add acumulan 700, representado como 0x2bc. El resultado se prepara en ESI para la llamada a printf.

La última dirección que aparece en el listado es el comienzo del quinto elemento. Para delimitar todo el array hay que incluir también sus cuatro bytes.

Comprobación desde la consola

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

CÓDIGO / SALIDA
pd 20 @ 0x4011af

Las direcciones corresponden a la compilación grabada. Localiza las funciones y operaciones de tu propio ejecutable antes de repetir los comandos. Descargar el fuente del vídeo ↙ · Preparar el companion ↗

Práctica manual

Vuelve al binario

Leer datos contiguos y relacionar el índice con la dirección de cada elemento. 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: array-sum.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 4 --example array-sum
./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.