レポート

日本初開催JSConf!「JSConf JP 2019」参加レポート[後編:2日目]

この記事を読むのに必要な時間:およそ 6 分

「Analysis of an exploited npm package」

Jarrod OversonさんのセッションAnalysis of an exploited npm packageでは,npmパッケージに仕掛けらた攻撃について,詳細な分析を紹介しました発表スライド⁠。

サプライチェーンアタックとは

Jarrodさんはまずサプライチェーンアタックについて,次のように説明しました。

「我々はアプリケーションを作るときに大抵,自分自身の書いたコード以外の,一緒に読み込まれるコードのことを無視したり考えに入れなかったりします。直接/間接の依存関係,バンドル,プリプロセッサー,CSS,その他いろいろ読み込まれますが,気にしていません。サプライチェーンアタックは,この依存関係のどこか1箇所が傷つけられて,その結果システム全体が傷つけられることを指します」⁠Jarrodさん)

event-streamに対する攻撃の準備

この発表では,昨年11月にevent-streamパッケージでおこった,サプライチェーンアタックを取り上げました。

event-streamは,node streamを抽象化した古くからあるパッケージです。この当時1,600ものパッケージがevent-streamに依存していました。そして毎週120万もダウンロードされていました。その後11月に悪用されてから,800万ダウンロードされました。ダウンロードした人たちは,悪用されていることを知らずに動かしてしまっただろうと言います。

事の発端は,event-streamの開発者が,2018年9月にright9ctrlというユーザーにevent-streamの所有権(オーナーシップ)を与えたところから始まります。right9ctrlは,何回か問題のないコミット(変更)を行い,コミュニティの信頼を獲得しました。

そして9月9日に新しい依存flatmap-streamを追加して,新しいバージョン3.3.6をリリースしました。この時はまだ問題はありませんでした。package.jsonの依存関係には,"flatmap-stream": "^0.1.0"と記述されました。これは0.x.xに自動でアップデートする指定になります。次にevent-stream v4.0.0がリリースされ,この最新版ではflatmap-streamへの依存は削除されました(その結果,最新版ではflatmap-streamに対する依存は隠されました⁠⁠。それ自体は普通の行いで,特におかしなところはありません。ここまで12日間です。

画像

悪意のあるコードのリリース

そして,10月5日に悪意のあるflatmap-stream v0.1.1がリリースされました。event-stream 3.3.6を新しくインストールした人は,この悪意のあるflatmap-stream v0.1.1がインストールされるようになりました(package.jsonの"^0.1.0"の効果です⁠⁠。

今まで実績のあるevent-stream v3.3.5に依存していた他のパッケージも,次にインストールするときにはevent-stream v3.3.6を読み込み,その結果として悪意のあるflatmap-stream v0.1.1を読み込んでしまいました。

発見は幸運

Jarrodさんによると,今回のアタックが発見できたのは純粋に幸運によるものだとのことです。

悪意のあるコードが,Node.js v11.0.0では非推奨となったメッソドを使っていたため,8日後にリリースされたNode.js v11.0.0で実行すると,event-stream v3.3.5に依存しているプロジェクトでは(実際にはevent-stream v3.3.6を読み込んだため⁠⁠,コンソールに非推奨の警告が出るようになりました。

この非推奨の警告はcryptoに関するもので,event-stream v4にバージョンアップすると,⁠flatmap-stream v0.1.1の依存はなくなっているため)警告は消えます。この発見をevent-streamの所有者交代を結びつけて考える人が出てきて,flatmap-streamで何かおかしなことが行なわれていることが分かったのです。

3段階の攻撃

実際に,どのように攻撃が行われたのかを説明しました。

flatmap-stream v0.1.0からv0.1.1の変更では,382バイトが追加され(Payload A⁠⁠,これが攻撃を読み込むブートストラップとして機能していると言います。この部分を見やすく変換すると,./test/dataというファイルを読み込んでいることが分かります。

./test/dataというファイルには暗号化されたJavaScriptが含まれており,新しいモジュールを動的に作って,その暗号化されたJavaScript(Payload B)を復号化してコンパイルします。その際,復号のための鍵として,環境変数からnpmモジュールの説明を取得して利用しています。

これはnpm scirptから実行され,説明(description)に特定の文字列を含んでいる場合に限って発動します。

次に狙われたモジュールを探すために,すべてのモジュールのpackage.jsonをひっくり返す必用があります。この目的には,幸運なことにall-the-packagesというモジュールが使えます。all-the-packagesを使えばnpmに登録されたすべてのモジュールを順番にたどることができます。

まとめると,ターゲットを探すプランは次のとおりです。

  1. すべてのパッケージを辿りながら,メタデータを集める
  2. 各パッケージのメタデータに含まれるdescriptionを鍵にして,testData[0](Payload B)を復号化できるか試す
  3. もし復号化できたら,JavaScriptパーサーにかける
  4. もしJavaScirptとしてパースできたら,そのパッケージがターゲット

このような試みは実際には上手く行かないことも多いのですが,やってみたら20秒で該当するパッケージが見つかったそうです。それはcopayというビットコインのセキュアウォレットでした。

copayはOSSだったので詳しく調べることができました。復号化されたJavaScriptのPayload Bの役割は,⁠build:*-release⁠というスクリプトの実行を見つけた場合に,さらなる攻撃(Payload C)をしかけることでした。それはJavaScriptを使ったモバイルアプリのリリース版をビルドする処理において発動し,差し込まれたPayload Cのコードは,アプリからウォレットのパスワードを盗もうとしていました。

このように,ターゲットのnpmモジュールは,誰も想像していなかったモバイルアプリで動くものだったのです。

今回の教訓

今回の攻撃を振り返って,これがnode/npm特有の問題ではなく,すべての公開されたレポジトリは影響を受ける可能性があると強調しました。このケースではコミュニティは素早く対処できたことを良いニュースとしながら,悪いニュースとして同様なことはこれまで何度も起きていると話します。

今回は大きな被害にはならなかったがもっと酷い事態になった可能性があったと説明し,最後に,開発者ができる対策として,次のことを挙げました。

  • audit your dependency:自分が何に依存しているかを把握し
  • lock your dependency:依存をロックし,安易にパターンマッチを使わない
  • cache/check your deependency:依存対象を(企業内のレポジトリーに保持して)キャッシュしたり,チェックする
  • think twice before add dependency:依存を追加する前によく考える

そして,⁠疑わしい時は,賭けをせずに依存は追加しないことです。リスクが小さく価値が大きい時だけ賭けをしよう」と話しました。

また,DevSecOpsとしてできることとして,次のことを挙げました。

  • subresource integrity:サブリソース完全性(SRI)の仕組みを使って,ハッシュ値でリソースが意図せず改ざんされていないかをチェックする
  • contents security policy:コンテンツセキュリティポリシー(CSP)ヘッダーの仕組みを使って,どこから来たリソースに何の実行許可を与えるかを定める

Jarrodさんは「リリース前にアプリと依存先をスキャンをして,怪しい・知らない変更が加わっていないことを確認しよう」と述べ,発表を締めくくりました。

スポンサーセッション

2日目にもスポンサーセッションがあり,Yahoo! Japan,リクルート,ドワンゴ,DMM,Twilloの5社から発表がありました。

著者プロフィール

がねこまさし

WebRTC Meetup Tokyo スタッフ。
インフォコム株式会社で研究チームのマネージャーをしている。

GitHub:https://github.com/mganeko
Qiita:https://qiita.com/massie_g