Creacion de shellcodes en linux
Table of contents
Veremos a como puedes crear shellcodes, te enseñare como crear una usando la syscall write() y otra usando execve() para ejecutar bin/sh
Shellcodes
Las shellcodes son un conjunto de instrucciones de bajo nivel (ensamblador) que se inyectan en la memoria de un programa con el fin de ejecutar codigo
Actualemente son usadas ampliamente para ejecutar codigo malicioso, de hecho, si alguna vez explotaste un buffer overflow stack-based, estoy seguro de que las usaste al menos un vez, por otra parte, tambien son muy usadas en el desarrollo de malware, ya que hay muchas tecnicas diferentes para inyectarlas, al fin de cuenta, con las shellcodes puedes ejecutar casi cualquier cosa o cualquier cosa, y esto es gracias a la syscalls
Syscalls
Las syscalls son metodos establecidos por el sistema operativo para decirle al kernel que realice una tarea, por ejemplo, tenemos este codigo:
#include <stdio.h>
void main() {
printf("ola\n");
}
Es muy simple, de hecho, podemos decir que nomas tiene 3 intrucciones, el include
, printf
y el void main()
. pero la cantidad de syscalls que hace es increible, usaremos el comando strace
para interceptar y registrar todas las syscalls que hace nuestro binario: strace ./binario
, y esto arroja:
execve("./prueba", ["./prueba"], 0x7ffc3236ede0 /* 57 vars */) = 0
brk(NULL) = 0x5555f3c59000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffe25dc3f10) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_size=167079, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 167079, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fde591cf000
close(3) = 0
openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\20:\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
newfstatat(3, "", {st_mode=S_IFREG|0755, st_size=1961632, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fde591cd000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
mmap(NULL, 2006640, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fde58fe3000
mmap(0x7fde59005000, 1429504, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7fde59005000
mmap(0x7fde59162000, 360448, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x17f000) = 0x7fde59162000
mmap(0x7fde591ba000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1d6000) = 0x7fde591ba000
mmap(0x7fde591c0000, 52848, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fde591c0000
close(3) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fde58fe1000
arch_prctl(ARCH_SET_FS, 0x7fde591ce640) = 0
set_tid_address(0x7fde591ce910) = 60068
set_robust_list(0x7fde591ce920, 24) = 0
rseq(0x7fde591cef60, 0x20, 0, 0x53053053) = 0
mprotect(0x7fde591ba000, 16384, PROT_READ) = 0
mprotect(0x5555f2670000, 4096, PROT_READ) = 0
mprotect(0x7fde59229000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
munmap(0x7fde591cf000, 167079) = 0
newfstatat(1, "", {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x1), ...}, AT_EMPTY_PATH) = 0
getrandom("\x52\x39\x5a\x38\x9f\x83\x1b\x9e", 8, GRND_NONBLOCK) = 8
brk(NULL) = 0x5555f3c59000
brk(0x5555f3c7a000) = 0x5555f3c7a000
write(1, "ola\n", 4ola
) = 4
exit_group(4) = ?
+++ exited with 4 +++
Todas estas syscalls es lo que hace nuestro simple binario por detras, y nosotros no vemos este funcionamiento, solo vemos que imprime la cadena ola.
A lo que quiero llegar es que muchas de las instrucciones que hacemos en los lenguajes de programacion, tienen una syscall asociada, ya sea C, Python, Rust, Golang, etc, y es importante conocer las syscalls, por que cuando se desarrollan shellcodes, se llaman directamente a las sycalls tomando en cuenta su estructura.
Para llamar a syscalls desde lenguaje ensamblador se toman en cuenta las calling conventions, asi que para llamar a uns syscall se hace de esta forma syscall(rdi,rsi,rdx,r10,r8,r9)
, recuerda que los primeros 6 argumentos se pasan por los registos rdi,rsi,rdx,r10,r8,r9, y lo demas se pasa por el stack
Shellcode usando write()
Esta syscall es usada para imprimir algo por la salida estandar (stdout), tiene la estructura:
ssize_t write(int fd, const void *buf, size_t count);
int fd: Es el descriptor de archivos donde se va a escribir, se puede establecer como 1(stdout), 2(sdterr), o cualquier otro descriptor que se abrio
void *buf: Es un puntero al buffer desde donde se empezaran a leer los datos
size_t count: Es el numero de bytes que se van a escribir
Ahora toca programiar en ensamblador:
Primeramente vamos a definir el inicio de nuestro programa, y lo hare de esta manera:
section .text
global _start:
_start:
Con section .text
le estoy diciendo que las instrucciones que le siguen, o apartir de ese punto, se van a almacenar en la seccion .text
, y eso es asi, por que esa seccion contiene las instrucciones que se van a ejecutar.
Con global _start
le estoy diciendo que exporte los simbolos a partir de _start
, para que sean visiable desde fuera del archivo, y se hace con el fin de que puedan ser leidos por el enlazador (ld).
Y dentro de _start
va todo nuestro codigo, y el programa completo se veria asi:
section .text
global _start:
_start:
mov rax, 1
mov rsi, 0x616e757a614e
push rsi
mov rsi, rsp
mov rdx, 6
syscall
mov rax, 60
syscall
Con
mov rax, 1
estamos copiando el numero 1, al registrorax
, es 1, por que la syscall write esta asociada a ese numero en GNU/Linuxmov rsi, 0x616e757a614e
le estamos diciendo que copie el valor que queremos mostrar (0x616e757a614e), al registrorsi
, y en este caso,rsi
va a funcionar como puntero al valor que se va a escribirpush rsi
Esta poniendo el registrorsi
en el stack, con el fin de que la syscall pueda acceder a lo que queremos imprimirmov rsi, rsp
Aqui le indicamos que copie la direccion delrsp
al registrorsi
, y ahora si,rsi
apunta al valor que vamos a imprimir el cual esta en el stackmov rdx, 6
Le estamos indicando el tamaño en bytes de lo que se va a mostrar, asi que copia el tamaño de 6 bytes al registro rdxsyscall
se hace el llamado a writeLas ultima dos instrucciones se hace el llamado a la syscall exit para que todo termine correctamente
Ojo
Te podras dar cuenta que no se usa el registro rdi, que para write, es usado para el descriptor de archivos, y diras: “Como es posible eso, si en la estructura de write se debe de establecer el registro rdi en 1, de hecho y aca lo dice”. Y pues no necesariamente, por que el descriptor de archivos ya tiene establecido por defecto el valor 1 para stdout.
Ahora procedemos a compilar: nasm -f elf64 shellcode.asm
Esto nos dejara un archivo .o el cual debemos de enlazar: ld -m elf_x86_64 -s -o write shellcode.o
Estos dos comandos nos dejo un binario elf, el cual si ejecutamos, se ejecutara la syscall write, y mostrara el texto:
Y a toda madre, funciona, pero eso no es una shellcode, es un binario elf, y si lo vemos en un editor hexadecimal podemos ver la shellcode en hexadecimal
Asi que para pasarla a un byte array, que seria la forma en la que se inyectan, primero usaremos
objcopy -j .text -O binary shellcode.o write.bin
para generar un archivo .binhexdump -v -e '"\\" 1/1 "x%02x"' write.bin; echo
para imprimir el byte array
Y ahora si, esto si es una shellcode a como estamos acostumbrados a verlas:
\xb8\x01\x00\x00\x00\x48\xbe\x4e\x61\x7a\x75\x6e\x61\x00\x00\x56\x48\x89\xe6\xba\x06\x00\x00\x00\x0f\x05\xb8\x3c\x00\x00\x00\x0f\x05
Shellcode para conseguir una shell
Ahora usaremos execve para poder ejecutar /bin/sh, y que consigamos una shell, el codigo es el siguiente:
section .text
global _start
_start:
xor rdx, rdx
push rdx
mov rax, 0x68732f2f6e69622f
push rax
mov rdi, rsp
push rdx
push rdi
mov rsi, rsp
xor rax, rax
mov al, 0x3b
syscall
Explicare directamente lo que esta dentro de _start
por que lo demas ya saben que hace
xor rdx, rdx
El registordx
haciendo una operacion XORing consigo mismo, esto es para que el registro este limpio antes de usarlo, es buena practica nomas, si se le quita no pasa nadapush rdx
Metemos un null bye al stack para indicar el final de la cadena que se le pasara como argumento a execvemov rax, 0x68732f2f6e69622f
Copiamos /bin/sh al registro rax (la cadena esta en hex)push rax
Metemosrax
al stack para que pueda ser accedida por elrsp
mov rdi, rsp
Copiamos la direccion de /bin/sh enrdi
, la cual sera el primer argumento de execve, en ese momento elrsp
apunta a la direccion de /bin/shpush rdx
Metemos otro byte nulo al stackpush rdi
Metemos la direccion de /bin/sh en el stackmov rsi, rsp
Copiamos la direccion delrsp
enrsi
, esto funcionara como segundo argumento de execve, que representa el entorno del programa que se ejecutaraxor rax, rax
Lo mismo que el primerxor
mov al, 0x3b
Copiamos el valor 0x3b que representa el numero de la syscall, al registroal
que funciona como un byte inferior derax
, y por ultimo se ejecuta la syscall
Si volvemos a hacer todo el tramite de compilar y enlazar, ya tendremos nuestro ejetutable que nos da una shell, si vemos la shellcode en hex, todo esta perfecto:
Y si ejecutamos, nos da una shell:
Y listo, tenemos otra shellcode:
\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05
Y como ultimo, estos dos codigos que te explique, no es la unica forma de llamar a write o execve, ya depende del creador y del contexto donde se usa, y supongo que te ha pasado que cuando haces un BoF, una shellcode te sirve y otra no, aun que tengas todo tu exploit bien.
Eso ha sido todo, gracias por leer ❤