In a project I’m working on, I need to change the Group Policy Object (GPO) registry settings, both for the computer and for the logged in user. Both of these present problems. This article addresses the logged in user scenario.

The actual application is a Windows service running on the client that accepts requests from a remote machine. The complication is that the service will need to run as System so that it can write to any part of the registry.

Below is prototyping and troubleshooting using a Console application.

Goal

Run a Console application as the System user and write to the GPO that restricts running applications. Here’s the GPO if you want to look it up in Group Policy Editor (gpedit.msc).

User Configuration\Administrative Templates\System\Don't run specified Windows applications.

Challenges/Issues

64-bit Windows has both logical and physical registry entries. This is a problem when updating keys that are used by other programs (in this case, Windows itself). You have to be sure you’re updating the right key. The problem really presents itself when writing to the LOCAL_MACHINE hive. The physical location—such as the Wow6432Node key—depends on whether the application is 64 or 32 bits.

The CURRENT_USER hive doesn’t care about 64/32 bits. But remember that a computer can have multiple users logged on. Also, where does the System user’s settings go? Finally, how will we find out who is logged in? What a mess!

Tools and Environment

I’m running Visual Studio 2015 Community on 64-bit Windows 10.

This is the registry key/value we’ll be creating throughout, setting its value to 1.

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\DisallowRun

Test Writing to the Registry

We first need to write to the registry and see the results. Create a Console Application with the following code.

using System;
using Microsoft.Win32;

namespace RegistryPrototyping
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                WriteRegistry();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.GetBaseException().Message);
            }
            Console.WriteLine("Finished. Press any key to close.");
            Console.ReadLine();
        }

        static void WriteRegistry()
        {
            string keyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
            string valueName = "DisallowRun";
            string value = "1";
            RegistryValueKind dataType = RegistryValueKind.DWord;
            Registry.SetValue(keyName, valueName, value, dataType);
        }
    }
}

Run the application. You should get an error

Access to the registry key 'HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer' is denied.

This is our first problem. Our code needs permissions to write to the registry.

For some developers, this problem will be hidden because they turn off UAC or always run Visual Studio as administrator. I don’t do either of these exactly because I’ll catch some permissions issue early.

The obvious next step is to close our app, open Visual Studio using Run as Administrator, then reopen/run the app.

image

No error, and the key is written to the expected registry key (i.e. the currently logged in user).

image

Great! Next task, testing as Local System.

But first, delete the DisallowRun key!

Testing as Local System

We need to run our app—either the .exe or Visual Studio—as Local System. I didn’t find a built-in Windows way to do this. The recommended method is to use Sysinternals’ psexec utility, which is normally used to execute commands on remote domain machines.

  1. Download psexec.
  2. Open a command prompt As Administrator (you must do this), and navigate to the folder where you extracted psexec.exe.
  3. Launch the desired application with the –i and –s switches. These run the application locally in interactive mode under the System account.

I right-clicked on my Visual Studio icon > Properties to get the path, and ended up with this command.

psexec –i –s “C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\devenv.exe"

When you launch this way, you’re running as if logged in under a different profile, so VS will go through its initial startup prompts.

  1. Open your Console solution; you’ll need to navigate to its location via C:\Users\[username].
  2. Run it. Check your registry key. Where the heck is the new value? Because we’re running as System, the registry value will be written to the .Default user key.

image

Well, heck! This obviously isn’t what we want. We need to write to the currently logged on user’s profile. But this result gives us a clue how we’ll solve our problem later.

Impersonation – False Start

At first, impersonation seems like the way to go. If I can just find out who’s currently logged on and working, I should be able to impersonate that user and write to the registry on that user’s behalf.

There are two problems with this.

  1. What if there are multiple users logged on? How do I find out which user is actively working?
  2. Will I need to run with elevated privileges? (hint: yes, of course)

If there are multiple users logged in, I didn’t find a way to determine who is actively using the computer. That seems like a poor requirement anyway. Don’t I care if, for instance, a scheduled task starts running as the logged in (but not active) user? Yes, because I might want my agent to prohibit it. What if it’s a headless system that’s always logged on? Worse, what if I want to run the client on a Windows server (which allows multiple simultaneous users via RDP)?

But maybe someone more clever than me wants to figure this out. There might be a way by looking at the active desktop(s).

The second problem is, for us, the impractical barrier to overcome. Writing to the registry requires elevation. The way we’d be impersonating the user would be to get a handle to a running process, then impersonating via that handle. But the user will not be running a known-elevated process. So, we’d be stuck. As soon as we tried to write to the registry, it would fail. And if we try to elevate the process (if we even could do that), the user would get prompted, defeating the goal of the program.

But we can still do what we want. After all, as System we can write to most parts of the physical registry.

Writing to the User’s Key

When we wrote to the System user’s logical hive CURRENT_USER, we ended up in the physical hive USERS. That’s where all the user registry settings are. CURRENT_USER is a symbolic link, a redirection for the active user. Here’s my computer’s USERS hive:

image

Those long SIDs are the users. If I can get those, I can write to the user’s Group Policy setting.

Step one, revise the WriteRegistry method.

static void WriteRegistry(string sid)
    {
        string keyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
        string valueName = "DisallowRun";
        string value = "1";
        keyName = keyName.Replace("HKEY_CURRENT_USER", @"HKEY_USERS\" + sid);
        RegistryValueKind dataType = RegistryValueKind.DWord;
        Registry.SetValue(keyName, valueName, value, dataType);
    }

Step two, crazy lots of code!

You’ll need these addtional references.

using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
using System.Diagnostics;
using System.Collections.Generic;

Use a Win32 API function to get the logged on users. This is some deeper stuff, and I didn’t write it. The most I did was put it in a class and make the function return all the users.

The meat of this—the cool part—is using Process.GetProcessesByName with an argument of “explorer”. This will return anyone logged on, because explorer is (typically) always running.

class WindowsIdentityHelper
{

    [DllImport("advapi32", SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    static extern int OpenProcessToken(
    System.IntPtr ProcessHandle, // handle to process
    int DesiredAccess, // desired access to process
    ref IntPtr TokenHandle // handle to open access token
    );

    [DllImport("kernel32", SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    static extern bool CloseHandle(IntPtr handle);
    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
    int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

    public const int TOKEN_ASSIGN_PRIMARY = 0x0001;
    public const int TOKEN_DUPLICATE = 0x0002;
    public const int TOKEN_IMPERSONATE = 0x0004;
    public const int TOKEN_QUERY = 0x0008;
    public const int TOKEN_QUERY_SOURCE = 0x0010;
    public const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
    public const int TOKEN_ADJUST_GROUPS = 0x0040;
    public const int TOKEN_ADJUST_DEFAULT = 0x0080;
    public const int TOKEN_ADJUST_SESSIONID = 0x0100;
    public const int TOKEN_READ = 0x00020000 | TOKEN_QUERY;
    public const int TOKEN_WRITE = 0x00020000 | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT;
    public const int TOKEN_EXECUTE = 0x00020000;

    public static List GetLoggedOnUsers()
    {
        List users = new List();
        string errs = "";
        IntPtr hToken = IntPtr.Zero;
        //Get a process that will always be available.
        foreach (Process proc in Process.GetProcessesByName("explorer"))
        {
            try
            {
                if (OpenProcessToken(proc.Handle,
                TOKEN_QUERY | TOKEN_IMPERSONATE | TOKEN_DUPLICATE,
                ref hToken) != 0)
                {
                    WindowsIdentity newId = new WindowsIdentity(hToken);
                    CloseHandle(hToken);
                    users.Add(newId);
                }
                else
                {
                    errs += String.Format("OpenProcess Failed {0}, privilege not held\r\n", Marshal.GetLastWin32Error());
                }

            }
            catch (Exception ex)
            {
                errs += String.Format("OpenProcess Failed {0}\r\n", ex.Message);
            }
        }
        if (errs.Length > 0) { throw new Exception(errs); }
        return users;
    }

And a method to write to the users’ registries.

static void WriteToLoggedOnUsers()
{
    foreach (var user in WindowsIdentityHelper.GetLoggedOnUsers())
    {
        try
        {
            WriteRegistry(user.Owner.Value);
            Console.WriteLine("Updated user " + user.Name + "  SID " + user.Owner.Value);
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.GetBaseException().Message);
        }
    }
}

 

Notice we’re using WindowsIdentity.Owner.Value to return the SID. The property .Owner.AccountDomainSid doesn’t return the full, correct SID.

Finally, change the Main method.

static void Main(string[] args)
{
    try
    {
        WriteToLoggedOnUsers();
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.GetBaseException().Message);
    }
    Console.WriteLine("Finished. Press any key to close.");
    Console.ReadLine();
}

One last thing before running the app. If you really want to test it, create a local user (named Test would be good), log that user on, leave it running and switch back to your regular user.

Now, run the application.

image

Check those User SIDs!

image

image

And finally, to prove the redirection, we’ll look at our CURRENT_USER hive.

image

(Remember, now, to delete those keys. You might also log off and delete the test user.)

Wrap Up

Our goal was to write to the logged on users’ Group Policy keys, with the future intention of preventing them from running certain applications. We need to do this running as System, and found that impersonation wouldn’t work because we had use an elevated process. Instead, we wrote to the running users’ physical registry keys.

One interesting aspect of this session is that there’s not a certain automated way to test for correctness. If I wrote a test that looked at a logical key (like CURRENT_USERS\…), and then checked if the value existed, I wouldn’t know if the correct physical key was updated. This may be a time when a human needs to test. (Maybe there’s a clever way around this, though.)

Full source code can be found after the References.

References

Registry Redirector

Registry Keys Affected by WOW64

Group Policy Object – Disallow Run

WindowsIdentity.Impersonate

Using OpenProcessToken  <=this is where the Win32 code came from. Very useful.

OpenProcessToken function

OpenProcessToken Access Rights

Constants for Token Access Rights

Process.GetProcessesByName

My machine’s named Nesbit, after the great children’s fantasy author.

Full Source

using System;
using Microsoft.Win32;
using System.Runtime.InteropServices;
using System.Security;
using System.Security.Principal;
using System.Diagnostics;
using System.Collections.Generic;

namespace RegistryPrototyping
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                WriteToLoggedOnUsers();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.GetBaseException().Message);
            }
            Console.WriteLine("Finished. Press any key to close.");
            Console.ReadLine();
        }

        static void WriteRegistry(string sid)
        {
            string keyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
            string valueName = "DisallowRun";
            string value = "1";
            keyName = keyName.Replace("HKEY_CURRENT_USER", @"HKEY_USERS\" + sid);
            RegistryValueKind dataType = RegistryValueKind.DWord;
            Registry.SetValue(keyName, valueName, value, dataType);
        }

        static void WriteRegistry()
        {
            string keyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer";
            string valueName = "DisallowRun";
            string value = "1";
            RegistryValueKind dataType = RegistryValueKind.DWord;
            Registry.SetValue(keyName, valueName, value, dataType);
        }

        static void WriteToLoggedOnUsers()
        {
            foreach (var user in WindowsIdentityHelper.GetLoggedOnUsers())
            {
                try
                {
                    WriteRegistry(user.Owner.Value);
                    Console.WriteLine("Updated user " + user.Name + "  SID " + user.Owner.Value);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.GetBaseException().Message);
                }
            }
        }
    }
}


class WindowsIdentityHelper
{

    [DllImport("advapi32", SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    static extern int OpenProcessToken(
    System.IntPtr ProcessHandle, // handle to process
    int DesiredAccess, // desired access to process
    ref IntPtr TokenHandle // handle to open access token
    );

    [DllImport("kernel32", SetLastError = true),
    SuppressUnmanagedCodeSecurityAttribute]
    static extern bool CloseHandle(IntPtr handle);
    [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
    public extern static bool DuplicateToken(IntPtr ExistingTokenHandle,
    int SECURITY_IMPERSONATION_LEVEL, ref IntPtr DuplicateTokenHandle);

    public const int TOKEN_ASSIGN_PRIMARY = 0x0001;
    public const int TOKEN_DUPLICATE = 0x0002;
    public const int TOKEN_IMPERSONATE = 0x0004;
    public const int TOKEN_QUERY = 0x0008;
    public const int TOKEN_QUERY_SOURCE = 0x0010;
    public const int TOKEN_ADJUST_PRIVILEGES = 0x0020;
    public const int TOKEN_ADJUST_GROUPS = 0x0040;
    public const int TOKEN_ADJUST_DEFAULT = 0x0080;
    public const int TOKEN_ADJUST_SESSIONID = 0x0100;
    public const int TOKEN_READ = 0x00020000 | TOKEN_QUERY;
    public const int TOKEN_WRITE = 0x00020000 | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT;
    public const int TOKEN_EXECUTE = 0x00020000;

    public static List GetLoggedOnUsers()
    {
        List users = new List();
        string errs = "";
        IntPtr hToken = IntPtr.Zero;
        //Get a process that will always be available.
        foreach (Process proc in Process.GetProcessesByName("explorer"))
        {
            try
            {
                if (OpenProcessToken(proc.Handle,
                TOKEN_QUERY | TOKEN_IMPERSONATE | TOKEN_DUPLICATE,
                ref hToken) != 0)
                {
                    WindowsIdentity newId = new WindowsIdentity(hToken); 
                    CloseHandle(hToken);
                    users.Add(newId);
                }
                else
                {
                    errs += String.Format("OpenProcess Failed {0}, privilege not held\r\n", Marshal.GetLastWin32Error());
                }

            }
            catch (Exception ex)
            {
                errs += String.Format("OpenProcess Failed {0}\r\n", ex.Message);
            }
        }
        if (errs.Length > 0) { throw new Exception(errs); }
        return users;
    }

}