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.