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

Entrada de texto y terminación

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

Distinguir la capacidad de un buffer, la longitud de una cadena y su terminador nulo.

Consultar conceptos ↗

En esta entrega vamos a profundizar un poco más en los arrays de caracteres y las cadenas para asentar una base sólida sobre el tema y poder avanzar hacia estructuras de datos más complejas (como arrays multidimensionales y structs).

Repaso de los arrays de caracteres

Como vimos en la entrega anterior, las cadenas pueden declararse como arrays de char de tamaño fijo y mostrarse con printf usando %s como especificador de formato.

C
# include <stdio.h>

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

func(){

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

}

Algo interesante de scanf es que lee hasta el primer espacio en blanco. Veámoslo:

ENSAMBLADOR / REFERENCIA
[0x5653b864f78e]> pdf
/ (fcn) sym.func 111
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x5653b864f773 (sym.main)
|           0x5653b864f78e      55             push rbp
|           0x5653b864f78f      4889e5         mov rbp, rsp
|           0x5653b864f792      4883ec30       sub rsp, 0x30           ; '0'
|           0x5653b864f796      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x5653b864f79f      488945f8       mov qword [local_8h], rax
|           0x5653b864f7a3      31c0           xor eax, eax
|           0x5653b864f7a5      488d3dd80000.  lea rdi, qword str.Name_: ; 0x5653b864f884 ; "Name?: "
|           0x5653b864f7ac      b800000000     mov eax, 0
|           0x5653b864f7b1      e86afeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5653b864f7b6      488d45d0       lea rax, qword [local_30h]
|           0x5653b864f7ba      4889c6         mov rsi, rax
|           0x5653b864f7bd      488d3dc80000.  lea rdi, qword [0x5653b864f88c] ; "%s"
|           0x5653b864f7c4      b800000000     mov eax, 0
|           0x5653b864f7c9      e872feffff     call sym.imp.__isoc99_scanf
|           0x5653b864f7ce      488d45d0       lea rax, qword [local_30h]
|           0x5653b864f7d2      4889c6         mov rsi, rax
|           0x5653b864f7d5      488d3db30000.  lea rdi, qword str.Hi___s ; 0x5653b864f88f ; "Hi, %s\n"
|           0x5653b864f7dc      b800000000     mov eax, 0
|           0x5653b864f7e1      e83afeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5653b864f7e6      90             nop
|           0x5653b864f7e7      488b55f8       mov rdx, qword [local_8h]
|           0x5653b864f7eb      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x5653b864f7f4      7405           je 0x5653b864f7fb
|       |   0x5653b864f7f6      e815feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x5653b864f7fb      c9             leave
\           0x5653b864f7fc      c3             ret
[0x5653b864f78e]> db 0x5653b864f7dc
[0x7f1e4dda4090]> dc
Name?: sample text
hit breakpoint at: 5653b864f7dc
[0x5653b864f7dc]> 

Tras colocar un breakpoint justo después del printf y ejecutar el programa, se nos pedirá una entrada. Si introducimos algo como «sample text», el sentido común nos dice que la salida debería ser «Hi, sample text», pero no es así.

ENSAMBLADOR / REFERENCIA
|           0x5653b864f7c9      e872feffff     call sym.imp.__isoc99_scanf
|           0x5653b864f7ce      488d45d0       lea rax, qword [local_30h]
|           0x5653b864f7d2      4889c6         mov rsi, rax
|           0x5653b864f7d5      488d3db30000.  lea rdi, qword str.Hi___s ; 0x5653b864f88f ; "Hi, %s\n"
|           ;-- rip:
|           0x5653b864f7dc b    b800000000     mov eax, 0
|           0x5653b864f7e1      e83afeffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5653b864f7e6      90             nop
|           0x5653b864f7e7      488b55f8       mov rdx, qword [local_8h]
|           0x5653b864f7eb      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x5653b864f7f4      7405           je 0x5653b864f7fb
|       |   0x5653b864f7f6      e815feffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x5653b864f7fb      c9             leave
\           0x5653b864f7fc      c3             ret
[0x5653b864f7dc]> dr
rax = 0x7ffceeaaabd0
rbx = 0x00000000
rcx = 0x7f1e4dd9e560
rdx = 0x7f1e4dd9f8d0
r8 = 0x00000000
r9 = 0x00000000
r10 = 0x00000000
r11 = 0x5653b864f88e
r12 = 0x5653b864f660
r13 = 0x7ffceeaaacf0
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x7ffceeaaabd0
rdi = 0x5653b864f88f
rsp = 0x7ffceeaaabd0
rbp = 0x7ffceeaaac00
rip = 0x5653b864f7dc
rflags = 0x00000206
orax = 0xffffffffffffffff
[0x5653b864f7dc]> pxw @ 0x7ffceeaaabd0
0x7ffceeaaabd0  0x706d6173 0x0000656c 0xb864f84d 0x00005653  sample..M.d.SV..

La conversión %s de scanf lee una secuencia hasta el siguiente espacio en blanco. Por eso el ejemplo guarda «sample» y deja el resto de la entrada pendiente. Una conversión de conjunto de caracteres permite leer hasta el salto de línea:

CÓDIGO / SALIDA
scanf("%[^\n]",text);

El formato %[^\n] acepta los espacios y se detiene ante un salto de línea. En un programa actual hay que limitar también la anchura de la conversión a la capacidad del destino, reservando un byte para el terminador, y comprobar el retorno de scanf.

Entrada y salida: puts y gets

Los listados históricos siguientes comparan gets y puts con las llamadas anteriores. gets leía una línea de la entrada estándar, conservaba sus espacios y sustituía el salto de línea por un byte nulo. puts escribe una cadena y añade un salto de línea. Ambas carecen del argumento de formato de scanf y printf.

gets fue retirada de C11 porque su interfaz no permite indicar la capacidad del buffer. Aquí se conserva para reconocerla en los binarios originales; la práctica actual del companion utiliza un ejemplo acotado. Referencia de gets.

Inspeccionemos el siguiente código:

C
# include <stdio.h>

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

}

func(){

    char text[40];
    puts("Name?: ");
    gets(text);
    printf("Ho, %s\n", text);
}

Aquí puts se usa igual que printf y gets se usa para leer de stdin. Veámoslo:

ENSAMBLADOR / REFERENCIA
[0x5618704127a4]> pdf
/ (fcn) sym.func 99
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x561870412793 (sym.main)
|           0x5618704127a4      55             push rbp
|           0x5618704127a5      4889e5         mov rbp, rsp
|           0x5618704127a8      4883ec30       sub rsp, 0x30           ; '0'
|           0x5618704127ac      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x5618704127b5      488945f8       mov qword [local_8h], rax
|           0x5618704127b9      31c0           xor eax, eax
|           0x5618704127bb      488d3dd20000.  lea rdi, qword str.Name_: ; 0x561870412894 ; "Name?: "
|           0x5618704127c2      e859feffff     call sym.imp.puts       ; int puts(const char *s)
|           0x5618704127c7      488d45d0       lea rax, qword [local_30h]
|           0x5618704127cb      4889c7         mov rdi, rax
|           0x5618704127ce      b800000000     mov eax, 0
|           0x5618704127d3      e888feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x5618704127d8      488d45d0       lea rax, qword [local_30h]
|           0x5618704127dc      4889c6         mov rsi, rax
|           0x5618704127df      488d3db60000.  lea rdi, qword str.Ho___s ; 0x56187041289c ; "Ho, %s\n"
|           0x5618704127e6      b800000000     mov eax, 0
|           0x5618704127eb      e850feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5618704127f0      90             nop
|           0x5618704127f1      488b55f8       mov rdx, qword [local_8h]
|           0x5618704127f5      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x5618704127fe      7405           je 0x561870412805
|       |   0x561870412800      e82bfeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x561870412805      c9             leave
\           0x561870412806      c3             ret
[0x5618704127a4]> 

La dirección de la cadena «Name…» se pasa a puts mediante RDI. El listado también muestra una puesta a cero de EAX; puts no es una función variádica, de modo que esa instrucción no debe interpretarse como un requisito de argumentos vectoriales de la llamada. A continuación aparece gets:

ENSAMBLADOR / REFERENCIA
|           0x5618704127c7      488d45d0       lea rax, qword [local_30h]
|           0x5618704127cb      4889c7         mov rdi, rax
|           0x5618704127ce      b800000000     mov eax, 0
|           0x5618704127d3      e888feffff     call sym.imp.gets       ; char*gets(char *s)

De nuevo, local_30h se usa como puntero a la cadena que se va a leer y se pasa a la función gets mediante el registro rdi.

Si depuramos la función, esta vez deberíamos poder ver cómo se almacena la cadena completa, incluidos los espacios en blanco.

ENSAMBLADOR / REFERENCIA
|           0x5618704127d3      e888feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x5618704127d8      488d45d0       lea rax, qword [local_30h]
|           0x5618704127dc      4889c6         mov rsi, rax
|           0x5618704127df      488d3db60000.  lea rdi, qword str.Ho___s ; 0x56187041289c ; "Ho, %s\n"
|           0x5618704127e6      b800000000     mov eax, 0
|           0x5618704127eb      e850feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5618704127f0      90             nop
|           0x5618704127f1      488b55f8       mov rdx, qword [local_8h]
|           0x5618704127f5      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x5618704127fe      7405           je 0x561870412805
|       |   0x561870412800      e82bfeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x561870412805      c9             leave
\           0x561870412806      c3             ret
[0x5618704127a4]> db 0x5618704127df
[0x5618704127a4]> dc
Name?: 
SAMPLE TEXT
hit breakpoint at: 5618704127df
[0x5618704127a4]> dr
rax = 0x7ffe03c95df0
rbx = 0x00000000
rcx = 0x7f9f10693a00
rdx = 0x7f9f106958d0
r8 = 0x561870acd67c
r9 = 0x7f9f108a34c0
r10 = 0x561870acd010
r11 = 0x00000246
r12 = 0x561870412680
r13 = 0x7ffe03c95f10
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x7ffe03c95df0
rdi = 0x7ffe03c95df1
rsp = 0x7ffe03c95df0
rbp = 0x7ffe03c95e20
rip = 0x5618704127df
rflags = 0x00000246
orax = 0xffffffffffffffff
[0x5618704127a4]> pxw @ 0x7ffe03c95df0
0x7ffe03c95df0  0x504d4153 0x5420454c 0x00545845 0x00005618  SAMPLE TEXT..V..¡

La lectura observada conserva ahora la cadena completa, incluidos los espacios. gets no comprueba cuánto espacio queda en el destino; una entrada demasiado larga puede sobrescribir memoria ajena al array. Ese comportamiento explica la retirada de la función. La presencia de datos en memoria, por sí sola, no significa que esos datos se ejecuten como código.

Longitud de una cadena: strlen

Podemos interactuar con los arrays de caracteres de distintas formas. Una función que probablemente encontraremos en muchos programas y retos CTF es strlen, que devuelve la longitud de una cadena.

C
# include <stdio.h>

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

}

func(){

    char text[40];
    puts("Name?: ");
    gets(text);
    printf("Hi, %s\n", text);
    printf("Length: %d chars", strlen(text));
}

strlen recorre la cadena y cuenta cuántas posiciones tiene; avanza carácter a carácter hasta encontrar el terminador nulo (\0). Veamos qué significa eso:

ENSAMBLADOR / REFERENCIA
[0x55df8ad0a7e4]> pdf
/ (fcn) sym.func 131
|   sym.func ();
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x55df8ad0a7d3 (sym.main)
|           0x55df8ad0a7e4      55             push rbp
|           0x55df8ad0a7e5      4889e5         mov rbp, rsp
|           0x55df8ad0a7e8      4883ec30       sub rsp, 0x30           ; '0'
|           0x55df8ad0a7ec      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x55df8ad0a7f5      488945f8       mov qword [local_8h], rax
|           0x55df8ad0a7f9      31c0           xor eax, eax
|           0x55df8ad0a7fb      488d3df20000.  lea rdi, qword str.Name_: ; 0x55df8ad0a8f4 ; "Name?: "
|           0x55df8ad0a802      e849feffff     call sym.imp.puts       ; int puts(const char *s)
|           0x55df8ad0a807      488d45d0       lea rax, qword [local_30h]
|           0x55df8ad0a80b      4889c7         mov rdi, rax
|           0x55df8ad0a80e      b800000000     mov eax, 0
|           0x55df8ad0a813      e888feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x55df8ad0a818      488d45d0       lea rax, qword [local_30h]
|           0x55df8ad0a81c      4889c6         mov rsi, rax
|           0x55df8ad0a81f      488d3dd60000.  lea rdi, qword str.Hi___s ; 0x55df8ad0a8fc ; "Hi, %s\n"
|           0x55df8ad0a826      b800000000     mov eax, 0
|           0x55df8ad0a82b      e850feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55df8ad0a830      488d45d0       lea rax, qword [local_30h]
|           0x55df8ad0a834      4889c7         mov rdi, rax
|           0x55df8ad0a837      e824feffff     call sym.imp.strlen     ; size_t strlen(const char *s)
|           0x55df8ad0a83c      4889c6         mov rsi, rax
|           0x55df8ad0a83f      488d3dbe0000.  lea rdi, qword str.Length:__d_chars ; 0x55df8ad0a904 ; "Length: %d chars"
|           0x55df8ad0a846      b800000000     mov eax, 0
|           0x55df8ad0a84b      e830feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55df8ad0a850      90             nop
|           0x55df8ad0a851      488b55f8       mov rdx, qword [local_8h]
|           0x55df8ad0a855      644833142528.  xor rdx, qword fs:[0x28]
|       ,=< 0x55df8ad0a85e      7405           je 0x55df8ad0a865
|       |   0x55df8ad0a860      e80bfeffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x55df8ad0a865      c9             leave
\           0x55df8ad0a866      c3             ret
[0x55df8ad0a7e4]> 

Centrémonos en la función strlen.

ENSAMBLADOR / REFERENCIA
|           0x55df8ad0a82b      e850feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x55df8ad0a830      488d45d0       lea rax, qword [local_30h]
|           0x55df8ad0a834      4889c7         mov rdi, rax
|           0x55df8ad0a837      e824feffff     call sym.imp.strlen     ; size_t strlen(const char *s)
|           0x55df8ad0a83c      4889c6         mov rsi, rax

Como de costumbre, la cadena se encuentra en local_30h. Esa referencia se carga en rdi como parámetro y luego se llama a strlen. Coloquemos algunos breakpoints y analicemos el programa.

CÓDIGO / SALIDA
[0x55df8ad0a7e4]> dc
Name?: 
SAMPLE TEXT
Hi, SAMPLE TEXT
hit breakpoint at: 55df8ad0a834

[0x55df8ad0a834]> dr
rax = 0x7ffcfc542170
[...]
[0x55df8ad0a834]> pxw @ 0x7ffcfc542170
0x7ffcfc542170  0x504d4153 0x5420454c 0x00545845 0x000055df  SAMPLE TEXT..U..

Podemos ver cómo «SAMPLE TEXT» se ha almacenado como cadena en local_30h y, si prestamos un poco más de atención al volcado de memoria, vemos que la cadena termina con 0x00, es decir, un terminador nulo, por lo que strlen seguirá leyendo hasta alcanzarlo.

Si continuamos y examinamos strlen, veremos que la función devuelve su valor (la longitud de la cadena) mediante el registro RAX.

CÓDIGO / SALIDA
[0x55df8ad0a834]> dc
hit breakpoint at: 55df8ad0a83c
[0x55df8ad0a834]> dr
rax = 0x0000000b

Como podemos ver, rax contiene 0xb = 11 en decimal, y «SAMPLE TEXT» tiene una longitud de 11 (espacio incluido).

La biblioteca string

La biblioteca «string.h» contiene diversas funciones muy útiles para la manipulación de cadenas. Estas funciones nos permiten operar con cadenas de múltiples formas; aquí veremos cómo resulta útil para copiar cadenas.

Consideremos el siguiente programa:

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


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

}

func(){

    char text1[40], text2[40], text3[10];

    printf("Enter a string NOW: ");
    gets(text1);

    strcpy(text2, text1);
    printf("Copied string =  %s\n", text2);
    strncpy(text3, text1, 4);
    printf("4 first chars = %s\n", text3);

}

Vemos que strings.h se incluye al principio; luego se declaran tres arrays de char y se usa strcpy para copiar el contenido de uno en otro.

Podemos detectar fácilmente el uso de la biblioteca de cadenas con afl.

CÓDIGO / SALIDA
:~/chapter5$ radare2 ./string
[0x000006d0]> aaa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze len bytes of instructions for references (aar)
[x] Analyze function calls (aac)
[x] Use -AA or aaaa to perform additional experimental analysis.
[x] Constructing a function name for fcn.* and sym.func.* functions (aan)
[0x000006d0]> afl
0x00000000    2 25           sym.imp.__libc_start_main
0x00000630    3 23           sym._init
0x00000660    1 6            sym.imp.strncpy
0x00000670    1 6            sym.imp.strcpy
0x00000680    1 6            sym.imp.__stack_chk_fail
0x00000690    1 6            sym.imp.printf
0x000006a0    1 6            sym.imp.getchar
0x000006b0    1 6            sym.imp.gets
0x000006c0    1 6            sub.__cxa_finalize_248_6c0
0x000006d0    1 43           entry0
0x00000700    4 50   -> 40   sym.deregister_tm_clones
0x00000740    4 66   -> 57   sym.register_tm_clones
0x00000790    4 49           sym.__do_global_dtors_aux
0x000007d0    1 10           entry1.init
0x000007da    1 26           sym.main
0x000007f4    3 171          sym.func
0x000008a0    4 101          sym.__libc_csu_init
0x00000910    1 2            sym.__libc_csu_fini
0x00000914    1 9            sym._fini
[0x000006d0]> 

Y con ii/il.

CÓDIGO / SALIDA
[0x7f47eeef5090]> il
[Linked libraries]
libc.so.6

1 library

[0x7f47eeef5090]> ii
[Imports]
   1 0x55896339b660  GLOBAL    FUNC strncpy
   2 0x55896339b000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   3 0x55896339b670  GLOBAL    FUNC strcpy
   4 0x55896339b680  GLOBAL    FUNC __stack_chk_fail
   5 0x55896339b690  GLOBAL    FUNC printf
   6 0x55896339b000  GLOBAL    FUNC __libc_start_main
   7 0x55896339b6a0  GLOBAL    FUNC getchar
   8 0x55896339b000    WEAK  NOTYPE __gmon_start__
   9 0x55896339b6b0  GLOBAL    FUNC gets
  10 0x55896339b000    WEAK  NOTYPE _ITM_registerTMCloneTable
  11 0x55896339b000    WEAK    FUNC __cxa_finalize
   2 0x55896339b000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   6 0x55896339b000  GLOBAL    FUNC __libc_start_main
   8 0x55896339b000    WEAK  NOTYPE __gmon_start__
  10 0x55896339b000    WEAK  NOTYPE _ITM_registerTMCloneTable
  11 0x55896339b000    WEAK    FUNC __cxa_finalize

[0x7f47eeef5090]> 

Estas importaciones pertenecen a libc. string.h es la cabecera que declara las funciones; no tiene por qué existir una biblioteca dinámica separada con ese nombre. El desensamblado es el siguiente:

ENSAMBLADOR / REFERENCIA
[0x5585de6317f4]> pdf
/ (fcn) sym.func 171
|   sym.func ();
|           ; var int local_6ah @ rbp-0x6a
|           ; var int local_60h @ rbp-0x60
|           ; var int local_30h @ rbp-0x30
|           ; var int local_8h @ rbp-0x8
|              ; CALL XREF from 0x5585de6317e3 (sym.main)
|           0x5585de6317f4      55             push rbp
|           0x5585de6317f5      4889e5         mov rbp, rsp
|           0x5585de6317f8      4883ec70       sub rsp, 0x70           ; 'p'
|           0x5585de6317fc      64488b042528.  mov rax, qword fs:[0x28] ; [0x28:8]=-1 ; '(' ; 40
|           0x5585de631805      488945f8       mov qword [local_8h], rax
|           0x5585de631809      31c0           xor eax, eax
|           0x5585de63180b      488d3d120100.  lea rdi, qword str.Enter_a_string_NOW: ; 0x5585de631924 ; "Enter a string NOW: "
|           0x5585de631812      b800000000     mov eax, 0
|           0x5585de631817      e874feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5585de63181c      488d45a0       lea rax, qword [local_60h]
|           0x5585de631820      4889c7         mov rdi, rax
|           0x5585de631823      b800000000     mov eax, 0
|           0x5585de631828      e883feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x5585de63182d      488d55a0       lea rdx, qword [local_60h]
|           0x5585de631831      488d45d0       lea rax, qword [local_30h]
|           0x5585de631835      4889d6         mov rsi, rdx
|           0x5585de631838      4889c7         mov rdi, rax
|           0x5585de63183b      e830feffff     call sym.imp.strcpy     ; char *strcpy(char *dest, const char *src)
|           0x5585de631840      488d45d0       lea rax, qword [local_30h]
|           0x5585de631844      4889c6         mov rsi, rax
|           0x5585de631847      488d3deb0000.  lea rdi, qword str.Copied_string_____s ; 0x5585de631939 ; "Copied string =  %s\n"
|           0x5585de63184e      b800000000     mov eax, 0
|           0x5585de631853      e838feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5585de631858      488d4da0       lea rcx, qword [local_60h]
|           0x5585de63185c      488d4596       lea rax, qword [local_6ah]
|           0x5585de631860      ba04000000     mov edx, 4
|           0x5585de631865      4889ce         mov rsi, rcx
|           0x5585de631868      4889c7         mov rdi, rax
|           0x5585de63186b      e8f0fdffff     call sym.imp.strncpy    ; char *strncpy(char *dest, const char *src, size_t  n)
|           0x5585de631870      488d4596       lea rax, qword [local_6ah]
|           0x5585de631874      4889c6         mov rsi, rax
|           0x5585de631877      488d3dd00000.  lea rdi, qword str.4_first_chars____s ; 0x5585de63194e ; "4 first chars = %s\n"
|           0x5585de63187e      b800000000     mov eax, 0
|           0x5585de631883      e808feffff     call sym.imp.printf     ; int printf(const char *format)
|           0x5585de631888      90             nop
|           0x5585de631889      488b4df8       mov rcx, qword [local_8h]
|           0x5585de63188d      6448330c2528.  xor rcx, qword fs:[0x28]
|       ,=< 0x5585de631896      7405           je 0x5585de63189d
|       |   0x5585de631898      e8e3fdffff     call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
|       `-> 0x5585de63189d      c9             leave
\           0x5585de63189e      c3             ret
[0x5585de6317f4]> 

El desensamblado es más extenso esta vez; no hay que dejarse intimidar, casi todo ya nos resulta conocido. Empezamos viendo tres variables detectadas por r2.

A continuación se lee una cadena con gets y se llama a strcpy. Veámoslo:

ENSAMBLADOR / REFERENCIA
|           0x5585de63181c      488d45a0       lea rax, qword [local_60h]
|           0x5585de631820      4889c7         mov rdi, rax
|           0x5585de631823      b800000000     mov eax, 0
|           0x5585de631828      e883feffff     call sym.imp.gets       ; char*gets(char *s)
|           0x5585de63182d      488d55a0       lea rdx, qword [local_60h]
|           0x5585de631831      488d45d0       lea rax, qword [local_30h]
|           0x5585de631835      4889d6         mov rsi, rdx
|           0x5585de631838      4889c7         mov rdi, rax
|           0x5585de63183b      e830feffff     call sym.imp.strcpy     ; char *strcpy(char *dest, const char *src)

Como podemos ver, la cadena (entrada del usuario) se almacenará en local_60h; después, local_60h y local_30h se pasarán ambas a strcpy. En ese punto ya sabemos que local_60h contiene la entrada del usuario, pero ¿y local_30h? Ese será el destino, de modo que la entrada del usuario se copiará allí. Depurémoslo.

ENSAMBLADOR / REFERENCIA
[0x5585de6317f4]> db 0x5585de631840
[0x5585de6317f4]> dc
Enter a string NOW: SAMPLE TEXT
hit breakpoint at: 5585de631840
[0x5585de631840]> pdf
[...]
|           0x5585de631838      4889c7         mov rdi, rax
|           0x5585de63183b      e830feffff     call sym.imp.strcpy     ; char *strcpy(char *dest, const char *src)
|           ;-- rip:
|           0x5585de631840 b    488d45d0       lea rax, qword [local_30h]
|           0x5585de631844      4889c6         mov rsi, rax
|           0x5585de631847      488d3deb0000.  lea rdi, qword str.Copied_string_____s ; 0x5585de631939 ; "Copied string =  %s\n"

Y si inspeccionamos esas variables:

CÓDIGO / SALIDA
[0x5585de631840]> afvd
var local_8h = 0x7fffd173cd38  0x71dec5cbb8695200   .Ri....q
var local_60h = 0x7fffd173cce0  0x5420454c504d4153   SAMPLE T @rsi ascii
var local_30h = 0x7fffd173cd10  0x5420454c504d4153   SAMPLE T @rdi ascii
var local_6ah = 0x7fffd173ccd6  0x0000000000000000   ........ r15
[0x5585de631840]> 

[0x5585de631840]> pxw @ 0x7fffd173cce0
0x7fffd173cce0  0x504d4153 0x5420454c 0x00545845 0x00000000  SAMPLE TEXT.....
0x7fffd173ccf0  0x00000009 0x00000000 0x39a71660 0x00007fa0  ........`..9....
0x7fffd173cd00  0xd173cd68 0x00007fff 0x00f0b5ff 0x00000000  h.s.............
0x7fffd173cd10  0x504d4153 0x5420454c 0x00545845 0x00005585  SAMPLE TEXT..U..

La cadena ha sido copiada. Nótese también que ambas cadenas terminan en \0, por lo que strcpy seguirá copiando hasta encontrar un \0.

Más adelante en el código se llama a strNcpy. Es una función distinta; la «n» hace referencia a lo siguiente: con strncpy el contenido de una cadena se copia en otra, pero solo se copian los primeros n bytes. Inspeccionémoslo:

ENSAMBLADOR / REFERENCIA
|           0x5585de631858      488d4da0       lea rcx, qword [local_60h]
|           0x5585de63185c      488d4596       lea rax, qword [local_6ah]
|           0x5585de631860      ba04000000     mov edx, 4
|           0x5585de631865      4889ce         mov rsi, rcx
|           0x5585de631868      4889c7         mov rdi, rax
|           0x5585de63186b      e8f0fdffff     call sym.imp.strncpy    ; char *strncpy(char *dest, const char *src, size_t  n)
|           0x5585de631870      488d4596       lea rax, qword [local_6ah]

Es muy sencillo: de nuevo se pasan las direcciones de memoria de origen y destino, pero también se pasa 4, la cantidad de bytes que queremos copiar.

CÓDIGO / SALIDA
[0x5585de631840]> db 0x5585de631870
[0x5585de631840]> dc
Copied string =  SAMPLE TEXT
hit breakpoint at: 5585de631870
[0x5585de631840]> afvd
var local_8h = 0x7fffd173cd38  0x71dec5cbb8695200   .Ri....q
var local_60h = 0x7fffd173cce0  0x5420454c504d4153   SAMPLE T @rsi ascii
var local_30h = 0x7fffd173cd10  0x5420454c504d4153   SAMPLE T ascii
var local_6ah = 0x7fffd173ccd6  0x00000000504d4153   SAMP.... @rdi ascii

strncpy copia hasta el límite indicado. Si encuentra antes el terminador de la fuente, rellena los bytes restantes con ceros. Si la longitud de la fuente alcanza o supera el límite, la secuencia resultante puede carecer de terminador. En este ejemplo, copiar cuatro bytes de «SAMPLE TEXT» deja cuatro caracteres; para tratarlos como una cadena hay que asegurar también el byte nulo. Referencia de strncpy.

Si queremos truncar un array podemos insertar manualmente un terminador nulo de esta forma:

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

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

}

func(){

    char text1[40], text2[40], text3[10];

    printf("ENTER A STRING: ");
    gets(text1);

    strcpy(text2, text1);
    printf("Copied string = %s\n", text2);
    strncpy(text3, text1, 4);
    text3[4] = '\0';
    printf("4 FIRST LETTERS %s\n", text3);
}

Como podemos ver, se añade manualmente un cero al final de la cadena.

ENSAMBLADOR / REFERENCIA
       0x561d7710e86b      e8f0fdffff     call sym.imp.strncpy    ; char *strncpy(char *dest, const char *src, size_t  n)
|           0x561d7710e870      c6459a00       mov byte [local_66h], 0
|           0x561d7710e874      488d4596       lea rax, qword [local_6ah]
|           0x561d7710e878      4889c6         mov rsi, rax
|           0x561d7710e87b      488d3dd70000.  lea rdi, qword str.4_FIRST_LETTERS__s ; 0x561d7710e959 ; "4 FIRST LETTERS %s\n"
|           0x561d7710e882      b800000000     mov eax, 0

En la siguiente lección avanzaremos hacia estructuras de datos más complejas.

Sesión en vídeo / Companion0:36

Seguir la copia de una cadena.

La consulta parte de una entrada concreta y del tamaño declarado del destino. Hermes localiza la copia y la escritura del cero que termina el texto.

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

Analiza únicamente el caso de entrada LOBERA. text3 está declarado como char[10]. Identifica la llamada que copia sus primeros cuatro caracteres y la instrucción que escribe text3[4]. Describe los cinco primeros bytes resultantes. Responde en un máximo de 70 palabras, sin estudiar otras entradas.

Qué muestra la sesión

El destino text3 tiene diez posiciones. La llamada a strncpy utiliza cuatro: son el límite de esta operación de copia.

Con la entrada LOBERA se copian L, O, B y E. La instrucción posterior escribe un cero en text3[4], de modo que printf recibe una cadena terminada.

Si el origen es AB, la llamada escribe A, B, cero y cero: completa las cuatro posiciones con relleno. Esa diferencia permite interpretar correctamente las escrituras que siguen a la llamada.

Comprobación desde la consola

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

CÓDIGO / SALIDA
pd 12 @ 0x401258

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

Distinguir la capacidad de un buffer, la longitud de una cadena y su terminador nulo. 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: strncpy-terminated.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-ii --example strncpy-terminated
./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.