### 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]

今日の出来事 (May, 2018)

●May.28,2018

LHMelt 等での DLL ハイジャック対策覚え書き

 2016 年頃から再び巷を賑わせている DLL 読み込みの脆弱性ですが, 拙作でも UNLHA32.DLL の作成する自己解凍書庫モジュールが直接該当し, その本体である UNLHA32.DLL や お仲間の UNARJ32.DLL, そして それらを利用している LHMelt についても「対応優先度は下がるものの脆弱性をもつことに変わりなし」というわけで, 対応版を公開してきたところです。

 この脆弱性は言わば「結果論としての Windows の仕様バグ」で, ちょうど LZH, ZIP, ARJ といった書庫での絶対パス付きメンバーと同じで, 「元々正式な機能で普通に使われてもいたものが, 後々攻撃手段として使われるようになった」ことで脆弱性として認識されるようになったものです。 結果, アーカイバー方面では (少なくとも既定値では) 絶対パスを拒否するようになりました。

それに対して, 「DLL 読み込みの脆弱性」の元ネタとなっている仕様については, Windows 系プログラムの開発手法とあまりにも深く結びついてしまったがために, もはや仕様変更を行うわけにもいかず, Microsoft などは「仕様」ということで正式に「即時の対応は行わない」と宣言してしまいました。 (笑)  もっとも, その一方で Windows 7 等では SetDefaultDllDirectories() を始めとした API の追加, Windows 10 1703 では PreferSystem32 設定の導入…といった感じにプログラム側での対策手段を用意するなどもしていて, 決して単純に放置しているわけではありません。

ともあれ, MS 側の対応を期待できない以上自前で対策するしかない…というわけで, 「Windows アプリケーションにおける DLL 読み込みに関する脆弱性について」の記事に書いた方法で上述した拙作ソフトについては対策してあるわけですが, 多少なりとも話を一般化するため抽象的に書いたせいで少々解り辛いのも事実です。 もちろん, 自身に限ってはコードを書いた本人ですから, 拙作ソフトのソースを読めば何をやっているのかは一目瞭然なのですが, 全てを一箇所で処理できない以上ソースについても当然ながら一箇所に纏まってはいず, 必然的に流れも掴みづらくなってしまっています。

 そこで, 自身への覚え書きを兼ねて簡単に手順を書いておくことにしました。 5 年もすれば自身が他人に成り下がるでしょうから, ここへ書いておけばソース解読への足しにもなるでしょう。 コメントは付けてあるものの, それで未来の自分が全て理解できるとは思えません。 (笑)

さて, 対象ですが…表題のとおり LHMelt とします。 拙作の中では一番プログラムの規模が大きく, システムやアーカイバー関連など使用している DLL も一番多いので…。 その他「呼び出す側の LHMelt と呼び出される側の UNLHA32.DLL 等では事情が異なる」といった理由もあります。 あと, 「例に挙げるなら そもそも脆弱性の対象である自己解凍書庫が良いのでは?」という話もありますが, 対策コードとしては LHMelt のサブセットなので, LHMelt のほうが適任なのでした。

 それでは, 実際の手順へ入る前に まずは前提条件です。

0:前提条件

  • 対処可能なのは基本的に Windows 7 以降の OS となります。 Windows Vista も含めた対処法とはなっていますが, 後で述べるように Vista においては完全な対応は OS の仕様的に無理となります。 さらに, Win 7/Vista については Windows のセキュリティーパッチである KB2533623 の更新プログラムを適用していることが前提となります。 対処に不可欠な API が このパッチを当てていないと使えませんので…。
    ただし, ここで示す手順自体は, OS にかかわらず出来ないなら出来ないで極力悪あがきするものとなっています。
  • .NET Framework や MFC などのランタイム使用は想定していません。 それらのランタイム DLL 読み込み自体が脆弱性へ繋がる上に, それらランタイムの読み込むシステム DLL の制御が絡むと一層話がややこしくなってしまいます。 ちなみに拙作ソフトでは, C ランタイムすらスタートアップルーチンの crt0init.obj と それらが使用する分の付随モジュールくらいしか使っていず, それ以外は排除されています。 (^^;)
  • より厳密にロード順を制御するため, DLL の読み込みは全て LoadLibraryEx() / LoadLibrary() API を使用して自前で行っています。 従って, ヘルパーライブラリーを使用した遅延ロード化の手順とはなっていません。 試したことはあるのですが, 接触してくる他ソフトの DLL について, 予防的先行ロードとしての順序やロードタイミングを制御しきれず一部 OS で脆弱性を回避できなかったことと, そもそも それに対応したリンカーを使うと必須サポートの Win32s 環境で動作しなくなってしまうので, 使用を諦めました。 (笑)
    遅延ロードを使用する場合においても, 少なくとも ここで説明するタイミングで個々の DLL がロードされるよう調整する必要があります。
  • 「作成した新規ディレクトリー上で子プロセスを実行」の手順は採用していません。 そもそも その「子プロセスを実行する親プロセス」が完璧に対処できていないことには無意味ですし, その親プロセスが MFC などのランタイムを使用していようものなら, 子プロセスを使わずに頭から対処するのと何も変わりありませんので。
  • 「DLL のエクスポート転送」や「DLL リダイレクト」など, 独自のルールと検索方法でロードされる DLL については, 統一的に同じものとして扱っています。 自身が行うものであれば判別も個別対処も可能ですが, 他プログラムが行うものについては「結果的に独自の読み込みが行われる」という発生した結果としての事象が全てですので。
  • ここで示す手順は, あくまでも「拙作の UNLHA32.DLL (と それが作成する自己解凍書庫), UNARJ32.DLL, そして LHMelt といったプログラムで通用しているもの」でしかありません。 参考には出来ても それが自身のプログラムに適用可能とは限らない点に注意が必要となります。
  • 実際のソースコードを示すわけではありません。 (ここ重要。 ^^;;)  いきなり梯子を外していますが, 上の 2 番目 3 番目から判るように, その 2 点だけでも難読化必至で さらに Win32s から Win 10 までに対応したコードとなっていますので…。 多環境対応の「#if defined() 文の権化」と化したコードをイメージしていただければ, お解りになると思います。 (実際そのようになっているわけでは有りませんけれど。 ^^;)

以上のような前提の基に手順を示していくこととします。

1:下準備 (簡易 OS 判定)

 脆弱性回避にも OS の判別は必要となりますので, GetVersionEx() を使用して OS の簡易判定を行っておきます。 拙作ソフトが この段階でフラグとして設定しているものは次のとおり:

  • bIsWin32s … Win32s 環境 (VER_PLATFORM_WIN32s) の場合に TRUE
  • bIsWin32s_130 … Win32s Ver 1.30 以降の場合に TRUE。 (Windows NT 3.5 までと NT3.51 以降の違いと同じく WinHelp() へ渡すフラグが異なってきます。 当然ながら ここでの手順には関係ありません。 ^^;)
  • bIsWin95 … Windows 95 系 (VER_PLATFORM_WIN32_WINDOWS) の場合に TRUE
  • bIsWin98 … Windows 98 以降の場合に TRUE
  • bIsWinMe … Windows Me の場合に TRUE
  • bIsNT31 … Windows NT 3.1 以降の場合に TRUE。 (NT 3.5 以降を想定していますので, 「3.5 かどうか?」については判定していません)
  • bIsNT351 … Windows NT 3.51 以降の場合に TRUE
  • bIsNT40 … Windows NT 4.0 以降の場合に TRUE
  • bIsWin2k … Windows 2000 以降の場合に TRUE。 (環境値を読み込む前のコードページの初期値として CP_THREAD_ACP を使用するか否かにも使用しています)
  • bIsWinXP … Windows XP 以降の場合に TRUE
  • bIsWinVista … Windows Vista 以降の場合に TRUE
  • bIsWin7 … Windows 7 以降の場合に TRUE
  • bIsWin8 … Windows 8 以降の場合に TRUE
  • bIsExplorer … bIsWin95 か bIsNT40 が真なら TRUE。 (後段で関係してきますが, 自前で CTL3D32.DLL をロードするかどうかに使います)

「そのほうが便利」ということで, 多くのフラグは「その OS そのものを意味する」ものではなく「その OS 以降であることを意味する」ものとなっています。 あと, 幸いながら今回の手順では必要ないのですが, VERSION.DLL を必要とする「Windows 10 か?」や「1703 以降か?」などのバージョン判定は この段階では行えません。 使えるのは KERNEL32.DLL だけです。 なので, 本来このタイミングで使用したい PreferSystem32 も使えないのでした。 (LOAD_LIBRARY_SEARCH_SYSTEM32 など LoadLibraryEx() の追加フラグと同様, API の追加ではなくフラグの追加による実装なので。)

2:KERNEL32.DLL のロード (NTDLL.DLL によりロード済み)

 KERNEL32.DLL はプロセスやメモリー管理, ファイル入出力等を行う DLL です。 NTDLL.DLL により自プログラム (例えば LHMelt) へ制御が渡る前にロードされていますので, 以降の処理のために GetModuleHandle() を使用してハンドルを取得しておきます。

2.1:SetDllDirectory() の実行

 SetDllDirectory() が使用可能であれば SetDllDirectory("") を実行してカレントディレクトリーからの DLL ロードを抑止しておきます。 また bCanSetDllDir を TRUE にしておきます。 (アーカイバー DLL などオプション的 DLL ロードの際に使用)

カレントディレクトリー型 DLL 読み込みの脆弱性については これの実行で基本的に対応可能ですが, 「他のプロセスにフック等で接触するソフト」がカレントディレクトリーからのロードに依存していた場合, 当該ソフトが自プログラムへの接触に失敗し, 自プログラムの起動時等に「○○が見つからなかったため、 このアプリケーションを開始できませんでした」の警告が表示される結果となります。 (場合によっては起動できなくなる。)

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

兎にも角にも, この SetDllDirectory("") の実行を最優先で行っておく必要があります。

2.2:SetDefaultDllDirectories() の実行 (Windows 7 以降の場合)

 SetDefaultDllDirectories() が使用可能かチェックし, 使用可能であれば bCanSetDefDllDirs を TRUE にしておきます。 その上で, Windows 7 以降であれば SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) を実行して, システムディレクトリー (C:\Windows\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 に怒られることとなる。)

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

Side-by-Side アセンブリーが使用されている場合, 指定した版にマッチする DLL がウィンドウズディレクトリー配下である C:\Windows\WinSxS の (さらに) サブディレクトリー上から読み込まれるわけですが, 困ったことに, フルパスでの指定や LOAD_LIBRARY_SEARCH_SYSTEM32 を指定して SetDefaultDllDirectories() や 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 を使用する…といった対策が必要となります。

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

 そのような理由から, Windows 7 以降の場合のみ このタイミングで SetDefaultDllDirectories() を使用しています。 「スタイル指定が機能しない」のみなど制限を看過できるのであれば, 強引に ここで指定してしまっても構いません。 実際, UNLHA32.DLL が作成する自己解凍書庫については, 安全性に重きをおいて このタイミングで実行しています。 (結果, Windows Vista 以前の環境では旧スタイルで表示される。)

2.3:SetSearchPathMode() の実行

 SetSearchPathMode() が使用可能かチェックし, 使用可能であれば SetSearchPathMode(BASE_SEARCH_PATH_ENABLE_SAFE_SEARCHMODE) を実行して SearchPath() API でのカレントディレクトリー検索の優先度を最低にしておきます。 検索自体の行われることに変わりはありませんので, 注意が必要となります。
BASE_SEARCH_PATH_PERMANENT を付加するかどうかは, 仕様などプログラムの状況しだいですが, LHMelt においては付加した指定を行っています。

自プログラムが使用していなくとも外部プログラムが SearchPath() を使用している可能性がありますので, 自身が使用するかどうかにかかわらず安全策で指定しておくのが得策です。 また, SetDllDirectory("") が担保されているのであればカレントディレクトリーの検索されることはないわけですが, とりあえず指定しておきましょう。

3:独自ルールと検索方法でロードされる DLL の先行ロード

 前提条件の項で書いたように, Windows 7 環境での SSPICLI.DLL など, 「DLL のエクスポート転送」や「DLL リダイレクト」といった機能により, SetDefaultDllDirectories() API 等の設定に因らない, 独自ルールの検索と読み込みの行われる DLL が存在します。 これらは必ずといって良いほど (システム DLL 内で) 遅延ロードが行われるため, 起動時のみの動作確認では表面化しないケースが殆どです。 そして, SHBrowseForFolder() API の呼び出し時など, 忘れた頃に表面化します。

独自ルールが絡む上に SHBrowseForFolder() など COM が絡む API で発生するカレントディレクトリー変更が拍車を掛けて事態を複雑化しますので, 単純化が可能な起動時の段階で さっさとロードしておきましょう。 一旦ロードさえしておけば, 後からどのようなロードが行われようとも, 実質参照数が 1 つ増えるだけですので。

3.1:SSPICLI.DLL のロード

 独自ルーチンの MyLoadLibrary() を使用して SSPICLI.DLL のロードを行っておきます。 MyLoadLibrary() については後述します。

SSPICLI.DLL はネットワークへアクセスしようとした際などのセキュリティーサポートを行う DLL で, 上で書いたように「DLL のエクスポート転送」などによる独自ルールの検索が行われた上で SHBrowseForFolder() API などが使用された段階で遅延ロードが行われます。 初期化など前処理段階で使用する API がトリガーとなっていないため, 上述のとおり起動時のみの動作確認では表面化しません。 SSPICLI.DLL 絡みの脆弱性情報を良く見掛けるのは, このためです。

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

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

3.2:APPHELP.DLL のロード

 MyLoadLibrary() を使用して APPHELP.DLL のロードを行っておきます。

APPHELP.DLL はユーザーアカウント制御 (UAC) や互換モードの処理などに関わる DLL で, 「インストーラーを実行したら管理者アカウントの入力を求められた」「ソフトを実行しようとしたら互換性問題の報告画面が表示された」「ソフトを終了させた際に『正常に動作 (終了) したか?』の確認画面が表示された」などは, 全て この DLL が関与しています。 この DLL は, システム (Windows) の監視・調査結果に従って, プログラムが実行されようとした時から終了されるまでの, それが必要となった何れかのタイミングで, システムによりロード・実行されます。

ここで問題となるのは上述した内の前者 2 つのケースです。 というのも, そのタイミングが NTDLL.DLL により KERNEL32.DLL がロードされるのと同じか さらに前…つまり, 自プログラムへ制御が渡る前だからで, このタイミングでロードされたが最後, 脆弱性を回避することは出来ません。 しかし, 次のような策を施すことで, ロード自体を抑制若しくはタイミングを遅らせることが可能です:

  • マニフェストで requestedExecutionLevel を指定する
  • (英語であれば) "Setup" や "Install" などインストーラー…広義には管理者権限の必要性を匂わせる文字列データーやリソース名の使用を避ける
  • 環境の仕様に沿った形で API を使用する

requestedExecutionLevel については, 管理者権限を必要としないのであれば "asInvoker" を, インストーラーなど必要とするプログラムであるのなら "requireAdministrator" を明示的に指定することで, 当該プログラムロード時のシステムによるスキャンを抑制します。 uiAccess は拙作ソフトにおいては "false" を指定しています。

文字列使用については, そういった文字列の使用を避けることで, 主に上述の requestedExecutionLevel 指定が存在しない場合に行われるシステムスキャンで, 要管理者権限と判定される確率を下げることが出来ます。

API については, 例えば「Ver 5 形式のリソースなのに, Ver 4 の仕様で GetSaveFileName() API を呼び出す」といったようなちぐはぐな使用法を避け, 例えば Windows 7 なら 7 に合った形式, 古いものを使用せざるを得ないのであれば, せめて Ver 4 なら 4 の形式を…といった感じに仕様を合わせておくことで, スキャンにより互換性モードの発動する確率を下げられます。

これらの策を施すことでシステムによるロードタイミングを遅らせ, 自プログラムによるロードが間に合う確率を上げておきます。 ここで重要なのは「あくまでも間に合う確率を上げるだけ」な点です。 システムは例えばインポートテーブル (IMT) などにより使用 API についてもスキャンしますから, "asInvoker" を指定していたとしても UAC で管理者権限を求められるケースは発生します。

 というわけで, 自プログラムへ制御が渡る前に APPHELP.DLL ロードのトリガーが発生してしまうケースは避けられないわけですが, 幸い Windows 7 以降であれば「即ロード」には至らないため, 拙作を含む大抵のプログラムにおいては自前によるロードが間に合います。

一方, Windows Vista の場合は, 残念ながら自プログラムへ制御が渡る前の APPHELP.DLL ロードは避けられません。 なので, 一例を挙げれば, 管理者権限を明示的に指定…若しくは要管理者権限と判定された場合には, 必ず APPHELP.DLL に対する攻撃が成立してしまうことになります。

その場合においても, 単に同名の DLL を作成した程度では初期化ルーチンを含めて実際に攻撃 DLL のコードが実行されることはありません。 そういった意味では一定の保護が行われていることになります。 ただし, DLL のロード自体は試みられるため, 例えば実行ファイル以外の形式が使われた場合にはシステムによる「○○は Windows 上では実行できないか、エラーを含んでいます。」の警告が表示されることになります。
実行形式か どうかにかかわらず, どちらの場合も実際にロードされるのはシステムディレクトリー上の正規版 APPHELP.DLL となります。

ちなみに余談ですが, スキャンによりインストーラーの類いとシステムが判定した場合には, 「プログラムのアンインストールまたは変更」への登録が行われない…つまりアンインストール情報がシステムへ登録されないと, 当該プログラム終了時に APPHELP.DLL による警告画面が表示されます。

3.3:UXTHEME.DLL のロード

 MyLoadLibrary() を使用して UXTHEME.DLL のロードを行っておきます。

UXTHEME.DLL は, 名前から何となく想像できるようにスタイルなどに関与する DLL で, 何らかのウィンドウを作成したり, ウィンドウクラスの登録が行われたりしたタイミングで, スタイル指定の有無にかかわらずロードされます。 あまり複雑な関わり方をしていないので, それらが行われる前の このタイミングで自前ロードを行っておけば, ハイジャックを避けることが可能です。

3.4:先行ロードのまとめ

 拙作ソフトにおいては, 上で説明した SSPICLI.DLL, APPHELP.DLL, そして UXTHEME.DLL の 3 つについて先行ロードを行っておくことで, 独自ルールが絡む DLL についてはハイジャックを抑止できています。 ただ, それは「他の同様の DLL については, 今回の一連の手順を施すことにより, たまたま当該 DLL の遅延ロードに先んじて (依存関係の結果として) 明示的にロードできている」だけで, 上述した 3 つのみの対策を行えば良いわけではない点に注意が必要となります。

4:使用 API を擁する DLL のロード

 ここからは LoadLibraryEx() + GetProcAddress() による, 自プログラムが使用している各 DLL の動的ロードとなります。

4.1:GDI32.DLL のロード

 独自ルーチンの LoadApiLibrary() を使用して GDI32.DLL のロードを行います。 LoadApiLibrary() については後述します。 その上で自プログラムが使用する GDI32.DLL 内 API の全てについて GetProcAddress() によりアドレスをポインターとして取得しておきます。 当然ですが, API を使用する際には ここで取得したポインターを使用することになります。 GDI32.DLL は名前のとおり GDI レベルの描画を司る DLL です。

4.2:USER32.DLL のロード

 LoadApiLibrary() を使用して USER32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 USER32.DLL はウィンドウ関係や言語ロケール, キー入出力といったユーザーインターフェイスを司る DLL です。

4.3:ADVAPI32.DLL のロード

 LoadApiLibrary() を使用して ADVAPI32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 ADVAPI32.DLL はレジストリー関係を司る DLL です。

4.4:COMDLG32.DLL のロード

 MyLoadLibrary() を使用して COMDLG32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 COMDLG32.DLL は名前のとおりコモンダイアログを司る DLL です。

4.5:VERSION.DLL のロード

 MyLoadLibrary() を使用して VERSION.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 VERSION.DLL は OS の詳細バージョンやファイルのバージョンといった情報取得を行うための DLL です。

4.6:SHELL32.DLL のロード

 MyLoadLibrary() を使用して SHELL32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 SHELL32.DLL は関連付け起動やドラッグ&ドロップなどシェル機能を司る DLL です。

4.7:OLE32.DLL のロード

 MyLoadLibrary() を使用して OLE32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 OLE32.DLL は OLE32 関連や COM を司る DLL です。

4.8:HHCTRL.OCX のロード

 MyLoadLibrary() を使用して HHCTRL.OCX のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 HHCTRL.OCX は HTML ヘルプを司る ActiveX コントロールです。 DLL と同様に LoadLibraryEx() + GetProcAddress() が可能です。

5:COMCTL32.DLL のロード

 あえて LoadLibraryEx() ではなく LoadLibrary() を使用して COMCTL32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 COMCTL32.DLL はツールバーやステータスバーを始めとしたコモンコントロールを司る DLL です。

LoadLibrary() の使用は, Windows Vista や XP など, システムディレクトリーに Ver 5 の COMCTL32.DLL が配置されている OS において, Side-by-Side アセンブリーを機能させた Ver 6 以降の DLL ロードを行うための処置で, これを行わないと先に書いたようにスタイルによる表示やイメージリストの使用を行えなくなってしまいます。 もちろん, イメージリストに依存したコードとなっているのであれば, プログラムが起動しないか一般保護エラーが発生します。 運が良ければ「リボン (ツールバー) にアイコンが描かれない」程度で済むかもしれません。

作成するプログラムが Windows 7 以降のみを想定しているのであれば, 他の DLL と同様 MyLoadLibrary() によるロードでも構いません。

随分遅いタイミングでの COMCTL32.DLL ロードですが, 依存関係が絡むことから このタイミングでないと何らかの DLL に対してハイジャックが成立してしまうのでした。

6:SetDefaultDllDirectories() の実行 (Windows Vista の場合)

 bCanSetDefDllDirs が真で環境が Windows Vista である場合には, ここで SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) を実行して, システムディレクトリー (C:\Windows\System32) 以外からの DLL ロードを抑止します。

非常に遅いタイミングでの API 実行ですが, ここまでの各 DLL のロードについて LoadLibraryEx() でのフラグ指定を適切に行っておけば問題ありません。

7:CTL3D32.DLL のロード

 bIsExplorer が否の場合, MyLoadLibrary() を使用して CTL3D32.DLL のロードを行います。 その後 GDI32.DLL と同様 API アドレスの取得等を行います。 CTL3D32.DLL は旧スタイルにおいて立体的なコントロール表示を行うための DLL です。

CTL3D32.DLL のロードについては Windows NT 4.0 や Windows 95 以降であれば明示的に行う必要がありません。 bIsExplorer が否, つまり Windows NT 3.51 以前や Win32s 環境の場合に「あれば 3D ボタンなどの表示が可能」というもので, これまでの DLL と異なり, ロードに失敗してもエラーとしないオプション扱いの DLL となります。

8:アーカイバー DLL などオプション扱いな DLL や自プログラムとセットでインストールされる DLL のロード

 オプション扱いの DLL については, ここまでの処理を行った後であればロードタイミングを問いません。 必要なタイミングで独自ルーチンの SafeLoadLibrary() を使用して DLL のロードを行います。 API アドレスの取得等は これまでの DLL と同様です。 SafeLoadLibrary() については後述します。

9:処理手順のまとめ

 以上が DLL ハイジャックを避けるために行う一連の処理となります。 何と言っても手動で KERNEL32.DLL 以外の使用全 API について GetProcAddress() するのが面倒で, LHMelt 辺りでも 500 以上の API を, それ以外でも半分以上の 300 くらいは GetProcAddress() する地獄を見ています。 (^^;)  可能であればヘルパーライブラリーを使用した遅延ロード化を行ったほうが良いでしょう。

10:独自ルーチンでの DLL ロードについて

 ここからは上で登場した独自ロードルーチンについて説明します。 …とはいっても, SetDefaultDllDirectories() や SetDllDirectory() を行えない環境というものは存在しますから, そのような環境でのロードを考慮したルーチンを, DLL の性格に合わせて複数用意しているだけで, 特に難しいことをやっているわけではありません。

10.1:MyLoadLibrary()


HINSTANCE MyLoadLibrary(
  LPCWSTR lpLibFileName
)
{
    HINSTANCE hModule;
    WCHAR    szLoadLibrary[FNAME_MAX32 + 1];

    if (bCanSetDefDllDirs) {
        hModule = LoadLibraryExW(lpLibFileName),
            NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
    } else {
        if (bIsWin32s != TRUE) {
            GetSystemDirectoryW(szLoadLibrary, FNAME_MAX32);
            MylstrcatnW(szLoadLibrary, L"\\", FNAME_MAX32);
        } else {
            *szLoadLibrary = L'\0';
        }
        MylstrcatnW(szLoadLibrary, lpLibFileName, FNAME_MAX32);
        hModule = LoadLibraryExW(szLoadLibrary,
            NULL, LOAD_WITH_ALTERED_SEARCH_PATH);
    }
    return hModule;
}
	  

ソースを示しているようでソースではありません。 (笑)  話を単純化するために, 文字は WCHAR, バッファーのサイズを示す固定値を FNAME_MAX32 で統一し, そしてエラー処理を省いています。 さらに, API を直接呼ぶ形にしています。 その他のルーチンも同じです。 '{', '}' のインデントを揃えたい向きには悪いのですが, 行を減らすために縮めたお作法で書いてもあります。 (^^;)

MylstrcatnW() とか "My" が付加された API 代替文字列操作関数は, 要は「バッファー全体はこのサイズっ!!」の指定で済むようにしてある関数です。 何しろ Win32 の文字列操作 API は, バッファー全体を指定するように見せかけて実は「バッファーの残りサイズを指定」する必要があったり, バッファー溢れが生じた場合は, オーバーフローを起こさない代わりに終端記号の '\0' が書き込まれなかったり (しかも API による。) と, 沢山罠が仕掛けられていますので。 (ソースの見通しも悪くなりますしね。 ^^;)

脱線はこれくらいにして, MyLoadLibrary() はシステムライブラリーのロードに使用する関数なので, bCanSetDefDllDirs が真…つまり LoadLibraryEx() の追加フラグを利用できる環境なら LOAD_LIBRARY_SEARCH_SYSTEM32 を指定, 利用できない環境ならシステムディレクトリーのパスを付加した上で LOAD_WITH_ALTERED_SEARCH_PATH を指定して LoadLibraryEx() を呼んでいます。

bIsWin32s が真…つまり Win32s 環境では DLL 名のみの相対パス指定です。 さらに ここでは LoadLibraryExW() を直接呼んでいますが, 実際は lpLoadLibraryExW() となっていて, Win32s の場合は, フラグを全て無視した上で単純に LoadLibrary() を呼ぶ API 代替関数へのポインターとなっています。

10.2:LoadApiLibrary()


HMODULE LoadApiLibrary(LPCWSTR szLibrary)
{
    HMODULE     hModule;

    if (bIsWin32s != TRUE) {
        hModule = MyLoadLibrary(szLibrary);
    } else {
        hModule = MyLoadLibrary(L"W32SCOMB.DLL");
    }
    return hModule;
}
	  

LoadApiLibrary() は Win32s 対策の関数です。 Win32s では Windows NT や Windows 9x といった他の Win32 API 実装環境と違い DLL 体系が大きく異なっていて, リンカーを使用した暗黙的ロードについては, GDI32.DLL, USER32.DLL, そして ADVAPI32.DLL が指定された場合には, その指定を一切無視して W32SCOMB.DLL が適用されるようにシステム内部で変換が行われます。 つまり, IMT の DLL 情報と実際に読み込まれる DLL が異なるわけです。

なので, Win32s 環境の場合は強制的に W32SCOMB.DLL を, それ以外の場合は指定されたものを使用して MyLoadLibrary() を呼ぶようになっています。

10.3:SafeLoadLibrary()


HMODULE WINAPI SafeLoadLibraryW(
    LPCWSTR	lpFileName
)
{
    HMODULE     h;
    WCHAR       szFileName[FNAME_MAX32 + 1];

    h = NULL;
    if (bCanSetDllDir) {
        h = LoadLibraryExW(
                lpFileName, NULL,
                (
                    bForcedSys32 ? 0 :
                    LOAD_LIBRARY_SEARCH_APPLICATION_DIR
                ) |
                LOAD_LIBRARY_SEARCH_SYSTEM32
            );
    } else
    if (SafeSearchPathW(
                NULL,
                lpFileName,
                NULL,
                FNAME_MAX32 + 1,
                szFileName,
                NULL
            ) != 0) {
        h = LoadLibraryExW(
                szFileName, NULL,
                LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR |
                LOAD_LIBRARY_SEARCH_SYSTEM32
            );
    }
    return h;
}
	  

SafeLoadLibrary() はアーカイバー DLL などオプション扱いの DLL をロードするのに使用する関数です。 なので, bCanSetDllDir が真…つまり SetDllDirectory("") している環境においては LOAD_LIBRARY_SEARCH_APPLICATION_DIR と LOAD_LIBRARY_SEARCH_SYSTEM32 を指定して LoadLibraryExW() を呼んでいます。 アプリケーションディレクトリーとシステムディレクトリーの どちらかに DLL が存在すれば読み込んでくれるわけですね。

何やら bForcedSys32 が真の場合に LOAD_LIBRARY_SEARCH_APPLICATION_DIR を無効化するようになっていますが, これは「あくまでもシステムディレクトリーのみ許可」するための LHMelt のドキュメント化されていない隠しメニュー用の処理です。 LHMelt における任意の DLL 読み込みに関する脆弱性で書いているとおり, 「アーカイバー DLL と同じ名前の DLL を LHMelt がインストールされているディレクトリーへ配置する」ことでアーカイバー DLL を対象としたハイジャックが成立してしまいますので, それを防止するためのメニューです。

bCanSetDllDir が否…つまり SetDllDirectory() による担保が存在しない環境においては, 独自ルーチンの SafeSearchPathW() により DLL の検索を行った上で, その検索結果を使用しつつ LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR と LOAD_LIBRARY_SEARCH_SYSTEM32 を指定して LoadLibraryExW() を呼んでいます。 アプリケーションディレクトリー上で見つかるとは限りませんので, LOAD_LIBRARY_SEARCH_APPLICATION_DIR は指定していません。 要は「見つかった場所のディレクトリーとシステムディレクトリーのみ許可」ということです。

SafeSearchPathW() は名前から想像できるように SearchPath() の API 代替関数で, 要は自前で強制的にカレントディレクトリーを排除して SearchPath() と同様の検索を行っているわけです。 ややこしくなりますので ここでは示しませんが, 一番簡単に済ませるのであれば, 一時的にカレントディレクトリーをシステムディレクトリーにして (SetCurrentDirectory(GetSystemDirectory()) のノリ。) SearchPath() を呼び出せば良いでしょう。 最悪システムディレクトリーを 2 度検索することになりますが, 手を抜いているわけですから気にしてはダメです。 (笑)

最初に書いたように, この関数を使用してロードするのはオプション扱いの配置場所を問わない DLL なので, LoadLibraryExW() の追加フラグを使用できるか否かについては関知しないコードになっています。 使えない環境では単純に無視されますから, それで何の問題もありません。

10.4:独自ルーチン余談

独自ルーチン内で LoadLibraryExW() を呼ぶ際に, 例えば SetDefaultDllDirectories() による担保が存在するのであれば必要ないはずの LOAD_LIBRARY_SEARCH_SYSTEM32 を指定していたり, 明示的に絶対パス付きでライブラリーを指定していたりするのは, 担保されていない環境用のコードと共用化することで, 「担保されているはずが されていなかった」場合についても悪あがきするようになっているためです。

自プログラムが前もって SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) していたとしても, 読み込んだ外部の DLL が それを上書きするかもしれませんし, 監視ソフトなど接触してくる外部プログラムが同様に上書きしてしまうかもしれません。 そういったケースを想定して保険を掛けてあるわけです。

ただ, この場合「指定したフラグと付加した絶対パスに整合性をもたせる」必要があります。 整合性が保たれていないと指定したフラグやパスの結果が不定となってしまいますので, 注意が必要となります。

11:対策コードの限界

 ここまで対策手順を簡単に示してきたわけですが, それなりの手間を掛けて策を施した上でも, 例えば拙作ソフトにおいては, なお以下のような限界が存在します:

  • Windows 10 や Windows 8/8.1 については現状回避できているものの, OS のアップグレードや修正パッチの適用, 対策ソフトの変更といった環境の変化により DLL の依存関係が変化することで, 対応できなくなる可能性がある。
  • Windows 7 については, 少なくとも SSPICLI.DLL について独自ルールの検索と読み込みが行われていることから, 追加の対策が必要となる (対応済みではあるものの, 危険性が高いといえる。)
  • Windows Vista については, KERNEL32.DLL が NTDLL.DLL により読み込まれる時点で APPHELP.DLL に対する DLL ハイジャックが成立してしまうことから, 完全な対応は不可能。LHMelt など管理者権限を要しないプログラムでは発生しないケースが多いものの, 「成立する」と考えておいたほうが得策。
    なお, APPHELP.DLL に限って言えば, 単に同名の DLL を作成した程度では, 初期化ルーチンを含めて実際にコードが実行されることはない。 ただし, DLL のロードが試みられるため, 実行ファイル以外の形式が使われた場合, システムによる「○○は Windows 上では実行できないか、エラーを含んでいます。」の警告が表示される。 どちらの場合も, 実際にロードされるのはシステムディレクトリー上の正規版 APPHELP.DLL となる。
  • Windows XP 以前についてはシステムに保護機能が備わっていず, 多数の DLL が Vista での APPHELP.DLL と同じ状況に陥っていることから, 対応自体が不可能。
  • LHMelt を例とすれば, そもそも「アーカイバー DLL と同じ名前の DLL を LHMelt がインストールされているディレクトリーへ配置する」ことで, アーカイバー DLL を対象としたハイジャックが成立してしまう。
    これについては, 全てのアーカイバー DLL をシステムディレクトリーへインストールした上で, 隠しメニューの「システムディレクトリのみ許可」を有効にすることで回避が可能です。 ただし, LMLzh32.DLL やバージョン情報表示ダイアログ用 DLL 等は読み込まれなくなりますので, 注意が必要となります。 (LHMelt と同じディレクトリー上へインストールされているため。)  その辺りもあって, 隠しメニューとした上で既定が無効となっています。 DLL ロードの挙動を把握できる方のみ設定を お使いください。
  • UNLHA32.DLL を例とすれば, そもそも UNLHA32.DLL を呼び出す側のプログラム (例えば LHMelt) が この脆弱性に対応していないと, UNLHA32.DLL 側での対応が無意味となってしまう。
  • 自プログラムへ接触してくるプログラムが この脆弱性に対応していないと, 自身側での対応が無意味となってしまう。 監視ソフトの類いが これをやらかすと被害は甚大で, 自プログラムのせいにされたりと, さらに火の粉まで降りかかってくることがある。
  • Windows 7/Vista については, KB2533623 の更新プログラムを適用していることが前提。 これが適用されていないと SetDefaultDllDirectories() API などが使えないことから, 根本的に対応が破綻する。

 反対に, SetDllDirectory() や SetDefaultDllDirectories() の項で説明したように, それを実行することで他プログラムへ影響を及ぼしたりもしますので注意が必要となります。 特に注意が必要なのは LoadLibraryEx() で, ここで指定したフラグは「自身がロードしようとした DLL 及び当該 DLL と依存関係にある全ての DLL」と, 想像以上に広範囲へ適用されますので十分なテストが必要となりますし, また, 上述した UNRAR32.DLL の場合など, 影響を避けられないケースも発生してきます。

12:全体のまとめ

 以上, ここまで DLL ハイジャック対策について書いてきたわけですが, 前提条件の項で触れたように, ここで示した手順は あくまでも「拙作の UNLHA32.DLL (と それが作成する自己解凍書庫), UNARJ32.DLL, そして LHMelt といったプログラムで通用しているもの」であって, 参考には出来ても それが自身のプログラムに適用可能とは限らない点に注意が必要となります。

反対に, 「少なくとも拙作ソフトにおいては, 上述した手順 (内容と処理順を含む。) が必要だった」点についても押さえておいてください。

 …と, これくらい書いておけば 5 年後の自身もソースの内容を理解してくれることでしょう。 (笑)

Jun.3,2018 追記

 「Windows アプリケーションにおける DLL 読み込みに関する脆弱性について」のほうで触れてあったことから, こちらでは特に言及しなかったのですが, デジタル署名の確認などは, こちらのソースもどきで直接呼んでいる LoadLibraryEx() API の中で行うことになります。 (実際のソースでは直接呼んでいません。)

8 年前の「マイクロソフト セキュリティ アドバイザリ : 2269637 安全でないライブラリのロードにより、リモートでコードが実行される」…つまりカレントディレクトリー型 DLL 読み込みの脆弱性と異なり, 今回のアプリケーションディレクトリー型 DLL 読み込みの脆弱性は, 「他人 (=自プログラム以外) が他人をロードする際」に (自身への) 攻撃の成立する点が重要であって, 「それをどう防ぐか?」という観点においては, デジタル署名の確認などは本質から外れた余計な処理で, 見通しが悪くなるだけでしたので…。

何件か質問をいただいたので書いておきます。

 あと, 自身のプログラムについてチェックするのに一番簡単な方法は, テキストファイルで構わないので, 攻撃用 DLL 等を 2000~3000 ほど拵えて, そこに自プログラムを置いて起動や操作一通りをテストすれば良いと思います。 「○○は Windows 上では実行できないか、エラーを含んでいます。」のエラーが表示されたらアウトと判別も簡単です。  その際, くれぐれも「起動と終了だけ」で済まさないように。 それで済めば大手ベンダーを含めて誰も再発などで苦労していません。 (笑)

テスト用攻撃 DLL (テキストファイルですが ^^;) 一式は, 次のコマンドで作成が可能です:


for /f %i in ('dir /b c:\windows\syswow64\*.dll c:\windows\syswow64\*.ocx
    c:\windows\syswow64\*.drv') do @echo "test dll" >%i
		

スクロールバーが表示されると表示レイアウトが崩れてしまうので 2 行にしていますが, 実行する際は 1 行で入力することになります。

コマンドライン上でこれを実行すると, システムディレクトリーに存在する DLL や OCX などのダミーがカレントディレクトリーへ作成されます。 これは「64 ビット版 OS 上での 32 ビット版アプリ用」なので, "syswow64" の部分は適当に変更してください。

「自プログラムと同じ場所へ置くことを想定した DLL」などについては本物を置いてください。 ただし, 「あくまでもシステムディレクトリーのみ許可」する設定等が存在するのであれば, それらについてもダミーを置くことになります。

Jun.9,2018 追記

 「4:使用 API を擁する DLL のロード」の目的ですが, Windows アプリケーションにおける DLL 読み込みに関する脆弱性についての「3:KERNEL32.DLL 以外についてインポートライブラリーを使用しない」で書いたように, そもそもインポートテーブル (IMT) の存在が脆弱性に繋がっていることから, 排除しようのない KERNEL32.DLL を除いて, 残り全ての DLL に関する情報が登録されないようにしているわけです。

拙作プログラムでは そこまでやっていないので, KERNEL32.DLL の使用 API を含めた情報が IMT へ記録されていますが, 対策ソフトの一部インストーラーなどは それすら排除されていて, 残骸といえる "KERNEL32.DLL" の文字列 1 つのみ…なんてところまで徹底されています。 (^^;)  当然, 今回の記事で書いたような点に全て対応済みなのは言うまでもありません。

そこまで徹底しておいて初めて「作成した新規ディレクトリー上で子プロセスを実行」に意味がある…という点は頭の隅に置いておいたほうが良いでしょう。 まぁ, チェックを徹底しすぎて攻撃 DLL が存在すると無限ループに陥ってしまったりする某対策ソフトのインストーラーなどもありますけれど。 (笑)

 あと, これは余談ですが, 今回の一連の手順は ある意味「使用 API を隠蔽した上あれこれ複雑化した手順を踏んでいる」…つまり程度は軽くともマルウエアと同じことをやっていますので, 対策ソフトの類いに危険認定される確率は確実に跳ね上がります。 特に Symantec 辺りは高確率で抹殺してくれますので, 「作者による提出」モードでの検体提出が必要になるでしょう。 (^^;)

●May.21,2018

Windows 10 1803 と 2018-05 累積更新パッチ...

 ゴールデンウィーク中の今月 1 日 (米国現地時間 4 月 30 日) に Windows 10 の最新版である April 2018 Update が公開されました。 概ね半年ごとに繰り返されるアップグレードで, 最初の版である 1507 から 1511, 1607, 1703, 1709 そして 1803 と今回が 5 回目に当たります。 それだけなら良かったのですが, 問題は これが「大型アップデート」ではなく実質「アップグレード」であることです。 同じ Windows 10 の名前を冠していても例えば 1709 と 1803 は別物で, ちょうど Windows 7 から 10 へ上げたのと同じように互換性や不具合の問題が発生するのでした。

実際自身も互換性問題や不具合の発生に遭遇していて, こうなると「気軽にアップグレード」とは行きません。 何しろ Windows 10 では「回復」を始めとしたロールアップ系処理の信頼度がボロボロで, 特にリカバリー領域の存在するノート系に至っては, 10 台以上の PC で未だかつてロールアップに成功したためしがないのですから。 (笑)  とはいうものの, 自作ソフトの動作確認は必要なので, メイン使用している東芝 dynabook Qosmio T851/D8CR について, 2TB SSD→2TB SSHD の丸ごとコピーという保険を掛けた上で 1 日夜から 2 日にかけて 1803 へのアップグレードを敢行しました。 2 日掛かっていますが, ダウンロードを始めたのが 23 時頃だったのでダウンロード終了を待たず就寝…という毎度のパターンで, 実際には半日程度の勘定です。

結果ですが, 巷と異なり幸い手元の環境では不具合等は発生しませんでした。 前に書いたとおり Windows 7 から 10 へのアップグレードの時点で東芝謹製のプリインストールソフトや特殊ハード (裸眼 3D など) は全滅していますので, 互換性問題も皆無です。

 さらに連休が終わった 9 日, 今度は毎月恒例の Windows Update で 手元の環境では 10 個ほど更新プログラムが配信されました。 巷の情報によると その中の一つ「2018-05 x64 ベース システム用 Windows 10 Version 1803 の累積更新プログラム (KB4103721)」が曲者で, これが 1803 自体と変わらないほど数々の不具合や互換性問題を発生させる代物のようです。 幸い何も問題は発生しませんでしたけれども…。

というわけで右上画像のような感じとなったわけですが, 一つだけ違和感が…。 それは拙作 LHMelt のバージョン情報で, クリックして拡大表示させると判るのですが, Windows の版が「Windows 10 Enterprise x64」となっています。 一方 Windows 設定画面のバージョン情報では「Windows 10 Pro」と本来のエディションが表示されています。 「はて?」と思いながら少々調べてみたところ, システム…つまり x64 側レジストリーのエディション情報が Pro なのに対して, WoW64 (x86) 側のレジストリーでは Enterprise となっているのでした。

当該情報は単なる文字列ですし特にシステムの動作に違いが発生しているわけでもなく特に問題はないのですが, 何かモヤモヤします。 (笑)  そこで, アップグレード方面のテストを兼ねて外の環境でも試してみることにしました。

 さて, まずは同じ Windows 10 Pro ではありますが, VMware Workstation 12.5.8 Pro のゲスト環境を試してみました:

上画像, ホストと同じ Windows 10 Pro (x64) 環境ですが, こちらでも WoW64 側では Enterprise 表記となっていました。 Twitter で呟いてみたところ巷でも発生しているらしく, どうやら「たまたま」ではなく普遍的に発生する現象のようです。 仕様なのかバグなのかは判りません。

続いて下画像, 今度は 32 ビットの x86 環境で 1803 へのアップグレードを試してみたところ, …出ました。 KB4103721 のパッチ当てが終了したあとの再起動でブラックアウト現象が。 (笑)  ログイン画面までは普通に進むのですがログインした途端ブラックアウトします。 こうなると巷の情報どおり「スリープ→復帰」か「『Win キー + Ctrl + Shift + B』同時押しによるグラフィックドライバーのリセット」を行わないと復帰できません。

ただ, 仮想環境だからなのか「発生するときは再起動しても連続して発生」「発生しないときは何度再起動しても発生しない」と頻度にムラがあるようです。 それはともかく, 頻繁に遭遇するのは困りものですので, 仮想環境のメリットを活かしサクッとパッチ当てを「無かったこと」にして再度パッチを当てたところ, 現象は発生しなくなりました。 普通に起動するようになりましたので LHMelt のバージョン情報を確認してみたところ, 32 ビット環境では「Windows 10 Pro」の表記でした。 「32 ビットだから」ではなく「WoW64 だから」の現象のようですね。 >Pro なのに Enterprise 表記なレジストリー

 次に適当な実機を選定し試してみたのですが, 64 ビット機 4 台, 32 ビット機 1 台と都合 5 台を試したにもかかわらず, 1803 と KB4103721 の どちらでも何も問題は発生しませんでした。 何も発生しないと非常に悲しいのですが, 出ないものは仕方がありません。 あ, 64 ビット機では上述の Enterprise 表記現象が発生しています, 念のため。 ともあれ, 最後にタブレット系を試すことにしました:

まずは左画像の GamePad Digital GPD Pocket です。 こちらは WoW64 側でも「Windows 10 Home」と正常な表記になっています。 「WoW64 側が全滅」というわけでも無さそうで, 謎は深まりました。 バグではなくて仕様なのかもしれませんね。 (^^;)  あ, フォントや女の子の画像がボヤけ気味なのは, 画面設定が 125% の拡大表示になっているからです。 7 インチでフル HD では, さすがに字が小さすぎて辛いものがありますので。 (^^;;

続いて右画像の Lenovo Miix 2 8 (32GB モデル) です。 ストレージが 32GB しかないところへ Genymotion + VirtualBox な Android 4.3 環境を構築してありますので, 空き容量確保が大変だったのですが, 1803 へのアップグレードや KB4103721 で問題の出ることはありませんでした。 GPD Pocket の Atom x7-Z8750 辺りと異なり Atom Z3740 や Z3775 では そろそろアップグレードできなくなるかとも思ったのですが, 今回は まだ上げさせて貰えたようです。 恐らく今回が最後でしょうね。 (^^;)

 …と, 都合実機 8 台 (64 ビット 6 台, 32ビット 2 台) と仮想環境 2 台 (64 ビットと 32 ビットが 1 台ずつ) で 1803 へのアップグレードと KB4103721 の適用を試したところ, 唯一仮想 32 ビット PC のみで不具合が発生しました。 頻度としては そんなに高くなかったわけですが, 「仮想環境でも発生する」というのがなかなか興味深い結果と言えそうです。 いえ, 結果として解消できたからこそ言えるわけですけれども…。 (笑)

拙作ソフトの動作確認テスト環境 (常時) 覚え書き...

 上の記事の余談として書く予定だったのですが, こちらのほうが長くなりそうだった上にカテゴリーも異なることから, 別記事として書くことにしました。 同じ画像が使われていたりするのは その名残です。 (笑)

それはともかく, 今回 UNARJ32.DLL の修正で久々に実機を含む手元の環境全てで動作確認を行う必要がありましたので, その中から修正の大小にかかわらず毎回必ずテストを行っているメイン環境上のものを選んで, 当該環境の選択理由や拙作ソフトでの対応内容などを, LHMelt のヘルプ風に簡単に纏めて覚え書き代わりに書いておくことにします。

 いろいろなところで時々書いているように, 拙作ソフト…特に UNLHA32.DLL と LHMelt については Win32s 環境での動作が必須となっています。 というのも, 未だに要望や質問メールが飛んでくるからなのでした。 ただ, 昔なら国内外含めて 15 カ国ほどから飛んできていたメールも, 今では生き残りがフランスとチェコだけになってしまいましたけれど…。 随分粘っていた米国辺りも ついに絶滅したようです。 (^^;)  もっとも, 「絶滅」といってもメールが飛んでこないだけで, 潜在的には存在していると思います。 どうしようもなく古い環境が必要…というケースはありますから。

結果, Win32s…つまり MS-DOS 6.2/V + 日本語版 Windows 3.1 から Windows 10 1803 までの各 OS でテストを行う事態を招いています。 (笑)  10 年ほど前までなら実機も残っていたのですが, さすがに殆どが廃棄されてしまい, 今では基本的に仮想 PC ソフトを使った仮想環境上でのテストとなってしまいました。 救いは実機と仮想の違いが影響するようなジャンルのソフトではなかったことかしら?

 さて, 前段の余談はこれくらいにして各環境の説明に入っていきましょう。 まずはホスト環境である実機の東芝 dynabook Qosmio T851/D8CR です:

 OS は Windows 10 Pro (x64) です。 Windows 8.1 の後継バージョンとして 2015 年に発売された現在最新の OS で, Windows NT としてのバージョンは 10.0 と OS 名に合わせた数字へ引き上げられています。 (Windows 8.1 は 6.3)

名前の由来は「7 番目だった Windows 7 から数えて 3 番目」…つまり, 6.1 (Windows 7) → 6.2 (Windows 8) → 6.3 (Windows 8.1) → 10.0 (Windows 10) ということのようです。

スタートメニューの復活や UWP アプリのオーバーラップウィンドウ化など, Win 8/8.1 での失敗を教訓に むしろ Win 7 へ先祖返りしている感もありますが, セキュリティー方面の強化を筆頭に中身自体は大きな変更が加えられています。

この OS を使う上で最大の難点は「半年ごとに繰り返される互換性問題を伴った大型アップデート」でしょう。 もはやアップグレードレベルで, 毎回互換性問題や不具合の発生で巷が阿鼻叫喚に陥っています。 幸い手元では Windows 10 へのアップグレード時と 1607 の頃以外大きな問題には遭遇していません。 あとは, 近々 32 ビット版アプリ…どころかデスクトップアプリそのものの使えなくなる運命が待ち受けていることでしょうか?

拙作ソフトでは特に当該 OS 個別の対応は行っていません。 何か挙げるとすれば Win 10 で またもや変更が必要になった Win のバージョン判定ルーチンくらいでしょうか? 本当は DLL 読み込みの脆弱性絡みで PreferSystem32 辺りに対応したいところなのですが, その変更が逆に脆弱性の発生に繋がりかねないので, 様子見を決め込んでいます。 幸い Windows 7 までの対応で完全に抑え込めていますし…。 (^^;)

あと, デザインを含めた描画周りの変更にも本当は対応しないとならないのですが, ことデザインに限っては「何が悲しくて Windows 3.1 もどきへ退化させる必要があったの?」と思っていることから, 放置しています。 (笑)

テスト環境としては「巷の一般的な環境」としての役目を担っていますが, 元々が Windows 7 Home Premium だったものを Ultimate へアップグレードし, さらに Windows 10 Pro 1511 へ, 続いて 1607, 1703, 1709 そして 1803 と 4 回の大型アップデートを経ている環境である点は, 頭に置いておく必要がありそうです。

そういえば, 1703 で「圧縮 (lzh 形式) フォルダー」が使えなくなりましたが, あれは脆弱性方面は全く関係なく, 単に日本語版対応のプライオリティーがガタ落ちしたからに過ぎませんので, 念のため。 (^^;)  Windows 8 辺りから顕著になり始め Windows 10 で さらに酷くなりましたが, もはや日本語版は他の超マイナー言語版と同じプライオリティーでしかなく, 縦書き方面を含めて今の MS に まともな日本向けの個別対応を望むのは無理でしょうね。

1803 といえば, タスクバーが殆ど不透明となってしまった上に その透明度を上げられなくなった点が, 個人的に激しく許せなかったりします。 (笑)

それはともかく, 2011 年 8 月の購入と近々 7 年にもなろうという最長不倒の長期政権を樹立していて, 退役は近かったはずの Qosmio T851/D8CR なのですが, MZ-75E2T0B/IT…つまり 2TB SSD な SAMSUNG 850 EVO へ換装したことによる体感速度向上の威力が凄まじすぎて, あと 5 年くらい…要は当該機の寿命まで使えてしまいそうな気配です。 (笑)

 ここからは VMware Workstation 12.5.8 Pro を使用してホスト上に構築されたゲスト環境になります。 VMware Workstation 14 Pro を買ってはあるのですが, 以前書いたとおり, 仮想環境が まともに動作しなくなってしまうため 12.5 を使い続けざるを得なくなっています:

 ホストと OS が同じ Windows 10 Pro (x64) 環境ゲストです。 上述したとおり一般的な環境を想定した役目を担っているホストですが, VMware を始めとした各種仮想 PC ソフトに仮想 DNS…といった面々を筆頭にいろいろなソフトを入れすぎて, もはや一般的な環境とは言えなさそう…というわけで, こちらは純粋な環境としての役目を担っています。 経験則的には, 「報告された不具合がホストでも この環境でも発生しない」という場合は, インストールされているソフトなど報告者の環境固有の問題であるケースが殆どです。

拙作ソフトは どれも 32 ビット版なので, どうしても 64 ビットである x64 版 OS では対応しきれない部分が発生してしまいます。 一番目立つものとしては「ダイアログ系のウィンドウがアプリの中央ではなくデスクトップの中央に表示されてしまう」現象が挙げられます。 対応は可能なのですが, Win9x や Win32s など 16 ビット版 OS への対応もあって少々面倒なのと, 巷のソフトでも同様のものが多数存在していることから, 放置されているのが実情です。 (^^;)

ちなみに, ホスト PC が更新された暁には, ゲスト名のとおり Qosmio T851/D8CR を再現した環境としての役目を担うことになり, 現在ホストに入っている主要ソフトが こちらへインストールされることになります。

 Windows 10 Pro (x86) 環境ゲストです。 Windows 10 や Windows 7 など, Windows XP 以降の OS には x64 (64 ビット) 版と x86 (32 ビット) 版とがあって, Intel 系であれば Core 2 Duo 辺り以降の Intel 64 / AMD64 対応 CPU を搭載している PC であれば双方使用が可能です。 昔はデメリットのほうが大きい場合も多々…と言わざるを得なかったのですが, 今では x64 版のほうが普通となっています。

とはいえ まだまだ x86 環境も使われていることから, そちらのテスト用として当該環境が用意されています。 上の記事で書いたように x86 環境と WoW64 環境でも状況が異なってきますから, 別途専用のテスト環境は必要なのでした。

拙作ソフトとしては 32 ビット環境がホームなので, 「32 ビット環境への対応」というものは行っていません。 反対にプログラム的には数値を筆頭とした諸々のデーターが 64 ビットで扱われていますので, 所謂「32ビット (4GB) の壁」はソフトレベルとしては存在しません。

 Windows 8.1 Pro (x64) 環境ゲストです。 不評だった Windows 8 の後継として 2013 年に登場した OS です。 スタートボタンが復活したりもしましたが, そのわりにボタンをクリックして表示されるのは全画面なスタート画面のままと, ユーザーにしてみれば「それなら無いままのほうがマシ」と言いたくなる変更でしかなかったことから, 不評を払拭するには至らず, 早々に Windows 10 へ切り替わることとなりました。 (^^;)

「Windows 8/8.1 時代のタブレットを Windows 10 へアップグレードすると使い物にならなくなる」というケースが意外とあったことから, この OS に留めて使用しているユーザーも多かったりします。 (自身も その一人)

次の Windows 8 もそうなのですが, Windows 8/8.1 系の OS は, Modern UI 実装が影響してか Windows 7 までと Windows 10, つまり 8 系以外の OS と異なる挙動を示すことの多い点が, ソフト作成での難点となっています。 特にコモンダイアログ系において顕著で, 例えば UNLHA32.DLL が作成する自己解凍書庫においては, 設定したはずの初期値が画面表示までにクリアーされてしまう…といった現象が発生しています。

さらに, DLL 読み込みの脆弱性方面では「SetDefaultDllDirectories() API 実行などの影響で暗黙的ロードのできない DLL が発生すると, NTDLL.DLL が一般保護エラーを起こす」といった現象も発生していて, 安全性の意味でも むしろ Windows 7 辺りより状況の悪くなっているイメージを受けます。 その辺りもあって, 「呼び出される側のプログラム」である UNLHA32.DLL や UNARJ32.DLL などでは SetDefaultDllDirectories() を使用していません。

 Windows 8 Pro (x64) 環境ゲストです。 Windows 7 の後継としてスマホやタブレットなどとの融合を目指して 2012 年に発売された OS です。 その過程で Modern UI や Store アプリが採用されもしましたが, スタートメニューの廃止などモバイル系デバイスを意識しすぎたことから大不評を買ってしまった経緯があります。

Windows 10 辺りでもそうですが, 個人的には「タッチパネルでの操作を重視したわりには, タッチパネルのみで完結できない手順の存在する, 何時まで経ってもキーボードとマウスの必須な操作体系」なのが最悪だと思っています。 (^^;)

Modern UI といえば, これに対応した Store アプリが Windows 10 では使えない…というのも凶悪ですね。 自身は 1000 円程度のアプリだったことから笑って済ませられる程度の被害で済んでいますが, 高額アプリを買った方だと「金を返せ!!」と (MS に) 言いたくなることでしょう。 (^^;)  あと, 「画面分割で 2 つの Modern UI アプリを同時表示」とか謳っていましたが, あの MS-DOS 5.0 の分割画面にすら劣る画面のどこに宣伝効果が有るというのでしょうか? 甚だ疑問です。 (笑)

拙作ソフト的には個別対応は行っていませんが, 何か挙げるとすれば, この OS から それまでのバージョン取得の方法の通用しなくなった点でしょうか?  それはともかく, この OS から色々と迷走…といいますか, おかしくなり始めた気がします。 ほんと, MS はモバイルが絡むと碌なことがない…といいますか, 昔も今もモバイル方面への対応能力がないベンダーだと言わざるを得ません。 (笑)

 Windows 7 Professional (x64) 環境ゲストです。 Windows Vista の後継バージョンとして 2009 年に発売された, まだまだ主流の一つと言っても過言ではないだろう OS で, Windows NT としてのバージョンは 6.1 と, Windows 2000 (5.0) に対する Windows XP (5.1) と同様, 構造的な変更は比較的大きいものの, 基本的には改良版 Vista と言えるものになっています。

名前の由来は「Windows 1.0 から数えて 7 番目」と説明されていたりもしますが, 「1.x (Windows 1.0) → 2.x (Windows 2.0/286/386) → 3.x (Windows 3.0/3.1) → 4.x (Windows 95/98/98SE/Me) → 5.x (Windows 2000/XP) → 6.0 (Windows Vista) → 6.1 (Windows 7)」という数え方は, どう考えても おかしいでしょう。 わりと異なる 5.0 な Windows 2000 と 5.1 な XP を同列で扱っているくせに, 6.0 な Vista と 6.1 な Windows 7 を別勘定としているわけですから。 (笑)

Windows XP 同様安定した OS であることから根強い人気を誇っていて, サポートが終了する 2020 年 1 月まで使い続ける方も多いと予想されます。 自身も こちら (Windows 7) のほうが好みですね。 開発の関係上最新版を追い掛ける必要がありますので, ホストは Windows 10 へ上がっていますけれど…。

こちらは「純粋な環境」組の役目を担っていますので, Office すらインストールされていません。 ある意味それが問題となるケースも有りそうですが, それは他の Office インストール済み環境が担ってくれることでしょう。 ちなみに, 「純粋な環境」からは外れてしまいますが, ホストで動作しなくなった Parallels Workstation 6Windows Virtual PC だけはインストールする予定となっています。

拙作ソフト的には, DLL 読み込みの脆弱性方面で SSPICLI.DLL や APPHELP.DLL への対応といった個別対応を行っていますが, 今では この Windows 7 環境が対応の基本となっています。 Vista で始まった UAC (アカウント制御) も まともになり, 「Vista でやりたかった事が漸く完成された」といった感がありますね。

Windows 10 ほどではありませんが, この OS から本格的にシステム DLL が保護されるようになり, Vista までと異なり常に最新の DLL がシステムディレクトリーに配置される分 Side-by-Side アセンブリーへの対応も楽…と, 脆弱性方面の対処は随分楽になっています。 そういった意味では「Windows 7 以降のみ対応」とするのが吉かしら?

あと, 描画 (グラフィックドライバー) 方面では, Vista で変更された構成が Win 7 で またもや変更…と, ソフトによっては影響があるかもしれません。 拙作ソフトではプログレスバーなど一部コントロールでのメッセージ発出のタイミングくらいしか影響がありませんでしたけれども…。

 Windows 7 Ultimate (x86) 環境ゲストです。 Win 7 系での一般環境を想定して前作メインの東芝 Satellite WXW/78DW を再現したような環境になっていて, VMware の Direct3D 方面のテスト環境も兼ねていることから, ゲーム系を含めて色々なソフトがインストールされています。 (笑)  それはともかく, Win 7 も主要 OS 組…というわけで, x64 と x86 双方の環境を用意しています。 ちなみに, 最新の Windows 10 から この Windows 7 までは今でも実機が残っていて, そちらでのテストも可能となっています。

拙作ソフト的に個別対応等は行っていませんが, Windows 10 以上に x64 環境との差異が存在しますので, そういう意味でも専用環境を用意する必要があるのでした。 なので, Win 7 が主要 OS 組の座から落ちたとしても, x64 と x86 両方の環境が残り続けます。

そういえば, サーバー版 OS ではありますが「コンシューマー向けで使われたりもする」という意味で, Windows Home Server 2011 のゲストは用意しておいたほうが良いかもしれませんね。 近々試してみましょう。

 これ以降は 32 ビット (x86) 環境のみとなります。 まずは Windows NT 系です:

 Windows Vista Ultimate (x86) 環境ゲストです。 2007 年 (ボリュームライセンス版は 2006 年) に発売された, Windows XP の後継となる OS です。 インターフェイスを始めとして いろいろと刷新が図られましたが, 結果論としては ある意味過渡期といえそうな OS で, 次の Windows 7 で完成した感があります。

巷的には「Windows Me 並みに不安定」と言われたりもしましたが, 自身に限っていえば そこまで酷い状況に遭遇したことはありません。 職場のクライアント PC でも Vista の時代は ありましたが, 周り共々普通に使えていましたし…。 ちなみに実機も残っていますが, 東芝 dynabook SS 1610 90C/2 はまだしも, SHARP WILLCOM D4 は もはやテスト環境としては使えない気がします。 (笑)

この OS で最大の変更点は何と言っても WDDM ドライバーと Aero の実装で, それに伴いプログレスバーなど基本コントロールの挙動すら変わってしまい, 拙作ソフトでも色々と対処が必要になっています。 また, UAC が実装されたのも この OS からで, それに対応するためのマニフェスト追加なども行っています。

UAC といえば, マニフェストで制御し管理者権限を求められることのないソフトでは大丈夫なのですが, インストーラーなど逆に管理者権限を指定するケースや, システムによる自動判定で管理者権限での実行を強制された場合 (時には強制されなくとも。) には, NTDLL.DLL により KERNEL32.DLL が読み込まれる時点で APPHELP.DLL に対するハイジャックが成立してしまいます。 こと DLL 読み込みの脆弱性方面に限っていえば, 残念ながら Vista においては完全な対処が不可能です。

さらに, APPHELP.DLL が問題とならないケースでも, Windows XP や Vista では Side-by-Side アセンブリーで用意されたものよりも古い DLL がシステムディレクトリーに配置されていたりするため, 脆弱性への対応が難しくなっています。 可能なら Vista への対応を切ってしまうのが一番ですが, 不人気ながら意外と今でも使われているような気がします。 (^^;)

 Windows XP Media Center Edition 2002 (x86) 環境ゲストです。 Windows XP は 2001 年に発売された Windows 系において長らく主流の座を占めていた OS で, Professional と Home という 2 つの主要エディションがあります。 Home はコンシューマー向けで普通に PC を買うと これのプリインストールされているケースが多いのに対して, 職場では Professional が使われます。 この 2 つ以外では当該環境の MCE 2002 や TPCE 2005 なども存在しますが, それらは Pro が基本となっています。

Windows 95 が登場した頃から Windows NT 系への統一と移行を目論んでいた MS ですが, 6 年前後の時を経て, この Win XP 登場により漸くその夢が果たされたことになります。

この OS 最大の特徴が, 角の丸いウィンドウや より立体的でカラフルになったタスクバーを始めとしたコントロールなど, 所謂スタイルの実装で, これを実現するために, 拙作ソフトでもマニフェストの組み込み…つまり Side-by-Side アセンブリーへの対応化が行われています。 さらに, ツールバーなどがイメージリストを使ったものへ変更されてもいます。

DLL 読み込みの脆弱性方面については, Windows XP までは OS 側で何も保護対策が施されていないことから, 対応不能のボロボロな状況です。 (笑)

ちなみに, この Win XP MCE 2002 ゲストが拙作ソフトの開発環境だったりします。 なので, 上述した Windows 7 Ultimate (x86) 環境以上に種々のソフトがインストールされていて, 左上画像をクリックして拡大すると判るのですが, ATOK すらインストールされている辺りが それを物語っています。 (笑)  逆に, ソフトをインストールしすぎたせいで, 開発環境にもかかわらず次で説明する Windows XP Home 環境と比べて少々不安定になってしまっています。 サウンド周りなので殆ど無視できる程度なのが救いかしら? (^^;)

 Windows XP Home Edition (x86) 環境ゲストです。 コンシューマー向けの OS で Windows 9x 系後継としての役目を担っていたことから, 多少 Professional とは毛色が異なるものとなっています。 最大の違いが Win 9x と同じ「万人 (全てのアカウント) が管理者」という変態仕様で, 何のための NT 系 OS なのか ある意味解らない事態に陥っています。 (^^;)

コンシューマー向けだけあって こちらのほうが遥かに多く販売されたからなのか, Pro のほうが本来の姿である Win XP なのに「Home でないと使い物にならないソフト」が意外と多く, それが逆に職場…つまり Pro で使う障害となったりしました。

拙作ソフトでは特に Pro と Home の違いを想定した対応等は必要なかったのですが, テスト環境は必要…というわけで当該環境が用意されています。

一昔前までは もう 1 つ Pro の英語版を使っていたのですが, その環境でのテストを必要とする拙作主要ソフトが Unicode 版となりテストを必要としなくなったことから, 今では使われていません。 手元にある HDD の いずれかに残っているはずなので, 機会があったら使ってみることにしましょう。

それとは別に x64 版 Pro も復活させたほうが良いかしら? 何しろ「クライアント用なのに OS 自体はサーバー版。 ソフトもサーバー版が必要」という変態仕様で, 専用の対策が必要ですから。 (^^;)

 Windows 2000 Professional (x86) 環境ゲストです。 2000 年に発売された Windows XP の一つ前の OS で, Win XP は これの発展系といえます。 (実際 Win2k/XP の内部バージョンは 5.0/5.1 となっています。)  Win XP が登場して随分経った頃でも (特に職場では) 意外と こちらが使われていたりもしました。

元々 MS が Win2k で Windows NT 系への統一を目指していたこともあって, Win2k には Win9x 系である Windows 98 の機能も取り込まれていたことから, MS が目論んだほどでは無かったものの, この辺りで Win 9x 系から乗り換えたユーザーも多くいました。 自身も Win2k で NT 系へ乗り換えたクチです。

コモンダイアログなどが この OS で改版されたことから, 拙作ソフトでも その辺りへの対応が行われています。

Win 9x 系のソフトを そのまま動かすのに この OS が便利だったことから, ゲーム方面を含めて このゲスト環境にも意外と多くのソフトがインストールされていたりします。 唯一残念だったのは, VMware との相性が悪くて YAMAHA S-YXG 100 Plus といったソフトウェア MIDI が動作しなかったことですね。

 Windows NT Workstation 4.0 (x86) 環境ゲストです。 1996 年に発売された, Windows 2000 の一つ前の OS です。 Windows NT 系で初めて Explorer など Win 9x と同じ操作体系が採用されもしましたが, 当時は まだまだ使い分けが はっきりしていて, 個人ユーザーが使うことは あまりありませんでした。

Windows NT 3.51 とは, ちょうど Win2k に対する Windows XP と似たような関係にあって, 基本的にはシェルを一新した NT 3.51 の改良版 OS と言えるものになっています。 (実際, 開発段階では 3.52 というバージョンでした。)

拙作くらいの小さなソフトでは Win2k や Win9x 辺りに対応しておけば違いを気にする必要がなかったことから, この OS への個別対応は行っていません。

 Windows NT Workstation 3.51 (x86) 環境ゲストです。 1995 年に英語版が発売された Windows NT 4.0 の一つ前の OS で, API 体系としては Windows 95 のものを取り込んでいたものの, この OS までは Windows 3.1 と同じ操作体系でした。 安定動作していたことから, NT 4.0 よりも こちらを好んで使用していたユーザーも多かった経緯があります。

0.01 大きいだけのバージョンナンバーで実際マイナーチェンジではあったのですが, Windows NT 3.5 とは結構差のある OS となっていて, NT 3.51 で動いても NT 3.5 では動作しないというソフトも意外と多くありました。 今にして思えば, Win 95 系 API の有無が影響していたのかもしれませんね。

拙作ソフトでは, Windows 3.1 時代と同じ旧形態のコモンダイアログへの対応や NT 4.0 以降でしか実装されていないコモンコントロールと同じ機能を持つダミークラスの作成…といった対応が行われています。

実は このゲスト環境…, ハイカラーでのテストを行うために「VMware の Windows XP MCE 2002 ゲスト上にインストールされた Virtual PC 2007 SP1 の Win NT 3.51 環境」…つまり孫ゲスト環境だったりします。 (笑)

 …と, ここまでが Windows NT 系の環境で, Win32 API としては Unicode 版が本家の, むしろ「ANSI 版も使える」といった形態の OS となっています。 今では LHMelt や UNLHA32.DLL も Unicode 版なので, そういった意味では Windows 9x 系より こちらのほうが日本語関連で問題の発生する確率は小さくなっています。

 さて, 次は Windows 9x 系の環境です。 Win32 API が実装されているものの OS 自体は 16 ビット版なので, そこから来る制限が どうしても付きまといます。 また, ANSI 版 API だけなので, 日本語版と他言語版の間で文字コードの問題が発生したりと, その方面への対応も必要となるのでした。 以降は 32 ビット環境のみ…どころか, 32 ビットでさえない環境ばかりですので, "x86" の表記は省略します:

 Windows Millennium Edition 環境ゲストです。 2000 年に発売された Windows 9x 系最後の OS です。 Windows 95 バカ売れの余波で ここまで Win9x 系が使われ続けてきましたが, いろいろ詰め込みすぎたせいで この OS が (特に日本語版では) 相当不安定になってしまい, Windows XP への移行に拍車の掛かってしまった感があります。 Windows 2000 での Windows NT 系への統合を断念したのに伴い, 半ば予定外に開発・発売が行われた…といった辺りも一因となっていることでしょう。

NT 系のみとなって 15 年以上経ちますが, 業務用独自開発ソフトやゲームなどを動作させるために, 意外と この辺りの OS が稼働している PC も残っているような気がします。

拙作ソフトでは NT 系と Win9x 系という違いはありますが, 処理としては むしろ Windows 2000 に近いものとなっています。 さすがは同じ年に登場した Win2k (Ver 5.0) と Win Me (Ver 4.9) といったところでしょうか? (^^;)

ここからは これ以降で説明する環境を含めた Windows 9x 系全てに該当する話です。

上述したように Windows 9x 系の Win32 API には Unicode 版が存在せず ANSI 版のみとなっています。 Win32s とも異なるので Win32c ('c' は Windows 95 の開発コード名 "Chicago" から来ているとか "Consumer" の 'c' だとか言われています。) と呼ばれたりもするのですが, 兎にも角にも ANSI 版しか用意されていないので, Unicode 版のソフトを動作させることは出来ません。

一方, LHMelt や UNLHA32.DLL などは昔はともかく今では Unicode 版となっています。 つまり本来であれば Win 9x 系で動作しないことになります。 なので, これらのソフトでは「Unicode←→ANSI の変換を行った上で ANSI 版 API を呼び出すダミーの Unicode 版 API」を内部的にもつことで Win9x 系でも動作するようになっているのでした。 コンパイラーのランタイムが Unicode 版 API を使用しているとアウトなので, ランタイムすらメモリー関係や初期化ルーチンなど最低限のものに使用を留めてあります。 (笑)

その上, 時刻関係など Windows XP 辺りの段階ですらバグの仕込まれている API が存在することから, 一部については API すら信用せず自前でルーチンを拵えています。 …と, 手間だけは やたらと掛かっていますが, やっていることは大したことのない単純作業ばかりです。 (^^;)

さらに, 16 ビット版 OS は 32 ビット版以降と異なりプリエンプティブなマルチタスクではありませんので, メッセージループを回す…なんてこともやっています。 もっとも, こちらの処理は「元々必要だったものが後々必要なくなった」パターンで手間は掛かっていませんけれども…。

 Windows 98 Second Edition 環境ゲストです。 1999 年に発売された, マルチメディア強化版 Windows 98 といった感じの OS で, DVD-ROM への対応や IE 5.0 の同梱などが行われています。 Windows 9x 系では この辺り (Win98SE や Win98SP1) が一番安定していた気がします。

この OS で基本機能が出揃ったといえることから, Win9x 系がバッサリ切られるまでは, Win98SE 以降を要求するソフトの多かった経緯があります。 今では Windows XP や 2000…どころか Windows 7 以降を要求するソフトが普通になっていますね。 (^^;)

この頃の Win9x 系 OS は複数同時に使用していると ぱっと見で区別しづらいところがあることから, 壁紙で どの OS か判るようにしてあったりします。 Windows 98 系は親の再婚パターンな義妹キャラ。 15 年以上も前に行ったお遊びですが, 未だにそのまま。 (笑)

 Windows 98 (SP1) 環境ゲストです。 1998 年に発売された Windows 95 の後継 OS で, FAT32 や USB に対応している辺りは Windows 95 OSR 2.5 と同じです。 この OS で漸く Win9x 系が完成 (といいますか一段落) の域へ達した感があります。

後継の Windows Me が不安定だったことから, わざわざ この OS へダウングレードして使っているユーザーも多くいました。 実機はともかく, VMware や VirtualBox のゲスト環境上では, ゲーム目的などで今でも意外と稼働しているような気がします。

そういえば, 「Windows 98 SE よりも こちら (SP1) のほうが安定している」といった話も当時多く聞こえてきましたが, 自身に限っては実機・仮想共々 SE のほうが今でも安定しています。

 Windows 95 OSR 2.5 環境ゲストです。 SHOP などで箱買いされた普通の Windows 95 と異なり, こちらはプリインストールを前提とした OEM 版です。 微妙に改良が加わっていることから, Win95 と OSR とを (Windows 98 と SE 程度の) 別物として見なされることもあります。

OSR 2.0 (DMA, FAT32 対応など) / 2.1 (AGP 対応など) / 2.5 (IE 4.0 同梱など)…と, 大きく分けて 3 つの版がありますが, 場合によっては この 3 つを区別しないといけないことも…。 幸い拙作ソフトでは そのような区別は必要ありませんでした。

拙作ソフトに関しては, 98 以降にしか存在しない (しかも Win NT 系対応ソフト的には必須な) API が存在することから, そのダミー API を用意する…といった対応が行われています。

Win95 系の壁紙は「日本史担当の教育実習生…ならぬ, 幼なじみ Part 2 なお姉さんキャラ」が採用されています。 (笑)

 Windows 95 (OSR 1.0) 環境ゲストです。 LHMelt の表示しているバージョン表記が次で説明する Win95 と同じで, OS 的にも同じ扱いを受けるのが普通ですが, こちらは OSR 1.0 に当たる OEM 版です。 ツールボタンなどのデザインが OSR 2.x や Windows 98 と同じ…といった辺りで見た目でも区別が可能です。

OEM 版ではありますが OSR 2.x と異なり最初期のものなので, よほど PC ベンダーが特殊なものを組み込むなどしていない限り, 箱売りの Windows 95 と区別する必要はありません。 そういった意味では この環境を用意する必要は本来ないのですが, Internet Explorer の違いによりシステム挙動に変化が生じることから, その方面を確かめるために用意されています。

 Windows 95 (SP1) 環境ゲストです。 上の OSR 1.0 と異なり, こちらは SHOP で箱売りされていたもので, 1995 年に発売された Windows 9x 系最初の OS です。 Windows 3.1 から Windows NT 系 (といいますか 32 ビット版 OS) への移行を促すことを目的として作られた OS ですが, これがバカ売れしてしまったおかげで, 真の移行 (と統合) は Windows XP の登場まで持ち越しとなってしまいました。

NT 系と異なり MS-DOS 及び Win 3.1 との互換性が至上命題となっていたために, MS-DOS 7.0 + Windows 4.0 という (DOS + Win の) 構造すら再現されていて, MS-DOS 7.0 + Windows 3.1, MS-DOS 6.2/V + Windows 4.0 といった使用法も可能でした。 (行う意味はありませんけれど。)  反対に, 互換性維持のために いろいろ足かせ (制限) が掛かってしまい, それが後に Windows Me で破綻を来す原因ともなりました。

「極力発売当初の環境を再現」する役目を担ったゲストで, Internet Explorer 2.0 に留めてあったりします。 その辺りもあって, 画像をクリックして拡大すると判るのですが, Explorer の実装された Win 9x でも, 最初はツールバーのボタンが ちゃんとボタンのデザインだったことが分かります。 (笑)

 Windows 9x 系に続いては Windows 3.1 系…もはや Win32 API すら実装されていません。 (笑):

 MS-DOS 6.2/V + 日本語版 Windows 3.1 環境ゲストです。 Windows 3.1 は (日本語版では) 1993 年に発売された Windows 3.0 (91 年) の改良版 OS です。 OS として説明されるのが普通ですが, 最初 (Windows 1.0) は MS-DOS 上の GUI モジュールとして出発していて, いろいろと OS 的な仕事をこなすようになった 3.1 でも本質は変わっていません。

当時としては結構使えるものだったことから, Windows NT 系への移行が なかなか進まず, 打開策として Windows 95 が登場することとなりました。

一方の MS-DOS ですが, 「MS-DOS」と聞くととても古い印象を受け, MS-DOS 3.x 辺りで 1984 年と確かに古いのですが, 6.2/V に限れば Windows 3.1 と同じ 1993 年に発売されています。 昔は PC を起動しても Win が立ち上がることはなく, これ (MS-DOS) だけが起動しました。 (^^;)  MS-DOS 3.x の頃までは日本語版と英語版は別物だったのですが, MS-DOS 5.0 や MS-DOS 6.2/V の頃では (PC/AT 用については) 英語版の上に日本語用のモジュールを適用する形態となっています。

Windows 3.1 は純粋な 16 ビット版 OS のため Win32 API 自体が実装されていません。 そのような Win 3.1 上で Windows NT / Windows 95 系のプログラムを動作させるためのモジュールが Win32s です。 とはいうものの, かなり強い制限があることから, Win32s を意識して作られていないと実際には動作しないのが普通です。

先に書いたとおり, 未だに この環境が使われているのか, 要望・動作報告等が国外から上がってきます。 (笑)

Windows 95 以降で当たり前のように存在する Win32 API であっても, その中の意外と多くが Win32s には実装されていないことから, 拙作ソフトでは同じ機能をもったダミー API が内部的に実装されています。 さらに, ツールバーやステータスバーなど Windows 95 以降で普通に使われているコモンコントロールも存在しないことから, これまた同じ仕様を備えた (ただし必要な機能のみを用意したサブセットな) ウィンドウクラスを自前で拵えていたりします。 (^^;)

あと, 個々の API と それを担当している DLL の体系が Windows NT / Windows 9x 系 OS と全く異なるので, LoadLibrary() + GetProcAddress() による DLL の遅延ロード化には少々苦労しました…主に手間的に。 (笑)

…と, やっていることは大したことがなくとも手間だけは掛けているので, Win32s 上でも拙作ソフトが見た目も同様に動作しているわけですが, 逆に考えると, 文字列変換など必要最低限なもののみとはいえ Unicode 版 API が備わっていることで, Windows 10 にも対応した Unicode 版の拙作ソフトが動作し, OLE2 によるドラッグ&ドロップまで使える Win32s は本当に凄いと唸らざるを得ません。 (^^;)

反対に, この環境へ対応するために拙作ソフトでは Windows 3.1 時代の遺物ともいえる DDE すら使っているのですが, いくら OLE2 が その発展系だとはいえ, それが最新の Windows 10 (x64) でも機能するというのは, これまた凄いことだと思います。

ちなみに, この環境も「VMware の Windows XP MCE 2002 ゲスト上にインストールされた Virtual PC 2007 SP1 の Win 3.1環境」な孫ゲスト環境だったりします。 なのでハイカラーと判るように「義妹の幼なじみな お嬢様」が壁紙になっています。 (笑)

 最後を締めくくるのは Linux 系…ついには Windows ですらなくなってしまいました (爆):

 Ubuntu 16.04 LTS 環境ゲストです。 Linux という用語は, Linus Torvalds 氏により開発が始められたカーネル (linux カーネル) をもつ, 一連の UNIX 互換 OS を指して使われます。 本来 Linux とは根底であるカーネルのみを指す用語なのですが, 一般的にはシェルや X Window System などを始めとした様々なソフトが付加されたディストリビューション全体を指して使われることが多くなっています。

Windows とは全く異なる OS なので当然ながら Windows 用のソフトは使えないわけなのですが, Wine という Linux 上で Windows NT / Windows 9x 系プログラムを動作させるための Windows レイヤーをインストールすると, Windows 用ソフトの使用が可能となります。

PC エミュレーターのシームレス表示機能と見かけは似ていますが, Wine が Win32 API を始めとした各種 API やコントロールの動作を肩代わりすることにより, プログラム自体は X Window System 上で直接動作しています。 そういった意味では Win32 API インタープリターと言えるものになっています。

Wine 1.0 より前の頃は製品版 Windows のモジュールを必要としましたが, 今では Wine 単体でも結構安定して Win 系のソフトを動作させることが可能となっています。 とはいうものの, コモンダイアログ系など対応の不十分な部分も多く, やはり Wine を意識して作られていないと正常動作しないのが実情で, 拙作ソフトでも色々と対応コードが追加されています。

ここでは「ちゃんと Win なソフトが動くんだよ~」という意味で DirectX 9.0c な Direct3D の必要な『千の刃濤、桃花染の皇姫』の体験版にも御登場願っています。 (笑)  さすがに 3D バリバリなゲームまではゲスト上では無理です。 (^^;)

 …と, 全てゲストといっても過言ではありませんが, 一応常時 20 以上の環境でテストを行っています。 実機を合わせると軽く 30 を超えてしまいますので, 「メンテが~メンテが~」と Twitter 辺りで頻繁に呟く事態となっているのでした。 (笑)

Jun.11,2018 追記

 孫環境な NT 3.51 や Win32s 環境について「どういうことなの?」といった質問メールが昔も今も飛んできますので, これまでも何度か書いていますが, 今回も書いておきます。

それぞれの箇所で書いているように, 例えば Win32s 環境であれば, 当該環境は:

  • ホスト:Windows 10 1803 (x64)
    使用ソフト:VMware Workstation 12.5 Pro
  • ゲスト:Windows XP MCE 2002 (x86)
    使用ソフト:Virtual PC 2007 SP1
  • 孫ゲスト:MS-DOS 6.2/V + Windows 3.1 + Win32s Ver 1.25

といった構造になっていて, Virtual PC の全画面表示を解けば:

見てのとおり孫環境であることが はっきりします。 (笑)

 なぜ比較的新しい Windows 7 ゲスト辺りではなく古い Windows XP 環境を使っているのかですが, 1 CPU 環境でないと Virtual PC の NT 3.51 ゲストが正常動作しないからなのでした。 これは Windows Virtual PC でも同じで, 逆に VMware の Windows 7 ゲストは論理物理の別に関係なく 2 CPU 以上でないとプチフリーズが発生して半分使い物にならない…という制限があることから, 当該ゲスト上で孫環境を構築することは出来ないのです。