気まま研究所ブログ

ITとバイク、思ったことをてきとーに書きます。

レジストリハイブを読み込み値を操作する

f:id:AonaSuzutsuki:20181231234056p:plain

Windows環境下のプログラムではしばしばレジストリを扱うことがあり、Win32APIでアクセスする手法が提供されています。
C#では汎用化されたクラスで提供されていて、レジストリへのアクセスは比較的容易です。
しかしながら、レジストリにはユーザが個別で外部ファイルからロードすることのでいるレジストリハイブが存在するものの、それに関しては標準ライブラリではカバーされていません。
今回はそんな外部レジストリハイブをC#でロードする内容です。
Win32APIの細かい部分わかってないので間違いがあれば指摘してもらえると助かります。
一日で検証から全部やったので途中から雑記化してます・・・。

*追記
最近書き直しなどしてましたが、regedit覗きながら実行するとアンロードできなかったりするので扱いがちょっと厄介かもしれません。
結構古い記事なので取り扱いにはご注意ください。

レジストリハイブ

レジストリは編集する場合、RegEdit.exeを使用すると思います。
このソフトでは一つの画面に収まっていますが、一部のレジストリキー(例えばHKEY_LOCAL_MACHINE\SOFTWAREなど)は個別に管理されています。
RegEditではそういった個別ファイルをそれぞれロードすること(ロード自体はOSレベルの処理?)であたかも一つのレジストリであるかのように振る舞っているようです。

そんなレジストリハイブですが、RegEditでロードすることができるのですが、C#では標準ライブラリではできないんですよね。
RegEditで一度ロードしてやれば標準ライブラリでもアクセスはできますが、システムが深く絡むのであまりユーザにさせる操作ではありません。
また、ロードしたとしても読み込み以外の操作は権限不足でエラーがでます。(管理者権限でもNG)

そういったことからレジストリハイブをC#上で扱うにはWin32APIを呼び出す処理を自前で用意する他ありません。

実装

レジストリハイブのロード

アクセストークンの取得

まず権限を有効化するためにOpenProcessToken関数を呼び出します。

[DllImport("kernel32.dll")]
private static extern IntPtr GetCurrentProcess();
[DllImport("advapi32.dll")]
private static extern int OpenProcessToken(IntPtr processHandle, int desiredAccess, ref IntPtr tokenhandle);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);

private const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;

private static bool EnabledPrivilege()
{
    // アクセストークンのポインタ (IntPtr)
    IntPtr tokenHandle = IntPtr.Zero;
    // アクセストークンの取得
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, ref tokenHandle) == 0)
        return false; // 失敗

    // ToDo

    // 後始末
    CloseHandle(tokenHandle);
    return true;
}

OpenProcessTokenはプロセスのハンドルと希望するアクセス権、最後にアクセストークンのポインタ(IntPtr)を渡します。
アクセス権は後のAdjustTokenPrivileges関数のドキュメントで説明されていますが、TOKEN_ADJUST_PRIVILEGES(0x00000020)の値が必要になります。
PreviousStateは使用しないのでTOKEN_QUERYは指定していません。
後の権限をもとに戻す必要がある場合はPreviousStateにポインタを渡し、TOKEN_QUERYも指定する必要があります。

権限に対応したLUIDの取得

次にLookupPrivilegeValueA関数でローカル一意識別子(LUID)を取得します。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int LookupPrivilegeValueA(string lpsystemname, string lpname, [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid);

[StructLayout(LayoutKind.Sequential)]
private struct LUID
{
    public int LowPart;
    public int HighPart;
}

private static bool AdjustTokenPrivilege(IntPtr tokenHandle, string lpname)
{
    // LUID構造体のインスタンスを生成
    var serLuid = new LUID();
    // 特権に対応したローカル一意識別子を取得
    if (LookupPrivilegeValueA(null, lpname, ref serLuid) == 0)
        return false;

    // ToDo

    return true;
}

ローカル一意識別子がどういうものなのかは詳しくはわかりませんでしたが、LookupPrivilegeValue関数は第二引数で指定された文字列あるいは既知の定数に合わせて第三引数のポインタへ特権に関する識別子を渡します。
今回はRegLoadKey関数を使うのでSeRestorePrivilegeの特権を有効化します。
その他の特権はPrivilege Constantsで確認できます。

LUIDから権限の有効化

最後にAdjustTokenPrivileges関数で特権を有効化します。

[DllImport("advapi32.dll")]
private static extern int AdjustTokenPrivileges(IntPtr tokenhandle, bool disableprivs, [MarshalAs(UnmanagedType.Struct)]ref TOKEN_PRIVILEGES newstate, int bufferlength, IntPtr preivousState, int returnlength);

[StructLayout(LayoutKind.Sequential)]
private struct TOKEN_PRIVILEGES
{
    public int PrivilegeCount;
    public LUID Luid;
    public int Attributes;
}

private const int SE_PRIVILEGE_ENABLED = 0x00000002;

private static bool EnabledPrivilege()
{
    IntPtr tokenHandle = IntPtr.Zero;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, ref tokenHandle) == 0)
        return false;

    // SeBackupPrivilege特権を有効化
    AdjustTokenPrivilege(tokenHandle, "SeBackupPrivilege");
    // SeRestorePrivilege特権を有効化
    AdjustTokenPrivilege(tokenHandle, "SeRestorePrivilege");

    CloseHandle(tokenHandle);
    return true;
}

private static bool AdjustTokenPrivilege(IntPtr tokenHandle, string lpname)
{
    var serLuid = new LUID();
    if (LookupPrivilegeValue(null, lpname, ref serLuid) == 0)
        return false;

    // 特権に対応したLUIDを有効化する旨のTOKEN_PRIVILEGES構造体
    var serTokenp = new TOKEN_PRIVILEGES
    {
        PrivilegeCount = 1,
        Luid = serLuid,
        Attributes = SE_PRIVILEGE_ENABLED
    };
    // 特権を有効化
    if (AdjustTokenPrivileges(tokenHandle, false, ref serTokenp, 0, IntPtr.Zero, 0) == 0)
        return false;

    return true;
}

TOKEN_PRIVILEGES構造体を生成し、先程取得したLUIDとSE_PRIVILEGE_ENABLED(0x00000002)を属性にします。
SE_PRIVILEGE_ENABLEDは特権を有効化する場合に指定するようです。
なお、TOKEN_PRIVILEGESは本来配列で定義されていますが、1つの要素しか使わないのでSequentialにして配列となるはずの構造体をそのまま定義しました。

レジストリハイブのロード

あとは先の権限を有効化する関数を呼び出して成功すればRegLoadKey関数を呼び出すとレジストリハイブがロードされます。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int RegLoadKeyA(uint hKey, string lpSubKey, string lpFile);

public enum ExRegistryKey : uint
{
    HKEY_LOCAL_MACHINE = 0x80000002,
    HKEY_USERS = 0x80000003,
}

public static bool ExLoadHive(string hivename, string filepath, ExRegistryKey rkey)
{
    if (!EnabledPrivilege())
        return false;

    return RegLoadKeyA((uint)rkey, hivename, filepath) == 0;
}

/*
ExLoadHive("hive_test", "example.hive", ExRegistryKey.HKEY_USERS);
*/

ロードされたレジストリハイブはこのプログラム上だけでなくRegEditからも操作できます。
なお、ExRegistryKey列挙型はHKEY_CLASSES_ROOTなども定義できますが、RegEditでレジストリハイブをロードできないので指定しないほうが良さそうです。

レジストリハイブから値を取得

レジストリハイブから値を取得するだけなら標準ライブラリで簡単にできます。
例えば、以下のようなレジストリ構造があるとします。

f:id:AonaSuzutsuki:20181231234056p:plain

そこから2つの値を取り出す時は次のようなコードで取得できます。

using (var baseKey = Registry.Users.OpenSubKey("test_hive\\test_value"))
{
    var testDward = baseKey.GetValue("test_dward").ToString();
    var testInt = baseKey.GetValue("test_int").ToString();

    // ToDo
}

もしこれもWin32APIで取得する場合はRegQueryValueExA関数でできます。

レジストリハイブ上のキーへ値を追加する

問題となるのは値を追加したりキーを追加するような書き込みが入る処理です。
標準ライブラリではUnauthorizedAccessException例外が発生して書き込みができません。
管理者権限でも同様なのでWin32APIからアクセスする必要があるようです。

キーのオープン

書き込む前に標準ライブラリの時と同様にキーを開く必要があります。
ここではWin32APIのRegOpenKeyExA関数を利用します。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int RegOpenKeyExA(IntPtr hKey, string lpSubKey, int ulOptions, int samDesired, ref IntPtr phkResult);

private const int KEY_ALL_ACCESS = 0xF003F;

public static IntPtr ExOpenSubKey(ExRegistryKey rkey, string subKeyName)
{
    IntPtr ptr = IntPtr.Zero;
    RegOpenKeyExA(new IntPtr((int)rkey), subKeyName, 0, KEY_ALL_ACCESS, ref ptr);
    return ptr;
}

/*
var ptr = ExOpenSubKey(ExRegistryKey.HKEY_USERS, "test_hive\\test_value");
*/

RegOpenKeyEx関数ではキーのハンドルを第三引数のポインタとして渡されます。
それが標準ライブラリで言うところのRegistryKeyクラスに当たるもので、このポインタを通じてキーに対する処理を行います。

キーへ値を追加する

キーを追加する場合はRegSetValueExA関数を利用します。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int RegSetValueEx(IntPtr hKey, string lpValueName, int Reserved, int dwType, IntPtr lpData, int cbData);

public static bool ExSetValue(string key, int value, IntPtr rkey)
{
    var size = Marshal.SizeOf(value.GetType());
    var pData = Marshal.AllocHGlobal(size);
    Marshal.WriteInt32(pData, value);

    var rtn = RegSetValueExA(rkey, key, 0, (int)RegistryValueKind.DWord, pData, size);
    Marshal.Release(pData);    

    return rtn == 0;
}

public static bool ExSetValue(string key, string value, IntPtr rkey)
{
    var size = value.Length + 1;
    var pData = Marshal.StringToHGlobalAnsi(value);

    return RegSetValueExA(rkey, key, 0, (int)RegistryValueKind.String, pData, size) == 0;
}

/*
var ptr = ExOpenKey(ExRegistryKey.HKEY_USERS, "test_hive\\test_value");
ExSetValue("test_int", 12, ptr);
ExSetValue("test_str", "test", ptr);
*/

書き込みたい値をマーシャリングし、RegSetValueExA関数へ渡すだけです。
ただ、Win32API側の構造体はバイト型のポインタですが、配列を渡すのではない部分が注意点でしょうか。
また、ロードのときのような特権が必要なわけではないようで、RegEditでロードしたハイブにもそのまま書き込むことができました。
標準ライブラリで失敗したのは単純に標準ライブラリでは対応してないだけなのか、その辺りはわかりませんでした。

キーを閉じる

開いたキーは勝手に閉じないので明示的に閉じる必要があります。
特に閉じ忘れるとハイブ自体をアンロードする時に失敗するので忘れないように。

[DllImport("advapi32.dll")]
private static extern int RegCloseKey(IntPtr hKey);

public static bool ExCloseKey(IntPtr key)
{
    return RegCloseKey(key) == 0;
}

レジストリハイブへキーを追加する

値の追加と同様に、キーを追加する場合もWin32APIを呼び出す必要があります。
この場合はRegCreateKeyExA関数を利用します。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int RegCreateKeyExA(IntPtr hKey, string lpSubKey, int Reserved, string lpClass, int dwOptions, int samDesired, IntPtr lpSecurityAttributes, ref IntPtr phkResult, ref int lpdwDisposition);

public static IntPtr ExCreateSubKey(ExRegistryKey rkey, string subKeyName)
{
    int lpdwDisposition = 0; // REG_CREATED_NEW_KEY(0x00000001)  REG_OPENED_EXISTING_KEY(0x00000002)
    var ptr = IntPtr.Zero;
    RegCreateKeyExA(new IntPtr((int)rkey), subKeyName, 0, null, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, IntPtr.Zero, ref ptr, ref lpdwDisposition);
    return ptr;
}

/*
var ptr = ExCreateSubKey(ExRegistryKey.HKEY_USERS, "test_hive\\test_value");
*/

RegCreateKeyExA関数はキーが存在しない場合に新しくキーを作成し、存在する場合はキーをオープンします。
RegOpenKeyExA関数と同様にphkResult引数で渡されたポインタにはキーのポインタが入るためあとはRegSetValueExA関数で値の追加ができます。
なお、lpdwDispositionはキーが作成された場合はREG_CREATED_NEW_KEY(0x00000001L)が、存在した場合はREG_OPENED_EXISTING_KEY(0x00000002)が入ります。

レジストリハイブのアンロード

レジストリハイブは一度ロードするとプログラムが終了してもロードされ続けるので不要なものは明示的にアンロードする必要があります。
幸いアンロードはロードの時と比べてかなりシンプルにできます。

[DllImport("advapi32.dll", CharSet = CharSet.Ansi)]
private static extern int RegUnLoadKeyA(uint hKey, string lpSubKey);

public static bool ExUnloadHive(string hivename, ExRegistryKey rkey)
{
    return RegUnLoadKeyA((uint)rkey, hivename) == 0;
}

ただし、別のプロセスがロードしたハイブはアンロードできません。
特に自プロセスがアンロードし忘れて一度終了するともう一度起動してロードしてもアンロードできないのでちょっと困ったものです。

全文

今回紹介しなかった削除も含めて全文はこちらで
ExRegistry.cs

汎用化したクラスライブラリ

これらをまとめてクラス化したものをGitHub上で公開しています。
一時は放置してましたが、ブログ執筆と同時に管理再開したので定期的にヤバイところ見つけたら治すようにはしてます。
今回紹介しなかった値の削除やキーの削除も対応しています。
ただ完璧に理解できてるわけではないので利用は自己責任で。
現状MIT Licenseにしてますが、著作権表記は特に必須ではないです。
もうちょっとゆるいのにしたいんですが・・・。
というわけで良しなに使ってください。