SeAssignPrimaryTokenPrivilege

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.

 © Andrew Brehm 2016