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:
- 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).
- 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.
- 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.
- 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:
- 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.
- 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.
- They are console applications; you have to type anyway.
- They need the target user's password, even you are a Administrator member who can set passwords for anybody.
- 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;
HDESK hDesk = CreateDesktop(
_T("WinStatABC\DeskABC"), NULL, NULL, 0,
DESKTOP_ENUMERATE | DESKTOP_CREATEWINDOW, NULL);
if(hDesk == NULL) return 0;
::Sleep(50000);
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:
- Administrators
- RESTRICTED //Stands for it is a token created by
CreateRestrictedToken
- S-1-5-5-0-XXX
- SYSTEM
Go to my security dialog and you will get the SID of these items, as in the following:
- S-1-5-32-544
- S-1-5-12 (from
SECURITY_RESTRICTED_CODE_RID
)
- S-1-5-5-0-XXX
- 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:
RunAsEx
has to be kept running when the target is running
- 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,
LUID* lpAuthenticationId,
LARGE_INTEGER* lpExpirationTime,
TOKEN_TYPE* lpTokenType,
SECURITY_IMPERSONATION_LEVEL* lpImpersonationLevel,
DWORD* lpDynamicCharged,
DWORD* lpDynamicAvailable,
DWORD* lpGroupCount,
DWORD* lpPrivilegeCount,
LUID* lpModifiedId
<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:
- 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
- If need a user profile, load userenv.dll now�
LoadLibrary
, GetProcAddress
- Enable All Privileges needed�
LookupPrivilegeValue
, OpenProcessToken
, AdjustTokenPrivileges
- Query Current Desktop Name�
GetUserObjectInformation
- If the Target Desktop in Other WinStat, Yes�
SetProcessWinStat
(after closing All Windows, Hooks of Current Process)
- If the Desktop is non-existing�
OpenWindowStation
, CreateWindowStation
, CreateDesktop
- If the user wants to use
LogonUser
, call it to get Token�LogonUser
- If the user has no password, Zw�
ZwCreateToken
- If you want to launch in another session, change session ID�
SetTokenInformation
- If the user wants to shoot in NT Service�
CreateService
, OpenSCManager
...
- User Token Group SIDs to modify target desktop DACL, get rid of all deny ACE, add positive ACE�
AllocateACE
, InitializeAcl
, AddAce
...
CreateProcessAsUser
it
- 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,
LPTSTR pszDomainName, LPTSTR pszUserName, LPTSTR pszPassword,
LPTSTR pszDesktop,
BOOL bCreateTokenDirectly,
DWORD dwSession,
BOOL bLoadProfile,
BOOL bCopyTokenPropFromCaller,
BOOL bKeepPriv,
DWORD dwLogonType,
DWORD dwLogonProvider
)
{
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;
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;
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)
{
_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;
__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);
bRet = GetUserObjectInformation(
hSrcWinStat,
UOI_NAME,
(LPVOID)szSrcWinStat,
MAX_PATH * sizeof(TCHAR),
&dwFakeLen
);
if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;
bRet = GetUserObjectInformation(
hSrcDesktop,
UOI_NAME,
(LPVOID)szSrcDesktop,
MAX_PATH * sizeof(TCHAR),
&dwFakeLen
);
if(!bRet || dwFakeLen > MAX_PATH * sizeof(TCHAR)) err;
if(pszDesktop == 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
{
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);
fProcess = CreateProcessAsUser(hToken, NULL,
(LPTSTR)szLocalCmdLine,
&sa, &sa,
FALSE,
bLoadProfile ? CREATE_UNICODE_ENVIRONMENT : 0,
bLoadProfile ? pEnvBlock : NULL,
NULL, &si, &pi);
if(!fProcess)
err;
fSuccess = TRUE;
}
__finally
{
if(bLoadProfile && pEnvBlock)
_DestroyEnvironmentBlock(pEnvBlock);
if(bLoadProfile && hToken && profInfo.hProfile)
_UnloadUserProfile( hToken, profInfo.hProfile );
if(pSD) HeapFree(GetProcessHeap(), 0, pSD);
if(hToken) CloseHandle(hToken);
if(hwinstaOld) ::SetProcessWindowStation(hwinstaOld);
if(hdeskOld) ::SetThreadDesktop(hdeskOld);
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
)
BOOL CreatePureSystemToken( 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;
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;
if(userToken.User.Sid == NULL) err;
bRet = AllocateLocallyUniqueId(&luid);
if(!bRet) err;
TOKEN_SOURCE sourceToken = {{'*', 'S', 'Y', 'S', 'T', 'E',
'M', '*'},
{luid.LowPart, luid.HighPart}};
LUID authid = SYSTEM_LUID;
lpStatsToken = ::CreateTokenStatistics(NULL, NULL, 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::PLARGE_INTEGER(&lpStatsToken->ExpirationTime),
&userToken,
lpGroupToken, lpPrivToken, &ownerToken,
&primGroupToken, lpDaclToken,
&sourceToken);
if(ntStatus == STATUS_SUCCESS)
bRet = TRUE;
else
err;
}
__finally
{
if(lpSidOwner) ::FreeSid(lpSidOwner);
if(primGroupToken.PrimaryGroup)
::FreeSid(primGroupToken.PrimaryGroup);
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);
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,
LPCTSTR pszUserName,
LPCTSTR pszDomainName,
char* szTokenSource,
PLUID lpluidLogonSID,
PLUID lpluidLogonSessionID,
LPCTSTR* szPrivNeeded,
BOOL bAllPriv,
BOOL bKeepPriv,
BOOL bDisableAllRelatedGroup,
PSID* pNewAddedGroup,
PSID sidPrimaryGroup,
PACL lpDefaultDACL,
DWORD dwLogonType,
DWORD dwLogonProvider
)
{
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;
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)
{
primGroupToken.PrimaryGroup = ::GetEveryoneSID();
}
else
{
primGroupToken.PrimaryGroup = sidPrimaryGroup;
}
PTOKEN_DEFAULT_DACL lpDaclToken = NULL;
TOKEN_DEFAULT_DACL daclToken;
daclToken.DefaultDacl = NULL;
if(lpDefaultDACL)
{
daclToken.DefaultDacl = lpDefaultDACL;
lpDaclToken = &daclToken;
}
HANDLE hTokenCaller;
bRet = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY
| TOKEN_QUERY_SOURCE, &hTokenCaller);
SID_AND_ATTRIBUTES lpEachGrp[256];
int len = 0;
int index1 = 0;
int index2 = 0;
__try
{
index1 = 0;
index2 = 0;
if(!bDisableAllRelatedGroup)
{
PSID* lppSidLocal = ::QueryLocalGroupSIDs(pszUserName);
while(lppSidLocal && lppSidLocal[index1] != NULL)
{
| SE_GROUP_OWNER;
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
{
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++;
}
}
lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
| SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
lpEachGrp[index2++].Sid = ::GetEveryoneSID();
lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
| SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
lpEachGrp[index2++].Sid = ::GetAuthenticatedUsersSID();
lpEachGrp[index2].Attributes = SE_GROUP_ENABLED
| SE_GROUP_ENABLED_BY_DEFAULT | SE_GROUP_MANDATORY;
lpEachGrp[index2++].Sid = ::GetLocalSID();
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
{
}
if(lpluidLogonSID)
{
if(((LUID)*lpluidLogonSID).LowPart == 0)
{
}
else
{
SID_IDENTIFIER_AUTHORITY sidAuth = SECURITY_NT_AUTHORITY;
psidLogonSID = NULL;
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
{
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;
if(userToken.User.Sid == NULL) err;
bRet = AllocateLocallyUniqueId(&luid);
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::PLARGE_INTEGER(&lpStatsToken->ExpirationTime),
&userToken,
lpGroupToken, lpPrivToken, &ownerToken,
&primGroupToken, lpDaclToken,
&sourceToken);
if(ntStatus == STATUS_SUCCESS)
bRet = TRUE;
else
err;
}
__finally
{
if(lpSidOwner) ::FreeSid(lpSidOwner);
if(primGroupToken.PrimaryGroup)
::FreeSid(primGroupToken.PrimaryGroup);
if(psidLogonSID)
::FreeSid(psidLogonSID);
if(lpDaclToken)
{}
for(int i = 0; i < index2; i++)
{
if(lpEachGrp[i].Sid)
::FreeSid(lpEachGrp[i].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
...

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 a library function. I will also publish the file format of the EVT file. (I tried to submit it here 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 here (or here) 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 a 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. By the way, 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 an API called SwitchDesktop
, it is only for desktop hosted by WinStat0. Interesting readers can go here 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
does not need a password which is really fun. By taking the mask of the other user, you leave that user's name in the Event Log when accessing local audited resources (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,
LPCTSTR pszUserName,
LPCTSTR pszDomainName,
char* szTokenSource,
PLUID lpluidLogonSID,
PLUID lpluidLogonSessionID,
LPCTSTR* szPrivNeeded,
BOOL bAllPriv,
BOOL bKeepPriv,
BOOL bDisableAllRelatedGroup,
PSID* pNewAddedGroup,
PSID sidPrimaryGroup,
PACL lpDefaultDACL,
DWORD dwLogonType,
DWORD dwLogonProvider
)
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 through szPrivNeeded
and set bAllPriv
and bKeepPriv
to FALSE
. In the end, if you just want to have a token bestowed with what it deserves, 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:
- Create a user account named Tuesday for the sole purpose of adding restrictions to securable objects.
- 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:
- When a user connects to your server, use the
GetSystemTime
function to determine whether it is Tuesday.
- 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.
- 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.
- 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:
- Create a user account named Tuesday for the sole purpose of adding expansion to securable objects.
- 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:
- When a user connects to your server, use the
GetSystemTime
function to determine whether it is Tuesday.
- 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.
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 returnsERROR_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
--- __final
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 running as LocalService and NetworkService, and go to ZTokenMan and check their token information. What will you find? Here is LocalService on my machine:

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 tells 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
- Programming Server-Side Applications for Microsoft Windows 2000, ISBN 0-7356-0753-2, Microsoft Press, 1999.
- 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.
- Programming Windows Security, by Keith Brown, ISBN: 0201604426, Addison-Wesley Pub Co, 2000.
- Windows NT/2000 Native API Reference, by Gary Nebbett, ISBN: 1578701996, Que, 2000 (NtDll.h and 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.
- Programming Applications for MS Windows, 4th Edition, by Jeffrey Richter, ISBN 1-57231-996-8, Microsoft Press, 1999.
- Stefan Kuhr's website. 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.
- www.sometips.com. Check the psu tool. EXE only. Unfortunately, this is a Simplified Chinese-only page.
- A WSU, EXE only. Unfortunately, this is a Simplified Chinese-only page. The tokens generated use Caller's Group SIDs.
- 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 as soon as possible. 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
Read This Before Running RunAsEx!!!
There are some problems to pass Domain\User to some APIs like LookupAccountName
and NetUserGetLocalGroups
, so when you are running in a domain environment, do the following:
- If you are using LogOnUser Launch mode, input your domain name in Domain edit box and user name in User edit box, separately.
- If you are using ZwCreateToken Launch mode, leave your domain edit box empty, and input �full-domain-name\\username� in the user edit box. Please use the full domain name like �rd.terminator.com�!!! Note you could run as anyone inside the domain if you are a local admin on that computer!!!