Windows could not update the computer’s boot configuration

Windows Server 2016 が GA になったので、週末を利用して、仕事で使っているマシン 2 台の OS を Windows Server 2016 TP5 から RTM に入れ替えたのですが、インストーラーの怪しげな動作でハマったので書いておきます。強引にインストールは終わらせましたが、完全に直っていない上に根本原因が確認できていないので、時間を見つけて調べて追記する予定です。

(2016/10/30 追記)
最終確認はまだですが、NVRAM へのアクセスが失敗していることが原因っぽいです。(詳細は後述)

インストールを行った二台は HP Z420 Workstation と HP Z440 Workstation。前者がメインの開発機で、コードを書いてビルドする以外に、デバッグ用の Hyper-V 仮想マシンを動かしたり、メールを書いたりもします。後者はサブの開発機で、ほぼビルドと仮想マシンのみ。なお Windows 10 ではなく Server 2016 を入れるのは、Universal App などの煩わしい機能が嫌いだからです。サーバー SKU おすすめです。

家で使うマシンも仕事のマシンも同じですが、OS の入れ替えでは常にクリーン インストールを行います。インプレースアップグレードはいまいち動作が信用できないのです。

作業は、サブ機の Z440 から行いました。これには光学ドライブがないので、USB ドライブで Windows PE を起動して、別の USB ドライブに入れておいた Windows Server 2016 のインストーラー (MSDN からダウンロードした ISO を単にコピーしたもの) である setup.exe を起動して行います。前の OS が入っていたパーティションを消して、新しくパーティションを作って入れるだけです。作業としては普通です。

Z440 の作業は何の問題もなかったのですが、同じ USB ドライブを使って Z420 に Server 2016 をインストールしたところ、インストールの最後のフェーズで "Windows could not update the computer’s boot configuration. Installation cannot proceed." が出ました。

01-error
じゃじゃーん

これけっこう深刻なエラー・・・。メッセージの内容を信用するならば、インストーラーの wim イメージをボリュームにコピーした後、ブート情報を書き換えるところが失敗しているわけす。当然マシンは起動しなくなります。Windows RE も起動できません。ちなみにインストールが失敗したディスクからシステムを起動すると "Your PC/Device needs to be repaired – The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034" 画面が出ました。BCD 情報を書けなかったのはお前んところのインストーラーなんだけどな!

02-bcd
起動失敗の図

慌てず騒がず、とりあえず以下の作業を順番に試してみました。

  1. まったく同じ作業 (同じ USB ドライブを使って、同じハードディスク (以降ディスク A) をインストール先として指定) を試す → 現象変わらず
  2. 別のハードディスク (以降ディスク B) を使ってみる → 現象変わらず
  3. ディスク B を diskpart の clear コマンドでクリアしてからインストール → 現象変わらず

おいおい・・。一台目の Z440 で上手くいっていることからして、USB ドライブに問題はないはず。ハードディスク側にも問題があるとは思えない。ぱっと思いつくのは、BIOS/UEFI の起動方式とディスク形式の MBR/GPT。ディスクをクリアしても同じ現象が出る時点でインストーラーのバグくさいが、だとすると Z440 でうまくいった説明がつかない。Z420 も Z440 も UEFI のはずなのに。

しばらく悩んだ結果、Z420 と Z440 とで、パーティション構成に唯一の違いがあることに気づきました。Z440 のインストール先ディスクは 150GB なので、まるまる OS 用のパーティションを入れています。一方、Z420 のインストール先ディスク A は 1TB なので、後ろ半分の 500GB はデータ用にして、先頭の 500GB を OS 用にしていました。今回の作業では、前の OS のパーティションだけを消して、インストーラーから新しくパーティションを作り直していました。また、上記作業 2. と 3. の作業でも、インストーラー経由でパーティションを作ってからインストールをしていました。そこで、新しいディスク C を用意して、パーティションを作らず、ディスク全体を指定して OS をインストールしたところ・・・成功。謎は深まるばかり。

そしてすぐに次の問題が発生。インストール後に再起動がかかっても、またも "Your PC/Device needs to be repaired" エラーで Windows 起動しない・・・。インストール成功したんじゃなかったのかよ。

試行錯誤の末、Boot menu を開いてインストール先のディスクを明示的に選択すれば起動することが判明。これで何が起こっているのかは分かった、気がする。

03-boot

Z420 のブート メニュー
(Legacy Boot Sources の WDC WD2500AA.. を選択すれば起動できた)

起動がうまくいったあと、以下の情報を確認しました。

  • ディスク A は GPT 形式になっていて、EFI System Partition は作成されている
    → インストーラーの最後で BCD 情報の書き換えに失敗した理由が不明
  • ディスク B は MBR 形式になっている
  • Z420 の msinfo32 を見ると、BIOS Mode は Legacy
  • Z440 の起動ディスクは GPT 形式
  • Z440 の msinfo32 で、BIOS Mode は UEFI

上記を整理すると・・

  1. Windows インストーラー、及び OS 本体は Z420 を Legacy BIOS だと思っているので、MBR 形式のディスクを対象にしたインストールを試みる。
  2. しかし、インストーラーの中のパーティションを作る部分では、システムを UEFI と認識しているためか、MBR ではなく GPT 形式でパーティションを作る。
  3. Z420 のブートの順番は、UEFI を試してから各ディスクの MBR を使って起動しようとする。このとき、UEFI の Windows Boot Manager がなぜか中途半端に実行できてしまうため、MBR の実行を試そうとしない。
  4. システムは MBR 形式でインストールされているので当然起動できない。

不可解なのは以下の点。

  • そもそも最初に試したときに、BCD 情報が更新できなかった理由
    → 諸悪の根源。これが上手くいっていれば問題はここまで拗れない。
  • インストーラーでパーティションを作るとディスクが GPT になるくせに、実際にシステムをインストールするときは MBR でディスクを切っている。インストーラーがシステムの BIOS モードをチェックするコードが少なくとも二ヶ所あって、一方は Legacy BIOS、他方は UEFI だと認識してしまうっぽい・・?

確か似たような現象を調べたときに、Windows のインストーラーにはディスクをクリアしてディスク形式を変更する機能がないので、インストーラーは Legacy BIOS のシステムでは GPT 形式のディスクを認識せず、逆に UEFI のシステムでは MBR のディスクを認識しなかった記憶があります。この場合、diskpart などを使ってあらかじめディスク形式を変えておく必要があります。

今回の場合は不可解で、GPT と MBR の扱いがかなり混在してしまっている印象がです。そもそもインストーラーやシステムがシステムを Legacy BIOS として認識しているのであれば、GPT で切れたディスクは認識できるべきではないし、インストールが始まる前にエラーになって欲しいものです。イメージのコピーはうまくいって、最後にこけるのは一番タチが悪い。これが Server 2016 のメディアで新しく発生するのか、もっと前の OS のディスクから発生するのかどうかは確かめていません。

今のところ、マシンを再起動するたびに boot menu を起動してディスクを選択しないといけない。超不便。

(2016/10/30 追記)

まだ最終確認は取れていませんが、どうやら NVRAM への読み書きができないハードウェア障害のような気がしてきました。調べた内容を以下に記します。

まず、Windows インストール時のログはインストール先ディスクの $WINDOWS.~BT\Sources\Panther\setupact.log に残っているので、失敗している箇所を確認。

log

2016-10-22 16:46:58, Info       [0x060228] IBS    Callback_UpdateBootFiles:Successfully updated Windows boot files.
2016-10-22 16:46:58, Info                  IBSLIB ModifyBootEntriesLegacy: Not in first boot. No actions to perform. SetupPhase[2]
2016-10-22 16:46:58, Info                  IBSLIB ModifyBootEntriesBCD:Setup phase is [2]
2016-10-22 16:46:58, Info                  IBSLIB BfsInitializeBcdStore flags(0x00000008) RetainElementData:n DelExistinObject:n
2016-10-22 16:46:58, Info                  IBSLIB VolumePathName for H:\Windows is H:\
2016-10-22 16:46:58, Info                  IBSLIB Opening template from \Device\HarddiskVolume7\Windows\System32\config\BCD-Template.
2016-10-22 16:46:58, Info                  IBSLIB System BCD store does not exist, creating.
2016-10-22 16:46:58, Error      [0x064230] IBSLIB Failed to create a new system store. Status = [c0000001]
2016-10-22 16:46:58, Error      [0x0641b8] IBSLIB ModifyBootEntries: Error modifying bcd boot entries. dwRetCode=[0x1F][gle=0x0000001f]
2016-10-22 16:46:58, Info       [0x060216] IBS    CallBack_MungeBootEntries:Failed to modify boot entries; GLE = 31
2016-10-22 16:46:58, Info       [0x0640ae] IBSLIB PublishMessage: Publishing message [Windows could not update the computer’s boot configuration. Installation cannot proceed.]
2016-10-22 16:46:58, Info       [0x0a013d] UI     Accepting Cancel. Exiting Page Progress.

やはり BCD の新規作成に失敗している模様。

とりあえず BIOS の設定 (Advanced –> Device Options) を確認。もともとはこんな設定だった。後で触れるがこの時点でちょっと変。

bios01

とりあえず Option ROM を Legacy から EFI に変えて試してみる。

bios02

なお、ここで Video Options ROMS を EFI に変えてはいけない。間違って変えてしまうと、ビープ音が 6 回鳴ってシステムが起動しない悲しい状態になります。

Advisory: HP Z1, Z220, Z420, Z620, Z820 Workstation – 6 Beeps After Changing BIOS Settings
http://h20564.www2.hp.com/hpsc/doc/public/display?docId=emr_na-c04045903

お決まりだが、見事にこの罠を踏んでしまったのだ。上記 Resolution にあるように、CMOS リセットを行って無事復活。PXE Option ROMS と Mass Storage Option ROMS だけを EFI にして再起動。そして同様にインストーラーを動かすが、状況は変わらず、結局 "Windows could not update the computer’s boot configuration. Installation cannot proceed." で失敗する。

埒が明かなくなってきたので、真面目に setup.exe をデバッグしてみることに。最近の Windows PE では NIC の標準ドライバーが入っているので TCP/IP ネットワークで簡単にユーザーモードのリモート デバッグができます。Windows PE 起動後に以下のコマンドを実行すると、ネットワークが有効になるので dbgsrv.exe を起動できます。

> wpeutil initializenetwork
> wpeutil disablefirewall

詳細は省きますが、問題となっているインストーラーの最終フェーズでは、まず空の BCD を作ってから、インストール イメージ内にある windows\system32\config\BCD-Template と NVRAM の状態を元に BCD にデータを入れていくようなことをやっているようです。空の BCD を作るところは問題なく成功して、NVRAM の内容を取ってくると思われるシステム コールから c0000001 が返ってきていました。カーネル デバッグまではやっていないので、カーネルの中で何が失敗しているのかはまだ不明のままです。

NVRAM がおかしいとすると、Option ROM が Legacy 設定だったにも関わらず "Your PC/Device needs to be repaired – The Boot Configuration Data file is missing some required information. File: \BCD Error code: 0xc0000034" が出る理由も分かるような気もします。NVRAM の内容が Server 2016 を入れる前の状態のまま変わっていない可能性が高く、本来であれば Option ROM を Legacy に変えたら NVRAM はクリアされて、MBR からの起動を自動的に試すのではないだろうか。

デバッガーを使って、NVRAM にアクセスしてエラー コードを返している箇所 (4 箇所あった) で、片っ端からエラーコードを変更してエラーがなかったように見せかける禁断の領域に踏み込んだところ、インストールは終わりましたが、初回起動で OOBE が始まる前の段階で "Windows could not complete the installation. To install Windows on this computer, restart the installation." というポップアップが出て結局起動できず。どうやら Windows 起動時にも NVRAM へのアクセスを行なっているようだ。まあそりゃそうだろう。

image

Z420 で NVRAM をクリアする方法を探してみたが、どうにも見つからない。意図せずして行なった CMOS クリアではクリアされなかった。システム設定で ROM をクリアするオプションはあるのだが、何が消えるか分からないためちょっと恐くて試していない。びびり。

(2016/11/6 追記)

次は、何とかして NVRAM がおかしいという確証が欲しいところです。以下の資料を見ると、実は bcdedit /enum で出力される値は NVRAM variable から来ているらしい。

Presentations and Videos | Unified Extensible Firmware Interface Forum
http://www.uefi.org/learning_center/presentationsandvideos

上記ページからダウンロードできる "Windows Boot Environment" という PDF の 20 ページ目によると

– BCD has 1:1 mappings for some UEFI global variables

– Any time {fwbootmgr} is manipulated, NVRAM is automatically updated

さらに、以下のページによると

Remove Duplicate Firmware Objects in BCD and NVRAM
https://technet.microsoft.com/en-us/library/cc749510(v=ws.10).aspx

When bcdedit opens the BCD, it compares entries in NVRAM with entries in BCD. Entries in NVRAM that were created by the firmware that do not exist in BCD are added to the system BCD. When bcdedit closes the system BCD, any boot manager entries in BCD that are not in NVRAM are added to NVRAM.

同様のことは以下の日本語のブログにもまとめられています。詳しくていい感じ。

PC-UEFI – DXR165の備忘録
http://dxr165.blog.fc2.com/blog-category-45.html

何はともあれ、Windows PE を起動して bcdedit を実行してみます。

X:\windows\system32> bcdedit /enum {fwbootmgr}
The boot configuration data store could not be opened.
A device attached to the system is not functioning.

あっさり失敗。インストーラーを起動するまでもないですね。Server 2016 のメディアが悪いのではなく、やはりハードウェアがおかしいくさい。

さて、次のステップはいよいよ Windows PE のカーネル デバッグだろうか。幸い、Z420 と Z440 を 1394 ケーブルで繋げられたので、これでデバッグすることにする。最近だとイーサネットも使えるらしいが、試したことはない。

上記の bcdedit /enum {fwbootmgr} を実行すると、NT カーネルから HAL を経由し、EFI のランタイム サービス テーブルにおける GetNextVariableName が指すアドレスの関数を呼んで変数名を列挙し、GetVariable で値を取ってきます。ランタイム サービス テーブルの定義は、↓ のファイルにおける EFI_RUNTIME_SERVICES という構造体です。

TianoCore EDK2: MdePkg/Include/Uefi/UefiSpec.h Source File
http://www.bluestop.org/edk2/docs/trunk/_uefi_spec_8h_source.html

ランタイム サービス テーブルは、EFI システム テーブルの一部であり、EFI システム テーブルは efi_main 関数が取る 2 つのパラメーターのうちの一つらしい。

Programming for EFI: Using EFI Services
http://www.rodsbooks.com/efi-programming/efi_services.html

GetNextVariableName のプロトタイプ宣言は分かっているので、呼び出し部分から変数名を GUID を確認しました。102 個ありますが、とりあえず全部載せておきます。

ffffd000`208578a0  "CurrentDevicePath"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "BootCurrent"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "LangCodes"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "PlatformLangCodes"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "SSID"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "UsbMassDevNum"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "SetupFeatureSupport"
ffffd000`20857868  b6ad93e3 4c8519f7 c58072aa c7db9471
ffffd000`208578a0  "ErrOut"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "ErrOutDev"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "RstSataV"
ffffd000`20857868  193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0  "PNP0510_0_VV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "RstScuV"
ffffd000`20857868  193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0  "BootOptionSupport"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "ConInDev"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "UsbMassDevValid"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "PNP0501_1_VV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0400_0_VV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0501_0_VV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "ConOutDev"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "VgaDeviceInfo"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "VgaDeviceCount"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "RstScuO"
ffffd000`20857868  193dfefa 4302a445 3aefd899 c6041aad
ffffd000`208578a0  "DebuggerSerialPortsEnabledVar"
ffffd000`20857868  97ca1a5b 4d1fb760 90d14ba5 902c0392
ffffd000`208578a0  "SerialPortsEnabledVar"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "DriverHlthEnable"
ffffd000`20857868  0885f288 4be1418c ad8bafa6 fe08da61
ffffd000`208578a0  "DriverHealthCount"
ffffd000`20857868  7459a7d4 44806533 e279a7bb c943445a
ffffd000`208578a0  "S3SS"
ffffd000`20857868  4bafc2b4 410402dc f1d636b2 849e8db9
ffffd000`208578a0  "RSCInfoAddresss"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "PlatformLang"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "AMITSESetup"
ffffd000`20857868  c811fa38 457942c8 e960bba9 34fbdd4e
ffffd000`208578a0  "UsbSupport"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "SlotEnable"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "FrontUsbEnable"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "RearUsbEnable"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "InternalUsbEnable"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "PowerOnTime"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "MSDigitalMarker"
ffffd000`20857868  c43c9947 4a343578 f38e5fb9 8e43c7a5
ffffd000`208578a0  "NotFirstBoot"
ffffd000`20857868  70040abc 45886387 cdddb187 f57a7d6c
ffffd000`208578a0  "ONBOARD_DEVS_PRESENT"
ffffd000`20857868  d98397ee 457a7a9d 68e5dfa9 18cc87ae
ffffd000`208578a0  "MemoryInfo"
ffffd000`20857868  7ee396a1 431bff7d cd8c53fa c5447c12
ffffd000`208578a0  "MEMajorVersion"
ffffd000`20857868  59416f8c 48c4b82d cb107e88 bbc38ec4
ffffd000`208578a0  "PciSerialPortsLocationVar"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "HeciErrorReset"
ffffd000`20857868  31d2fce0 11e164b3 00086cb8 669a0c20
ffffd000`208578a0  "MeInfoSetup"
ffffd000`20857868  78259433 4db37b6d c436e89a 7da1c3c2
ffffd000`208578a0  "NetworkStackVar"
ffffd000`20857868  d1405d16 46957afc 454112bb a295369d
ffffd000`208578a0  "HSTime"
ffffd000`20857868  ae601ef0 11e0360b 0008429e 669a0c20
ffffd000`208578a0  "LastHDS"
ffffd000`20857868  ae601ef0 11e0360b 0008429e 669a0c20
ffffd000`208578a0  "ConsoleLock"
ffffd000`20857868  368cda0d 4b9bcf31 d1e7f68c 7e15ffbf
ffffd000`208578a0  "SetupAmtFeatures"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "SlotPresent"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "MMIOR."
ffffd000`20857868  3b2158f5 48c039d3 530384aa dbc6ba65
ffffd000`208578a0  "SysBuses"
ffffd000`20857868  55e6fc89 40763e74 e9d4c298 10e8c413
ffffd000`208578a0  "ThermalErrorLog"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "ConOut"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "SetupCpuSockets"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "FrontUsbPresent"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "RearUsbPresent"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "InternalUsbPresent"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "Lang"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "SBRealRevID"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "TdtAdvancedSetupDataVar"
ffffd000`20857868  7b77fb8b 4d7e1e0d 80393f95 76e061a2
ffffd000`208578a0  "HP_CTRACE"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "NBRealRevID"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "MeBiosExtensionSetup"
ffffd000`20857868  1bad711c 4241d451 3785f3b1 700c2e81
ffffd000`208578a0  "HpWriteOnceMetaData"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "PBRDevicePath"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "TrueStruct"
ffffd000`20857868  7349bea7 420bc95c 1e6dcd8d a88b4d9d
ffffd000`208578a0  "PNP0501_11_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0501_12_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "SetupLtsxFeatures"
ffffd000`20857868  ec87d643 4bb5eba4 3e3fe5a1 a90db236
ffffd000`208578a0  "ucal"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "HpPassphraseStructureVariable"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "EfiTime"
ffffd000`20857868  9d0da369 46f8540b 5f2ba085 151e302c
ffffd000`208578a0  "Setup"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PlatformLang"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "Timeout"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "AMITSESetup"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "IDESecDev"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "SystemIds"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "UsbSupport"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PNP0501_0_NV"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PNP0501_1_NV"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PNP0400_0_NV"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PNP0510_0_NV"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "HpMfgData"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "SlotEnable"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "FrontUsbEnable"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "RearUsbEnable"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "InternalUsbEnable"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PowerOnTime"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "MSDigitalMarker"
ffffd000`20857868  8173aefa 4574adf0 377139a0 1af24aab
ffffd000`208578a0  "PNP0501_0_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0501_1_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0400_0_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "PNP0510_0_NV"
ffffd000`20857868  560bf58a 4d7e1e0d 80293f95 31e061a2
ffffd000`208578a0  "HpMor"
ffffd000`20857868  707c9176 4e27a4c1 371c1d85 c873cab7
ffffd000`208578a0  "PBRDevicePath"
ffffd000`20857868  a9b5f8d2 42c2cb6d ffb501bc 5e33e4aa
ffffd000`208578a0  "PetAlertCfg"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "ConIn"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "Timeout"
ffffd000`20857868  8be4df61 11d293ca e0000daa 8c2b0398
ffffd000`208578a0  "CurrentPolicy"
ffffd000`20857868  77fa9abd 4d320359 f42860bd 4b788fe7
ffffd000`208578a0  "a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3"
ffffd000`20857868  ffffffff ffffffff ffffffff ffffffff

GetNextVariableName からの戻り値は 0 なのですが、最後の変数は名前、GUID ともに明らかに変です。このあと、最後の "a8ff1f3f4a8c94074056b76e9d7fab3a862c68d3" に対して GetVariable すると、戻り値が 14 (= EFI_NOT_FOUND) になります。呼び出し元は hal!HalEnumerateEnvironmentVariablesEx なのですが、GetVariable が失敗すると、この関数は見慣れたエラー コード c0000001 (= STATUS_UNSUCCESSFUL) を返すようになっていました。

というわけで、EFI の GetNextVariableName で列挙された変数名がなぜか GetVariable できないためインストールが上手くいかない、ことが分かりました。アセンブラでどうやって NVRAM にアクセスしているのか分かりませんが、やはり NVRAM が怪しいです。

それにしても HAL の動作には不満が残ります。GetNextVariableName で列挙された値が GetVariable 出来なかった場合は、全体をエラーにするのではなく、その変数だけをスキップして欲しいですね。

Brute-force attack against NTLMv2 Response

2016 年 9 月の Windows Update で、NTLM SSO の動作に関連する脆弱性 CVE-2016-3352 が修正されたようです。

Microsoft Security Bulletin MS16-110 – Important
https://technet.microsoft.com/library/security/MS16-110

An information disclosure vulnerability exists when Windows fails to properly validate NT LAN Manager (NTLM) Single Sign-On (SSO) requests during Microsoft Account (MSA) login sessions. An attacker who successfully exploited the vulnerability could attempt to brute force a user’s NTLM password hash.

To exploit the vulnerability, an attacker would have to trick a user into browsing to a malicious website, or to an SMB or UNC path destination, or convince a user to load a malicious document that initiates an NTLM SSO validation request without the consent of the user.

CVE – CVE-2016-3352
http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-3352

Microsoft Windows 8.1, Windows RT 8.1, and Windows 10 Gold, 1511, and 1607 do not properly check NTLM SSO requests for MSA logins, which makes it easier for remote attackers to determine passwords via a brute-force attack on NTLM password hashes, aka "Microsoft Information Disclosure Vulnerability."

"NTLM SSO" などのキーワードで適当にインターネット検索すると、今年 8 月に書かれた以下の記事が見つかります。

The Register – Reminder: IE, Edge, Outlook etc still cough up your Windows, VPN credentials to strangers
http://www.theregister.co.uk/2016/08/02/smb_attack_is_back/?mt=1474231650819

画面キャプチャーを見ると、なんと Microsoft アカウントのパスワードが解析されてしまっています。そして Youtube の埋め込み動画では、SMB パケットがブラウザーからリークしていることを示しています。つまり、SMB パケットに埋め込まれた NTLM メッセージに対して brute-force 攻撃をしかけることで Microsoft アカウントのパスワードを解析できる、ことを示しているようです。

これは理論的に可能でしょう。が、もしそれが 「簡単に」 できるのであれば、それは NTLM プロトコルの死を意味するべきであり、パッチとして一朝一夕に対応できるものではないはずです。SSLv3 や RC4 と同様にそのプロトコルの使用を止めるべきですが、非ドメイン環境において NTLM の代わりとなるような認証プロトコルは Windows には実装されていないはずです。

というわけで、NTLM に対する brute-force がどれぐらい簡単なのかを調べることにしました。まず、NTLM プロトコルがどうやってユーザー認証を行っているかを説明します。と言っても以下の PDF を読み解くだけです。

[MS-NLMP]: NT LAN Manager (NTLM) Authentication Protocol
https://msdn.microsoft.com/en-us/library/cc236621.aspx

NTLM が混乱を招く点として、一つのプロトコルが複数のプロトコル バージョン (LanMan, NTLMv1, NTLMv2) に対応していることです。Wiki の情報を見ると NTLMv2 は NT 4.0 SP4 から採用されているので、 NTLMv2 だけで現在は問題なく生きていけるはずです。ただし XP などの古い OS において、さらに古い OS との互換性のために NTLMv1 や Lanman に fallback するような動作が有効になっている場合があります。このへんの細かい話は長くなりそうなので、以下の KB に丸投げします。

NT LAN Manager – Wikipedia, the free encyclopedia
https://en.wikipedia.org/wiki/NT_LAN_Manager

How to prevent Windows from storing a LAN manager hash of your password in Active Directory and local SAM databases
https://support.microsoft.com/en-us/kb/299656

Security guidance for NTLMv1 and LM network authentication
https://support.microsoft.com/en-us/kb/2793313

NTLM は Challenge Reponse Authentication の一つで、簡単に書くとサーバーとクライアントが以下 3 つのメッセージを交換することで認証が行われます。(仕様によると Connectionless モードの場合は Negotiate が存在しないようですが、見たことがないのでパス。)

Client: "I want you to authenticate me."
(= Negotiate Message)

Server: "Sure. Challenge me. Use 0x0123456789abcdef as a ServerChallenge."
(= Challenge Message)

Client: "My response is !@#$%^&*()_+…"
(= Authenticate Message)

NTLM は単体で使われるプロトコルではなく、必ず別のプロトコルに埋め込まれて使われます。例えば SMB の中で使われる場合には、SMB Session Setup コマンドに埋め込まれます。ダイレクト SMB ポートである 445/tcp をキャプチャーしたときの Network Monitor 上での見え方は以下の通りです。

01
SMB packets over 445/tcp (Lines highlited in purple contain NTLM messages)

02
NTLM Negotiate Message

03
NTLM Challenge Message

04
NTLM Authenticate Message

セクション 3.3.2 NTLMv2 Authentication から、パスワードを解析するのに必要な疑似コードの計算式だけを抜き出すと以下の通りです。

[MS-NLMP]: NTLM v2 Authentication – 3.3.2 NTLM v2 Authentication
https://msdn.microsoft.com/en-us/library/cc236700.aspx

Define NTOWFv2(Passwd, User, UserDom) As
  HMAC_MD5(MD4(UNICODE(Passwd)),
           UNICODE(ConcatenationOf(Uppercase(User), UserDom)))
EndDefine

Set temp to ConcatenationOf(Responserversion,
                            HiResponserversion,
                            Z(6),
                            Time,
                            ClientChallenge,
                            Z(4),
                            ServerName, Z(4))
Set NTProofStr to HMAC_MD5(ResponseKeyNT,
                           ConcatenationOf(CHALLENGE_MESSAGE.ServerChallenge,
                                           temp))
Set NtChallengeResponse to ConcatenationOf(NTProofStr, temp)

使用しているハッシュ関数は HMAC_MD5 と MD4 のみ。入力データはいろいろあって面倒そうに見えますが、それほど難しくありません。特に、疑似コードの中で temp として扱われているバージョンやタイムスタンプなどのメタ情報が、ハッシュ値である NTProofStr と連結してそのまま Authenticate Message の NtChallengeResponse になっているからです。図示したものを ↓ に示します。図中の Metainfo が、上記擬似コードで言うところの temp です。

実際に処理するときは、パスワードを UTF-16 に変換する点と、ユーザー名を大文字に変換する点に注意が必要です。

05
Calculation of NtChallengeResponse in NTLMv2

これで材料が出揃ったのでコードを書きます。まずサーバー側のコードとして、Samba サーバーが NTLM メッセージを処理しているときに、brute-force に必要となる情報をテキスト ファイルとして書き出すようにします。これによって、前述の Youtube 動画がやっていることとほぼ同じことができます。

Yandex SMB hash capture on IE with email message – YouTube
https://www.youtube.com/watch?v=GCDuuY7UDwA

msmania/samba at ntlm-hack
https://github.com/msmania/samba/tree/ntlm-hack

テキスト ファイルと同じ内容をレベル 0 のデバッグ メッセージとしても出力するようにしました。gdb を使って smbd を実行しておくと、SMB 経由で NTLM 認証が行なわれたときにコンソールにログが記録されます。出力される情報はこんな感じです。後でまとめて grep できるようにあえてテキスト ファイルにしました。

Domain:
User: ladyg
Client: LIVINGROOM-PC
UserAndDomain=4c004100440059004700
Challenge=ae65c9f0192d64b9
Auth=0101000000000000b62acefc2d11d201ea3017d2f290d64e00..(長いので省略)
Response=39329a4a4e9052fe3d4dea4ea9c79ac5

次に、Samba サーバーが書き出したテキスト ファイルに対して実際に brute-force を行うプログラムを書きます。hashcat に新しいモードを付け足すことができればベストだったのですが、OpenCL を勉強する時間がなかったので、OpenSSL の関数を呼び出すだけの簡単なプログラムになりました。

msmania/ntlm-crack
https://github.com/msmania/ntlm-crack

このプログラムは Samba が生成したテキスト ファイルに対して、別の引数として指定したのテキスト ファイルの各行をパスワードとして NTLMv2 Reponse を生成し、Samba が出力したデータと一致するかどうかを比較します。パスワード一覧ファイルは、ネット上で探せば簡単に見つかります。ntlm-crack リポジトリにサンプルとして入れてある 10_million_password_list_top_1000.txt は、以下のリポジトリからコピーしたものです。

GitHub – danielmiessler/SecLists
https://github.com/danielmiessler/SecLists

では実際にコマンドを実行して brute-force の速度を計測します。マシン スペックは以下の通り。Windows マシンであればもっと新しいマシンで hashcat をぶん回せたのですが・・無念。

  • OS: Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-36-generic x86_64)
  • CPU: Intel(R) Core(TM) i5-2520M CPU @ 2.50GHz (Sandy Bridge)

とりあえず 100 万通りのパスワードを試してみます。

$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3343 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3372 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 3344 msec.

大体 3 秒ちょいです。特に工夫をしたわけでもないのですが、予想していたより速いです。UTF-16 変換の処理を予め済ませて、かつ並列処理をすれば 1M/s は軽く越えられそう。

もちろん実際に使われているのはこんな子供騙しではありません。参考として 4 年前の記事ですが、25-GPU を使って 95^8 通りのハッシュを 5.5 時間で生成できたと書かれています。ここでいう NTLM cryptographic algorithm が厳密に何を意味するのかは書かれていません。巷では、UTF-16 エンコードしたパスワードの MD4 ハッシュを NTLM ハッシュと呼ぶことが多く、仮にそうだとすると、5.5 時間という数字には HMAC-MD5 を 2 回計算する部分が含まれていません。試しに、今回作った ntlm-crack から HMAC-MD5 の演算を飛ばして再度実行してみます。

$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 347 msec.
$ ./t -f sample.in -l /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt
No matching password in /data/src/SecLists/Passwords/10_million_password_list_top_1000000.txt.
Tried 999999 strings in 346 msec.

計算時間が約 1/10 で済んでしまいました。この比率が 25-GPU マシンにも適用されるとすると、8 文字のパスワードをクラックするのに 4 年前は 55 時間かかっていたはずです。怪しい概算ですが。

25-GPU cluster cracks every standard Windows password in <6 hours | Ars Technica
http://arstechnica.com/security/2012/12/25-gpu-cluster-cracks-every-standard-windows-password-in-6-hours/

今年は AlphaGo のニュースが衝撃でしたが、そのときの Nature 論文では 1,920 CPUs + 280 GPUs のマシンで AlphaGo を動かしたという実績が書かれているので、個人での所有は難しいにしても、あるべきところ (Google や NSA?) には 4 桁のプロセッサーを動かせるマシンが存在していると仮定できます。これで 2 桁分稼げるので、10 桁程度のパスワードであれば数日で解ける恐れがあります。12 桁のパスワードにしておけば年単位の時間が必要になるので安心かも・・・?

話が逸れておきましたが、冒頭の CVE-2016-3352 の話に戻ります。Security Bulletin には、以下のような修正がなされたと書かれています。つまり、誰彼構わず NTLM SSO 認証のための SMB パケットを送るのは止めた、ということでしょうか。

The security update addresses the vulnerability by preventing NTLM SSO authentication to non-private SMB resources when users are signed in to Windows via a Microsoft Account network firewall profile for users who are signed in to Windows via a Microsoft account (https://www.microsoft.com/account) and connected to a “Guest or public networks” firewall profile.

この修正ができたということは、 「不特定多数のサーバーに SMB パケットを送ってしまう動作があったため、本来できないはずの brute-force 攻撃の標的になってしまう」 ことが問題とされていたわけです。これでようやく、Security Bulletin の "An attacker who successfully exploited the vulnerability could attempt to brute force" という部分が腑に落ちました。この件に関して言えば、brute-force の成功が現実的かどうかは関係なく、brute-force が可能であることそのものが問題だったわけです。NTLM はまだ生きていていいんだ。

あえてケチをつけるならば、CVE の方の記述における "to determine passwords via a brute-force attack on NTLM password hashes" でしょうか。NTLM は複数のハッシュ (MD4 と HMAC-MD5) を使いますが、NTLM password hash と書くと、パスワードのハッシュ、すなわち一段階目の MD4 ハッシュを想定するのが普通です。しかし、MD4 ハッシュは一回目の HMAC-MD5 の鍵として使われるだけで、ネットワーク上を流れることはなく、brute-force の攻撃対象にはなりません。Microsoft 側の Security Bulletin では "attempt to brute force a user’s NTLM password hash" となっており、こちらの記述のほうがより正確な気がします。最近の流行は pass-the-hash 攻撃なので、平文のパスワードに替えて、ハッシュ値が brute-force の標的であってもおかしくはありません。

ところで、なぜ Microsoft アカウントに関する言及があるかというと、冒頭で紹介した The Register の記事にもありますが、Microsoft アカウントのユーザー名がユーザーのメール アドレスであり、OneDrive や MSDN などのサービスのアカウントとしても使われているからです。世界のどこかにあるパソコンのユーザー アカウント "Mike" のパスワードが分かってもできることは限られていますが、Microsoft アカウントのパスワードが解析されると大変なことになります。だからこそ今までこの古いバグが放置されてきたのかもしれません。ただ、ユーザー名が平文で SMB として流れるのは気持ち悪いですが。

(2016/9/19 追記)

9 月のアップデート後に、LAN ディスクや SAMBA にアクセスできなくなったというツイートやフォーラムを幾つか見つけましたが、おそらく CVE-2016-3352 に対する修正が原因と思われます。どうするんでしょうかね。

update kb3185614の不具合について、LANDISKへの接続やリモートアクセスができなくなる – マイクロソフト コミュニティ
http://answers.microsoft.com/ja-jp/windows/forum/windows_10-update/update/f5219540-a2a5-4b09-b9b6-e944dcbbed38

SUDO for Windows

Windows で動く Linux の sudo コマンド的なものを作って GitHub で公開しました。

msmania/sudo: SUDO for Windows
https://github.com/msmania/sudo

Windows でコンソールを使った作業をする場合、必要がない限りは、管理者権限のない制限付きトークンのコマンド プロンプト (もしくは PowerShell) で作業すると思います。Windows の困ったところは、管理者権限が必要なコマンドを実行するときには、新しいコンソールを別途起動しないといけないことです。しかし、1 コマンド実行したいだけなのに、わざわざ新しくコンソールを開いて使い終わったら閉じるのは時間がもったいないのです。というわけで、通常のコンソールと管理者権限のコンソールの二窓体制で作業する、というのが日常かと思います。しかし、最近の Windows は無駄にグラフィカルで、ウィンドウを切り替えるときの Alt+Tab やタスクバー アイコンのプレビューがいちいち大袈裟です。今度はこれをレジストリで無効にしておく、というような具合に、理不尽なカスタマイズが次々と必要になります。そこで、sudo を作ることにしました。

代替策も探しましたが、意外とありません。例えば、runas.exe を使って built-in の Administrator ユーザーでログオンすれば管理者権限は使えます。しかし次の 2 点において不満があり使えません。

  • runas.exe で cmd.exe を起動すると、結局新しいコンソールが起動する
  • Build-in の Administrator 以外では管理者権限にならない

SysInternals の psexec.exe を使うと runas みたいなことができますが、こちらはそもそも管理者権限がないと実行できないので本末転倒です。

sudo に求めたい要件は以下の通りです。

  1. 通常のコンソール上でそのまま作業できる (= 新しいコンソールは開かない)
  2. Administrator ではなく、管理者グループに所属している作業ユーザーで管理者権限を使える
  3. .NET やスクリプト経由ではなく、ネイティブの sudo.exe が欲しい

GitHub 上を "sudo windows" などで検索すると、幾つかそれっぽいプロジェクトは見つかりますが、1. の要件を満たしてくれません。

maxpat78/Sudo: Executes a command requesting for elevated privileges in Windows Vista and newer OS.
https://github.com/maxpat78/Sudo

jpassing/elevate: elevate — start elevated processes from the command line
https://github.com/jpassing/elevate

どちらも動作原理は同じで、ShellExecute API を lpVerb="runas" で実行しています。UAC プロンプトを表示させて管理者権限を得るには一番簡単な方法なのですが、コンソールから cmd.exe を起動すると別のコンソールが開いてしまうのでこれは使えません。

ShellExecute function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/bb762153(v=vs.85).aspx

ShellExecute が駄目となると CreateProcess でプロセスを作るしかありません (他に WinExec という化石のような API もありますが)。が、以下のブログに記載があるように、CreateProcess で権限昇格が必要なプロセスを起動することはできません。

Dealing with Administrator and standard user’s context | Windows SDK Support Team Blog
https://blogs.msdn.microsoft.com/winsdk/2010/05/31/dealing-with-administrator-and-standard-users-context/

CreateProcess() and CreateProcessWithLogonW do not have new flags to launch the child process as elevated. Internally, CreateProcess() checks whether the target application requires elevation by looking for a manifest, determining if it is an installer, or if it has an app compat shim. If CreateProcess() determines the target application requires elevation, it simply fails with ERROR_ELEVATION_REQUIRED(740). It will not contact the AIS to perform the elevation prompt or run the app.

最後の AIS というのは Application Information Service (AppInfo サービス) という UAC を司る憎いやつです。いや、実際はお世話になっているのですが。

Understanding and Configuring User Account Control in Windows Vista
https://technet.microsoft.com/en-us/library/cc709628(v=ws.10).aspx

となると残された道は 1 つしかなく、sudo.exe を昇格させて実行し、そこから CreateProcess で子プロセスを作る方法です。権限は自動的に継承されるので、子プロセスも昇格されたままになるはずです。

さらに要件 1. を満たすためには、もう一捻り必要です。sudo.exe をコンソール アプリケーション、すなわち /SUBSYSTEM:CONSOLE オプションを使ってリンクした場合、そのコンソール プログラムを昇格していないコンソール上から起動すると、UAC プロンプトの後にやはり新たなコンソールが起動して、プログラム終了時にコンソールが破棄される動作になるので、標準出力が見えません。したがって、sudo.exe は /SUBSYSTEM:WINDOWS を使ってリンクしなければなりません。この場合、単純にプログラムから printf などで標準出力に文字を出力しても、データは破棄されるだけでどこにも表示されません。そこで、プログラムの標準出力を親プロセスのコンソールに関連付けて、小プロセスからの標準出力を受け取って親プロセスの標準出力にリダイレクトするようにします。うう面倒くさい・・。

小プロセスの標準入出力をパイプとして受け取る部分は、以下のサンプルのコードを流用できます。

Creating a Child Process with Redirected Input and Output (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx

親プロセスのコンソールを自分の標準出力に関連付ける部分は、AttachConsole API に ATTACH_PARENT_PROCESS を渡すことで簡単に実現できます。

AttachConsole function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms681952(v=vs.85).aspx

sudo.exe 起動時に UAC プロンプトを表示させるには、マニフェスト XML で requestedExecutionLevel を highestAvailable に設定するだけです。Mt.exe を使うと、exe にマニフェストを埋め込むことが出来るので、Makefile の中でリンカーの後に Mt.exe を実行するように記述します。

Mt.exe (Windows)
https://msdn.microsoft.com/en-us/library/aa375649(v=vs.85).aspx

これらを組み合わせれば、基本の動きはほぼ達成できますが、最初の MSDN のサンプルには若干問題があります。サンプルをそのままコピーして標準出力に文字を出力するだけの簡単なプロセス (ipconfig.exe など) を実行すると、文字は出力されるのですが、ipconfig.exe が終了しても sudo.exe が終わりません。ReadFromPipe における ReadFile の呼び出しで、子プロセスが終了しているにも関わらず制御が返ってこないためです。原因は、STARTUPINFO 構造体に渡した子プロセス用のパイプへのハンドル、すなわち g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を閉じていないためと考えられます。ちゃんと確かめていないのですが、STARTUPINFO 構造体に渡したハンドルは、複製されてから小プロセスに渡されるので、子プロセス側で標準入出力のハンドルを破棄しても、複製元の g_hChildStd_OUT_Wr と g_hChildStd_IN_Rd を自分で保持している限りパイプが有効で、ReadFile はそれを待ち続けているのだと思います。じゃあ単純に CreateProcess の後でをさっさとハンドルをクローズしてしまえばよいかというと、大方のシナリオでは動くとは思いますが、微妙だと思います。子プロセスによっては、すぐに標準出力にデータを出力しない動作をするものがあってもおかしくありません。しかし、sudo.exe 側はパイプにデータが残っているかどうかだけで出力を続けるかどうかを判断しているので、子プロセスが動作中にも関わらずループを抜けてしまうかもしれません。

現時点のバージョンの sudo.exe では、単純に CreateProcess のあと子プロセスが終了するまで WaitForSingleObject で待って、その後でじっくりとパイプの中を読み出すようにしました。この実装も微妙で、子プロセスの出力量が膨大であったときにパイプが溢れる可能性がありますし、そもそも子プロセスが終わるまで待っているのは美しくありません。そのうち、子プロセスの状態を監視するようなスレッドを作って対応します。

WaitForSingleObject は使うものの、INIFINITE を渡して永遠に待ち続けるのはさすがに嫌だったので、タイムアウト値を設けることにしました。初め、このタイムアウト値は環境変数経由で設定しようと思っていました。そうすれば、CreateProcess の第二引数には WinMain の引数の pCmdLine をそのまま渡すだけで済むので、楽ができるのです。しかし、ここでも Windows のコマンド プロンプトの困った仕様が立ちはだかります。コマンドを実行している時だけに有効になる一時的な環境変数が使えないのです。正式に何というのかわかりませんが、つまり以下のようなことができません。make を実行するときによく使いますね。

$ cat test.sh
echo $HOGE

$ echo $HOGE

$ HOGE=1 ./test.sh
1

$ echo $HOGE

$

一応、似たようなことはできます。それが以下のフォーラムで出てきているやり方で、cmd /C を使って子プロセスの中で set コマンドを実行してから目的のコマンドを実行する方法です。

Setting environment variable for just one command in Windows cmd.exe – Super User
http://superuser.com/questions/223104/setting-environment-variable-for-just-one-command-in-windows-cmd-exe

ちなみにこの中で紹介されている、/V オプションを使った環境変数の遅延評価は、子プロセスでは必要ですが孫プロセスでは不要です。マニアックですが、こんな感じです。

> cmd /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!"
%HOGE% !HOGE!
1 !HOGE!

> cmd /V /C "set HOGE=1 && echo %HOGE% !HOGE! && cmd /c echo %HOGE% !HOGE!"
%HOGE% 1
1 1

したがって、sudo.exe を使って cmd /c "set TIMEOUT=10 && sudo ipconfig" 的なことをやれば一応は環境変数からタイムアウト値を取れる、と思えますが結局駄目でした。UAC 昇格を要求するコマンドを起動した場合、環境変数が引き継がれません。これは Linux の sudo のデフォルト動作と同じですが、sudo に -E オプションを付加すると環境変数を引き継ぐことができます。

$ echo $HOGE

$ HOGE=1 sudo ./test.sh

$ HOGE=1 sudo -E ./test.sh
1

上記の理由で、環境変数を使うのは諦めて引数を取ることにしました。空白などの扱いを自分で決めたかったので pCmdLine をパースするステート マシンを 1 から書きました。それが CCommandOptions クラスですが、無駄に長いです。たぶんもっとまともな実装方法があると思います。アルゴリズムは苦手・・。

とりあえずこれで要件を満たすコマンドができました。出力例は ↓ の通りです。ここまで書いて言うのもなんですが、UAC ポップアップの表示が遅いので、もっさりした動作にしかなりません。なんだかんだスピードを求めるなら二窓体制が無難かも。

> sudo /t 10 powershell "get-vm | ?{$_.State -eq ‘Running’} | select -ExpandProperty networkadapters | select vmname, macaddress, switchname, ipaddresses | fl *"
[sudo] START
Spawning a process (Timeout = 10 sec.)

VMName : VM1
MacAddress : 00155DFE7A01
SwitchName : External
IPAddresses : {10.124.252.117, fe80::8d2d:7dca:4498:82f9, 2001:4898:200:13:fc0e:945e:ffce:2bb5,
2001:4898:200:13:8d2d:7dca:4498:82f9}

[sudo] END
Press Enter to exit …

Export Excel to XML using VBA

CSV などのテーブル データから XML を作りたいことが多々あります。Excel にはもともと XML へのエクスポートを行なう機能がありますが、望みどおりに動いてくれないため、VBA でマクロを書いてみましたのでコードを公開します。しかしこのご時勢では XML より JSON 派のほうが多いんですかね・・。

ちなみに開発環境は、Windows 7 SP1 上の Excel 2010 です。Office 2013 はどうも好きになれない。Windows 10 は入れたいのだが、諸事情でこの PC には入れられない・・。

さて、無味乾燥なサンプルだと面白くないので、楽天 API 経由で取ってきた商品データを使うことにします。といっても簡単で、PowerShell で以下のコマンドレットを実行するだけです。クエリ URL の <APPID> のところは、各自のアプリ ID を入れてください。持っていない人は、https://webservice.rakuten.co.jp/ でアカウントを登録して作ることができます。

PS> $Client = New-Object System.Net.WebClient
PS> $QueryUrl = ‘
https://app.rakuten.co.jp/services/api/BooksTotal/Search/20130522?format=xml&keyword=%E5%AF%BF%E5%8F%B8&booksGenreId=000&hits=10&applicationId=<APPID>’
PS> $Client.DownloadFile($QueryUrl, ‘E:\MSWORK\Generator\Sushi.xml’)

上記は、楽天ブックスから "寿司" というキーワードで 10 件の検索結果を取ってきて、Sushi.xml という XML ファイルで保存するものです。これを Excel で開くと、以下のダイアログが出てくるので、「XML テーブルとして開く」 を選んで OK をクリックします。

image

スキーマ情報がないので以下のダイアログが表示されます。OK をクリックします。

image

テーブルができます。

image

ちなみにこのまま、XML ソースの画面にある [エクスポートする対応付けの確認..] をクリックすると、"例外的なデータ" (= denormalized data) が存在するとかでエクスポートできません。意味不明・・。

image

と、いうわけで、このようなテーブルを XML にエクスポートするためのマクロを書きます。

元の Sushi.xml の構造は、大まかには以下のようになっています。検索結果の各アイテムの情報が root  > items > item というノードに保存されています。簡単のため、items 以外の count や page といった検索のメタ情報は Excel のシートから除外することにします。

<root>
  <count>4926</count>
  <page>1</page>
  <first>1</first>
  <last>10</last>
  <hits>10</hits>
  <carrier>0</carrier>
  <pageCount>100</pageCount>
  <Items>
    <Item>
      <title>検索結果1</title>
      …
    </Item>
    <Item>
      <title>検索結果2</title>
      …
    </Item>
  </Items>
  <GenreInformation />
</root>

列を削除して XMLGenerator.xlsm という名前で保存します。マクロを使うので、拡張子は xlsm という形式にします。

image

リボンに [開発] タブが表示されていない人は、以下の手順で有効にしてください。

[開発] タブを表示する – Office のサポート
https://support.office.com/ja-jp/article/-%E9%96%8B%E7%99%BA-%E3%82%BF%E3%83%96%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B-e1192344-5e56-4d45-931b-e5fd9bea2d45

コードを書く前にもう一点確認。テーブル内のセルのひとつにカーソルを置いた状態で [デザイン] タブを開いてテーブル名を確認します。以下の画面キャプチャだと、左上の方に "テーブル1" と入力されている部分がそれです。

image

では、[開発] タブにある [Visual Basic[ をクリックして VBA の画面を起動します。ほとんど VB6 と同じですね。

XML の読み書きには MSXML を使いたいので、まずは参照設定を追加します。メニューから [ツール] > [参照設定] を選びます。

image

ライブラリのリストから Microsoft XML というのを探して、チェックをつけてから OK をクリックします。v3.0 と v6.0 の両方がありましたが、新しい方の v6.0 を選んでおきます。

image

コードを追加します。Sheet1 または ThisWorkbook のどちらかのモジュールに紐付ける、もしくは新規にモジュールを作る、という選択肢がありますが、今回は ThisWorkbook にコードを追加します。ThisWorksheet を右クリックして、コンテキスト メニューの [コードの表示] を選びます。

image

コードをごにょごにょ書きます。先ほど確認した "テーブル1" というテーブル名をそのまま使っています。

Option Explicit
Private Function AddElementToNode(xmlObj As MSXML2.DOMDocument60, rootNode As MSXML2.IXMLDOMNode, elementName As String, elementValue As String) As MSXML2.IXMLDOMNode
    Dim newElement As MSXML2.IXMLDOMElement
    Set newElement = xmlObj.createElement(elementName)
    newElement.Text = elementValue
    Set AddElementToNode = rootNode.appendChild(newElement)
End Function

Private Sub AddAttributeToElement(xmlObj As MSXML2.DOMDocument60, rootElement As MSXML2.IXMLDOMElement, attributeName As String, attributeValue As String)
    Dim newAttribute As MSXML2.IXMLDOMAttribute
    Set newAttribute = xmlObj.createAttribute(attributeName)
    newAttribute.Text = attributeValue
    rootElement.setAttributeNode newAttribute
End Sub

Sub ExportTableToXml()
    Dim sheet1 As Worksheet
    Set sheet1 = ThisWorkbook.Worksheets(1)
   
    Dim outputFile As String
    outputFile = ThisWorkbook.Path & "\001.xml"
   
    Dim msg As String
    Dim result As String
    msg = "If [" & outputFile & "] exists, overwrite it?"
    result = MsgBox(msg, vbYesNo, "Just to make sure…")
   
    If result = vbNo Then Exit Sub
   
    Dim table1 As range
    Set table1 = range("テーブル1")
              
    Dim xmlObj As MSXML2.DOMDocument60
    Set xmlObj = New MSXML2.DOMDocument60

    xmlObj.async = False
    xmlObj.setProperty "SelectionLanguage", "XPath"
   
    xmlObj.appendChild xmlObj.createProcessingInstruction("xml", "version=’1.0′ encoding=’utf-8’")

    Dim rootElement As MSXML2.IXMLDOMElement
    Set rootElement = xmlObj.createElement("root")
   
    Dim itemsContainer As MSXML2.IXMLDOMElement
    Set itemsContainer = xmlObj.createElement("items")
    rootElement.appendChild itemsContainer
   
    Dim row As Integer
    For row = 1 To table1.Rows.Count
        Dim itemElem As MSXML2.IXMLDOMElement
        Set itemElem = xmlObj.createElement("item")
       
        Dim col As Integer
        For col = 1 To table1.Columns.Count
            Dim colName As String
            colName = table1.Cells(0, col)
           
            If LCase(colName) = "title" Then
                AddAttributeToElement xmlObj, itemElem, colName, table1.Cells(row, col)
            Else
                AddElementToNode xmlObj, itemElem, colName, table1.Cells(row, col)
            End If
        Next

        itemsContainer.appendChild itemElem
    Next
   
    xmlObj.appendChild rootElement
    xmlObj.Save outputFile
   
    msg = "[" & outputFile & "] has been created/updated."
    MsgBox msg, vbOKOnly, "Done."
End Sub

プロシージャ呼び出しにはカッコをつけない、オブジェクトを New するときは Set を使う、などの意味不明な文法に苦労しましたが、まあこれで動きます。

ユーザビリティを考えるのであれば、ワークシートのほうにボタンを配置して、そこからマクロを呼ぶようにするのが自然でしょうか。というわけで、ワークシートに戻って [開発] タブから [デザイン モード] に入ってボタンを挿入します。[マクロの登録] ダイアログが出てくるので、先ほど作ったプロシージャ ExportTableToXml() を選択して OK をクリックします。

image

こんな感じになりました。

image

ボタンをクリックすると出力ファイルのパスとともに確認ダイアログが出るようにしています。出力ディレクトリは Excel ファイルと同じところ、ファイル名は 001.xml で固定です。

image

出力がうまくいけば、以下のようなダイアログが出ます。

image

出力された XML をブラウザー (ここでは Chrome) で開いて確認します。もとの Sushi.xml とほぼ同じです。変えた部分は、root 直下の items 以外のノードを削除したことと、本のタイトルを <title> ノードの値からく、<item> の name 属性に移動したことです。

image

16bit programming in Windows 95

前回 MS-DOS で QuickAssembler を動かしてみたのはいいものの、32bit のアセンブリ言語をコンパイルできないことに気づいてしまったので、結局 Windows を入れることに。

しかし Hyper-V で Windows 95 のインストーラーを実行すると、仮想マシンがハングしてしまう現象に遭遇。Windows 95 のインストーラーはマウスに対応しているため、このハングの原因はおそらく MS-DOS の MOUSE コマンドを実行するとハングしてしまう現象と同じ気がします。Hyper-V 使えないじゃん、ということで ESXi 5.1 で試してみたら、こちらは問題なし。やっぱり開発用途には VMware に軍配が上がります。

OS のインストール後、開発環境として最初は何も考えずに Visual C++ 6.0 を入れてみたのですが、Visual C++ 6.0 は 16bit アプリケーションの開発に対応していなかった、ということに初めて気づき、以下の 2 つを入れ直しました。何とも世話の焼ける・・

  • VIsual C++ 1.52
  • MASM 6.11

セットアップ中の画面の一部を紹介します。まずは Windows 95。実に懐かしい。

image image

image

十数年ぶりに触ってみると、今では当然のように使っている機能がまだ実装されていなくて驚きます。例えば・・

  • コマンド プロンプトが cmd.exe じゃなくて command.com
  • コマンド プロンプトで矢印キー、 Tab キーが使えない
  • コマンド プロンプトで、右クリックを使ってクリップボードからの貼り付けができない
  • メモ帳で Ctrl+A、Ctrl+S が使えない
  • エクスプローラーにアドレスバーがない

以下は Visual C++ と MASM のインストール画面を抜粋したもの。

image image

MASM 6.11 のインストーラーは CUI。

image

DOS, Windows, WIndows NT の全部に対応しているらしい。

image

MASM はいろいろと注意事項が多い。

image image

image

インストールが完了したら、環境変数の設定を行なうためのバッチ ファイルを作ります。サンプルのスクリプトがインストールされているので、それを組み合わせるだけです。

@echo off
set PATH=C:\MSVC\BIN;C:\MASM611\BIN;C:\MASM611\BINR;%PATH%
set INCLUDE=C:\MSVC\INCLUDE;C:\MSVC\MFC\INCLUDE;C:\MASM611\INCLUDE;%INCLUDE%
set LIB=C:\MSVC\LIB;C:\MSVC\MFC\LIB;C:\MASM611\LIB;%LIB%
set INIT=C:\MSVC;C:\MASM611\INIT;%INIT%

MASM、VC++ ともに NMAKE を持っていますが、VC++ 1.52 に入っている NMAKE のバージョンが新しかったので、VC++ を先頭にしました。

環境ができたところで、今回は 「はじめて読む 486」 の例題を試してみます。本文に掲載されている例題は、Borland C++ 3.1/Turbo Assembler 3.2 用の文法になっているため、細かいところは VC++/MASM 用に書き換えないといけません。というわけでこんな感じ。

まずは C ソースファイル main.c。

#include <stdlib.h>
#include <stdio.h>

extern short GetVer();
extern void RealToProto();
extern void ProtoToReal();

void main(int argc, char **argv) {
    printf("Hello, MS-DOS%d!\n", GetVer());
    RealToProto();
    ProtoToReal();
    printf("Successfully returned from Protected mode.\n");
    exit(0);
}

次がアセンブリ utils.asm。

.386
.MODEL small
.code

;* GetVer – Gets DOS version.
;*
;* Shows:   DOS Function – 30h (Get MS-DOS Version Number)
;*
;* Params:  None
;*
;* Return:  Short integer of form (M*100)+m, where M is major
;*          version number and m is minor version, or integer
;*          is 0 if DOS version earlier than 2.0

_GetVer  PROC

        mov     ah, 30h                 ; DOS Function 30h
        int     21h                     ; Get MS-DOS version number
        .IF     al == 0                 ; If version, version 1
        sub     ax, ax                  ; Set AX to 0
        .ELSE                           ; Version 2.0 or higher
        sub     ch, ch                  ; Zero CH and move minor
        mov     cl, ah                  ;   version number into CX
        mov     bl, 100
        mul     bl                      ; Multiply major by 10
        add     ax, cx                  ; Add minor to major*10
        .ENDIF
        ret                             ; Return result in AX

_GetVer  ENDP

public _RealToProto
_RealToProto    proc    near
                push bp
                mov bp, sp
                ;
                mov eax, cr0
                or eax, 1
                mov cr0, eax
                ;
                jmp flush_q1
flush_q1:
                pop bp
                ret
_RealToProto    endp

public _ProtoToReal
_ProtoToReal    proc    near
                push bp
                mov bp, sp
                ;
                mov eax, cr0
                and eax, 0fffffffeh
                mov cr0, eax
                ;
                jmp flush_q2
flush_q2:
                pop bp
                ret
_ProtoToReal    endp

        END

最後に Makefile。QuickC のときよりは現在の書式に近づきましたが、相変わらずリンカへの入力の渡し方がおかしい。

PROJ = TEST
USEMFC = 0
CC = cl
ML = ml
CFLAGS =/nologo /W3 /O /G3
LFLAGS =/NOLOGO /ONERROR:NOEXE
AFLAGS =
LIBS =
MAPFILE =nul
DEFFILE =nul

all: $(PROJ).EXE

clean:
    @del *.obj
    @del *.exe
    @del *.bnd
    @del *.pdb

UTILS.OBJ: UTILS.ASM
    $(ML) $(AFLAGS) /c UTILS.ASM $@

MAIN.OBJ: MAIN.C
    $(CC) $(CFLAGS) /c MAIN.C $@

$(PROJ).EXE:: MAIN.OBJ UTILS.OBJ
    echo >NUL @<<$(PROJ).CRF
MAIN.OBJ +
UTILS.OBJ
$(PROJ).EXE
$(MAPFILE)
$(LIBS)
$(DEFFILE)
;
<<
    link $(LFLAGS) @$(PROJ).CRF
    @copy $(PROJ).CRF $(PROJ).BND

プログラムはとても単純で、前回と同様に int 21 で DOS のバージョンを表示してから、コントロール レジスタ 0 の PE ビットを変更して特権レベルを変更し、また戻ってくる、というものです。ただし上記のコードでは、C、アセンブリのコンパイルは通るものの、以下のリンクエラーが出てうまくいきません。

UTILS.OBJ(UTILS.ASM) : fatal error L1123: _TEXT : segment defined both 16- and 32-bit

image

ここで 2 時間ぐらいハマりました。リンクエラーの原因は単純で、VC++ 1.52 のコンパイラは 16bit コードを生成しているのに対し、MASM は 32bit コードを生成しているためです。エラー メッセージの通り、16bit と 32bit の 2 つのコード セグメントを含む実行可能ファイルを生成できないためのエラーです。

このプログラムで確かめたいのは、16bit リアル モードから PE ビットを変更してプロテクト モードに移行し、再び 16bit リアル モードに戻ってくる動作です。したがって生成されるべき EXE は 16bit アプリケーションであり、VC++ の生成する main.obj は問題なく、MASM が 16bit コードを生成するように指定したいところです。

MASM が 32bit コードを生成する理由は、utils.asm の先頭の .386 で CPU のアーキテクチャを指定しているためです。これがないと、32bit レジスタの eax などが使えません。CPU のアーキテクチャを指定したことで、その後の .code セグメント指定が自動的に 32bit コード セグメントになってしまっています。

32bit 命令を有効にしつつ、セグメントは 16bit で指定する方法が見つかればよいわけです。NASM や GNU Assembler だと簡単に指定できるようですが・・いろいろと探して、以下のフォーラムを見つけました。10 年前に同じ悩みを抱えている人がいた!

Link Fatal Error 1123
http://www.masmforum.com/board/index.php?PHPSESSID=786dd40408172108b65a5a36b09c88c0&topic=1382.0

ファイルの先頭で .MODEL と CPU 指定を入れ替えればいいらしい。こんなん知らんて。

.MODEL small
.386

.code

;* GetVer – Gets DOS version.
;*
;* Shows:   DOS Function – 30h (Get MS-DOS Version Number)
;*
;* Params:  None
;*
;* Return:  Short integer of form (M*100)+m, where M is major
;*          version number and m is minor version, or integer
;*          is 0 if DOS version earlier than 2.0

_GetVer  PROC

以下、ずっと同じなので省略

これで無事ビルドが成功し、実行できました。文字が出力されているだけなので、本当にプロテクト モードに変わったかどうか疑わしくはありますが、まあ大丈夫でしょう。16bit の道は険しい。

image

Custom WebOC in Non-MFC Dialog-Based App

WebOC と呼ばれる ActiveX コントロールを使うと、IE が利用している HTML レンダリング エンジン (Trident; mshtml.dll) を、IE 以外のアプリケーションからも利用することができます。余談ですが、このページに、"Web Browser ActiveX control (WebOC)" と書かれていますが、どう略したら WebOC になるのか謎です。Web Browser OLE Control の略じゃないのか、と思っていますが真相は不明です。

MFC を使うと、あまりコードを書かなくても、ダイアログ ベースのアプリケーションに WebOC を追加できます。MSDN にある以下のサンプルが分かりやすいです。この情報は、VC++6.0/IE4 時代に書かれたようですが、今でも問題なく動きます。

Using MFC to Host a WebBrowser Control (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa752046(v=vs.85).aspx

ほぼ MSDN 通りに書いた MFC アプリがこれ。Visual Studio 2012/IE11 の環境で動きました。

https://github.com/msmania/miniweboc

宗教上の理由で MFC が使えない場合でも、書くコードは増えますが、WebOC は利用可能です。今の時代、WebOC を使う人が早々いないので、サンプルを探すのに苦労しましたが、いろいろと切り貼りして、こんなコードを書いてみました。ダイアログ ベースの非 MFC ネイティブ アプリケーションで WebOC を使う例です。

https://github.com/msmania/minibrowser

なお、単に HTML コンテンツを表示するダイアログを表示させたいときは、mshtml.dll がエクスポートしている ShowHTMLDialog 関数を呼ぶ方法もあります。ただし、WebOC を使う場合と比べると、できることは限られます。

ShowHTMLDialog function (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa741858(v=vs.85).aspx

例えば WebOC では、IDocHostUIHandler::GetExternal を独自実装することで、ホストしているページの DOM ツリーを拡張して、JavaScript から window.external オブジェクト経由で C++ の処理を実行できるようにしています。

サンプルを書くにあたって、以下のサイトを参考にしました。

How to disable the default pop-up menu for CHtmlView in Visual C++
https://support.microsoft.com/en-us/kb/236312
—-> MFC を使っている場合で、IDocHostUIHandler を独自実装するサンプル

Use an ActiveX control in your Win32 Project without MFC with CreateWindowEx or in a dialog box – CodeProject
http://www.codeproject.com/Articles/18417/Use-an-ActiveX-control-in-your-Win-Project-witho
—-> 非 MFC のダイアログ ベースのアプリケーションから WebOC を使うサンプル

WebBrowser Customization (Internet Explorer)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa770041(v=vs.85).aspx
—-> WebOC リファレンス。

IDocHostUIHandler interface (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa753260(v=vs.85).aspx
—-> IDocHostUIHandler インターフェース リファレンス。

プログラムにおける主要なポイントは以下の通り。

  1. ダイアログ リソース上で、適当なコントロール クラス名を指定してカスタム コントロールを配置。
    サンプルでは "MINIHOST" という名前を指定。
  2. カスタム コントロールが WM_CREATE メッセージを処理する際の初期化コードにおいて、WebOC オブジェクトを紐付けた OLE オブジェクトをコントロール ウィンドウに登録
  3. OLE オブジェクトの IDocHostUIHandler::GetExternal が呼ばれたときに、独自実装した IDispatch のオブジェクトを返すように実装
  4. IDispatch::GetIDsOfNames と IDispatch::Invoke を実装

3. と 4. は window.external の実装で、上述の KB236312 に書いてある内容です。最大のポイントは 2. で、以下 2 つのメソッドを実行して WebOC をダイアログ上のカスタム コントロールに登録します。

IOleObject::SetClientSite method (COM)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms684013(v=vs.85).aspx
—-> WebOC のオブジェクトを CoCreateInstance で作成し、"サイト" として IOleObject を実装したコンテナー オブジェクトに登録

IOleObject::DoVerb method (COM)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms694508(v=vs.85).aspx
—-> OLE オブジェクトを、ウィンドウ上で有効化

WebOC のコンテナーとなる OLE オブジェクトは SetWindowLongPtr を使ってウィンドウ ハンドルに紐づけました。これは必須ではなく、例えばグローバル変数として保存しておくだけでもいいですが、SetWindowLongPtr を使う方法が一般的だと思います。

WebOC に限らず COM オブジェクト全般におけるポイントですが、参照カウントを使ってオブジェクトの確保/解放を管理しないといけません。まず、参照カウンターとなるメンバー変数を適当に定義し、IUnknown::AddRef と IUnknown::Release の中でインクリメント/デクリメントする処理を書きます。

単に ++ と — を使うだけでも動きますが、マルチ スレッド環境を考慮して InterlockedIncrement/InterlockedDecrement を使うのが一般的です。

STDMETHODIMP_(ULONG) MiniBrowserSite::AddRef(void) {
    return InterlockedIncrement(&_ulRefs);
}

STDMETHODIMP_(ULONG) MiniBrowserSite::Release(void) {
    ULONG cref = (ULONG)InterlockedDecrement(&_ulRefs);
    if (cref > 0) {
        return cref;
    }
    delete this;
    return 0;
}

オブジェクトの作成は、new 演算子を使いますが、ポインターを SetWindowLongPtr で登録するときに、AddRef() を自分で呼び出して参照カウントを増やさないといけません。さらに、WM_DESTROY でオブジェクトを破棄するときには、delete 演算子で直接オブジェクトを解放するのではなく、Release() を使って、参照カウントを減らすだけにしておきます。

case WM_CREATE:
    BrowserSite = new MiniBrowserSite(h);
    if (BrowserSite == nullptr || FAILED(BrowserSite->InPlaceActivate())) {
        if (BrowserSite)
            delete BrowserSite;

        return -1;
    }

    SetWindowLongPtr(h, GWLP_USERDATA, (LONG_PTR)BrowserSite);
    BrowserSite->AddRef();
    break;

case WM_DESTROY:
    BrowserSite = (MiniBrowserSite*)GetWindowLongPtr(h, GWLP_USERDATA);
    if (BrowserSite) {
        BrowserSite->Cleanup();
        BrowserSite->Release(); // do not ‘delete BrowserSite’ directly
    }
    break;

WebOC のコンテナーとなる OLE オブジェクトの参照カウントは、IWebBrowser2::Navigate などの WebOC に対する操作を行ったときに、mshtml.dll 内部で頻繁に変更されます。これは、mshtml.dll 内部でもオブジェクトへのポインターが複数個所に保存されていることが予想されるためで、勝手にアプリケーションから delete してしまうと、mshtml.dll 内部のポインターが Use-After-Free を誘発する可能性があります。また、WM_CREATE で AddRef を忘れると、逆に WM_DESTROY で deleete を実行するときに double free でクラッシュする可能性があります。

minibrowser!MiniBrowserSite::Release にブレークポイントを置いてスタックを見てみると、Navigate を呼ぶだけで 20  回以上はヒットします。その中の一つを例として以下に示します。スタックの中に ieframe!CWebBrowserOC::Navigate が含まれていない代わりに、jscript9 の処理が見えます。

Child-SP          RetAddr           Call Site
00000000`002be298 000007fe`f10647ab minibrowser!MiniBrowserSite::Release
00000000`002be2a0 000007fe`f13ce7f5 ieframe!IUnknown_SafeReleaseAndNullPtr<IACList>+0x2b
00000000`002be2d0 000007fe`f13cdcd8 ieframe!CIEFrameAuto::_GetParentFramePrivate+0xc5
00000000`002be320 000007fe`f142cfbe ieframe!CIEFrameAuto::GetParentFrame+0x18
00000000`002be360 000007fe`f12d34cf ieframe!CWebBrowserSB::QueryService+0x13e
00000000`002be3a0 000007fe`ff101c82 ieframe!CIEFrameAuto::QueryService+0x10d4df
00000000`002be3d0 000007fe`f11c6105 SHLWAPI!IUnknown_QueryService+0x5a
00000000`002be430 000007fe`f10db378 ieframe!GetTopWBConnectionPoints+0x52
00000000`002be480 000007fe`f10db323 ieframe!FireEvent_BeforeScriptExecute+0x38
00000000`002be4e0 000007fe`ea00faf3 ieframe!CBaseBrowser2::FireBeforeScriptExecute+0x33
00000000`002be510 000007fe`ea00f9e7 mshtml!CWebOCEvents::BeforeScriptExecute+0xf3
00000000`002be590 000007fe`e9d72a88 mshtml!CScriptCollection::FireFirstScriptExecutionEvent+0xa0
00000000`002be5d0 000007fe`e7b4aee1 mshtml!CActiveScriptHolder::OnEnterScript+0xaa
00000000`002be600 000007fe`e7b4ae17 jscript9!ScriptEngine::OnEnterScript+0xbd
00000000`002be660 000007fe`e7b4ade4 jscript9!ScriptSite::ScriptStartEventHandler+0x27
00000000`002be690 000007fe`e7b4b075 jscript9!Js::ScriptContext::OnScriptStart+0x7d
00000000`002be6d0 000007fe`e7b4acc7 jscript9!Js::JavascriptFunction::CallRootFunction+0xc5
00000000`002be7b0 000007fe`e7b4ac1c jscript9!ScriptSite::CallRootFunction+0x63
00000000`002be810 000007fe`e7bd1346 jscript9!ScriptSite::Execute+0x122
00000000`002be8a0 000007fe`e7bd080d jscript9!ScriptEngine::ExecutePendingScripts+0x208
00000000`002be990 000007fe`e7bd1fb4 jscript9!ScriptEngine::ParseScriptTextCore+0x4a5
00000000`002beaf0 000007fe`ea00f6e1 jscript9!ScriptEngine::ParseScriptText+0xc4
00000000`002beba0 000007fe`ea00df0b mshtml!CActiveScriptHolder::ParseScriptText+0xc1
00000000`002bec20 000007fe`ea00db91 mshtml!CJScript9Holder::ParseScriptText+0xf7
00000000`002becd0 000007fe`ea00ef9d mshtml!CScriptCollection::ParseScriptText+0x28c
00000000`002bedb0 000007fe`ea00e9ae mshtml!CScriptData::CommitCode+0x3d9
00000000`002bef80 000007fe`ea00e731 mshtml!CScriptData::Execute+0x283
00000000`002bf040 000007fe`ea58fb75 mshtml!CHtmScriptParseCtx::Execute+0x101
00000000`002bf080 000007fe`e9d7db8d mshtml!CHtmParseBase::Execute+0x241
00000000`002bf170 000007fe`e9ff8e1f mshtml!CHtmPost::Exec+0x534
00000000`002bf380 000007fe`e9ff8d70 mshtml!CHtmPost::Run+0x3f
00000000`002bf3b0 000007fe`e9ffa6f8 mshtml!PostManExecute+0x70
00000000`002bf430 000007fe`e9ffe0c3 mshtml!PostManResume+0xa1
00000000`002bf470 000007fe`e9d54917 mshtml!CHtmPost::OnDwnChanCallback+0x43
00000000`002bf4c0 000007fe`e9c9295c mshtml!CDwnChan::OnMethodCall+0x41
00000000`002bf4f0 000007fe`e9c8aa74 mshtml!GlobalWndOnMethodCall+0x246
00000000`002bf590 00000000`77269bd1 mshtml!GlobalWndProc+0x186
00000000`002bf620 00000000`772698da USER32!UserCallWinProcCheckWow+0x1ad
00000000`002bf6e0 00000000`77274c1f USER32!DispatchMessageWorker+0x3b5
00000000`002bf760 00000000`77274edd USER32!DialogBox2+0x1b2
00000000`002bf7f0 00000000`77274f52 USER32!InternalDialogBox+0x135
00000000`002bf850 00000000`7726d476 USER32!DialogBoxIndirectParamAorW+0x58
00000000`002bf890 00000001`3f141fe4 USER32!DialogBoxParamW+0x66
00000000`002bf8d0 00000001`3f142ec4 minibrowser!wWinMain+0x94
00000000`002bf960 00000000`771459cd minibrowser!__tmainCRTStartup+0x148
00000000`002bf9a0 00000000`7737b981 kernel32!BaseThreadInitThunk+0xd
00000000`002bf9d0 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

参照カウントの処理が意図通りに動作しているかどうかを確認する方法として、Application Verifier でメモリのフラグを有効にして動作確認を行う方法があります。ただし Application Verifier は、モジュールが解放されるときにリークしているメモリを検出するので、モジュールのアンロードを行なわずにいきなりプロセスが終了するプログラムだと、メモリ リークを検出できません。

image

このような場合、クラスのデストラクターが呼ばれるかどうかをデバッガーから確認するの方法が確実です。ただし、今回のサンプル プログラムのようにデストラクターが空の場合、minibrowser!MiniBrowserSite::~MiniBrowserSite や minibrowser!CExternalDispatch::~CExternalDispatch にはヒットしないので注意が必要です。(最適化しないでコンパイルすれば使えるかも・・)

Release() から delete 演算子を呼んでいるところで止めて、そのシンボルを見ると、minibrowser!MiniBrowserSite::`scalar deleting destructor’ という不思議な関数が呼ばれています。

0:002> uf 00000001`3f801c80
minibrowser!MiniBrowserSite::Release:
00000001`3f801c80 4883ec28        sub     rsp,28h
00000001`3f801c84 83c8ff          or      eax,0FFFFFFFFh
00000001`3f801c87 f00fc14118      lock xadd dword ptr [rcx+18h],eax
00000001`3f801c8c ffc8            dec     eax
00000001`3f801c8e 7512            jne     minibrowser!MiniBrowserSite::Release+0x22 (00000001`3f801ca2)

minibrowser!MiniBrowserSite::Release+0x10:
00000001`3f801c90 4885c9          test    rcx,rcx
00000001`3f801c93 740b            je      minibrowser!MiniBrowserSite::Release+0x20 (00000001`3f801ca0)

minibrowser!MiniBrowserSite::Release+0x15:
00000001`3f801c95 488b01          mov     rax,qword ptr [rcx]
00000001`3f801c98 ba01000000      mov     edx,1
00000001`3f801c9d ff5048          call    qword ptr [rax+48h]

minibrowser!MiniBrowserSite::Release+0x20:
00000001`3f801ca0 33c0            xor     eax,eax

minibrowser!MiniBrowserSite::Release+0x22:
00000001`3f801ca2 4883c428        add     rsp,28h
00000001`3f801ca6 c3              ret

0:002> bp 00000001`3f801c9d
0:002> g
Breakpoint 2 hit
minibrowser!MiniBrowserSite::Release+0x1d:
00000001`3f801c9d ff5048          call    qword ptr [rax+48h] ds:00000001`3f820a20={minibrowser!Mini
BrowserSite::`scalar deleting destructor’ (00000001`3f8013a0)}
0:000> ln poi(rax+48)
(00000001`3f8013a0)   minibrowser!MiniBrowserSite::`scalar deleting destructor’   |  (00000001`3f801420)   minibrowser!CExternalDispatch::AddRef
Exact matches:
    minibrowser!MiniBrowserSite::`scalar deleting destructor’ (void)
0:000>

そこで、この `scalar deleting destructor’ という関数にブレークポイントを置いてダイアログを閉じたときの動作を見ると、コンテナー OLE オブジェクトも IDispatch オブジェクトも解放されていることが確認できました。

0:000> k
Child-SP          RetAddr           Call Site
00000000`0024efc8 00000001`3fd21c70 minibrowser!CExternalDispatch::`scalar deleting destructor’
00000000`0024efd0 00000001`3fd213ec minibrowser!CExternalDispatch::Release+0x20
00000000`0024f000 00000001`3fd21ca0 minibrowser!MiniBrowserSite::`scalar deleting destructor’+0x4c
00000000`0024f040 00000001`3fd219be minibrowser!MiniBrowserSite::Release+0x20
00000000`0024f070 00000000`77269bd1 minibrowser!MiniBrowserSite::MiniHostProc+0xee
00000000`0024f500 00000000`772672cb USER32!UserCallWinProcCheckWow+0x1ad
00000000`0024f5c0 00000000`77266829 USER32!DispatchClientMessage+0xc3
00000000`0024f620 00000000`7739dae5 USER32!_fnDWORD+0x2d
00000000`0024f680 00000000`7725cbfa ntdll!KiUserCallbackDispatcherContinue
00000000`0024f708 00000000`7727505b USER32!ZwUserDestroyWindow+0xa
00000000`0024f710 00000000`77274edd USER32!DialogBox2+0x2ec
00000000`0024f7a0 00000000`77274f52 USER32!InternalDialogBox+0x135
00000000`0024f800 00000000`7726d476 USER32!DialogBoxIndirectParamAorW+0x58
00000000`0024f840 00000001`3fd21fe4 USER32!DialogBoxParamW+0x66
00000000`0024f880 00000001`3fd22ec4 minibrowser!wWinMain+0x94
00000000`0024f910 00000000`771459cd minibrowser!__tmainCRTStartup+0x148
00000000`0024f950 00000000`7737b981 kernel32!BaseThreadInitThunk+0xd
00000000`0024f980 00000000`00000000 ntdll!RtlUserThreadStart+0x1d

`scalar deleting destructor’ を始め、COM オブジェクトのデバッガーからの見え方については次回ってことで。

Reflective DLL Injection

社内のツールで、任意のプロセスに対して DLL のコードを埋め込んでそれを呼び出すツールがありました。そのソースを見ると、VirtualAllocEx、WriteProessMemory、そして CreateRemoteThread を使ってけっこう簡単に他プロセスへのコードの埋め込みを実現していました。

VirtualAllocEx function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa366890(v=vs.85).aspx

CreateRemoteThread function (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/ms682437(v=vs.85).aspx

何これ超便利・・・と感動しつつこの方法についてググると、かなり古くから知られた手法の一つのようで、"createremotethread virtualallocex コード" などのキーワードで日本語のサイトもたくさんヒットします。検索上位に来て分かりやすいのが↓あたり。CreateRemoteThread 以外にも、ウィンドウのサブクラス化を使う方法がメジャーなようです。

ドリーム工房 – デスクトップを乗っ取る
http://www18.atpages.jp/skydreamer/maniax/desktop.php

別のプロセスにコードを割り込ませる3つの方法 – インターネットコム
http://internetcom.jp/developer/20050830/26.html

上記の一つ目のサイトで紹介されている方法は、事前にアセンブリ言語で書いておいたペイロードを WriteProcessMemory でターゲットに埋め込んでおいて、そのアドレスを開始アドレスとしてCreateRemoteThreaad を呼び出しています。ペイロードの中に LoadLibrary する DLL のファイル名をハードコードして、LoadLibrary を直接アドレス指定で call しています。

二つ目のサイトの方法はもっと単純で、開始アドレスを LoadLibrary のアドレスにし、リモートスレッドのパラメーターとして、予めターゲットに埋め込んておいた DLL のファイル名となる文字列のアドレスを渡すというものです。

これらの方法は、Kernel32.dll に実装された LoadLibrary() のエントリポイント (実体は kernelbase.dll) がプロセス間で共有であるという前提に基づいています。この前提については、上記 internetcom.jp の 「付録 A)なぜKERNEL32.DLLとUSER32.DLLは常に同じアドレスにマッピングされるのか」 で説明されています。

これだけでも十分に実用に値するのですが、個人的にはちょっとエレガントさに欠ける気がします。そもそも、kernel32.dll が同じところにマップされるという前提に依存するのがあまり美しくないのですが、もう一つ問題点があります。インジェクターとターゲットのビット数が違うときに、LoadLibrary のアドレスをインジェクター側で取得できないという点です。インジェクターが 64bit プロセスのときは、64bit kernel32.dll 上の LoadLibrary のアドレスが取得できますが、ターゲットが 32bit のときにこれは使えません。逆の場合も同じです。とはいっても些細な問題であり、32bit と 64bit それぞれのインジェクターを用意すればいいだけの話ですが、ターゲットによってインジェクターを変えるのはやはり美しくないのです。

後始末の問題もあります。ターゲットの中で LoadLibrary を実行することができれば、ロードした DLL の DllMain が呼ばれるので、そこからまた新たに新しいスレッドを作るなどして任意のコードを実行させることができます(ローダーロックの問題があるので、DllMain 関数自体はさっさと終わらせないとおかしなことになる、はず。)。また、立つ鳥跡を濁さず、という諺もあるように、全てが終わった後は DLL は FreeLibrary され、さらに VirtualAllocEx しておいたメモリ領域も解放されている、というのが日本的美的センスから見た美しいコード インジェクションではないでしょうか。

簡単そうなのは、ターゲットプロセスではなくインジェクターで後処理を行う方法です。これは上記のinternetcom.jp で紹介されています。メモリ領域の解放は単純に VirtualFreeEx を呼ぶだけですが、FreeLibrary には少しトリックが必要です。というのも、FreeLibrary を呼ぶためには、LoadLibrary からの戻り値である HMODULE、すなわちロードされた DLL のベース アドレスが必要だからです。紹介されている例では、CreateRemoteThread で起動したリモートスレッドが終了するまで待機してから、ベース アドレスをスレッドの終了コードとして GetExitCodeThread を使って取得しています。これはなかなか面白い方法ですが、64bit だと使えないはずです。なぜなら、スレッドの開始関数である LPTHREAD_START_ROUTINE は、本来 DWORD を返すことが想定されており、GetExitCodeThread で取得できるのも DWORD の 32bit 整数だけです。64bit では当然 HMODULE も 64bit アドレスなので、GetExitCodeThread だと、イメージベースの 上位 32bit を取得できません。そのほかの方法として、インジェクトしたコードの中でイメージベースをバッファー内に書き込んでおいて、それをインジェクター側から参照する方法が考えられます。これなら任意のサイズのデータをターゲットからインジェクターに返すことができるので、64bit でも問題はありません。ただ、そもそものデザインとして、インジェクター側でリモート スレッドの終了を待機するのは不満です。

というわけで、後処理をターゲット内で行う方法を考えます。ターゲット内で後処理を行う場合、単純な関数呼び出しで FreeLibrary や VirtualFree を実行して後処理を行うと、制御が戻ってきたときのリターン アドレスが解放済みになってしまうため、アクセス違反が起こります。これもいわゆる use-after-free でしょうか。

そこでまず、FreeLibrary は DLL 外部から実行する必要があります。これは上述の 1 つ目のサイト (ドリーム工房) で紹介されている方法のように、アセンブリで書いたシェルコードを用意してそこから LoadLibrary/FreeLibrary を呼べば解決です。

シェルコードがあるのであれば、メモリ解放についても、VirtualFree をそのまま call するのではなく、スタックを自分で積んでから jmp で VirtualFree を実行することで、VirtualFree 実行後のコード位置を指定して use-after-free 問題を解決できそうです。

アセンブリでシェルコードを書く場合でも、ドリーム工房の方法のように、インジェクター側で取得したアドレスをシェルコードに動的に埋め込むことは可能です、が、何とかしてシェルコード内でイメージベースのアドレスを取得したいところです。そんな方法はないものかと探したところ、けっこう簡単に見つかりました。それがこれ、今回の記事のタイトルにした Reflective DLL Injection。

GitHub – stephenfewer/ReflectiveDLLInjection
https://github.com/stephenfewer/ReflectiveDLLInjection/

ポイントは dll/src/ReflectiveLoader.c に実装された ReflectiveLoader という関数です。そういえば昔覚えたことをすっかり忘れていましたが、Windows では、セグメント レジスタ fs または gs が指すセグメントに PEB や TEB を保存しています。覚えておくとけっこう使えます。

Wikipedia にも情報があります。

Win32 Thread Information Block – Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Win32_Thread_Information_Block

このセグメントレジスタはけっこう身近なところでも使われています。最たる例は、GetLastError() や GetCurrentProcessId() で、x86/x64 それぞれのアセンブリを示すとこんな感じです。また、ちゃんと確かめていませんが、metasploit で遊んでいた時に meterpreter の先頭のコードでもセグメント レジスタを見ていた記憶があります。

0:000> uf kernelbase!GetLastError
KERNELBASE!GetLastError:
775cecd0 64a118000000    mov     eax,dword ptr fs:[00000018h]
775cecd6 8b4034          mov     eax,dword ptr [eax+34h]
775cecd9 c3              ret
0:000> uf kernelbase!GetCurrentProcessId
KERNELBASE!GetCurrentProcessId:
7767cf60 64a118000000    mov     eax,dword ptr fs:[00000018h]
7767cf66 8b4020          mov     eax,dword ptr [eax+20h]
7767cf69 c3              ret

0:000> uf kernelbase!GetLastError
KERNELBASE!GetLastError:
00007ffe`d4f21470 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ffe`d4f21479 8b4068          mov     eax,dword ptr [rax+68h]
00007ffe`d4f2147c c3              ret
0:000> uf kernelbase!GetCurrentProcessId
KERNELBASE!GetCurrentProcessId:
00007ffe`d4f98b60 65488b042530000000 mov   rax,qword ptr gs:[30h]
00007ffe`d4f98b69 8b4040          mov     eax,dword ptr [rax+40h]
00007ffe`d4f98b6c c3              ret

上記関数は、TEB にある情報を使うので、x86 では fs:18h、x64 では gs:30h のアドレスが TEB であると分かります。

Reflective DLL Injection は、PEB にあるロード済みモジュールのリストからイメージ ベースを取得しています。以下の MSDN に書かれているように、PEB::Ldr->InMemoryOrderModuleList が示すデータには、ロード済みモジュールの名前とベースアドレスの双方向リンクト リストが入っています。

PEB structure (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx

PEB_LDR_DATA structure (Windows)
https://msdn.microsoft.com/en-us/library/windows/desktop/aa813708(v=vs.85).aspx

というわけで、以下のロジックをアセンブリで書いて CreateRemoteThread で埋め込めば、まさに実現したい動作が可能です。

  1. セグメント レジスタから PEB を取得
  2. PEB からロード済モジュール リストを取得
  3. リストから Kernel32.dll のベースアドレスを取得
    (ユーザーモード プロセスに kernel32.dll は必ずロードされていると考えてよい)
  4. ベースアドレスから PE イメージの構造を解析し、エクスポート テーブルを取得
  5. エクスポート テーブルを検索して LoadLibrary の開始アドレスを取得

少々めんどくさそうですが、一旦 C で書いて、コンパイルされたアセンブリを整形すればそれほど難しくはないはずです。

この方法を使えば、LoadLibrary だけでなく、シェルコードの中で実行したい API のアドレスを全部取得することができます。また、本記事とは関係ありませんが、コード領域内をパターン検索すれば、エクスポートされていない関数のアドレスも探せるはずです。とにかく、これで後片付けの問題は解決しました。すなわち、シェルコードを以下のようなロジックにします。

  1. LoadLibrary で好きな DLL をロード (ファイル パスは予めバッファーに入れておく)
  2. GetProcAddress でエクスポート関数のアドレスをゲット (DllMain には何も書かなくてよい)
  3. 関数を単純に call で実行
  4. FreeLibrary で DLL をアンロード
  5. ExitThread のアドレスを push して VirtualFree に jmp

というわけで書いたコードがこれ↓

https://github.com/msmania/procjack/tree/1.0

メインのインジェクターは、pj.exe で、これに ターゲットの PID とインジェクトしたい DLL、そして実行したいエクスポート関数の序数を指定すると、ターゲットの中でコードが実行されます。実行の様子はこんな感じ。

Capture

シェルコードをデバッグしたい場合は、ターゲット側でスレッド作成時に例外を捕捉するようにしておくと、CreateRemoteThread が実行された時点でブレークしてくれて便利です。こんな感じ。

02

前述の通り、シェルコードは予め C で書いて expeb.exe としてコンパイルしてからアセンブリを整形しました。したがって、expeb のロジック自体は windows.h をインクルードしなくても動きます。Visual C++ だけでなく clang と gcc でもコンパイルして、もっとも効率の良いアセンブリを使おうと考えたのですが、結局 VC++ のアセンブリに落ち着きました。clang あたりがトリッキーなアセンブリを生成してくれるかと期待したのですが、どれも大差のないもので、最後は呼び出し規約の関係で VC++ に軍配が上がった感じです。長さは 1000 バイト前後です。手で頑張ればもう少し短くできそうな気はします。

試していませんが、Windows XP や 2000 でも動くはずです。少なくとも expeb.exe を Windows XP で動かして関数アドレスをとってくるところは問題ありませんでした。

ターゲットが AppContainer プロセスの時にも対応させるため、インジェクトする DLL のアクセス権をチェックして、"APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES" (SID=S-1-15-2-1) に対して読み取りと実行権限を割り当てています。実はこっちのコードを書く方が面倒だった・・・。Low プロセス用の権限を追加し忘れたので、Low だと動かないかも・・そのうち追加して更新します。

これで当初の目的は達成できた、と思いきや、実は一点だけ実現できていないことがあります。というのも、Wow64 のプロセスから Win64 のプロセスに対して CreateRemoteThread を実行すると、ERROR_ACCESS_DENIED で失敗してスレッドを作れないのです。VirtualAllocEx や WriteProcessMemory は問題ないのですが。逆の、Win64 から Wow64 への CreateRemoteThread は問題ありません。次のブログでいろいろ考察しているのですが、ちょっと時間がなくちゃんと読めていません。何かうまい方法があると思っているのですが。

DLL Injection and WoW64
http://www.corsix.org/content/dll-injection-and-wow64

最後にふと思い出したのが、知る人ぞ知るやねうらお氏の不真面目なほうのプロフィール。Reflective programming というより、やっていることはこっちに近いような。

やねうらおプロフィール
http://bm98.yaneu.com/bickle/profile.html

メインルーチンをオールアセンブラで組んだ、縦スクロールシューティングゲームを作成。創刊当時のマイコンBASICマガジンに投稿。BASICの部分は、16進ダンプをreadしてpokeしたのち、それを実行しているだけというBASICマガジンをナメ切った自慢の作品。当然のごとく、ボツにされる。

これでようやく小学校五年生の頃のやねうらお氏に追いついた!かもしれない。