Editing registry value for newly created user

删除回忆录丶 提交于 2020-01-15 04:52:59

问题


I have a .NET application that creates a new local user like so:

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();

Next, I want to set some registry values for that user. For that, I need the user's SID:

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

And create a new subkey:

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

However, I get an IOException on the call to CreateSubKey that tells me the Parameter is invalid. This happens because the subkey for that user does not exist yet until the user logs in for the first time. If I check regedit (under admin privileges) before logging in as the new user I can see that the SID does not exist under HKEY_Users. If I log in as the new user, then log out and back in as my original user and refresh regedit, the new SID exists.

My question is: is there a way to add subkeys for users that haven't logged in yet? I'd like to avoid having to log in as the new user and then back out halfway during the process.


回答1:


I've since found a solution to the problem, but it's not pretty and it raises all sorts of new problems you have to deal with. Still, it works. I'm posting the solution here for my own reference and for others who may have need for it in the future.

The problem is that a user's registry hive is in their user profile folder (e.g. c:\users\username) in a file called NTUSER.DAT. However, a user's user profile folder isn't created until they log in, so when you create a new user there's no user profile yet and no NTUSER.DAT file containing their registry hive, so you can't edit any of their registry settings.

There's a trick, though: the user profile does get created when you run something under that user's credentials. There's an executable called runas.exe that lets you run a second executable under a specified user's credentials. If you create a new user and make it run, say, cmd.exe, like so:

runas /user:newuser cmd.exe

...it'll open a Cmd instance, but more importantly, create newuser's profile in the \users folder, including NTUSER.DAT.

Now, Cmd.exe leaves a command window open, which you can close manually but it's kind of clunky. https://superuser.com/a/389288 pointed me to rundll32.exe which, when run without any parameters, does nothing and exits immediately. Also, it's available on every Windows installation.

So, by calling runas and telling it to run rundll32.exe as the new user, we can create the user's profile without any further interaction:

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

Well... almost with no interaction. Runas opens a console window that requires you to enter the user's password, even if no password is set (it wants you to just press enter). This is annoying, but can be solved with some clever use of Pinvoke and optionally System.Windows.Forms to bring the window to the foreground and send it some keypresses:

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}");

This creates the profile, waits until the window has a handle, and then calls the Win32 function SetForegroundWindow() to bring it to the foreground. Then, it uses SendKeys.SendWait to send an enter key to that window. If you don't want to use a WinForms DLL, there are Win32 functions you can PInvoke for this, but for this particular scenario I found the winforms way quicker and easier.

This works, but reveals yet another problem: runas won't let you run stuff under an account that has no password. Superuser to the rescue again; https://superuser.com/a/470539 points out that there's a Local Policy called Limit local account use of blank passwords to console logon only that can be disabled to allow this exact scenario. I didn't want users to have to manually disable this policy, so I used the corresponding registry value LimitBlankPasswordUse in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa.

I now disable the policy by setting the registry value to 0, run the runas command to create the profile, then re-enable the policy by setting the value to 1 afterwards.(It would probably be cleaner to check the value first and only re-enable it if it was set in the first place, but for demonstration purposes this will do:

    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");

This works! However, the user's registry hive isn't loaded yet, so you still won't be able to read or write to it. For that, the process needs a couple of privileges, which you can again provide using some Win32 methods:

        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);

And finally load the hive using the new user's 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);

I found most of this code in Load registry hive from C# fails.

RegLoadKey should return 0 on success. I noted that occasionally, it would fail to load the hive for no apparent reason. Reasoning that perhaps the necessary files in the user profile had not yet been created, I added a Thread.Sleep(2000) before loading the hive to give Windows time to create all the necessary files. There's probably a neater way to do this, but for now this'll work.

Now, you can load and set registry values for newuser using the newuser's SID, for instance:

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);

Just to be sure, I also unloaded the registry hive when I was done. I'm not sure if it's required, but it seems like the neat thing to do:

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


来源:https://stackoverflow.com/questions/34288818/editing-registry-value-for-newly-created-user

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!