インストーラを ドラッグ&ドロップ で投げ込むと、そのインストーラがシステムへ 行う変更(レジストリ書き込み・インストールフォルダ以外へのファイル書き込み)を 横取りして 1 つのフォルダの中だけ に閉じ込め、フォルダをコピーするだけで 別 PC でも動く ポータブル版 を自作するツールです。
- レジストリ書き込み → フォルダ内のテキストファイル
dni.regに保存 - フォルダ外へのファイル書き込み → フォルダ内の
files\以下に保存 - 実行に必要な「フックツール」もフォルダ内に配置
実行時(ポータブル版の起動時)は、dni.reg を一時的な退避先レジストリに復元 →
アプリ起動 → 終了時に変更を dni.reg に書き戻して退避先を削除します。
したがって 実レジストリは最終的に汚れません。ファイルアクセスも同様に
files\ 以下へリダイレクトされるため、C:\ProgramData や C:\Program Files を
汚しません。
商用の ThinApp / Cameyo / Enigma Virtual Box などと同系統の「アプリ仮想化 (ポータブル化)」ツールです。自分で用意したインストーラを、自分の PC 用に ポータブル化する用途を想定しています。
┌──────────────────────┐ 起動(suspended)+DLL注入
│ DoNotInstaller.exe │ ───────────────────────────────▶ installer.exe(フック済み)
│ (ホスト本体) │ │ │
└──────────────────────┘ │ └─ 子プロセス(msiexec等)にも自動注入
▲ │
│ ジョブオブジェクトでプロセス木の終了を待機 ▼
│ ┌──────────────────────┐
│ │ DNIHook64/32.dll │
│ │ Reg* → シャドウキー │
│ │ File* → files\ へ │
│ └──────────────────────┘
終了後:シャドウキーを dni.reg に書き出し → シャドウキー削除 → EXE を検出
多くのゲーム/アプリは、レジストリに記録した絶対インストールパス
(例: InstallDir="C:\Program Files (x86)\Will\…")を読み、そこから
データファイル(OP.DAT 等)を開きます。ポータブル実行時にこの値を素直に
読むと実フォルダを見に行ってしまい、しかも FindFirstFile のようにフック
していないファイルAPI経由だとリダイレクトも効かず失敗します。
そこで実行時(run モード)に、シャドウレジストリ内の絶対パス値のうち、
対応するファイルがパッケージ内(files\)に実在するものを、ポータブル上の
実際の場所に書き換え(吸収し)ます。これにより、アプリは最初からローカルの
実パスを読むので、あらゆるファイルAPIで確実にパッケージ内を参照します。
終了時には逆変換して dni.reg は正規の実パスのまま保ちます(別PCへ移動しても
その場所に合わせて毎回吸収し直すため)。
レジストリもファイルも、キャプチャ中にシャドウへ捕捉した項目は、以降の 読み取りでもシャドウを優先します。インストーラは「キーに値を書く → 開き直して その値を読んで検証/続行」という動作を多用するため、ここで実レジストリを読んで しまうと自分の書いた値を見失い、同じ処理を延々リトライして最終的にエラー終了 します。シャドウ優先の読み取りでこの不整合を防ぎます(捕捉していないシステム キー/ファイルは実体を読みます)。
DirectShow(ActiveMovie)で動画(例: オープニングの OP.DAT)を再生するアプリは、
HKCR\Media Type や HKCR\CLSID を読んで対応フィルタ/コーデックを探します。
これらをシャドウへ逸らすと実体のコーデック登録が見えず「ファイルが開けません」に
なります。そこで COM/コーデック/DirectX 系のシステムキー
(CLSID / Media Type / DirectShow / Direct3D / DirectInput ほか)は、
明示的な書き込み以外(読み取り・MAXIMUM_ALLOWED)では実レジストリへ素通し
します(実ハンドルを返すのでサブキー列挙も実体を参照)。インストーラが明示的に
行う COM 登録は従来どおりシャドウへ捕捉します。
RegOpenKeyEx は存在しないキーに対しては失敗するのが正しい挙動で、キーを
作ってはいけません(キーを作るのは RegCreateKeyEx)。MAXIMUM_ALLOWED 等で
開いた際にシャドウキーを新規作成してしまうと、本来未インストールのソフトが
「インストール済み」に見えてしまいます(例: AutoRun が
...\Uninstall\<製品> を開いて「インストール/アンインストール」表示を切り替える
場合)。本ツールは、書き込み目的の open でも「シャドウに既にある/実キーが
存在する」場合のみシャドウを返し、どちらも無ければ実APIと同じく
FILE_NOT_FOUND を返します(実際の作成は RegCreateKeyEx のフックが担当)。
フック中の RegCreateKeyEx / RegOpenKeyEx(A/W)を捕捉し、
HKLM\Software\... などへの 書き込み を
HKCU\Software\DoNotInstaller\Shadow\<パッケージID>\HKLM\Software\...
へ透過的に付け替えます。返すハンドルはシャドウ側の実ハンドルなので、以降の
RegSetValueEx / RegQueryValueEx / RegEnumKey などは無改変で動作します。
書き込みのために既存キーを開いた場合は、そのキーの値をシャドウへ複製してから
渡すので(key-level copy-on-write)、「開いて読んでから書く」コードも壊れません。
取り込み終了後にホストがこのシャドウ部分木を dni.reg(regedit 互換・UTF-16)へ
書き出し、シャドウキー自体は削除します。
CreateFile(A/W)や CopyFile*・MoveFile* に加え、古い・独自系インストーラが
多用する次の API も捕捉してリダイレクトします。
SHFileOperation(A/W): シェル経由の一括ファイルコピー/移動。古いゲーム インストーラ等が本体ファイルの大量コピーに使うため、ここを捕捉しないと 「指定フォルダは空のまま、実フォルダへコピーされる」状態になります。OpenFile: レガシーなファイル作成 API(OF_CREATE等)。 ※OpenFileは仕様上パス長が約 128 文字までのため、ポータブル用フォルダの パスが長いと失敗することがあります。取り込み時は短いパスの出力先 (例:C:\dni\<名前>)を選ぶと安全です。- レジストリも
RegCreateKey/RegOpenKey(Ex なしのレガシー 2/3 引数形)を追加で 捕捉します。
フック中の CreateFile / CreateDirectory / DeleteFile / MoveFile /
CopyFile / GetFileAttributes 等(A/W)を捕捉し、パッケージフォルダの外への
書き込みを files\ 以下へリダイレクトします。
| 本来の書き込み先 | 実際の保存先 |
|---|---|
C:\ProgramData\ABC\x.ini |
<root>\files\ProgramData\ABC\x.ini |
C:\Program Files\ABC\app.exe |
<root>\files\Program Files\ABC\app.exe |
C:\Users\me\AppData\Roaming\ABC\.. |
<root>\files\Users\me\AppData\Roaming\ABC\.. |
D:\foo\bar.dat |
<root>\files\_drives\D\foo\bar.dat |
仕様書の例では
C:\installpath\ProgramData\ABC\でしたが、本ツールはdni.reg/dni.ini/ DLL 群と混ざらないよう仮想ファイルをfiles\の下に まとめています(1 階層増えるだけで意味は同じです)。
setup.exe が別の EXE を呼ぶ、msiexec.exe /i xxx.msi を起動する、といった
多段構成に追従するため、次のプロセス起動 API を捕捉し、ビットネスを判定して
対応する DLL を自動注入します(64bit ↔ 32bit のクロスビットは、もう一方の
ホスト EXE を注入ヘルパとして使用)。
CreateProcessW/CreateProcessA/CreateProcessAsUserWShellExecuteExW(インストーラが「次の段を管理者で起動」する際によく使う)
自己展開型インストーラ(最近の InstallShield 2010〜/v16 など、setup.exe が
%TEMP% に自分を展開してからそこで動くタイプ)に対応するため、%TEMP% /
%TMP% / Windows\Temp / LocalAppData\Temp への書き込みはリダイレクトせず、
実ディスクのままにしています。ここを files\ に逸らすと、展開した実行ファイルが
本物の一時フォルダに存在せず「見つからない」でクラッシュするためです。一時
フォルダはスクラッチ領域であり、アンインストールが必要な痕跡ではありません
(本来の書き込み先=Program Files・レジストリ・AppData 等は従来どおり捕捉します)。
「プログラムと機能」への登録(...\Microsoft\Windows\CurrentVersion\Uninstall\)、
自動起動(...\CurrentVersion\Run)、スタートメニュー/デスクトップの
ショートカット(.lnk)は、いずれも他の書き込みと同様に シャドウ側
(dni.reg / files\)へリダイレクト されます。したがって、正しく注入
できている限り実機には登録・作成されず、アンインストール作業は不要です。
また、.lnk をレガシー API(OpenFile+_lwrite 等)や長いパスで書き込む
インストーラ対策として、取り込み完了時にスタートメニュー/デスクトップ等の
実フォルダを走査し、取り込み中に作られたショートカットをパッケージの files\
へ回収して実機側を削除します(取り込み開始時刻以降の *.lnk のみが対象で、
無関係な項目には触れません)。
さらに取り込み完了時に、ホストが 実機のレジストリを読み取り専用でチェック
し、取り込んだ「プログラムと機能」登録・自動起動エントリと同一のものが実機に
残っていないかを照合します(フックできないプロセスが書き込んでしまった場合の
検知)。残っていた場合は、完了メッセージと dni.log に該当箇所を表示します
(誤削除を避けるため自動削除はしません。表示された場所を手動で削除してください)。
64bit Windows には 16bit 実行環境(NTVDM)が無いため、Windows 3.x 時代の
16bit インストーラ(InstallShield 3〜5 世代の SETUP.EXE など)は起動できません。
そこで投入されたファイルの EXE ヘッダ(NE/PE)を判定し、16bit だった場合は
次の2段構えで取り込みます。
- 方式1(32bit エンジンで起動):同じフォルダに InstallShield の 32bit
起動ファイル(
Setup32.exe、無ければ PE32 のsetup*/install*)があれば、 それをフック下で起動して通常どおり取り込みます。32bit 起動ファイルが無くても、 InstallShield 5 系なら圧縮された 32bit エンジンINSTALL.EXE(_INST32I.EX_内)を展開して、16bitSETUP.EXEの代わりに起動します(同梱の IS32Setup と 同じ仕組み。tools/is32setup/参照)。 - 方式2(CAB 展開 + ルースファイル復元):方式1 が無い/何も取り込めなかった
場合、フォルダ内の InstallShield CAB(
data1.cab/_sys1.cab/_user1.cab/*1.cab)を同梱の unshield でfiles\_extracted\へ展開します。 さらに、CAB が索引のみで本体データを持たない配布形態(本体が配布フォルダ直下に 難読化された状態で置かれているタイプ)に対応するため、CAB の索引(ファイル名・ サイズ)を手がかりに、配布フォルダのルースファイルから実体を復元します (InstallShield 難読化は自動で解除)。これにより、CAB 展開で 0 バイトに なってしまうファイルも実体を取り出せます。この方式ではレジストリ設定は 取り込めません(ファイルのみ)。
方式1→失敗時に方式2へ自動で切り替わります。取り込み後、起動したい EXE を
dni.ini の target に設定してください。
成功率をさらに上げるため、ウインドウに 「16bit インストーラ対策」 の チェックボックス(既定でオン)を用意しています。オンのまま取り込むと、 DO-NOT-INSTALLER が自動で次を行います。
- インストーラのフォルダ一式を書き込み可能な作業用フォルダ(
%TEMP%配下) へ丸ごとコピーします(元のメディア/フォルダは一切変更しません。CD-ROM など 読み取り専用メディアでも動くようにするためのコピーです)。 - コピー先を再帰的に走査し、16bit(NE 形式)の
setup*/install*/inst.exeを、同梱の 32bit 版IS32Setup.exeと同名で差し替えます (元の 16bit 版は<名前>.16.exeとして隣に退避)。これにより、AutoRun や メニューのボタン、ユーザーのダブルクリックなどどの経路からsetup.exeが 呼ばれても 32bit エンジンが動くようになります。 - 差し替え後のコピーをフック下で取り込み、終了時に作業用フォルダを自動削除します。
ユーザーが手作業で setup.exe を差し替える必要はありません(DO-NOT-INSTALLER が
自動で行います)。差し替え対象が無い場合でも、作業用フォルダへのコピーだけは行う
ため、読み取り専用メディアや書き込み保護されたフォルダからの取り込みが安定します。
不要な場合はチェックを外せば、従来どおり元の場所のインストーラをそのまま取り込みます。
同梱の
IS32Setup.exeはDoNotInstaller.exeと同じフォルダに置いてください (配布 zip ではすでに同梱済み)。見つからない場合は差し替えをスキップし、 作業用フォルダへのコピーのみ行います。 コマンドラインからは--capture <installer> <出力先>が既定でこの対策オン、--capture <installer> <出力先> --no-fix16でオフになります。
InstallShield 5 系の 32bit エンジン INSTALL.EXE は、実体は 32bit PE なのに
PE ヘッダの Characteristics に IMAGE_FILE_DLL(0x2000) ビットが立った
「実行可能DLL」形式(例: 0x210E)になっていることがあります。現代の Windows は
DLL フラグ付きイメージの CreateProcess を拒否し、エラー 193
(ERROR_BAD_EXE_FORMAT) を返します。本ツールはエンジン展開時に、起動対象の
*.EXE に限って PE の IMAGE_FILE_DLL ビットを自動でクリアしてから起動するように
しました(本物のヘルパ DLL はそのまま)。IS32Setup 経由・DNI 本体の方式1経由の
どちらでも適用されます。「ゆきのかなた」等でこのエラーが出ていたケースが対象です。
注意:この年代のソフトは Windows 9x 専用の VxD ドライバ(
*.DRV/*.VXD、LE形式) に依存していることがあります。VxD は NT 系 Windows では読み込め ないため、ポータブル化できても本来の機能が動かない場合があります。その場合は Windows 98 の仮想マシン(86Box / PCem / VirtualBox 等)が現実的です。 また、CAB が複数ボリューム構成で一部(data2.cab等)が欠落している配布物は、 方式2でも完全には展開できません。
32bit の InstallShield スタブが、Microsoft の 16bit 補助コンポーネント
(C:\Program Files (x86)\Microsoft\16-Bit Install Shield Support\setup.exe)を
起動しようとして、x64 環境には存在しないため失敗するケースです。16bit 実行が
必要なので実行での完走は不可能ですが、この手のインストーラは自己展開の
過程で CAB を一時フォルダ(%TEMP%)に吐き出して います。DO-NOT-INSTALLER は
一時フォルダ書き込みを files\ へリダイレクトしているので、取り込み後に
files\ 以下を再帰的に探して CAB を見つけ、自動で展開します(方式2 を
「失敗時のみ」ではなく常に補助的に実行)。これにより、実行が途中でコケても
中身のファイルは取り出せます。ログ(dni.log)には 16bit 補助の起動試行が
記録されます。
多くのインストーラは、実際のレジストリ/ファイル書き込みをインストーラ本体 ではなく、別プロセスのエンジンに行わせます。これらはインストーラの子プロセス ではなく、SCM や DCOM が起動するため、プロセス起動フックでは捕捉できません。
- MSI:
msiexec /iのクライアントではなく、SYSTEM で動く **Windows Installer サービス(別のmsiexec.exe)**が書き込みます。 - InstallShield:UI の
setup.exeではなく、DCOM 起動のikernel.exe(IS5/6 エンジン) や **IDriver.exe(IS の MSI エンジン)**が ファイルをコピーします。これを取りこぼすと、C:\Program Filesなどに 本物のままコピーされてしまいます(フォルダだけポータブル側に作られる、 という分裂が起きるのはこのためです)。
そこで取り込み中は、ホストがこれらのエンジンプロセス(msiexec.exe /
ikernel.exe / IDriver.exe / setup.exe 等)を監視し、出現した時点で
自動注入します。注入された DLL は、環境変数を継承しない相手でも設定を
受け取れるよう、ホストが公開する共有メモリ(Global\DoNotInstaller.Config.v1)
から構成を読みます。あわせて CopyFileW/A・MoveFileExW/A などコピー系 API も
フック対象に含めています。
このエンジン注入には管理者権限が必須です(SYSTEM / 別セッションの プロセスを開くため)。MSI・InstallShield 系は必ず 「管理者として実行」 した
DoNotInstaller.exeから取り込んでください。非管理者では書き込みを 取りこぼし、本物のフォルダに入ってしまうことがあります。 また、取り込み中に別件のmsiexec.exe等が動いていると巻き込むため、 インストール中は他のインストール操作を避けてください(best-effort な方式です)。
C++17。外部依存は同梱の MinHook(BSD-2-Clause, third_party/minhook)のみです。
x64 と Win32 の両方をビルドしてください(子プロセス追従に両方必要)。
DoNotInstaller.slnをダブルクリックして開く。- 「ソリューションの再ターゲット」ダイアログが出たら OK(プラットフォーム
ツールセットと Windows SDK が、インストール済みの最新版に揃います。本
.slnは v143 を既定にしていますが、VS2026 側の v14x へ自動で引き上げられます)。 - 構成を Release / x64 にして「ソリューションのビルド」。続けて Release / Win32 でもビルド。
- 4 ファイル(
DNIHook64.dllDNIHook32.dllDoNotInstaller.exeDoNotInstaller32.exe)がbin\に揃います。そのまま使えます。
ソリューションには 2 プロジェクト(
DNIHook=フックDLL、DoNotInstaller=ホスト) が含まれ、MinHook と共通コードは各プロジェクトに直接取り込んでいるので、 プロジェクト参照や別途ライブラリのビルドは不要です。Open Folder(CMake)で開く場合は同梱のCMakePresets.json(x64 / x86)が 使えます。
:: 64bit
cmake -S . -B build\x64 -A x64
cmake --build build\x64 --config Release
:: 32bit
cmake -S . -B build\x86 -A Win32
cmake --build build\x86 --config Release生成物(ソリューション/CMake どちらでも同じ):
| 構成 | 生成されるファイル |
|---|---|
| x64 | DNIHook64.dll, DoNotInstaller.exe |
| Win32 | DNIHook32.dll, DoNotInstaller32.exe |
この 4 ファイルを同じフォルダ に集めて使ってください(ソリューションの場合は
bin\ に自動で揃います)。
./build_mingw.sh # build-mingw/ に 4 ファイルを生成本リポジトリの Linux 側では、この MinGW クロスビルドでコンパイル・リンクの 確認まで行っています。実機 Windows での動作確認は未実施 です。
- ビルドした 4 ファイルが揃ったフォルダの
DoNotInstaller.exeを起動。- 管理者権限を要求するインストーラの場合は 「管理者として実行」 で起動。
- インストーラ(
setup.exe/*.msiを実行する.exe等)を指定します。- ウィンドウに ドラッグ&ドロップ、または
- 「ファイルを選択...」ボタン から選択。
- ※「管理者として実行」した場合、Windows の仕様(UIPI)で非管理者の エクスプローラからの ドラッグ&ドロップは効きません。その場合は 「ファイルを選択...」ボタンを使ってください。
- 出力先フォルダを選択(既定は
<インストーラ名>_portable)。 - インストーラがいつも通り起動するので、普段どおりインストール します
(インストール先はデフォルトのままで構いません。
C:\Program Files\...への 書き込みも自動的にfiles\へ取り込まれます)。 - インストーラ(と全子プロセス)が終了すると取り込み完了。フォルダに以下が 出来ます。
<root>\
├─ DoNotInstaller.exe ← これをダブルクリックするとアプリが起動(ランチャー)
├─ DoNotInstaller32.exe
├─ DNIHook64.dll / DNIHook32.dll
├─ dni.reg ← 取り込んだレジストリ(テキスト)
├─ dni.ini ← パッケージ設定(起動するEXE等)
├─ dni.log ← ログ(log=1 のとき)
└─ files\ ← 取り込んだファイル一式(仮想ファイルシステム)
<root>\DoNotInstaller.exe を ダブルクリック。dni.ini が隣にあると
ランチャーとして動作し、dni.reg を退避先へ復元してからアプリを起動します。
終了時に変更を dni.reg へ書き戻し、退避先レジストリを削除します。
別 PC へは フォルダごとコピー するだけです。
取り込み後、メイン EXE は自動推定して dni.ini の [launch] target に設定され
ますが、外れることもあります。その場合は dni.ini をテキストエディタで開き、
[candidates] の一覧から正しいものを target= に設定してください(<root>
からの相対パス)。
[launch]
target=files\Program Files\ABC\app.exe
workdir=
args=
log=0- 実機 Windows での動作確認は未実施です。コンパイル・リンクの確認のみ。 まず小さな単純アプリで検証してください。
- プロテクト/アクティベーション/DRM/アンチチートのあるソフトは対象外です。 DLL 注入・API フックを検知してブロック・誤作動する可能性が高いです。
- API フックは
advapi32/kernel32の公開 API レベルです。ntdllのNt*/Zw*を直接呼ぶ特殊なインストーラ・ランタイムは捕捉できません。 - ファイル列挙(
FindFirstFile)の実/仮想マージは未実装です。アプリが ディレクトリ列挙で自分のファイルを探す場合、実体がfiles\にあるので通常は 問題ありませんが、実システム側と混在して列挙する挙動は再現しません。 - COM 登録・サービス・ドライバ・スケジュールタスク・環境変数(PATH)等の システム統合はレジストリ/ファイルの範囲でしか再現しません。これらに依存 するアプリはポータブル化できないことがあります。
- .NET / MSIX / ストアアプリや、ランタイム(VC++ 再頒布可能パッケージ、 .NET ランタイム等)の別途インストールを前提とするアプリは、そのランタイムが コピー先 PC に無いと動きません。
- 明示的な環境ブロックを渡す子プロセスのうち ANSI 環境ブロックのものには
DNI_*設定を注入できず、フック対象外になります(Unicode 環境ブロックは対応)。 - DllMain 内で MinHook を初期化しているため、極めてまれにローダーロック絡みの 不具合があり得ます(注入直後はほぼ単一スレッドのため実用上は問題になりにくい)。
\\?\ロングパス・UNC パス(\\server\share)はリダイレクトしません。
- 取り込んでも空に近い → インストーラが管理者権限を要求し起動に失敗している 可能性。「管理者として実行」で再実行してください。
- 起動するが設定が保存されない →
dni.iniのtargetが実体と違う/files\に書けていない可能性。dni.log(log=1)でリダイレクト先を確認してください。 - 何が起きているか見たい → 取り込み時は既定で
dni.logに詳細が残ります。 ランチャーでログを取るにはdni.iniのlog=1を設定。
- 本体コード: 自由に利用・改変してください(無保証)。
- 同梱の MinHook: BSD-2-Clause(
third_party/minhook/LICENSE.txt)。 - 同梱の unshield: MIT(
third_party/unshield/LICENSE)。 - 同梱の zlib: zlib License(
third_party/zlib/LICENSE)。 - 同梱の blast(PKWare DCL 解凍, IS32Setup 用): zlib License(
third_party/blast/blast.h)。