The purpose of this article is to illustrate how to correctly launch an interactive process from a service in Windows Vista, and also to demonstrate how to launch that process with full Administrator privileges. An interactive process is one that is capable of displaying a UI on the desktop.
The article shows how to create a service called LoaderService that serves as an application loader and whose purpose is to launch, at boot time, a command prompt that runs as an Administrator. The article closes with a section discussing how the code could be extended for more practical purposes.
Let’s start from the beginning… you have just booted up your computer and are about to log on. When you log on, the system assigns you a unique Session ID. In Windows Vista, the first User to log on to the computer is assigned a Session ID of 1 by the OS. The next User to log on will be assigned a Session ID of 2. And so on and so forth. You can view the Session ID assigned to each logged on User from the Users tab in Task Manager:
Notice, I indicated that the User named Pero is in control of the Console. In this case, I mean the Physical Console. The Physical Console consists of the monitor, keyboard, and mouse. Since Pero is in control of the keyboard, monitor, and mouse, he is considered the currently active User. However, since Users can be impersonated, it is more appropriate to reference the currently active Session rather than the currently active User. The Win32 API contains a function called WTSGetActiveConsoleSessionId()
which returns the Session ID of the User currently in control of the Physical Console. If we were to call that method right now, it would return a value of 1 because that is the Session ID of the User Pero.
There exists a special Session in Vista that has a Session ID of 0. This is commonly referenced as Session0. All Windows Services run within Session0, and Session0 is non-interactive. Non-interactive means that UI applications cannot be launched; however, there is a way around this by activating the Interactive Services Detection Service (ISDS). This not a very elegant solution, and will not be covered in this article. There is a quick 5 minute Channel 9 video that demonstrates the ISDS for those interested. This article assumes the absence of the ISDS. Now, because Session0 is not a User Session, it does not have access to the video driver, and therefore any attempts to render graphics will fail. Session0 isolation is a security feature added in Vista to isolate system processes and services from potentially malicious user applications.
This is where things get interesting. The reason for this isolation is because the System account (or System User) has elevated privileges that allow it to run unhindered by the restrictions of Vista UAC. If everything were running under the System account, Vista UAC might as well be turned off.
Now, I know what you’re thinking, “If Windows Services run in Session0, and Session0 cannot start processes that have a UI, then how can our loader service spawn a new process that not only has a UI, but that also runs within the currently logged on User’s Session?” Take a look at this screenshot from the Processes tab in Task Manager, and pay particular attention to the winlogon.exe processes:
Notice there are two winlogon.exe processes, and the User who owns both of those processes is the System User. The System User is a highly privileged User unhindered by the Vista UAC that we were talking about earlier. Also, notice the Session IDs that indicate within which Sessions the winlogon.exe processes are running. If you remember from earlier, Session ID 1 refers to the User Pero’s Session, and Session ID 2 refers to the User Sienna’s Session. This means that there is a winlogon.exe process running under the System account within Pero’s Session. It also means that there is a winlogon.exe process running under the System account within Sienna’s Session. This is the appropriate time to mention that any Session with an ID greater than 0 is capable of spawning an interactive process, which is a process capable of displaying a UI.
The solution may not be totally clear yet, but it will be shortly, as now it is time to discuss our strategy!
First, we are going to create a Windows Service that runs under the System account. This service will be responsible for spawning an interactive process within the currently active User’s Session. This newly created process will display a UI and run with full admin rights. When the first User logs on to the computer, this service will be started and will be running in Session0; however the process that this service spawns will be running on the desktop of the currently logged on User. We will refer to this service as the LoaderService.
Next, the winlogon.exe process is responsible for managing User login and logout procedures. We know that every User who logs on to the computer will have a unique Session ID and a corresponding winlogon.exe process associated with their Session. Now, we mentioned above, the LoaderService runs under the System account. We also confirmed that each winlogon.exe process on the computer runs under the System account. Because the System account is the owner of both the LoaderService and the winlogon.exe processes, our LoaderService can copy the access token (and Session ID) of the winlogon.exe process and then call the Win32 API function CreateProcessAsUser
to launch a process into the currently active Session of the logged on User. Since the Session ID located within the access token of the copied winlogon.exe process is greater than 0, we can launch an interactive process using that token.
Now for the fun stuff… the code!
The Windows Service is located in a file called LoaderService.cs within the Toolkit project. Below is the code that gets called when the LoaderService
is started:
protected override void OnStart(string[] args)
{
// the name of the application to launch
String applicationName = "cmd.exe";
// launch the application
ApplicationLoader.PROCESS_INFORMATION procInfo;
ApplicationLoader.StartProcessAndBypassUAC(applicationName, out procInfo);
}
The code above calls the StartProcessAndBypassUAC(...)
function which will launch a command prompt (with full admin rights) as part of a newly created process. Information about the newly created process will get stored into the variable procInfo
.
The code for StartProcessAndBypassUAC(...)
is located in the file ApplicationLoader.cs. Let’s dissect that function to examine how a service running in Session0 will load a process into the currently logged on User’s Session. To begin, we will obtain the Session ID of the currently logged on User. This is achieved by making a call to the Win32 API function WTSGetActiveConsoleSessionId()
.
// obtain the currently active session id; every logged on
// User in the system has a unique session id
uint dwSessionId = WTSGetActiveConsoleSessionId();
Next, we will obtain the Process ID (PID) of the winlogon.exe process for the currently active Session. Remember, there are two Sessions currently running, and if we copy the access token of the wrong one, we could end up launching our new process on another User’s desktop.
// obtain the process id of the winlogon process that
// is running within the currently active session
Process[] processes = Process.GetProcessesByName("winlogon");
foreach (Process p in processes)
{
if ((uint)p.SessionId == dwSessionId)
{
winlogonPid = (uint)p.Id;
}
}
Now that we have obtained the PID of the winlogon.exe process, we can use that information to obtain its process handle. To do so, we make a Win32 API call to OpenProcess(...)
:
// obtain a handle to the winlogon process
hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);
Having acquired the process handle, we can make a Win32 API call to OpenProcessToken(...)
to obtain a handle to the access token of the winlogon.exe process:
// obtain a handle to the access token of the winlogon process
if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
{
CloseHandle(hProcess);
return false;
}
With a handle to the access token, we can proceed to call the Win32 API function DuplicateTokenEx(...)
which will duplicate the access token:
// Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser
// I would prefer to not have to use a security attribute variable and to just
// simply pass null and inherit (by default) the security attributes
// of the existing token. However, in C# structures are value types and therefore
// cannot be assigned the null value.
SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
sa.Length = Marshal.SizeOf(sa);
// copy the access token of the winlogon process;
// the newly created token will be a primary token
if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa,
(int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification,
(int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
{
CloseHandle(hProcess);
CloseHandle(hPToken);
return false;
}
There are many advantages to duplicating an access token. Most notable in our case is that we have a new copy of a primary access token which also contains within it the associated logon Session of that copied token. If you refer to the Task Manager screenshot above that shows the two winlogon.exe processes, you will notice that the duplicated Session ID will be 1, which is the Session ID of the currently logged on User, Pero. We can now call the Win32 API function CreateProcessAsUser
to spawn a new process within the Session of the currently logged on User; in this case, the process will spawn in the Session of the User Pero. To summarize, the code below runs in Session0, but will launch a new process in Session 1:
STARTUPINFO si = new STARTUPINFO();
si.cb = (int)Marshal.SizeOf(si);
// interactive window station parameter; basically this indicates
// that the process created can display a GUI on the desktop
si.lpDesktop = @"winsta0\default";
// flags that specify the priority and creation method of the process
int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;
// create a new process in the current User's logon session
bool result = CreateProcessAsUser(hUserTokenDup, // client's access token
null, // file to execute
applicationName, // command line
ref sa, // pointer to process SECURITY_ATTRIBUTES
ref sa, // pointer to thread SECURITY_ATTRIBUTES
false, // handles are not inheritable
dwCreationFlags, // creation flags
IntPtr.Zero, // pointer to new environment block
null, // name of current directory
ref si, // pointer to STARTUPINFO structure
out procInfo // receives information about new process
);
The above code will launch a command prompt that is running as an Administrator under the System account. I’d like to comment on the parameter @"winsta0\default"
. This is a hard-coded String
that Microsoft arbitrarily chose to indicate to the OS that the process we are about to spawn in CreateProcessAsUser
should have full access to the interactive windowstation and desktop, which basically means it is allowed to displayed UI elements on the desktop.