I will be trying to write a series of blog posts about Windows privileges, in alphabetical order.
In Windows NT (and originally OpenVMS) privileges are attributes of a process that allow the process to perform privileged actions. Yes, it is very tautological*.
A process appears to get its privileges from a combination of the privileges held by the security principal (for example the user) that started it minus privileges dropped (for example by the service control manager) plus the privileges held by any pseudo-group it might belong to (for example SERVICE if it is a service) plus the privileges injected into its access token by software designed to modify access tokens.
Privileged actions include starting threads and processes using another identity (for example a different user), reading and overwriting files regardless of permissions (a backup program would need such a privilege), setting time and time zone and shutting down the system.
In alphabetical order the
first privilege is Assign Primary
Token.
It allows a process to start another process using a different identity. (This is distinct from creating a thread using a different identity.)
In the Win32 API Assign Primary Token is known as SE_ASSIGNPRIMARYTOKEN_NAME as a constant or "SeAssignPrimaryTokenPrivilege" as a string. See https://learn.microsoft.com/en-us/windows/win32/secauthz/privilege-constants for a full list.
HANDLE hUserToken = NULL;
ok = LogonUserW(sUserName, L".", sPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hUserToken);
Error(L"LogonUser");
ok = CreateProcessAsUserW(hUserToken, pathImage, sArguments, NULL, NULL, FALSE, dwCreationFlags, NULL, NULL, &si, &pi);
Error(L"CreateProcessAsUser");
The code above will work if the user sUserName exists, if his password sPassword is correct and if the calling process holds SeAssignPrimaryTokenPrivilege. The privilege also has to be enabled, which it will not be.
Enabling a privilege can be done with a function like this:
void EnablePrivilege(LPWSTR sPrivilegeName)
{
HANDLE hCurrentProcessToken;
OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hCurrentProcessToken);
TOKEN_PRIVILEGES privs;
LUID luid;
ok = LookupPrivilegeValue(NULL, sPrivilegeName, &luid);
Error(L"LookupPrivilegeValue");
privs.PrivilegeCount = 1;
privs.Privileges[0].Luid = luid;
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
ok = AdjustTokenPrivileges(hCurrentProcessToken, FALSE, &privs, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
Error(L"AdjustTokenPrivileges");
}
And I am using an Error() function to return error messages which is this:
BOOL debug =
FALSE;
BOOL
ok = TRUE;
DWORD
error = 0;
LSTATUS
status = 0;
void Error(LPCWSTR sz)
{
if (!debug) { return; }
if (!ok || status) { error = GetLastError(); }
fwprintf(stderr, L"%sOK: [%d]STATUS: [%d], Error: [%d] ", sz, ok, status, error);
error = 0;
status = 0;
ok = TRUE;
}
If debug is FALSE the Error() function returns immediately. Otherwise it will react to ok or status and if ok is FALSE or status is positive it will get the last error and display ok, status and the last error. (This is quite handy.) Then it will reset error, status, and ok for the next call.
If you run this with the wrong user name or password, you will get an error 1326 which means that "the user name or password is incorrect": (certutil.exe is a good way to find out the meaning of error codes.)
LookupPrivilegeValue OK: [1] STATUS: [0], Error: [0]
AdjustTokenPrivileges OK: [1] STATUS: [0], Error: [0]
LogonUser OK: [0] STATUS: [0], Error: [1326]
CreateProcessAsUser OK: [0] STATUS: [0], Error: [0]
C:\temp>certutil/error 1326
0x52e (WIN32: 1326 ERROR_LOGON_FAILURE) -- 1326 (1326)
Error message text: The user name or password is incorrect.
CertUtil: -error command completed successfully.
(Incidentally, since the user could not be logged on, hUserToken remained NULL and CreateProcessAsUser() with a NULL token still works, it just creates a process as the same user as the calling user., hence the error for CreateProcessAsUser() was 0.)
The result is much weirder if the user name and password are correct but SeAssignPrimaryTokenPrivilege is not held.
LookupPrivilegeValue
OK: [1] STATUS: [0], Error: [0]
AdjustTokenPrivileges OK:
[1] STATUS: [0], Error: [0]
LogonUser
OK: [1] STATUS: [0], Error: [0]
CreateProcessAsUser
OK: [0] STATUS: [0], Error: [1314]
C:temp>certutil/error 1314
0x522 (WIN32:
1314
ERROR_PRIVILEGE_NOT_HELD) -- 1314 (1314)
Error message
text: A
required privilege is not held by the client.
CertUtil: -error command completed successfully.
Here the user could be logged on (LogonUser() gives us ok=1, status=0 and error=0) but our process cannot start another process using that logged-on user. This is the error 1314 from CreateProcessAsUser() which translates to "A required privilege it not held by the client".
After granting the executing user the Assign Primary Token privilege, the impersonation works:
C:\temp>whoami/priv
PRIVILEGES
INFORMATION
----------------------
Privilege Name
Description
State
=============================
====================================
========
SeAssignPrimaryTokenPrivilege
Replace
a process level token
Disabled
SeShutdownPrivilege
Shut down the system
Disabled
SeChangeNotifyPrivilege
Bypass traverse checking
Enabled
SeUndockPrivilege
Remove computer from docking
station Disabled
SeIncreaseWorkingSetPrivilege
Increase
a process working set
Disabled
SeTimeZonePrivilege
Change the time zone
Disabled
The resulting result is this:
LookupPrivilegeValue
OK: [1] STATUS: [0], Error: [0]
AdjustTokenPrivileges OK:
[1] STATUS: [0], Error: [0]
LogonUser
OK: [1] STATUS: [0], Error: [0]
CreateProcessAsUser
OK: [1] STATUS: [0], Error: [0]
Every ok is 1, every status is 0 and there are no errors.
Do note that starting a process as another user this way leads to interesting display issues when used interactively. This is apparently really something services are supposed to do rather than interactive programs. This is probably also why the LocalService and NetworkService accounts hold this privilege by default.
*A tautology is something that is tautological.