Distinguir la capacidad de un buffer, la longitud de una cadena y su terminador nulo.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 4-ii --example strncpy-terminatedEstoy siguiendo «Entrada de texto y terminación» del curso de Lobera. Distinguir la capacidad de un buffer, la longitud de una cadena y su terminador nulo. Dame primero una pista. Después contrasta la explicación con las instrucciones y guarda las evidencias que hayamos comprobado.
El agente y la consola comparten el estado de radare2. Preparación y funcionamiento.
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.
# 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:
[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í.
| 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:
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:
# 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:
[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:
| 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.
| 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.
# 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:
[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.
| 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.
[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.
[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:
# 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.
:~/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.
[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:
[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:
| 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.
[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:
[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:
| 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.
[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:
# 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.
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.
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.
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:
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 ↗
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.
./coursectl start 4-ii --example strncpy-terminated
./coursectl consoleCierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.
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.