Following table is a mapping Token-Elements and how to obtain them:
Name | API to Call | Function Name in RunAsEx Source |
User SID | LookupAccountName | PSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) |
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() ... |
Logon SID (inside Group SIDs) | No Way To Create From Scratch (but may be able to steal from elsewhere) |
always in format S-1-5-5-0-XXXX SYSTEM process (winlogon, lsass, etc...) does not have this User processes and other system-level processes (such as svchost running under local_service and network_service) have this member. A anonymous token token (with authenticationID = 998) does not have this member. Check http://www.develop.com/.../whatis_alogonsession.html |
Privileges | LookupPrivilegeValue | PTOKEN_PRIVILEGES CreateTokenPriv(DWORD& dwPrivGranted, LPCTSTR* lpszPriv, BOOL bGrantEnableAll) |
Token Owner | LookupAccountName | PSID Name2SID(LPCTSTR pszUserName, LPCTSTR pszDomainName) |
Token Primary Group | AllocateAndInitializeSid | Same as Group SIDs |
Token Default DACL | AllocateACE, InitializeAcl, AddAce... | Importable from caller; RunAsEx uses NULL DACL by default |
Token Source | An 8-char string; RunAsEx uses "RunAsEx+" | |
LUID TokenId, LARGE_INTEGER ExpirationTime, SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, LUID ModifiedId | AllocateLocallyUniqueId | PTOKEN_STATISTICS CreateTokenStatistics( LUID* lpTokenId, //Optional LUID* lpAuthenticationId, //Optional LARGE_INTEGER* lpExpirationTime, //Optional TOKEN_TYPE* lpTokenType, //Primary SECURITY_IMPERSONATION_LEVEL* lpImpersonationLevel, //Only When ImpersonteToken DWORD* lpDynamicCharged, //Optional DWORD* lpDynamicAvailable, //Optional DWORD* lpGroupCount, //Mandatory DWORD* lpPrivilegeCount, //Mandatory LUID* lpModifiedId //Optional Note: In the real world, all LUID members here set 0 as HighPart. RunAsEx will assign random values to these members because they are not critically important. |
TOKEN_TYPE TokenType | N/A Only Meaningful When Impersonate Token | |
DWORD DynamicCharged | Always 500 | |
DWORD DynamicAvailable | Undecided; 420 by default | |
DWORD GroupCount | Checks count of Group SIDs | |
DWORD PrivilegeCount | Checks count of Privileges | |
LUID AuthenticationId | The first 1000 LUIDs are reserved (0x3E7 = 999). SYSTEM_LUID { 0x3E7, 0x0 } ANONYMOUS_LOGON_LUID { 0x3e6, 0x0 } LOCALSERVICE_LUID { 0x3e5, 0x0 } NETWORKSERVICE_LUID { 0x3e4, 0x0 } | Caller pass. * Check RunAsEx desktop combo box and you will find Service-0x0-3e7$, Service-0x0-3e5$, and Service-0x0-3e4$. These are service logon sessions that get their names from this authenticate ID.
Note: This is different from Logon SID S-1-5-5-0-XXXX. I mean AuthenticationId.LowPart != XXXX. It is the Logon Session LUID. Use the ZTokenMan check system, and you will see its authentication ID is 999. It is just the Logon Session associated with WinStat Service-0x0-3e7$. Use PrccessExplorer to check it! All Tokens I met AuthenticationId.HighPart = 0. RunAsEx uses 999 as the default when the caller does not pass into. This member is closely related to the WinStat name. |
Note: AuthenticationId is closely related to the WinStat Name when you pass the token to CreateProcessAsUser with an Empty Desktop Name "". Take this as an example: Configure a NT Service running as a user (instead of the default system); it should be put into a WinStation called "Service-0x0-XXX___FCKpd___0quot;. Go to ZTokenMan and have a look at its authenticationId; it should be the same value.
Note: The word Session is overused indeed. When I mention Logon Session SID, it is a SID embedded in Token. When I say Logon Session LUID, it is a LUID (64bits) indentifying a logon session and shared, usually, by some processes. When I Session ID, when in a WTS environment, it is a 0-based integer showing the current connection's order.
Now, let's review what we need to do to RunAs and the corresponding API:
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).
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(GetProcAddress(hEvnModule, "CreateEnvironmentBlock")); _DestroyEnvironmentBlock = reinterpret_cast (GetProcAddress(hEvnModule, "DestroyEnvironmentBlock")); _LoadUserProfileW = reinterpret_cast (GetProcAddress(hEvnModule, "LoadUserProfileW")); _UnloadUserProfile = reinterpret_cast (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); }