为新创建的用户编辑注册表值

Editing registry value for newly created user

我有一个 .NET 应用程序可以像这样创建一个新的本地用户:

var principalContext = new PrincipalContext(ContextType.Machine);
                var userPrincipal = new UserPrincipal(principalContext);
                userPrincipal.Name = StandardUserName.Text;
                userPrincipal.Description = "New local user";
                userPrincipal.UserCannotChangePassword = true;
                userPrincipal.PasswordNeverExpires = true;
                userPrincipal.Save();

                // Add user to the users group
                var usersGroupPrincipal = GroupPrincipal.FindByIdentity(principalContext, UserGroupName.Text);
                usersGroupPrincipal.Members.Add(userPrincipal);
                usersGroupPrincipal.Save();

接下来,我想为该用户设置一些注册表值。为此,我需要用户的 SID:

private string GetSidForStandardUser()
{
    var principalContext = new PrincipalContext(ContextType.Machine);
    var standardUser = UserPrincipal.FindByIdentity(principalContext, StandardUserName.Text);
    return standardUser.Sid.ToString();
}

并创建一个新的子项:

var key = string.Format("{0}{1}", GetSidForStandardUser(), keyString);
var subKey = Registry.Users.CreateSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree);

但是,我在调用 CreateSubKey 时收到 IOException,告诉我参数无效。发生这种情况是因为该用户的子项在用户首次登录之前尚不存在。如果我在以新用户身份登录之前检查 regedit(在管理员权限下),我可以看到 SID 在 HKEY_Users 下不存在。如果我以新用户身份登录,然后注销并以我的原始用户身份重新登录并刷新 regedit,则新的 SID 存在。

我的问题是:有没有办法为尚未登录的用户添加子项?我想避免必须以新用户身份登录,然后在此过程中途退出。

此后我找到了问题的解决方案,但它并不完美,而且它会引发您必须处理的各种新问题。尽管如此,它仍然有效。我将解决方案发布在这里,供我自己参考,也供将来可能需要它的其他人参考。

问题是用户的注册表配置单元位于其用户配置文件文件夹(例如 c:\users\username)中名为 NTUSER.DAT 的文件中。但是,用户的用户配置文件文件夹在他们登录之前不会创建,因此当您创建新用户时,还没有用户配置文件,也没有包含其注册表配置单元的 NTUSER.DAT 文件,因此您无法编辑他们的任何内容注册表设置。

不过有一个窍门:当您 运行 使用该用户的凭证时,会创建用户配置文件 。有一个名为 runas.exe 的可执行文件,可让您 运行 在指定用户的凭据下获得第二个可执行文件。如果您创建一个新用户并将其设置为 运行,比如 cmd.exe,就像这样:

runas /user:newuser cmd.exe

...它将打开一个 Cmd 实例,但更重要的是,在 \users 文件夹中创建新用户的配置文件,包括 NTUSER.DAT.

现在,Cmd.exe 使命令 window 处于打开状态,您可以手动将其关闭,但有点笨拙。 https://superuser.com/a/389288 将我指向 rundll32.exe,当 运行 没有任何参数时,它什么都不做并立即退出。此外,它在每个 Windows 安装中都可用。

因此,通过调用 运行as 并告诉它 运行 rundll32.exe 作为新用户,我们可以创建用户的配置文件而无需任何进一步交互:

Process.Start("runas", string.Format("/user:{0} rundll32", "newuser"));

嗯……几乎没有互动。 Runas 打开一个控制台 window,要求您输入用户密码,即使没有设置密码(它希望您只需按回车键)。这很烦人,但可以通过巧妙地使用 Pinvoke 和可选的 System.Windows.Forms 将 window 带到前台并向其发送一些按键来解决:

var createProfileProcess = Process.Start("runas", 
                                         string.Format("/user:{0} rundll32", 
                                         "newuser"));

IntPtr hWnd;
do
{
    createProfileProcess.Refresh();
    hWnd = createProfileProcess.MainWindowHandle;
} while (hWnd.ToInt32() == 0);

SetForegroundWindow(hWnd);
System.Windows.Forms.SendKeys.SendWait("{ENTER}");

这会创建配置文件,等待 window 有句柄,然后调用 Win32 函数 SetForegroundWindow() 将其置于前台。然后,它使用 SendKeys.SendWait 向 window 发送回车键。如果您不想使用 WinForms DLL,您可以为此使用 PInvoke Win32 函数,但对于这种特殊情况,我发现 winforms 方式更快更容易。

这有效,但揭示了另一个问题:运行as 不允许您 运行 在没有密码的帐户下使用东西。超级用户再次救援; https://superuser.com/a/470539 指出有一个名为 Limit local account use of blank passwords to console logon only 的本地策略,可以将其禁用以允许这种情况。我不希望用户必须手动禁用此策略,所以我在 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa 中使用了相应的注册表值 LimitBlankPasswordUse

我现在通过将注册表值设置为 0 来禁用策略,运行 运行as 命令创建配置文件,然后通过将值设置为 1 来重新启用策略。 (首先检查该值并仅在首先设置它时才重新启用它可能会更清晰,但为了演示目的,这样做:

    const string keyName = "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa";
    Registry.SetValue(keyName, "LimitBlankPasswordUse", 0);

    var createProfileProcess = Process.Start("runas", 
                                             string.Format("/user:{0} rundll32", 
                                             "newuser"));

    IntPtr hWnd;
    do
    {
        createProfileProcess.Refresh();
        hWnd = createProfileProcess.MainWindowHandle;
    } while (hWnd.ToInt32() == 0);

    SetForegroundWindow(hWnd);
    System.Windows.Forms.SendKeys.SendWait("{ENTER}");

    Registry.SetValue(keyName, "LimitBlankPasswordUse ", "1");

这有效!但是,用户的注册表配置单元尚未加载,因此您仍然无法读取或写入它。为此,该进程需要一些权限,您可以使用一些 Win32 方法再次提供这些权限:

        OpenProcessToken(GetCurrentProcess(), 
                         TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
                         out _myToken);
        LookupPrivilegeValue(null, SE_RESTORE_NAME, out _restoreLuid);
        LookupPrivilegeValue(null, SE_BACKUP_NAME, out _backupLuid);

        _tokenPrivileges.Attr = SE_PRIVILEGE_ENABLED;
        _tokenPrivileges.Luid = _restoreLuid;
        _tokenPrivileges.Count = 1;

        _tokenPrivileges2.Attr = SE_PRIVILEGE_ENABLED;
        _tokenPrivileges2.Luid = _backupLuid;
        _tokenPrivileges2.Count = 1;

        AdjustTokenPrivileges(_myToken, 
                              false, 
                              ref _tokenPrivileges, 
                              0, 
                              IntPtr.Zero, 
                              IntPtr.Zero);
        AdjustTokenPrivileges(_myToken, 
                              false, 
                              ref _tokenPrivileges2, 
                              0, 
                              IntPtr.Zero, 
                              IntPtr.Zero);

最后使用新用户的 SID 加载配置单元:

        // Load the hive
        var principalContext = new PrincipalContext(ContextType.Machine);
        var standardUser = UserPrincipal.FindByIdentity(principalContext, "newuser");
        var sid = standardUser.Sid.ToString();

        StringBuilder path = new StringBuilder(4 * 1024);
        int size = path.Capacity;
        GetProfilesDirectory(path, ref size);

        var filename = Path.Combine(path.ToString(), "newuser", "NTUSER.DAT");

        Thread.Sleep(2000);
        int retVal = RegLoadKey(HKEY_USERS, sid, filename);

我在 Load registry hive from C# fails 中找到了大部分代码。

RegLoadKey 应该 return 0 成功。我注意到偶尔,它会无缘无故地加载配置单元失败。由于可能尚未创建用户配置文件中的必要文​​件,我在加载配置单元之前添加了 Thread.Sleep(2000),以便 Windows 有时间创建所有必要的文件。可能有更简洁的方法来执行此操作,但现在这行得通。

现在,您可以使用新用户的 SID 为新用户加载和设置注册表值,例如:

var subKeyString = "SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Advanced";
var keyString = string.Format("{0}{1}", sid, subKeyString);
var subKey = Registry.Users.CreateSubKey(keyString,
                                         RegistryKeyPermissionCheck.ReadWriteSubTree);
subKey.SetValue("EnableBalloonTips", 0, RegistryValueKind.DWord);

可以肯定的是,完成后我还卸载了注册表配置单元。我不确定是否需要这样做,但这似乎是一件很巧妙的事情:

var retVal = RegUnLoadKey(HKEY_USERS, GetSidForStandardUser());