Windows DLLプリロード攻撃の新しいパターンと防御法

作成: 2017/08/01
更新: 2017/08/06
白水啓章 (株)朝日ネット

概要

Windows DLLプリロード攻撃と呼ばれる脆弱性攻撃がありますが、原因や対応方法は広く知られています。(*1)
しかし、「特定のCOM I/F」や「WinAPI」の内部で「パス指定なしでDLLがロードされる」という、あまり知られていない経路があるのでご注意、というお話。

技術詳細

手元のWindows10Pro(x64)上で、FastCopyインストーラ(x64)で確認した範囲では、
下記のようなAPIで「不正なDLLの読み込みとそのDllMainが呼ばれること」を確認しています。
(*2)
1.IShellLink/IPersistFile COM I/F(ショートカット作成)のメンバ関数を呼び出すと、パス指定なしで"linkinfo.dll"等がロードされる。
2.ShellExecute API(ファイル/フォルダを開く)を呼び出すと、パス指定なしで"edputil.dll"等がロードされる。
 (注意: 第3引数がフルパスかどうかは無関係)
3.SHBrowseForFolder(フォルダ選択ダイアログ)を呼び出すと、パス指定なしで"WindowsCodecs.dll"等がロードされる。
4.その他、IPropertyStore, GetOpenFileName, GetSaveFileNameなどもパス指定なしDLLロードする。
簡単に調べても次々に見つかるので、他にも沢山存在(特にシェル関連API?)している可能性が高そうです。(*3)

従来の対策で考慮が必要な点

これらの資料にあるように、多くの注意喚起が行われており、およその要点は下記の通り。
0.パス指定なしでDLLをロード(LoadLibrary)すると、exeフォルダ、カレントフォルダの不正DLLを読む危険あり
1.パス指定でDLLをロードすることで防御可能
2.カレントの不正DLLロードはSetDllDirectory("")で防御可能
3.exeフォルダの不正DLLロードはSetDefaultDllDirectoriesで防御可能(ただし、Win8以降 or KB2533623が入ったWin7/Vistaのみ)
4.その他、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追記)

1.上記2-1でテンポラリ名を乱数生成して作成する場合、下記に注意します。
STL の std::random_device()を利用すると、内部で"cryptbase.dll"がパスなしでロードされます。
従い、std::random_device()を利用する前に、事前にパス指定でcryptbase.dllをロードしておきます。
(なお、CryptGenRandom APIを使った場合も、類似問題があるのでご注意)

2.起動からWinMainに到達するまでに、exeフォルダDLLが優先してロードされるDLLに注意します。
リンカやpragmaに指定しており、KnownDLLでないDLL全般が該当します。(手元で確認している例では、winmm.dll, mpr.dll など)
解決には「DLL遅延読み込み」を使います。
リンカの「DLL遅延読み込み(/DELAYLOAD)」に winmm.dll; mpr.dll 等を指定した上で、WinMain直後に上記の「今回の問題を考慮した対策方法」の1or2を行います。

3.いずれの場合でも、デバッガで起動から終了までのDLLロード状況を確認して、対策が正しく機能しているか確かめることをお勧めします。(
*4)

動作例

FastCopyインストーラをVS2015から実行したときのログ
1.不正DLL群を置いた状態での実行例
2.正常な実行例

備考

(*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 初版作成



..up menu