Únete al equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Contacta con el equipo de Lobera

Consulta nuestra Política de Privacidad sobre el tratamiento de esta información.

Sistemas y comunicaciones / 15

Linux: llamadas al sistema y ESIL

LoberaFormación técnica15 / 33 · 16 min de lectura · Linux · x86 / x86-64
15 / 33

Relacionar libc con syscalls y seguir operaciones de archivos y emulación de expresiones.

Consultar conceptos ↗

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.

C
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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:

ENSAMBLADOR / REFERENCIA
           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:

C
#include <stdio.h>

void main(){

        printf("ssssssssyscall\n");

}

Y dentro de radare vemos un puts, no un write.

C / EJEMPLO
[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í:

CÓDIGO / SALIDA
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.

CÓDIGO / SALIDA
[0x558c82dc075b]> dmh
No Heap section
C
#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

Descriptores y archivos / esquema de lectura
ProcesoTabla de descriptores
Archivo abiertoOffset y estado de apertura
InodoMetadatos del archivo

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:

C
#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.

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[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.

ENSAMBLADOR / REFERENCIA
          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:

CÓDIGO / SALIDA
[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:

C / EJEMPLO
#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:

C / EJEMPLO
[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)

ENSAMBLADOR / REFERENCIA
           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.

CÓDIGO / SALIDA
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.

CÓDIGO / SALIDA
[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:

C / EJEMPLO
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
          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.

ENSAMBLADOR / REFERENCIA
          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:

CÓDIGO / SALIDA
[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.

CÓDIGO / SALIDA
[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:

C / EJEMPLO
#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:

CÓDIGO / SALIDA
[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.

CÓDIGO / SALIDA
[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:

C / EJEMPLO
#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:

C / EJEMPLO
[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:

ENSAMBLADOR / REFERENCIA
                                                                      ; 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:

CÓDIGO / SALIDA
[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:

ENSAMBLADOR / REFERENCIA
           ;-- 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:

CÓDIGO / SALIDA
[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:

ENSAMBLADOR / REFERENCIA
           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:

CÓDIGO / SALIDA
[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:

ENSAMBLADOR / REFERENCIA
[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í:

ENSAMBLADOR / REFERENCIA
      ╎│   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:

CÓDIGO / SALIDA
[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í:

CÓDIGO / SALIDA
[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:

C / 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í:

CÓDIGO / SALIDA
[0x000005a0]> s main
[0x000005a0]> e asm.esil = true
[0x000005a0]> aei
[0x000005a0]> aeim
[0x000005a0]> aeip
C / EJEMPLO
[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í:

CÓDIGO / SALIDA
[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í:

C / EJEMPLO
[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:

CÓDIGO / SALIDA
[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.

Práctica manual

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.

TERMINAL / COMPANION
./coursectl start 13 --example libc-print
./coursectl console

Cierra la sesión anterior con ./coursectl stop antes de cambiar de laboratorio. Ver preparación.

Comprobación personal

Antes de continuar

Explica con tus palabras la operación estudiada y señala las instrucciones o capturas que la justifican. Distingue los datos observados de los nombres y tipos que has reconstruido.

El progreso se guarda en este navegador. Puedes recorrer las lecciones en cualquier orden.

Código original: AGPLv3. Textos y diagramas originales: CC BY-SA 4.0. Reutilización y alcance ↗.

Lobera

Conceptos de reversing

Consulta una definición y continúa donde estabas.