5,666,979 members and growing! (15,708 online)
Email Password   helpLost your password?
General Reading » Hardware & System » General     Advanced

GUI-Based RunAsEx

By Zhefu Zhang

An ultimate tool that lets you RunAs... (With support for non-Pwd, WTS, fake privilege, fake user groups, etc...)
VC6, C++Windows, Win2K, WinXPVS6, Visual Studio, Dev

Posted: 15 Mar 2004
Updated: 24 Oct 2006
Views: 119,637
Bookmarked: 112 times
Announcements
Loading...



Search    
Advanced Search
Sitemap
47 votes for this Article.
Popularity: 6.86 Rating: 4.10 out of 5
5 votes, 10.6%
1
4 votes, 8.5%
2
0 votes, 0.0%
3
3 votes, 6.4%
4
35 votes, 74.5%
5

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 Codeguru 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

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 authentication ID = 998) does not have this member. Check this link.

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_IMPERS-ONATION_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 ProcessExplorer 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: Authentication ID 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 authentication ID; 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) identifying 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);
}

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 dwPrivGra