Displaying a program on the Windows secure desktop

programming windows C# pinvoke secure desktop

Recently I ran into a situation where it was desirable to have a program that ran and was visible on the Windows lock screen. Under ordinary circumstances this is something that just shouldn't be done. The Windows lock and logon screens are... sacred, in a sense, and displaying something on those screens is both a usability and security no no. That said, it's still possible to do and in unique situations it may even be the right solution. Plus, it was an interesting research exercise.

In my case, the machines that needed a program displayed on their lock screen were being used to drive the display of LCD screens for communication purposes. They would never be touched by a human, and in fact the purpose of running the machines on the Windows lock screen is to prevent tampering. This led me to investigate the feasibility of having a program display on the lock screen and eventually a working implementing written in C#.

Ultimately, I decided to implement a different set of security protocols to prevent unauthorized access to the machines, but nevertheless I figured I'd share my research here. Let me reiterate: please carefully consider whether it is necessary to display a program on the lock or logon screens, as Windows is specifically designed to prevent programs from doing so (think of what a pain a malicious program could be). Done considering? Well then read on!

Some preliminary concepts

First, let me state that it is possible to run a program on the lock and logon screens in Windows XP, Windows Vista, and Windows 7. However, the focus of my code was on Windows 7, as that was the operating system that my machines were running. Any code in this post will be specific to Windows 7 and may or may not work properly on other versions of Windows. As will be explained in a moment, some portions of Windows that involve the logon and lock screens changed between Windows XP and Windows Vista.

Second, to understand how this technique works it helps to know some pieces of the Windows innards. Windows actually supports more than one desktop (for example, terminal services allows multiple users to access the same machine, each with their own desktop). When a user is signed into a Windows PC, in addition to the normal desktop we're all used to, there is also a secure desktop.

The secure desktop is used to display User Account Control dialogs. When an action is performed that requires user approval, the display buffer of the normal desktop is copied, the currently active desktop (meaning the one being displayed) is switched to the secure desktop, the copied buffer is gray scaled, darkened, and then set as the background of the secure desktop. Finally, the actual UAC prompt is displayed on the secure desktop. All this happens so quickly and seamlessly that it appears as if the UAC prompt is just opening on the normal desktop and throwing a semitransparent dark overlay on top of the desktop but under the prompt.

The secure desktop is also used to display a few other screens: the Ctrl+Alt+Del screen, the lock screen, and the logon screen. It is the secure desktop that is the target of this technique, as the lock screen is displayed on the secure desktop, thus the challenge becomes: How can a program be displayed on the secure desktop?

Before jumping into it further, it helps to take a step back and understand how desktops are organized in Windows. Multiple desktops can exist and those that are related are grouped together under a single window station. Window stations are in turn grouped together into a session.

Sessions

I'm going to start from the top down with the concept of sessions first. Multiple sessions can exist, and indeed do exist, as at any given time there are usually two sessions present on a machine. "Session 0" is the session that services run under. Other sessions represent users signed into the machine either physically or through terminal services. With Fast User Switching, each user signed into the machine has their own session.

When a process executes, it executes within the context of a specific session and cannot switch that session at runtime. Sessions prevent a program running in one session from messaging a program running in another session (this extends down to programs running in different desktops, even in the same session). They also stop programs from sharing certain resources with each other, such as UI elements and kernel objects. Sessions do not prevent programs from communicating using network protocols nor do they prevent programs running with administrative privileges from terminating programs in other sessions.

You can use Task Manager to see which session a program is running in by going to View > Select Columns > Check "Session ID". In Windows Vista and Windows 7, services and many other programs will be running in session 0 while explorer.exe and other interactive programs will be running in session 1. This is different than in Windows XP, which runs all programs in session 0, providing a form of privilege escalation by allowing user run programs to interact more intimately with programs running as the SYSTEM user.

Window stations

Within each session are one or more window stations. A window station holds data that can be shared across desktops, such as the clipboard, and can also be secured with specific access rights. Within each session there is one, and only one, window station that can output a display and receive input. This station is always named "Winsta0" and is automatically created and assigned when a user logs on to the machine. It is the "Winsta0" window station that contains both the default interactive desktop and secure desktop (as well as a desktop for the screen saver if it is secure).

You can read more about window stations on MSDN here.

Desktops

Desktops are the final piece of the hierarchy. A desktop contains the display surface that consists of multiple windows created by programs running in the session and associated with the desktop. At any given time, within the "Winsta0" window station, only one desktop is considered the input desktop — the one displaying on the screen and receiving input.

The input desktop can be changed and does change throughout normal Windows use. As mentioned earlier, the secure desktop is used in many situations, such as the lock screen, the Ctrl+Alt+Del screen, and for UAC prompts.

So, what makes the secure desktop... secure? First it's important to know that a process, starting another process, can specify the desktop of the process that is being started. Second, like many objects in Windows, desktops can be secured with a specific set of permissions. The secure desktop by default is secured in a way that prevents processes that aren't running as the SYSTEM user from executing within its context. That prevents somemalware.exe (or whatever) from starting itself within the context of the secure desktop. It's a very good thing, as otherwise programs could hijack the secure desktop and do nasty things like click redirection. Imagine hitting Ctrl-Alt-Del and then Shutdown only to have an advertisement displayed instead. That would suck!

You can read more about desktops on MSDN here.

Implementation specifics

The high level concept

How is it possible to display a program on the secure desktop? Suppose that there are two processes, Fred and George (nevermind why two are needed for the moment). Fred wants George to display on the secure desktop. To do that, Fred needs to have the proper permission to tell George to display on the secure desktop. That is, Fred has to be running as the SYSTEM user. This requirement creates a hurdle that must be... hurdled.

Running a process as the SYSTEM user can be achieved through two different techniques. The first, and likely more common, is to run the process as a service. Services can run as the SYSTEM user and most often do. Having Fred be a service is one solution. Of course, a service is a process that usually runs all the time in the background. Sometimes this is fine, but other times, depending on the situation, it may not be desired. It may be that the only purpose Fred has in life is to get George up and displaying on the secure desktop. Once that's done, Fred can end his poor existence. With a service, Fred will hang around doing nothing and wasting space.

For Fred to end his life he needs to run as the SYSTEM user but not be a service. This is possible, oddly enough, using a scheduled task. A scheduled task that will start a specified process as the SYSTEM user can be created using the schtasks command. But wait, doesn't using a scheduled task mean that Fred can only be started at a specific date and time rather than at will as determined by some other process? Actually, through the same schtasks command, it is possible to manually execute a named scheduled task at any time. As long as the scheduled task is given a name when it is created, it can be triggered to execute at will. Through this technique it is possible to create a scheduled task, configured only to run once at some time in the past (so that it only executes when told to do so), that will start Fred as a SYSTEM user running in session 0. Here is an example of such a scheduled task:

schtasks /Create /RU SYSTEM /SC ONCE /TN SecureDesktopLauncher /SD 01/01/2012 /ST 12:00:00 /TR "\"C:\Path\To\Fred.exe\" \"C:\Path\To\George.exe\""

That command will create a scheduled task named SecureDesktopLauncher configured to run once as the SYSTEM user on January 1st, 2012 at noon. It executes a program named Fred.exe, which will consist of code covered later in this article. It also passes the path to George.exe, as the first command line argument to Fred.exe. The paths to Fred and George are wrapped in escaped double quotes. The escaping is needed because the whole thing is already wrapped in double quotes as it is an argument to the schtasks command and the double quotes around Fred and George are necessary if either of those paths contains a space. If you look at the scheduled task in Task Scheduler, it will show the action as "C:\Path\To\Fred.exe" "C:\Path\To\George.exe".

Manually executing the scheduled task can be done using the schtasks command with a different set of arguments:

schtasks /Run /TN SecureDesktopLauncher

Running that command will start Fred.exe under the SYSTEM user in session 0. Fred is then responsible for starting the program given in his first command line argument in the secure desktop of session 1. Once this is done, Fred has fulfilled his duty and disappears into process Neverland.

That's all well and good, but what does Fred.exe actually do to get George.exe (or whatever command is passed in) started and executing in the secure desktop of session 1?

The low level concept

How Fred is able to tell George to run on the secure desktop of session 1 is the pivotal part of this whole exercise. Note that although the following code is written in C#, it consists almost entirely of P/Invoke calls into Windows API DLLs. Most of the necessary process manipulation has no representation within the .NET framework and requires the use of the Windows API. Thus, the technique can be implemented in any language or runtime that is capable of calling into Windows DLLs and working with the results of functions contained therein.

Before jumping into code it is necessary to recognize Fred's goal. Fred, running as the SYSTEM user, wants to start another process also as the SYSTEM user but specifically in the secure desktop of session 1. Process security revolves around creating and using tokens that contain security information. Each executing process has an associated token. It is the token that contains information like the user that the process will execute as and the session that it will execute within. The easiest course of action is to start with Fred's token, clone it, and then modifying it as needed. The steps look like this:

  1. Get the current process' token (Fred's token).
  2. Duplicate that token into a new token.
  3. Set the session ID of the new token to session 1.
  4. Start George using the new token and specifying that George run on the secure desktop.

To do all that requires numerous calls into, and the usage of structures within, the Windows API. Before getting into Fred's code I will cover the Windows APIs being used. Most of these signatures were taken from pinvoke.net.

[DllImport("advapi32.dll", SetLastError=true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool OpenProcessToken(
    IntPtr ProcessHandle,
    UInt32 DesiredAccess,
    out IntPtr TokenHandle);

OpenProcessToken retrieves a pointer to the token for the process given by the ProcessHandle argument. A pointer to the current process token can be retrieved using Process.GetCurrentProcess().Handle for the ProcessHandle argument.

[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Auto)]
public static extern bool CreateProcessAsUser(
    IntPtr hToken,
    string lpApplicationName,
    string lpCommandLine,
    IntPtr lpProcessAttributes,
    IntPtr lpThreadAttributes,
    bool bInheritHandles,
    uint dwCreationFlags,
    IntPtr lpEnvironment,
    string lpCurrentDirectory,
    ref STARTUPINFO lpStartupInfo,
    out PROCESS_INFORMATION lpProcessInformation);

CreateProcessAsUser does exactly as it is named and creates a process as a user. This function is used to start George. The arguments are detailed on the MSDN page, but the token being passed in for hToken is used to set the session and the STARTUPINFO structure passed in for the lpStartupInfo argument contains the desktop to use (the secure desktop).

[DllImport("advapi32.dll", CharSet=CharSet.Auto, SetLastError=true)]
public extern static bool DuplicateTokenEx(
    IntPtr hExistingToken,
    uint dwDesiredAccess,
    IntPtr lpTokenAttributes,
    SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
    TOKEN_TYPE TokenType,
    out IntPtr phNewToken);

DuplicateTokenEx creates a duplicate of a given token. It is used to fulfill step two (a token cannot be reused on more than one process).

[DllImport("advapi32.dll", SetLastError = true)]
public static extern bool SetTokenInformation(
    IntPtr TokenHandle,
    TOKEN_INFORMATION_CLASS TokenInformationClass,
    ref UInt32 TokenInformation,
    UInt32 TokenInformationLength);

SetTokenInformation is used to change the session ID of the duplicated token.

[DllImport("user32.dll", SetLastError = true)]
public static extern bool LockWorkStation();

LockWorkStation does exactly as it says, it locks the work station. In some circumstances it does not work, which will be explained later. Locking the workstation (which sets the input desktop to the secure desktop) is sometimes necessary for a program to execute probably on the secure desktop.

[DllImport("wtsapi32.dll", SetLastError=true)]
public static extern bool WTSQueryUserToken(
    UInt32 sessionId,
    out IntPtr Token);

WTSQueryUserToken returns the user token for specified session ID. It is used to get the user token of the currently logged in user.

[DllImport("kernel32.dll")]
public static extern uint WTSGetActiveConsoleSessionId();

WTSGetActiveConsoleSessionId retrieves the session ID for the physical session — that is, the session that is connected to the physical keyboard, mouse, and monitor (or other display device).

There are many structures and constants used by the above functions. I won't go into much detail (the MSDN has explanations for all of them) but here are the declarations for the structures and constants that are used.

[Flags]
public enum ACCESS_MASK : uint
{
    DELETE = 0x00010000,
    READ_CONTROL = 0x00020000,
    WRITE_DAC = 0x00040000,
    WRITE_OWNER = 0x00080000,
    SYNCHRONIZE = 0x00100000,
 
    STANDARD_RIGHTS_REQUIRED = 0x000f0000,
 
    STANDARD_RIGHTS_READ = 0x00020000,
    STANDARD_RIGHTS_WRITE = 0x00020000,
    STANDARD_RIGHTS_EXECUTE = 0x00020000,
 
    STANDARD_RIGHTS_ALL = 0x001f0000,
 
    SPECIFIC_RIGHTS_ALL = 0x0000ffff,
 
    ACCESS_SYSTEM_SECURITY = 0x01000000,
 
    MAXIMUM_ALLOWED = 0x02000000,
 
    GENERIC_READ = 0x80000000,
    GENERIC_WRITE = 0x40000000,
    GENERIC_EXECUTE = 0x20000000,
    GENERIC_ALL = 0x10000000,
 
    DESKTOP_READOBJECTS = 0x00000001,
    DESKTOP_CREATEWINDOW = 0x00000002,
    DESKTOP_CREATEMENU = 0x00000004,
    DESKTOP_HOOKCONTROL = 0x00000008,
    DESKTOP_JOURNALRECORD = 0x00000010,
    DESKTOP_JOURNALPLAYBACK = 0x00000020,
    DESKTOP_ENUMERATE = 0x00000040,
    DESKTOP_WRITEOBJECTS = 0x00000080,
    DESKTOP_SWITCHDESKTOP = 0x00000100,
 
    WINSTA_ENUMDESKTOPS = 0x00000001,
    WINSTA_READATTRIBUTES = 0x00000002,
    WINSTA_ACCESSCLIPBOARD = 0x00000004,
    WINSTA_CREATEDESKTOP = 0x00000008,
    WINSTA_WRITEATTRIBUTES = 0x00000010,
    WINSTA_ACCESSGLOBALATOMS = 0x00000020,
    WINSTA_EXITWINDOWS = 0x00000040,
    WINSTA_ENUMERATE = 0x00000100,
    WINSTA_READSCREEN = 0x00000200,
 
    WINSTA_ALL_ACCESS = 0x0000037f
}
 
[Flags]
public enum CreateProcessFlags : uint
{
    DEBUG_PROCESS                       = 0x00000001,
    DEBUG_ONLY_THIS_PROCESS             = 0x00000002,
    CREATE_SUSPENDED                    = 0x00000004,
    DETACHED_PROCESS                    = 0x00000008,
    CREATE_NEW_CONSOLE                  = 0x00000010,
    NORMAL_PRIORITY_CLASS               = 0x00000020,
    IDLE_PRIORITY_CLASS                 = 0x00000040,
    HIGH_PRIORITY_CLASS                 = 0x00000080,
    REALTIME_PRIORITY_CLASS             = 0x00000100,
    CREATE_NEW_PROCESS_GROUP            = 0x00000200,
    CREATE_UNICODE_ENVIRONMENT          = 0x00000400,
    CREATE_SEPARATE_WOW_VDM             = 0x00000800,
    CREATE_SHARED_WOW_VDM               = 0x00001000,
    CREATE_FORCEDOS                     = 0x00002000,
    BELOW_NORMAL_PRIORITY_CLASS         = 0x00004000,
    ABOVE_NORMAL_PRIORITY_CLASS         = 0x00008000,
    INHERIT_PARENT_AFFINITY             = 0x00010000,
    INHERIT_CALLER_PRIORITY             = 0x00020000,
    CREATE_PROTECTED_PROCESS            = 0x00040000,
    EXTENDED_STARTUPINFO_PRESENT        = 0x00080000,
    PROCESS_MODE_BACKGROUND_BEGIN       = 0x00100000,
    PROCESS_MODE_BACKGROUND_END         = 0x00200000,
    CREATE_BREAKAWAY_FROM_JOB           = 0x01000000,
    CREATE_PRESERVE_CODE_AUTHZ_LEVEL    = 0x02000000,
    CREATE_DEFAULT_ERROR_MODE           = 0x04000000,
    CREATE_NO_WINDOW                    = 0x08000000,
    PROFILE_USER                        = 0x10000000,
    PROFILE_KERNEL                      = 0x20000000,
    PROFILE_SERVER                      = 0x40000000,
    CREATE_IGNORE_SYSTEM_DEFAULT        = 0x80000000,
}
 
[StructLayout(LayoutKind.Sequential)]
public struct LUID {
    public uint LowPart;
    public int HighPart;
}
 
[StructLayout(LayoutKind.Sequential)]
public struct LUID_AND_ATTRIBUTES {
    public WinApi.LUID Luid;
    public UInt32 Attributes;
}
 
[StructLayout(LayoutKind.Sequential)]
public struct PROCESS_INFORMATION
{
    public IntPtr hProcess;
    public IntPtr hThread;
    public int dwProcessId;
    public int dwThreadId;
}
 
public enum SECURITY_IMPERSONATION_LEVEL
{
     SecurityAnonymous,
     SecurityIdentification,
     SecurityImpersonation,
     SecurityDelegation
}
 
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO
{
    public Int32 cb;
    public string lpReserved;
    public string lpDesktop;
    public string lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public Int32 dwFlags;
    public Int16 wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
}
 
public enum TOKEN_TYPE
{
    TokenPrimary = 1,
    TokenImpersonation
}
 
public enum TOKEN_INFORMATION_CLASS : int
{
    TokenUser = 1,
    TokenGroups,
    TokenPrivileges,
    TokenOwner,
    TokenPrimaryGroup,
    TokenDefaultDacl,
    TokenSource,
    TokenType,
    TokenImpersonationLevel,
    TokenStatistics,
    TokenRestrictedSids,
    TokenSessionId,
    TokenGroupsAndPrivileges,
    TokenSessionReference,
    TokenSandBoxInert,
    TokenAuditPolicy,
    TokenOrigin,
    MaxTokenInfoClass
};
 
public const int READ_CONTROL = 0x00020000;
public const int STANDARD_RIGHTS_REQUIRED = 0x000F0000;
public const int STANDARD_RIGHTS_READ = READ_CONTROL;
public const int STANDARD_RIGHTS_WRITE = READ_CONTROL;
public const int STANDARD_RIGHTS_EXECUTE = READ_CONTROL;
public const int STANDARD_RIGHTS_ALL = 0x001F0000;
public const int SPECIFIC_RIGHTS_ALL = 0x0000FFFF;
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_ALL_ACCESS_P = (STANDARD_RIGHTS_REQUIRED |
                                        TOKEN_ASSIGN_PRIMARY |
                                        TOKEN_DUPLICATE |
                                        TOKEN_IMPERSONATE |
                                        TOKEN_QUERY |
                                        TOKEN_QUERY_SOURCE |
                                        TOKEN_ADJUST_PRIVILEGES |
                                        TOKEN_ADJUST_GROUPS |
                                        TOKEN_ADJUST_DEFAULT);
public const int TOKEN_ALL_ACCESS = TOKEN_ALL_ACCESS_P | 
                                    TOKEN_ADJUST_SESSIONID;
public const int TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY;
public const int TOKEN_WRITE = STANDARD_RIGHTS_WRITE |
                                TOKEN_ADJUST_PRIVILEGES |
                                TOKEN_ADJUST_GROUPS |
                                TOKEN_ADJUST_DEFAULT;
public const int TOKEN_EXECUTE = STANDARD_RIGHTS_EXECUTE;

In my own code I grouped all of these declarations and signatures into a static WinApi class. The examples that follow will contain many references to the WinApi class.

With the parts of the Windows API declared, the steps one through four given above can be implemented. All of the code below exists within a Main method in the same order as the examples they're given.

if (args.Length == 0)
{
    throw new ArgumentException("args", "The program to launch " +
        "must be specified with the first command line argument.");
}
 
string startingDirectory = Path.GetDirectoryName(
    System.Reflection.Assembly.GetEntryAssembly().Location) +
    Path.DirectorySeparatorChar;
string process = args[0];
string processArgs = String.Empty;
if (args.Length > 1)
{
    // Skip the first argument because it is the command that
    // will be executed. Wrap the arguments in double quotes
    // so that paths with spaces make it through.
    processArgs = String.Join(" ", args.Skip(1)
        .Select((a) => "\"" + a + "\""));
}
string currentDirectory = Path.GetDirectoryName(process);

This first block of code sets up some variables that will be used later. The program expects the first argument to be the path to the program that will end up on the secure desktop (George). Subsequent arguments to the program will be passed in as arguments for George.

IntPtr currentToken = IntPtr.Zero;
 
if (!WinApi.OpenProcessToken(Process.GetCurrentProcess().Handle,
    WinApi.TOKEN_DUPLICATE, out currentToken))
{
    throw new InvalidOperationException("ERROR: OpenProcessToken " +
        "returned false - " + Marshal.GetLastWin32Error());
}

Here is the first call into the Windows API. It retrieves a pointer to the current process' token into the currentToken variable. WinApi.TOKEN_DUPLICATE is needed so that Fred is able to make a copy of the token to be used as an argument to CreateProcessAsUser. If the API call fails an exception is thrown containing the last Windows API error. In production code this could certainly be handled differently (such as logging the exception and firing off some kind of alert sequence) but for now just throwing the exception is adequate.

IntPtr newToken = IntPtr.Zero;
 
if (!WinApi.DuplicateTokenEx(currentToken,
    (uint)WinApi.ACCESS_MASK.GENERIC_ALL, IntPtr.Zero,
    WinApi.SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
    WinApi.TOKEN_TYPE.TokenImpersonation, out newToken))
{
    throw new InvalidOperationException("ERROR: DuplicateTokenEx " +
        "returned false - " + Marshal.GetLastWin32Error());
}

This block of code performs the token duplication using DuplicateTokenEx. There isn't anything particularly special here. The token is created as an impersonation token, which is the most common type of token. WinApi.ACCESS_MASK.GENERIC_ALL is used to ensure that all access rights are requested.

UInt32 dwSessionId = WinApi.WTSGetActiveConsoleSessionId();
if (!WinApi.SetTokenInformation(newToken,
    WinApi.TOKEN_INFORMATION_CLASS.TokenSessionId, ref dwSessionId,
    (UInt32)IntPtr.Size))
{
    throw new InvalidOperationException("ERROR: SetTokenInformation " +
        "returned false - " + Marshal.GetLastWin32Error());
}

The new token was duplicated from Fred's token, which means it is associated with session 0. SetTokenInformation is used to change the token's session ID to the session ID of the session associated with the physical keyboard, mouse, and display, which is returned by WTSGetActiveConsoleSessionId.

At this point Fred has a duplicate of his own token that has been updated to use the session ID of the user physically logged into the machine. That leaves only one step: starting George. But, since George will be displaying on the lock screen it might be helpful to lock the computer before executing George. In some instances it might be required, especially if George plans on doing some 3D rendering. At least, this is a problem I experienced. Code to lock the screen is as follows:

IntPtr interactiveUserToken = IntPtr.Zero;
if (WinApi.WTSQueryUserToken(WinApi.WTSGetActiveConsoleSessionId(),
    out interactiveUserToken))
{
    WinApi.STARTUPINFO siInteractive = new WinApi.STARTUPINFO();
    siInteractive.cb = Marshal.SizeOf(siInteractive);
    siInteractive.lpDesktop = @"Winsta0\default";
    WinApi.PROCESS_INFORMATION piInteractive =
        new WinApi.PROCESS_INFORMATION();
    WinApi.CreateProcessAsUser(interactiveUserToken, null,
        "rundll32.exe user32.dll,LockWorkStation", IntPtr.Zero,
        IntPtr.Zero, false,
        (uint)WinApi.CreateProcessFlags.CREATE_NEW_CONSOLE |
        (uint)WinApi.CreateProcessFlags.INHERIT_CALLER_PRIORITY,
        IntPtr.Zero, currentDirectory, ref siInteractive,
        out piInteractive);
}
else
    WinApi.LockWorkStation();
 
// Sleep for a second because locking the workstation is not
// an instantaneous action, especially if doing so through
// rundll32.exe.
// Note the proper technique here would be to block until the
// workstation can be confirmed as locked.
System.Threading.Thread.Sleep(1000);

Fred is executing as the SYSTEM user, so under normal circumstances he cannot lock the screen directly by calling LockWorkStation. To lock the screen Fred has to find a way to call LockWorkStation as the user that is physically logged into the workstation. WTSQueryUserToken and WTSGetActiveConsoleSessionId are used to get a user token for the physically logged in user. CreateProcessAsUser is used to start rundll32.exe, which is a program that can execute arbitrary functions in DLLs by passing it the DLL and function names as arguments. The result is that rundll32.exe is called in the context of the physically logged in user and executes the LockWorkStation function, which will succeed because rundll32.exe is running as the proper user. If the initial calls to get the user token fail then LockWorkStation is called directly, but know that it is likely to fail. The thread sleeps for a second to give the system an opportunity to lock the screen. As the comments indicate, simply sleeping for a second does not garuntee in any way that the system is locked when Fred resumes executing. A proper technique would be to somehow block until the locked state can be confirmed.

WinApi.PROCESS_INFORMATION pi = new WinApi.PROCESS_INFORMATION();
WinApi.STARTUPINFO si = new WinApi.STARTUPINFO();
 
si.cb = Marshal.SizeOf(si);
si.lpDesktop = @"Winsta0\Winlogon";
 
string commandline = "\"" + process + "\" " + processArgs;
 
// The moment of truth.
if (!WinApi.CreateProcessAsUser(newToken, null, commandline,
    IntPtr.Zero, IntPtr.Zero, true,
    (uint)WinApi.CreateProcessFlags.CREATE_NEW_CONSOLE |
    (uint)WinApi.CreateProcessFlags.INHERIT_CALLER_PRIORITY,
    IntPtr.Zero, currentDirectory, ref si, out pi))
{
    throw new InvalidOperationException(
        "ERROR: CreateProcessAsUser returned false - " +
        Marshal.GetLastWin32Error());
}

Now everything is in place. The final step is to use CreateProcessAsUser to launch George using the cloned and modified token. One of the parameters of CreateProcessAsUseris a STARTUPINFO structure. This structure contains a pointer to a string namedlpDesktop which determines the window station and desktop to use, separated by a backslash. As I mentioned earlier, the "Winsta0" window station is the default window station that has the normal desktop and secure desktop. The secure desktop is identified as "Winlogon".

Wrapping up

Now Fred is complete and can be compiled. Using the scheduled task technique outlined previously, Fred can be started as the SYSTEM user, passing in the path to George as the argument. If Fred executes successfully, Windows will be locked and the target program will be started within the secure desktop. If the target program isn't visible, Alt-Tabbing can reveal it.

To make things easy, here is a link to the source code in a compilable state.

Use at your own risk! Running a program as the SYSTEM user has serious security implications. The SYSTEM user has the broadest privileges on the system, even more so than the Administrator user, and can do anything.

Questions? Ask in the comments.

Add new comment

By submitting this form, you accept the Mollom privacy policy.

© 2012 Caleb Delnay
Powered by Drupal, an open source content management system