Por lo general, escribo programas de ensamblaje y luego vuelco su contenido para obtener el código de shell. Lo que me preguntaba es si podría escribir un programa en C ++ en lugar de un ensamblado, luego volcar y usar ese en lugar del ensamblado?
El kernel de Linux se puede ver como un tipo de código de shell definitivo, ya que se "inyecta" en un crudo (que solo tiene el código BIOS en ese punto) y luego proporciona una gran cantidad de funcionalidad. Ese kernel está escrito en C.
Si escribe el código de shell en C o C ++, se encontrará con problemas con llamadas de biblioteca y vinculación , que son dos facetas del mismo problema.
El código producido por un compilador de C puede contener referencias a otras partes del código, o funciones o variables externas. Por ejemplo, si accede a una variable static
, incluso si está definida en su propio código, entonces los códigos de operación del ensamblaje contendrán un "agujero": en el nivel del ensamblaje (el que obtiene con gcc -S
, si usa gcc como Compilador C), vería los códigos de operación "mov" básicos y el nombre de la variable. Cuando se traduce a binario, en el archivo objeto ( .o
o .obj
, dependiendo de la plataforma), el agujero aún no se ha llenado, porque la dirección real de la variable aún no se conoce; el archivo de objeto contiene una tabla (las "reubicaciones") que informan al vinculador de la posición de cada agujero que se va a rellenar en la etapa de enlace.
El problema, por supuesto, es que no tienes el lujo de un enlazador al crear un código de shell. Tiene que producir una secuencia de bytes que ya está lista, sin ningún agujero que rellenar y que puede ejecutarse en la dirección en la que se cargará, dirección que no es necesariamente conocida de antemano (gracias, en parte, a ASLR ). Debe escribir código independiente de la posición . Los compiladores a menudo ofrecen una marca de línea de comando para eso (por ejemplo, -fPIC
con gcc
) pero esto no es PIC "verdadero": ese tipo de código todavía tiene agujeros que rellenar con un enlazador, pero esta vez el dinámico enlazador; es PIC solo en virtud de que los agujeros se reagruparon en una tabla especial (GOT, también conocida como "Tabla de compensaciones globales"), lo que facilita la tarea para el vinculador dinámico.
Desafortunadamente, los compiladores de C y los compiladores de C ++ aún más, generarán código con agujeros sin su consentimiento. Por ejemplo, un compilador típico de C producirá código con llamadas ocultas a memcpy()
(la función de biblioteca estándar) cuando se trata de valores de struct
. C ++ hace que sea peor debido a toda la parafernalia de las características de C ++, que son compatibles con funciones ocultas y variables estáticas (por ejemplo, el objeto vtables).
El kernel de Linux resuelve estos problemas usando la MMU . El código de carga de inicio (que es una pieza del ensamblaje escrito a mano) toma el control de la MMU para asegurarse de que las páginas de RAM donde se encuentra el código del kernel se vean en una dirección conocida y fija en el espacio de direcciones: el kernel, cuando compilado, está vinculado con ese supuesto explícito; los orificios están llenos de direcciones que son correctas solo si el núcleo está de hecho en esa posición exacta en el espacio de direcciones. Jugar con la MMU es un privilegio del kernel, no puede hacerlo desde el código de usuario, y mucho menos desde un código de shell.
Podría imaginar hacer que su código de shell sea una pieza de ensamblaje que sea, de hecho, un enlazador dinámico, capaz de cargar un pedazo de código binario producido por un compilador C o C ++, parcheando dinámicamente, es decir, rellenando los "agujeros" con la dirección de carga real. Eso es lo que ocurre en los exploits provistos con metasploit : el exploit mismo instala ese pedazo de código ingenioso, que luego puede instalar "cargas útiles" que son DLL genérico, escrito en lenguajes de nivel superior, hasta e incluyendo un servidor VNC completo.
Sin embargo, como lo pones, tendrás que hacer algún código PIC en ensamblaje manual en algún momento. Escribir su propio cargador / enlazador es un gran ejercicio pedagógico, y se puede utilizar para aprovechar una explotación inicial a niveles de control arbitrarios, pero en su núcleo, aún será necesario el ensamblaje hecho a mano.
Es perfectamente válido escribir shellcode en cualquier idioma que se compile siguiendo las instrucciones del código de máquina. Siempre que no se requieran bibliotecas externas que no estén vinculadas por el programa víctima para su funcionamiento.
Sin embargo, casi nunca es el caso de que el código compilado directamente (incluso desde solo C) sea un código de shell inyectable válido. La razón más común sería el requisito de eliminar NULL
bytes en una inyección de cadena de búfer.
El código compilado también estará inflado, realizando varias tareas de tipo 'contabilidad', como prólogos de funciones, configuración de cuadros de pila y menú desplegable, etc. Todo esto hace que sea una mala elección para el código de shell, que normalmente tiene que ser muy pequeño. p>
Por lo tanto, deberá escribir su programa, compilarlo, y luego sintonizarlo a nivel de código de máquina . En realidad, este es un enfoque bastante común que muchas personas toman y que pueden encontrar un poco complicado crear un ensamblaje desde cero.
Sin embargo, en mi experiencia, terminará jugando con el Shellcode final tanto que estará muy familiarizado con cada byte que contiene antes de que termine, y probablemente habría sido más rápido usar el ensamblaje directamente, desde el inicio.
Por supuesto, si está intentando realizar una tarea grande y compleja, entonces escribirla en un lenguaje de nivel superior puede ser atractivo. En realidad, sin embargo, el mejor enfoque suele ser crear un simple stager shellcode, que es inyectado por el exploit. El stager descarga la carga útil real y la ejecuta. Esto permite que la carga útil real sea un ejecutable independiente y complejo que vincula sus propias bibliotecas a voluntad.
Recuerde, por supuesto, de dónde viene el nombre; un código de shell generalmente está destinado a realizar la tarea de un stager, con el stager arquetípico siendo execve("/bin/sh", null, null).
Como señala TildalWave, incluso al compilar desde C ++, es posible tener un control preciso sobre el código de máquina de salida. La forma más directa es incluir instrucciones de ensamblaje en línea dentro de la fuente de C ++, por ejemplo;
__asm__ ("movl %eax, %ebx\n\t"
"movb %ah, (%ecx)");
También puede modificar las directivas del compilador y, al compilar el código que se va a editar, debería desactivar siempre todas las optimizaciones del compilador . Pocas cosas son más confusas que las cosas extrañas que hacen los compiladores para reducir la cantidad de instrucciones.
Lea otras preguntas en las etiquetas penetration-test c++ assembly shellcode