Creating a user-interactive Task Sequence experience

This blog post was created by Michael Schmidt.

At Microsoft, we have the unique challenge of having to build a OSD experience that includes providing a great user experience prior to deployment of a new OS.  In our case, we had the challenge of figuring out how we could do this considering that task sequence runs in Session0 and many of our users would never get this pretty user experience. 

In the interest of helping others who have this same goal - bettering the user experience of OSD – we wanted to cover launching an interactive process from the Task Sequence (TS). No binaries are provided; this post only journals a high-level overview of creating a C++ binary to launch an interactive process.

A similar idea with source is available from The Deployment Guys.

1. What are we building?

Assuming the user interface/wizard is pre-built in the form of a standard binary (.exe), a launcher is necessary. Shown below is an example of the launcher concept, and how it might be used.

  • Binary: SessionLaunch.exe
  • Parameter: A process name to find; our next parameter will launch in the same session
  • Parameter: A process to launch interactively, in the user’s context

image

2. When is this useful?

Since processes launched under WinPE are visible, this launcher provides value only when run from an existing OS. Two of the primary modes for operating a TS are:

  • Standalone Media TS
  • Download and Execute / Download on Demand TS

With a custom launcher, a UI could be spawned from anyplace in your Task Sequence. It could be used to:

  • Prompt the user for information
  • Popup error information or warning dialogs
  • Show TS status, or replace the progress implemented by OSD completely (by hiding TSProgress UI via ShowWindow())

Another point worth mentioning is that a native launcher has no dependency on the .NET Framework. All API calls covered will run on Windows XP and higher. Building a launcher after this manner grants deployment flexibility, in relation to target OS and required runtimes.

3. Understanding Process Context

When running a Task Sequence within an existing Operating System (OS), all TS work is performed in SYSTEM context. As a result, all user interface (UI) wizardry you might inject wouldn’t be visible to the user (Windows Vista+). To quickly cover this (emphasis added):

Impact of Session 0 Isolation on Services and Drivers in Windows Vista “The Microsoft Windows Vista™ operating system mitigates this security risk by isolating services in Session 0 and making Session 0 noninteractive. In Windows Vista, only system processes and services run in Session 0. The first user logs on to Session 1, and subsequent users log on to subsequent sessions. This means that services never run in the same session as users’ applications and are therefore protected from attacks that originate in application code.”

Take note of the following points:

  1. Task Sequence execution occurs in Session 0
  2. User Logon occurs in Session 1+
  3. Unless you boot and execute your TS UI in WinPE, no one will see it

4. Which Session?

Our goal is to launch a process from SYSTEM context into a session where our user interacts. Since TS execution is performed in Session 0 (non-interactive), we need to locate an alternative, interactive session which the user is running under. How can one tell which Session ID the user is operating under? One useful API to determine this might be:

  • WTSGetActiveConsoleSessionId
    ”The WTSGetActiveConsoleSessionId function retrieves the Terminal Services session that is currently attached to the physical console. The physical console is the monitor, keyboard, and mouse. Note that it is not necessary that Terminal Services be running for this function to succeed.”

In most cases, WTSGetActiveConsoleSessionID is sufficient to retrieve the working session. Unfortunately, while this session provides the physical console session ID, problems arise when multiple users are logged in (or when our target user is connected over Remote Desktop/Terminal Services). A more appropriate function for our purpose is:

  • ProcessIdToSessionId
    ”Retrieves the Terminal Services session associated with a specified process.”

Now, only one TS can execute at a time. When the TS launches, a process is spawned by the TS to show progress; this process is “tsprogressui.exe”. OSD opens this process from SYSTEM context into the active user’s session, which is precisely what we’re attempting to do. Thus, in our launcher we simply search for the OSD-spawned “tsprogressui.exe” process. By searching for this process and calling ProcessIdToSessionId against it, we can determine the session ID our UI should spawn in.

Searching processes can be accomplished through the following API calls:

  • CreateToolhelp32Snapshot
    ”Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.”
  • Process32First
    ”Retrieves information about the first process encountered in a system snapshot.”
  • Process32Next
    ”Retrieves information about the next process recorded in a system snapshot.”

An example of this is provided by MSDN; using this example, we can extract the session ID by:

  1. Inserting a string comparison in the while-loop to match “pe32.szExeFile” against the name “tsprogressui.exe”
  2. Once found, call ProcessIdToSessionId to determine which session the TS is running in

5. Launching the UI

CreateProcessAsUser

Now for the tricky part. I defer source examples for this next section to the wide world of the web, but will overview on the idea. Most important is the CreateProcessAsUser API. Using this function, it becomes possible to launch your UI from the Task Sequence in SYSTEM context, into the user’s session. When this is accomplished, your UI will be visible/interactive.

  • CreateProcessAsUser
    ”Creates a new process and its primary thread. The new process runs in the security context of the user represented by the specified token.”

The difficulty in making this call is the preparation. Numerous parameters must be created and pre-populated prior to making the call, and this is where the trouble lies.

Creating an Access Token from the user’s Winlogon

An Access Token, representing the user we wish to execute under, is required for CreateProcessAsUser. Before launching a process under the user’s session, this token must be created/duplicated. To perform this, we extract and duplicate an Access Token from the existing user’s Winlogon session.

  1. Find the “winlogon.exe” process and obtain its handle for the target user.
    This can be done by looping through processes (shown previously) and matching the process name and session:
    • Loop through processes…
    • If process name (pe32.szExeFile) is “winlogon.exe” …
    • If process session ID (ProcessIdToSessionId) matches our user’s session ID, grab the Process ID (PID)
  2. Call OpenProcess against the PID.
  3. Extract a handle to the Winlogon token using OpenProcessToken.
Elevating token privileges with SE_DEBUG_NAME

Once a handle to Winlogon is obtained, an Access Token can then be extracted. First, a template containing the privileges we require is necessary (LUID type); this will be used when creating/duplicating our Access Token (TOKEN_PRIVILEGES object).

  1. Use LookupPrivilegeValue to find the LUID representing SE_DEBUG_NAME
  2. Create a new TOKEN_PRIVILEGES object
  3. Call DuplicateTokenEx using the handle from our user’s Winlogon; our desired access is MAXIMUM_ALLOWED
  4. Call SetTokenInformation with the previously extracted Session ID (where our UI will execute)
  5. Call AdjustTokenPrivileges to merge/append privileges from the LUID template, into our duplicated token
Launching, waiting, and returning

While many other parameters are required, they’re much simpler to generate. Creating the Access Token is the tricky part; MSDN documentation and other code samples suitably cover the rest. However, when launching from a TS you’ll likely need to wait for the process to end, and want to know the resulting exit code. Launch, wait, and process the return code in this order:

  1. CreateProcessAsUser( …, &piConsole)
  2. WaitForSingleObject(piConsole.hProcess, INFINITE);
  3. GetExitCodeProcess(piConsole.hProcess, dExitCode);

6. Summary

This is all it takes… a little bit of code and a little bit of work and you can give a “new” face to OSD like never before thought of.  To wrap up the order presented for creating a launcher:

  1. Find where “tsprogressui.exe” is executing (session ID)
  2. Find the corresponding “winlogon.exe” process associated with that session
  3. Duplicate the token
  4. Set the token session
  5. Adjust the token privilege
  6. Launch the process
  7. Wait
  8. Extract and return the exit code

7. Troubleshooting Tips

A few small tips to consider when developing, testing, or troubleshooting.

PsExec

Working between SYSTEM and USER context can be difficult and complex. PsExec is invaluable for troubleshooting; this utility can open a process in SYSTEM context, allowing the ability to test your binary. To open a SYSTEM context command-line, run the following from an elevated command window:

  • psexec –s –i –d cmd.exe
Task Manager: Adding the Session ID Column

It’s useful to see what tasks are executing under which Session ID. Add the “Session ID” column to Task Manager:

image