Windows Privileges Explained: SeDebugPrivilege and AdjustTokenPrivileges in C++

Author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.

Pavel Yosifovich, co-author of the Microsoft Press Windows Internals books, continues his walk through Windows security with a topic that trips up a lot of developers: privileges. In earlier posts we looked at Windows logon sessions and tokens, and privileges are one of the things stored inside those tokens. In this post I want to cover what a privilege actually is, how it differs from ordinary object access, where you can see the privileges on your system, and how you enable one from code. This is aimed at Windows developers and security researchers who want to understand why an API returns access denied even when they think they have the rights to make the call.

What Is a Privilege?

A privilege is a right to do something. Not something specific to a particular object, but something more generic in nature. The classic example is the ability to shut down the computer. You might be thinking, can I not just use the Start button to shut down the machine? Yes, you can, but only because the shutdown privilege is granted to all users by default. It does not have to be. On some extremely critical system, where shutting it down maliciously or accidentally would be a serious problem, you might strip that privilege from most users.

There are more powerful examples. The debug privilege lets you debug a process in a different session. If the process is in your own session you can debug it anyway, but if it is a Windows service running in session zero and your debugger is in session one, you cannot touch it unless you hold the debug privilege, which by default is granted to administrators. Take ownership is another one. It lets a user become the owner of a kernel object, and the owner can always change who can do what with that object, essentially handing access to anyone they like, including themselves. The point of a privilege like that is to make sure an object is never truly inaccessible. Even if an object has a security descriptor that grants no one access, a user with take ownership can become the owner and then decide access from there. The classic case is a user who leaves the company: their Pictures or Documents folders are now holding data nobody can reach, because you do not have that user’s credentials. An administrator uses take ownership to become the owner of those directories and then does whatever is needed with them.

Changing the system time is a privilege. Loading and unloading drivers is a privilege, granted to administrators by default. There is a long list, and I am not going to cover all of them. What matters is the shape of the thing: a privilege is a system-wide power that bypasses the normal per-object security checks.

Windows master developer badge 1

$2,111

$1,478 or $150 X 10 payments

Windows Master Developer

Takes you from a “generic” C programmer to a master Windows programmer in user mode and kernel mode.

Privileges Live in Tokens

Privileges are stored in the access token, which is why they only make sense once you already have a token. So how do you assign them? An administrator uses the Local Security Policy editor for a local machine, or Active Directory for domain management, and grants or removes privileges from a user. Here is the catch that surprises people: this only takes effect the next time that user logs off and logs on again. There is no way to add a privilege to a token that is already alive. The refreshed set of privileges is read from the user database at logon time, and after that the token’s list is fixed. You cannot add to it while the token is running.

Most privileges are disabled by default. Even when a privilege is present in your token, you usually have to enable it before you can use it. Since the privilege is already part of your token, enabling it cannot fail. So why the extra step? The idea is that you do not want a powerful privilege being used by accident, without proper intention. So you have to enable it explicitly. If you do not, the API you call next may simply fail and tell you that you do not have the privilege. That error is a little misleading: it means either you genuinely do not have the privilege, or you have it but it is not currently enabled. A few APIs are nice enough to look in your token, see that the privilege is present, enable it, do the work, and disable it again. Most are not that nice and rely on you to enable it yourself first.

Where Can You See the Privileges on Your System?

There are a few ways. For a specific process, double-click it in Process Explorer and go to the Security tab. Remember that the Security tab is really a token tab, so it shows properties that are part of the token, and one of those is the list of privileges. A standard user Explorer process has a very short list: it can shut down the computer, change the time zone (but not the time, interestingly), and it has the increase working set and change notify privileges. Take something running as SYSTEM, like LSASS, and the list is long. LSASS holds the backup privilege, which lets you read any file regardless of its security, and the create token privilege, which lets you build a token from scratch. That last one is not available to anyone by default except LSASS, for the obvious reason that if you can build a token from scratch you can put any privileges you like into it and become a super creature on the machine.

To see every privilege the system supports, open the Local Security Policy editor and go to Local Policies, then User Rights Assignment. For each entry, you see the users or groups that currently have it. Take ownership, for example, is assigned to Administrators by default. You can add a user or group here if you are an administrator, and again, the change only applies the next time that user logs on. Each entry also has an Explanation tab that describes what the privilege means, which is often enough to understand it without hunting through the documentation.

What Is the Difference Between a Privilege and a User Right?

That User Rights Assignment list is a little confusing, because it contains two different kinds of things. Some entries are true privileges. Others are user rights, like “Deny log on locally” or “Allow log on through Remote Desktop.” Those are not strictly privileges, and here is why. A user right applies before a user logs in, and if a user has not logged in yet there is no token. Privileges live in a token. No token, no privileges. A user right is about the ability to log on in the first place, which is part of the user database, not the token. Once you have logged on you have already passed those checks, and from that point privileges take over because now you have a token. So the guest account being denied local logon is a user right, not a privilege, and you will never find it inside a token.

One more detail worth noting: some privileges have no users assigned at all, and that is perfectly fine. “Act as part of the operating system,” known as the SeTcbPrivilege (trusted computing base), is so powerful that no user gets it by default. It effectively lets you impersonate anyone and do anything on the system. Services running as Local System, Local Service, or Network Service can always request the privileges they need, so they do not depend on this list.

How Do You Hit a Privilege Problem in Real Code?

Let me show where this bites you in practice. I have a small console app that enumerates processes with the tool help API. I covered CreateToolhelp32Snapshot and walking the PROCESSENTRY32 list in an older video, so I just reused that code. Running it gives me every process with its ID, parent ID, thread count, and short executable name. All of that comes for free, with no special privilege required, which is nice.

Now I want more: the full path of each executable, not just the short name. PROCESSENTRY32 does not carry that, so I have to open a handle to the process. I call OpenProcess asking for PROCESS_QUERY_LIMITED_INFORMATION, which is the minimum access that lets the next call work, and then I call QueryFullProcessImageName to get the path.

HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
    FALSE, pe.th32ProcessID);
if (hProcess) {
    WCHAR path[MAX_PATH];
    DWORD size = MAX_PATH;
    if (QueryFullProcessImageName(hProcess, 0, path, &size))
        printf("%ws\n", path);
    else
        printf("\tError in query path: %u\n", GetLastError());
    CloseHandle(hProcess);
} else {
    printf("\tError in open process: %u\n", GetLastError());
}

Run it as a normal user and some processes come back fine, but many fail with error 5, which by now should be very familiar: access denied. We simply do not have enough access to open a handle to those processes. The obvious thing to try is to run as administrator, so I restart Visual Studio elevated and run again. Now almost everything works. A few processes still return error 31 (the registry process, Secure System, System), but that is a generic “no executable associated” error, because those are kernel processes with no real image path to read. That is expected.

Here is where it gets interesting. If I open a separate elevated command prompt and run the same executable from there, some processes fail again with access denied: CSRSS, the font driver host, and a handful of others. Same binary, also elevated, but different results. How can that be? The difference is not administrator versus standard user. The difference is privileges.

How Do You Enable SeDebugPrivilege?

Run whoami /priv in each window and the mystery clears up. Anything I launch from a command prompt inherits that prompt’s token, because the new process gets a duplicate of it. In the plain elevated command prompt, the debug privilege is present but disabled. In the command prompt that Visual Studio opened, the debug privilege is enabled, because Visual Studio running elevated enables it so it can debug services in session zero under Local System. Those OpenProcess calls against certain processes succeed only when the debug privilege is enabled. Having it in the token is not enough. You have to turn it on.

So let me write a small function that enables a privilege by name. Privileges are exposed in the Win32 API as strings, which is a bit unfortunate, because internally they are just small numbers. The native API has nicer functions to enable a single privilege, but I will use the Win32 API here to show the full dance.

First, open your own process token with OpenProcessToken, asking for TOKEN_ADJUST_PRIVILEGES. Then convert the privilege’s string name into a LUID with LookupPrivilegeValue. We met LUIDs when we talked about logon sessions: every logon session has a unique LUID, and privileges are identified the same way, with a locally unique ID that is valid for as long as the system is alive. That is why you have to look it up every single time rather than hardcoding a number.

bool EnablePrivilege(PCWSTR privilegeName) {
    HANDLE hToken;
    if (!OpenProcessToken(GetCurrentProcess(),
            TOKEN_ADJUST_PRIVILEGES, &hToken))
        return false;

    LUID luid;
    if (!LookupPrivilegeValue(nullptr, privilegeName, &luid)) {
        CloseHandle(hToken);
        return false;
    }

    TOKEN_PRIVILEGES tp;
    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;  // 0 to disable

    AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp),
        nullptr, nullptr);
    bool result = GetLastError() == ERROR_SUCCESS;

    CloseHandle(hToken);
    return result;
}

The TOKEN_PRIVILEGES structure lets you adjust several privileges in one call. We only need one, and the structure conveniently holds room for one entry without any dynamic allocation. Set Attributes to SE_PRIVILEGE_ENABLED to enable, or 0 to disable. Then call AdjustTokenPrivileges. The second argument, FALSE, means do not disable all privileges in one sweep, which is definitely not what we want here.

AdjustTokenPrivileges has a genuinely weird quirk: it can return success even when it did not actually adjust every privilege you asked for. To really know it worked, you have to check that GetLastError() returns ERROR_SUCCESS after the call. A partial success, where some privileges were not held, comes back with ERROR_NOT_ALL_ASSIGNED. So the honest way to report success is to look at the last error, not just the boolean return.

Now I call EnablePrivilege(SE_DEBUG_NAME) at the top of the program. These SE_*_NAME constants are the string names the Win32 API expects. With the debug privilege enabled, I go back to the plain command prompt where it was disabled before, run the process enumerator again, and every path resolves. No more access denied on CSRSS or the others, because the privilege is now enabled.

What Happens Without the Privilege?

Enabling only works if the privilege is actually in your token. Run the same program as a standard user, with no administrator rights, and EnablePrivilege fails, because the debug privilege is not present in the token at all. There is nothing to enable. So it is worth reporting that honestly rather than silently pressing on:

if (!EnablePrivilege(SE_DEBUG_NAME))
    printf("Debug privilege not available\n");

Run that as a normal user and you get the message, followed by the familiar list of access denied errors. Privileges have to be enabled before use, but if they do not exist in the token, no amount of enabling will conjure them.

What This Means Practically

  • Treat access denied from OpenProcess as a privilege question, not just a “run as admin” question. Two elevated processes can get different results because one has SeDebugPrivilege enabled and the other does not.
  • Enable a privilege in code with OpenProcessToken for TOKEN_ADJUST_PRIVILEGES, LookupPrivilegeValue to get the LUID, and AdjustTokenPrivileges with SE_PRIVILEGE_ENABLED. Always check GetLastError() == ERROR_SUCCESS, because the boolean return alone can lie.
  • Remember that privileges live in the token and are fixed at logon. You can enable a privilege you already hold, but you cannot add a new one to a live token. Granting one through the Local Security Policy editor only applies after the user logs off and back on.
  • Distinguish privileges from user rights. User rights (like log on locally) govern whether a user can create a session at all and live in the user database, not the token.
  • Audit powerful privileges when you assess a system. Backup, restore, take ownership, load driver, and create token effectively bypass normal security, and holding any of them is a meaningful capability.

Keep Learning

If you want to understand Windows security internals, tokens, and the Win32 APIs behind features like this, these TrainSec courses go deeper:

Related reading in the knowledge library: Windows logon sessions and tokens, which explains where tokens come from and how LUIDs identify logon sessions, and DLL injection with Windows Application Verifier, another look at what it takes to reach into another process.


blue depth

About the author

Pavel Yosifovich has 25+ years as Software developer, trainer, consultant, author, and speaker. Co-author of “Windows Internals”. Author of “Windows Kernel Programming”, “Windows 10 System Programming, as well as System and kernel programming courses and “Windows Internals” series.