Windows DLLプリロード攻撃の新しいパターンと防御法
概要
Windows DLLプリロード攻撃と呼ばれる脆弱性攻撃がありますが、原因や対応方法は広く知られています。(*1)
しかし、「特定のCOM I/F」や「WinAPI」の内部で「パス指定なしでDLLがロードされる」という、あまり知られていない経路があるのでご注意、というお話。
技術詳細
手元のWindows10Pro(x64)上で、FastCopyインストーラ(x64)で確認した範囲では、下記のようなAPIで「不正なDLLの読み込みとそのDllMainが呼ばれること」を確認しています。(*2)
- IShellLink/IPersistFile COM I/F(ショートカット作成)のメンバ関数を呼び出すと、パス指定なしで"linkinfo.dll"等がロードされる。
- ShellExecute API(ファイル/フォルダを開く)を呼び出すと、パス指定なしで"edputil.dll"等がロードされる。
(注意: 第3引数がフルパスかどうかは無関係) - SHBrowseForFolder(フォルダ選択ダイアログ)を呼び出すと、パス指定なしで"WindowsCodecs.dll"等がロードされる。
- その他、IPropertyStore, GetOpenFileName, GetSaveFileNameなどもパス指定なしDLLロードする。 簡単に調べても次々に見つかるので、他にも沢山存在(特にシェル関連API?)している可能性が高そうです。(*3)
従来の対策で考慮が必要な点
これらの資料にあるように、多くの注意喚起が行われており、およその要点は下記の通り。
- パス指定なしでDLLをロード(LoadLibrary)すると、exeフォルダ、カレントフォルダの不正DLLを読む危険あり
- パス指定でDLLをロードすることで防御可能
- カレントの不正DLLロードはSetDllDirectory("")で防御可能
- exeフォルダの不正DLLロードはSetDefaultDllDirectoriesで防御可能(ただし、Win8以降 or KB2533623が入ったWin7/Vistaのみ)
- その他、SeachPathを使わない、SetSearchPathModeを使う等
これらは正しいのですが、これだけでOKではなく、
- 「SetDefaultDllDirectories API が利用できない環境(もしくはそれを実行していないバイナリ)」と、
- 「特定のWinAPIやCOM I/Fが、パス指定なしDLLロードを発生させる」
の2つが組み合わさった場合に、SetDefaultDllDirectories以外の対策を全て行っても、DLLプリロード攻撃が可能になります。
(特にインストーラexeの場合、ブラウザのダウンロードフォルダという不正DLLが存在しうる場所での実行になるため危険度が高い)
今回の問題を考慮した対策方法
一番良い方法は、
- 1 . SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32)を発行する
(これが発行できれば、WinAPIやCOM I/F によって、パス指定なしDLLのロードが発生しても、exeフォルダのDLLがロードされることはなくなります)
しかし、KB2533623が入っていないWin7やVistaではこのAPIがありません。
その場合、少し面倒ですが下記の方法が無難です。
(いくつかの汎用インストーラはこの形)
- 2-1. 起動されると同時に、自分自身(実行ファイル)を新規に作ったフォルダにコピー
- 2-2. (カレントを新規フォルダとして)コピーした実行ファイルを起動し、そこから本来のインストール動作を実行
上記の方法以外に、予め何をロードするかが判っていれば、
- 3 . それらを事前にパス指定でLoadLibraryしておくこと
により、その後、パス指定なしのLoadLibraryが発生しても、すでにロードされたモジュールが使われるようになります。
(しかし、3の方法ではどのDLL群をロードするか、事前に確定することが難しい(WinAPI等の内部実装に依存)という点を考慮する必要があります)
.
補足(2017/08/06追記)
- 上記 2-1 でテンポラリ名を乱数生成して作成する場合、下記に注意します。
STL の std::random_device()を利用すると、内部で"cryptbase.dll"がパスなしでロードされます。 従い、std::random_device()を利用する前に、事前にパス指定でcryptbase.dllをロードしておきます。 (なお、CryptGenRandom APIを使った場合も、類似問題があるのでご注意) - 起動からWinMainに到達するまでに、exeフォルダDLLが優先してロードされるDLLに注意します。
リンカやpragmaに指定しており、KnownDLLでないDLL全般が該当します。(手元で確認している例では、winmm.dll, mpr.dll など) 解決には 「DLL遅延読み込み」 を使います。 リンカの「DLL遅延読み込み(/DELAYLOAD)」に winmm.dll; mpr.dll 等を指定した上で、WinMain直後に上記の「今回の問題を考慮した対策方法」の 1. or 2. を行います。 - いずれの場合でも、デバッガで起動から終了までのDLLロード状況を確認して、対策が正しく機能しているか確かめることをお勧めします。(*4)
動作例
FastCopyインストーラをVS2015から実行したときのログ
備考
(*1) これまでのDLLプリロード攻撃に対する、原因や対処に関する資料
- Microsoft「DLL プリロード攻撃を防止するためのライブラリの安全な読み込み」
- IPA「任意のDLL/実行ファイル読み込みに関する脆弱性の注意喚起」
- JPCERT「WindowsのDLLだけが危ないのか?DLL hijacking vulnerability概説(前編)」
- JPCERT「WindowsのDLLだけが危ないのか?DLL hijacking vulnerability概説(後編)」
(*2) MS署名がないDLLの多くは、読み込んでもすぐにアンロードされ、DLL側関数であるDllMainは呼ばれないようですが、上記の例にある "linkinfo.dll", "edputil.dll", "WindowsCodecs.dll" 等ではそういった機構は働かないようです。
(それ以外にも "urlmon.dll", "propsys.dll", "secur32.dll", "sspicli.dll" 等多々あるようです)
(*3) Microsoftさんへ: APIやMS提供のCOM I/Fの内部DLLロードに関しては、パス指定DLLロードにすべきと思います。
これをそのままに放置しているのは、DLL乗っ取りを前提としたソフトが存在している等でしょうか?
(あまり合理性のある理由が思いつかないのですが…)
(*4) デバッガでモニタすると、DLLによっては、パス指定でSystem32からロードしたのち、exeフォルダの同名DLLを再度ロードする、不可思議な挙動を示すDLLがあったりします。
(この挙動の原因をご存知の方はご連絡ください。なお、LoadLibraryEx(LOAD_LIBRARY_SEARCH_SYSTEM32)の場合は、この挙動が起きませんが、このフラグが使える環境=SetDefaultDllDirectoriesが使える環境なので、この方法はあまり助けになりません)
おまけ(今回の問題を見つけた経緯)
7月下旬に、公的機関から、IP Messengerインストーラに対する DLL Preload攻撃が成功した旨の報告があったことに端を発します。
しかし、私自身はこれらの資料にあるような確認はすでに行っており、IP Messengerインストーラでは、パス指定なしDLLロードを行っていないことを確認済みだったため、この報告にはかなり驚きました。
そこでデバッガ等でインストーラの挙動をトレースしたところ、ソースコードでは LoadLibrary していないにもかかわらず、ショートカット作成(IShellLink)内で、パス指定なしDLLロードが行われていることを発見し、ぐぬぬとなった…という流れになります。
関連リンク
JPCERT 戸田さん講演 「DLL読み込みの問題を読み解く」(オープンソース・カンファレンス2017京都)
免責
この内容は無保証とします。
可能な限り、実際のログ等を掲載して正確性を期したつもりですが、勘違いや事実誤認があれば随時訂正しますので、ご連絡ください。
履歴
2017/08/06 対策方法に「補足」を追記
2017/08/06 「関連リンク」を追記
2017/08/01 初版作成