A user’s SID can change, so make sure to check the SID history


It doesn't happen often, but a user's SID can change. For example, when I started at Microsoft, my account was in the SYS-WIN4 domain, which is where all the people on the Windows 95 team were placed. At some point, that domain was retired, and my account moved to the REDMOND domain. We saw some time ago that the format of a user SID is

S-1- version number (SID_REVISION)
-5- SECURITY_NT_AUTHORITY
-21- SECURITY_NT_NON_UNIQUE
-w-x-y- the entity (machine or domain) that issued the SID
-z the unique user ID for that entity

The issuing entity for a local account on a machine is the machine to which the account belongs. The issuing entity for a domain account is the domain.

If an account moves between domains, the issuing entity changes, which means that the old SID is not valid. A new SID must be issued.

Wait, does this mean that if my account moves between domains, then I lose access to all my old stuff? All my old stuff grants access to my old SID, not my new SID.

Fortunately, this doesn't happen, thanks to the SID history. When your account moves to the new domain, the new domain controller remembers all the previous SIDs you used to have. When you authenticate against the domain controller, it populates your token with your SID history. In my example, it means that my token not only says "This is user number 271828 on the REDMOND domain", it also says "This user used to be known as number 31415 on the SYS-WIN4 domain." That way, when the system sees an object whose ACL says, "Grant access to user 31415 on the SYS-WIN4 domain," then it should grant me access to that object.

The existence of SID history means that recognizing users when they return is more complicated than a simple Equal­Sid, because Equal­Sid will say that "No, S-1-5-21-REDMOND-271828 is not equal to S-1-5-21-SYS-WIN4-31415," even though both SIDs refer to the same person.

If you are going to remember a SID and then try to recognize a user when they return, you need to search the SID history for a match, in case the user changed domains between the two visits. The easiest way to do this is with the Access­Check function. For example, suppose I visited your site while I belong to the SYS-WIN4 domain, and you remembered my SID. When I return, you create a security descriptor that grants access to the SID you remembered, and then you ask Access­Check, "If I had an object that granted access only to this SID, would you let this guy access it?"

(So far, this is just recapping stuff I discussed a few months ago. Now comes the new stuff.)

There are a few ways of building up the security descriptor. In all the cases, we will create a security descriptor that grants the specified SID some arbitrary access, and then we will ask the operating system whether the current user has that access.

My arbitrary access shall be

#define FROB_ACCESS     1 // any single bit less than 65536

One way to build the security descriptor is to let SDDL do the heavy lifting: Generate the string D:(A;;1;;;⟨SID⟩) and then pass it to String­Security­Descriptor­To­Security­Descriptor.

Another is to build it up with security descriptor functions. I defer to the sample code in MSDN for an illustration.

The hard-core way is just to build the security descriptor by hand. For a security descriptor this simple, the direct approach involves the least amount of code. Go figure.

The format of the security descriptor we want to build is

struct ACCESS_ALLOWED_ACE_MAX_SIZE
{
    ACCESS_ALLOWED_ACE Ace;
    BYTE SidExtra[SECURITY_MAX_SID_SIZE - sizeof(DWORD)];
};

The ACCESS_ALLOWED_ACE_MAX_SIZE structure represents the maximum possible size of an ACCESS_ALLOWED_ACE. The ACCESS_ALLOWED_ACE leaves a DWORD for the SID (Sid­Start), so we add additional bytes afterward to accommodate the largest valid SID. If you wanted to be more C++-like, you could make ACCESS_ALLOWED_ACE_MAX_SIZE derive from ACCESS_ALLOWED_ACE.

struct ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR
{
    SECURITY_DESCRIPTOR_RELATIVE Header;
    ACL Acl;
    ACCESS_ALLOWED_ACE_MAX_SIZE Ace;
};

const ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR c_sdTemplate = {
  // SECURITY_DESCRIPTOR_RELATIVE
  {
    SECURITY_DESCRIPTOR_REVISION,           // Revision
    0,                                      // Reserved
    SE_DACL_PRESENT | SE_SELF_RELATIVE,     // Control
    FIELD_OFFSET(ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR, Ace.Ace.SidStart),
                                            // Offset to owner
    FIELD_OFFSET(ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR, Ace.Ace.SidStart),
                                            // Offset to group
    0,                                      // No SACL
    FIELD_OFFSET(ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR, Acl),
                                            // Offset to DACL
  },
  // ACL
  {
    ACL_REVISION,                           // Revision
    0,                                      // Reserved
    sizeof(ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR) -
    FIELD_OFFSET(ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR, Acl),
                                            // ACL size
    1,                                      // ACE count
    0,                                      // Reserved
  },
  // ACCESS_ALLOWED_ACE_MAX_SIZE
  {
    // ACCESS_ALLOWED_ACE
    {
      // ACE_HEADER
      {
        ACCESS_ALLOWED_ACE_TYPE,            // AceType
        0,                                  // flags
        sizeof(ACCESS_ALLOWED_ACE_MAX_SIZE),// ACE size
      },
      FROB_ACCESS,                          // Access mask
    },
  },
};

Our template security descriptor says that it is a self-relative security descriptor with an owner, group and DACL, but no SACL. The DACL consists of a single ACE. We set up everything in the ACE except for the SID. We point the owner and group to that same SID. Therefore, this security descriptor is all ready for action once you fill in the SID.

BOOL IsInSidHistory(HANDLE Token, PSID Sid)
{
  DWORD SidLength = GetLengthSid(Sid);

  if (SidLength > SECURITY_MAX_SID_SIZE) {
    // Invalid SID. That's not good.
    // Somebody is playing with corrupted data.
    // Stop before anything bad happens.
    RaiseFailFastException(nullptr, nullptr, 0);
  }

  ALLOW_ONLY_ONE_SECURITY_DESCRIPTOR Sd = c_sdTemplate;
  CopyMemory(&Sd.Ace.Ace.SidStart, Sid, SidLength);

As you can see, generating the security descriptor is a simple matter of copying our template and then replacing the SID. The next step is performing an access check of the token against that SID.

  const static GENERIC_MAPPING c_GenericMappingFrob = {
    FROB_ACCESS,
    FROB_ACCESS,
    FROB_ACCESS,
    FROB_ACCESS,
  };
  PRIVILEGE_SET PrivilegeSet;
  DWORD PrivilegeSetSize = sizeof(PrivilegeSet);
  DWORD GrantedAccess = 0;
  BOOL AccessStatus = 0;
  return AccessCheck(&Sd, Token, FROB_ACCESS,
    const_cast<PGENERIC_MAPPING>(&c_GenericMappingFrob),
    &PrivilegeSet, &PrivilegeSetSize,
    &GrantedAccess, &AccessStatus) &&
    AccessStatus;
}

So let's take this guy out for a spin. Since I don't know what is in your SID history, I'm going to pick something that should be in your token already (Authenticated Users) and something that shouldn't (Local System).

// Note: Error checking elided for expository purposes.

void CheckWellKnownSid(HANDLE Token, WELL_KNOWN_SID_TYPE type)
{
  BYTE rgbSid[SECURITY_MAX_SID_SIZE];
  DWORD cbSid = sizeof(rgbSid);
  CreateWellKnownSid(type, NULL, rgbSid, &cbSid);
  printf("Is %d in SID history? %d\n", type,
         IsInSidHistory(Token, rgbSid));
}

int __cdecl wmain(int argc, wchar_t **argv)
{
  HANDLE Token;
  // In real life you had better error-check these calls,
  // to avoid a security hole.
  ImpersonateSelf(SecurityImpersonation);
  OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &Token);
  RevertToSelf();

  CheckWellKnownSid(Token, WinAuthenticatedUserSid);
  CheckWellKnownSid(Token, WinLocalSystemSid);
  CloseHandle(Token);

  return 0;
}

Related reading: Hey there token, long time no see! (Did you do something with your hair?)

Comments (23)
  1. Arde says:

    Is there a book on Windows Security programming which explains the security / crypto API? Or rather gives a overview of the best practices? My search turned up nothing useful. MSDN is good for the nitty-gritties, but not for the overall big picture and best practices

  2. Dioksin says:

    Arde, look at this book "Programming Windows Security" by Keith Brown

    http://www.amazon.com/…/0201604426

  3. stickboy says:

    Why can't local users make use of SID history?  What are home users supposed to do when buying a new computer and trying to access data on external storage?  All of this SID business is incredibly convoluted and hard for anyone to get right.  It seems like it'd be simpler to allow administrators to explicitly set a user's SID at account creation time; local administrators can already reset the ownership/permissions on local files anyway.

  4. cheong00 says:

    @stickboy: In local account case, if your user account migration tool is doing things right, the objects transferred to new computer should already be under the new SID, therefore no SID history is needed.

    In the case where Active Directory is involved, since the other machines may not be switched on at the time of SID change, therefore SID history would be necessary.

  5. cheong00 says:

    As for allow administrators to explicitly set user SID, just imagine what happens if you accidentally (or intentionally) choose to create an account with the same SID as someone in domain, or for more complicated cases, a trusted domain in different forest.

  6. kermi says:

    I believe in the case of home users and USB drives, the default NTFS permissions grant RWX permissions to the Users group, which all users are part of by default. Changing computers should not cause problems accessing data, unless you have modified the permissions by hand.

  7. Avenida says:

    I like that you used e and pi as your placeholder numbers.

  8. Zan Lynx' says:

    @Engywuck: There's a security reason for it? I can't imagine…

    As you say, an Admin can overwrite the file permissions. Lesser known methods include using impersonation to read the files without touching the ownership or attaching the disk to a Linux machine and reading it as root.

    Being able to add an old identity to your current identity seems much more secure than those other options.

    [Suppose you connect to a network resource and hand it your token. "Hello, this is WORKGROUPzan, but I used to be known as REDMONDbillg, so, y'know, let me access anything that REDMONDbillg can." -Raymond]
  9. Engywuck says:

    @cheong00: probably stickboy meant NTFS-formatted external (USB-)drives, which obviously get the SID from the local system, but may or may not be accessible at time of migration.

    Those media usually aren't fully usable on other computers anyway, but this may be OK. Otherwise you'd have to use a filesystem like FAT which is writable for anyone accessing the device, but if you don't want to use ExFAT there are some limits in using it (file size and maximum file system (formatting) size being the worst)

    When you migrate the computer (or as most do (re)install) with new OS your old SID are lost and you *have* to go the hard way to access the NTFS-formatted external media (make yourself Owner, then hammer your access through and give new ACLs). There's no (obvious) way to say the computer "oh, btw, I was S-1-5-21-OLDCOMP-1034 before you even existed". Of course, there's a security reason for that, but I see the problem it may cause.  

  10. foo says:

    @Avenida. Hah neat! I skimmed too fast to notice that. But now I see that the 1-5-21 part == binary 1, 101, 10101 sequence.

  11. Joshua says:

    @kermi: I'll bet it does *now* but it didn't always.

  12. cheong00 says:

    @Engywuck: Hope you're aware that "convert.exe" has an option named "/NoSecurity" that'd allow all users full access right to the filesystem.

  13. Engywuck says:

    convert.exe only converts from FAT to NTFS, right? But now you have a – say – 64GB USB Stick (or an external HDD) and want to use it. Happy formatting to FAT32 with windows included tools before converting with convert.exe :-)

    @foo: nice find. Never thought about the numbers before – in hex it's even better: S-0x1-0x5-0x15 :-)

  14. kermi says:

    @Joshua

    Wonder how much time back do we have to go for that not to have happened. As far as i can recall, it hasnt' been issue in the 21st century..

  15. Joshua says:

    @kermi: Windows 2000 did it wrong for sure. I think early XP as well (fixed in SP1?).

    [Suppose you connect to a network resource and hand it your token. "Hello, this is WORKGROUPzan, but I used to be known as REDMONDbillg, so, y'know, let me access anything that REDMONDbillg can." -Raymond]

    Non-issue as SIDs don't cross like that. I can generate token for REDMONDbillg SID directly on a workstation but it doesn't work cross-network either. The only machines that can set it up are the machine it's expected to work on are the machine interpreting the SID or the domain controller thereof.

  16. Erik F says:

    External trusts add another level of complexity as SIDs are quarantined by default, which may prevent SID histories from authenticating resources (see technet.microsoft.com/…/cc755321%28v=ws.10%29.aspx : it's an interesting read!)

  17. Joshua says:

    Wow, the guy who wrote that document actually believes that making it difficult to modify the OS (by means of a disk editor) is a practical security measure. The attack may be difficult but if made attractive somebody will release the tool, just like the password reset tool was released.

  18. cheong00 says:

    @Engywuck: Not only from FAT to NTFS, also from NTFS to "NTFS with full access to everyone". I'm pretty sure I've done this a few time with my flash drives before.

  19. cheong00 says:

    Btw, even without that tool, nothing blocks you to give full access to "Authenticated Users" – it's SID is the same for all system ( msdn.microsoft.com/…/aa379649(v=vs.85).aspx ) to the base folder of the flash drive, and let folders inherit that access.

  20. kermi says:

    @Joshua

    Well it wasn't really a problem on XP, since admins have full controll over everything, and everyone was part of admin group :D

  21. ROT-13 says:

    Well thank goodness I encrypt all me lucky charms.

  22. Engywuck says:

    @cheong00: "Volume M: is already a NTFS-Filesystem" (translated) – so convert.exe really only likes FAT, at least on Win7

    I've also tested the whole NTFS formatting on external drives with Win7 Pro:

    – on USB sticks it sets Everyone: (OI)(CI)(F) ("Full Access, Inheritable")

    – on external (USB-)HDDs it sets Administrators and SYSTEM: (F) and (OI)(CI)(IO)(F) [Full, Inheritable], Authenticated Users: (M) and (OI)(CI)(IO)(M) [Modify, Inheritable], Users: (RX) and (OI)(CI)(IO)(GR,GE) [Read, Execute, Inheritable]

    I concur now that everyone should be able to use it on any computer by default. Could have sworn it wasn't like that, but maybe I (mis)remember some XP formatted drive. If that's the case then it's an example of the many small changes to make Windows better with each new iteration that nobody really notices ;-)

    Can't test with Win8 or Server 2k12 right now – all machines are virtual :-)

  23. Erik F says:

    @Engywuck: I'm pretty sure that devices recognized as HDDs have always had stricter permissions on the assumption that they are not shared between computers; a system partition with Everyone: (OI)(CI)(F) at the root seems like a bad idea IMO. You can always do "cacls m: /g everyone:f" to get the effect that you want in any case.

Comments are closed.

Skip to main content