Playing God on Windows

Published , updated

There are times during software development when you want to run in interactive mode with maximum privileges on a system, be God as it were. One might think running as Administrator would do it but it doesn't. To be truly omnipotent on Windows, you have to run under the LocalSystem account[1]. It is easy enough with Tcl and this post shows you how. As a side bonus, it also describes how to inject processes into the interactive user's desktop to run under the user's account as well.

Warning: For obvious security reasons, you should only use these techniques under special circumstances and on development or test machines, not implement them in production.

Background

In the (good?) old days of XP, it was straighforward to get an interactive command shell running under the LocalSystem account. First, you implemented a service (ok, maybe it was not that straightforward), marked it as Interacts with the desktop and you were off and running.

With newer versions of Windows, this is deprecated and is not possible without changing some system settings. A different, slightly more complicated, method is required that is described here.

As an aside, Google will show many links on the topic, all of which suffer from either showing a partial implementation, or using C++/C# where the steps are lost in the myriad operations required due to the low-level nature of the languages. In Tcl on the other hand, the steps are clearly visible and a complete solution, including the required Windows service, takes just a couple of pages of code (no font reduction required :-).

The Theory

Some basic knowledge of sessions, window stations and desktops is necessary to understand the steps that need to be taken. Short introductions present here and there on the Web suffice for our purposes. For full details you can consult the MSDN documentation.

Getting a program to run interactively as LocalSystem really has two separate parts to it:

  • In addition to the special privileges it enjoys, the LocalSystem account also differs from normal accounts in that it is not possible to log into a system using that account name and a password (in fact, no password is associated with the account). The only way to run as LocalSystem is to configure a service to run under that account and have it be started by the service control manager.

  • Given that services in Windows can no longer run as part of the interactive desktop, we have to then implement a means to have the service then run a program on the desktop under the same account.

The first part is straightforward in Tcl. We show it in the implementation later but see Windows Services for details on implementing a service in Tcl.

The second part is also not hugely complicated and we detail it here. Running interactively requires getting access to the window station and interactive desktop so our program can display its window on it. If you have read the above-referenced articles, you know that window stations and desktops are protected resources and have security descriptors attached which determine who can display windows on them. To actually access them then, one needs an appropriate security token. There are two token properties that are relevant here:

  • The account associated with the token must be the account under which we want to run our program, i.e. LocalSystem, and it must be permitted access by the window station and desktop security descriptor.

  • The session associated with the token must be the session containing the window station and desktop of interest.

Once we have such a token, we can use the CreateProcessAsUser call to instantiate our program on the desktop.

Stealing identities

Time to move on to the implementation. The following code assumes you are running Tcl 8.6 and TWAPI 4.1b25 or later.

We start off assuming we are running as a service under the LocalSystem account (we implement this later) and write a simple proc to run our Tcl shell with a given security token and environment variables. The latter is not strictly required but for the general case we discuss later we do not want the child process to inherit our environment.

proc run_child {tok environ} {
    twapi::create_process [info nameofexecutable] -token $tok -env $environ
}

The above routine is simple enough as the TWAPI create_process routine allows a token and environment to be specified.

We then need to create the appropriate token to pass to this routine. Since we are running as a LocalSystem service already, just retrieving the token for our process will get us halfway there - it has the right account but is associated with session 0 which is the session that services run in. We need it to be attached to the interactive session. This is easily done by duplicating the token and changing its session as shown in this code fragment:

set tok [twapi::duplicate_token $my_tok -access {
    token_query token_duplicate token_assign_primary token_adjust_sessionid token_adjust_default
    } -type primary]
twapi::set_token_tssession $tok 1

However, we will not use this method. Instead we will use a different method that is slightly more complicated but is more generally applicable. We will locate a process that is running in the interactive session under the account of interest (LocalSystem here) and steal its identity. So we first write another short proc to find our poor victim and return its token.

proc find_victim {name} {
    set tssession [expr {[twapi::min_os_version 6] ? 1 : 0}]
    foreach pid [twapi::get_process_ids -name $name] {
        if {[dict get [twapi::get_process_info $pid -tssession] -tssession] == $tssession} {
            return [twapi::open_process_token -pid $pid -access {
                token_query token_duplicate token_assign_primary
            }]
        }
    }
    error "Could not find a victim process $name in session $tssession"
}

The above code loops through all processes on the system of the given name. When it finds one that belong to the session of interest, it obtains and returns its token. The only point to be noted is that the "session of interest" where the interactive console desktop runs is 1 for Vista and later and 0 for earlier Windows versions.

Having a means to locate an appropriate token, we just need a proc to steal its identity. This is nothing more than a wrapper around twapi::duplicate_token that we saw earlier.

proc steal_identity {tok} {
    twapi::trap {
        return [twapi::duplicate_token $tok -access {
            token_query token_duplicate token_assign_primary
            token_adjust_default
        } -type primary]
    } finally {
        twapi::close_token $tok
    }
}

This duplicates the token and closes the original. The access rights specified are those we will need to spawn off our child process. Make a note of these since MSDN does not list them all in the documentation of CreateProcessAsUser.

We are then ready to write the routine wrapping all the above.

proc system_shell {} {
    set tok [steal_identity [find_victim winlogon.exe]]
    twapi::trap {
        run_child $tok [twapi::get_system_environment_vars]
    } finally {
        twapi::close_token $tok
    }
}

Why do we choose to steal the identity of winlogon? Because

  • first, it is present in every interactive session and
  • second, it runs as LocalSystem which is what we want to do.

The other point to be noted is the use of get_system_environment_vars. This will result in the child process getting an environment based on unmodified system settings as opposed to the service process' environment which might have been modified for whatever reason.

We are done with the code required for spawning a LocalSystem shell. We can now move on to our promised bonus feature - again starting a process from the service but this time with the logged on user's identity. This is implemented by user_shell below.

proc user_shell {} {
    set tssession [expr {[twapi::min_os_version 6] ? 1 : 0}]
    set tok [steal_identity [twapi::WTSQueryUserToken $tssession]]
    twapi::trap {
        twapi::load_user_profile $tok
        twapi::impersonate_token $tok
        twapi::trap {
            run_child $tok [twapi::get_user_environment_vars $tok]
        } finally {
            twapi::revert_to_self
        }
    } finally {
        twapi::close_token $tok
    }
}

A few differences from system_shell are to be noted:

  • We do not need to find a process with the appropriate identity because Windows conveniently provides an API WTSQueryUserToken to retrieve the token for an interactive session. We just steal the identity of the token returned by this command.

  • We explicitly load the user's profile, which includes user-specific registry settings. In our code this is strictly for pedagogic purposes because we are spawning a process under the identity of the logged-in user which implies the profile for that user is already loaded.

  • We pass the environment using get_user_environment_vars which will provide the spawned process with the same environment that a user logging under that account would see.

  • We impersonate the account before calling run_child because we want the access checks for the executable to be done using the interactive user's identity, not LocalSystem.

  • Lastly, it is very important that we revert to our own LocalSystem identity in all cases, even when exceptions are raised.

Implementing the service

The final step is implementing the Windows service that enables all the above code to run. Remember most of the above code requires running under LocalSystem to begin with and the only way to do that is to write a service for the purpose.

Once running, our service will respond to two control service signals 128 and 129 to start a Tcl shell running as LocalSystem or the logged-on user respectively. The code for the service is shown below and should be self explanatory, at least if you have read about Windows Services as referenced earlier.

set Script [info script]
set Name SysTcl
set State stopped

proc report_state {seq} {
    global State Name
    if {[catch {
        set ret [twapi::update_service_status $Name $seq $State]
    } msg]} {
        ::twapi::eventlog_log "Service $Name failed to update status: $msg\r\n$::errorInfo"
    }
}

# Callback handler
proc service_control_handler {control {name ""} {seq 0} args} {
    global State Done
    switch -exact -- $control {
        start {
            if {[catch {
                set State running
                report_state $seq
            } msg]} {
                twapi::eventlog_log "Could not start $name server: $msg\r\n$::errorInfo"
                set Done 1
            }
        }
        stop {
            set State stopped
            report_state $seq
            set Done 1
        }
        userdefined {
            set arg 128
            if {[llength $args]} {
                set arg [lindex $args 0]
            }
            if {[catch {
                if {$arg == 128} {
                    system_shell
                } else {
                    user_shell
                }
            } msg]} {
                twapi::eventlog_log "Error starting child: $msg\r\n$::errorInfo"
            }
        }
        default {
            # Ignore
        }
    }

}
proc usage {} {
    global Script
    puts stderr [format {
Usage:
    %1$s %2$s install
       -- installs the service
    %1$s %2$s uninstall
       -- uninstalls the service
Then start/stop the service using either "net start" or the services control
manager GUI.
    } [file tail [info nameofexecutable]] $Script]
    exit 1
}

#
# Main processing starts here

package require twapi

# Parse arguments
if {[llength $argv] > 1} {
    usage
}

switch -exact -- [lindex $argv 0] {
    service {
        # We are running as a service
        if {[catch {
            twapi::run_as_service [list [list $Name service_control_handler]] -controls [list stop]
        } msg]} {
            twapi::eventlog_log "Service error: $msg\n$::errorInfo"
        }
        # We sit in the event loop until service control stop us through
        # the event handler
        vwait Done
    }
    install {
        if {[twapi::service_exists $Name]} {
            puts stderr "Service $Name already exists"
            exit 1
        }

        # Make the names a short name to not have to deal with
        # quoting of spaces in the path

        set exe [file nativename [file attributes [info nameofexecutable] -shortname]]
        set script [file nativename [file attributes [file normalize [info script]] -shortname]]
        twapi::create_service $Name "$exe $script service" -interactive 1
    }
    uninstall {
        if {[twapi::service_exists $Name]} {
            twapi::delete_service $Name
        }
    }
    default {
        usage
    }
}

exit 0

Running the code

You can download the entire systcl.tcl script and run it as follows.

Install the service (note you have to be running with administrative privileges to install services). At the DOS prompt,

C:\temp> tclsh systcl.tcl install

Start the service using the sc or net Windows programs.

C:\temp> sc start systcl

Start an interactive system shell on the desktop by sending it control signal 128.

C:\temp> sc control systcl 128

To verify it works, we check with whoami:

System shell

Similarly, start an interactive shell using the logged-on user account by sending it control signal 129.

C:\temp> sc control systcl 129

User shell

Finally, if you don't want the service to be lying around to be misused, stop it and uninstall.

C:\temp> sc stop systcl
C:\temp> tclsh systcl.tcl uninstall

Polishing our example

So there you have it, running Tcl as LocalSystem, nothing is off limits; you truly are God.

Our sample could do with some polish though. Some suggestions for the reader:

  • A wrapper that installs the service, runs the system shell and then uninstalls the service so you can start the system shell on demand like psexec from Sysinternals.

  • The created shells continue running even after the service exits. Whether this is desirable or not depends on your needs but you may want to consider tracking the created processes and kill them when the service receives a stop signal.

  • The program is hardcoded to start the Tcl shell. It would be more useful to start any program of the user's choice.

  • Finally, an enhancement to run as any user, not just LocalSystem or the logged on user would be a useful enhancement. And possibly the subject of a future post.


  1. This is actually not strictly true as protected processes dealing with DRM are off-limits even for LocalSystem. �?�