ふつうの人はアプリをいったんアンインストールしてから再インストールしたほうが早い。
0. 概要
UWP あるいは Microsoft Store アプリと呼ぶべきなのか知らないが,winget.exe
などのアプリ本体は大抵の場合
C:\Program Files\WindowsApps\...
にある。しかし,その実行エイリアスは
C:\Users\****\AppData\Local\Microsoft\WindowsApps\...
に置かれていて,ファイルサイズが 0 バイトとなっている。また,通常のシンボリックリンクやジャンクションと異なり,リンク先が見えない謎の存在でもある。先日,その実行エイリアスをうっかり削除してしまい,専用のツールを自作してようやく復旧できたという話である。
1. はじめに
Python をインストールするとき,ユーザ別にインストールするか,全てのユーザで使えるようにするか選ぶことができることに気づかず,前者すなわち自分のアカウントにインストールしてしまった。いったんアンインストールし,今度は「Install for all users」を選んで再インストールすればよい。
ところが Python をアンインストールしたにも関わらず python.exe にパスが通っていることに気づいた。なお which
コマンドについては過去の記事を参照されたい。
C:\>which python
C:\Users\****\AppData\Local\Microsoft\WindowsApps\python.exe
で,該当フォルダに移動してみると,サイズが 0 バイトの実行ファイルの存在(複数)に気づいた。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>dir
ドライブ C のボリューム ラベルは Windows です
ボリューム シリアル番号は ****-**** です
C:\Users\****\AppData\Local\Microsoft\WindowsApps のディレクトリ
2024/08/16 00:18 <DIR> Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
2024/08/16 00:39 <DIR> Microsoft.GetHelp_8wekyb3d8bbwe
2024/08/16 00:23 <DIR> Microsoft.ZuneMusic_8wekyb3d8bbwe
2024/08/16 00:39 0 GetHelp.exe
2024/08/16 00:23 0 MediaPlayer.exe
2024/08/16 00:18 0 python.exe
2024/08/16 00:18 0 python3.exe
2024/08/16 00:18 0 WindowsPackageManagerServer.exe
2024/08/16 00:18 0 winget.exe
アンインストールに失敗したのかと思い,これも拙作の remove
コマンド(詳しくは過去の記事を参照されたい)でゴミ箱に送ろうとしたところ,何故か失敗してしまう。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>remove python*.exe
ファイル・フォルダの削除に失敗しました!!
なので,つい delete
コマンドで全部の実行ファイルを消してしまうという大失態を犯してしまった。とくに winget.exe
を消してしまったのが痛い・・・本来,こういうミスを犯さないようにするための remove
コマンドなんだけどね。
※remove
コマンドはファイルをゴミ箱に送るだけなので簡単に復活できるのだ。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>del *.exe
とうことで,うっかり削除してしまった 6 つの実行ファイルの復活が本記事のテーマである。
2. サブ PC による解析
ここから先は同じ Windows10 22H2 のサブ PC での調査結果になる。
不思議なことに Python をインストールしたことのないサブ PC にも同じサイズ 0 バイトの実行ファイルがあった。
実際に attrib
コマンドで見てみると,どうやらシンボリックリンクが貼られているようなのだが,ターゲットが見つからないと表示される。にも関わらず winget.exe
は使えるのだ。つまり,本体はどこかにあるはず。NTFS ならシンボリックリンクやハードリンクが貼れるので,サイズ 0 バイトの実行ファイルがあっても不思議ではない。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>attrib
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\GetHelp.exe のターゲットがありません
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\MediaPlayer.exe のターゲットがありません
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\python.exe のターゲットがありません
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\python3.exe のターゲットがありません
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\WindowsPackageManagerServer.exe のターゲットがありません
シンボリック リンク C:\Users\****\AppData\Local\Microsoft\WindowsApps\winget.exe のターゲットがありません
コマンドプロンプトだとアレだが,PowerShell だとリンクが貼られているのが一目で分かる。
PS C:\Users\****\AppData\Local\Microsoft\WindowsApps> ls
ディレクトリ: C:\Users\****\AppData\Local\Microsoft\WindowsApps
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2024/08/16 0:18 Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
d----- 2024/08/16 0:39 Microsoft.GetHelp_8wekyb3d8bbwe
d----- 2024/08/16 0:23 Microsoft.ZuneMusic_8wekyb3d8bbwe
-a---l 2024/08/16 0:39 0 GetHelp.exe
-a---l 2024/08/16 0:23 0 MediaPlayer.exe
-a---l 2024/08/16 0:18 0 python.exe
-a---l 2024/08/16 0:18 0 python3.exe
-a---l 2024/08/16 0:18 0 WindowsPackageManagerServer.exe
-a---l 2024/08/16 0:18 0 winget.exe
しかも ReparsePoint(再解析ポイント)という属性が付けられていると判明した。
PS C:\Users\****\AppData\Local\Microsoft\WindowsApps> get-item * | select name,attributes
Name Attributes
---- ----------
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe Directory
Microsoft.GetHelp_8wekyb3d8bbwe Directory
Microsoft.ZuneMusic_8wekyb3d8bbwe Directory
GetHelp.exe Archive, ReparsePoint
MediaPlayer.exe Archive, ReparsePoint
python.exe Archive, ReparsePoint
python3.exe Archive, ReparsePoint
WindowsPackageManagerServer.exe Archive, ReparsePoint
winget.exe Archive, ReparsePoint
なので fsutil
コマンドで ReparsePoint のデータを見てみる。参考文献[3]によれば,再解析タグ値 0x8000001b
は IO_REPARSE_TAG_APPEXECLINK
,すなわち Universal Windows Platform (UWP) アプリへのリンクであることを意味する。タグ値については Microsoft とサードパーティの二択らしいが,NTFS の仕様が公開されていない以上,通常は Microsoft 一択だろう。再解析データについて,先頭の 4 バイト 03 00 00 00
の意味は不明であるが,その後のバイト列はリンク先へのパスを示す Unicode 文字列のように思える。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>fsutil reparsepoint query winget.exe
再解析タグ値 : 0x8000001b
タグ値: Microsoft
再解析データの長さ: 0x190
再解析データ:
0000: 03 00 00 00 4d 00 69 00 63 00 72 00 6f 00 73 00 ....M.i.c.r.o.s.
0010: 6f 00 66 00 74 00 2e 00 44 00 65 00 73 00 6b 00 o.f.t...D.e.s.k.
0020: 74 00 6f 00 70 00 41 00 70 00 70 00 49 00 6e 00 t.o.p.A.p.p.I.n.
0030: 73 00 74 00 61 00 6c 00 6c 00 65 00 72 00 5f 00 s.t.a.l.l.e.r._.
0040: 38 00 77 00 65 00 6b 00 79 00 62 00 33 00 64 00 8.w.e.k.y.b.3.d.
0050: 38 00 62 00 62 00 77 00 65 00 00 00 4d 00 69 00 8.b.b.w.e...M.i.
0060: 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2e 00 c.r.o.s.o.f.t...
0070: 44 00 65 00 73 00 6b 00 74 00 6f 00 70 00 41 00 D.e.s.k.t.o.p.A.
0080: 70 00 70 00 49 00 6e 00 73 00 74 00 61 00 6c 00 p.p.I.n.s.t.a.l.
0090: 6c 00 65 00 72 00 5f 00 38 00 77 00 65 00 6b 00 l.e.r._.8.w.e.k.
00a0: 79 00 62 00 33 00 64 00 38 00 62 00 62 00 77 00 y.b.3.d.8.b.b.w.
00b0: 65 00 21 00 77 00 69 00 6e 00 67 00 65 00 74 00 e.!.w.i.n.g.e.t.
00c0: 00 00 43 00 3a 00 5c 00 50 00 72 00 6f 00 67 00 ..C.:.\.P.r.o.g.
00d0: 72 00 61 00 6d 00 20 00 46 00 69 00 6c 00 65 00 r.a.m. .F.i.l.e.
00e0: 73 00 5c 00 57 00 69 00 6e 00 64 00 6f 00 77 00 s.\.W.i.n.d.o.w.
00f0: 73 00 41 00 70 00 70 00 73 00 5c 00 4d 00 69 00 s.A.p.p.s.\.M.i.
0100: 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2e 00 c.r.o.s.o.f.t...
0110: 44 00 65 00 73 00 6b 00 74 00 6f 00 70 00 41 00 D.e.s.k.t.o.p.A.
0120: 70 00 70 00 49 00 6e 00 73 00 74 00 61 00 6c 00 p.p.I.n.s.t.a.l.
0130: 6c 00 65 00 72 00 5f 00 31 00 2e 00 32 00 33 00 l.e.r._.1...2.3.
0140: 2e 00 31 00 39 00 31 00 31 00 2e 00 30 00 5f 00 ..1.9.1.1...0._.
0150: 78 00 36 00 34 00 5f 00 5f 00 38 00 77 00 65 00 x.6.4._._.8.w.e.
0160: 6b 00 79 00 62 00 33 00 64 00 38 00 62 00 62 00 k.y.b.3.d.8.b.b.
0170: 77 00 65 00 5c 00 77 00 69 00 6e 00 67 00 65 00 w.e.\.w.i.n.g.e.
0180: 74 00 2e 00 65 00 78 00 65 00 00 00 30 00 00 00 t...e.x.e...0...
同フォルダの全ての実行ファイルの ReparsePoint データについて,手作業で編集して見易くしたものを以下に示す。
※ちなみに C:\Program Files\WindowsApps
は隠しフォルダ属性になっている。
GetHelp.exe
----
Microsoft.GetHelp_8wekyb3d8bbwe
Microsoft.GetHelp_8wekyb3d8bbwe!App
C:\Program Files\WindowsApps\Microsoft.GetHelp_10.2403.20861.0_x64__8wekyb3d8bbwe\GetHelp.exe
0
MediaPlayer.exe
----
Microsoft.ZuneMusic_8wekyb3d8bbwe
Microsoft.ZuneMusic_8wekyb3d8bbwe!Microsoft.ZuneMusic
C:\WINDOWS\system32\SystemUWPLauncher.exe
1
python.exe
----
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!PythonRedirector
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\AppInstallerPythonRedirector.exe
0
python3.exe
----
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!PythonRedirector
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\AppInstallerPythonRedirector.exe
0
WindowsPackageManagerServer.exe
----
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!WinGetComServer
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\WindowsPackageManagerServer.exe
0
winget.exe
----
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe
Microsoft.DesktopAppInstaller_8wekyb3d8bbwe!winget
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\winget.exe
0
で,実際にその隠しフォルダに行ってみると確かに実体が存在する。ちなみに Python.exe
も Python3.exe
も実体は同じで Python のインストーラへのリダイレクトのようだ。
C:\Program Files\WindowsApps>dir /s /b gethelp.exe
C:\Program Files\WindowsApps\Microsoft.GetHelp_10.2403.20861.0_x64__8wekyb3d8bbwe\GetHelp.exe
C:\Program Files\WindowsApps>dir /s /b AppInstallerPythonRedirector.exe
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\AppInstallerPythonRedirector.exe
C:\Program Files\WindowsApps>dir /s /b WindowsPackageManagerServer.exe
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\WindowsPackageManagerServer.exe
C:\Program Files\WindowsApps>dir /s /b winget.exe
C:\Program Files\WindowsApps\Microsoft.DesktopAppInstaller_1.23.1911.0_x64__8wekyb3d8bbwe\winget.exe
もちろん残りのファイル MediaPlayer.exe
の実体もある。
C:\Windows\System32>dir /s /b SystemUWPLauncher.exe
C:\Windows\System32\SystemUWPLauncher.exe
3. 再解析ポイントのデータがコピーできない
二台の PC のディレクトリ構造が同じであれば,サイズ 0 バイトの実行ファイルをコピーして持っていけばいい・・・と思ったのだが実際にはコピーできない!
C:\Users\****\AppData\Local\Microsoft\WindowsApps>copy winget.exe d:
ファイルにアクセスできません。
0 個のファイルをコピーしました。
管理者権限で robocopy
コマンドを使用すればファイルのコピー自体は可能だが,残念ながらサイズ 0 バイトのファイルが作られるだけで ReparsePoint のデータまではコピーできない。いやあ,なかなか手強いな・・・
4. 再解析ポイントデータを移植するツールを作る
残念ながら再解析ポイントデータの仕様は公開されていない。ごく一部,シンボリックリンクやジャンクションなどを作成するためのデータ構造は公開されているが,Microsoft Store アプリの実行エイリアスのデータ構造は公開されていないのだ。
ちなみに参考文献[9]によれば,Microsoft Store アプリの実行エイリアスのデータ構造は 32bit のバージョン番号(00000003h)のあと,Unicode のヌル終端文字列が 4 つ続き,それぞれ
- パッケージ ID
- エントリーポイント
- ターゲットパス
- アプリケーションタイプ
という定義のようだ。だが,肝心のパッケージ ID やらエントリーポイントが分からなければ設定できないので,ゼロからデータ構造を作成することは諦めた。
そういう訳で再解析ポイントデータを移植,すなわち再解析ポイントデータのエクスポートおよびインポートするツールを作ることにした。Win32 API を直接扱うので開発言語は C となる。
4.1 再解析ポイントデータをエクスポートする
エクスポートするほうはさほど難しくない。まずデータを保存するバッファ buffer
を用意する。サイズは 16kB である。
static DWORD length;
static BYTE buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
次に,念のためファイルに再解析ポイントが設定されているか確認する。
DWORD attr = GetFileAttributesW( filename );
if( attr == INVALID_FILE_ATTRIBUTES ) {
fwprintf( stderr, L"ファイル %s の属性取得に失敗しました!!\n", filename );
return( -1 );
}
if( ( attr & FILE_ATTRIBUTE_REPARSE_POINT ) == 0 ) {
fwprintf( stderr, L"ファイル %s に再解析ポイントは設定されていません!!\n", filename );
return( -1 );
}
そしてファイルをオープンして再解析ポイントデータを取得する。
HANDLE hfile = CreateFileW(
filename, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
int ret = ( 0 != DeviceIoControl( hfile, FSCTL_GET_REPARSE_POINT, NULL, 0,
buffer, sizeof(buffer), &length, NULL ) ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s のデバイス I/O コントロールに失敗しました!!\n", filename );
後は適当なファイル名を付けてバッファ buffer
のデータを保存すれば良い。
なお CreateFileW
の引数であるが,ファイルのみを対象とするのであれば表1の値で良いが,ディレクトリも含める場合は表2の値にする必要があった。
引数 | 値 |
---|---|
第1引数 | filename |
第2引数 | GENERIC_READ |
第3引数 | 0 |
第4引数 | NULL |
第5引数 | OPEN_EXISTING |
第6引数 | FILE_FLAG_OPEN_REPARSE_POINT |
第7引数 | NULL |
引数 | 値 |
---|---|
第1引数 | filename |
第2引数 | 0 |
第3引数 | FILE_SHARE_READ | FILE_SHARE_WRITE |
第4引数 | NULL |
第5引数 | OPEN_EXISTING |
第6引数 | FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS |
第7引数 | NULL |
ちなみに本記事は Microsoft Store アプリの実行エイリアスの復活が目的,すなわちあくまでファイルのみを復旧の対象としているが,僅かな変更でディレクトリの情報(ジャンクションなど)も得られることが分かったので,オマケで追加に過ぎない。
4.2 再解析ポイントデータをインポートする
一方,インポートする場合は多少手間がかかる。シンボリックリンクを作成可能な特権が必要なので,実行には管理者権限が必要である。加えて管理者権限でもシンボリックリンクを作成可能な特権はデフォルトでは無効化されているので,これを有効化する手続きが必要になる。
HANDLE htoken;
TOKEN_PRIVILEGES token;
if( 0 == LookupPrivilegeValueW(
NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &token.Privileges[0].Luid ) ) {
fwprintf( stderr, L"シンボリックリンクを作成可能な特権が存在しません!!\n" );
return( -1 );
}
token.PrivilegeCount = 1;
token.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if( 0 == OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &htoken ) ) {
fwprintf( stderr, L"アクセストークンの取得に失敗しました!!\n" );
return( -1 );
}
int ret = ( 0 != AdjustTokenPrivileges( htoken, FALSE, &token, 0, NULL, NULL )
&& GetLastError() == ERROR_SUCCESS ) ? 0 : -1;
CloseHandle( htoken );
if( ret != 0 ) {
fwprintf( stderr, L"特権の有効化に失敗しました!!\n" );
return( -1 );
}
こうしてシンボリックリンクを作成可能な特権を得られた後に,事前に読み込んでいた再解析ポイントデータを設定することができる。
HANDLE hfile = CreateFileW( filename, GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
ret = ( 0 != DeviceIoControl(
hfile, FSCTL_SET_REPARSE_POINT, buffer, length, NULL, 0, NULL, NULL ) ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s のデバイス I/O コントロールに失敗しました!!\n", filename );
4.3 実装コード
実装コードを以下に示す。ちなみに Visual Studio 2022 Community Edition でビルドした。
RPUtil.c のソースコードはコチラ
#define UNICODE
#include <windows.h>
#include <shlwapi.h>
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#pragma comment( lib, "advapi32" )
#pragma comment( lib, "shlwapi" )
//------------------------------------------------------------------------------
// マクロ定義
//------------------------------------------------------------------------------
#define TOWPRINT(c) ((L' '<=(c)&&(c)<=L'~')?(c):L'.')
//------------------------------------------------------------------------------
// 再解析ポイントデータバッファ
//------------------------------------------------------------------------------
typedef struct {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
WCHAR DataBuffer[1];
} REPARSE_DATA_BUFFER;
//------------------------------------------------------------------------------
// タグテーブル
//------------------------------------------------------------------------------
typedef struct {
int tag;
wchar_t *name;
} TAG_TABLE;
//------------------------------------------------------------------------------
// グローバル変数
//------------------------------------------------------------------------------
static DWORD length;
static BYTE buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
//------------------------------------------------------------------------------
// 再解析ポイントタグの文字列を返す
//------------------------------------------------------------------------------
static wchar_t *get_tag_string( int tag ) {
static TAG_TABLE table[] = {
{ IO_REPARSE_TAG_MOUNT_POINT, L"Mount Point" },
{ IO_REPARSE_TAG_HSM, L"HSM" },
{ IO_REPARSE_TAG_HSM2, L"HSM2" },
{ IO_REPARSE_TAG_SIS, L"SIS" },
{ IO_REPARSE_TAG_WIM, L"WIM" },
{ IO_REPARSE_TAG_CSV, L"CSV" },
{ IO_REPARSE_TAG_DFS, L"DFS" },
{ IO_REPARSE_TAG_SYMLINK, L"Symbolic Link" },
{ IO_REPARSE_TAG_DFSR, L"DFSR" },
{ IO_REPARSE_TAG_DEDUP, L"DEDUP" },
{ IO_REPARSE_TAG_NFS, L"NFS" },
{ IO_REPARSE_TAG_FILE_PLACEHOLDER, L"File PlaceHolder" },
{ IO_REPARSE_TAG_WOF, L"WOF" },
{ IO_REPARSE_TAG_WCI, L"WCI" },
{ IO_REPARSE_TAG_WCI_1, L"WCI_1" },
{ IO_REPARSE_TAG_GLOBAL_REPARSE, L"Global Reparse" },
{ IO_REPARSE_TAG_CLOUD, L"Cloud" },
{ IO_REPARSE_TAG_CLOUD_1, L"Cloud 1" },
{ IO_REPARSE_TAG_CLOUD_2, L"Cloud 2" },
{ IO_REPARSE_TAG_CLOUD_3, L"Cloud 3" },
{ IO_REPARSE_TAG_CLOUD_4, L"Cloud 4" },
{ IO_REPARSE_TAG_CLOUD_5, L"Cloud 5" },
{ IO_REPARSE_TAG_CLOUD_6, L"Cloud 6" },
{ IO_REPARSE_TAG_CLOUD_7, L"Cloud 7" },
{ IO_REPARSE_TAG_CLOUD_8, L"Cloud 8" },
{ IO_REPARSE_TAG_CLOUD_9, L"Cloud 9" },
{ IO_REPARSE_TAG_CLOUD_A, L"Cloud_A" },
{ IO_REPARSE_TAG_CLOUD_B, L"Cloud B" },
{ IO_REPARSE_TAG_CLOUD_C, L"Cloud C" },
{ IO_REPARSE_TAG_CLOUD_D, L"Cloud D" },
{ IO_REPARSE_TAG_CLOUD_E, L"Cloud E" },
{ IO_REPARSE_TAG_CLOUD_F, L"Cloud F" },
{ IO_REPARSE_TAG_CLOUD_MASK, L"Cloud Mask" },
{ IO_REPARSE_TAG_APPEXECLINK, L"AppExecLink" },
{ IO_REPARSE_TAG_PROJFS, L"Projected FS" },
{ IO_REPARSE_TAG_STORAGE_SYNC, L"Storage Sync" },
{ IO_REPARSE_TAG_WCI_TOMBSTONE, L"WCI TombStone" },
{ IO_REPARSE_TAG_UNHANDLED, L"Unhandled" },
{ IO_REPARSE_TAG_ONEDRIVE, L"One Drive" },
{ IO_REPARSE_TAG_PROJFS_TOMBSTONE, L"Projected FS TombStone" },
{ IO_REPARSE_TAG_AF_UNIX, L"AF UNIX" }
};
#define TABLE_NUM (sizeof(table)/sizeof(table[0]))
for( int i = 0; i < TABLE_NUM; i++ )
if( table[i].tag == tag ) return( table[i].name );
return( (wchar_t*)NULL );
}
//------------------------------------------------------------------------------
// ヘルプメッセージ
//------------------------------------------------------------------------------
static int usage( void ) {
fwprintf( stderr, L"再解析ポイント・ユーティリティ\n" );
fwprintf( stderr, L"\n" );
fwprintf( stderr, L"RPUtil(.EXE) [コマンド] ...\n" );
fwprintf( stderr, L"\n" );
fwprintf( stderr, L"<コマンド>\n" );
fwprintf( stderr, L" dump [ファイル名]\n" );
fwprintf( stderr, L"export [ファイル名] [データファイル名]\n" );
fwprintf( stderr, L"import [ファイル名] [データファイル名]\n" );
return( -1 );
}
//------------------------------------------------------------------------------
// データのエクスポート
//------------------------------------------------------------------------------
static int export_data( wchar_t *filename ) {
HANDLE hfile = CreateFileW(
filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
DWORD len;
int ret = ( 0 != WriteFile( hfile, buffer, length, &len, NULL ) && len == length ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s の書き込みに失敗しました!!\n", filename );
return( 0 );
}
//------------------------------------------------------------------------------
// データのインポート
//------------------------------------------------------------------------------
static int import_data( wchar_t *filename ) {
HANDLE hfile = CreateFileW(
filename, GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
int ret = ( 0 != ReadFile( hfile, buffer, sizeof(buffer), &length, NULL ) ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s の読み出しに失敗しました!!\n", filename );
return( ret );
}
//------------------------------------------------------------------------------
// 再解析ポイントの設定
//------------------------------------------------------------------------------
static int set_reparse_point( wchar_t *filename ) {
HANDLE htoken;
TOKEN_PRIVILEGES token;
if( 0 == LookupPrivilegeValueW( NULL, SE_CREATE_SYMBOLIC_LINK_NAME, &token.Privileges[0].Luid ) ) {
fwprintf( stderr, L"シンボリックリンクを作成可能な特権が存在しません!!\n" );
return( -1 );
}
token.PrivilegeCount = 1;
token.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if( 0 == OpenProcessToken( GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &htoken ) ) {
fwprintf( stderr, L"アクセストークンの取得に失敗しました!!\n" );
return( -1 );
}
int ret = ( 0 != AdjustTokenPrivileges( htoken, FALSE, &token, 0, NULL, NULL )
&& GetLastError() == ERROR_SUCCESS ) ? 0 : -1;
CloseHandle( htoken );
if( ret != 0 ) {
fwprintf( stderr, L"特権の有効化に失敗しました!!\n" );
return( -1 );
}
HANDLE hfile = CreateFileW( filename, GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
ret = ( 0 != DeviceIoControl(
hfile, FSCTL_SET_REPARSE_POINT, buffer, length, NULL, 0, NULL, NULL ) ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s のデバイス I/O コントロールに失敗しました!!\n", filename );
return( ret );
}
//------------------------------------------------------------------------------
// 再解析ポイントの取得
//------------------------------------------------------------------------------
static int get_reparse_point( wchar_t *filename ) {
DWORD attr = GetFileAttributesW( filename );
if( attr == INVALID_FILE_ATTRIBUTES ) {
fwprintf( stderr, L"ファイル %s の属性取得に失敗しました!!\n", filename );
return( -1 );
}
if( ( attr & FILE_ATTRIBUTE_REPARSE_POINT ) == 0 ) {
fwprintf( stderr, L"ファイル %s に再解析ポイントは設定されていません!!\n", filename );
return( -1 );
}
HANDLE hfile = CreateFileW(
filename, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL );
if( hfile == INVALID_HANDLE_VALUE ) {
fwprintf( stderr, L"ファイル %s のオープンに失敗しました!!\n", filename );
return( -1 );
}
int ret = ( 0 != DeviceIoControl(
hfile, FSCTL_GET_REPARSE_POINT, NULL, 0, buffer, sizeof(buffer), &length, NULL ) ) ? 0 : -1;
CloseHandle( hfile );
if( ret != 0 )
fwprintf( stderr, L"ファイル %s のデバイス I/O コントロールに失敗しました!!\n", filename );
return( ret );
}
//------------------------------------------------------------------------------
// コマンド DUMP の処理
//------------------------------------------------------------------------------
static int command_dump( int argc, wchar_t *argv[] ) {
if( argc < 3 ) {
fwprintf( stderr, L"コマンド DUMP の引数が不足しています!!\n" );
return( -1 );
}
if( 0 != get_reparse_point( argv[2] ) ) return( -1 );
REPARSE_DATA_BUFFER *p = (REPARSE_DATA_BUFFER*)buffer;
fwprintf( stdout, L"再解析タグ値:%08Xh\n", p->ReparseTag );
if( IsReparseTagMicrosoft( p->ReparseTag ) )
fwprintf( stdout, L"タグ値:Microsoft\n" );
if( IsReparseTagNameSurrogate( p->ReparseTag ) )
fwprintf( stdout, L"タグ値:Name Surrogate\n" );
if( IsReparseTagNameSurrogate( p->ReparseTag ) )
fwprintf( stdout, L"タグ値:Directory\n" );
wchar_t *s = get_tag_string( p->ReparseTag );
if( (wchar_t*)NULL != s )
fwprintf( stdout, L"タグ値:%s\n", s );
fwprintf( stdout, L"再解析データの長さ:%d bytes\n", p->ReparseDataLength );
fwprintf( stdout, L"再解析データ:\n" );
int count = p->ReparseDataLength / 2;
for( int i = 0, j = 0; i < count; i += 16 ) {
for( j = i; j < i + 16 && j < count; j++ )
fwprintf( stdout, L"%04X ", p->DataBuffer[j] );
for( ; j < i + 16; j++ )
fwprintf( stdout, L" " );
for( j = i; j < i + 16 && j < count; j++ )
fwprintf( stdout, L"%C", TOWPRINT( p->DataBuffer[j] ) );
fwprintf( stdout, L"\n" );
}
return( 0 );
}
//------------------------------------------------------------------------------
// コマンド EXPORT の処理
//------------------------------------------------------------------------------
static int command_export( int argc, wchar_t *argv[] ) {
if( argc < 4 ) {
fwprintf( stderr, L"コマンド EXPORT の引数が不足しています!!\n" );
return( -1 );
}
if( 0 != get_reparse_point( argv[2] ) ) return( -1 );
if( 0 != export_data( argv[3] ) ) return( -1 );
fwprintf( stderr, L"ファイル %s のデータをファイル %s にエクスポートしました。\n",
argv[2], argv[3] );
return( 0 );
}
//------------------------------------------------------------------------------
// コマンド IMPORT の処理
//------------------------------------------------------------------------------
static int command_import( int argc, wchar_t *argv[] ) {
if( argc < 4 ) {
fwprintf( stderr, L"コマンド IMPORT の引数が不足しています!!\n" );
return( -1 );
}
if( 0 != import_data( argv[3] ) ) return( -1 );
if( 0 != set_reparse_point( argv[2] ) ) return( -1 );
fwprintf( stderr, L"ファイル %s にファイル %s のデータをインポートしました。\n",
argv[2], argv[3] );
return( 0 );
}
//------------------------------------------------------------------------------
// メイン関数
//------------------------------------------------------------------------------
int wmain( int argc, wchar_t *argv[] ) {
setlocale( LC_ALL, "" );
if( argc < 2 ) return usage();
/*--*/ if( !StrCmpIW( argv[1], L"DUMP" ) ) { return command_dump ( argc, argv );
} else if( !StrCmpIW( argv[1], L"EXPORT" ) ) { return command_export( argc, argv );
} else if( !StrCmpIW( argv[1], L"IMPORT" ) ) { return command_import( argc, argv );
} else {
fwprintf( stderr, L"コマンド %s には対応していません!!\n", argv[1] );
return( -1 );
}
}
5. 実行例
5.1 サブ PC での実行
コチラはサブ PC での実行である。
引数なしで実行するとヘルプメッセージを表示する。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>rputil
再解析ポイント・ユーティリティ
RPUtil(.EXE) [コマンド] ...
<コマンド>
dump [ファイル名]
export [ファイル名] [データファイル名]
import [ファイル名] [データファイル名]
確認のため DUMP コマンドを作成した。見易いように Unicode(16bit)単位で表示するようにした。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>rputil dump winget.exe
再解析タグ値:8000001Bh
タグ値:Microsoft
タグ値:AppExecLink
再解析データの長さ:400 bytes
再解析データ:
0003 0000 004D 0069 0063 0072 006F 0073 006F 0066 0074 002E 0044 0065 0073 006B ..Microsoft.Desk
0074 006F 0070 0041 0070 0070 0049 006E 0073 0074 0061 006C 006C 0065 0072 005F topAppInstaller_
0038 0077 0065 006B 0079 0062 0033 0064 0038 0062 0062 0077 0065 0000 004D 0069 8wekyb3d8bbwe.Mi
0063 0072 006F 0073 006F 0066 0074 002E 0044 0065 0073 006B 0074 006F 0070 0041 crosoft.DesktopA
0070 0070 0049 006E 0073 0074 0061 006C 006C 0065 0072 005F 0038 0077 0065 006B ppInstaller_8wek
0079 0062 0033 0064 0038 0062 0062 0077 0065 0021 0077 0069 006E 0067 0065 0074 yb3d8bbwe!winget
0000 0043 003A 005C 0050 0072 006F 0067 0072 0061 006D 0020 0046 0069 006C 0065 .C:\Program File
0073 005C 0057 0069 006E 0064 006F 0077 0073 0041 0070 0070 0073 005C 004D 0069 s\WindowsApps\Mi
0063 0072 006F 0073 006F 0066 0074 002E 0044 0065 0073 006B 0074 006F 0070 0041 crosoft.DesktopA
0070 0070 0049 006E 0073 0074 0061 006C 006C 0065 0072 005F 0031 002E 0032 0033 ppInstaller_1.23
002E 0031 0037 0039 0031 002E 0030 005F 0078 0036 0034 005F 005F 0038 0077 0065 .1791.0_x64__8we
006B 0079 0062 0033 0064 0038 0062 0062 0077 0065 005C 0077 0069 006E 0067 0065 kyb3d8bbwe\winge
0074 002E 0065 0078 0065 0000 0030 0000 t.exe.0.
次に EXPORT コマンドで再解析ポイントデータを保存する。D ドライブは USB メモリである。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>rputil export winget.exe D:\winget.dat
ファイル winget.exe のデータをファイル D:\winget.dat にエクスポートしました。
念のため certutil -dump
コマンドで内容を確認する。エクスポートしたデータの先頭には,再解析タグ値や再解析データの長さなどの情報が付与されている。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>certutil -dump d:\winget.dat
0000 ...
0198
0000 1b 00 00 80 90 01 00 00 03 00 00 00 4d 00 69 00 ............M.i.
0010 63 00 72 00 6f 00 73 00 6f 00 66 00 74 00 2e 00 c.r.o.s.o.f.t...
0020 44 00 65 00 73 00 6b 00 74 00 6f 00 70 00 41 00 D.e.s.k.t.o.p.A.
0030 70 00 70 00 49 00 6e 00 73 00 74 00 61 00 6c 00 p.p.I.n.s.t.a.l.
0040 6c 00 65 00 72 00 5f 00 38 00 77 00 65 00 6b 00 l.e.r._.8.w.e.k.
0050 79 00 62 00 33 00 64 00 38 00 62 00 62 00 77 00 y.b.3.d.8.b.b.w.
0060 65 00 00 00 4d 00 69 00 63 00 72 00 6f 00 73 00 e...M.i.c.r.o.s.
0070 6f 00 66 00 74 00 2e 00 44 00 65 00 73 00 6b 00 o.f.t...D.e.s.k.
0080 74 00 6f 00 70 00 41 00 70 00 70 00 49 00 6e 00 t.o.p.A.p.p.I.n.
0090 73 00 74 00 61 00 6c 00 6c 00 65 00 72 00 5f 00 s.t.a.l.l.e.r._.
00a0 38 00 77 00 65 00 6b 00 79 00 62 00 33 00 64 00 8.w.e.k.y.b.3.d.
00b0 38 00 62 00 62 00 77 00 65 00 21 00 77 00 69 00 8.b.b.w.e.!.w.i.
00c0 6e 00 67 00 65 00 74 00 00 00 43 00 3a 00 5c 00 n.g.e.t...C.:.\.
00d0 50 00 72 00 6f 00 67 00 72 00 61 00 6d 00 20 00 P.r.o.g.r.a.m. .
00e0 46 00 69 00 6c 00 65 00 73 00 5c 00 57 00 69 00 F.i.l.e.s.\.W.i.
00f0 6e 00 64 00 6f 00 77 00 73 00 41 00 70 00 70 00 n.d.o.w.s.A.p.p.
0100 73 00 5c 00 4d 00 69 00 63 00 72 00 6f 00 73 00 s.\.M.i.c.r.o.s.
0110 6f 00 66 00 74 00 2e 00 44 00 65 00 73 00 6b 00 o.f.t...D.e.s.k.
0120 74 00 6f 00 70 00 41 00 70 00 70 00 49 00 6e 00 t.o.p.A.p.p.I.n.
0130 73 00 74 00 61 00 6c 00 6c 00 65 00 72 00 5f 00 s.t.a.l.l.e.r._.
0140 31 00 2e 00 32 00 33 00 2e 00 31 00 37 00 39 00 1...2.3...1.7.9.
0150 31 00 2e 00 30 00 5f 00 78 00 36 00 34 00 5f 00 1...0._.x.6.4._.
0160 5f 00 38 00 77 00 65 00 6b 00 79 00 62 00 33 00 _.8.w.e.k.y.b.3.
0170 64 00 38 00 62 00 62 00 77 00 65 00 5c 00 77 00 d.8.b.b.w.e.\.w.
0180 69 00 6e 00 67 00 65 00 74 00 2e 00 65 00 78 00 i.n.g.e.t...e.x.
0190 65 00 00 00 30 00 00 00 e...0...
CertUtil: -dump コマンドは正常に完了しました。
5.2 メイン PC での実行
ここから PC を移動してメイン PC での実行になる。
管理者権限でコマンドプロンプトを開き,whoami /priv
コマンドで特権を確認する。「シンボリックリンク作成」の特権が一覧の中にあることを確認する。ここでは「無効」状態でも構わない。ツールの中で「有効」状態に遷移させるからだ。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>whoami /priv
PRIVILEGES INFORMATION
----------------------
特権名 説明 状態
========================================= ====================================================== ====
SeIncreaseQuotaPrivilege プロセスのメモリクォータの増加 無効
SeSecurityPrivilege 監査とセキュリティログの管理 無効
SeTakeOwnershipPrivilege ファイルとその他のオブジェクトの所有権の取得 無効
SeLoadDriverPrivilege デバイスドライバーのロードとアンロード 無効
SeSystemProfilePrivilege システムパフォーマンスのプロファイル 無効
SeSystemtimePrivilege システム時刻の変更 無効
SeProfileSingleProcessPrivilege 単一プロセスのプロファイル 無効
SeIncreaseBasePriorityPrivilege スケジューリング優先順位の繰り上げ 無効
SeCreatePagefilePrivilege ページ ファイルの作成 無効
SeBackupPrivilege ファイルとディレクトリのバックアップ 無効
SeRestorePrivilege ファイルとディレクトリの復元 無効
SeShutdownPrivilege システムのシャットダウン 無効
SeDebugPrivilege プログラムのデバッグ 無効
SeSystemEnvironmentPrivilege ファームウェア環境値の修正 無効
SeChangeNotifyPrivilege 走査チェックのバイパス 有効
SeRemoteShutdownPrivilege リモートコンピューターからの強制シャットダウン 無効
SeUndockPrivilege ドッキング ステーションからコンピューターを削除 無効
SeManageVolumePrivilege ボリュームの保守タスクを実行 無効
SeImpersonatePrivilege 認証後にクライアントを偽装 有効
SeCreateGlobalPrivilege グローバルオブジェクトの作成 有効
SeIncreaseWorkingSetPrivilege プロセスワーキングセットの増加 無効
SeTimeZonePrivilege タイムゾーンの変更 無効
SeCreateSymbolicLinkPrivilege シンボリックリンクの作成 無効
SeDelegateSessionUserImpersonatePrivilege 同じセッションで別のユーザーの偽装トークンを取得します 無効
もしもここで「シンボリックリンク作成」の特権が一覧にない場合,グループポリシーエディタ gpedit.msc
を用いて設定する必要がある。詳しくは参考文献[8]を参照されたい。
そしてようやく D ドライブ(USBメモリ)から再解析ポイントデータをインポートする。実行前に winget.exe
が存在しているとエラーになってしまうので注意されたい。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>rputil import winget.exe d:\winget.dat
ファイル winget.exe にファイル d:\winget.dat のデータをインポートしました。
こうするとサイズ 0 バイトの winget.exe
が作られる。実体へのリンクは貼られており,無事,実行することはできた。
C:\Users\****\AppData\Local\Microsoft\WindowsApps>winget
v1.8.1791 の Windows パッケージ マネージャー
Copyright (c) Microsoft Corporation. All rights reserved.
WinGet コマンド ライン ユーティリティを使用すると、コマンド ラインからアプリケーションやその他のパッケージをインストールできます。
使用法: winget [<コマンド>] [<オプション>]
使用できるコマンドは次のとおりです:
install 指定されたパッケージをインストール
show パッケージに関する情報を表示します
source パッケージのソースの管理
search アプリの基本情報を見つけて表示
list インストール済みパッケージを表示する
upgrade 利用可能なアップグレードの表示と実行
uninstall 指定されたパッケージをアンインストール
hash インストーラー ファイルをハッシュするヘルパー
validate マニフェスト ファイルを検証
settings 設定を開くか、管理者設定を設定する
features 試験的な機能の状態を表示
export インストールされているパッケージのリストをエクスポート
import ファイル中のすべてのパッケージをインストール
pin パッケージ ピンの管理
configure システムを適切な状態に構成します
download 指定されたパッケージからインストーラをダウンロードする
repair 選択したパッケージを修復します
特定のコマンドの詳細については、そのコマンドにヘルプ引数を渡します。 [-?]
次のオプションを使用できます。
-v,--version ツールのバージョンを表示
--info ツールの一般情報を表示
-?,--help 選択したコマンドに関するヘルプを表示
--wait 終了する前に任意のキーを押すプロンプトをユーザーに表示します
--logs,--open-logs 既定のログの場所を開く
--verbose,--verbose-logs WinGet の詳細ログを有効にする
--nowarn,--ignore-warnings 警告出力を非表示にする
--disable-interactivity 対話型プロンプトを無効にします
--proxy この実行に使用するプロキシを設定します
--no-proxy この実行に対するプロキシの使用を無効にする
その他のヘルプについては、次を参照してください: https://aka.ms/winget-command-help
本ツールを用いて再解析ポイントの一種であるシンボリックリンクも作ることができるが,同じく同種のジャンクション(ディレクトリを対象にしたリンク)は作ることができない。
6. まとめ
Microsoft Store アプリの実行エイリアスは Windows の NTFS というファイルシステムにおける再解析ポイント ReparsePoint という技術で作られている。再解析ポイントはシンボリックリンクやジャンクションなどのリンクを貼る機能の基盤技術で,技術自体は Windows2000 の頃から存在していたようだが,それを扱う API であったり,コマンドラインツールが整備されてきたのは最近である。しかも Windows10 ですら完全には対応し切れていない(たとえば attrib.exe
コマンドなど)という状況である。Windows10 では再解析ポイントを作成する API は提供されているとはいえ,肝心の REPARSE_DATA_BUFFER
構造体の仕様が明らかにされていないため,構造体のデータをゼロから作ることはできず,同様の環境を持つ PC から構造体のデータを移植するという手段を取らざるを得なかったのだ。
以下は反省点である。
- グループポリシーエディタ
gpedit.msc
を弄って「シンボリックリンクの作成」権限を与えることができるのなら,robocopy
コマンドで(もしかしたらcopy
コマンドでも)普通にコピーできたかもしれない。 - 完全復旧ではないが,リンク先が分かっているのであれば普通に
mklink
コマンドで通常のシンボリックリンクを貼っても良かったような気がする。 - 今回はたまたま環境の近い PC を二台持っていたので助かったが,一台しか持っていなかったら冒頭で述べたようにアプリを再インストールするしかなかっただろう。
参考文献[10]によると,PowerShell にて Microsoft Store アプリの実行エイリアス(AppExecLink リンク)のターゲットを表示しないのは仕様のようだ。ターゲットパス C:\Program Files\WindowsApps
が隠し属性なのも含めて,マイクロソフトは積極的に公開したくないのかもしれない。