Top.Mail.Ru
? ?
En seguridad ofensiva una de las principales actividades a realizar es generar shellcodes que sean ejecuten ataques exitosos para poder generar las contramedidas necesarias, sin embargo, escribir shellcodes consumen tiempo y son un desafío técnico, ya que están escritos en lenguaje assembly. En este trabajo, Pietro Liguori, Erfan Al-Hossami, Domenico Cotroneo, Roberto Natella, Bojan Cukic y Samira Shaikhb presentan un método para generar shellcodes automáticamente, partiendo puramente de descripciones en lenguaje natural, proponiendo un enfoque basado en la Neural Machine Translation (NMT). Presentan un estudio empírico utilizando un nuevo conjunto de datos (Shellcode_IA32), que consiste en 3200 fragmentos de código assembly de shellcodes reales de Linux/x86 de bases de datos públicas, anotados utilizando lenguaje natural. Además, proponen métricas novedosas para evaluar la precisión de NMT en la generación de shellcodes. El análisis empírico muestra que NMT puede generar fragmentos de código assembly desde el lenguaje natural con alta precisión y que en muchos casos puede generar shellcodes completos sin errores.
Básicamente siguiendo trabajos previos construyen una red neuronal que modela directamente la probabilidad condicional de traducir una intención, en lenguaje natural en un fragmento de código en lenguaje assembly. El principal desafío hacia el objetivo de generar automáticamente shellcodes está representado por el lenguaje de programación, es decir, assembly. Este lenguaje es significativamente diferente de otros lenguajes abordados hasta ahora por la investigación sobre NMT, que se centró hasta ahora en los principales lenguajes imperativos como Python y Java. Assembly es un lenguaje de programación de bajo nivel con muchas diferencias sintácticas con respecto a estos lenguajes. Por ejemplo, el assembly no proporciona el concepto de variable, que en su lugar se reemplaza por registros, direcciones de memoria, modos de direccionamiento y etiquetas. Además, algunas construcciones de programación en assembly requieren múltiples instrucciones, que en su lugar podrían expresarse con una sola instrucción de otros lenguajes de programación. Para abordar este nuevo lenguaje para NMT, optaron por basar su solución en las arquitecturas de redes neuronales profundas existentes: Seq2Seq with Attention, y CodeBERT.
Seq2Seq es un modelo común utilizado en una variedad de tareas de traducción automática neuronal. Similar a la arquitectura codificador-decodificador con el mecanismo de atención de Bahdanau Bahdanau, utilizaron un LSTM bidireccional como codificador, para transformar una secuencia de intención incrustada en un vector de estados ocultos con igual longitud.  Dentro del codificador LSTM bidireccional, cada estado oculto corresponde a un token incrustado. El codificador LSTM es bidireccional, lo que significa que lee la secuencia de origen ordenada de izquierda a derecha y de derecha a izquierda. Para combinar ambas direcciones, cada estado oculto para el codificador LSTM bidireccional se calcula concatenando los estados ocultos hacia adelante y hacia atrás en el codificador.
CodeBERT es una gran arquitectura de transformador bidireccional multicapa.  Al igual que Seq2Seq, la arquitectura Transformer se compone de codificadores y decodificadores. CodeBERT tiene 12 codificadores apilados y 6 decodificadores apilados. En comparación con Seq2Seq, la arquitectura Transformer introduce mecanismos para abordar cuestiones clave en la traducción automática: (i) la traducción de una palabra depende de su posición dentro de la oración; ii) en el lenguaje de destino, el orden de las palabras (por ejemplo, adjetivos antes de un sustantivo) puede ser diferente del orden de las palabras en el lenguaje de origen (por ejemplo, adjetivos después de un sustantivo); (iii) varias palabras en la misma oración pueden ser correlacionadas (por ejemplo, pronombres). Estos problemas son especialmente importantes cuando se trata de oraciones largas.
Este trabajo representa un primer paso hacia el ambicioso objetivo de generar automáticamente shellcodes a partir del lenguaje natural, proporciona datos recopilados originalmente, permite la replicación y describe los éxitos y desafíos a través de una evaluación rigurosa.
Este paper está bien explicativo de como construir un JOP, tuve que dedicar bastante tiempo para seguir el algoritmo, pero es un buen candidato para replicar. Los autores proponen una programación orientada a saltos (JOP) para mejorar la programación orientada al retorno (ROP) existente sin técnicas de retorno. Además, presentan una herramienta que puede construir automáticamente el código shell de programación orientado a saltos (JOP). En particular, hacen las siguientes contribuciones:

  • Proponen una mejora de las técnicas de ROP. En comparación con la programación orientada al retorno de última generación sin retornos (propuesta por Checkoway et all.), este método es más factible para construir los programas, sin el tedioso trabajo manual. En particular, utilizan el gadget combinado para invocar las llamadas del sistema y un gadget de control para establecer el registro de salto.

  • Proponen técnicas para construir automáticamente los gadgets, el bloque de construcción básico en programación orientada a saltos (JOP), y muestran que estos gadgets también son Turing completo.

  • Han implementado estas técnicas en una herramienta, y lo han aplicado para construir automáticamente un gran número de código de shell de jump-oriented programming (JOP) del mundo real a partir de milw0rm. Los resultados experimentales muestran que esta herramienta puede construir eficientemente el shellcode en pocos segundos.

Este paper viene a superar algunas deficiencias presentadas en el paper de Checkoway y otros y que deben diseñarse manualmente para construir un shellcode o incluso inviables para construir un shellcode sofisticado.
En particular proponen un método para encontrar gadget JOP que sean Turing completo en dos librerías específicas libc-2.3.5.so (biblioteca C) y libgcj.so.5.0.0 (una biblioteca de tiempo de ejecución Java).
En este diseño, se selecciona principalmente gadgets de control en forma de "pop-jmp" o "mov-jmp" para establecer el registro de salto (esto me parece muy parecido al Gadget dispatch lo propuesto por Bletch en paper que leí anteriormente); y gadgets de función que se pueden utilizar para lograr el movimiento de datos, la operación aritmética de datos, la operación lógica, la bifurcación incondicional, la rama condicional, la llamada al sistema y la llamada a la función.
Sin embargo, proponen que los gadgets JOP propuestos, son tan potentes como los gadgets ROP y deducen que se podría encontrar estos gadgets tienen una funcionalidad idéntica que el gadget ROP propuesto en por Shacham. Por lo tanto, asumen que este gadget JOP es Turing completo.
En principio pensé que esta técnica es igual a la propuesta por Bletch, pero lo aclaran en el mismo paper. Bletsch et al. [1] propusieron la técnica de Jump-oriented programming, pero aquí hay dos diferencias de método. En primer lugar, Bletsch et al. aprovechan el gadget que termina en la instrucción de llamada independiente, que no tiene ninguna instrucción ret correspondiente. Y en segundo lugar En segundo lugar, los gadget de despachadores propuestos por Bletsh pueden conectar sólo unos pocos gadgets, porque no proporcionan el método de cómo configurar los registros de salto distintos de los de Dispatcher Gadgets. Pero lo que me llamó la atención, es que los autores afirman que herramientas de defensa ROP existentes (por ejemplo, ROPdefender) se puede modificar fácilmente para detectar su código de shell JOP con instrucciones de llamada independientes mediante la supervisión del desequilibrio en la relación entre la llamada ejecutada y el ret, algo que Bletch dijo que no sucedía en el paper anterior.
Me pregunto si la generación automática de gadget puede ser enfrentada con programación genética (pero no, no me desviaré de mi objetivo)

Referencia:

  1. P. Chen, X. Xing, B. Mao, L. Xie, X. Shen, y X. Yin, «Automatic construction of jump-oriented programming shellcode (on the x86)», en Proceedings of the 6th ACM Symposium on Information, Computer and Communications Security, New York, NY, USA, mar. 2011, pp. 20-29. doi: 10.1145/1966913.1966918.

Referencias adicionales.

  1. S. Checkoway, L. Davi, A. Dmitrienko, A.-R. Sadeghi, H. Shacham, and M. Winandy, “Return-oriented programming without returns," in Proceedings of CCS 2010, A. Keromytis and V. Shmatikov, Eds. ACM Press, Oct. 2010, pp. 559{72.

  2. H. Shacham, “The geometry of innocent esh on the bone: return-into-libc without function calls (on the x86)," in Proceedings of the 14th ACM Conference on Computer and Communications Security(CCS). New York, NY, USA: ACM, 2007, pp. 552{561.

  3. T. Bletsch, X. Jiang, V. W. Freeh, y Z. Liang, «Jump-oriented programming: a new class of code-reuse attack», en Proceedings of the 6th ACM Symposium on Information, Computer and Communications Security, New York, NY, USA, mar. 2011, pp. 30-40. doi: 10.1145/1966913.1966919.

Un shellcode inyectable

Como se dijo en post anterior el lugar más probable en el que colocará shellcode es en un búfer asignado para la entrada del usuario. Aún más probable, este búfer será una matriz de caracteres. Asumamos que tenemos el siguiente shellcode:

char shellcode[]"\xbb\x00\x00\x00\x00"
                    "\xb8\x01\x00\x00\x00"
                    "\xcd\x80";
int main()
{
  int *ret;
ret = (int *)&ret + 2;
  (*ret) = (int)shellcode;
}

En shellcode[] observará que hay algunos valores NULL (x00) presentes. Estos valores NULL harán que shellcode falle cuando se inserta en una matriz de caracteres porque el carácter nulo se utiliza para terminar cadenas. Se necesita ser un poco creativos y encontrar maneras de cambiar los valores NULL en códigos de operación no nulos. Hay dos métodos populares para hacerlo. La primera consiste simplemente en reemplazar las instrucciones de assembly que crean valores NULL con otras instrucciones que no lo hacen. El segundo método es un poco más complicado: implica agregar valores NULL en tiempo de ejecución con instrucciones que no crean valores NULL. Este método también es complicado porque tendremos que saber la dirección exacta en la memoria donde se encuentra nuestro shellcode. Encontrar la ubicación exacta del shellcode implica usar otro truco.
Podemos hacer una traza de las tres instrucciones de assembly y los códigos de operación correspondientes:
mov ebx,0             \xbb\x00\x00\x00\x00         
mov eax,1             \xb8\x01\x00\x00\x00                
int 0x80                 \xcd\x80  
Las dos primeras instrucciones son responsables de crear los valores NULL. En assembly, la instrucción OR exclusivo (xor) devolverá cero si ambos operandos son iguales. Esto significa que si usamos la instrucción OR exclusiva en dos operandos que sabemos que son iguales, podemos obtener el valor de 0 sin tener que usar un valor de 0 en una instrucción. Por lo tanto, no tendremos que tener un código de operación nulo. En vez de usar la instrucción mov para establecer el valor de EBX en 0, se usará la instrucción OR exclusivo (xor). Por lo tanto, la primera instrucción:
mov ebx,0
Se convierte en:
xor ebx,ebx    

Se espera que una de las instrucciones se haya eliminado de nulls.
Al ser esta una arquitectura de 32 bits (en otra ocasión trabajaré en 64 bits) el registro EAX tiene espacio para cuatro, recuerde que estamos moviendo sólo un byte en el registro. El resto del registro se va a llenar con nulls para compensar.
Esto se puede evitar recordando que cada registro de 32 bits se divide en dos "áreas" de 16 bits; se puede acceder al área de 16 bits con el registro AX. Además, el registro AX de 16 bits se puede desglosar aún más en los registros AL y AH.  Si solo desea los primeros 8 bits, puede utilizar el registro AL.  Por lo tanto, el valor binario de 1 ocupará sólo 8 bits, por lo que se puede encajar el valor en este registro y evitar que EAX se llene con nulls. Para ello cambiamos nuestra instrucción original
mov eax,1
a uno que usa AL en vez de EAX:
mov al,1
Con esto se resuelve el tema de los valores nulos, por loq ue el assembly final quedaría así:
Section      .text
global _start
_start:
xor ebx,ebx   
mov al,1
int 0x80

Y desensamblamos usando objdump:

jar@debian:~/exploit$ nasm -f elf exit_shellcode_real.asm
jar@debian:~/exploit$ ld -o exit_shellcode_real exit_shellcode_real.o
jar@debian:~/exploit$ objdump -d exit_shellcode_real

exit_shellcode_real:     formato del fichero elf32-i386

Desensamblado de la sección .text:

08049000 <_start>:
8049000:       31 db                     xor    %ebx,%ebx
8049002:       b0 01                   mov    $0x1,%al
8049004:       cd 80                      int    $0x80
jar@debian:~/exploit$

Todos los códigos de operación nulos se han eliminado, y hemos reducido significativamente el tamaño de nuestro código de shell. Ahora usted tiene completamente trabajando, y lo que es más importante, un shellcode inyectable.

Referencias:
[1] The Shellcoder’s Handbook: Discovering and Exploiting Security Holes (1st Edition) was written by Jack Koziol, David Litchfield, Dave Aitel, Chris Anley, Sinan Eren, Neel Mehta, and Riley Hassell.
[2] The Design and Implementation of the 4.4 BSD Operating System. Authors: Marshall McKusick, Keith Bostic, Michael Karels, John Quarterman.
[3] Beginning x64 Assembly Programming. From Novice to AVX Professional. Authors: Jo Van Hoey

Como funciona syscall

Shellcode es un término que, desde su origen, específica un exploit utilizado para generar un shell raíz. Estos artefactos se colocan en la entrada (de un programa) y, a continuación, se engaña al programa para que ejecute el shellcode suministrado. Entender un shellcode es esencial para poder determinar si una vulnerabilidad es explotable o no.
Por otra parte Syscalls es un conjunto extremadamente potente de funciones que permiten acceder al sistema operativo, funciones específicas como obtener entrada, producir salida, salir de un proceso y ejecutar un archivo binario son prerrogativas que proporciona syscall. También permite acceder directamente al kernel, lo que proporciona acceso a funciones de nivel inferior como leer y escribir archivos. Si recordamos el curso de sistemas operativos las llamadas de sistema son la interfaz entre el modo kernel protegido y el modo de usuario. La implementación de un modo de kernel protegido, en teoría, evita que las aplicaciones de usuario interfieran o comprometan el sistema operativo. Cuando un programa de modo de usuario intenta acceder al espacio de memoria del kernel, se genera una excepción de acceso.
Los syscalls se implementaron como una interfaz entre el modo de usuario normal y el modo kernel.

En una entrada anterior ya he hablado un poco sobre la administración de memoria, que es fundamental entender antes. Desempolvar los viejos libros de Sistemas Operativos es también una buena idea.
Hay dos métodos comunes de ejecutar una syscall en Linux:

  • Biblioteca libc: Muy utilizada para ataques return-into-libc, donde el atacante sobrescribe la dirección de retorno de una función vulnerable con la dirección de cualquier función de la biblioteca libc.

  • llamada sys: Se puede hacer una llamada a sys directamente con assembly. cargando los argumentos adecuados en registros y, a continuación, llamando a una interrupción de software.

Los contenedores Libc se crearon para que los programas puedan seguir funcionando (cuando cambia un sys por ejemplo) y para proporcionar algunas funciones como malloc.
Llegado a este punto es importante señalar algunos aspectos de Assembly. Un computador no hace distinción entre instrucciones y datos. Si un procesador puede ser nutrido con instrucciones cuando debe estar recibiendo datos, el procesador ejecutará las instrucciones sin cuestionarlas. Esto hace posible la explotación de sistemas. En IA32 los registros de uso general para operaciones matemáticas incluyen registros EAX, EBX y ECX, y se utilizan para almacenar datos y direcciones, direcciones de desplazamiento, realizar funciones de recuento y muchas otras cosas. El Extended Stack Pointer (ESP) señala la dirección de memoria donde tendrá lugar la siguiente operación de stack. Para entender un desbordamiento de stack se debe comprender cómo se utiliza ESP con instrucciones de assembly y el efecto que tiene en los datos del stack, por otra parte, el Extended Instruction Pointer (EIP) contiene la dirección de la siguiente instrucción de máquina que se va a ejecutar, si desea controlar la ruta de ejecución de un programa, es importante tener la capacidad de acceder y cambiar el valor almacenado en el registro EIP. Por último, Extended Flags (EFLAGS) comprende muchos registros de un solo bit que se utilizan para almacenar los resultados de varias pruebas realizadas por el procesador.
Comprender esto es fundamental para la programación assembly y la explotación de sistemas.
Las llamadas del sistema en Linux se realizan a través de interrupciones de software y se llaman con la instrucción int 0x80
El proceso funciona de la siguiente manera:

  1. El número de llamada sys específico se carga en EAX.

  2. Los argumentos de la función syscall se colocan en otros registros.

  3. Se ejecuta la instrucción int0x80.

  4. La CPU cambia al modo kernel.

  5. Se ejecuta la función syscall.

Un valor entero específico se asocia a cada syscall; este valor debe colocarse en EAX. Cada syscall puede tener un máximo de seis argumentos, que se insertan en EBX, ECX, EDX, ESI, EDI y EPB, respectivamente. Si se requieren más de los seis argumentos de stock para syscall, los argumentos se pasan a través de una estructura de datos al primer argumento.
El syscall más básico que podemos ejecutar es exit(). Esta instrucción finaliza el proceso actual. Podemos crear entonces el siguiente programa en C:

#include<stdlib.h>
int main() {
      exit(0);
}
Luego compile este programa utilizando gcc con la opción -static, para evitar la vinculación dinámica:

$gcc –static –o exit exit.c

Y luego desensamble el programa binario obtenido:

jar@debian:~$ gdb exit
GNU gdb (Debian 8.2.1-2+b3) 8.2.1
Copyright (C) 2018 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 "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
  <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from exit...(no debugging symbols found)...done.
(gdb) disas _exit
Dump of assembler code for function _exit:
0x0806c553 <+0>:     mov    0x4(%esp),%ebx
0x0806c557 <+4>:     mov    $0xfc,%eax
0x0806c55c <+9>:     call   *%gs:0x10
0x0806c563 <+16>:    mov    $0x1,%eax
0x0806c568 <+21>:    int    $0x80
0x0806c56a <+23>:    hlt   
End of assembler dump.
(gdb)
Al analizar la salida de gdb podemos ver que tenemos una instrucción que carga el argumento a nuestra llamada syscall en EBX y tiene el valor cero.
0x0806c553 <+0>:     mov    0x4(%esp),%ebx
El valor de la llamada de sistema se almacena en EAX en líneas 0x0806c557 <+4> y 0x0806c563 <+16>:
0x0806c557 <+4>:     mov    $0xfc,%eax
0x0806c563 <+16>:    mov    $0x1,%eax
Finalmente, tenemos la instrucción int 0x80, que cambia la CPU al modo kernel que es el que ejecuta realmente el syscalls:
0x0806c568 <+21>:    int    $0x80
Todo esto es lo que hace assembly cuando ejecutamos una simple llamada exit() a syscall.

Referencias:

  1. The Shellcoder’s Handbook: Discovering and Exploiting Security Holes (1st Edition) was written by Jack Koziol, David Litchfield, Dave Aitel, Chris Anley, Sinan Eren, Neel Mehta, and Riley Hassell.

  2. The Design and Implementation of the 4.4 BSD Operating System. Authors: Marshall McKusick, Keith Bostic, Michael Karels, John Quarterman.

  3. Beginning x64 Assembly Programming. From Novice to AVX Professional. Authors: Jo Van Hoey

Administración de memoria

Cuando se ejecuta un programa, se presenta de forma organizada: varios elementos del programa se asignan a memoria. En primer lugar, el sistema operativo crea un espacio de direcciones en el que se ejecutará el programa. Este espacio de direcciones incluye las instrucciones reales del programa, así como los datos necesarios. A continuación, se carga información desde el archivo ejecutable del programa al espacio de direcciones recién creado. Hay tres tipos de segmentos: .text, .bss y .data. El segmento .text se asigna como de solo lectura, mientras que .data y .bss se pueden escribir. Los segmentos .bss y .data están reservados para variables globales. El segmento .data contiene datos inicializados estáticos y el segmento .bss contiene datos no inicializados. El segmento final, .text, contiene las instrucciones del programa. Por último, se inicializan el stack y el heap. El stack es una estructura de datos, más específicamente una estructura de datos Last In First Out (LIFO), lo que significa que los datos más recientes colocados o insertados en el stack es el siguiente elemento que se va a quitar o extraer de la pila. Una estructura de datos LIFO es ideal para almacenar información transitoria o información que no necesita almacenarse durante un largo período de tiempo. El stack almacena variables locales, información relacionada con llamadas de funciones y otra información utilizada para limpiar el stack después de llamar a una función o procedimiento. Otra característica importante del stack es que decrece (grows down) en el espacio de direcciones: a medida que se agregan más datos al stack, se agrega a valores de dirección cada vez más bajos. Heap es otra estructura de datos utilizada para contener información del programa, más específicamente, variables dinámicas. Heap es (aproximadamente) una estructura de datos First In First Out (FIFO). Los datos se colocan y se quitan del heap a medida que se compilan. El Heap aumenta el espacio de direcciones: a medida que se agregan datos al heap, se agrega a un valor de dirección cada vez más alto.

Referencias:

  1. The Shellcoder’s Handbook: Discovering and Exploiting Security Holes (1st Edition) was written by Jack Koziol, David Litchfield, Dave Aitel, Chris Anley, Sinan Eren, Neel Mehta, and Riley Hassell.

  2. The Design and Implementation of the 4.4 BSD Operating System (paperback) (Addison-wesley Unix and Open Systems Series) 1st Edition

Acerca de Juan Anabalón

deoxyt2
Juan Rodrigo Anabalón R.
He estado escribiendo sobre temas de ciberseguridad en Livejournal desde el 2008 y en mi horrible y extinto MSN Spaces desde el 2006. Soy profesor de ciberseguridad en
Universidad San Sebastián, Presidente en ISSA Chile capitulo chileno de ISSA International y fundador de MonkeysLab.
También tomo y publico fotos en flickr y 500px
Este sitio web es personal y no expresa la opinión de esas organizaciones.




MonkeysLab




Copyleft
Copyleft: Atribuir con enlace.




Sindicar

RSS Atom

Tags

Powered by LiveJournal.com
Designed by Lilia Ahner