GUI-Based RunAsEx

Environment: Win2K+ ONLY, VC6+, MS SDK (Platform SDK), DDK for Win2K+, Process Explorer, Local Administrator Identity

Prerequisite Knowledge: Win2K Security stuff (SID, Token, ACL, Privilege, WinStation/Desktop, and so on), NT Service, SEH

Note:
  1. To compile and link successfully, half of the functionality provided by this tool (the more interesting half) requires that you have DDK for Win2K+ installed for using Native API exported by ntdll.dll, though the executable file of this tool does not have any dependency on it (DDK).
  2. The user MUST be a local administrator member; he/she can be a common domain user meanwhile. If a certain privilege is not GRANTED, the user needs to LOGOFF (NOT REBOOT) ONCE to use this tool. This is a "Do-It-Once" job if no more domain policy is involved.
  3. The VC++6 Project File is here for this RunAsEx with all source code and final executable file. VS. NET users can easily upgrade to Solution file.
  4. Go to the kitchen and prepare a cup of coffee or tea for yourself before reading this lengthy article. You should be familiar with Token inside-out when you finish it.
Important Note: Before the time of this writing (November 2003), I planed to include several sample programs from the book Programming Server-Side Applications for Microsoft Windows 2000, ISBN 0-7356-0753-2, Microsoft Press, 1999 to save my re-creation of a similar helper program. But, I can NOT get permission from the author, so I included my own programs I wrote years ago when I read this book. The point is that I rewrote these original samples' GUI framework with MFC and extended the user interaction (e.g. I list all Process ID in a combo to choose with clicking instead of edit box you need to type with a number found in Task Manager).
WARNING: I only expect that these helper programs will help you understand what I will be talking about here concerning this RunAsEx tool. You may find a few menus/button handlers empty there, while most do give you massive information. Again, by no means are they serious programs; they are only my coding practice when reading a cool book as a habit. And, I have no intention to polish them later. Oh, btw, at the time of writing these program, Visual Studio 6.0 Japanese Enterprise Version is my standard IDE, so the wizard generated all comments in Japanese and they look weird in the English version of Windows, but I have re-opened them and make sure they can be compiled on an English OS. Just ignore all Japanese (unreadable) comments.

1. What's Unique in This RunAsEx and What's It For?

So, many of you must have heard of a tool called "RunAs" that originated from the WinNT4 Resource Kit; and from Win2K. Its functionality was even integrated to the OS itself. It definitely gives the user a fast way to RunAs somebody. For example, when you want to run some software that you can not trust fully, having it running on a separate machine may be a way for the rich, ordering a Virtual Machine Utility such as VMWare also costs you dozens of bucks, and the zero-price way is that you carefully design ACLs on a directory and the Registry and let the un-trusted program run under a limited user context. One of the most serious, if not deadly, drawbacks of MS version RunAs provided by Windows OS is that it can only RunAs somebody and not the SYSTEM (LocalSystem) which is important to developers.

There come some programs for your rescue, two of which that are open source code are Mr. Keith Brown's tool CmdRunAs in Feb 2000, MSJ or Mr. Martyn 'Ginner' Brown's tool Start a Command As Any User in www.codeguru.com 2001. Both are based on same idea—To overcome the privilege requirement, launch (after installing) the second instance of self as a NT Service, from there call LogonUser and CreateProcessAsUser to launch the target program, and finally uninstall the NT Service. In the midst of these operations, they will modify the destination WinStation/Desktop's DACL (it is a hard coded WinStat0\Default) and grant the target user access to the target WinStation/Desktop. Besides that, they handle the chores of loading/unloading the target user's profile gracefully. In fact, I highly recommended you read Mr. Brown's article before continuing unless you are quite familiar with Win2K Security already.

There are still some shortcomings of these two programs:

  1. They have no support for WTS (Windows Terminal Service) and, by default, NT Service is running in Session0 even you install one from Session1. The net result: The program you RunAs-ed from these tools always starts from the Session0 desktop. This is especially inconvenient, if not useless, for a WinXP+ FUS (Fast User Switch) user for the program RunAs-ed is always put in Session0 by these two tools.
  2. They always shoot the program to WinStat0's Default Desktop. If you want to RunAs on a Logon Desktop, they are not usable. Go to my previous articles, "COM Interface Hooking" and "Super PasswordSpy++", and you will find the application of RunAs on the Winlogon Desktop.
  3. They are console applications; you have to type anyway.
  4. They need the target user's password, even you are a Administrator member who can set passwords for anybody.
  5. The final token used to CreateProcessAsUser is obtained from the LogonUser API; you have no control over it (Advanced User Only).

There are, naturally, other non-open-source tools that can handle part of the above problems and I list several of them in the ending reference list. But, none of them, it seems, can handle all and that's why I present here a full open source RunAsEx as the the solution.

2. GUI Overview of RunAsEx

Figure 1. GUI of RunAsEx

As a whole, RunAsEx is a dialog-based Windows application written with plain C and Win32 SDK (the previous version that uses MFC is too meaty in size). It also accepts a command line so even a batch file can use it. If you do not have a command line with the target program or do not want to create a new desktop, using the mouse can satisfy all your needs without typing. It prepares a list of most used program to RunAsEx, saving your time searching them. You can even drag-and-drop the target program (including its Ink) onto it.

If you're running on Win2K (including Pro, Svr, Adv Svr, and a WTS Remote Desktop connecting to Win2K Svr), it can show you the plain text password cached in the winlogon process. If you're running in a Terminal Service environment, it permits you to RunAs a program in any existing session. It can make fake Tokens with any privileges, any user group (needing a slight modification in code, which I will explain later) tucked into it and use it to RunAs a program (domain users have certain limitation due to domain policy).

It will generate a command line for itself and copy it to the Clipboard so you save quite a lot typing (the CmdText button) when you want to start it from a batch file or a Command Window. It has four choices on how to RunAs, giving you enough control on how to RunAs. With a "(Password) Test" button, you can test the usr-pwd match before RunAs. A user list dialog lets you have an overview on the properties of the users on any machine/domain that you have rights to do so. A domain list dialog will show all shared net resources (it is an overkill for now, I have to admit). A privilege dialog is there to help enable some necessary privileges to save you opening MMC and clicking items one by one. A combo box containing all existing desktop on machine permits you RunAs on them and generate a new desktop on your request. And, finally, it will log the internal error to disk file or Event Log for your troubleshooting task.

The user will find both WinStation and Desktop are listed in Desktop Combo Box; only choose Desktop when RunAs. The red colored icons are WinStation-inaccessible from your program, usually due to security reasons (ERROR_ACCESS_DENIED 5). Some empty icons in the Most Used Program ListView control mean that program is not installed on the machine or not installed on the default position (e.g. you installed VS .Net on %Program Files%\MyVSnet instead of %Program Files%\Microsoft Visual Studio .NET (2003), I will not take care of this behavior by scanning your hard disk).

The check boxes give the user more control on RunAs when they use a Zw-type RunAs (the two bottom right-most buttons). Direct Launcher's Session check box will be enabled only when in WTS mode, including a Win2K server+ client terminal and WinXP/2003 FUS. When this box is checked, the target program's session ID will NOT be adjusted. We will talk about it in detail later. The next combo box, when in WTS, lists all session IDs available on this machine. It is recommended that this check box is unchecked. The Load User Profile check box is just as its name. You may think it has no sense when RunAs SYSTEM, but it is not true. Token From Caller is meaningful only when the user uses the Zw type; we will defer its explanation until later. By default, it is not recommended to check it. Keep User Privilege, when checked, queries the actual privilege held by the user; when unchecked, the created Token will have all privileges granted and enabled. By default, it is not checked. The "SetPrivilege" button will pop up a dialog to let the user grant/enable more privileges to themselves. Domain User, please note here, your domain group policy may affect this action; for example, reboot will deprive you of some privileges. So, please check the privileges even after you are re-logged.

3. Before You RunAsEx

Because there is no SetProcessToken API (well, a SetThreadToken does exist, which accepts only a Impersonate Token), sooner or later we need to call CreateProcessAsUser and pass it a primary token. So, where can we turn to get one? According to the documented API, it is LogonUser (LogonUserEx is basically the same as LogonUser except that it returns more information that can be obtained elsewhere). By undocumented API, it is ZwCreateToken exported by NTDLL.DLL. The most interesting thing about this ZwCreateToken is that no password is needed directly or indirectly. If you go to MSDN and check GetTokenInformation, you will see that there is quite a lot of information inside a token. And, that's almost all you need to call ZwCreateToken. To give you an direct and intuitive image what's inside a token, I suggest you spend some time on the ZTokenMan program I enclosed with the RunAsEx program; its GUI is like following:

Figure 2. ZTokenMan Program GUI

Push the button marked with the number in order (in Step 2, choose a process yourself) and if you experience no access problem, the token information of the process you chose in the combo box will be shown in the bottom right panel. By the way, you'd better enable four privileges: TCB, ChangeNotify, IncreaseQuota, and AssignPrimaryToken for ZTokenMan to try and launch the second instance of itself under the context of SYSTEM (which will help you peek some system process's token). Try to peek several processes and find their common points and difference; this will be very useful when you forge your own token with ZwCreateToken. If you are really reluctant to avoid enabling these privileges, use RunAsEx to launch ZTokenMan as SYSTEM. By the way, CPUs are really getting faster and faster. Can you see there is a rainbow bar in the rear position of the status bar? I put it there because dumping a token is a time-consuming job. I remembered those days this program running on my PII 350M machine with Win2k Server, the bar will rotate several steps (the longer query token information it takes, the more the bar rotates). Now, on my WinXP running on PIV 2G CPU, it rotates one step at the most (nSeconds vs 50 milliseconds!!!)!

Figure 3. ZAccessMan Program GUI

Another tool program I want you to spare some time to play with is ZAccessMan. I want you to clear the edit box marked with 1, push the log button marked with 2 to launch the second instance under context of SYSTEM, choose correct object type (in this figure we are interested with Desktop), and choose the specific object. Now, you choose to see the object's DACL with standard Windows security dialog or a homemade one like this:

Figure 4. Standard Security Editor Box

Figure 5. Security Editor Box from ZAccessMan

Readers should be aware here that there is some strange implementation in the Windows Station and Desktop, plus poorly documented MSDN in this part. For example, suppose you are a local Administrator and grant AND enable the TakeOwnership Privilege to yourself, you still can NOT get the DACL of WinStat0\Winlogon Desktop. I remembered that Keith Brown also mentioned this in his book Programming Windows Security, ISBN 0-201-60442-6, 2000 from Addison-Wesley, and it is still true on my Win2k Server + SP3, WinXP, and Win2003 nowadays. According to MSDN: A process must exist in a certain Windows Station and only one, and its thread(s) must be attached to a Desktop. Besides, CreateDesktop's first input parameter can NOT contain a backslash (\). You can make a simple program to prove they are not true like this (suppose the program is started on your WinStat0\Default Desktop):

#include <windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
   HWINSTA hWinStat = ::CreateWindowStation(_T("WinStatABC"), 0, 
      WINSTA_CREATEDESKTOP | WINSTA_ENUMDESKTOPS | WINSTA_ENUMERATE,
      NULL);
   if(hWinStat == 0) return 0;
   //::SetProcessWindowStation(hWinStat); //You can do this here

   //Following will give you "WinStat0\WinStatABCDeskABC" without
   //an error
   HDESK hDesk = CreateDesktop(
         _T("WinStatABC\DeskABC"), NULL, NULL, 0,
         DESKTOP_ENUMERATE | DESKTOP_CREATEWINDOW, NULL);
   if(hDesk == NULL) return 0;
   ::Sleep(50000);    //Sleep long enough to let you check with
                      //other tools
   return 0;
}

You see, first, the desktop name CAN contain a backslash; the API just ignores it. Second, when you set a process to some newly created Window Station, your thread is not attached to any desktop you specified at that moment unless it is a Default desktop associated with the new WinStat, but I cannot find any documentation stating that. One possibility is that CreateWindowStation internally calls CreateDesktop with the name "Default". What's more, say, if you want to RunAs the program on MyWinStat\MyDesktop, you have to do it like this: CreateWindowStation, SetProcessWindowStation to MyWinStat, CreateDesktop. You also should be aware that when moving Processes between WinStat, if any inside thread has windows, menus, or hooks attached to the old desktop, the API call will fail. That's why you will notice that RunAsEx will flash a little on the screen when you RunAs. RunAsEx has to dismiss the GUI Dialog, doing the hard work, and re-create the dialog back to the user.

But, CreateProcessAsUser needs the desktop from you, and you have to open the desktop's DACL first to make sure the target user has permission on that desktop. There are a bunch of permissions and you choose them by your concrete need, e.g. you need not DESKTOP_JOURNALRECORD unless you want to install such a hook. Naturally, you also need to do the same on the desktop's parent—WinStat. Following is what you will see when these requirements are not met:

Figure 6. Error Dialog When Application is Running on a certain User Context

You can turn to ZAccessMan to reproduce this: Open WinStat0\Default desktop, pop up one of the security dialog (standard or my homemade one), (Note: this experiment need you re-log after it, so save all opened document at this point.) Delete all ACEs and press OK. Now try to start any application, and you will see this dialog. You are very likely to choose re-logging instead of editing the Default Desktop DACL.

4. Experiments on Token and Desktop

Now, use ZAccessMan to open the standard security dialog on WinStat0\Default. It may show something like this:

  1. Administrators
  2. RESTRICTED //Stands for it is a token created by CreateRestrictedToken
  3. S-1-5-5-0-XXX
  4. SYSTEM

Go to my security dialog and you will get the SID of these items, as in the following:

  1. S-1-5-32-544
  2. S-1-5-12 (from SECURITY_RESTRICTED_CODE_RID)
  3. S-1-5-5-0-XXX
  4. S-1-5-18

Start ZTokenMan now, and dump any common application (e.g. start Notepad, and dump its token), and you will find what S-1-5-5-0-XXX is; it is the Logon SID inside the token. Readers, please be aware here: Here comes an difference between using token generated by LogonUser and ZwCreateToken. The former API will provide a Logon SID for you while the latter can not. You can deem Logon SID as an instance of you. In other words, Logon SID, which is given to you from the moment you log on that machine, can be traced to identify you. You can Log on multiple times, thru programs or terminal clients; each time you get a different Logon SID. With this point in mind, you can understand why, when using LogonUser, you only grant access to that Logon SID in the target WinStat and Desktop; when using ZwCreateToken, you have to grant access to the actual user.

One thing you may find in RunAsEx is that it never reverts the DACL back. It is by design. In Keith Brown's cmdasusr, he starts a thread to wait the target process until it exits by the WaitForSingleObject(hProcess). When the process is over, the thread reverts the DACL back. RunAsEx can do it, BUT, doing so means two things:

  1. RunAsEx has to be kept running when the target is running
  2. The target process doesn't spawn child process.

What's worse is that multiple processes can overlap in time span, and modifying the DACL of the Desktop will affect others. Take an example, I start program A as Alice on Desktop Default, I modify Default's DACL by adding Alice positive ACE; and then wait until A is done. Then, I start program B as Alice too on Default; this time, I checked Default's DACL and find nothing needs to change. After some time, program A spawns program C and exits; the tracking thread reverts the DACL back by deleting that ACE. Then, programs B and C have problems because they lose access to Default. You may say that we could solve all these problems by setting up a global lookup table, using injection DLL monitor process spawn. That's true, you can program anything, but it makes things too complicated to say it is a RunAsEx, not a SpyRunAsEx.

So, to fire-and-forget the RunAs-ed program, the DACL will not be reverted, and in most cases it is not a problem. Anyway, you have ZAccessMan, which could help to modify the DACL of WinStat and the Desktop by mouse clicking.

For the sake of some readers who just touched the WinStat and Desktop, I want to emphasize that WinStat0 and its three Default, WinLogon, and Disconnect desktops are created even before you see the logon screen (WinLogon, which asks for a password from you to log on). After you input the usr-pwd pair, the system MODIFYs WinStat0 and WinStat0\Default's DACL (I know there must be some people thinking the Default desktop is created when you logon and deleted when you logoff); one of the modifications is adding your Logon SID ACE to the DACL. When you log off, your Logon SID becomes obsolete and the system deletes its ACE from the DACL of WinStat0 and WinStat0\Default. Because RunAsEx does NOT revert the DACL back, if you want the desktop to recover to its original state and do not want to use another tool, rebooting the machine is your only choice.

Now, let's turn to Token and see what's inside. Normally, you wouldn't worry about it when you call LononUser, for only a logon type makes sense. To RunAs a process, which means assigning the token to a process instead of a thread, it must be a primary token. The following table shows you different logon types and their effect:

Flag OS Primary Token Special Group SID Included (SID, Name, Domain) Comment
INTERACTIVE   y S-1-5-4, INTERACTIVE, NT AUTHORITY The logged-on user must have been granted/enabled SeInteractiveLogonRight NT Rights (not the caller!!!).

The most-used logon type, User will be granted a unique Logon SID. Tokens received with this logon will be cached with the system. This means that the local system can lose connection with the authenticating machine but still make future successful calls to LogonUser using cached credentials.

After ImpernateLoggedOnUser (or DuplicateTokenEx, SetThreadToken), the impersonate thread can access network resources.

BATCH   y S-1-5-3, BATCH, NT AUTHORITY The logged-on user must have been granted/enabled SeBatchLogonRight NT Rights (not the caller!!!).

A unique Logon SID will be issued

Tokens received with this logon type are not cached, increasing the performance of LogonUser and making the logon type appropriate for high-performance servers.

After ImpernateLoggedOnUser (or DuplicateTokenEx, SetThreadToken), the impersonated thread can access network resources.

SERVICE   y S-1-5-6, SERVICE, NT AUTHORITY The logged-on user must been granted/enabled SeServiceLogonRight NT Rights (not the caller!!!).

A unique Logon SID will be issued.

This token will be cached for future calls to LogonUser if the machine loses connection to the authenticating agent. LogonUser returns a primary token.

After ImpernateLoggedOnUser (or DuplicateTokenEx, SetThreadToken), the impersonate thread can access network resources.

*Note: You could (and should) deem the SERVICE and BATCH types as the same (except the cache); there is really not much difference, although it is the System SCM that takes care of Service logon and COM SCM that take cares of Batch Logon.

UNLOCK   y S-1-5-4, INTERACTIVE, NT AUTHORITY This intended for GINA DLLs only, but actual effect the the same as interactive!
NETWORK   n   The logged-on user must have been granted/enabled SeNetworkLogonRight NT Rights (not the caller!!!).

Tokens received with this logon type are not cached. In addition, this token will be an impersonation token and a "network token."

The impersonate thread can access network resources.

NETWORK_CLEARTEXT Win2K+ n   The logged-on user must have been granted/enabled SeNetworkLogonRight NT Rights (not the caller!!!).

This logon type returns an impersonation token while preserving a copy of the trustee's credentials so that network access is possible using the resulting token. The token has to be duplicated to a primary token before it can be used in calls to CreateProcessAsUser.

The impersonate thread can access network resources.

NEW_CREDENTIALS Win2K+ y   This logon type makes a copy of the calling thread's process token and adds a second identity to the token. This second identity will be the token's identity for all network access, whereas the token's identity for the local machine will remain the same as that of the original token. This makes the LOGON32_LOGON_NEW_CREDENTIALS unique in that it uses an existing token to build a new token with extra credentials. For an example of this logon type, see the RunAs.exe utility provided with Windows 2000. The "/NetOnly" switch uses the LOGON32_LOGON_NEW_CREDENTIALS logon type to create a token for the new process. The resulting token is a primary token.

The impersonate thread can access network resource.

List of Common Group SID inside User Process (non-system Process)
  • SID: S-1-1-0
    Use: Well-Known Group SID
    Name: Everyone
    Domain Name:
  • SID: S-1-2-0
    Use: Well-Known Group SID
    Name: LOCAL
    Domain Name:
  • SID: S-1-5-11
    Use: Well-Known Group SID
    Name: Authenticated Users
    Domain Name: NT AUTHORITY
  • SID: S-1-5-18
    Use: User SID
    Name: SYSTEM
    Domain Name: NT AUTHORITY

List of Common Group SID inside System Process (lsass, winlogon, rpcss, csrss)

  • SID: S-1-1-0
    Use: Well-Known Group SID
    Name: Everyone
    Domain Name:
  • SID: S-1-5-18
    Use: User SID
    Name: SYSTEM
    Domain Name: NT AUTHORITY

GUI-Based RunAsEx

Several points need to be cleared here, which could help some readers:

  • Both Privilege and NT Rights have three states to a user: Not Granted, Granted but Not Enabled, and Enabled. It is important to realize it, for usually granting is not enough, you must enable it to use it.
  • Children Process inherits a token from the Parent Process if no special action is taken. Take RunAsEx as an example, I need TCB privileges; if you do not have them, I call Lsa family API to grant one. Why do I log you off? explorer.exe (the shell) is your parent process which is started before I make the grant. Your modification will not take effect unless you restart explorer.exe. (Well, you can do that by killing the explorer and starting one from the Task Manager, which is too harsh.)
  • To call LogonUser, to be safe, you need TCB, ChangeNotify; to CreateProcessAsUser you need IncreaseQuota, AssignPrimaryToken; I add TakeOwnership for I want more to cope with Winstat\Desktop. I need CreateToken because I call ZwCreateToken
  • When logging on as Service and Batch, the user passed to LogonUser need have SeServiceLogonRight and SeBatchLogonRight NT Rights, not the caller; the same requirement on Interactive and Network is SeInteractiveLogonRight and SeNetworkLogonRight.
  • Domain User, if you are not Domain Administrator, please check your domain policy; maybe you can not obtain some privilege.

Following table is a mapping Token-Elements and how to obtain them:

Name API to Call Function Name in RunAsEx Source <Filename.cpp>
User SID LookupAccountName PSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) <CoreCode.CPP>
Group SIDs AllocateAndInitializeSid

NetUserGetLocalGroups

NetUserGetGroups

LookupAccountName

PTOKEN_GROUPS CreateTokenGroups(SID_AND_ATTRIBUTES* lpPSIDGroupsAttr)

PSID* QueryLocalGroupSIDs(LPCTSTR pszUserName)

PSID* QueryNetGroupSIDs(LPCTSTR pszUserName, LPCTSTR pszDomainName)

PSID GetEveryoneSID()

PSID GetAuthenticatedUsersSID()

PSID GetInteractiveSID()

...

<CoreCode.CPP>

Logon SID (inside Group SIDs) No Way To Create From Scratch

(but may be able to steal from elsewhere)

always in format S-1-5-5-0-XXXX

SYSTEM process (winlogon, lsass, etc...) does not have this

User processes and other system-level processes (such as svchost running under local_service and network_service) have this member.

A anonymous token token (with authenticationID = 998) does not have this member. Check http://www.develop.com/.../whatis_alogonsession.html

Privileges LookupPrivilegeValue PTOKEN_PRIVILEGES CreateTokenPriv(DWORD& dwPrivGranted, LPCTSTR* lpszPriv, BOOL bGrantEnableAll) <CoreCode.CPP>
Token Owner LookupAccountName PSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) <CoreCode.CPP>
Token Primary Group AllocateAndInitializeSid Same as Group SIDs
Token Default DACL AllocateACE, InitializeAcl, AddAce... Importable from caller; RunAsEx uses NULL DACL by default
Token Source   An 8-char string; RunAsEx uses "RunAsEx+"
LUID TokenId,
LARGE_INTEGER ExpirationTime,
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
LUID ModifiedId
AllocateLocallyUniqueId PTOKEN_STATISTICS CreateTokenStatistics(
LUID* lpTokenId, //Optional
LUID* lpAuthenticationId, //Optional
LARGE_INTEGER* lpExpirationTime, //Optional
TOKEN_TYPE* lpTokenType, //Primary
SECURITY_IMPERSONATION_LEVEL* lpImpersonationLevel, //Only When ImpersonteToken
DWORD* lpDynamicCharged, //Optional
DWORD* lpDynamicAvailable, //Optional
DWORD* lpGroupCount, //Mandatory
DWORD* lpPrivilegeCount, //Mandatory
LUID* lpModifiedId //Optional

<CoreCode.CPP>

Note: In the real world, all LUID members here set 0 as HighPart.

RunAsEx will assign random values to these members because they are not critically important.

TOKEN_TYPE TokenType   N/A Only Meaningful When Impersonate Token
DWORD DynamicCharged   Always 500
DWORD DynamicAvailable   Undecided; 420 by default
DWORD GroupCount   Checks count of Group SIDs
DWORD PrivilegeCount   Checks count of Privileges
LUID AuthenticationId The first 1000 LUIDs are reserved (0x3E7 = 999).
SYSTEM_LUID { 0x3E7, 0x0 }
ANONYMOUS_LOGON_LUID { 0x3e6, 0x0 }
LOCALSERVICE_LUID { 0x3e5, 0x0 }
NETWORKSERVICE_LUID { 0x3e4, 0x0 }
Caller pass. * Check RunAsEx desktop combo box and you will find Service-0x0-3e7$, Service-0x0-3e5$, and Service-0x0-3e4$. These are service logon sessions that get their names from this authenticate ID.

Note: This is different from Logon SID S-1-5-5-0-XXXX. I mean AuthenticationId.LowPart != XXXX.

It is the Logon Session LUID. Use the ZTokenMan check system, and you will see its authentication ID is 999. It is just the Logon Session associated with WinStat Service-0x0-3e7$. Use PrccessExplorer to check it!

All Tokens I met AuthenticationId.HighPart = 0.

RunAsEx uses 999 as the default when the caller does not pass into.

This member is closely related to the WinStat name.

Note: AuthenticationId is closely related to the WinStat Name when you pass the token to CreateProcessAsUser with an Empty Desktop Name "". Take this as an example: Configure a NT Service running as a user (instead of the default system); it should be put into a WinStation called "Service-0x0-XXX$". Go to ZTokenMan and have a look at its authenticationId; it should be the same value.
Note: The word Session is overused indeed. When I mention Logon Session SID, it is a SID embedded in Token. When I say Logon Session LUID, it is a LUID (64bits) indentifying a logon session and shared, usually, by some processes. When I Session ID, when in a WTS environment, it is a 0-based integer showing the current connection's order.

5. Implementation Explanation

Now, let's review what we need to do to RunAs and the corresponding API:

  1. Check If Caller is from Local Admin Group Member—SetTokenInformation, Enumerate Group SID to see if S-1-5-32-544 (alias S-1-5-32-544) there or not
  2. If need a user profile, load userenv.dll now—LoadLibrary, GetProcAddress
  3. Enable All Privileges needed—LookupPrivilegeValue, OpenProcessToken, AdjustTokenPrivileges
  4. Query Current Desktop Name—GetUserObjectInformation
  5. If the Target Desktop in Other WinStat, Yes—SetProcessWinStat (after closing All Windows, Hooks of Current Process)
  6. If the Desktop is non-existing—OpenWindowStation, CreateWindowStation, CreateDesktop
  7. If the user wants to use LogonUser, call it to get Token—LogonUser
  8. If the user has no password, Zw—ZwCreateToken
  9. If you want to launch in another session, change session ID—SetTokenInformation
  10. If the user wants to shoot in NT Service—CreateService, OpenSCManager...
  11. User Token Group SIDs to modify target desktop DACL, get rid of all deny ACE, add positive ACE—AllocateACE, InitializeAcl, AddAce...
  12. CreateProcessAsUser it
  13. If RunAs inside NT Service—DeleteService, ...

Although listing all the code (>3000 lines, compact with reasonable comments) seems daunting, I give the key code here so you can get something instantly without downloading, decompressing, and opening. Please note, you must handle exceptions if something wrong happened, especially in your NT service handler; otherwise, you will be stuck miserably (you can kill process, and modify the Registry manually if you like).

5.1. RunAsUser Function (showing handling of WinStat\Desktop affairs)

BOOL RunAsUser(
  LPTSTR pszEXE, LPTSTR pszCmdLine, //in, Target Program and Command
                                    //Line
  LPTSTR pszDomainName, LPTSTR pszUserName, LPTSTR pszPassword, //in
  LPTSTR pszDesktop, //in, Must Not be NULL, can be "NULL", "EMPTY",
                     //"WinStat\Desktop"
  BOOL bCreateTokenDirectly, //in, True: ZwCreateToken;
                             //FALSE: LogonUser
  DWORD dwSession,   //in, -1: No WTS; n (0-based): Target SessionID
  BOOL bLoadProfile, //in, TRUE: Load User Profile
  BOOL bCopyTokenPropFromCaller, //in, TRUE: All Token Information
                                 //Use Caller Process's
  BOOL bKeepPriv, //in, TRUE: Use Only Privileges User Holding;
                  //FALSE: Set All Privileges
  DWORD dwLogonType, //in, Same As LogonUser 
  DWORD dwLogonProvider //in, Reserved
)
{
  //LocalSystem no profile
  if(pszUserName == NULL && bLoadProfile) return FALSE;

  TCHAR szSrcWinStat[MAX_PATH];
  TCHAR szSrcDesktop[MAX_PATH];
  TCHAR szWinStat[MAX_PATH];
  TCHAR szDesktop[MAX_PATH];

  HWINSTA hSrcWinStat = ::GetProcessWindowStation();
  HDESK   hSrcDesktop = ::GetThreadDesktop(::GetCurrentThreadId());
  DWORD dwFakeLen;

  //Get Target User Token --> his.her SID
  HANDLE hToken   = NULL;

  BOOL   fProcess = FALSE;
  BOOL   fSuccess = FALSE;
  PROCESS_INFORMATION pi = {NULL, NULL, 0, 0};
  STARTUPINFO si;

  PSECURITY_DESCRIPTOR pSD = NULL; 
  BOOL bRet = FALSE;

  //save current win station and desktop to make return journey
  //smoothly if ...
  HWINSTA hwinstaOld = NULL;
  HDESK hdeskOld     = NULL;
  HWINSTA hwinstaNew = NULL;
  HDESK hdeskNew     = NULL;

  BOOL  bWinStatCreated = FALSE;
  BOOL  bDeskCreated    = FALSE;

  BOOL bSameWinStat = FALSE;
  BOOL bSameDesktop = FALSE;

  HANDLE hTokenSelf = NULL;
  if(!OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY,
     &hTokenSelf))
    err;

  PROFILEINFO profInfo = { sizeof(profInfo), 0, pszUserName };
  void* pEnvBlock = NULL;

  CreateEnvironmentBlock  _CreateEnvironmentBlock;
  DestroyEnvironmentBlock _DestroyEnvironmentBlock;
  LoadUserProfileW        _LoadUserProfileW;
  UnloadUserProfile       _UnloadUserProfile;
  HMODULE hEvnModule = NULL;
  if(bLoadProfile && (hEvnModule = LoadLibrary(_T("userenv.dll")))
     == NULL)
    err;

  if(hEvnModule)
  {
      //this unlikely fails
      _CreateEnvironmentBlock =
          reinterpret_cast<CreateEnvironmentBlock>
         (GetProcAddress(hEvnModule, "CreateEnvironmentBlock"));
      _DestroyEnvironmentBlock =
         reinterpret_cast<DestroyEnvironmentBlock>
         (GetProcAddress(hEvnModule, "DestroyEnvironmentBlock"));
      _LoadUserProfileW = reinterpret_cast<LoadUserProfileW>
         (GetProcAddress(hEvnModule, "LoadUserProfileW"));
      _UnloadUserProfile = reinterpret_cast<UnloadUserProfile>
         (GetProcAddress(hEvnModule, "UnloadUserProfile"));
      if(!_CreateEnvironmentBlock || !_DestroyEnvironmentBlock ||
         !_LoadUserProfileW || !_UnloadUserProfile)
         err;
  }

  BOOL bNullDesktop  = FALSE;
  BOOL bEmptyDesktop = FALSE;
  //a long try_finally block
  __try
  {
     if(!IsAdministrorMember()) err;

     if(!::EnablePrivilege(L"SeTakeOwnershipPrivilege", TRUE))
         err;

     EnablePrivilege(L"SeTcbPrivilege", TRUE);
     EnablePrivilege(L"SeChangeNotifyPrivilege", TRUE);
     EnablePrivilege(L"SeIncreaseQuotaPrivilege", TRUE);
     EnablePrivilege(L"SeAssignPrimaryTokenPrivilege", TRUE);
     EnablePrivilege(L"SeCreateTokenPrivilege", TRUE);

     //get caller thread's WinStat and Desktop,
     //used to decide whether SetProcessWinStat is needed
     bRet = GetUserObjectInformation(
        hSrcWinStat,              // handle to object
        UOI_NAME,                 // type of information to retrieve
        (LPVOID)szSrcWinStat,     // information buffer
        MAX_PATH * sizeof(TCHAR), // size of the buffer
        &dwFakeLen                // receives required buffer size
     );
     if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;

     bRet = GetUserObjectInformation(
        hSrcDesktop,              // handle to object
        UOI_NAME,                 // type of information to retrieve
        (LPVOID)szSrcDesktop,     // information buffer
        MAX_PATH * sizeof(TCHAR), // size of the buffer
        &dwFakeLen                // receives required buffer size
     );
     if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;

     if(pszDesktop == NULL) //|| _tcsstr(pszDesktop, _T("\\"))
                            //== NULL)
     {
        ::lstrcpy(szWinStat, szSrcWinStat);
        ::lstrcpy(szDesktop, szSrcDesktop);
     }
     else if(::lstrcmpi(pszDesktop, _T("NULL")) == 0)
     {
        bNullDesktop = TRUE;
     }
     else if(::lstrcmpi(pszDesktop, _T("EMPTY")) == 0)
     {
        bEmptyDesktop = TRUE;
     }
     else
     {
        //check the integrity of the pszDesktop
        TCHAR* pSlash1 = _tcsstr(pszDesktop, _T("\\"));
        TCHAR* pSlash2 = _tcsrchr(pszDesktop, TCHAR('\\'));
        if(pSlash1 != pSlash2) return FALSE;

        TCHAR* psz = (TCHAR*)pszDesktop;
        ::lstrcpyn(szWinStat, (LPCTSTR)pszDesktop, pSlash1
                   - psz + 1);
        ::lstrcpy(szDesktop, pSlash1 + 1);
     }

     if(!bNullDesktop && !bEmptyDesktop)
     {
        if(::lstrcmp(szWinStat, szSrcWinStat) == 0)   //same winstat
        {
           bSameWinStat = TRUE;
           if(::lstrcmp(szDesktop, szSrcDesktop) == 0) //same desktop
               bSameDesktop = TRUE;
           else
               bSameDesktop = FALSE;
        }
        else
        {
           bSameWinStat = FALSE;
           bSameDesktop = FALSE;
        }

        if(!bSameDesktop || !bSameWinStat)
        {
           //for quick reversion
           hwinstaOld = GetProcessWindowStation();
           hdeskOld = GetThreadDesktop(GetCurrentThreadId()); 
        }
     }

     if(!bNullDesktop && !bEmptyDesktop && !bSameWinStat)
     {
         //To Test The existing of a WinStat, you can
         //1. EnumWindowStations and compare the string returned
         //2. Call OpenWindowStation with WINSTA_ENUMERATE and test
         //   the handle

         //Way 2:
         //Because the caller func is from a Admin Grp Member,
         //this call is always OK unless WinStat not exists
         ::SetLastError(ERROR_SUCCESS);
         hwinstaNew = ::OpenWindowStation(szWinStat, FALSE,
                                          WINSTA_ENUMERATE);
         if(!hwinstaNew)
         {
            //winstat not existing
            ::CloseWindowStation(hwinstaNew);
            bWinStatCreated = TRUE;
         }
         else if(::GetLastError() == ERROR_ACCESS_DENIED)
            err;
         else
         {
            bWinStatCreated = FALSE;
         }
    }

    if(pszUserName == NULL || ::lstrlen(pszUserName) == 0 )
      //LogOn as LocalSystem
    {
         if(!bCreateTokenDirectly)
         {
            hToken = GetLSAToken();
            if(hToken == NULL) err;
         }
         else
         {
            if(!CreateTokenDirectlyEx(hToken,
                  bCopyTokenPropFromCaller,
                  pszUserName, pszDomainName,
                  "RunAsEx+", NULL, NULL, NULL, TRUE, bKeepPriv,
                  FALSE, NULL, NULL, NULL, dwLogonType,
                  dwLogonProvider) ||
               hToken == NULL) err;
         }
    }
    else
    {
         if(!bCreateTokenDirectly && !LogonUser(pszUserName,
            pszDomainName,
            pszPassword, dwLogonType, dwLogonProvider, &hToken))
         {
             err;
         }
         else if(bCreateTokenDirectly &&
             !CreateTokenDirectlyEx(hToken,
                   bCopyTokenPropFromCaller,
                   pszUserName, pszDomainName,
                   "RunAsEx+", NULL, NULL, NULL, TRUE, bKeepPriv,
                   FALSE, NULL, NULL, NULL, dwLogonType,
                   dwLogonProvider))
         {
             err;
         }
    }

    //Set Token Seesion ID
    if(dwSession != (DWORD)-1)
    {
        //need to set?
        DWORD dwSelfSession;
        if(ProcessIdToSessionId(::GetCurrentProcessId(),
                                &dwSelfSession))
        {
            if(dwSelfSession != dwSession)
            {
               if(!SetTokenInformation(hToken, TokenSessionId,
                  &dwSession, sizeof(DWORD)))
               {
                  if(GetLastError() == ERROR_ACCESS_DENIED)
                  {
                      //try again
                      if (!ModifySecurity(hToken, TOKEN_DUPLICATE
                            | TOKEN_ASSIGN_PRIMARY
                            | TOKEN_QUERY | TOKEN_ADJUST_SESSIONID))
                      {
                          err;
                      }
                      if(!SetTokenInformation(hToken,
                         TokenSessionId,
                         &dwSession, sizeof(DWORD)))
                          err;
                  }
               }
            }
         }
    }
    pSD = HeapAlloc(GetProcessHeap(), 0,
                    SECURITY_DESCRIPTOR_MIN_LENGTH);
    if(pSD == NULL) err;

    // We now have an empty security descriptor
    if (!InitializeSecurityDescriptor(pSD,
        SECURITY_DESCRIPTOR_REVISION))
       err;

    if(!SetSecurityDescriptorDacl(pSD, TRUE, NULL, FALSE))
       err;

    // Then we point to our SD from a SECURITY_ATTRIBUTES structure
    SECURITY_ATTRIBUTES sa = {0};
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = pSD;

    if(!bNullDesktop && !bEmptyDesktop && bWinStatCreated)
    {
        //Create a WinStat and naturally a new desktop
        //First make it a NULL DACL :=)
        hwinstaNew = CreateWindowStation(szWinStat, 0,
                                         MAXIMUM_ALLOWED, &sa);
        //using default security is good here since we are the owner
        if(!hwinstaNew) __leave;
        //We must SetProcessWindowStation when new desktop
        //needs created on a different WinStat
        if(!SetProcessWindowStation(hwinstaNew))
        {
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }
        hdeskNew = ::CreateDesktop(szDesktop, NULL, NULL, 0,
        //or DF_ALLOWOTHERACCOUNTHOOK
             MAXIMUM_ALLOWED, &sa);
        if(hdeskNew == NULL)
        {
           SetProcessWindowStation(hwinstaOld);
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }

        if(!AllowTokenFullAccessToObject(hTokenSelf, hwinstaNew,
          SE_WINDOW_OBJECT, _T("WinStat")))   err;

        if(!AllowTokenFullAccessToObject(hTokenSelf, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop"))) err;

        if(!AllowTokenFullAccessToObject(hToken, hwinstaNew,
           SE_WINDOW_OBJECT, _T("WinStat"))) err;

        if(!AllowTokenFullAccessToObject(hToken, hdeskNew,
           SE_WINDOW_OBJECT, _T("Desktop"))) err;
   }
   else if(!bNullDesktop && !bEmptyDesktop)//the WinStat exists
   {
        hwinstaNew = OpenWindowStation(szWinStat, FALSE,
                                       READ_CONTROL | WRITE_DAC);
        if(hwinstaNew == NULL) err;

        //give self such rights --
        //WINSTA_CREATEDESKTOP WINSTA_ENUMDESKTOPS
        if(!bSameWinStat && !SetProcessWindowStation(hwinstaNew))
        {
           ::CloseWindowStation(hwinstaNew);
           hwinstaNew = NULL;
           err;
        }
        //if bSameWinStat --
        //if not          -- SetProcessWindowStation called

        //check the desk
        //Does the desktop exists?
        ::SetLastError(ERROR_SUCCESS);
        hdeskNew = OpenDesktop(szDesktop, 0,
           //DF_ALLOWOTHERACCOUNTHOOK,
           FALSE, READ_CONTROL | WRITE_DAC);
        if(hdeskNew == NULL)
        {
           if(::GetLastError() == ERROR_ACCESS_DENIED)
              err;
           else     //not existing
           {
              bDeskCreated = TRUE;
              hdeskNew = ::CreateDesktop(szDesktop, NULL, NULL, 0,
              //or DF_ALLOWOTHERACCOUNTHOOK
                 MAXIMUM_ALLOWED, &sa);
              if(hdeskNew == NULL)  err;
           }
        }
        else
           bDeskCreated = FALSE;
        //modify DACL of the desk
        if(!AllowTokenFullAccessToObject(hTokenSelf, hwinstaNew, 
         SE_WINDOW_OBJECT, _T("WinStat")))
           err;

        if(!AllowTokenFullAccessToObject(hTokenSelf, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop")))
           err;

        if(!AllowTokenFullAccessToObject(hToken, hwinstaNew,
          SE_WINDOW_OBJECT, _T("WinStat")))
           err;

        if(!AllowTokenFullAccessToObject(hToken, hdeskNew,
          SE_WINDOW_OBJECT, _T("Desktop")))
           err;
    }
      
    //ready to launch
    if(bLoadProfile)
    {
        // load the user profile
        // PROFILEINFO profInfo = { sizeof(profInfo), 0,
        //                          pszUserName };
        if (!_LoadUserProfileW( hToken, &profInfo))  err;

        // set up an environment block
        //void* pEnvBlock = NULL;
        if(!_CreateEnvironmentBlock( &pEnvBlock, hToken, FALSE)) err;
    }

    si.cb  = sizeof(si);
    //Desktop or WinStat\Desktop
    //MSDN Error Here! Note: to be 100% safe use the latter!!!
    TCHAR szFullDesktop[MAX_PATH];
    ::lstrcpy(szFullDesktop, szWinStat);
    ::lstrcat(szFullDesktop, _T("\\"));
    ::lstrcat(szFullDesktop, szDesktop);
    //si.lpDesktop   = bSameWinStat && bSameDesktop ? NULL :
    //                 szFullDesktop; 

    if(!bNullDesktop && !bEmptyDesktop)
    {
       si.lpDesktop   = szFullDesktop;
    }
    else if(bNullDesktop)
    {
       si.lpDesktop   = NULL;
    }
    else    //if bEmptyDesktop
    {
       si.lpDesktop = _T("");
    }

    si.lpTitle     = NULL;
    si.dwFlags     = 0;
    si.cbReserved2 = 0;
    si.lpReserved  = NULL;
    si.lpReserved2 = NULL;

    TCHAR szLocalCmdLine[2 * MAX_PATH];
    ::SetLastError(ERROR_SUCCESS);

    ::lstrcpy(szLocalCmdLine, _T("\""));
    ::lstrcat(szLocalCmdLine, pszEXE);
    ::lstrcat(szLocalCmdLine, _T("\""));
    ::lstrcat(szLocalCmdLine, _T(" "));
    if(pszCmdLine)
       ::lstrcat(szLocalCmdLine, pszCmdLine);
    //LPCTSTR-->LPTSTR
    fProcess = CreateProcessAsUser(hToken, NULL,
              (LPTSTR)szLocalCmdLine, 
       &sa, &sa,    //lpProcessAttributes, lpThreadAttributes
       FALSE,
       bLoadProfile ? CREATE_UNICODE_ENVIRONMENT : 0,
       bLoadProfile ? pEnvBlock : NULL,
       NULL, &si, &pi);

    if(!fProcess)
       err;
    fSuccess = TRUE;
  }
  __finally
  {
    if(bLoadProfile && pEnvBlock)
    // free the environment block
    _DestroyEnvironmentBlock(pEnvBlock);

    if(bLoadProfile && hToken && profInfo.hProfile)
    // unload the user profile
       _UnloadUserProfile( hToken, profInfo.hProfile );

    if(pSD) HeapFree(GetProcessHeap(), 0, pSD);
    if(hToken) CloseHandle(hToken);

    if(hwinstaOld) ::SetProcessWindowStation(hwinstaOld);
    if(hdeskOld) ::SetThreadDesktop(hdeskOld);

    //Do NOT DO THAT!
    //if(hdeskNew) ::CloseDesktop(hdeskNew);
    //if(hwinstaNew) ::CloseWindowStation(hwinstaNew);

    if (fProcess)
    {
       CloseHandle(pi.hProcess);
       CloseHandle(pi.hThread);
    }

    if(hdeskOld)   ::CloseDesktop(hdeskOld);
    if(hwinstaOld) ::CloseWindowStation(hwinstaOld); 
  }
  return(fSuccess);
}

GUI-Based RunAsEx

5.2. CreatePureSystemToken (showing usage of ZwCreateToken)

//LocalSystemToken will have 3 group item -- 
//S-1-5-32-544 --- Administrators BUILTIN
//S-1-1-0      --- Everyone
//S-1-5-11     --- Authenticated Users
//its (token) owner is 
//S-1-5-18     --- SYSTEM
//All priv will be ganted and enabled for simplicity
//Default DACL like following
//ACCESS ALLOWED   NT AUTHORITY/SYSTEM
//ACCESS_MASK:  00010000000000000000000000000000
//   GENERIC_ALL
//
//ACCESS ALLOWED   BUILTIN/Administrators
//ACCESS_MASK:  10100000000000100000000000000000
//   READ_CONTROL
//   GENERIC_EXECUTE
//   GENERIC_READ
//the token source will be "*SYSTEM*"
BOOL CreatePureSystemToken( /*out*/ HANDLE &hToken)
{
  hToken = NULL;
  BOOL bRet = FALSE;

  if(!EnablePrivilege(SE_CREATE_TOKEN_NAME, TRUE))
    err; 
  PSID lpSidOwner = ::GetLocalSystemSID();
  if(lpSidOwner == NULL)
    err;{
  NTSTATUS ntStatus = 0;

  //SID_AND_ATTRIBUTES grpSIDAttr[3];    //SYSTEM Token has 3 group
                                         //SIDs
  PTOKEN_GROUPS lpGroupToken = NULL;
  DWORD dwGroupNumber = 3;

  PTOKEN_PRIVILEGES lpPrivToken = NULL;
  DWORD dwPrivGranted = 0;

  TOKEN_OWNER ownerToken;
  ownerToken.Owner = NULL;

  TOKEN_PRIMARY_GROUP primGroupToken;
  primGroupToken.PrimaryGroup = ::GetLocalSystemSID(); 
  if(primGroupToken.PrimaryGroup == NULL)
  {
     ::FreeSid(lpSidOwner);
     err;
  }
  PTOKEN_DEFAULT_DACL  lpDaclToken = NULL;

  SID_AND_ATTRIBUTES lpEachGrp[4];
  lpEachGrp[0].Sid = NULL;
  lpEachGrp[1].Sid = NULL;
  lpEachGrp[2].Sid = NULL;

  __try
  {
    lpEachGrp[0].Sid = ::GetAliasAdministratorsSID();
    lpEachGrp[0].Attributes = SE_GROUP_ENABLED
      | SE_GROUP_ENABLED_BY_DEFAULT
      | SE_GROUP_OWNER;
    lpEachGrp[1].Sid = ::GetEveryoneSID();
    lpEachGrp[1].Attributes = SE_GROUP_ENABLED
      | SE_GROUP_ENABLED_BY_DEFAULT
      | SE_GROUP_MANDATORY;
    lpEachGrp[2].Sid = ::GetAuthenticatedUsersSID();
    lpEachGrp[2].Attributes = SE_GROUP_ENABLED
      | SE_GROUP_ENABLED_BY_DEFAULT
      | SE_GROUP_MANDATORY;
    lpEachGrp[3].Sid = NULL;
    lpEachGrp[3].Attributes = 0;
    if(lpEachGrp[0].Sid == NULL || lpEachGrp[1].Sid == NULL ||
       lpEachGrp[2].Sid == NULL)
    {
       err;
    }
    lpGroupToken = ::CreateTokenGroups(lpEachGrp);
    if(lpGroupToken == NULL) err;

    lpPrivToken = ::CreateTokenPriv(dwPrivGranted, NULL, TRUE);
    if(lpPrivToken == NULL) err; 

    ownerToken.Owner = ::GetLocalSystemSID();
    if(ownerToken.Owner == NULL) err;

    TOKEN_USER userToken;
    userToken.User.Sid = ::GetLocalSystemSID();
    userToken.User.Attributes = 0;    //no use
    if(userToken.User.Sid == NULL) err;

    bRet = AllocateLocallyUniqueId(&luid);    //this luid only
                                              //unique in this
                                              //session
    if(!bRet) err;

    TOKEN_SOURCE sourceToken = {{'*', 'S', 'Y', 'S', 'T', 'E',
                                 'M', '*'},
                      {luid.LowPart, luid.HighPart}};

    // Allocate the System Luid.  The first 1000 LUIDs are reserved.
    // Use #999 here (0x3E7 = 999)
    //
    //#define SYSTEM_LUID                     { 0x3E7, 0x0 }
    //#define ANONYMOUS_LOGON_LUID            { 0x3e6, 0x0 }
    //#define LOCALSERVICE_LUID               { 0x3e5, 0x0 }
    //#define NETWORKSERVICE_LUID             { 0x3e4, 0x0 }

    LUID authid = SYSTEM_LUID;

    lpStatsToken = ::CreateTokenStatistics(NULL, NULL, NULL, NULL,
    NULL, NULL, NULL, &dwGroupNumber, &dwPrivGranted, NULL);
    if(lpStatsToken == NULL) err;

    //typedef enum _SECURITY_IMPERSONATION_LEVEL {
    //SecurityAnonymous,
    //SecurityIdentification,
    //SecurityImpersonation,
    //SecurityDelegation
    //} SECURITY_IMPERSONATION_LEVEL, *
    //  PSECURITY_IMPERSONATION_LEVEL;

    //typedef struct _SECURITY_QUALITY_OF_SERVICE {
    //DWORD Length;
    //SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
    //SECURITY_CONTEXT_TRACKING_MODE ContextTrackingMode;
    //BOOLEAN EffectiveOnly;
    //} SECURITY_QUALITY_OF_SERVICE, * PSECURITY_QUALITY_OF_SERVICE;

    //#define SECURITY_DYNAMIC_TRACKING      (TRUE)
    //#define SECURITY_STATIC_TRACKING       (FALSE)

    //typedef BOOLEAN SECURITY_CONTEXT_TRACKING_MODE,
    //              * PSECURITY_CONTEXT_TRACKING_MODE;

    //fake value,,,
    NT::SECURITY_QUALITY_OF_SERVICE sqos = {sizeof(sqos),
      NT::SecurityAnonymous,
      SECURITY_STATIC_TRACKING, FALSE};

    NT::OBJECT_ATTRIBUTES oa = {sizeof(oa), 0, 0, 0, 0, &sqos};

    ntStatus = NT::ZwCreateToken(&hToken, TOKEN_ALL_ACCESS, &oa,
               TokenPrimary,
               NT::PLUID(&authid),
                  // NT::PLUID(&stats->AuthenticationId),
               NT::PLARGE_INTEGER(&lpStatsToken->ExpirationTime),
               &userToken,
               lpGroupToken, lpPrivToken, &ownerToken,
               &primGroupToken, lpDaclToken,
               &sourceToken);
//You may get 0xC0000061 which is STATUS_PRIVILEDGE_NOT_HELD,
//If so, try to GRANT SeCreateTokenPrivilege and relog once...
//STATUS_PRIVILEDGE_NOT_HELD;
//#define STATUS_ACCESS_VIOLATION          ((NTSTATUS)0xC0000005L)    // winnt
//An invalid parameter was passed to a service or function.
//#define STATUS_INVALID_PARAMETER         ((NTSTATUS)0xC000000DL)
    if(ntStatus == STATUS_SUCCESS)
       bRet = TRUE;
    else
       err;
  }
  __finally
  {
    if(lpSidOwner) ::FreeSid(lpSidOwner);
    if(primGroupToken.PrimaryGroup)
      ::FreeSid(primGroupToken.PrimaryGroup);
    //free DACL if you allocate it yourself inside this Func
    if(lpDaclToken)
    {}

    if(lpEachGrp[0].Sid) ::FreeSid(lpEachGrp[0].Sid);
    if(lpEachGrp[1].Sid) ::FreeSid(lpEachGrp[1].Sid);
    if(lpEachGrp[2].Sid) ::FreeSid(lpEachGrp[2].Sid);

    //Free Inside SID
    if(lpGroupToken && lpGroupToken->GroupCount > 0)
    {
       SID_AND_ATTRIBUTES* lpEachGrp = lpGroupToken->Groups;
       for(int i = 0; i < (int)lpGroupToken->GroupCount; i++)
       {
          ::FreeSid(lpEachGrp->Sid);
          lpEachGrp++;
       }
    }
    if(lpGroupToken)
       ::LocalFree(lpGroupToken);
    if(lpStatsToken) ::LocalFree(lpStatsToken);
    if(lpPrivToken) ::LocalFree(lpPrivToken);
    return bRet;
  }
}

5.3. CreatePureUserToken (showing more sophisticated usage of ZwCreateToken)

BOOL CreatePureUserToken(
     HANDLE &hToken,              //out
     LPCTSTR pszUserName,         //in, Trustee Info
     LPCTSTR pszDomainName,       //in, Trustee Info 
     char* szTokenSource,         //in, maximum 8 char plain text
     PLUID lpluidLogonSID,        //in, Nullable, For making LogonSID
     PLUID lpluidLogonSessionID,  //in, Nullable, For AuthenticateID
     LPCTSTR* szPrivNeeded,       //in, string array of Privileges to
                                  //Add
     BOOL bAllPriv,               //in, if true, add all Privileges
                                  //to Token
     BOOL bKeepPriv,              //in, if true, query pszUserName's
                                  //holding Provileges
     BOOL bDisableAllRelatedGroup,//in, if true, only use Group SID
                                  //from pNewAddedGroup
     PSID* pNewAddedGroup,        //in, Nullable, SID array of Group
                                  //SID to Add
     PSID sidPrimaryGroup,
     //in, Nuallable, PSID of Primary Group. Default: Everyone.
     //Not Critical Para
     PACL lpDefaultDACL,          //in, Nullable, PDACL
     DWORD dwLogonType,           //in, Same as LogonUser
     DWORD dwLogonProvider        //in, Reserved
     )
{
   if(pszUserName == NULL || ::lstrlen(pszUserName) == 0)
   {
     return CreatePureSystemToken(hToken);
   }

   hToken = NULL;
   BOOL bRet = FALSE;

   if(!EnablePrivilege(SE_CREATE_TOKEN_NAME, TRUE)) err;
   PSID lpSidOwner = ::Name2SID(pszUserName, pszDomainName);
   if(lpSidOwner == NULL) err;

   LUID luid;
   PTOKEN_STATISTICS lpStatsToken = NULL;
   NTSTATUS ntStatus = 0;

   //SID_AND_ATTRIBUTES grpSIDAttr[3];
   PTOKEN_GROUPS lpGroupToken = NULL;
   DWORD dwGroupNumber = 0;
   PSID  psidLogonSID = NULL;

   PTOKEN_PRIVILEGES lpPrivToken = NULL;
   DWORD dwPrivGranted = 0;

   TOKEN_OWNER ownerToken;
   ownerToken.Owner = NULL;

   TOKEN_PRIMARY_GROUP primGroupToken;
   if(sidPrimaryGroup == NULL)    //Use Everyone Group for no
                                  //other way
   {
      primGroupToken.PrimaryGroup = ::GetEveryoneSID();
   }
   else
   {
      primGroupToken.PrimaryGroup = sidPrimaryGroup;
   }

   PTOKEN_DEFAULT_DACL  lpDaclToken = NULL;    //it may be NULL
   TOKEN_DEFAULT_DACL   daclToken;
   daclToken.DefaultDacl = NULL;
   if(lpDefaultDACL)
   {
      daclToken.DefaultDacl = lpDefaultDACL;
      lpDaclToken = &daclToken;
   }

   HANDLE hTokenCaller;
   //You can just use caller's token infomation even not recommended
   bRet = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY
                           | TOKEN_QUERY_SOURCE, &hTokenCaller);

   //maximum group in a token
   SID_AND_ATTRIBUTES lpEachGrp[256];
   int len = 0;    //for Token_Source
   int index1 = 0;
   int index2 = 0;
   __try
   {
     index1 = 0;
     index2 = 0;
     if(!bDisableAllRelatedGroup)
     {
        PSID* lppSidLocal = ::QueryLocalGroupSIDs(pszUserName);
        while(lppSidLocal && lppSidLocal[index1] != NULL)
        {
          //only one group will be SE_GROUP_ENABLED
          //| SE_GROUP_ENABLED_BY_DEFAULT
           | SE_GROUP_OWNER;
          //possible candidate is Administrators if user belongs to
          //Admins, ....
          lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
           | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
          lpEachGrp[index2].Sid = lppSidLocal[index1];
          index1++;
          index2++;
        }
    index1 = 0;
    PSID* lppSidNet = QueryNetGroupSIDs(pszUserName,
    (pszDomainName == NULL || ::lstrlen(pszDomainName) == 0) ?
                      NULL : pszDomainName);
        while(lppSidNet && lppSidNet[index1] != NULL)
        {
           lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
             | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
           lpEachGrp[index2].Sid = lppSidNet[index1];
           index1++;
           index2++;
        }
      }
      else
      {
         //PSID* pNewAddedGroup;
         while(pNewAddedGroup && pNewAddedGroup[index1] != NULL)
         {
            lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
             | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
            lpEachGrp[index2].Sid = pNewAddedGroup[index1];
            index1++;
            index2++;
         }
      }

      //add everyone, (interactive/service/batch), local and
      //authenticated users
      lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
        | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
      lpEachGrp[index2++].Sid = ::GetEveryoneSID();    //S-1-1-0

      lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
        | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
      lpEachGrp[index2++].Sid = ::GetAuthenticatedUsersSID();
          //S-1-5-11

      lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
        | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
      lpEachGrp[index2++].Sid = ::GetLocalSID();       //S-1-2-0
//#define LOGON32_LOGON_INTERACTIVE       2
//#define LOGON32_LOGON_NETWORK           3
//#define LOGON32_LOGON_BATCH             4
//#define LOGON32_LOGON_SERVICE           5
//#define LOGON32_LOGON_UNLOCK            7
//#if(_WIN32_WINNT >= 0x0500)
//#define LOGON32_LOGON_NETWORK_CLEARTEXT 8
//#define LOGON32_LOGON_NEW_CREDENTIALS   9
//#endif // (_WIN32_WINNT >= 0x0500)
      if(dwLogonType  == LOGON32_LOGON_INTERACTIVE ||
          dwLogonType == LOGON32_LOGON_UNLOCK)
      {
          lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
           | SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
          lpEachGrp[index2++].Sid = ::GetInteractiveSID();
      }
      else if(dwLogonType == LOGON32_LOGON_SERVICE)
      {
          lpEachGrp[index2].Attributes = SE_GROUP_ENABLED |
            SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
          lpEachGrp[index2++].Sid = ::GetServiceSID();
      }
      else if(dwLogonType == LOGON32_LOGON_BATCH)
      {
          lpEachGrp[index2].Attributes = SE_GROUP_ENABLED |
            SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
          lpEachGrp[index2++].Sid = ::GetBatchSID();
      }
      else
      {
          //just pass it...
      }

      //Note: you can make any fake number here but unless you
      //modify the target object's ACL it will be useless at all
      if(lpluidLogonSID) //Insert a Logon SID
      {
         if(((LUID)*lpluidLogonSID).LowPart == 0)
             //Pure Fake Logon SID
         {   //Do NOT Uncomment following, it leads to a GP error
            /*
            if(!AllocateLocallyUniqueId(&luidLogon)) __leave;
            SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_NT_AUTHORITY;
            psidLogonSID = NULL;
            //S-1-5-5-0-0xxxx
            AllocateAndInitializeSid( &sidAuth, 3,
                SECURITY_LOGON_IDS_RID, luidLogon.HighPart,
                                        luidLogon.LowPart,
                0, 0, 0, 0, 0, &psidLogonSID );
            if(!psidLogonSID) __leave;
            lpEachGrp[index2].Attributes = SE_GROUP_LOGON_ID
              | SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT
              | SE_GROUP_MANDATORY;
            lpEachGrp[index2++].Sid = psidLogonSID;
            */
         }
         else //Copy From A Valid Logon SID
         {
            SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_NT_AUTHORITY;
            psidLogonSID = NULL;
            //S-1-5-5-0-0xxxx
            AllocateAndInitializeSid( &sidAuth, 3,
               SECURITY_LOGON_IDS_RID,
               ((LUID)*lpluidLogonSID).HighPart,
               ((LUID)*lpluidLogonSID).LowPart,
               0, 0, 0, 0, 0, &psidLogonSID );
            if(!psidLogonSID) __leave;
            lpEachGrp[index2].Attributes = SE_GROUP_LOGON_ID
               | SE_GROUP_ENABLED | SE_GROUP_ENABLED_BY_DEFAULT
               | SE_GROUP_MANDATORY;
            lpEachGrp[index2++].Sid = psidLogonSID;
         }
       }

       lpEachGrp[index2].Sid = NULL;

       for(int i = 0; i < index2; i++)
       {
         if(lpEachGrp[i].Sid == NULL) err;
       }

       lpGroupToken = ::CreateTokenGroups(lpEachGrp);
       if(lpGroupToken == NULL) err;
       
       if(bKeepPriv)
       {
         lpPrivToken = ::CreateTokenPrivFromUser(dwPrivGranted,
            pszUserName, pszDomainName);
         if(lpPrivToken == NULL) err;
       }
       else    //Grant all priv to that user
       {
         lpPrivToken = ::CreateTokenPriv(dwPrivGranted,
                                         szPrivNeeded, bAllPriv);
         if(lpPrivToken == NULL) err;
       }

       ownerToken.Owner = ::Name2SID(pszUserName, 
           ((pszDomainName == NULL) || ::lstrlen(pszDomainName)
             == 0) ?
             NULL : pszDomainName);
       if(ownerToken.Owner == NULL) err;
      
       TOKEN_USER userToken;
       userToken.User.Sid = ::Name2SID(pszUserName, 
           ((pszDomainName == NULL) || ::lstrlen(pszDomainName)
             == 0) ? 
             NULL : pszDomainName);
       userToken.User.Attributes = 0;    //no use
       if(userToken.User.Sid == NULL) err;

       bRet = AllocateLocallyUniqueId(&luid);
           //this luid only unique in this session
       if(!bRet) err;

       TOKEN_SOURCE sourceToken = {{'*', '*', '*', '*', '*', '*',
                                    '*', '*'},
            {luid.LowPart, luid.HighPart}};

       len = ::strlen(szTokenSource);
       if(len > 8) len = 8;
       if(len > 0)
          ::CopyMemory((LPBYTE)sourceToken.SourceName,
                       (LPBYTE)szTokenSource, len);

       LUID authid = SYSTEM_LUID;
       lpStatsToken = ::CreateTokenStatistics(NULL,
            lpluidLogonSessionID ? lpluidLogonSessionID : &authid,
            NULL, NULL,
            NULL, NULL, NULL, &dwGroupNumber, &dwPrivGranted, NULL);
       if(lpStatsToken == NULL) err;

       NT::SECURITY_QUALITY_OF_SERVICE sqos = {sizeof(sqos),
            NT::SecurityAnonymous, SECURITY_STATIC_TRACKING, FALSE};

       NT::OBJECT_ATTRIBUTES oa = {sizeof(oa), 0, 0, 0, 0, &sqos};

       ntStatus = NT::ZwCreateToken(&hToken, TOKEN_ALL_ACCESS, &oa,
                TokenPrimary,
                NT::PLUID(&authid),    // NT::PLUID(&stats->
                                       //AuthenticationId),
                NT::PLARGE_INTEGER(&lpStatsToken->ExpirationTime),
                &userToken,
                lpGroupToken, lpPrivToken, &ownerToken,
                &primGroupToken, lpDaclToken,
                &sourceToken);
//You may get 0xC0000061 which is STATUS_PRIVILEDGE_NOT_HELD,
//If so, try to GRANT SeCreateTokenPrivilege and relog once...
//STATUS_PRIVILEDGE_NOT_HELD;
//#define STATUS_ACCESS_VIOLATION  ((NTSTATUS)0xC0000005L)
    // winnt
//STATUS_PARTIAL_COPY              ((NTSTATUS)0x8000000DL)
       if(ntStatus == STATUS_SUCCESS)
          bRet = TRUE;
       else
          err;
    }
    __finally
    {
       if(lpSidOwner) ::FreeSid(lpSidOwner);
       if(primGroupToken.PrimaryGroup)
          ::FreeSid(primGroupToken.PrimaryGroup);
       if(psidLogonSID)
          ::FreeSid(psidLogonSID);
       //free DACL if you allocate it yrself inside this Func
       if(lpDaclToken)
       {}
       for(int i = 0; i < index2; i++)
       {
          if(lpEachGrp[i].Sid)
             ::FreeSid(lpEachGrp[i].Sid);
       }

       //Free Inside SID
       if(lpGroupToken && lpGroupToken->GroupCount > 0)
       {
          SID_AND_ATTRIBUTES* lpEachGrp = lpGroupToken->Groups;
          for(int i = 0; i < (int)lpGroupToken->GroupCount; i++)
          {
            ::FreeSid(lpEachGrp->Sid);
            lpEachGrp++;
          }
       }
       if(lpGroupToken) ::LocalFree(lpGroupToken);
       if(lpStatsToken) ::LocalFree(lpStatsToken);
       if(lpPrivToken)  ::LocalFree(lpPrivToken);
       return bRet;
   }
   return TRUE;
}

6. Application of RunAsEx

6.1. Start a Program in a logon Screen

You can go to my previous article "COM Interface Hooking", where I launch SPY++ on a logon screen to see what windows are there. Another example is my "Super PasswordSpy++". It's launched in a logon desktop, which reads the password from the password dialog.

6.2. Start a Program in a Different Session of Terminal Server

WinXP/2003 Users: Enable your FUS, log on twice as different users, and use RunAsEx to shoot the program to both sessions.

Win2K Server+ Users: Enable your Terminal Service. From a Remote Desktop on a different machine, use RunAsEx to shoot the program to a different session.

6.3. Start a Program as SYSTEM

Besides the common usage of overcoming the access-denied problem when touching system resources, Running as SYSTEM is the first step of API hooking on a system process. What do you think of on the following figure? By the way, it is not obtained by calling the sequence ClearEventLog, SetSystemTime, ReportEvent, SetSystemTime, ReportEvent...

[RunUser07.jpg]

Figure 7. A coming tool using RunAs..., modifying the Win NT Event Log on the fly

Note: The above figure you see is a fake image, but I have finished the proof-of-concept coding already. It permits the user to insert, remove, and update any individual event entry at any position inside all event files (including security, system, and application). It also adds runtime monitoring/incepting/modifying to the NT Event Log if you want. To inject DLL into the services.exe process, the first step is to RunAs SYSTEM. I will present this tool soon with anlibrary function. I will also publish the file format of EVT file. (I tried to submit it to http://www.wotsit.org/ but got no response. Readers, please comment below when you have an idea of where to publish this file format.)

You should also be aware that "Running Code under a Different User Context Inside One Process" can be a cool (Server Side Impersonate) thing or a nasty trouble. Go to http://www.codeguru.com/system/winspy.html (or http://www.codeproject.com/threads/winspy.asp) to get Robert Kuster's "Three Ways to Inject Your Code into Another Process." There he provides three ways to inject (hook) into another process. Now, make a simple MFC Dialog program and only put a edit box with password style (you can finish doing it in one minute so I do not offer a demo below). Using RunAsEx to runas it as SYSTEM, and try LibSpy.exe and HookSpy.exe from Kuster's toolbox. Do the same thing again without runas, and you will see SetWindowsHookEx way failed while CreateRemoteThread still works. It should be no surprise to you since the SendMessage(hEdit, WM_GETTEXT,...) is executed in different user context. If you still doubt about it. Well, runas both the password dialog and HookSpy.exe as SYSTEM, and you will be able to get the password again. btw, you can replace HookSpy.exe with my "Super PasswordSpy++." They are basically the same.

6.4. Start a Program in an Invisible Desktop (Partial Process Hidden)

Putting a program in a WinStat other than WinStat0 will make it absolutely invisible. (Sure, you still can find it on task manager unless you make other tricks.) Though there is a API called SwitchDesktop it is only for desktop hosted by WinStat0. Interesting readers can go to http://www.codeguru.com/system/DesktopSwitcher.html to have a look Michael Fatzi's "Creating and Switching to Different Desktops," Nov 2003.

6.5. Start a Program as a User Without Password

It may be interesting to quite some readers. Yes, ZwCreateToken do not need a password which is really fun. By taking the mask of other user, you leave that user's name in the Event Log when accessing local audited resource (including file, directory, and registry).

6.6. Should I call it Anti-CreateRestrictedToken or a CreateExpandToken?

Please go to MSDN and read the document on API CreateRestrictedToken. This API let you do the following:

  • Delete privileges
  • Disable token SIDs for trustee accounts
  • Add "restricted SIDs" of trustee accounts

As a whole, what's this API doing is just as it name. "Delete Privilege" will shrink user's privileges set; "Disable token SID" will put the Group SID in deny access usage only; "Add Restrict SID" is similar to creating a second token and making sure that both tokens have access to a securable object before performing an action on the object.

More interesting things come when you modifying the privileges and group SIDs of the token using the CreatePureUserToken in CoreCode.cpp, its signature is listed here (by the way, it need to call another dozen simpler helper function which consists most of the part of CoreCode.cpp there).

BOOL CreatePureUserToken(
     HANDLE &hToken,                //out
     LPCTSTR pszUserName,           //in, Trustee Info
     LPCTSTR pszDomainName,         //in, Trustee Info 
     char* szTokenSource,           //in, maximum 8 char plain text
     PLUID lpluidLogonSID,          //in, Nullable, For making
                                    //LogonSID
     PLUID lpluidLogonSessionID,    //in, Nullable, For
                                    //AuthenticateID
     LPCTSTR* szPrivNeeded,         //in, string array of Privileges
                                    //to Add
     BOOL bAllPriv,                 //in, if true, add all Privileges
                                    //to Token
     BOOL bKeepPriv,                //in, if true, query
                                    //pszUserName's holding
                                    //Privileges
     BOOL bDisableAllRelatedGroup,  //in, if true, only use Group SID
                                    //from pNewAddedGroup
     PSID* pNewAddedGroup,          //in, Nullable, SID array of
                                    //Group SID to Add
     PSID sidPrimaryGroup,          //in, Nuallable, PSID of
                                    //Primary Group.
                                    //Default: Everyone
     PACL lpDefaultDACL,            //in, Nullable, PDACL
     DWORD dwLogonType,             //in, Same as LogonUser
     DWORD dwLogonProvider          //in, Reserved
     )

Superior to CreateFont with 15 parameters, it is fairly easy to use for most of them can be omitted with a default value. See, it can do both the "adding" and "cutting" job. Let's take a look at privileges, if you pass TRUE in bAllPriv, the token passed back to you will have all privileges enabled. If you choose to control the privileges set yourself, pass a string (LPCTSTR) array thru szPrivNeeded and set bAllPriv and bKeepPriv to FALSE. In the end, if you just want to token bestowed with it deserved, pass TRUE in bKeepPriv, this function will call LookupPrivilegeValue and do the right thing. As to the Group SIDs, if you pass FALSE to bDisableAllRelatedGroup, CreatePureUserToken will scan which Group the principle pszUserName is in, and assign them to Group SIDs. pNewAddedGroup gives you more control by allowing more Group SIDs added.

The following italic text is extracted from Jason D. Clark's Programming Server-Side Program for MS Windows 2000, 1999, MS Press (Chap11 User Context, Section 5 Restricted Tokens).

CreateRestrictedToken is a powerful function allowing you flexible restriction of existing tokens. Consider, for example, the following scenario, which illustrates this flexibility. Imagine that you wanted to secure a group of objects for which you explicitly allow or deny users certain access, in a somewhat unorthodox manner. You want to restrict access to some objects only on Tuesdays, regardless of what the typical access is for this object. You can take the following steps to implement this functionality without undermining the typical non-Tuesday access to the objects. Here are the administrative tasks:

  1. Create a user account named Tuesday for the sole purpose of adding restrictions to securable objects.
  2. Modify the objects' DACLs to include the restrictions desired on Tuesdays. Assign these access-denied ACEs to the SID of the Tuesday account.

Here are the server tasks:

  1. When a user connects to your server, use the GetSystemTime function to determine whether it is Tuesday.
  2. If it is Tuesday, rather than use the impersonation token for the user, create a restricted token before executing code on behalf of the client.
  3. Build a list of restricting SIDs that matches the groups in the source token, and include the user SID for the token. Additionally include in the list of restricting SIDs an entry for the Tuesday trustee.
  4. Impersonate the new restricted token using ImpersonateLoggedOnUser.

Now, let's take an opposite example. I want the object to be accessible only on Tuesday. What do you do? Don't tell me you use the above way in Monday, Wednesday, Thursday, Friday, Saturday, and Sunday (Yes, I have to agree it works...). The point is that now we need "expand" instead of "restrict." Suppose the object's DACL does not contain a related ACE to the token (I mean the token Group SID and the ACE points to the same trustee. Note: I have to make this assumption, if the DACL already have ACE deny the token, I have to pass bDisableAllRelatedGroup TRUE, and pass a brand new Group SID). Here are the administrative task:

  1. Create a user account named Tuesday for the sole purpose of adding expansion to securable objects.
  2. Modify the object's DACL to include the expansion desired on Tuesday. Assign these access-granted ACEs to the SID of the Tuesday account.

Here are the server tasks:

  1. When a user connects to your server, use the GetSystemTime function to determine whether it is Tuesday.
  2. If it is Tuesday, rather than use the impersonate token for the user, create a "pure token" before executing code on behalf of the client. (NOTE: Deprive its Logon SID and pass thru lpdwLogonSessionID.) Add the Tuesday trustee.
  3. ImpersonateLoggedOnUser and you're ready to go.

You may ask why we reset the token group thru SetTokenInformation. Unfortunately, this API will not accept TokenGroups like its counterpart GetTokenInformation.

7. Known Limitations/Side-Effects/Pitfalls of RunAsEx and Their Solutions

From my point of view, CreateProcessAsUser is a crappy API because it is addictive to bringing us nonsense thru GetLastError. Besides, all code in RunAsEx only uses low-level Security APIs, such as AllocateACE, InitializeAcl, and AddAce instead of those high-level SetEntriesInAcl, GetExplicitEntriesFromAcl, and so on, because the latter APIs have a sordid history of bugs and performance issues. Let's perform an experiment: In Session1 (you need WTS), use the token obtained from LogonUser and pass it to CreateProcessAsUser. Guess what error is thrown back? ERROR_FILE_NOT_FOUND (The system can not find the file specified!!!) But, everything is fine when you working in Session0.

7.1. Target WinStat\Desktop DACL not reverted after Target Program Exited

It was explained in detail in Section 4. As a whole, it is by design. If you do have a problem with it, you have to write your own code to get rid of the access-grant ACEs wisely.

7.2. RunAsEx Does Not Close Newly Created WinStat\Desktop Handle, Thus Handles Accumulated with Launching

It is also by design but out of my desire. You see, I created WinStat\Desktop when they don't exist and modified their DACL after the Token Creation. Then, I call CreateProcessAsUser in my process, which has been moved to the new WinStat. Here is the weird thing: Even after CreateProcessAsUser returns ERROR_SUCCESS, if I close the WinStat\Desktop Handle I got from CreateWindowStation and CreateDesktop, my process will still be okay, but the target process will be terminated miserably with Figure 6. Reason: Unknown. Solution: None except closing RunAsEx.

7.3. Sometimes I failed to launch the Target Program to another Session from my current Session. I am using LogonUser Mode.

I have no idea why this happens. According to the log file, it usually is ERROR_FILE_NOT_FOUND (the system cannot find the file specified) when calling CreateProcessAsUser or ERROR_ACCESS_DENIED (Access is Denied) when calling SetTokenInformation. But, anyway, use Zw Mode plus NT Service Mode; they will always let you do that.

7.4. Intrinsic Limitation with Zw plus NT Service Mode:

Even as the most powerful/useful RunAs Mode in RunAsEx, there is a intrinsic limitation that is surmountable by coding more. If you have a look at the CreatePureUserToken, you will find it calls a dozen helper functions that include two functions named QueryLocalGroupSIDs and QueryNetGroupSIDs. These two functions internally call NetUserGetLocalGroups and NetUserGetGroups respectively to get the group the trustee belongs to. An experienced user now may realize the problem—the performance time. You see, in the NT Service handler, we created the token, launched the target program, and then, stop, remove the service. I put this code in the service startup part and report an estimated five seconds to finish this operation.

Remember: In NT Service, if StartServiceCtrlDispatcher is not called within 30 seconds, the SCM thinks that the service executable is malfunctioning and calls TerminateProcess to forcibly kill the process. Okay, here is the problem: If NetUserGetGroups takes me more than 30 seconds to be back, the SCM will be doing that. It may kill RunAsEx before, in the middle of, or after CreateProcessAsUser is executed. So, the final behavior is uncertain. A more robust way will be launch a separate thread to call RunAs, but I left it out because I have no intention to support such a sluggish network. Check HOWTO: Manage Threads in a Windows NT System Service (KB Q189996) for implementing multiple threads in an NT Service.

7.5. Error Logging is hard-code directed to C:\RunAsEx.log

By design, change #define EvtLogName in CoreCode.cpp and you go with the NT Event Log. It is not a big deal to dump the log to disk. Your C drive exists and is writable, isn't it?

7.6. NT Service Mode makes the Target Process have no direct Parent Process

It is natural because the NT Service (second instance of RunAsEx) is stopped and removed after launching. Not a defect at all.

7.7. Faking Domain User will not pass Machine Boundary (Zw Mode Only)

If you want to take the mask of another Domain User for which you have no password for him/her using the Zw Mode and access network resource like him/her, you are wrong. This tool will not help you break the security settings. For example, you could use Zw mode (with domain and user name only) to start explorer.exe and try to access a local network resource, say, a shared folder. What will happen? See, if you log on using LogonUser mode, the system will generate a logon session and cache your user name - password pair, but your fake token do not have one. Now, you can pass your current (the shell you are working) LogonSID to the fake token thru lpluidLogonSID in CreatePureUserToken, if so, when you access audited network resource, your name will be left there instead of the Target User Name in RunAsEx; If you have no LogonSID there in the fake token, you will not have access on a protected share resource, but to the Everyone-OK share folder you can access; leave the machine name there if audition is enabled there. As a whole, RunAsEx has no use on the network for Token. From the very beginning, it has sense only on the machine where it is created. It is by WinNT security design.

7.8. Logging Record Entry Seems Odd. Why Did One Failure Trigger At Least Two Logging Entries?

If you read the source code, it is simple answer. All sensitive code code is protected by SEH __try --- __fnal block. If anything unexpectedly goes wrong—for example, API returns a failure code—__leave is called and the execution goes to __final block. In this case, two logging entries are make: One reports the API fails, and one reports in __final saying the execution flow goes to the final block. It is totally by design.

7.9. Miscellaneous Finding With ZTokenMan (Showing Token Implementation Problem in SVCHOST.EXE)

You must launch ZTokenMan with a SYSTEM context or you enable the four privileges I mentioned before to let ZTokenMan launch itself with SYSTEM context. My machine is a WinXP Pro. Use ProcessExplorer to locate the PID (process ID) of svchost.exe runnig as LocalService and NetworkService, and go to ZTokenMan and check their token information. What will you find? Here is LocalService on my machine:

[RunUser08.jpg]

Figure 8. Locating PID of LocalService and NetworkService svchost.exe

*********************************************
USER SID
*********************************************

SID:          S-1-5-20
Use:          Well-Known Group SID
Name:         NETWORK SERVICE
Domain Name:  NT AUTHORITY

*********************************************
GROUP SIDS
Group Count:  11
*********************************************
Sid #0
SID:          S-1-5-20
Use:          Well-Known Group SID
Name:         NETWORK SERVICE
Domain Name:  NT AUTHORITY

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
----------------------------------------------
Sid #1
SID:          S-1-1-0
Use:          Well-Known Group SID
Name:         Everyone
Domain Name:

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
----------------------------------------------
Sid #2
SID:          S-1-5-32-545
Use:          Alias SID
Name:         Users
Domain Name:  BUILTIN

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
-----------------------------------------------
Sid #3
SID:          S-1-1-0
Use:          Well-Known Group SID
Name:         Everyone
Domain Name:

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
-----------------------------------------------
Sid #4
SID:          S-1-5-11
Use:          Well-Known Group SID
Name          Authenticated Users
Domain Name:  NT AUTHORITY

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
-----------------------------------------------
Sid #5
SID:          S-1-2-0
Use:          Well-Known Group SID
Name:         LOCAL
Domain Name:

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
-----------------------------------------------
Sid #6
SID:          S-1-5-32-545
Use:          Alias SID
Name:         Users
Domain Name:  BUILTIN

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
------------------------------------------------
Sid #7
SID:          S-1-5-5-0-37873
Use:          Logon SID
Name:         [Logon SID]
Domain Name:

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
      SE_GROUP_LOGON_ID
-------------------------------------------------
Sid #8
SID:          S-1-2-0
Use:          Well-Known Group SID
Name:         LOCAL
Domain Name:

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
--------------------------------------------------
Sid #9
SID:          S-1-5-6
Use:          Well-Known Group SID
Name:         SERVICE
Domain Name:  NT AUTHORITY

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
---------------------------------------------------
Sid #10
SID:          S-1-5-11
Use:          Well-Known Group SID
Name:         Authenticated Users
Domain Name:  NT AUTHORITY

SID Attribs:
      SE_GROUP_MANDATORY
      SE_GROUP_ENABLED_BY_DEFAULT
      SE_GROUP_ENABLED
---------------------------------------------------
***************************************************
Priveleges
***************************************************
SeAuditPrivilege
SeIncreaseQuotaPrivilege
SeAssignPrimaryTokenPrivilege
SeChangeNotifyPrivilege
   SE_PRIVILEGE_ENABLED_BY_DEFAULT
   SE_PRIVILEGE_ENABLED
SeShutdownPrivilege
SeUndockPrivilege
***************************************************
Token Owner
***************************************************
SID:          S-1-5-20
Use:          Well-Known Group SID
Name:         NETWORK SERVICE
Domain Name:  NT AUTHORITY

***************************************************
Token Primary Group
***************************************************
SID:          S-1-5-20
Use:          Well-Known Group SID
Name:         NETWORK SERVICE
Domain Name:  NT AUTHORITY

***************************************************
Token Default DACL
***************************************************
ACCESS ALLOWED  NT AUTHORITY/SYSTEM
ACCESS_MASK:  00010000000000000000000000000000
   GENERIC_ALL

ACCESS ALLOWED  NT AUTHORITY/NETWORK SERVICE
ACCESS_MASK:  00010000000000000000000000000000
   GENERIC_ALL


*************************************************
Token Source:  Advapi
*************************************************
*************************************************
Token Type:  Primary
*************************************************
*************************************************
LUID TokenID = 0-941067
LUID AuthenticationId = 0-996
LUID ModifiedId = 0-37882
*************************************************

*************************************************
Token is not Restricted
Restricted SID Count:  0
Restricted SIDs
*************************************************

The problem is that there are double LOCALs (S-1-2-0) residing there with the same SID Attributes. In the end, there is no use of it besides increasing the access checking time and lowering the performance. Why they are there? No idea.

Keith Brown also mentioned this in his book why there are two LUIDs to identify a token (AuthenticateID and LogonSID) and the high part is wasted to be 0 all the time. But, that's how Windows works. Period.

7.10. Do not make a fake LogonSID yourself

Although most of the data you passed to ZwCreateToken are highly up to your need, especially in the statistic part, you cannot make a fake Logon SID, even though you can promise that its SID is unique on that machine. ZwCreateToken and CreateProcessAsUser will return success, but a GP error dialog will pop up, stating your Target Process is terminated by the system. One solution is to run LogonUser first and obtain a valid LogonSID and pass it to CreatePureUserToken. You may ask: What's the point doing double work? The point is you have a truly genuine token with full control on group SIDs and privileges. Remember, by default, CreatePureUserToken gives you a token without a Logon SID.

7.11. RunAsEx is a Dialog-Only GUI program

Absolutely Not! Although I showed the picture above, RunAsEx can parse your command line passed to it. If you give enough information (Target Desktop, Target Program) it will start to RunAs the Target Program without a user interface. So, you can use it in a batch file. I highly recommended you generate the command by using the "CmdText" button, choose every setting you need, push the button, and paste the final command line from the Clipboard. Sound like a piece of cake?

7.12. RunAsEx has no support for Disable Privilege Dialog and it makes no difference between "Disabled" and "Not-Granted" privileges for both cases make the check box unchecked.

Yes, it is not perfect; maybe I should use a 3-state check box. Well, this is just by design and to save my code typing. IMHO, because you must be a local Admin member to use this tool, there is no point not granting yourself these privileges. See, if some underground code has invaded the Admin context, it is no use to keep your safety by not granting the privileges. Well, maybe it can keep you safe when the code is not well written. Plus, the install-launch-remove NT service takes time. So, I hope you enable these privileges. If you do not like it, just use the NT Service mode. The base line is you can get rid of these privileges with MMC anyway.

7.13. Miscellaneous Implementation Tricks

Because ZwCreateToken is undocumented, you have to try coding it. You see, MSDN tell you in CreateProcess(AsUser) you have to allocate a writable local buffer to hold the command line; it doesn't tell you in NetUseAdd's USE_INFO_2 the password also needs a writable one. In ZwCreateToken, the tokenOwner is the same, but only for WinXP+; in Win2K it, is more tolerant to permit you to use a read-only buffer. More interest comes when in WinXP I cannot add SE_MACHINE_ACCOUNT_NAME to the fake Token; otherwise, it will kick me out with a GP error. There are other tricks and I comment them in the code; you have to read the code to learn.

8. Reference List

  1. Programming Server-Side Applications for Microsoft Windows 2000, ISBN 0-7356-0753-2, Microsoft Press, 1999.
  2. Professional NT Services, by Kevin Miller, ISBN: B00005Y2AZ, Wrox Press, 1998.
    Kevin mentioned that there is a limitation on the Desktop number and Total ACL usage. I remember it is around 40 desktops at the most, although I am not quite sure about it. But, when Kevin wrote the cool book, it was the WinNT4 era. But, anyway, do not make too many new WinStat\Desktops.
  3. Programming Windows Security, by Keith Brown, ISBN: 0201604426, Addison-Wesley Pub Co, 2000.
  4. Windows NT/2000 Native API Reference, by Gary Nebbett, ISBN: 1578701996, Que, 2000. (NtDll.h & Ntfs.h are from this book)
    Note: When referring the ZwCreateToken part, please note that the author, perhaps to save space, just passes the caller's Token information to the system token. In the real world, this kind of hybrid token will have trouble, for the access check heavily rely on these group SIDs.
  5. Programming Applications for MS Windows, 4th Edition, by Jeffrey Richter, ISBN 1-57231-996-8, Microsoft Press, 1999.
  6. http://www.stefan-kuhr.de/. Stephan Kuhr did an excellent job on another SU; navigate to the SUperior SU page yourself. Special Thanks to Stephan for pointing out the SetTokenInformation can move processes between WTS sessions.
  7. http://www.sometips.com/goodstuff/default.htm.Check the psu tool. EXE only. Unfortunately, this is a Simplified Chinese-only page.
  8. http://www.binglesite.net a WSU, EXE only. Unfortunately, this is a Simplified Chinese-only page. The tokens generated use Caller's Group SIDs.
  9. CmdRunAs in Feb 2000, MSJ

Thanks & Claims

Again, I would like to express my gratitude to the editor of CodeGuru Web site—Susan Moore—who helps so much to post it in place. Most of my articles on CodeGuru are brutally long, full of code excerpts, or rather, all code frame, which take the editor double energy. The purpose is to let readers pass the article asap. That is, if the reader can touch the essence of the article without downloading, unzipping, and opening with the huge VS editor, they can decide to whether to peruse or skip it the first time. Another standing point of mine is I have no interest on repeating, so I try to present something that is new (hope so) AND useful (can be copy-and-paste to some guy's project). Inevitably, new things bring a lot of bugs and misunderstanding, so please feel free to comment on this article. Thank you all, readers, again, for reading such a long text.

History

Version 1: 2002 July
Version 2: 2003 Nov



Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: November 20, 2014 @ 2:00 p.m. ET / 11:00 a.m. PT Are you wanting to target two or more platforms such as iOS, Android, and/or Windows? You are not alone. 90% of enterprises today are targeting two or more platforms. Attend this eSeminar to discover how mobile app developers can rely on one IDE to create applications across platforms and approaches (web, native, and/or hybrid), saving time, money, and effort and introducing apps to market faster. You'll learn the trade-offs for gaining long …

  • IBM Worklight is a mobile application development platform that lets you extend your business to mobile devices. It is designed to provide an open, comprehensive platform to build, run and manage HTML5, hybrid and native mobile apps.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds