### Micco's Home Page ### Welcome to Micco's page!!
Sorry, but this web page is written in Japanese.
<English>
■ 更新情報
■ このWebページについて
■ お知らせ
■ ダウンロード
■ DLL のインストール方法
■ SFX の設定例
■ いろいろ
対応ブラウザー
[Internet Explorer] [Firefox] [Opera] [Sleipnir] [Safari] [Google Chrome]
連絡先:Micco
[e-mail]
[→Home]

<公開:Jul.17,2017>

MHVI#20170718:
Windows アプリケーションにおける DLL 読み込みに関する脆弱性について

 このページでは, 2017 年 5 月 25 日に公開された JPCERT/CC の注意喚起情報『Windows アプリケーションによる DLL 読み込みやコマンド実行に関する問題』 (JVNTA#91240916) で述べられている DLL 読み込みに関する脆弱性について, 主に拙作『LHMelting for Win32』を例として, プログラム側で必要となる対応について記載しています。 ここで説明されている対応を行った上で なお残る問題など詳細については, 参考情報及び各ソフトのドキュメント等を参照してください。

 なお, 実際に行うべき脆弱性への対応については, 「(直接・間接にかかわらず) どの DLL が使用されているか?」「DLL が どのような順序・タイミングでロードされているか?」「IME や (システムを含む) 対策ソフトなど, 当該プログラムに接触する他プログラムの状況」といった様々な要因によって, 各対策の優先順位や処理の順序が異なってきます。 「ここで述べられている順番で処理を行えば良い」「ここで述べられている処理を行えば外に対応は不要」というわけではない点に注意が必要となります。


●カレントディレクトリー型 DLL 読み込みの脆弱性について

どのような脆弱性か? 何が起きるのか? (技術情報)

 本脆弱性は, LHMelt が『統合アーカイバ』 DLL を任意のディレクトリー上から読み込めるように作成されていたことから発生していたものです。 LHMelt は対話型のアプリケーションですが, 起動時スイッチを指定してコマンドライン的に使用されることがあり, また, ユーザーによって DLL のインストール先が様々に異なるため, LHMelt 側で明示的に読み込み元を指定してのロードは行っていません。 (カレントディレクトリーを利用するケースもあったことから, SetDllDirectory() API による設定も行っていませんでした。)

 読み込み元を明示的に指定していない場合, Windows は定められた優先順位に従って複数のディレクトリーを検索して DLL を読み込みます。 例えば Safe DLL search mode が有効の場合は次のようになります:

  1. LHMelt のインストールされているディレクトリー
  2. ウインドウズ・システムディレクトリー
  3. 16 ビット用システムディレクトリー
  4. ウインドウズディレクトリー
  5. カレントディレクトリー
  6. 環境変数 PATH で指定された各ディレクトリー

この時に, カレントディレクトリーが検索対象となっているため, 『統合アーカイバ』 DLL と同じ名前の細工された攻撃用 DLL と同じディレクトリー上に書庫など任意のファイルを置き, 当該書庫等を関連付けなどを利用して起動と同時に LHMelt に開かせることで, 攻撃用 DLL を読み込ませることが可能となります。

DLL の読み込み時には必ず初期化が行われることから, 初期化ルーチンにおいて LHMelt に継承されているユーザー権限の範囲で任意のコードを実行することが可能となります。 初期化ルーチンを利用できることから, 細工された攻撃用 DLL が『統合アーカイバ』の仕様を満たしている必要はありません。

 ここで攻撃対象となるのは, 「必ずしも必要ではない, あれば使用される追加機能などの DLL」「古い OS には含まれていない, 新 OS のみに含まれる DLL」です。 存在するかどうか判らない DLL の使われるところがミソで, そうでなければ この脆弱性を突く意味がありません。

対策方法

1:SetDllDirectory() API を使用する

 とりあえず何も考えずに SetDllDirectory("") して, カレントディレクトリーが検索されないようにしておきます。

この対策を行うと, カレントディレクトリーからの DLL 等のロードが禁止されることから, 「他のプロセスにフック等で接触し監視等を行うソフト」がカレントディレクトリーからのロードに依存していた場合, 当該ソフトが LHMelt への接触に失敗し, LHMelt の起動時等に「○○が見つからなかったため、 このアプリケーションを開始できませんでした」の警告が表示される結果となります。 (場合によっては LHMelt を起動できなくなる。)

この「○○が見つからなかったため〜」が表示された場合は, その当該 DLL を使用しているソフトに「安全でないライブラリのロードにより、リモートでコードが実行される (マイクロソフト セキュリティ アドバイザリ 2269637)」の脆弱性が存在することになります。 特に, 他のプロセスを監視する類のソフトでは影響が甚大 (ソフトの性格からして, システム・管理者権限で動作, 若しくは それらの権限を取得可能な状況で動作している可能性が高い。) ですので, そのようなソフトを考慮する必要はないでしょう。

2.1:カレントディレクトリーを対象としない独自の検索ルーチンを用意する

 何らかの要因で GetProcAddress() などによる SetDllDirectory() API 呼び出し先の取得に失敗することがあります。 (古い OS など SetDllDirectory() API が存在しない場合を含む。)  そのようなケースに対応するため, カレントディレクトリーを含まない, 必要最低限のディレクトリーのみを対象とする検索ルーチンを用意しておきましょう。

2.2:可能であれば DLL の内容チェックを行う

 2.1 で検索された DLL について, 可能であれば DLL の内容チェックを行います。 アーカイバー DLL に限っていえば明確なチェック方法が存在しないので無理なのですが, チェック方法が存在するなら その方法を, また対象が絞られるのであればハッシュなどによるチェックを行うことになるでしょう。 間違っても, ここで初期化ルーチンが呼び出される設定での DLL ロードを行わないように!! (^^;)

2.3:チェックをパスした DLL について, ロード元を明示的に指定した上でロードする

 2.2 のチェックをパスした DLL について, 2.1 で検索されたディレクトリーを明示的に指定して LoadLibrary() API を使用してロードします。 SetDllDirectory("") の正常終了が担保されているのであれば必ずしも必要ではありませんので, 明示的に指定するかしないかは お好みで。

3:アプリケーションディレクトリー型 DLL 読み込みの脆弱性への対応

 次項で説明されている「アプリケーションディレクトリー型 DLL 読み込みの脆弱性」への対応を行えば, 必要なフラグを設定した上で DLL をフルパスで指定して LoadLibraryEx() API を使用してロードすることになるので, こちらの脆弱性への対応についても, より安全性が高くなります。

●アプリケーションディレクトリー型 DLL 読み込みの脆弱性について

どのような脆弱性か? 何が起きるのか? (技術情報)

 Windows においては, ほぼ全てのプログラムが "KERNEL32.DLL" や "USER32.DLL" といったシステムライブラリーに実装されている Win32 API を使用しています。 DLL の利用については, 「存在するか判らない DLL」「必ずしも必要としない DLL」であれば, LoadLibrary() API を使用して DLL のロードを行った上で使用したい API の呼び出し先を GetProcAddress() で得る…といった手順を踏むことになります。

一方, 「Win32 API を使用するために KERNEL32.DLL や USER32.DLL をロード」といった, 「必ず使用する DLL」「システムファイルとして Windows に必ず含まれている DLL」を利用する際には, 例えば USER32.DLL であれば USER32.LIB といった, それぞれの DLL に対応したインポートライブラリーを使用することにより, DLL のロードや呼び出し先の取得といった手順を Windows に任せる…という手法が一般的に採用されます。 インポートライブラリーを使用した場合, プログラムの実行ファイル内に「当該プログラムが使用する DLL と (例えば Win32 API などの) 呼び出す機能」が一覧されたインポートテーブル…つまり, 外部参照の一覧表が作成されます。

 インポートテーブルが含まれるプログラムを起動すると, 実際に当該プログラムへ制御が渡る前の段階で, Windows のシステムライブラリーである NTDLL.DLL に存在するイメージローダーが, インポートテーブル上にある外部参照の情報を読み込んで, 必要な DLL のロードと機能の呼び出し先の取得を行います。 読み込んだ DLL がインポートテーブルをもっていれば同様の手順を再帰的に行います。 これらの作業は, 全ての外部参照が解決されるまで繰り返されます。

この際, Windows は定められた優先順位に従って複数のディレクトリーを検索して DLL を読み込みます。 例えば Safe DLL search mode が有効の場合は次のようになります:

  1. プログラム (今回であれば LHMelt) の置かれているディレクトリー
  2. ウインドウズ・システムディレクトリー
  3. 16 ビット用システムディレクトリー
  4. ウインドウズディレクトリー
  5. カレントディレクトリー
  6. 環境変数 PATH で指定された各ディレクトリー

この時に, プログラムの置かれているディレクトリーが最も優先順位の高い検索対象となっているため, システム DLL と同じ名前の細工された攻撃用 DLL をプログラムと同じディレクトリーへ置くことで, 優先的に攻撃用 DLL を読み込ませることが可能となります。

DLL の読み込み時には必ず初期化が行われることから, 初期化ルーチンにおいてプログラムの実行に使用したアカウントがもつ権限の範囲で任意のコードを実行することが可能となります。 初期化ルーチンを利用できることから, 細工された攻撃用 DLL がシステム DLL の仕様を満たしている必要はありません。

 ここで攻撃対象となるのは, カレントディレクトリー型とは逆に「常に使われる DLL」「使用頻度の高い DLL」です。 とはいうものの, 闇雲に USER32.DLL 辺りを使ったとしても, Windows 自身やプログラム側により対策済みだったりしますので, 実際には変化球が投げられることになります。

詳細については後述しますが, とある理由により, この脆弱性を突く上で人気 DLL となっている最右翼は "SSPICLI.DLL" となっています。 カレントディレクトリー型としても利用すべく, 前処理として当該 DLL がシステムから削除されているケースも多々あります。 この辺りの DLL はトラブル対策として Web 上に沢山アップロードされていたりしますが, ダウンロードは控えておくのが得策です。 「ハッシュの SHA256 が合っていれば大丈夫」くらいで信用するのは自殺行為といえます。 (^^;)

なお, SSPICLI.DLL は基本的に Windows 7 (まで) 用で, Windows 8/8.1 や Windows 10 においては攻撃対象として機能しないケースが多いことから, 人気 DLL は今後移り変わっていくことが予想されます。 変化球として考えた場合, OneDrive 周りが危険と言えそうです。 (ロードされるタイミングなどのトリガーが同じ。)

この脆弱性のミソ (補足情報)

 この脆弱性は, カレントディレクトリー型と同じ起源をもつ脆弱性ではありますが, 置かれている状況は大きく異なります。 そのミソは, なんと言っても「安全でないライブラリーのロードを行っているのが Windows システムの一部である NTDLL.DLL (上のイメージローダー)」であり, 「しかもそれが『ユーザーが起動しようとしたプログラム (例えば LHMelt) へ制御が渡る前』に行われる」点です。 つまり, LHMelt 側には通常の方法では対処のしようがないことになります。 何しろ, LHMelt へ制御が渡る前に攻撃が成立してしまうのですから…。

この点は市販ソフトや巷に溢れている Windows プログラムも同じで, 言ってしまえば「ほぼ全てのプログラム」が該当してしまうことになります。 「市販ソフトなど一般的なアプリケーションでは, 書き込みに管理者権限を必要とする "C:\Program Files" ディレクトリー配下へインストールされるケースが多く, その分安全性が若干高まっている」に過ぎません。 反対に, インストーラーやツール, 自己解凍書庫といったプログラムは「置き場所 (実行場所) を選ばず使われることから, 脆弱性として問題になっている」といえます。

対策方法

 今回の脆弱性の素となっている NTDLL.DLL (イメージローダー) の仕様ですが, MSVCRT.DLL や CTL3D.DLL, CTL3DV2.DLL といった DLL に始まり, MFC32.DLL などの MFC ライブラリーで大問題となってしまった「バージョンヘル」の軽減策として, 「プログラムと同じディレクトリーへ DLL を配置することで使用 DLL をオーバーライドする」という手法が定着してしまったことから, 今となっては仕様変更を行えない事態に陥ってしまっています。 また, 過去のしがらみから, 本来行われるべき「(システムライブラリーについて) システムディレクトリー以外から読み込まない, 読み込めないようにする」といった保護策についても, 一部のシステム DLL にしか講じられていません。

この辺りは MSDN でも説明されていて, それを避けるために用意されているのが, SetDllDirectory() や SetDefaultDllDirectories(), SetSearchPathMode(), LoadLibraryEx() (の追加機能) といった API 群, そしてリンカーとヘルパーライブラリーによる「DLL の遅延ロード」です。 遅延ロードは, ランタイムの一部ともいえるヘルパーライブラリーがプログラムの起動後に LoadLibrary() + GetProcAddress() を行う機能で, 「自前で LoadLibrary() + GetProcAddress() する」のと本質的に変わりありません。

 そこで, これらの機能を使ってプログラム側で脆弱性の回避を行うことになります:

0.1:前提条件その 1

 カレントディレクトリー型の項で説明した対策について, 適用済みであることを前提としています。 あちらのほうが遥かに対処が簡単なので…。

0.2:前提条件その 2

 Windows 7/Vista については, Windows のセキュリティーパッチである KB2533623 の更新プログラムを適用していることが前提となります。 これが適用されていないと SetDefaultDllDirectories() API などが使えないことから, 根本的に対応が破綻してしまい, Windows XP 以前と同じ事態に陥ります。 (要は「対応不可」)

1:SetDefaultDllDirectories() API を使用する

 とりあえず SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) して, システムディレクトリー以外から DLL をロードできないようにしておきます。 他のフラグを付加するかどうかはプログラムしだいですが, LoadLibraryEx() API でオーバーライド可能なので, こちらでは なるべく安全性に重きをおいた指定を行っておいたほうが良いでしょう。

 なお, ここでの設定は自身のみならず「自身が使用する DLL 及び当該 DLL と依存関係にある全ての DLL」「自身が DLL などの『呼び出される側』であるなら, 自身を呼び出した側のプログラム」「呼び出した側のプログラムが使用する DLL 及び当該 DLL と依存関係にある全ての DLL」に適用されますので, 十分な注意が必要となります。

特に注意が必要なのは「自身が呼び出される側である」場合で, 例えば呼び出し側が この脆弱性に未対応の (SetDefaultDllDirectories() API の使用について考慮されていない。) ケースでは, 最悪当該プログラムを起動できなくなる…どころか, Windows 8/8.1 では NTDLL.DLL が一般保護エラーを起こしてしまいます。 そのため, 呼び出し元が多岐に渡る UNLHA32.DLL については, DLL 側での SetDefaultDllDirectories() は行っていません。

反対に, 「外部の DLL を自身が呼び出している」場合にも注意が必要となります。 例えば LHMelt においては, 「LHMelt 側の行っている SetDefaultDllDirectories() などによる脆弱性への対策が, 呼び出される側である UNRAR32.DLL に影響してしまい, システムディレクトリーにインストールされていないと, UNRAR.DLL が読み込まれなくなる」という弊害が発生しています。 (UNRAR.DLL の読み込みに失敗すると UNRAR32.DLL がバージョン情報として 0 を返すので, 結果として「版が古すぎる」と LHMelt に怒られることとなる。)

2:SetSearchPathMode() API を使用する

 とりあえず SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE) して, SearchPath() API でのカレントディレクトリー検索の優先度を最低にしておきましょう。 検索自体の行われることに変わりはありませんので, 注意が必要となります。
BASE_SEARCH_PATH_PERMANENT を付加するかどうかは, 仕様などプログラムの状況次第なので, ここでは言及しません。

 自身が SearchPath() API を使用するかどうかにかかわらず, 安全策で指定しておくのが得策です。

3:KERNEL32.DLL 以外についてインポートライブラリーを使用しない

 そもそも, インポートテーブルの存在が脆弱性に繋がっているわけなので, 極力それが作成される "USER32.LIB" といったインポートライブラリーを使わないようにします。 自前で LoadLibraryEx() + GetProcAddress() するか, リンカーとヘルパーライブラリーによる「DLL の遅延ロード」機能を使うかは, お好みで。 LHMelt 辺りでも 500 以上の API を GetProcAddress() する地獄を見ていますので後者のほうが楽ではありますが, ロードタイミングの指定なども必要になってきますので, その辺りについては前者のほうが遥かに楽です。 要は一長一短ということですね。

なお, KERNEL32.DLL だけは「それが読み込まれていないと, そもそも DLL のロードを行えない…どころか ほぼ何も出来ない」事態に陥りますので, 大人しく "KERNEL32.LIB" を使っておきましょう。 .NET Framework アプリであれば, イメージローダーがバイパスされ直接 Framework のルーチンに制御が渡りますので, KERNEL32.DLL を含めた制御が可能となります。

4:LoadLibraryEx() API を使用して DLL をロードする

 DLL のロードには, LoadLibrary() API ではなく LoadLibraryEx() API を使用しましょう。 指定するフラグはプログラムしだいですが, USER32.DLL 辺りの Win32 主要 API を擁した DLL については, LOAD_LIBRARY_SEARCH_SYSTEM32 のみを指定して, システムディレクトリーのみ許可しておくのが得策です。

自プログラムとセットでインストールされるような DLL については, LOAD_LIBRARY_SEARCH_APPLICATION_DIR を指定することになるでしょう。 ただし, こちらについては「インストール済みであることが担保されている」必要があります。 されていなかった場合は, 当該 DLL を対象とした攻撃が成立します。

安全性を高めておく観点から, フルパスでロードする DLL を指定しておきましょう。 ただし, フラグとの整合性は保つ必要がありますので, 注意が必要となります。 整合性が保たれなかった場合は, フラグ指定の効果が不定となります。

 LHMelt でもそうですが, アーカイバー DLL といったオプション的な DLL や自プログラムとバンドルされる部品的 DLL…といった, システムライブラリー以外の DLL が増えれば増えるほど, 個々の DLL に対する LoadLibraryEx() でのフラグ指定が複雑化しますので, 注意が必要となります。

5:SSPICLI.DLL などの DLL を先行してロードしておく

 Windows 7 環境での SSPICLI.DLL など, SetDefaultDllDirectories() API 等の設定に因らない, 独自ルールの検索と読み込みが行われる DLL が存在することから, その時点で呼び出す必要がなくとも, USER32.DLL のロード時など初期化の段階で, 先行して DLL のロードを行っておきます。

当該 DLL 自体は (システム DLL 内で) 遅延ロードが行われるため, 起動時のみの動作確認では表面化しないケースが殆どです。 そして, SHBrowseForFolder() API の呼び出し時など, 忘れた頃に表面化します。 (笑)  独自ルールが絡む上に SHBrowseForFolder() API など COM が絡む API で発生するカレントディレクトリー変更が拍車を掛けて事態を複雑化しますので, 単純化が可能な起動時の段階で さっさとロードしておきましょう。 一旦ロードさえしておけば, 後からどのようなロードが行われようとも, 実質参照数が 1 つ増えるだけですので。

 事前ロードが必要となる最右翼は SSPICLI.DLL ですが, 外にも APPHELP.DLL について対応しておく必要があり, 後述しますがスタイル指定目的などを含めて, マニフェストを使用する場合には UXTHEME.DLL への追加対応も必要となってきます。

ただし, Windows Vista 環境については, KERNEL32.DLL が NTDLL.DLL により読み込まれる時点で APPHELP.DLL に対する DLL ハイジャックが成立してしまうことから, 完全な対応は不可能となります。 現状, LHMelt では発生していないのですが, 「成立する」と考えておいたほうが得策といえます。 (UNLHA32.DLL や作成された自己解凍書庫では発生している。)

6:Side-by-Side アセンブリーへの対応

 最も多い使用目的と推測されるスタイル指定を始めとして, マニフェストにより Side-by-Side アセンブリーを利用している場合は, それに対する追加対応が必要となります。 マニフェスト以降の同様の機能も全て該当します。

 Side-by-Side アセンブリーが使用されている場合, 指定した版にマッチする DLL がウィンドウズディレクトリー配下である C:\Windows\WinSxS のサブディレクトリー上から読み込まれるわけですが, 困ったことに, フルパスでの指定や LOAD_LIBRARY_SEARCH_SYSTEM32 を指定して LoadLibraryEx() API などを使用した場合は, 指定どおりシステムディレクトリー上の DLL のみが検索されます。

その場合でも, Windows 7 以降についてはシステムディレクトリー上に最新の DLL が配置されているため問題とはならないのですが, 例えば Windows Vista や Windows XP 環境では, 過去のしがらみから, COMCTL32.DLL 辺りについては, 最も古いバージョン 5 の DLL がシステムディレクトリー上に配置されています。 そのため, 結果としてスタイルが適用されない…どころか, イメージリスト関連の API も機能しない…という不具合が発生します。

 従って, 自身が指定しているものを含めて, Side-by-Side アセンブリーが関係する DLL については, あえてパスなし指定で LoadLibrary() API を使用するなど, 当該 DLL が C:\Windows\WinSxS のサブディレクトリー上からロードされるようにした上で, それらの DLL をロードし終わった後などの段階で SetDefaultDllDirectories() API を使用する…といった対策が必要となります。

 ある意味, この対策が一番面倒かもしれません。 「システムディレクトリー上の DLL でも問題ないかどうか」はプログラムしだいなので, それによって SetDefaultDllDirectories() API の使用タイミングが変化し, その結果 5 での先行ロードを必要とする DLL についても変わってきます。 (SSPICLI.DLL と APPHELP.DLL は確定)

●関連付け起動やコマンド実行について

どのような脆弱性か? 何が起きるのか? (技術情報)

 プログラムがデーターファイルを直接指定して ShellExecuteEx() API を使用することで関連付け起動を行ったり, 同様にコマンドを指定して ShellExecuteEx() や CreateProcess() API を使用した場合には, 関連付けされたプログラムやコマンドを対象とした攻撃の成立する場合があります。

どのようなプログラムやコマンドを対象とするかによって異なってきますが, この場合, カレントディレクトリー型とアプリケーションディレクトリー型双方の脆弱性を利用することが可能です。

対策方法

 データーファイルやコマンドを直接指定せず, 関連付けされたプログラムやコマンドをフルパスで指定して ShellExecuteEx() や CreateProcess() API を使用します。 この場合, 関連付けされたプログラムの取得が問題となりますが, 例えば LHMelt においては, "open" や "edit" など, ShellExecuteEx() API で使用するモード指定の観点もあって, HKEY_CLASSES_ROOT を始めとしたレジストリー上の関連付け情報を直接読み取って, 起動するプログラムの取得を行っています。

●その他

更新履歴

  • 脆弱性情報の公開 [Jul.17,2017]

参考情報

[→Page top] [→Home]