How come a duplicated token doesn’t behave identically to the original?


A customer was experimenting with tokens and discovered that things fall apart when they have a thread impersonate itself. Shouldn't that have no effect? Here's what they discovered. Error checking and cleanup have been elided for expository purposes.

// This call succeeds
CComPtr<IUnknown> something;
CoCreateInstance(CLSID_Something, nullptr,
                 CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&something));

// Get the current token for the thread.
// This call also succeeds. (Note that OpenThreadToken
// fails if the thread is not impersonating.)
HANDLE token;
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, TRUE, &token);

// Duplicate the token. This call succeeds.
HANDLE dupToken;
DuplicateToken(token, SecurityImpersonation, &dupToken);

// Impersonate the duplicate. This call succeeds.
ImpersonateLoggedOnUser(dupToken);

// But now, CoCreateInstance fails with E_ACCESSDENIED!
CComPtr<IUnknown> something2;
CoCreateInstance(CLSID_Something, nullptr,
                 CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&something2));

The Duplicate­Token function says that the new token duplicates the original, but it does not appear to be a true duplicate because when we swap out the original thread token for the duplicate, things stop working. What's going on?

There are a lot of things in a token. But there's something important that's not in the token.

One of my colleagues from the kernel team explains: When you duplicate a token with the Duplicate­Token function, it creates a new kernel object, namely the token, and the new token is a duplicate of the original. But the new token has its own properties, and the important one here is the security descriptor.

When a new kernel object is created, and you don't provide an explicit security descriptor for the new object, then the object is given a default security descriptor. And that default security descriptor comes from the default DACL of the token that is in effect at the point of the call.

When you apply this rule to tokens, you find that, even though the behavior is consistent with other kernel objects, it also means that it is very easy to create a token that doesn't have access to itself. When you impersonate with that token, bad things happen.

It's like going to the FedEx Office store and giving them a DHL envelope with the instructions, "Please make a copy of this letter." They take the letter out of the envelope, make a copy, and then take the copy and give it to you in a FedEx Office envelope. They copied the letter, like you instructed, but it's in a different envelope.

If you also want to duplicate the security descriptor, you can get the original token's security descriptor with Get­Kernel­Object­Security or Get­Security­Info, and then pass that security descriptor to Duplicate­Token­Ex.

The customer confirmed that the recommendation worked.

// This call succeeds
CComPtr<IUnknown> something;
CoCreateInstance(CLSID_Something, nullptr,
                 CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&something));

// Get the current token for the thread.
// This call also succeeds. (Note that OpenThreadToken
// fails if the thread is not impersonating.)
HANDLE token;
OpenThreadToken(GetCurrentThread(), TOKEN_ALL_ACCESS, TRUE, &token);

//Get the security descriptor for the token.
// This call succeeds.
PACL dacl;
PSECURITY_DESCRIPTOR sd;
GetSecurityInfo(token, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION,
    nullptr, &dacl, &sd);

// Duplicate the token with that security descriptor.
// This call succeeds.
SECURITY_ATTRIBUTES sa = { sizeof(sa), sd, TRUE };
HANDLE dupToken;
DuplicateTokenEx(token, MAXIMUM_ALLOWED, &sa, SecurityImpersonation,
    TokenImpersonation, &dupToken);

// Impersonate the duplicate. This call succeeds.
ImpersonateLoggedOnUser(dupToken);

// CoCreateInstance now succeeds.
CComPtr<IUnknown> something2;
CoCreateInstance(CLSID_Something, nullptr,
                 CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&something2));
Comments (7)
  1. Henri Hein says:

    I have mixed feelings about the default security descriptor. On the one hand, creating an SD is enough work, and would result in enough leaks, that it is probably necessary to accept NULL in all those calls that take an SD. On the other hand, lots of subtle bugs come up because the default security descriptor is sometimes not the right thing, and they are hard to solve, because only those deeply familiar with the system will naturally look towards it as a source of a problem. It can also be intimidating for a developer to contemplate creating one from scratch, and there is not always an object at hand from which to retrieve a copy, as in the above instance. If developers were forced to create an SD more often, they might learn to understand what it is, what it can represent, and when the default one might not be correct.

    1. On the whole I think the balance is firmly on the side of having the defaults. I’m prepared to bet that if we didn’t, 90% of software would use security descriptors consisting of a single ACE granting full access to Everyone, because learning how to do anything else would be too much trouble.

      1. Henri Hein says:

        I absolutely agree. I do wish there were more material around security descriptors. Such as examples, utility/helper classes, and blog posts on gotchas, like this one. The ATL classes are actually decent, but I rarely see them used, or pointed to.

        1. Now that I come to think of it, it *would* be pretty neat if every API function that accepted a SECURITY_ATTRIBUTES structure could accept an SDDL instead, perhaps wrapped in a macro that does the cast and adds a magic value at the beginning for disambiguation. But you know, -100 points and all. :-)

          … and all too many programmers still wouldn’t bother to learn how to use the string, and would just use some example code they found somewhere that sets it to Everyone:(F) or equivalent. :-(

          1. Nick says:

            setacl 777 #jackpot

    2. cheong00 says:

      Except for really simple one, attempt to create new security descriptor instead of copying a security descriptor is bad. In that way you can accidentially erase properties introduced in a new version of Windows. (In similar sense to “newing a DCB and setting it” instead of “calling GetCommState(), copy it and set the desired values” for serial communication)

  2. Killer{R} says:

    Interesting, if pseudohandle returned by GetCurrentThreadToken() will work if thread impersonates token thats inaccessible for open by it?
    Its interesting because same surprise may happen (or happened in some older Windows) with CreateProcessAsUser function if called without proper impersonation – caused created process/thread do not have access rights to themselves, that subsequently causes something (AFAIR again COM stuff) to behave incorrectly. And pseudohandles returned by GetCurrentThread()/Process() worked fine, so that issue was not very obvious.

Comments are closed.

Skip to main content