Relacionar libc con syscalls y seguir operaciones de archivos y emulación de expresiones.
Trabaja sobre el mismo ejemplo
Selecciona este ejemplo en el companion. El número del comando corresponde al identificador original del laboratorio.
./coursectl chat 13 --example libc-printEstoy siguiendo «Linux: llamadas al sistema y ESIL» del curso de Lobera. Relacionar libc con syscalls y seguir operaciones de archivos y emulación de expresiones. 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.
Hoy damos un paso más en reversing. Desde el primer tutorial hasta ahora hemos visto los fundamentos del análisis de ingeniería inversa, centrado en binarios Linux x86-64 escritos en C. Aunque el objetivo principal eran binarios C para Linux, las técnicas que vimos son bastante comunes y se pueden aplicar, por ejemplo, al hacer reversing de binarios Windows de 32 bits escritos en C++. Al fin y al cabo, trabajábamos con bibliotecas como stdio y strings, que son comunes en ambos entornos. Por otro lado, hemos usado radare2, pero el conocimiento adquirido se puede aplicar con otras herramientas como Ghidra, IDA, Binary Ninja, etc.
Como se ha dicho, hoy subimos de nivel. A medida que nos adentramos en temas más avanzados como programación de sistemas, gestión avanzada de memoria, técnicas de malware o explotación y shellcoding, encontramos que sistemas como Windows y Linux gestionan las cosas internamente de forma diferente. A partir de aquí veremos nuevos temas de reversing, principalmente relacionados con los internos del sistema y técnicas de malware, mostrando cómo funciona todo ello tanto en Linux como en Windows por separado.
En este artículo recorremos el concepto del kernel de Linux y las syscalls más básicas.
Espacio de kernel y espacio de usuario
El kernel es el núcleo real del sistema operativo: proporciona servicios como la gestión de procesos y memoria, el sistema de archivos y la pila TCP. Controla el acceso a archivos y memoria, y también nos proporciona los módulos y drivers que se comunican con dispositivos externos como el teclado o el ratón. El kernel se sitúa sobre el hardware y trata directamente con la RAM, el disco, el acceso a la red, etc., además de gestionar los programas, preparar su entorno y administrar los recursos que necesitan, como espacio en memoria o acceso a dispositivos de E/S.
El código puramente relacionado con el kernel opera en el llamado «espacio de kernel», una zona de memoria donde solo hay código y datos del kernel. ¿Por qué hace falta esa separación? Por muchas razones, pero pensemos en la seguridad: el código del kernel, el núcleo del sistema operativo, necesita control total sobre la máquina, mientras que un programa cualquiera que hace unos cálculos y muestra un resultado no lo necesita. Esos programas deben estar separados por razones de seguridad. El kernel se ejecuta en su propio espacio de memoria y es quien lanza los programas de usuario, por lo que tiene control total para hacerlos correr en el espacio de usuario. El kernel puede operar en ambos espacios; los programas de usuario solo en el espacio de usuario.
Al analizar un programa, las llamadas de biblioteca orientan sobre su relación con el sistema. printf gestiona salida, fopen abre un stream y malloc obtiene memoria del asignador. No todas esas llamadas necesitan entrar en el kernel en cada invocación: pueden trabajar con buffers o regiones ya disponibles. Una operación matemática como sqrt suele resolverse en espacio de usuario; bajo la ABI System V x86-64, sus argumentos y resultado de coma flotante utilizan registros XMM.
En sistemas que corren sobre el kernel de Linux, cualquier operación que involucre CPU, memoria, dispositivos de E/S, sistema de archivos, etc., se realiza mediante una llamada al sistema o syscall. Las syscalls son funciones especiales que requieren al kernel para ejecutar una acción concreta. Un ejemplo de syscall que veremos en este artículo es write(). Write escribe datos en un dispositivo: la pantalla, un archivo, un socket o lo que sea; y sí, en Linux «todo es un dispositivo». Para escribir texto en pantalla se puede usar write() directamente, pero también printf(). La diferencia es que dentro de printf() se llama a write(); printf simplemente la envuelve de forma más cómoda, permitiendo definir el formato y demás.
Llamadas al sistema y manejo de errores
Hagamos nuestra primera syscall. En este ejemplo usaremos la syscall open() para crear un archivo y luego la syscall write() para escribir contenido en él y también en pantalla.
#include <fcntl.h>
void main(){
int fd = open("foo", O_WRONLY | O_CREAT, 0644);
write(fd, "hello_world", 11);
write(1,"hello world", 11);
close(fd);
printf("\nhello world2\n");
}
Como se puede ver, primero se llama a open para abrir el archivo «foo»; devuelve un valor int que se asigna a la variable fd. open devuelve el descriptor de archivo del fichero que acaba de abrir. Hablaremos de los descriptores de archivo enseguida, pero por ahora basta saber que un descriptor de archivo es una referencia a un archivo, dispositivo externo, socket o cualquier cosa de la que podamos leer o a la que podamos escribir datos. Luego se llama a write con ese número y algo de texto como parámetro (siendo 11 el tamaño de ese texto).
Inspeccionemos la cabecera de la función: http://man7.org/linux/man-pages/man2/write.2.html
ssize_t write(int fd, const void *buf, size_t count);
ssize_t es un tipo entero con signo que estas interfaces utilizan para devolver cantidades de bytes o un indicador de error. Su definición depende de la plataforma y la ABI. En el entorno Linux x86-64 del ejemplo tiene 64 bits; consulta el tipo de retorno y el contrato de cada función al interpretar su resultado.
También vemos que open se llama con dos flags: O_WRONLY combinado con O_CREAT, de modo que el archivo se abrirá solo para escritura y se creará si no existe. ¿Recuerdas el tutorial anterior sobre operaciones a nivel de bit? Aquí ves cómo se pasan los flags en un ejemplo real. El valor 644 corresponde a los permisos Unix del archivo; no entraremos en detalles porque se supone que ya los conoces.
Después vemos que write se llama de nuevo, esta vez con 1 como fd. Hay tres descriptores de archivo estándar (POSIX) que conviene conocer, ya que están presentes en todo proceso Unix: 0 es la entrada estándar (lo que el usuario introduce al programa, normalmente por teclado), 1 es la salida estándar (pantalla) y 2 es el error estándar. Con esto, esa segunda llamada a write debería tener más sentido.
Pasemos a radare2:
[0x558c82dc071a]> pdf
; DATA XREF from entry0 @ 0x558c82dc062d
┌ 122: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_4h @ rbp-0x4
│ 0x558c82dc071a 55 push rbp
│ 0x558c82dc071b 4889e5 mov rbp, rsp
│ 0x558c82dc071e 4883ec10 sub rsp, 0x10
│ 0x558c82dc0722 baa4010000 mov edx, 0x1a4 ; 420
│ 0x558c82dc0727 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x558c82dc072c 488d3df10000. lea rdi, [0x558c82dc0824] ; "foo"
│ 0x558c82dc0733 b800000000 mov eax, 0
│ 0x558c82dc0738 e8b3feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x558c82dc073d 8945fc mov dword [var_4h], eax
│ 0x558c82dc0740 8b45fc mov eax, dword [var_4h]
│ 0x558c82dc0743 ba0b000000 mov edx, 0xb ; 11
│ 0x558c82dc0748 488d35d90000. lea rsi, [0x558c82dc0828] ; "hello_world"
│ 0x558c82dc074f 89c7 mov edi, eax
│ 0x558c82dc0751 b800000000 mov eax, 0
│ 0x558c82dc0756 e875feffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ 0x558c82dc075b ba0b000000 mov edx, 0xb ; 11
│ 0x558c82dc0760 488d35cd0000. lea rsi, str.hello_world ; 0x558c82dc0834 ; "hello world"
│ 0x558c82dc0767 bf01000000 mov edi, 1
│ 0x558c82dc076c b800000000 mov eax, 0
│ 0x558c82dc0771 e85afeffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ 0x558c82dc0776 8b45fc mov eax, dword [var_4h]
│ 0x558c82dc0779 89c7 mov edi, eax
│ 0x558c82dc077b b800000000 mov eax, 0
│ 0x558c82dc0780 e85bfeffff call sym.imp.close ; int close(int fildes)
│ 0x558c82dc0785 488d3db40000. lea rdi, str.hello_world2 ; 0x558c82dc0840 ; "\nhello world2"
│ 0x558c82dc078c e82ffeffff call sym.imp.puts ; int puts(const char *s)
│ 0x558c82dc0791 90 nop
│ 0x558c82dc0792 c9 leave
└ 0x558c82dc0793 c3 ret
[0x558c82dc071a]>
En la llamada a open vemos 420 dec = 0644 octal pasado como argumento, junto con el nombre del archivo y 0x41, que es la combinación de esos dos flags. Tras llamar a open obtenemos el descriptor de archivo:
│ 0x558c82dc0738 e8b3feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x558c82dc073d 8945fc mov dword [var_4h], eax
[0x558c82dc073d]> dr eax
0x00000003
Tres es el descriptor de archivo de nuestro fichero, lo cual tiene sentido: 0, 1 y 2 están ocupados por defecto, así que 3 es el siguiente disponible.
Ese descriptor de archivo se pasa luego a write:
│ 0x558c82dc0740 8b45fc mov eax, dword [var_4h]
│ 0x558c82dc0743 ba0b000000 mov edx, 0xb ; 11
│ 0x558c82dc0748 488d35d90000. lea rsi, [0x558c82dc0828] ; "hello_world"
│ 0x558c82dc074f 89c7 mov edi, eax
│ 0x558c82dc0751 b800000000 mov eax, 0
│ 0x558c82dc0756 e875feffff call sym.imp.write
Y en la última llamada a write simplemente se pasa un 1:
│ 0x558c82dc075b ba0b000000 mov edx, 0xb ; 11
│ 0x558c82dc0760 488d35cd0000. lea rsi, str.hello_world ; 0x558c82dc0834 ; "hello world"
│ 0x558c82dc0767 bf01000000 mov edi, 1
│ 0x558c82dc076c b800000000 mov eax, 0
│ 0x558c82dc0771 e85afeffff call sym.imp.write
Comparemos ahora write con printf(). Escribir en pantalla (stdout) con printf es tan sencillo como:
#include <stdio.h>
void main(){
printf("ssssssssyscall\n");
}
Y dentro de radare vemos un puts, no un write.
[0x558589dc963a]> pdf
; DATA XREF from entry0 @ 0x558589dc954d
┌ 19: int main (int argc, char **argv, char **envp);
│ 0x558589dc963a 55 push rbp
│ 0x558589dc963b 4889e5 mov rbp, rsp
│ 0x558589dc963e 488d3d8f0000. lea rdi, str.ssssssssyscall ; 0x558589dc96d4 ; "ssssssssyscall"
│ 0x558589dc9645 e8c6feffff call sym.imp.puts ; int puts(const char *s)
│ 0x558589dc964a 90 nop
│ 0x558589dc964b 5d pop rbp
└ 0x558589dc964c c3 ret
[0x558589dc963a]> dcs*
Sin embargo, la syscall write se usa internamente dentro de puts. Con radare2 puedes rastrear todas las syscalls usando dcs*, así:
child stopped with signal 133
--> SN 0x7f7b6205a154 syscall 1 write (0x1 0x55858a4e3260 0xf)
ssssssssyscall
child stopped with signal 133
--> SN 0x7f7b6202ee06 syscall 231 exit_group (0xf)
child exited with status 15
==> Process finished
Como se puede ver, write se llama justo antes de que nuestra cadena aparezca en pantalla. Las funciones como printf, incluidas en la biblioteca stdio.h, son abstracciones que nos facilitan la vida proporcionando funcionalidad adicional; internamente gestionan las syscalls.
También hay que tener en cuenta que al ejecutar la syscall write no se usa ningún búfer interno (a diferencia de lo que vimos con la función fwrite), por lo que no se crea espacio en el heap tras write. Esta función vuelca los datos directamente al fd y el kernel crea internamente el contenido dentro del archivo.
[0x558c82dc075b]> dmh
No Heap section
#include <fcntl.h>
#include <stdio.h>
void main(){
fd = open("foo", O_WRONLY | O_CREAT, 0644);
if(fd >= 0){
write(fd, "hello_world", 11);
close(fd);
}
else{
printf("error number %d\n", errno);
perror("foo");
exit(1);
}
}
Descriptores de archivo
Hablemos un poco de los descriptores de archivo, ya que es un concepto que veremos mucho durante nuestras sesiones de reversing y puede no estar del todo claro para todos.
En Unix y sistemas operativos relacionados, un descriptor de archivo o fd es un indicador abstracto (handle) que se usa para acceder a un archivo u otro recurso de entrada/salida, como una tubería o un socket de red. En términos generales, cualquier cosa de la que se pueda leer información o a la que se pueda volcar información puede representarse mediante un descriptor de archivo.
En la implementación tradicional de Unix, los descriptores de archivo indexan una tabla de descriptores por proceso mantenida por el kernel, que a su vez indexa una tabla global de archivos abiertos por todos los procesos, llamada tabla de archivos. Esta tabla registra el modo con el que el archivo (u otro recurso) ha sido abierto: lectura, escritura, adición y posiblemente otros modos. También indexa una tercera tabla llamada tabla de inodos, que describe los archivos subyacentes reales. Para realizar entrada o salida, el proceso pasa el descriptor de archivo al kernel mediante una syscall, y el kernel accede al archivo en nombre del proceso. El proceso no tiene acceso directo a las tablas de archivos ni de inodos.
En sistemas tipo Unix, los descriptores de archivo pueden referirse a cualquier tipo de archivo Unix nombrado en un sistema de archivos. Además de archivos regulares, esto incluye directorios, dispositivos de bloque y de carácter (también llamados «archivos especiales»), sockets de dominio Unix y tuberías con nombre. Los descriptores de archivo también pueden referirse a otros objetos que normalmente no existen en el sistema de archivos, como tuberías anónimas y sockets de red. Hablaremos mucho de esos dispositivos, como sockets o tuberías, a lo largo de estos artículos sobre temas avanzados de reversing.
¿Recuerdas el tutorial anterior, cuando hablamos de trabajar con archivos usando fwrite y fread? La estructura FILE de la biblioteca de E/S estándar de C suele incluir un descriptor de archivo de bajo nivel para el objeto en cuestión en sistemas tipo Unix, por lo que el objeto FILE y fwrite/fread usan internamente las syscalls write y read.
La siguiente tabla muestra cómo el sistema gestiona los descriptores de archivo para operaciones de lectura/escritura en archivos usando la tabla mencionada:
https://en.wikipedia.org/wiki/File_descriptor
Los descriptores de un proceso referencian entradas de archivo abierto. Esas entradas conservan estado y se relacionan con la información del archivo.
Copia de archivos a nivel de sistema
Profundicemos un poco más en las syscalls. Si existe una llamada write, debe existir una read, ¿verdad? Considera el siguiente ejemplo:
#include <fcntl.h>
#include <stdlib.h>
#define BSIZE 16384
void main(){
int fin, fout;
char buf[BSIZE];
int count;
if ((fin = open("foo", O_RDONLY)) < 0){
perror("foo");
exit(2);
}
if((ffout = open("bar", O_WRONLY | O_CREAT, 0644)) < 0){
perror("bar");
exit(2);
}
while ((count = read(fin, buf, BSIZE)) > 0)
write(fout, buf, count);
close(fin);
close(fout);
}
Este ejemplo abre dos archivos: uno para lectura (O_RDONLY) y otro para escritura (O_WRONLY). El programa comprueba que ambos se abren correctamente y luego lee bytes de uno y los vuelca en el otro.
Inspeccionemos el programa.
[0x5575eada481a]> pdf
; DATA XREF from entry0 @ 0x5575eada472d
┌ 300: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_401ch @ rbp-0x401c
│ ; var int64_t var_4018h @ rbp-0x4018
│ ; var int64_t var_4014h @ rbp-0x4014
│ ; var int64_t var_4010h @ rbp-0x4010
│ ; var int64_t var_8h @ rbp-0x8
│ 0x5575eada481a 55 push rbp
│ 0x5575eada481b 4889e5 mov rbp, rsp
│ 0x5575eada481e 4881ec204000. sub rsp, 0x4020
│ 0x5575eada4825 64488b042528. mov rax, qword fs:[0x28]
│ 0x5575eada482e 488945f8 mov qword [var_8h], rax
│ 0x5575eada4832 31c0 xor eax, eax
│ 0x5575eada4834 be00000000 mov esi, 0
│ 0x5575eada4839 488d3d940100. lea rdi, [0x5575eada49d4] ; "foo"
│ 0x5575eada4840 b800000000 mov eax, 0
│ 0x5575eada4845 e886feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5575eada484a 8985e4bfffff mov dword [var_401ch], eax
│ 0x5575eada4850 83bde4bfffff. cmp dword [var_401ch], 0
│ ┌─< 0x5575eada4857 791b jns 0x5575eada4874
│ │ 0x5575eada4859 488d3d740100. lea rdi, [0x5575eada49d4] ; "foo"
│ │ 0x5575eada4860 b800000000 mov eax, 0
│ │ 0x5575eada4865 e876feffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5575eada486a bf02000000 mov edi, 2
│ │ 0x5575eada486f e87cfeffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5575eada4874 baa4010000 mov edx, 0x1a4 ; 420
│ 0x5575eada4879 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x5575eada487e 488d3d530100. lea rdi, [0x5575eada49d8] ; "bar"
│ 0x5575eada4885 b800000000 mov eax, 0
│ 0x5575eada488a e841feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5575eada488f 8985e8bfffff mov dword [var_4018h], eax
│ 0x5575eada4895 83bde8bfffff. cmp dword [var_4018h], 0
│ ┌─< 0x5575eada489c 793d jns 0x5575eada48db
│ │ 0x5575eada489e 488d3d330100. lea rdi, [0x5575eada49d8] ; "bar"
│ │ 0x5575eada48a5 b800000000 mov eax, 0
│ │ 0x5575eada48aa e831feffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5575eada48af bf02000000 mov edi, 2
│ │ 0x5575eada48b4 e837feffff call sym.imp.exit ; void exit(int status)
│ ┌──> 0x5575eada48b9 8b95ecbfffff mov edx, dword [var_4014h]
│ ╎│ 0x5575eada48bf 488d8df0bfff. lea rcx, [var_4010h]
│ ╎│ 0x5575eada48c6 8b85e8bfffff mov eax, dword [var_4018h]
│ ╎│ 0x5575eada48cc 4889ce mov rsi, rcx
│ ╎│ 0x5575eada48cf 89c7 mov edi, eax
│ ╎│ 0x5575eada48d1 b800000000 mov eax, 0
│ ╎│ 0x5575eada48d6 e8b5fdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ ╎└─> 0x5575eada48db 488d8df0bfff. lea rcx, [var_4010h]
│ ╎ 0x5575eada48e2 8b85e4bfffff mov eax, dword [var_401ch]
│ ╎ 0x5575eada48e8 ba00400000 mov edx, 0x4000
│ ╎ 0x5575eada48ed 4889ce mov rsi, rcx
│ ╎ 0x5575eada48f0 89c7 mov edi, eax
│ ╎ 0x5575eada48f2 b800000000 mov eax, 0
│ ╎ 0x5575eada48f7 e8c4fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎ 0x5575eada48fc 8985ecbfffff mov dword [var_4014h], eax
│ ╎ 0x5575eada4902 83bdecbfffff. cmp dword [var_4014h], 0
│ └──< 0x5575eada4909 7fae jg 0x5575eada48b9
│ 0x5575eada490b 8b85e4bfffff mov eax, dword [var_401ch]
│ 0x5575eada4911 89c7 mov edi, eax
│ 0x5575eada4913 b800000000 mov eax, 0
│ 0x5575eada4918 e893fdffff call sym.imp.close ; int close(int fildes)
│ 0x5575eada491d 8b85e8bfffff mov eax, dword [var_4018h]
│ 0x5575eada4923 89c7 mov edi, eax
│ 0x5575eada4925 b800000000 mov eax, 0
│ 0x5575eada492a e881fdffff call sym.imp.close ; int close(int fildes)
│ 0x5575eada492f 90 nop
│ 0x5575eada4930 488b45f8 mov rax, qword [var_8h]
│ 0x5575eada4934 644833042528. xor rax, qword fs:[0x28]
│ ┌─< 0x5575eada493d 7405 je 0x5575eada4944
│ │ 0x5575eada493f e85cfdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x5575eada4944 c9 leave
└ 0x5575eada4945 c3 ret
[0x5575eada481a]>
Primero se abren esos dos archivos:
│ 0x5575eada4845 e886feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5575eada484a 8985e4bfffff mov dword [var_401ch], eax
│ 0x5575eada4850 83bde4bfffff. cmp dword [var_401ch], 0
│ ┌─< 0x5575eada4857 791b jns 0x5575eada4874
│ │ 0x5575eada4859 488d3d740100. lea rdi, [0x5575eada49d4] ; "foo"
│ │ 0x5575eada4860 b800000000 mov eax, 0
│ │ 0x5575eada4865 e876feffff call sym.imp.perror ; void perror(const char *s)
│ │ 0x5575eada486a bf02000000 mov edi, 2
│ │ 0x5575eada486f e87cfeffff call sym.imp.exit ; void exit(int status)
│ └─> 0x5575eada4874 baa4010000 mov edx, 0x1a4 ; 420
│ 0x5575eada4879 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x5575eada487e 488d3d530100. lea rdi, [0x5575eada49d8] ; "bar"
│ 0x5575eada4885 b800000000 mov eax, 0
│ 0x5575eada488a e841feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5575eada488f 8985e8bfffff mov dword [var_4018h], eax
│ 0x5575eada4895 83bde8bfffff. cmp dword [var_4018h], 0
Si depuramos el código, veremos cómo el proceso maneja 2 descriptores de archivo adicionales tras esas llamadas a open:
[0x5575eada4895]> dr eax
0x00000004
[0x5575eada481a]>
El último open devuelve el descriptor de archivo 4, ya que los fds se asignan de forma consecutiva.
Es fácil deducir que el fd del segundo archivo (fout) se asignará a var_4018h y el del primero (in) irá a var_401ch.
Luego vemos el bucle de lectura/escritura, que lee del primer archivo y vuelca en el segundo.
│ ╎ 0x5575eada48f7 e8c4fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎ ;-- rip:
│ ╎ 0x5575eada48fc b 8985ecbfffff mov dword [var_4014h], eax
│ ╎ 0x5575eada4902 83bdecbfffff. cmp dword [var_4014h], 0
[0x5575eada48fc]> dr rax
0x0000000b
Read devuelve 11 dec, el número de bytes que se han leído y cargado en el búfer correspondiente:
[0x5575eada48fc]> afvd
var var_8h = 0x7ffc6daf1518 = (qword)0x2f161817be55cb00
var var_401ch = 0x7ffc6daed504 = (qword)0x0000000400000003
var var_4018h = 0x7ffc6daed508 = (qword)0x0000000000000004
var var_4010h = 0x7ffc6daed510 = (qword)0x6f775f6f6c6c6568
var var_4014h = 0x7ffc6daed50c = (qword)0x6c6c656800000000
[0x5575eada48fc]> pxw @ 0x7ffc6daed510
0x7ffc6daed510 0x6c6c6568 0x6f775f6f 0x00646c72 0x00000000 hello_world.....
0x7ffc6daed520 0x00000000 0x00000000 0x00000000 0x00000000 ................
Como se muestra, las variables incluyen los descriptores de archivo y un puntero al búfer para read.
La syscall read tomará el número de bytes especificado (o hasta el final del archivo si hay menos) y los volcará en un búfer. Así es como funciona.
Pero read y write no son la única forma de transferir información entre archivos; syscalls como sendfile permiten hacer lo mismo sin necesidad de usar ningún búfer.
Considera el siguiente programa:
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char **argv) {
int src; /* file descriptor for source file */
int dest; /* file descriptor for destination file */
struct stat stat_buf; /* hold information about input file */
off_t offset = 0; /* byte offset used by sendfile */
/* check that source file exists and can be opened */
src = open(argv[1], O_RDONLY);
/* get size and permissions of the source file */
fstat(src, &stat_buf);
/* open destination file */
dest = open(argv[2], O_WRONLY|O_CREAT, stat_buf.st_mode);
/* copy file using sendfile */
sendfile (dest, src, &offset, stat_buf.st_size);
/* clean up and exit */
close(dest);
close(src);
return 0;
}
Aquí hay varias cosas nuevas. Este programa lee argumentos del usuario, luego ejecuta la syscall fstat y finalmente hace sendfile. Analicemos su desensamblado:
[0x5606844bd78a]> pdf
; DATA XREF from entry0 @ 0x5606844bd69d
┌ 258: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_c0h @ rbp-0xc0
│ ; var int64_t var_b4h @ rbp-0xb4
│ ; var int64_t var_b0h @ rbp-0xb0
│ ; var int64_t var_ach @ rbp-0xac
│ ; var int64_t var_a8h @ rbp-0xa8
│ ; var int64_t var_a0h @ rbp-0xa0
│ ; var int64_t var_88h @ rbp-0x88
│ ; var int64_t var_70h @ rbp-0x70
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int argc @ rdi
│ ; arg char **argv @ rsi
│ 0x5606844bd78a 55 push rbp
│ 0x5606844bd78b 4889e5 mov rbp, rsp
│ 0x5606844bd78e 4881ecc00000. sub rsp, 0xc0
│ 0x5606844bd795 89bd4cffffff mov dword [var_b4h], edi ; argc
│ 0x5606844bd79b 4889b540ffff. mov qword [var_c0h], rsi ; argv
│ 0x5606844bd7a2 64488b042528. mov rax, qword fs:[0x28]
│ 0x5606844bd7ab 488945f8 mov qword [var_8h], rax
│ 0x5606844bd7af 31c0 xor eax, eax
│ 0x5606844bd7b1 48c78558ffff. mov qword [var_a8h], 0
│ 0x5606844bd7bc 488b8540ffff. mov rax, qword [var_c0h]
│ 0x5606844bd7c3 4883c008 add rax, 8
│ 0x5606844bd7c7 488b00 mov rax, qword [rax]
│ 0x5606844bd7ca be00000000 mov esi, 0
│ 0x5606844bd7cf 4889c7 mov rdi, rax
│ 0x5606844bd7d2 b800000000 mov eax, 0
│ 0x5606844bd7d7 e884feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5606844bd7dc 898550ffffff mov dword [var_b0h], eax
│ 0x5606844bd7e2 488d9560ffff. lea rdx, [var_a0h]
│ 0x5606844bd7e9 8b8550ffffff mov eax, dword [var_b0h]
│ 0x5606844bd7ef 4889d6 mov rsi, rdx
│ 0x5606844bd7f2 89c7 mov edi, eax
│ 0x5606844bd7f4 b800000000 mov eax, 0
│ 0x5606844bd7f9 e812010000 call sym.fstat ; int fstat(int fildes, void *buf)
│ 0x5606844bd7fe 8b9578ffffff mov edx, dword [var_88h]
│ 0x5606844bd804 488b8540ffff. mov rax, qword [var_c0h]
│ 0x5606844bd80b 4883c010 add rax, 0x10 ; 16
│ 0x5606844bd80f 488b00 mov rax, qword [rax]
│ 0x5606844bd812 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x5606844bd817 4889c7 mov rdi, rax
│ 0x5606844bd81a b800000000 mov eax, 0
│ 0x5606844bd81f e83cfeffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x5606844bd824 898554ffffff mov dword [var_ach], eax
│ 0x5606844bd82a 488b4d90 mov rcx, qword [var_70h]
│ 0x5606844bd82e 488d9558ffff. lea rdx, [var_a8h]
│ 0x5606844bd835 8bb550ffffff mov esi, dword [var_b0h]
│ 0x5606844bd83b 8b8554ffffff mov eax, dword [var_ach]
│ 0x5606844bd841 89c7 mov edi, eax
│ 0x5606844bd843 b800000000 mov eax, 0
│ 0x5606844bd848 e803feffff call sym.imp.sendfile
│ 0x5606844bd84d 8b8554ffffff mov eax, dword [var_ach]
│ 0x5606844bd853 89c7 mov edi, eax
│ 0x5606844bd855 b800000000 mov eax, 0
│ 0x5606844bd85a e8d1fdffff call sym.imp.close ; int close(int fildes)
│ 0x5606844bd85f 8b8550ffffff mov eax, dword [var_b0h]
│ 0x5606844bd865 89c7 mov edi, eax
│ 0x5606844bd867 b800000000 mov eax, 0
│ 0x5606844bd86c e8bffdffff call sym.imp.close ; int close(int fildes)
│ 0x5606844bd871 b800000000 mov eax, 0
│ 0x5606844bd876 488b4df8 mov rcx, qword [var_8h]
│ 0x5606844bd87a 6448330c2528. xor rcx, qword fs:[0x28]
│ ┌─< 0x5606844bd883 7405 je 0x5606844bd88a
│ │ 0x5606844bd885 e896fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x5606844bd88a c9 leave
└ 0x5606844bd88b c3 ret
[0x5606844bd78a]>
Primero identificamos una llamada a fstat o file status. Esta llamada obtiene información relacionada con un archivo, como la ruta, los permisos, etc. (https://linux.die.net/man/2/fstat)
│ 0x5606844bd7dc 898550ffffff mov dword [var_b0h], eax
│ 0x5606844bd7e2 488d9560ffff. lea rdx, [var_a0h]
│ 0x5606844bd7e9 8b8550ffffff mov eax, dword [var_b0h]
│ 0x5606844bd7ef 4889d6 mov rsi, rdx
│ 0x5606844bd7f2 89c7 mov edi, eax
│ 0x5606844bd7f4 b800000000 mov eax, 0
│ 0x5606844bd7f9 e812010000 call sym.fstat ; int fstat(int fildes, void *buf)
│ 0x5606844bd7fe 8b9578ffffff mov edx, dword [var_88h]
La información devuelta por fstat se almacena en una struct en memoria, por lo que como vemos se pasa un puntero a esa struct como uno de los parámetros. Esta técnica es muy común en código de sistemas: como las funciones en C no devuelven objetos, cuando necesitamos recuperar información estructurada es habitual pasar un puntero a la struct correspondiente a la función, y la función «rellena» esa struct.
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
Como se puede ver, tras la llamada el espacio ha sido rellenado en memoria; podemos identificar, por ejemplo, el valor 0xB = 11 dec correspondiente al campo off_t.
[0x5606844bd7fe]> pxw @ 0x7fff922b2700
0x7fff922b2700 0x0000fd01 0x00000000 0x00225848 0x00000000 ........HX".....
0x7fff922b2710 0x00000001 0x00000000 0x000081a4 0x000003e8 ................
0x7fff922b2720 0x000003e8 0x00000000 0x00000000 0x00000000 ................
0x7fff922b2730 0x0000000b 0x00000000 0x00001000 0x00000000 ................
0x7fff922b2740 0x00000008 0x00000000 0x5ec14670 0x00000000 ........pF.^....
0x7fff922b2750 0x144d8dec 0x00000000 0x5ec06dfa 0x00000000 ..M......m.^....
También vemos que r2 no reconoce la struct (como de costumbre) y usa referencias de variables independientes para cada valor, correspondientes al tamaño de la struct.
Structs en archivos
¿Qué ocurre si el programa necesita guardar su estado de alguna forma? Algunos programas usan archivos propios para guardar información útil en disco. Considera este programa:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
struct person
{
int id;
char fname[20];
char lname[20];
};
int main ()
{
int outfile;
outfile = open ("person.dat", O_WRONLY | O_CREAT, 0644);
if(outfile > 0){
struct person input1 = {1, "artik", "blue"};
struct person input2 = {2, "john", "doe"};
write (outfile , &input2, sizeof(struct person));
write (outfile , &input1, sizeof(struct person));
close(outfile);
}
return 0;
}
El programa vuelca literalmente una struct al disco. Veamos cómo se hace:
[0x55f0d1e2e73a]> pdf
; DATA XREF from entry0 @ 0x55f0d1e2e64d
┌ 272: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_74h @ rbp-0x74
│ ; var int64_t var_70h @ rbp-0x70
│ ; var int64_t var_6ch @ rbp-0x6c
│ ; var int64_t var_64h @ rbp-0x64
│ ; var int64_t var_5ch @ rbp-0x5c
│ ; var int64_t var_58h @ rbp-0x58
│ ; var int64_t var_50h @ rbp-0x50
│ ; var int64_t var_48h @ rbp-0x48
│ ; var int64_t var_40h @ rbp-0x40
│ ; var int64_t var_3ch @ rbp-0x3c
│ ; var int64_t var_34h @ rbp-0x34
│ ; var int64_t var_2ch @ rbp-0x2c
│ ; var int64_t var_28h @ rbp-0x28
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55f0d1e2e73a 55 push rbp
│ 0x55f0d1e2e73b 4889e5 mov rbp, rsp
│ 0x55f0d1e2e73e 4883c480 add rsp, 0xffffffffffffff80
│ 0x55f0d1e2e742 64488b042528. mov rax, qword fs:[0x28]
│ 0x55f0d1e2e74b 488945f8 mov qword [var_8h], rax
│ 0x55f0d1e2e74f 31c0 xor eax, eax
│ 0x55f0d1e2e751 baa4010000 mov edx, 0x1a4 ; 420
│ 0x55f0d1e2e756 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x55f0d1e2e75b 488d3d720100. lea rdi, str.person.dat ; 0x55f0d1e2e8d4 ; "person.dat"
│ 0x55f0d1e2e762 b800000000 mov eax, 0
│ 0x55f0d1e2e767 e8a4feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x55f0d1e2e76c 89458c mov dword [var_74h], eax
│ 0x55f0d1e2e76f 837d8c00 cmp dword [var_74h], 0
│ ┌─< 0x55f0d1e2e773 0f8eb6000000 jle 0x55f0d1e2e82f
│ │ 0x55f0d1e2e779 c74590010000. mov dword [var_70h], 1
│ │ 0x55f0d1e2e780 48b861727469. movabs rax, 0x6b69747261 ; 'artik'
│ │ 0x55f0d1e2e78a ba00000000 mov edx, 0
│ │ 0x55f0d1e2e78f 48894594 mov qword [var_6ch], rax
│ │ 0x55f0d1e2e793 4889559c mov qword [var_64h], rdx
│ │ 0x55f0d1e2e797 c745a4000000. mov dword [var_5ch], 0
│ │ 0x55f0d1e2e79e 48c745a8626c. mov qword [var_58h], 0x65756c62 ; 'blue'
│ │ 0x55f0d1e2e7a6 48c745b00000. mov qword [var_50h], 0
│ │ 0x55f0d1e2e7ae c745b8000000. mov dword [var_48h], 0
│ │ 0x55f0d1e2e7b5 c745c0020000. mov dword [var_40h], 2
│ │ 0x55f0d1e2e7bc 48c745c46a6f. mov qword [var_3ch], 0x6e686f6a ; 'john'
│ │ 0x55f0d1e2e7c4 48c745cc0000. mov qword [var_34h], 0
│ │ 0x55f0d1e2e7cc c745d4000000. mov dword [var_2ch], 0
│ │ 0x55f0d1e2e7d3 48c745d8646f. mov qword [var_28h], 0x656f64 ; 'doe'
│ │ 0x55f0d1e2e7db 48c745e00000. mov qword [var_20h], 0
│ │ 0x55f0d1e2e7e3 c745e8000000. mov dword [var_18h], 0
│ │ 0x55f0d1e2e7ea 488d4dc0 lea rcx, [var_40h]
│ │ 0x55f0d1e2e7ee 8b458c mov eax, dword [var_74h]
│ │ 0x55f0d1e2e7f1 ba2c000000 mov edx, 0x2c ; ',' ; 44
│ │ 0x55f0d1e2e7f6 4889ce mov rsi, rcx
│ │ 0x55f0d1e2e7f9 89c7 mov edi, eax
│ │ 0x55f0d1e2e7fb b800000000 mov eax, 0
│ │ 0x55f0d1e2e800 e8dbfdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ │ 0x55f0d1e2e805 488d4d90 lea rcx, [var_70h]
│ │ 0x55f0d1e2e809 8b458c mov eax, dword [var_74h]
│ │ 0x55f0d1e2e80c ba2c000000 mov edx, 0x2c ; ',' ; 44
│ │ 0x55f0d1e2e811 4889ce mov rsi, rcx
│ │ 0x55f0d1e2e814 89c7 mov edi, eax
│ │ 0x55f0d1e2e816 b800000000 mov eax, 0
│ │ 0x55f0d1e2e81b e8c0fdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ │ 0x55f0d1e2e820 8b458c mov eax, dword [var_74h]
│ │ 0x55f0d1e2e823 89c7 mov edi, eax
│ │ 0x55f0d1e2e825 b800000000 mov eax, 0
│ │ 0x55f0d1e2e82a e8d1fdffff call sym.imp.close ; int close(int fildes)
│ └─> 0x55f0d1e2e82f b800000000 mov eax, 0
│ 0x55f0d1e2e834 488b75f8 mov rsi, qword [var_8h]
│ 0x55f0d1e2e838 644833342528. xor rsi, qword fs:[0x28]
│ ┌─< 0x55f0d1e2e841 7405 je 0x55f0d1e2e848
│ │ 0x55f0d1e2e843 e8a8fdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55f0d1e2e848 c9 leave
└ 0x55f0d1e2e849 c3 ret
[0x55f0d1e2e73a]>
Como de costumbre, no reconoce la struct y mantiene una referencia para cada valor:
│ │ 0x55f0d1e2e779 c74590010000. mov dword [var_70h], 1
│ │ 0x55f0d1e2e780 48b861727469. movabs rax, 0x6b69747261 ; 'artik'
│ │ 0x55f0d1e2e78a ba00000000 mov edx, 0
│ │ 0x55f0d1e2e78f 48894594 mov qword [var_6ch], rax
│ │ 0x55f0d1e2e793 4889559c mov qword [var_64h], rdx
│ │ 0x55f0d1e2e797 c745a4000000. mov dword [var_5ch], 0
│ │ 0x55f0d1e2e79e 48c745a8626c. mov qword [var_58h], 0x65756c62 ; 'blue'
│ │ 0x55f0d1e2e7a6 48c745b00000. mov qword [var_50h], 0
│ │ 0x55f0d1e2e7ae c745b8000000. mov dword [var_48h], 0
│ │ 0x55f0d1e2e7b5 c745c0020000. mov dword [var_40h], 2
│ │ 0x55f0d1e2e7bc 48c745c46a6f. mov qword [var_3ch], 0x6e686f6a ; 'john'
│ │ 0x55f0d1e2e7c4 48c745cc0000. mov qword [var_34h], 0
│ │ 0x55f0d1e2e7cc c745d4000000. mov dword [var_2ch], 0
│ │ 0x55f0d1e2e7d3 48c745d8646f. mov qword [var_28h], 0x656f64 ; 'doe'
│ │ 0x55f0d1e2e7db 48c745e00000. mov qword [var_20h], 0
│ │ 0x55f0d1e2e7e3 c745e8000000. mov dword [var_18h], 0
Como se ve, creará e inicializará esas structs y rellenará los huecos con ceros.
│ │ 0x55f0d1e2e7fb b800000000 mov eax, 0
│ │ 0x55f0d1e2e800 e8dbfdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ │ 0x55f0d1e2e805 488d4d90 lea rcx, [var_70h]
│ │ ;-- rip:
│ │ 0x55f0d1e2e809 b 8b458c mov eax, dword [var_74h]
Luego simplemente escribe el contenido en disco. Inspeccionemos qué se envía al archivo:
[0x55f0d1e2e809]> pxw @ 0x7ffdb5e2bf3c
0x7ffdb5e2bf3c 0x00000003 0x00000001 0x69747261 0x0000006b ........artik...
0x7ffdb5e2bf4c 0x00000000 0x00000000 0x00000000 0x65756c62 ............blue
0x7ffdb5e2bf5c 0x00000000 0x00000000 0x00000000 0x00000000 ................
0x7ffdb5e2bf6c 0x00000000 0x00000002 0x6e686f6a 0x00000000 ........john....
0x7ffdb5e2bf7c 0x00000000 0x00000000 0x00000000 0x00656f64 ............doe.
La struct, mapeada en memoria, se vuelca «literalmente» al archivo.
[0x55f0d1e2e809]> "td struct person { long id; char fname[20]; char lname[20]; }; "
[0x55f0d1e2e809]> tp person @ 0x7ffdb5e2bf3c
id : 0x7ffdb5e2bf3c = (qword)0x0000000100000003
fname : 0x7ffdb5e2bf44 = "artik"
lname : 0x7ffdb5e2bf58 = "blue"
Moverse dentro del archivo
¿Qué ocurre si queremos saltar directamente a un valor concreto? Si queremos hacer eso en lugar de tener que recorrer el archivo completo (imagina un archivo largo con un formato conocido), podemos usar lseek. Con lseek podemos movernos hacia adelante y hacia atrás dentro del archivo.
Observa esto:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdlib.h>
struct person
{
int id;
char fname[20];
char lname[20];
};
int main ()
{
int infile, outfile;
struct person input;
infile = open ("person.dat", O_RDONLY , 0644);
lseek(infile, 1*sizeof(struct person), SEEK_CUR);
read(infile, &input, sizeof(struct person));
printf("second person val = %d, %s, %s \n", input.id, input.fname, input.lname);
close (infile);
return 0;
}
El código presentado abre el archivo para lectura y usa lseek para indicar al kernel que se mueva dentro del archivo. Avanzar una vez el sizeof de la struct equivale a moverse una posición desde el primer valor, es decir, ir directamente al segundo valor.
El archivo que se carga tiene este aspecto:
[0x00000000]> x
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00000000 0200 0000 6a6f 686e 0000 0000 0000 0000 ....john........
0x00000010 0000 0000 0000 0000 646f 6500 0000 0000 ........doe.....
0x00000020 0000 0000 0000 0000 0000 0000 0100 0000 ................
0x00000030 6172 7469 6b00 0000 0000 0000 0000 0000 artik...........
0x00000040 0000 0000 626c 7565 0000 0000 0000 0000 ....blue........
0x00000050 0000 0000 0000 0000 ffff ffff ffff ffff ................
[0x00000000]> izz
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x00000004 0x00000004 4 5 ascii john
1 0x00000030 0x00000030 5 6 ascii artik
2 0x00000044 0x00000044 4 5 ascii blue
[0x00000000]>
Podemos editar fácilmente el archivo para escribir nuevo contenido nosotros mismos con wx.
[0x00000000]> wz red @ 0x00000018
[0x00000000]> x
- offset - 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
0x00000000 0200 0000 6a6f 686e 0000 0000 0000 0000 ....john........
0x00000010 0000 0000 0000 0000 7265 6400 0000 0000 ........red.....
0x00000020 0000 0000 0000 0000 0000 0000 0100 0000 ................
0x00000030 6172 7469 6b00 0000 0000 0000 0000 0000 artik...........
0x00000040 0000 0000 626c 7565 0000 0000 0000 0000 ....blue........
0x00000050 0000 0000 0000 0000 ffff ffff ffff ffff ................
El fichero generado anteriormente, que ahora se carga de nuevo en memoria, es un fichero de datos —contenido hexadecimal, no ASCII—, por lo que para editarlo hay que usar un editor hexadecimal; de lo contrario se puede corromper. Este ejemplo es muy sencillo: contiene esos valores en posiciones muy concretas y el resto está relleno de ceros. Muchos ficheros tienen formatos específicos que incluyen bytes en posiciones determinadas (como la firma MZ en los programas de Windows); hablaremos de eso más adelante en el curso.
Carga de ficheros y cifrado XOR de cadenas
El siguiente programa lee del fichero, carga esos valores de nuevo en una estructura, los cifra y vuelca el resultado en otro fichero. El objetivo de este programa sencillo es mostrar que operaciones de gestión de datos como las presentadas se realizan con facilidad y, de hecho, las llevan a cabo muchos programas, incluido el malware (de forma algo más avanzada). La lógica es la misma; veámoslo:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <stdlib.h>
struct person
{
int id;
char fname[20];
char lname[20];
};
void cryp(char arr[]){
char k[20] = "01234567890123456789";
for(int i = 0; i < sizeof(arr); i ++){
arr[i] ^= k[i];
}
arr[sizeof(arr)-1]='\0';
}
int main ()
{
int infile, outfile;
struct person input;
infile = open ("person.dat", O_RDONLY , 0644);
outfile = open ("person.cry", O_WRONLY | O_CREAT, 0644);
while(read(infile, &input, sizeof(struct person))){
cryp(input.fname);
cryp(input.lname);
write(outfile, &input, sizeof(struct person));
}
close (infile);
return 0;
}
Saltamos directamente a r2:
[0x7fb0c4197090]> s main
[0x55de52c0c82c]> pdf
; DATA XREF from entry0 @ 0x55de52c0c69d
┌ 217: int main (int argc, char **argv, char **envp);
│ ; var int64_t var_48h @ rbp-0x48
│ ; var int64_t var_44h @ rbp-0x44
│ ; var int64_t var_40h @ rbp-0x40
│ ; var int64_t var_8h @ rbp-0x8
│ 0x55de52c0c82c 55 push rbp
│ 0x55de52c0c82d 4889e5 mov rbp, rsp
│ 0x55de52c0c830 4883ec50 sub rsp, 0x50
│ 0x55de52c0c834 64488b042528. mov rax, qword fs:[0x28]
│ 0x55de52c0c83d 488945f8 mov qword [var_8h], rax
│ 0x55de52c0c841 31c0 xor eax, eax
│ 0x55de52c0c843 baa4010000 mov edx, 0x1a4 ; 420
│ 0x55de52c0c848 be00000000 mov esi, 0
│ 0x55de52c0c84d 488d3d400100. lea rdi, str.person.dat ; 0x55de52c0c994 ; "person.dat"
│ 0x55de52c0c854 b800000000 mov eax, 0
│ 0x55de52c0c859 e802feffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x55de52c0c85e 8945b8 mov dword [var_48h], eax
│ 0x55de52c0c861 baa4010000 mov edx, 0x1a4 ; 420
│ 0x55de52c0c866 be41000000 mov esi, 0x41 ; 'A' ; 65
│ 0x55de52c0c86b 488d3d2d0100. lea rdi, str.person.cry ; 0x55de52c0c99f ; "person.cry"
│ 0x55de52c0c872 b800000000 mov eax, 0
│ 0x55de52c0c877 e8e4fdffff call sym.imp.open ; int open(const char *path, int oflag)
│ 0x55de52c0c87c 8945bc mov dword [var_44h], eax
│ ┌─< 0x55de52c0c87f eb3b jmp 0x55de52c0c8bc
│ ┌──> 0x55de52c0c881 488d45c0 lea rax, [var_40h]
│ ╎│ 0x55de52c0c885 4883c004 add rax, 4
│ ╎│ 0x55de52c0c889 4889c7 mov rdi, rax
│ ╎│ 0x55de52c0c88c e8f9feffff call sym.cryp
│ ╎│ 0x55de52c0c891 488d45c0 lea rax, [var_40h]
│ ╎│ 0x55de52c0c895 4883c018 add rax, 0x18 ; 24
│ ╎│ 0x55de52c0c899 4889c7 mov rdi, rax
│ ╎│ 0x55de52c0c89c e8e9feffff call sym.cryp
│ ╎│ 0x55de52c0c8a1 488d4dc0 lea rcx, [var_40h]
│ ╎│ 0x55de52c0c8a5 8b45bc mov eax, dword [var_44h]
│ ╎│ 0x55de52c0c8a8 ba2c000000 mov edx, 0x2c ; ',' ; 44
│ ╎│ 0x55de52c0c8ad 4889ce mov rsi, rcx
│ ╎│ 0x55de52c0c8b0 89c7 mov edi, eax
│ ╎│ 0x55de52c0c8b2 b800000000 mov eax, 0
│ ╎│ 0x55de52c0c8b7 e864fdffff call sym.imp.write ; ssize_t write(int fd, const char *ptr, size_t nbytes)
│ ╎│ ; CODE XREF from main @ 0x55de52c0c87f
│ ╎└─> 0x55de52c0c8bc 488d4dc0 lea rcx, [var_40h]
│ ╎ 0x55de52c0c8c0 8b45b8 mov eax, dword [var_48h]
│ ╎ 0x55de52c0c8c3 ba2c000000 mov edx, 0x2c ; ',' ; 44
│ ╎ 0x55de52c0c8c8 4889ce mov rsi, rcx
│ ╎ 0x55de52c0c8cb 89c7 mov edi, eax
│ ╎ 0x55de52c0c8cd b800000000 mov eax, 0
│ ╎ 0x55de52c0c8d2 e879fdffff call sym.imp.read ; ssize_t read(int fildes, void *buf, size_t nbyte)
│ ╎ 0x55de52c0c8d7 85c0 test eax, eax
│ └──< 0x55de52c0c8d9 75a6 jne 0x55de52c0c881
│ 0x55de52c0c8db 8b45b8 mov eax, dword [var_48h]
│ 0x55de52c0c8de 89c7 mov edi, eax
│ 0x55de52c0c8e0 b800000000 mov eax, 0
│ 0x55de52c0c8e5 e856fdffff call sym.imp.close ; int close(int fildes)
│ 0x55de52c0c8ea b800000000 mov eax, 0
│ 0x55de52c0c8ef 488b55f8 mov rdx, qword [var_8h]
│ 0x55de52c0c8f3 644833142528. xor rdx, qword fs:[0x28]
│ ┌─< 0x55de52c0c8fc 7405 je 0x55de52c0c903
│ │ 0x55de52c0c8fe e82dfdffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55de52c0c903 c9 leave
└ 0x55de52c0c904 c3 ret
[0x55de52c0c82c]>
Podemos empezar por la primera función, main, y saltar directamente tras el primer open:
│ ; 0x55de52c0c994 ; "person.dat"
│ 0x55de52c0c854 b800000000 mov eax, 0
│ 0x55de52c0c859 e802feffff call sym.imp.open ; int open(const char *path, int oflag)
│ ;-- rip:
│ 0x55de52c0c85e b 8945b8 mov dword [var_48h], eax
│ 0x55de52c0c861 baa4010000 mov edx, 0x1a4 ; 420
El primer open lanza y abre person.dat, devolviendo el descriptor de fichero correspondiente. En radare2 podemos listar los descriptores de fichero abiertos por el programa con dd:
[0x55de52c0c85e]> dd
0 0x0 r-C /dev/pts/2
1 0x0 r-C /dev/pts/2
2 0x0 r-C /dev/pts/2
3 0x0 r-S /home/red/c/part2/new/person.dat
Los descriptores 0, 1 y 2 se utilizan convencionalmente para entrada estándar, salida estándar y error estándar. Pueden estar cerrados o redirigidos. Una apertura suele recibir el descriptor libre de menor número; observa el retorno concreto tras el segundo open:
│ ;-- rip:
│ 0x55de52c0c87c b 8945bc mov dword [var_44h], eax
│ ┌─< 0x55de52c0c87f eb3b jmp 0x55de52c0c8bc
│ ┌──> 0x55de52c0c881 488d45c0 lea rax, [var_40h]
│ ╎│ 0x55de52c0c885 4883c004 add rax, 4
El programa gestiona ahora dos ficheros; se ha añadido el fd número 4:
[0x55de52c0c87c]> dd
0 0x0 r-C /dev/pts/2
1 0x0 r-C /dev/pts/2
2 0x0 r-C /dev/pts/2
3 0x0 r-S /home/red/c/part2/new/person.dat
4 0x0 r-S /home/red/c/part2/new/person.cry
A continuación el programa recorre el fichero bloque a bloque, carga los datos en la estructura y pasa una referencia a esos campos a la función crypt:
│ 0x55de52c0c87c b 8945bc mov dword [var_44h], eax
│ ┌─< 0x55de52c0c87f eb3b jmp 0x55de52c0c8bc
│ ┌──> 0x55de52c0c881 488d45c0 lea rax, [var_40h]
│ ╎│ ;-- rip:
│ ╎│ 0x55de52c0c885 b 4883c004 add rax, 4
│ ╎│ 0x55de52c0c889 4889c7 mov rdi, rax
│ ╎│ 0x55de52c0c88c e8f9feffff call sym.cryp
│ ╎│ 0x55de52c0c891 488d45c0 lea rax, [var_40h]
│ ╎│ 0x55de52c0c895 4883c018 add rax, 0x18 ; 24
│ ╎│ 0x55de52c0c899 4889c7 mov rdi, rax
│ ╎│ 0x55de52c0c89c e8e9feffff call sym.cryp
Podemos inspeccionar esos valores antes de entrar en la función crypt:
[0x557979560885]> afvd
var var_8h = 0x7ffc1c931058 = (qword)0xdb75146eaecef500
var var_48h = 0x7ffc1c931018 = (qword)0x0000000400000003
var var_44h = 0x7ffc1c93101c = (qword)0x0000000200000004
var var_40h = 0x7ffc1c931020 = (qword)0x6e686f6a00000002
[0x557979560885]> pxw @ 0x7ffc1c931020
0x7ffc1c931020 0x00000002 0x6e686f6a 0x00000000 0x00000000 ....john........
0x7ffc1c931030 0x00000000 0x00000000 0x00646572 0x00000000 ........red.....
0x7ffc1c931040 0x00000000 0x00000000 0x00000000 0x00005579 ............yU..
0x7ffc1c931050 0x1c931140 0x00007ffc 0xaecef500 0xdb75146e @...........n.u.
0x7ffc1c931060 0x79560910 0x00005579 0xd4fb9b97 0x00007f20 ..VyyU...... ...
La función crypt es la siguiente:
[0x557979560885]> db sym.cryp
[0x557979560885]> dc
hit breakpoint at: 55797956078a
[0x55797956078a]> pdf
; CALL XREFS from main @ 0x55797956088c, 0x55797956089c
;-- rip:
┌ 162: sym.cryp (int64_t arg1);
│ ; var int64_t var_38h @ rbp-0x38
│ ; var int64_t var_24h @ rbp-0x24
│ ; var int64_t var_20h @ rbp-0x20
│ ; var int64_t var_18h @ rbp-0x18
│ ; var int64_t var_10h @ rbp-0x10
│ ; var int64_t var_8h @ rbp-0x8
│ ; arg int64_t arg1 @ rdi
│ 0x55797956078a b 55 push rbp
│ 0x55797956078b 4889e5 mov rbp, rsp
│ 0x55797956078e 4883ec40 sub rsp, 0x40
│ 0x557979560792 48897dc8 mov qword [var_38h], rdi ; arg1
│ 0x557979560796 64488b042528. mov rax, qword fs:[0x28]
│ 0x55797956079f 488945f8 mov qword [var_8h], rax
│ 0x5579795607a3 31c0 xor eax, eax
│ 0x5579795607a5 48b830313233. movabs rax, 0x3736353433323130 ; '01234567'
│ 0x5579795607af 48ba38393031. movabs rdx, 0x3534333231303938 ; '89012345'
│ 0x5579795607b9 488945e0 mov qword [var_20h], rax
│ 0x5579795607bd 488955e8 mov qword [var_18h], rdx
│ 0x5579795607c1 c745f0363738. mov dword [var_10h], 0x39383736 ; '6789'
│ 0x5579795607c8 c745dc000000. mov dword [var_24h], 0
│ ┌─< 0x5579795607cf eb31 jmp 0x557979560802
│ ┌──> 0x5579795607d1 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x5579795607d4 4863d0 movsxd rdx, eax
│ ╎│ 0x5579795607d7 488b45c8 mov rax, qword [var_38h]
│ ╎│ 0x5579795607db 4801d0 add rax, rdx
│ ╎│ 0x5579795607de 0fb630 movzx esi, byte [rax]
│ ╎│ 0x5579795607e1 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x5579795607e4 4898 cdqe
│ ╎│ 0x5579795607e6 0fb64c05e0 movzx ecx, byte [rbp + rax - 0x20]
│ ╎│ 0x5579795607eb 8b45dc mov eax, dword [var_24h]
│ ╎│ 0x5579795607ee 4863d0 movsxd rdx, eax
│ ╎│ 0x5579795607f1 488b45c8 mov rax, qword [var_38h]
│ ╎│ 0x5579795607f5 4801d0 add rax, rdx
│ ╎│ 0x5579795607f8 31ce xor esi, ecx
│ ╎│ 0x5579795607fa 89f2 mov edx, esi
│ ╎│ 0x5579795607fc 8810 mov byte [rax], dl
│ ╎│ 0x5579795607fe 8345dc01 add dword [var_24h], 1
│ ╎│ ; CODE XREF from sym.cryp @ 0x5579795607cf
│ ╎└─> 0x557979560802 8b45dc mov eax, dword [var_24h]
│ ╎ 0x557979560805 83f807 cmp eax, 7 ; 7
│ └──< 0x557979560808 76c7 jbe 0x5579795607d1
│ 0x55797956080a 488b45c8 mov rax, qword [var_38h]
│ 0x55797956080e 4883c007 add rax, 7
│ 0x557979560812 c60000 mov byte [rax], 0
│ 0x557979560815 90 nop
│ 0x557979560816 488b45f8 mov rax, qword [var_8h]
│ 0x55797956081a 644833042528. xor rax, qword fs:[0x28]
│ ┌─< 0x557979560823 7405 je 0x55797956082a
│ │ 0x557979560825 e806feffff call sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ └─> 0x55797956082a c9 leave
└ 0x55797956082b c3 ret
[0x55797956078a]>
El XOR se realiza así:
│ ╎│ 0x5579795607f1 488b45c8 mov rax, qword [var_38h]
│ ╎│ 0x5579795607f5 4801d0 add rax, rdx
│ ╎│ 0x5579795607f8 31ce xor esi, ecx
│ ╎│ 0x5579795607fa 89f2 mov edx, esi
│ ╎│ 0x5579795607fc 8810 mov byte [rax], dl
│ ╎│ 0x5579795607fe 8345dc01 add dword [var_24h], 1
Con la siguiente clave, que se carga antes de ejecutar el XOR:
[0x5579795607fa]> pxw @ 0x7ffc1c930fc8
0x7ffc1c930fc8 0x1c931024 0x00007ffc 0x1c9892a8 0x00007ffc $...............
0x7ffc1c930fd8 0xd55b2710 0x00000001 0x33323130 0x37363534 .'[.....01234567
0x7ffc1c930fe8 0x31303938 0x35343332 0x39383736 0x00000000 890123456789....
El cifrado se hace carácter a carácter, como se ve aquí:
[0x5579795607f8]> dr esi
0x0000006f
[0x5579795607f8]> dr ecx
0x00000031 (= ascii l from blue)
[0x5579795607f8]>
[0x5579795607f8]> ds
[0x5579795607fa]> dr esi
0x0000005e
[0x5579795607fa]> dr rax
0x7ffc1c931025
[0x5579795607fa]> pxw @ 0x7ffc1c931024
0x7ffc1c931024 0x6e686f5a 0x00000000 0x00000000 0x00000000 Zohn............
0x7ffc1c931034 0x00000000 0x00646572 0x00000000 0x00000000 ....red.........
Cuando se trabaja con funciones muy largas y complejas que devuelven un valor, puede ser útil poner un breakpoint antes y después de su ejecución y limitarse a observar el resultado sin entrar en los detalles internos.
A veces se hace análisis estático sobre un programa que no se puede ejecutar; en esos casos las rutinas de cifrado pueden ser muy costosas de analizar. ESIL resulta útil en esas situaciones.
Unas palabras sobre ESIL
ESIL son las siglas de Evaluable Strings Intermediate Language y, en esencia, permite depurar programas sin necesidad de ejecutarlos en nuestra máquina. Trataré este tema de forma breve aquí, ya que lo exploraremos con más detalle a lo largo del curso.
Empecemos con este ejemplo:
#include <stdio.h>
void main(){
char a[4] = "abcd";
char b[4] = "XYZU";
for (int i=0; i < 4; i++){
b[i] ^= a[i];
}
printf("%s \n",b);
}
En términos generales: habrá ocasiones en que no queramos o no podamos ejecutar el programa en nuestra máquina. Eso puede ocurrir al trabajar con malware o al hacer reversing de programas para arquitecturas poco habituales (como Game Boy). En esos casos el análisis estático no es suficiente, ya que puede ser necesario calcular hacia dónde saltará el código en función de una entrada concreta, o recorrer una rutina de cifrado que sería muy costosa de analizar a mano.
ESIL ayuda con esto; r2 ofrece la ESIL VM. Puedes leer sobre ella aquí: https://radare.gitbooks.io/radare2book/analysis/emulation.html
Con esa VM podemos depurar el programa sin ejecutarlo realmente. Las posibilidades son limitadas, ya que solo estamos emulando y no contamos con el respaldo del sistema operativo, por lo que no podremos depurar syscalls como las que acabamos de ver. Aun así, ESIL es capaz de realizar cálculos.
Se activa así:
[0x000005a0]> s main
[0x000005a0]> e asm.esil = true
[0x000005a0]> aei
[0x000005a0]> aeim
[0x000005a0]> aeip
[0x000006aa]> pdf
; DATA XREF from entry0 @ 0x5bd
;-- rip:
┌ 134: int main (int argc, char **argv, char **envp);
│ ; var signed int64_t var_14h @ rbp-0x14
│ ; var int64_t var_10h @ rbp-0x10
│ ; var char *var_ch @ rbp-0xc
│ ; var int64_t canary @ rbp-0x8
│ 0x000006aa 55 rbp,8,rsp,-,=[8],8,rsp,-=
│ 0x000006ab 4889e5 rsp,rbp,=
│ 0x000006ae 4883ec20 32,rsp,-=,63,$o,of,:=,63,$s,sf,:=,$z,zf,:=,$p,pf,:=,64,$b,cf,:=
│ 0x000006b2 64488b042528. 0x28,[8],rax,=
│ 0x000006bb 488945f8 rax,0x8,rbp,-,=[8]
│ 0x000006bf 31c0 eax,rax,^,0xffffffff,&,rax,=,$z,zf,:=,$p,pf,:=,31,$s,sf,:=,0,cf,:=,0,of,:=
│ 0x000006c1 c745f0616263. 1684234849,0x10,rbp,-,=[4] ; 'abcd'
│ ; 0x64636261
│ 0x000006c8 c745f458595a. 1431984472,0xc,rbp,-,=[4] ; 'XYZU'
│ ; 0x555a5958
│ 0x000006cf c745ec000000. 0,0x14,rbp,-,=[4]
│ ┌─< 0x000006d6 eb23 0x6fb,rip,=
│ │ ; CODE XREF from main @ 0x6ff
│ ┌──> 0x000006d8 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006db 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006dd 0fb65405f4 0xc,rax,rbp,+,-,[1],rdx,=
│ ╎│ 0x000006e2 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006e5 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006e7 0fb64405f0 0x10,rax,rbp,+,-,[1],rax,=
│ ╎│ 0x000006ec 31c2 eax,rdx,^,0xffffffff,&,rdx,=,$z,zf,:=,$p,pf,:=,31,$s,sf,:=,0,cf,:=,0,of,:=
│ ╎│ 0x000006ee 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006f1 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006f3 885405f4 dl,0xc,rax,rbp,+,-,=[1]
│ ╎│ 0x000006f7 8345ec01 1,0x14,rbp,-,+=[4],31,$o,of,:=,31,$s,sf,:=,$z,zf,:=,31,$c,cf,:=,$p,pf,:=
│ ╎│ ; CODE XREF from main @ 0x6d6
│ ╎└─> 0x000006fb 837dec03 3,0x14,rbp,-,[4],==,$z,zf,:=,32,$b,cf,:=,$p,pf,:=,31,$s,sf,:=,31,$o,of,:=
│ └──< 0x000006ff 7ed7 of,sf,^,zf,|,?{,1752,rip,=,}
│ 0x00000701 488d45f4 0xc,rbp,-,rax,=
│ 0x00000705 4889c6 rax,rsi,=
│ 0x00000708 488d3da50000. 0xa5,rip,+,rdi,= ; str.s
│ ; 0x7b4 ; "%s \n" ; const char *format
│ 0x0000070f b800000000 0,rax,=
│ 0x00000714 e867feffff 1408,rip,8,rsp,-=,rsp,=[],rip,= ; sym.imp.printf ; int printf(const char *format)
│ 0x00000719 90 ,
│ 0x0000071a 488b45f8 0x8,rbp,-,[8],rax,=
│ 0x0000071e 644833042528. 0x28,[8],rax,^=,$z,zf,:=,$p,pf,:=,63,$s,sf,:=,0,cf,:=,0,of,:=
│ ┌─< 0x00000727 7405 zf,?{,1838,rip,=,}
│ │ 0x00000729 e842feffff 1392,rip,8,rsp,-=,rsp,=[],rip,= ; sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from main @ 0x727
│ └─> 0x0000072e c9 rbp,rsp,=,rsp,[8],rbp,=,8,rsp,+=
└ 0x0000072f c3 rsp,[8],rip,=,8,rsp,+=
Tras entrar en ESIL, las instrucciones aparecen en su formato propio. El conjunto de instrucciones está disponible aquí: https://radare.gitbooks.io/radare2book/disassembling/esil.html
Dentro de ESIL podemos usar aesu para emular la ejecución hasta alcanzar una dirección de memoria concreta:
Por ejemplo, en nuestro programa podemos inspeccionar el XOR byte a byte dentro del bucle con ESIL así:
[0x000006aa]> aesu 0x000006ee
[0x000006d8]> dr
rax = 0x00000061
rbx = 0x00000000
rcx = 0x00000000
rdx = 0x00000039
La operación se ha calculado y los registros de ESIL se han actualizado (obsérvense los códigos ASCII).
En nuestro caso también podemos «saltar» al punto posterior al cifrado e inspeccionar el resultado así:
[0x000006aa]> aesu 0x00000701
[0x000006d8]> pdf
; DATA XREF from entry0 @ 0x5bd
┌ 134: int main (int argc, char **argv, char **envp);
│ ; var signed int64_t var_14h @ rbp-0x14
│ ; var int64_t var_10h @ rbp-0x10
│ ; var char *var_ch @ rbp-0xc
│ ; var int64_t canary @ rbp-0x8
│ 0x000006aa 55 rbp,8,rsp,-,=[8],8,rsp,-=
│ 0x000006ab 4889e5 rsp,rbp,=
│ 0x000006ae 4883ec20 32,rsp,-=,63,$o,of,:=,63,$s,sf,:=,$z,zf,:=,$p,pf,:=,64,$b,cf,:=
│ 0x000006b2 64488b042528. 0x28,[8],rax,=
│ 0x000006bb 488945f8 rax,0x8,rbp,-,=[8]
│ 0x000006bf 31c0 eax,rax,^,0xffffffff,&,rax,=,$z,zf,:=,$p,pf,:=,31,$s,sf,:=,0,cf,:=,0,of,:=
│ 0x000006c1 c745f0616263. 1684234849,0x10,rbp,-,=[4] ; 'abcd'
│ ; 0x64636261
│ 0x000006c8 c745f458595a. 1431984472,0xc,rbp,-,=[4] ; 'XYZU'
│ ; 0x555a5958
│ 0x000006cf c745ec000000. 0,0x14,rbp,-,=[4]
│ ┌─< 0x000006d6 eb23 0x6fb,rip,=
│ │ ; CODE XREF from main @ 0x6ff
│ ┌──> 0x000006d8 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006db 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006dd 0fb65405f4 0xc,rax,rbp,+,-,[1],rdx,=
│ ╎│ 0x000006e2 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006e5 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006e7 0fb64405f0 0x10,rax,rbp,+,-,[1],rax,=
│ ╎│ 0x000006ec 31c2 eax,rdx,^,0xffffffff,&,rdx,=,$z,zf,:=,$p,pf,:=,31,$s,sf,:=,0,cf,:=,0,of,:=
│ ╎│ 0x000006ee 8b45ec 0x14,rbp,-,[4],rax,=
│ ╎│ 0x000006f1 4898 eax,rax,=,31,rax,>>,?{,0xffffffff00000000,rax,|=,}
│ ╎│ 0x000006f3 885405f4 dl,0xc,rax,rbp,+,-,=[1]
│ ╎│ 0x000006f7 8345ec01 1,0x14,rbp,-,+=[4],31,$o,of,:=,31,$s,sf,:=,$z,zf,:=,31,$c,cf,:=,$p,pf,:=
│ ╎│ ; CODE XREF from main @ 0x6d6
│ ╎└─> 0x000006fb 837dec03 3,0x14,rbp,-,[4],==,$z,zf,:=,32,$b,cf,:=,$p,pf,:=,31,$s,sf,:=,31,$o,of,:=
│ └──< 0x000006ff 7ed7 of,sf,^,zf,|,?{,1752,rip,=,}
│ ;-- rip:
│ 0x00000701 488d45f4 0xc,rbp,-,rax,=
│ 0x00000705 4889c6 rax,rsi,=
│ 0x00000708 488d3da50000. 0xa5,rip,+,rdi,= ; str.s
│ ; 0x7b4 ; "%s \n" ; const char *format
│ 0x0000070f b800000000 0,rax,=
│ 0x00000714 e867feffff 1408,rip,8,rsp,-=,rsp,=[],rip,= ; sym.imp.printf ; int printf(const char *format)
│ 0x00000719 90 ,
│ 0x0000071a 488b45f8 0x8,rbp,-,[8],rax,=
│ 0x0000071e 644833042528. 0x28,[8],rax,^=,$z,zf,:=,$p,pf,:=,63,$s,sf,:=,0,cf,:=,0,of,:=
│ ┌─< 0x00000727 7405 zf,?{,1838,rip,=,}
│ │ 0x00000729 e842feffff 1392,rip,8,rsp,-=,rsp,=[],rip,= ; sym.imp.__stack_chk_fail ; void __stack_chk_fail(void)
│ │ ; CODE XREF from main @ 0x727
│ └─> 0x0000072e c9 rbp,rsp,=,rsp,[8],rbp,=,8,rsp,+=
└ 0x0000072f c3 rsp,[8],rip,=,8,rsp,+=
Y también podemos inspeccionar la memoria igual que en una sesión de depuración normal:
[0x000006d8]> pxw @ 0x00177fe8
0x00177fe8 0x64636261 0x31393b39 0x00001960 0x00000000 abcd9;91`.......
0x00177ff8 0x00178000 0x00000000 0x00000000 0x00000000 ................
En la siguiente entrada exploraremos temas similares en un sistema Windows.
Vuelve al binario
Relacionar libc con syscalls y seguir operaciones de archivos y emulación de expresiones. Identifica al menos una instrucción y el dato que utiliza; conserva la compilación con la que lo has observado.
Puedes utilizar la consola del companion sin conversar con el agente, o abrir el ejecutable en tu desensamblador habitual.
./coursectl start 13 --example libc-print
./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.