Localizar buffers y llamadas de apertura, lectura, escritura y desplazamiento.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 8 --example read-linesEstoy siguiendo «Lectura y escritura de archivos» del curso de Lobera. Localizar buffers y llamadas de apertura, lectura, escritura y desplazamiento. 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.
La lectura y la escritura de archivos permiten seguir cómo un programa obtiene, transforma y conserva datos. En estos ejemplos observaremos las llamadas de la biblioteca C, sus argumentos y los buffers que utilizan.
Escribir en un fichero
Empecemos con este programa:
#include <stdio.h>
#include <string.h>
main(){
func();
getchar();
}
func()
{
FILE* ftest;
ftest = fopen("test.txt", "wt");
fputs("This is a line\n", ftest);
fputs("Another line", ftest);
fputs(" that follows the second line\n", ftest);
fclose(ftest);
}
Para entrar en calor, inspeccionemos la información del programa:
[0x7f9d4a962090]> iI
arch x86
baddr 0x55ea4645b000
binsz 6605
bintype elf
bits 64
canary false
class ELF64
compiler GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
crypto false
endian little
havecode true
intrp /lib64/ld-linux-x86-64.so.2
laddr 0x0
lang c
linenum true
lsyms true
machine AMD x86-64 architecture
maxopsz 16
minopsz 1
nx true
os linux
pcalign 0
pic true
relocs true
relro full
rpath NONE
sanitiz false
static false
stripped false
subsys linux
va true
[0x7f9d4a962090]>
Luego las cadenas:
[0x7f9d4a962090]> iz
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x0000084b 0x55ea4645b84b 8 9 .rodata ascii test.txt
1 0x00000854 0x55ea4645b854 15 16 .rodata ascii This is a line\n
2 0x00000864 0x55ea4645b864 12 13 .rodata ascii Another line
3 0x00000878 0x55ea4645b878 30 31 .rodata ascii that follows the second line\n
[0x7f9d4a962090]>
Y las importaciones:
[0x7f9d4a962090]> iS
[Sections]
nth paddr size vaddr vsize perm name
―――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000000 0x0 0x00000000 0x0 ----
1 0x00000238 0x1c 0x55ea4645b238 0x1c -r-- .interp
2 0x00000254 0x20 0x55ea4645b254 0x20 -r-- .note.ABI_tag
3 0x00000274 0x24 0x55ea4645b274 0x24 -r-- .note.gnu.build_id
4 0x00000298 0x1c 0x55ea4645b298 0x1c -r-- .gnu.hash
5 0x000002b8 0xf0 0x55ea4645b2b8 0xf0 -r-- .dynsym
6 0x000003a8 0x99 0x55ea4645b3a8 0x99 -r-- .dynstr
7 0x00000442 0x14 0x55ea4645b442 0x14 -r-- .gnu.version
8 0x00000458 0x20 0x55ea4645b458 0x20 -r-- .gnu.version_r
9 0x00000478 0xc0 0x55ea4645b478 0xc0 -r-- .rela.dyn
10 0x00000538 0x60 0x55ea4645b538 0x60 -r-- .rela.plt
11 0x00000598 0x17 0x55ea4645b598 0x17 -r-x .init
12 0x000005b0 0x50 0x55ea4645b5b0 0x50 -r-x .plt
13 0x00000600 0x8 0x55ea4645b600 0x8 -r-x .plt.got
14 0x00000610 0x222 0x55ea4645b610 0x222 -r-x .text
15 0x00000834 0x9 0x55ea4645b834 0x9 -r-x .fini
16 0x00000840 0x57 0x55ea4645b840 0x57 -r-- .rodata
17 0x00000898 0x44 0x55ea4645b898 0x44 -r-- .eh_frame_hdr
18 0x000008e0 0x128 0x55ea4645b8e0 0x128 -r-- .eh_frame
19 0x00000da0 0x8 0x55ea4665bda0 0x8 -rw- .init_array
20 0x00000da8 0x8 0x55ea4665bda8 0x8 -rw- .fini_array
21 0x00000db0 0x1f0 0x55ea4665bdb0 0x1f0 -rw- .dynamic
22 0x00000fa0 0x60 0x55ea4665bfa0 0x60 -rw- .got
23 0x00001000 0x10 0x55ea4665c000 0x10 -rw- .data
24 0x00001010 0x0 0x55ea4665c010 0x8 -rw- .bss
25 0x00001010 0x29 0x00000000 0x29 ---- .comment
26 0x00001040 0x648 0x00000000 0x648 ---- .symtab
27 0x00001688 0x247 0x00000000 0x247 ---- .strtab
28 0x000018cf 0xfe 0x00000000 0xfe ---- .shstrtab
[0x7f9d4a962090]>
Bien, ahora podemos desensamblar la función principal:
[0x55ea4645b734]> pdf
; CALL XREF from main @ 0x55ea4645b723
┌ 133: sym.func ();
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55ea4645b734 55 push rbp
│ 0x55ea4645b735 4889e5 mov rbp, rsp
│ 0x55ea4645b738 4883ec10 sub rsp, 0x10
│ 0x55ea4645b73c 488d35050100. lea rsi, [0x55ea4645b848] ; "wt"
│ 0x55ea4645b743 488d3d010100. lea rdi, str.test.txt ; 0x55ea4645b84b ; "test.txt"
│ 0x55ea4645b74a e891feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ 0x55ea4645b74f 488945f8 mov qword [var_8h], rax
│ 0x55ea4645b753 488b45f8 mov rax, qword [var_8h]
│ 0x55ea4645b757 4889c1 mov rcx, rax
│ 0x55ea4645b75a ba0f000000 mov edx, 0xf ; 15
│ 0x55ea4645b75f be01000000 mov esi, 1
│ 0x55ea4645b764 488d3de90000. lea rdi, str.This_is_a_line ; 0x55ea4645b854 ; "This is a line\n"
│ 0x55ea4645b76b e880feffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x55ea4645b770 488b45f8 mov rax, qword [var_8h]
│ 0x55ea4645b774 4889c1 mov rcx, rax
│ 0x55ea4645b777 ba0c000000 mov edx, 0xc ; 12
│ 0x55ea4645b77c be01000000 mov esi, 1
│ 0x55ea4645b781 488d3ddc0000. lea rdi, str.Another_line ; 0x55ea4645b864 ; "Another line"
│ 0x55ea4645b788 e863feffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x55ea4645b78d 488b45f8 mov rax, qword [var_8h]
│ 0x55ea4645b791 4889c1 mov rcx, rax
│ 0x55ea4645b794 ba1e000000 mov edx, 0x1e ; 30
│ 0x55ea4645b799 be01000000 mov esi, 1
│ 0x55ea4645b79e 488d3dd30000. lea rdi, str.that_follows_the_second_line ; 0x55ea4645b878 ; " that follows the second line\n"
│ 0x55ea4645b7a5 e846feffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x55ea4645b7aa 488b45f8 mov rax, qword [var_8h]
│ 0x55ea4645b7ae 4889c7 mov rdi, rax
│ 0x55ea4645b7b1 e80afeffff call sym.imp.fclose ; int fclose(FILE *stream)
│ 0x55ea4645b7b6 90 nop
│ 0x55ea4645b7b7 c9 leave
└ 0x55ea4645b7b8 c3 ret
[0x55ea4645b734]>
El programa llama a fopen con la ruta y el modo de escritura. El resultado es un FILE * que se devuelve en rax y se guarda en var_8h. Las llamadas posteriores a fwrite reciben el buffer, el tamaño de cada elemento, el número de elementos y ese stream. fclose finaliza su uso.
FILE representa el estado de un stream gestionado por la biblioteca C. Su organización interna depende de la implementación: puede incluir buffers, indicadores y una referencia al descriptor utilizado por el sistema. Para seguir las llamadas basta con reconocer el FILE * que comparten. En POSIX, el descriptor subyacente se consulta con fileno. Véase la referencia de fopen.
El FILE * permite relacionar apertura, lectura, escritura y cierre del mismo stream. El buffer que contiene los bytes y la estructura que mantiene su estado deben identificarse por separado.
Como sabemos, funciones como fopen hacen uso del heap; lo inspeccionaremos con dmhg:
Podemos colocar un breakpoint después de la llamada e inspeccionar el heap, que tendrá este aspecto:
│ 0x55fe49a82743 488d3d010100. lea rdi, str.test.txt ; 0x55fe49a8284b ; "test.txt"
│ ;-- rip:
│ 0x55fe49a8274a b e891feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ 0x55fe49a8274f b 488945f8 mov qword [var_8h], rax
│ 0x55fe49a82753 488b45f8 mov rax, qword [var_8h]
[0x55fe49a8274a]> dmhg
No Heap section
Pero justo después de ejecutar fopen:
[0x55fe49a8274f]> dr
rax = 0x55fe4ada2260
rbx = 0x00000000
rcx = 0x00000063
rdx = 0x55fe49a8284a
r8 = 0x0000002c
r9 = 0x00000000
[...]
[0x55fe49a8274f]> dmhg
Heap Layout
┌────────────────────────────────────┐
│ Malloc chunk @ 0x55fe4ada2250 │
│ size: 0x230 status: allocated │
└────────────────────────────────────┘
v
│
└──┐
│
┌───────────────────────────────┐
│ Top chunk @ 0x55fe4ada2480 │
└───────────────────────────────┘
[0x55fe49a8274f]>
Si seguimos la ejecución hasta el siguiente punto de interés, tras la primera llamada a fwrite, veremos cómo se añade más contenido al heap y esa estructura se actualiza:
[0x55fe49a82770]> dmhg
Heap Layout
┌────────────────────────────────────┐
│ Malloc chunk @ 0x55fe4ada2250 │
│ size: 0x230 status: allocated │
└────────────────────────────────────┘
v
│
│
┌────────────────────────────────────┐
│ Malloc chunk @ 0x55fe4ada2480 │
│ size: 0x1010 status: allocated │
└────────────────────────────────────┘
v
│
└──┐
│
┌───────────────────────────────┐
│ Top chunk @ 0x55fe4ada3490 │
└───────────────────────────────┘
[0x55fe49a82770]> pxw @ 0x55fe4ada2480
0x55fe4ada2480 0x33b4ad60 0x00007f00 0x00001011 0x00000000 `..3............
0x55fe4ada2490 0x73696854 0x20736920 0x696c2061 0x000a656e This is a line..
0x55fe4ada24a0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada24b0 0x00000000 0x00000000 0x00000000 0x00000000 ................
Parece que hemos identificado un posible buffer para nuestro fichero. Si seguimos la ejecución a través de todos los fwrite sucesivos, deberíamos ver el contenido escribiéndose allí. El espacio en el heap al final de la ejecución tendrá este aspecto:
[0x55fe49a827aa]> pxw 900 @ 0x55fe4ada2250
0x55fe4ada2250 0x00000000 0x00000000 0x00000231 0x00000000 ........1.......
0x55fe4ada2260 0xfbad2c84 0x00000000 0x4ada2490 0x000055fe .,.......$.J.U..
0x55fe4ada2270 0x4ada2490 0x000055fe 0x4ada2490 0x000055fe .$.J.U...$.J.U..
0x55fe4ada2280 0x4ada2490 0x000055fe 0x4ada24c9 0x000055fe .$.J.U...$.J.U..
0x55fe4ada2290 0x4ada3490 0x000055fe 0x4ada2490 0x000055fe .4.J.U...$.J.U..
0x55fe4ada22a0 0x4ada3490 0x000055fe 0x00000000 0x00000000 .4.J.U..........
0x55fe4ada22b0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada22c0 0x00000000 0x00000000 0x33b4f680 0x00007f00 ...........3....
0x55fe4ada22d0 0x00000003 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada22e0 0x00000000 0x00000000 0x4ada2340 0x000055fe ........@#.J.U..
0x55fe4ada22f0 0xffffffff 0xffffffff 0x00000000 0x00000000 ................
0x55fe4ada2300 0x4ada2350 0x000055fe 0x00000000 0x00000000 P#.J.U..........
0x55fe4ada2310 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2320 0xffffffff 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2330 0x00000000 0x00000000 0x33b4b2a0 0x00007f00 ...........3....
0x55fe4ada2340 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2350 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2360 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2370 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2380 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2390 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23a0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23b0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23c0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23d0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23e0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada23f0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2400 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2410 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2420 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2430 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2440 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2450 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2460 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2470 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55fe4ada2480 0x33b4ad60 0x00007f00 0x00001011 0x00000000 `..3............
0x55fe4ada2490 0x73696854 0x20736920 0x696c2061 0x410a656e This is a line.A
0x55fe4ada24a0 0x68746f6e 0x6c207265 0x20656e69 0x74616874 nother line that
0x55fe4ada24b0 0x6c6f6620 0x73776f6c 0x65687420 0x63657320 follows the sec
0x55fe4ada24c0 0x20646e6f 0x656e696c 0x0000000a 0x00000000 ond line........
Ahí está el contenido que se escribe en el fichero test.txt. ¿Ha sido útil en este caso? Depende mucho de lo que estés buscando, pero inspeccionar el heap tras la ejecución de funciones sospechosas puede llevar a hallazgos interesantes.
Leer desde un fichero
El anterior era sencillo; ahora leamos desde un fichero.
#include <stdio.h>
#include <string.h>
main(){
funcion();
getchar();
}
funcion()
{
FILE* ftest;
char name[80] = "test.txt";
char line[81];
ftest = fopen(name, "rt");
if (ftest == NULL)
{
printf("file not found!\n");
exit(1);
}
fgets(line, 80, ftest);
puts(line);
fgets(line, 80, ftest);
puts(line);
fclose(ftest);
}
Esta vez el programa inicializa algo de espacio en memoria y abre text.txt con fopen. fopen devuelve NULL si hay algún error al abrir el fichero, por lo que el programa usa cmp y jne para comprobar si el fichero se abre correctamente; si hay algún error, llama a exit() y termina.
│ 0x55d1449c31c9 48b874657374. movabs rax, 0x7478742e74736574 ; 'test.txt'
│ 0x55d1449c31d3 ba00000000 mov edx, 0
│ 0x55d1449c31d8 48898550ffff. mov qword [var_b0h], rax
│ 0x55d1449c31df 48899558ffff. mov qword [var_a8h], rdx
│ 0x55d1449c31e6 48c78560ffff. mov qword [var_a0h], 0
│ 0x55d1449c31f1 48c78568ffff. mov qword [var_98h], 0
│ 0x55d1449c31fc 48c78570ffff. mov qword [var_90h], 0
│ 0x55d1449c3207 48c78578ffff. mov qword [var_88h], 0
│ 0x55d1449c3212 48c745800000. mov qword [var_80h], 0
│ 0x55d1449c321a 48c745880000. mov qword [var_78h], 0
│ 0x55d1449c3222 48c745900000. mov qword [var_70h], 0
│ 0x55d1449c322a 48c745980000. mov qword [var_68h], 0
│ 0x55d1449c3232 488d8550ffff. lea rax, [var_b0h]
│ 0x55d1449c3239 488d35c40d00. lea rsi, [0x55d1449c4004] ; "rt"
│ 0x55d1449c3240 4889c7 mov rdi, rax
│ 0x55d1449c3243 e838feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ 0x55d1449c3248 b 48898548ffff. mov qword [var_b8h], rax
│ 0x55d1449c324f 4883bd48ffff. cmp qword [var_b8h], 0
│ ┌─< 0x55d1449c3257 7516 jne 0x55d1449c326f
La comprobación se realiza aquí: si no se encuentra el fichero, el programa lo indica y sale con el código 1 (salida con error).
│ ┌─< 0x55d1449c3257 7516 jne 0x55d1449c326f
│ │ 0x55d1449c3259 488d3da70d00. lea rdi, str.file_not_found ; 0x55d1449c4007 ; "file not found!"
│ │ 0x55d1449c3260 e8cbfdffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x55d1449c3265 bf01000000 mov edi, 1
│ │ 0x55d1449c326a e821feffff call sym.imp.exit ; void exit(int status)
│ └─> 0x55d1449c326f 488b9548ffff. mov rdx, qword [var_b8h]
Aquí tenemos el puntero al fichero:
│ 0x55d1449c3240 4889c7 mov rdi, rax
│ 0x55d1449c3243 e838feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ ;-- rip:
│ 0x55d1449c3248 b 48898548ffff. mov qword [var_b8h], rax
│ 0x55d1449c324f 4883bd48ffff. cmp qword [var_b8h], 0
En memoria:
[0x55d1449c3248]> dr
rax = 0x55d145eef260
rbx = 0x00000000
rcx = 0x00000005
rdx = 0x00000000
r8 = 0x00000000
r9 = 0x55d1449c4006
r10 = 0x00000000
r11 = 0x00000246
r12 = 0x55d1449c30b0
r13 = 0x7ffc18e1dc20
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x00000000
rdi = 0x55d1449c4005
rsp = 0x7ffc18e1da70
rbp = 0x7ffc18e1db30
rip = 0x55d1449c3248
rflags = 0x00000206
orax = 0xffffffffffffffff
[0x55d1449c3248]> pxw @ 0x55d145eef260
0x55d145eef260 0xfbad2488 0x00000000 0x00000000 0x00000000 .$..............
0x55d145eef270 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef280 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef290 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef2a0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef2b0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef2c0 0x00000000 0x00000000 0xf2630680 0x00007f28 ..........c.(...
0x55d145eef2d0 0x00000003 0x00000000 0x00000000 0x00000000 ................
0x55d145eef2e0 0x00000000 0x00000000 0x45eef340 0x000055d1 ........@..E.U..
0x55d145eef2f0 0xffffffff 0xffffffff 0x00000000 0x00000000 ................
0x55d145eef300 0x45eef350 0x000055d1 0x00000000 0x00000000 P..E.U..........
0x55d145eef310 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef320 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x55d145eef330 0x00000000 0x00000000 0xf2631560 0x00007f28 ........`.c.(...
0x55d145eef340 0x00000000 0x00000000 0x00000000 0x00000000 ................
fgets(line, 80, ftest) recibe el buffer de destino, su capacidad y el stream. Lee como máximo 79 bytes, conserva el salto de línea si lo encuentra y añade el terminador nulo tras los bytes leídos. Puede terminar antes por un salto de línea, fin de archivo o error. Comparemos el buffer antes y después de la llamada. La especificación de fgets detalla su retorno y sus límites.
[0x55d1449c3282]> dr
rax = 0x7ffc18e1dad0
rbx = 0x00000000
rcx = 0x00000005
rdx = 0x55d145eef260
r8 = 0x00000000
r9 = 0x55d1449c4006
r10 = 0x00000000
r11 = 0x00000246
r12 = 0x55d1449c30b0
r13 = 0x7ffc18e1dc20
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x00000050
rdi = 0x7ffc18e1dad0
rsp = 0x7ffc18e1da70
rbp = 0x7ffc18e1db30
rip = 0x55d1449c3282
rflags = 0x00000206
orax = 0xffffffffffffffff
# BEFORE fgets() IS CALLED
[0x55d1449c3282]> pxw @ 0x7ffc18e1dad0
0x7ffc18e1dad0 0x00000000 0x00000000 0x00f0b5ff 0x00000000 ................
0x7ffc18e1dae0 0x000000c2 0x00000000 0x18e1db16 0x00007ffc ................
0x7ffc18e1daf0 0x00000001 0x00000000 0xf2505b55 0x00007f28 ........U[P.(...
0x7ffc18e1db00 0x00000000 0x00000000 0x449c3325 0x000055d1 ........%3.D.U..
0x7ffc18e1db10 0xf2661b20 0x00007f28 0x00000000 0x00000000 .f.(...........
0x7ffc18e1db20 0x449c32e0 0x000055d1 0xb67e8600 0x110eefa4 .2.D.U....~.....
0x7ffc18e1db30 0x18e1db40 0x00007ffc 0x449c31a3 0x000055d1 @........1.D.U..
0x7ffc18e1db40 0x449c32e0 0x000055d1 0xf2471b6b 0x00007f28 .2.D.U..k.G.(...
0x7ffc18e1db50 0x00000000 0x00000000 0x18e1dc28 0x00007ffc ........(.......
0x7ffc18e1db60 0x00040000 0x00000001 0x449c3195 0x000055d1 .........1.D.U..
0x7ffc18e1db70 0x00000000 0x00000000 0xdb1818b4 0xd55ddb37 ............7.].
0x7ffc18e1db80 0x449c30b0 0x000055d1 0x18e1dc20 0x00007ffc .0.D.U.. .......
0x7ffc18e1db90 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dba0 0x087818b4 0x810763cc 0x889e18b4 0x80aeb681 ..x..c..........
0x7ffc18e1dbb0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dbc0 0x00000000 0x00000000 0x18e1dc38 0x00007ffc ........8.......
[0x55d1449c3282]> dc
hit breakpoint at: 55d1449c3287
# AFTER fgets() IS CALLED
[0x55d1449c3287]> pxw @ 0x7ffc18e1dad0
0x7ffc18e1dad0 0x4c4c4548 0x4c45484f 0x45484f4c 0x0a4f4c4c HELLOHELLOHELLO.
0x7ffc18e1dae0 0x00000000 0x00000000 0x18e1db16 0x00007ffc ................
0x7ffc18e1daf0 0x00000001 0x00000000 0xf2505b55 0x00007f28 ........U[P.(...
0x7ffc18e1db00 0x00000000 0x00000000 0x449c3325 0x000055d1 ........%3.D.U..
0x7ffc18e1db10 0xf2661b20 0x00007f28 0x00000000 0x00000000 .f.(...........
0x7ffc18e1db20 0x449c32e0 0x000055d1 0xb67e8600 0x110eefa4 .2.D.U....~.....
0x7ffc18e1db30 0x18e1db40 0x00007ffc 0x449c31a3 0x000055d1 @........1.D.U..
0x7ffc18e1db40 0x449c32e0 0x000055d1 0xf2471b6b 0x00007f28 .2.D.U..k.G.(...
0x7ffc18e1db50 0x00000000 0x00000000 0x18e1dc28 0x00007ffc ........(.......
0x7ffc18e1db60 0x00040000 0x00000001 0x449c3195 0x000055d1 .........1.D.U..
0x7ffc18e1db70 0x00000000 0x00000000 0xdb1818b4 0xd55ddb37 ............7.].
0x7ffc18e1db80 0x449c30b0 0x000055d1 0x18e1dc20 0x00007ffc .0.D.U.. .......
0x7ffc18e1db90 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dba0 0x087818b4 0x810763cc 0x889e18b4 0x80aeb681 ..x..c..........
0x7ffc18e1dbb0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dbc0 0x00000000 0x00000000 0x18e1dc38 0x00007ffc ........8.......
[0x55d1449c3287]>
El segundo fgets hace lo mismo:
│ ;-- rip:
│ 0x55d1449c32a6 b e8b5fdffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
│ 0x55d1449c32ab 488d45a0 lea rax, [var_60h]
│ 0x55d1449c32af 4889c7 mov rdi, rax
│ 0x55d1449c32b2 e879fdffff call sym.imp.puts ; int puts(const char *s)
│ 0x55d1449c32b7 488b8548ffff. mov rax, qword [var_b8h]
│ 0x55d1449c32be 4889c7 mov rdi, rax
│ 0x55d1449c32c1 e87afdffff call sym.imp.fclose ; int fclose(FILE *stream)
│ 0x55d1449c32c6 90 nop
│ 0x55d1449c32c7 488b4df8 mov rcx, qword [var_8h]
│ 0x55d1449c32cb 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x55d1449c32d4 7405 je 0x55d1449c32db
│ │ 0x55d1449c32d6 e875fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55d1449c32db c9 leave
└ 0x55d1449c32dc c3 ret
[0x55d1449c32a6]> db 0x55d1449c32ab
[0x55d1449c32a6]> pxw @ 0x7ffc18e1dad0
0x7ffc18e1dad0 0x4c4c4548 0x4c45484f 0x45484f4c 0x0a4f4c4c HELLOHELLOHELLO.
0x7ffc18e1dae0 0x00000000 0x00000000 0x18e1db16 0x00007ffc ................
0x7ffc18e1daf0 0x00000001 0x00000000 0xf2505b55 0x00007f28 ........U[P.(...
0x7ffc18e1db00 0x00000000 0x00000000 0x449c3325 0x000055d1 ........%3.D.U..
0x7ffc18e1db10 0xf2661b20 0x00007f28 0x00000000 0x00000000 .f.(...........
0x7ffc18e1db20 0x449c32e0 0x000055d1 0xb67e8600 0x110eefa4 .2.D.U....~.....
0x7ffc18e1db30 0x18e1db40 0x00007ffc 0x449c31a3 0x000055d1 @........1.D.U..
0x7ffc18e1db40 0x449c32e0 0x000055d1 0xf2471b6b 0x00007f28 .2.D.U..k.G.(...
0x7ffc18e1db50 0x00000000 0x00000000 0x18e1dc28 0x00007ffc ........(.......
0x7ffc18e1db60 0x00040000 0x00000001 0x449c3195 0x000055d1 .........1.D.U..
0x7ffc18e1db70 0x00000000 0x00000000 0xdb1818b4 0xd55ddb37 ............7.].
0x7ffc18e1db80 0x449c30b0 0x000055d1 0x18e1dc20 0x00007ffc .0.D.U.. .......
0x7ffc18e1db90 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dba0 0x087818b4 0x810763cc 0x889e18b4 0x80aeb681 ..x..c..........
0x7ffc18e1dbb0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dbc0 0x00000000 0x00000000 0x18e1dc38 0x00007ffc ........8.......
[0x55d1449c32a6]> dc
hit breakpoint at: 55d1449c32ab
[0x55d1449c32ab]> pxw @ 0x7ffc18e1dad0
0x7ffc18e1dad0 0x4c524f57 0x524f5744 0x4f57444c 0x0a444c52 WORLDWORLDWORLD.
0x7ffc18e1dae0 0x00000000 0x00000000 0x18e1db16 0x00007ffc ................
0x7ffc18e1daf0 0x00000001 0x00000000 0xf2505b55 0x00007f28 ........U[P.(...
0x7ffc18e1db00 0x00000000 0x00000000 0x449c3325 0x000055d1 ........%3.D.U..
0x7ffc18e1db10 0xf2661b20 0x00007f28 0x00000000 0x00000000 .f.(...........
0x7ffc18e1db20 0x449c32e0 0x000055d1 0xb67e8600 0x110eefa4 .2.D.U....~.....
0x7ffc18e1db30 0x18e1db40 0x00007ffc 0x449c31a3 0x000055d1 @........1.D.U..
0x7ffc18e1db40 0x449c32e0 0x000055d1 0xf2471b6b 0x00007f28 .2.D.U..k.G.(...
0x7ffc18e1db50 0x00000000 0x00000000 0x18e1dc28 0x00007ffc ........(.......
0x7ffc18e1db60 0x00040000 0x00000001 0x449c3195 0x000055d1 .........1.D.U..
0x7ffc18e1db70 0x00000000 0x00000000 0xdb1818b4 0xd55ddb37 ............7.].
0x7ffc18e1db80 0x449c30b0 0x000055d1 0x18e1dc20 0x00007ffc .0.D.U.. .......
0x7ffc18e1db90 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dba0 0x087818b4 0x810763cc 0x889e18b4 0x80aeb681 ..x..c..........
0x7ffc18e1dbb0 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffc18e1dbc0 0x00000000 0x00000000 0x18e1dc38 0x00007ffc ........8.......
[0x55d1449c32ab]>
Ten en cuenta que, al usar el mismo array de caracteres (buffer) para leer, el segundo fgets sobreescribe lo que volcó el primero. Si queremos conservar ambas líneas o realizar operaciones de lectura/escritura más avanzadas que incluyan cambios de posición, podemos declarar un array más grande y usar fgets, seek y fgets. Pero ¿qué es seek? Vamos a verlo.
Seek en fichero y escritura
Habrás notado que al llamar a fgets varias veces avanzamos en el contenido del fichero. ¿Qué pasa si queremos retroceder? ¿O saltar directamente al final? fseek resulta muy útil en estos casos. Inspeccionemos el siguiente programa:
#include <stdio.h>
int main () {
FILE *fp;
fp = fopen("fseek.txt","w+");
fputs("This is a simple file, feel free to visit the course to get fresh reversing stuff", fp);
fseek( fp, 7, SEEK_SET );
fputs(" C Programming Language", fp);
fclose(fp);
return(0);
}
Como vemos, primero escribe en un fichero, luego desplaza el puntero a la posición 7 (piensa en un array de caracteres), escribe contenido allí y finalmente cierra el fichero y termina. Dentro de radare:
[0x7f376be20090]> s main
[0x560d0969d165]> pdf
; DATA XREF from entry0 @ 0x560d0969d09d
┌ 130: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_8h @ rbp-0x8
│ 0x560d0969d165 55 push rbp
│ 0x560d0969d166 4889e5 mov rbp, rsp
│ 0x560d0969d169 4883ec10 sub rsp, 0x10
│ 0x560d0969d16d 488d35940e00. lea rsi, [0x560d0969e008] ; "w+"
│ 0x560d0969d174 488d3d900e00. lea rdi, str.fseek.txt ; 0x560d0969e00b ; "fseek.txt"
│ 0x560d0969d17b e8d0feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ 0x560d0969d180 488945f8 mov qword [var_8h], rax
│ 0x560d0969d184 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d188 4889c1 mov rcx, rax
│ 0x560d0969d18b ba51000000 mov edx, 0x51 ; 'Q' ; 81
│ 0x560d0969d190 be01000000 mov esi, 1
│ 0x560d0969d195 488d3d7c0e00. lea rdi, str.This_is_a_simple_file__feel_free_to_visit_the_course_to_get_fresh_reversing_stuff ; 0x560d0969e018 ; "This is a simple file, feel free to visit the course to get fresh reversing stuff"
│ 0x560d0969d19c e8bffeffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x560d0969d1a1 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d1a5 ba00000000 mov edx, 0
│ 0x560d0969d1aa be07000000 mov esi, 7
│ 0x560d0969d1af 4889c7 mov rdi, rax
│ 0x560d0969d1b2 e889feffff call sym.imp.fseek ; int fseek(FILE *stream, long offset, int whence)
│ 0x560d0969d1b7 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d1bb 4889c1 mov rcx, rax
│ 0x560d0969d1be ba17000000 mov edx, 0x17 ; 23
│ 0x560d0969d1c3 be01000000 mov esi, 1
│ 0x560d0969d1c8 488d3d9b0e00. lea rdi, str.C_Programming_Language ; 0x560d0969e06a ; " C Programming Language"
│ 0x560d0969d1cf e88cfeffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x560d0969d1d4 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d1d8 4889c7 mov rdi, rax
│ 0x560d0969d1db e850feffff call sym.imp.fclose ; int fclose(FILE *stream)
│ 0x560d0969d1e0 b800000000 mov eax, 0
│ 0x560d0969d1e5 c9 leave
└ 0x560d0969d1e6 c3 ret
[0x560d0969d165]>
Vemos lo mismo que antes, salvo que esta vez aparece una nueva llamada (seek). fseek recibe 0x7 y la variable que contiene el puntero FILE, sin sorpresas.
│ 0x560d0969d1a1 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d1a5 ba00000000 mov edx, 0
│ 0x560d0969d1aa be07000000 mov esi, 7
│ 0x560d0969d1af 4889c7 mov rdi, rax
│ 0x560d0969d1b2 e889feffff call sym.imp.fseek ; int fseek(FILE *stream, long offset, int whence)
│ 0x560d0969d1b7 488b45f8 mov rax, qword [var_8h]
Este código omite comprobar el retorno de fseek. El siguiente acceso utiliza la posición establecida si la operación ha tenido éxito. También se observa que el compilador ha sustituido la escritura de una cadena constante por una llamada a fwrite con una longitud calculada al compilar; esa transformación explica el argumento de tamaño que no aparece explícitamente en la fuente.
│ 0x560d0969d1b7 488b45f8 mov rax, qword [var_8h]
│ 0x560d0969d1bb 4889c1 mov rcx, rax
│ 0x560d0969d1be ba17000000 mov edx, 0x17 ; 23
│ 0x560d0969d1c3 be01000000 mov esi, 1
│ 0x560d0969d1c8 488d3d9b0e00. lea rdi, str.C_Programming_Language ; 0x560d0969e06a ; " C Programming Language"
│ 0x560d0969d1cf e88cfeffff call sym.imp.fwrite ; size_t fwrite(const void *ptr, size_t size, size_t nitems, FILE *stream)
│ 0x560d0969d1d4 488b45f8 mov rax, qword [var_8h]
Ya sabemos cómo funciona esto; nada nuevo aquí.
Leer el fichero completo línea a línea
¿Qué pasa si simplemente queremos leer el fichero completo, línea a línea hasta el final? Seguimos leyendo líneas con fgets hasta llegar a EOF. Veámoslo:
#include <stdio.h>
void main(){
char buffer[500];
FILE *fp;
int lineno = 0;
if ((fp = fopen("myinputfile.txt","r")) == NULL)
{
printf("Could not open myinputfile.txt\n");
exit(1);
}
while ( !feof(fp))
{
// read in the line and make sure it was successful
if (fgets(buffer,500,fp) != NULL)
{
printf("%d: %s",lineno++,buffer);
}
}
}
Esta función debería resultarnos familiar:
[0x55651c3ee195]> pdf
; DATA XREF from entry0 @ 0x55651c3ee0cd
┌ 209: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_20ch @ rbp-0x20c
│ ; var int64_t var_208h @ rbp-0x208
│ ; var int64_t var_200h @ rbp-0x200
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55651c3ee195 55 push rbp
│ 0x55651c3ee196 4889e5 mov rbp, rsp
│ 0x55651c3ee199 4881ec100200. sub rsp, 0x210
│ 0x55651c3ee1a0 64488b042528. mov rax, qword fs:[0x28]
│ 0x55651c3ee1a9 488945f8 mov qword [var_8h], rax
│ 0x55651c3ee1ad 31c0 xor eax, eax
│ 0x55651c3ee1af c785f4fdffff. mov dword [var_20ch], 0
│ 0x55651c3ee1b9 488d35480e00. lea rsi, [0x55651c3ef008] ; "r"
│ 0x55651c3ee1c0 488d3d430e00. lea rdi, str.myinputfile.txt ; 0x55651c3ef00a ; "myinputfile.txt"
│ 0x55651c3ee1c7 e8b4feffff call sym.imp.fopen ; file*fopen(const char *filename, const char *mode)
│ 0x55651c3ee1cc 488985f8fdff. mov qword [var_208h], rax
│ 0x55651c3ee1d3 4883bdf8fdff. cmp qword [var_208h], 0
│ ┌─< 0x55651c3ee1db 755f jne 0x55651c3ee23c
│ │ 0x55651c3ee1dd 488d3d3c0e00. lea rdi, str.Could_not_open_myinputfile.txt ; 0x55651c3ef020 ; "Could not open myinputfile.txt"
│ │ 0x55651c3ee1e4 e847feffff call sym.imp.puts ; int puts(const char *s)
│ │ 0x55651c3ee1e9 bf01000000 mov edi, 1
│ │ 0x55651c3ee1ee e89dfeffff call sym.imp.exit ; void exit(int status)
│ ┌──> 0x55651c3ee1f3 488b95f8fdff. mov rdx, qword [var_208h]
│ ╎│ 0x55651c3ee1fa 488d8500feff. lea rax, [var_200h]
│ ╎│ 0x55651c3ee201 bef4010000 mov esi, 0x1f4 ; 500
│ ╎│ 0x55651c3ee206 4889c7 mov rdi, rax
│ ╎│ 0x55651c3ee209 e852feffff call sym.imp.fgets ; char *fgets(char *s, int size, FILE *stream)
│ ╎│ 0x55651c3ee20e 4885c0 test rax, rax
│ ┌───< 0x55651c3ee211 7429 je 0x55651c3ee23c
│ │╎│ 0x55651c3ee213 8b85f4fdffff mov eax, dword [var_20ch]
│ │╎│ 0x55651c3ee219 8d5001 lea edx, [rax + 1]
│ │╎│ 0x55651c3ee21c 8995f4fdffff mov dword [var_20ch], edx
│ │╎│ 0x55651c3ee222 488d9500feff. lea rdx, [var_200h]
│ │╎│ 0x55651c3ee229 89c6 mov esi, eax
│ │╎│ 0x55651c3ee22b 488d3d0d0e00. lea rdi, str.d:__s ; 0x55651c3ef03f ; "%d: %s"
│ │╎│ 0x55651c3ee232 b800000000 mov eax, 0
│ │╎│ 0x55651c3ee237 e814feffff call sym.imp.printf ; int printf(const char *format)
│ └─└─> 0x55651c3ee23c 488b85f8fdff. mov rax, qword [var_208h]
│ ╎ 0x55651c3ee243 4889c7 mov rdi, rax
│ ╎ 0x55651c3ee246 e825feffff call sym.imp.feof ; int feof(FILE *stream)
│ ╎ 0x55651c3ee24b 85c0 test eax, eax
│ └──< 0x55651c3ee24d 74a4 je 0x55651c3ee1f3
│ 0x55651c3ee24f 90 nop
│ 0x55651c3ee250 488b45f8 mov rax, qword [var_8h]
│ 0x55651c3ee254 644833042528. xor rax, qword fs:[0x28]
│ ┌─< 0x55651c3ee25d 7405 je 0x55651c3ee264
│ │ 0x55651c3ee25f e8dcfdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55651c3ee264 c9 leave
└ 0x55651c3ee265 c3 ret
[0x55651c3ee195]>
Si esto parece algo complejo, podemos usar el decompilador r2dec, como vimos en la entrada anterior:
#include <stdint.h>
int32_t main (void) {
int64_t var_20ch;
int64_t var_208h;
int64_t var_200h;
int64_t var_8h;
rax = *(fs:0x28);
var_8h = *(fs:0x28);
eax = 0;
var_20ch = 0;
rax = fopen ("myinputfile.txt", 0x55651c3ef008);
var_208h = rax;
if (var_208h != 0) {
goto label_0;
}
puts ("Could not open myinputfile.txt");
exit (1);
do {
rax = &var_200h;
rax = fgets (rax, 0x1f4, var_208h);
if (rax != 0) {
eax = var_20ch;
edx = rax + 1;
var_20ch = edx;
rdx = &var_200h;
esi = eax;
eax = 0;
printf ("%d: %s");
}
label_0:
rax = var_208h;
eax = feof (var_208h);
} while (eax == 0);
rax = var_8h;
rax ^= *(fs:0x28);
if (eax != 0) {
stack_chk_fail ();
}
return rax;
}
Lo clave ocurre al comparar eax con 0 después de llamar a feof.
Esa comparación se produce aquí:
│ ╎ 0x55651c3ee246 e825feffff call sym.imp.feof ; int feof(FILE *stream)
│ ╎ 0x55651c3ee24b 85c0 test eax, eax
│ └──< 0x55651c3ee24d 74a4 je 0x55651c3ee1f3
feof consulta el indicador de fin del stream, que se establece cuando una lectura alcanza esa condición. Un bucle de lectura debe comprobar primero el retorno de fgets y procesar el buffer solo cuando la llamada tiene éxito. Después puede distinguir fin de archivo y error mediante feof y ferror. Consulta la referencia de los indicadores del stream.
Interpretar una lectura con fgets.
El análisis sigue dos retornos: la apertura del archivo y la lectura de una línea. Los saltos muestran cuándo se alcanza la llamada que imprime el contenido.
Leer la explicación y los comandos
La consulta
En read-lines, la fuente declara char buffer[500]. Sigue la llamada a fgets y el uso de su retorno. ¿Qué condición permite imprimir la línea y cuántos caracteres puede leer? Identifica también la comprobación de fopen. Apoya las conclusiones en el desensamblado.
Qué muestra la sesión
Tras fopen, el programa compara el puntero con cero. Si la apertura falla, muestra el error y termina; si obtiene un FILE válido, llega al bucle.
El segundo argumento de fgets es 500, el tamaño del buffer declarado en la fuente. La lectura puede almacenar hasta 499 caracteres y el terminador. El desplazamiento del buffer respecto de RBP localiza el objeto dentro del marco de pila.
test rax, rax comprueba el retorno de fgets. Cuando es NULL, je evita la impresión y vuelve a la comprobación del estado del archivo. Un error de lectura también puede devolver NULL; feof comprueba específicamente el indicador de final de archivo.
Comprobación desde la consola
Desde la consola del companion se revisan estas instrucciones y datos:
pd 10 @ 0x40121e
pd 16 @ 0x401252
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
Localizar buffers y llamadas de apertura, lectura, escritura y desplazamiento. 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: read-lines.c ↓ · myinputfile.txt ↓.
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 8 --example read-lines
./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.