Home > Articles

Security Model

  • Print
  • + Share This

Security Model

In This Chapter

  • Securing Objects

  • Components

  • The Flow of a User Logon

In this chapter, I focus on the Windows 2000 internal components that provide security. First, I look at the schema Windows 2000 uses to protect objects, and then I look at the mechanisms that enforce those protections.

It's important to keep in mind where the objects I discuss actually live—in user memory or (protected) kernel memory.

The answer, not surprisingly, is that these objects are located in a little bit of both spaces. Like the process, thread, and job objects you saw in Chapter 2, "Processes and Threads," both the kernel and the user-mode portion of the Win32 subsystem keep security information.

For the most part, the information kept in kernel space is the "real thing," and the user-mode structures are just used to pass information back and forth to the kernel. This is clear when you consider that security descriptors are attached to kernel objects, which live in kernel space, and tokens are kernel objects themselves. All of this will become clear in the following pages.

Securing Objects

At the heart of the Windows 2000 security model are Security Descriptors (SDs) and Access Control Lists (ACLs). Every securable object (files, devices, pipes, processes, threads, timers, printers, you name it) has a security descriptor attached to it. A security descriptor contains the following pieces of information:

  • The SID of the object owner

  • The SID of the primary owning group

  • Discretionary Access Control List (DACL)

  • System Access Control List (SACL)

WINNT.H, which is included in the Windows SDK, contains the SECURITY_DESCRIPTOR structure, as well as a brief explanation of the fields:

typedef struct _SECURITY_DESCRIPTOR {
  BYTE Revision;
  BYTE Sbz1;
  PSID Owner;
  PSID Group;
  PACL Sacl;
  PACL Dacl;

// Where:
//   Revision - Contains the revision level of the security
//     descriptor. This allows this structure to be passed between
//     systems or stored on disk even though it is expected to
//     change in the future.
//   Control - A set of flags that qualify the meaning of the
//     security descriptor or individual fields of the security
//     descriptor.
//   Owner - A pointer to an SID representing an object's owner.
//     If this field is null, then no owner SID is present in the
//     security descriptor. If the security descriptor is in
//     self-relative form, then this field contains an offset to
//     the SID, rather than a pointer.
//   Group - A pointer to an SID representing an object's primary
//     group. If this field is null, then no primary group SID is
//     present in the security descriptor. If the security descriptor
//     is in self-relative form, then this field contains an offset to
//     the SID, rather than a pointer.
//   Sacl - A pointer to a system ACL. This field value is only
//     valid if the DaclPresent control flag is set. If the
//     SaclPresent flag is set and this field is null, then a null
//     ACL is specified. If the security descriptor is in
//     self-relative form, then this field contains an offset to
//     the ACL, rather than a pointer.
//   Dacl - A pointer to a discretionary ACL. This field value is
//     only valid if the DaclPresent control flag is set. If the
//     DaclPresent flag is set and this field is null, then a null
//     ACL (unconditionally granting access) is specified. If the
//     security descriptor is in self-relative form, then this field
//     contains an offset to the ACL, rather than a pointer.

The only thing I can add to this discussion is that the group SID is only used by POSIX applications.


The POSIX standard requires that an object can be owned by a group. Folks familiar with UNIX will recognize this. Windows 2000, being POSIX-compliant, supports this notion, but it is not used in Win32 applications nor by the system.

Now that you're wondering what the heck an SID is, take a look.


Ever feel as if you were just a number? Well, in Windows 2000, that's exactly what you are. Internally, Windows 2000 represents each account, group, machine, and domain with a security identifier, or SID. The SID is independent of the account name. You'll recall that if you delete an account, Windows displays the warning message shown in Figure 3.1.

Figure 3.1 Windows 2000 alludes to the existence of SIDs when you attempt to delete an account.

Here, I have assembled some pieces of WINNT.H so you can see how SIDs are defined:

// Pictorially the structure of an SID is as follows:
//     1  1  1  1  1  1
//     5  4  3  2  1  0  9  8  7  6  5  4  3  2  1  0
//   +---------------------------------------------------------------+
//   |   SubAuthorityCount    |Reserved1 (SBZ)|  Revision  |
//   +---------------------------------------------------------------+
//   |          IdentifierAuthority[0]           |
//   +---------------------------------------------------------------+
//   |          IdentifierAuthority[1]           |
//   +---------------------------------------------------------------+
//   |          IdentifierAuthority[2]           |
//   +---------------------------------------------------------------+
//   |                                |
//   +- - - - - - - - SubAuthority[] - - - - - - - - -+
//   |                                |
//   +---------------------------------------------------------------+

  BYTE Value[6];

typedef struct _SID {
  BYTE Revision;
  BYTE SubAuthorityCount;
#ifdef MIDL_PASS
  [size_is(SubAuthorityCount)] DWORD SubAuthority[*];
#else // MIDL_PASS
#endif // MIDL_PASS

In English, an SID is a variable-length numeric structure that contains the following fields (right-to-left in the pictorial):

  • 8-bit SID revision level

  • 8-bit count of the number of subauthorities contained within

  • 48 bits containing up to three identifier authority SIDs

  • Any number of subauthority SIDs and relative identifiers (RIDs)

Let's take apart an actual SID to make this more concrete. In regedt32, I look at an SID on my local machine in the HKEY_USERS subtree (see Figure 3.2). The name of the subtree is the user's SID.

Figure 3.2 The HKEY_USERS Registry subtree contains a key for each local user.

You can see the textual representation of my SID is


I say textual representation because the S prefix and hyphens separating the fields are added to make SIDs more readable. The true internal representation is just a bunch of numbers all run together.

The SID has a revision level of 1. You can see from WINNT.H that Windows 2000's current revision level is 1:

#define SID_REVISION           (1)  // Current revision level

What follows are three identifier authorities: namely 5 and 21. There are several built-in authorities, as shown in Table 3.1.

Table 3.1 Built-in Identifier Authorities

Identifier Authority












Back to the SID, 5 means that this SID was assigned by the Windows 2000 security authority (as most are), and 21 means simply that this is not a built-in SID. Windows 2000 calls these non-unique, which means that a relative identifier (RID) is required to make the SID unique. You'll see this in just a second.

The three subauthority values identify the local machine and the domain (if any) the machine belongs to. In the example, all accounts on my machine share the same three subauthorities.

At setup, Windows 2000 creates a random-base SID based on a number of items, including the current date and time and Ethernet address (if available). Windows goes to great pains to make sure this is a globally unique identifier. It is very unlikely that any machine anywhere in the world has the same base SID.

The last chunk, namely the 1000, is the RID. This number is tacked on to the end of the machine SID to create unique SIDs for users and groups. Windows starts numbering at 1,000, so this account is the first account created on this machine. The next account or group created would have the RID 1001, and so on. Voila, we have a unique identifier.

There are a number of predefined (or built-in) SIDs that serve special roles within Windows. Again, WINNT.H serves as the reference here:

//                                                                         //
// Universal well-known SIDs                                               //
//                                                                         //
//   Null SID           S-1-0-0                                            //
//   World            S-1-1-0                                              //
//   Local            S-1-2-0                                              //
//   Creator Owner ID       S-1-3-0                                        //
//   Creator Group ID       S-1-3-1                                        //
//   Creator Owner Server ID   S-1-3-2                                     //
//   Creator Group Server ID   S-1-3-3                                     //
//                                                                         //
//   (Non-unique IDs)       S-1-4                                          //
//                                                                         //

//                                                                         //
// NT well-known SIDs                                                      //
//                                                                         //
//   NT Authority     S-1-5                                                //
//   Dialup        S-1-5-1                                                 //
//                                                                         //
//   Network        S-1-5-2                                                //
//   Batch         S-1-5-3                                                 //
//   Interactive      S-1-5-4                                              //
//   Service        S-1-5-6                                                //
//   AnonymousLogon    S-1-5-7    (aka null logon session)                 //
//   Proxy         S-1-5-8                                                 //
//   ServerLogon      S-1-5-9    (aka domain controller account)           //
//   Self         S-1-5-10   (self RID)                                    //
//   Authenticated User  S-1-5-11   (Authenticated user somewhere)         //
//   Restricted Code    S-1-5-12   (Running restricted code)               //
//                                                                         //
//   (Logon IDs)      S-1-5-5-X-Y                                          //
//                                                                         //
//   (NT non-unique IDs)  S-1-5-0x15-...                                   //
//                                                                         //
//   (Built-in domain)   s-1-5-0x20                                        //
//                                                                         //

You'll notice the NT non-unique IDs) S-1-5-0x15-... reference. That matches the SID I discuss as hexadecimal 15 = 21. So an S-1-5-21-... SID is an NT non-unique SID that receives a RID to make it unique. This schema describes all user and group accounts created by the administrator.

Similarly, there are built-in RIDs that you must recognize. They are usually referred to as "well-known" SIDs or RIDs because the SID can easily be determined. The nice source commenting in WINNT.H you saw earlier breaks down at this part, so I created some tables to illustrate. Table 3.2 identifies the well-known user RIDs.

Table 3.2 Well-Known User RIDs









That means that on my machine, the SID of the local administrator account is


The SIDs of the guest account and Kerberos TGT are also well known. Table 3.3 identifies the well-known group RIDs.

Table 3.3 Well-Known Group RIDs



















You see the same idea here. The SID of my local administrators group is


Table 3.4 identifies the well-known alias RIDs.

Table 3.4 Well-Known Alias RIDs























Here you see the aliases that any NT administrator is familiar with. These too are well-known SIDs. Table 3.5 outlines miscellaneous reserved RIDs.

Table 3.5 Miscellaneous reserved RIDs



RESERVED (previous tables)






First user/group


Second user/group


...and so on



Are these well-known SIDs a security risk? Well, the answer is "It depends." If your machine has NetBIOS open to the outside world, then yes, it is a risk. It is trivial for a cracker to enumerate the account names and SIDs and thus find the real administrator account name, even if it was renamed (which some security checklists recommend). However, if NetBIOS is blocked to the outside world (as it should be), the SIDs aren't accessible and there is no risk.

Now that you're comfortable with SIDs, take a look at the other cornerstone of Windows 2000 security: Access Control Lists.

Access Control Lists (ACLs)

Access Control Lists (ACLs) contain the actual permissions assigned to an object, as well as audit instructions for the kernel. An ACL consists of a header followed by zero or more Access Control Entries (ACEs). An ACL with zero ACEs is called a null ACL.

There are two types of ACLs: discretionary ACLs (DACLs) and system ACLs (SACLs). DACLs define access permissions to the object they protect, whereas SACLs contain audit instructions for the system.

Again, turn to WINNT.H for the definition of an ACL:

// Define an ACL and the ACE format. The structure of an ACL header
// followed by one or more ACEs. Pictorially the structure of an ACL header
// is as follows:
//    3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//    1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//   +-------------------------------+---------------+---------------+
//   |      AclSize      |   Sbz1   | AclRevision |
//   +-------------------------------+---------------+---------------+
//   |       Sbz2       |      AceCount      |
//   +-------------------------------+-------------------------------+
// The current AclRevision is defined to be ACL_REVISION.
// AclSize is the size, in bytes, allocated for the ACL. This includes
// the ACL header, ACES, and remaining free space in the buffer.
// AceCount is the number of ACES in the ACL.

typedef struct _ACL {
  BYTE AclRevision;
  BYTE Sbz1;
  WORD  AclSize;
  WORD  AceCount;
  WORD  Sbz2;
} ACL;
typedef ACL *PACL;

Similarly, the definition of an ACE follows:

// The structure of an ACE is a common ace header followed by ace type
// specific data. Pictorially the structure of the common ace header is
// as follows:
//    3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//    1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//   +---------------+-------+-------+---------------+---------------+
//   |      AceSize      |  AceFlags  |   AceType  |
//   +---------------+-------+-------+---------------+---------------+
// AceType denotes the type of the ace; there are some predefined ace
// types
// AceSize is the size, in bytes, of ace.
// AceFlags are the Ace flags for audit and inheritance, defined shortly.

typedef struct _ACE_HEADER {
  BYTE AceType;
  BYTE AceFlags;
  WORD  AceSize;

Notice that in the ACL header, the size field is defined to contain the size of the ACL header plus ACEs plus any extra space. A whole ACL looks like Figure 3.3.

Figure 3.3 A complete ACL.

Currently, there are four defined ACE structures, outlined in Table 3.6. The type of ACE determines whether the ACL is system (SACL) or discretionary (DACL).

Table 3.6 Currently Defined ACE Types


Makes ACL type


Discretionary (DACL)


Discretionary (DACL)


System (SACL)


System (SACL)

Even though this mysterious SYSTEM_ALARM_ACE type is defined, it is not yet supported in Windows 2000.

Let's look at the ACCESS_ALLOWED_ACE type:

typedef struct _ACCESS_ALLOWED_ACE {
  ACE_HEADER Header;
  DWORD SidStart;

You see here that an ACCESS_ALLOWED_ACE contains the generic ACE header, which you just saw, as well as an ACCESS_MASK and an SID.

You can guess the story here: The existence of an ACCESS_ALLOWED_ACE grants the access specified by the ACCESS_MASK to the user or group identified by the SID. Similarly, the existence of a SYSTEM_AUDIT_ACE causes the system to log an event to the security audit log when the user or group specified by the SID requests the access specified in the ACCESS_MASK. Pretty straightforward. You can imagine that the ACCESS_DENIED_ACE and the eventual ACCESS_ALARM_ACE ACEs do pretty much the same thing.

For a discussion of access masks, turn to WINNT.H:

// Define the access mask as a longword sized structure divided up as
// follows://
//    3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//    1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//   +---------------+---------------+-------------------------------+
//   |G|G|G|G|Res'd|A| StandardRights|     SpecificRights    |
//   |R|W|E|A|   |S|        |                |
//   +-+-------------+---------------+-------------------------------+
//   typedef struct _ACCESS_MASK {
//     WORD  SpecificRights;
//     BYTE StandardRights;
//     BYTE AccessSystemAcl : 1;
//     BYTE Reserved : 3;
//     BYTE GenericAll : 1;
//     BYTE GenericExecute : 1;
//     BYTE GenericWrite : 1;
//     BYTE GenericRead : 1;
// But to make life simple for programmers we'll allow them to specify
// a desired access mask by simply OR'ing together multiple single rights
// and treat an access mask as a DWORD. For example
//   DesiredAccess = DELETE | READ_CONTROL
// So we'll declare ACCESS_MASK as DWORD


This is the overall structure of the access mask. You can see that there are bits for generic read, write, execute, and all privileges. It is also possible to specify specific rights that vary depending on the object type. For example, printers have different rights (clear queue, change paper to tray assignments, and so on) than files do. The specific rights are defined on a per-object-type basis. Programmers should consult the SDK for information on specific objects or on how to create specific rights for your objects.

I have some special cases to address for completeness. If an object's SD contains no DACL, then everyone has full access to the object. This is important to keep in mind. On the other hand, if the DACL is null (contains 0 ACEs), then no one has access to the object (except the owner, as you'll see in the next section).

If the SACL is null (or contains no ACEs), then no auditing occurs.

Assigning and Inheriting ACLs and ACEs

The algorithm for assigning ACLs to new objects follows:

  1. If the caller explicitly provides an SD when creating the object, that SD is used if possible (as long as the caller has sufficient rights).

  2. Any ACEs marked for mandatory inheritance in the object hierarchy above our new object are included.

  3. If the caller didn't specify an SD, the object manager searches for ACEs marked as inheritable in the object hierarchy above our new object and includes those.

  4. If none of the preceding steps applies, the default ACL from the caller's token is applied. This is the most common occurrence.

Windows 2000 contains some significant improvements over previous versions of Windows NT when it comes to ACL inheritance. In previous versions, ACEs could only be inherited at object creation or when a new ACL was explicitly set on an existing object. The system did not propagate inheritable ACEs to child objects. Moreover, the system did not differentiate between inherited and directly applied ACEs—so an object was unable to protect itself from inherited ACEs.

In Windows 2000, the SetNamedSecurityInfoEx() and SetSecurityInfoEx() functions support automatic propagation of inheritable ACEs. For example, if you use these functions to add an inheritable ACE to a directory in an NTFS volume, the system applies the ACE as appropriate to the ACLs of any subdirectories and files.

Win2K also introduces a new inheritance model where directly assigned ACEs have precedence over inherited ACEs. This is accomplished by adjusting the order of the ACEs in the ACL. To ensure that non-inherited ACEs have priority over inherited ACEs, all non-inherited ACEs precede all inherited ACEs. This ordering ensures, for example, that a non-inherited access-denied ACE is enforced regardless of any inherited ACE that allows access.

With the new inheritance model comes a few additional particulars:

  • If a child object with no DACL inherits an ACE, the result is a child object with a DACL containing only the inherited ACE.

  • If a child object with an empty DACL inherits an ACE, the result is a child object with a DACL containing only the inherited ACE.

  • If you remove an inheritable ACE from a parent object, automatic inheritance removes any copies of the ACE inherited by child objects.

  • If automatic inheritance results in the removal of all ACEs from a child object's DACL, the child object has an empty DACL rather than no DACL.

These rules can have the unexpected result of converting an object with no DACL to an object with an empty DACL. You'll recall that there is a big difference here: An object with no DACL allows full access, but an object with an empty DACL allows no access. To ensure that inheritable ACEs do not affect a child object with no DACL, set the SE_DACL_PROTECTED flag in the object's security descriptor.

SD/ACL/ACE Summary

Summing up, Figure 3.4 shows a few hypothetical objects, their security, and the effects on the system.

Figure 3.4 Some ACLs containing useful ACEs.


If security descriptors and ACLs are the locks, then tokens are the keys. Each process and thread has a token, which contains the SID of the process's owner as well as the SIDs of the groups to which they belong.

The system uses an access token to identify the user when a thread tries to obtain a handle to a securable object or tries to perform a privileged system task. Access tokens contain the following information:

  • The SID of the logon account

  • SIDs for the groups the account is a member of

  • A logon SID that identifies the current logon session

  • A list of the privileges held by either the user or the user's groups

  • The SID for the primary group

  • The default DACL that the system uses when the user creates a securable object without specifying a security descriptor

  • The source of the access token

  • Whether the token is a primary or impersonation token

  • An optional list of restricting SIDs

  • Current impersonation levels

  • Other statistics

The kernel's security reference monitor (covered later in this chapter) compares the SID in the token with the SIDS in the ACLs to determine whether access will be permitted, whether auditing will be performed, and so on.

Let's look at a token. Instead of looking at the Win32 structures, I use the kernel debugger's !tokenfields command. The Win32 structures are similar, but for the illustration the !tokenfields output is more concise. Remember, the values shown are the offsets to the fields, not actual values. This is just what the kernel structure looks like:

kd> !tokenfields
  TOKEN structure offsets:
  TokenSource:      0x0
  AuthenticationId:   0x18
  ExpirationTime:    0x28
  ModifiedId:      0x30
  UserAndGroupCount:   0x3c
  PrivilegeCount:    0x44
  VariableLength:    0x48
  DynamicCharged:    0x4c
  DynamicAvailable:   0x50
  DefaultOwnerIndex:   0x54
  DefaultDacl:      0x6c
  TokenType:       0x70
  ImpersonationLevel:  0x74
  TokenFlags:      0x78
  TokenInUse:      0x79
  ProxyData:       0x7c
  AuditData:       0x80
  VariablePart:     0x84

In the token you find all the fields you expect plus a few additional fields for housekeeping purposes.

Using the kernel debugger, let's take apart the token associated with the WINWORD.EXE process I am currently using to write this book:

PROCESS fdd4b020 Cid: 02a0  Peb: 7ffdf000 ParentCid: 0280
  DirBase: 02023000 ObjectTable: fdd24a48 TableSize: 98.
  Image: winword.exe
  VadRoot fdd2b388 Clone 0 Private 348. Modified 3. Locked 0.
  DeviceMap fdf14128
  Token               e1eec030
  ElapsedTime            0:02:04.0539
  UserTime             0:00:00.0270
  KernelTime            0:00:00.0600
  QuotaPoolUsage[PagedPool]     37828
  QuotaPoolUsage[NonPagedPool]   5540
  Working Set Sizes (now,min,max) (1325, 50, 345) (5300KB, 200KB, 1380KB)
  PeakWorkingSetSize        1325
  VirtualSize            40 Mb
  PeakVirtualSize          46 Mb
  PageFaultCount          1500
  MemoryPriority          FOREGROUND
  BasePriority           8
  CommitCharge           425

Looking at the process object, you see the token is stored at 0xe1eec030. Using the !token extension to elaborate

kd> !token e1eec030
TOKEN e1eec030 Flags: 9 Source User32 \ AuthentId (0, 5ceb)
  Type:          Primary (IN USE)
  Token ID:        127b6
  ParentToken ID:     0
  Modified ID:       (0, a6a8)
  TokenFlags:       0x9
  SidCount:        10
  Sids:          e1eec180
  RestrictedSidCount:   0
  RestrictedSids:     0
  PrivilegeCount:     17
  Privileges:       e1eec0b4

Here is the actual token and its values. This is a primary token because the WinWord process isn't impersonating anyone. (I get to impersonation in a moment.)

Most interesting are the SIDs, which I explore here. First, I display the dwords (with the kernel debugger dd command) starting at the memory location specified in the token, namely 0xe1eec180:

kd> dd e1eec180
e1eec180 e1eec1d0 00000000 e1eec1ec 00000007
e1eec190 e1eec208 00000007 e1eec214 0000000f
e1eec1a0 e1eec224 00000007 e1eec234 00000007
e1eec1b0 e1eec244 c0000007 e1eec258 00000007
e1eec1c0 e1eec264 00000007 e1eec270 00000007
e1eec1d0 00000501 05000000 00000015 74d97781
e1eec1e0 65d637a8 3f32a78a 000003e8 00000501
e1eec1f0 05000000 00000015 74d97781 65d637a8

Taking note of the SidCount = 10 in the token, I display the 10 SIDs using the !sid command:

kd> !sid e1eec1d0
SID is: S-1-5-21-1960408961-1708537768-1060284298-1000
kd> !sid e1eec1ec
SID is: S-1-5-21-1960408961-1708537768-1060284298-513
kd> !sid e1eec208
SID is: S-1-1-0
kd> !sid e1eec214
SID is: S-1-5-32-544
kd> !sid e1eec224
SID is: S-1-5-32-547
kd> !sid e1eec234
SID is: S-1-5-32-545
kd> !sid e1eec244
SID is: S-1-5-5-0-23483
kd> !sid e1eec258
SID is: S-1-2-0
kd> !sid e1eec264
SID is: S-1-5-4
kd> !sid e1eec270
SID is: S-1-5-11

Great. You see a list of SIDs that are contained in this token. Boy, there sure seem to be a lot. Table 3.7 lists the SIDs with some descriptions.

Table 3.7 SIDs Included in Jeff's Word Token




My user account SID as seen in the previous SID discussion.


Local users group.


World (everyone). Every token has this SID.


Local built-in domain administrators group.


Local built-in power users group.


Local built-in users group.


Logon ID (see logon, later in this chapter, for more info)


Local—this is a local logon.


Interactive—I am at the console.


Authenticated user—all non-anonymous users (see Note).


The Authenticated Users group was added in a patch to Windows NT 4.0 as a result of the "red button" vulnerability.

Figure 3.5 shows that I am a member of the local administrators group.

My membership in the power users group comes from the fact that authenticated users is defined as a member of power users on a Windows 2000 Professional machine, as mine is. This is shown in Figure 3.6. The net effect is that all non-anonymous users (that is, authenticated users) are also power users.

The SIDs in the token you examined are my keys to the system. As I access resources, the security reference monitor makes sure that the security descriptor (and contained DACLs) attached to each resource permit one of the SIDs listed in my token to execute whatever action I am requesting. Similarly, if there is an SACL contained within the security descriptor, the system checks whether auditing is necessary given my list of SIDs and the access I am requesting. Pretty cool, eh?

Figure 3.5 I am a member of the local administrators group, as the SIDs suggest.

Figure 3.6 The group authenticated users is a member of the group power users.

Privileges and User Rights

I sidetrack here for just a moment and talk about privileges. Privileges, unlike access rights, affect the system as a whole, whereas access rights affect only a certain securable object. As an example, the ability to read the file c:\hi.txt is an access right—the type of thing I've been talking about thus far. However, shutting down the system, for example, is a privilege because its scope is not limited to just one object.

Table 3.8 comes directly from the SDK and gives a brief description of each privilege.

Table 3.8 Common Privileges




Required to assign the primary token of a process.


Required to generate audit-log entries. Give this privilege to secure servers.


Required to perform backup operations.


Required to receive notifications of changes to files or directories. This privilege also causes the system to skip all traversal access checks. It is enabled by default for all users.


Required to create a paging file.


Required to create a permanent object.


Required to create a primary token.


Required to debug a process.


Required to increase the base priority of a process.


Required to increase the quota assigned to a process.


Required to load or unload a device driver.

SE_LOCK_MEMORY_NAME Required to lock physical pages in memory.


Required to gather profiling information for a single process.


Required to shut down a system using a network request.


Required to perform restore operations. This privilege enables you to set any valid user or group SID as the owner of an object.


Required to perform a number of security-related functions, such as controlling and viewing audit messages. This privilege identifies its holder as a security operator.


Required to shut down a local system.


Required to modify the non-volatile RAM of systems that use this type of memory to store configuration information.


Required to gather profiling information for the entire system.


Required to modify the system time.


Required to take ownership of an object without being granted discretionary access. This privilege allows the owner value to be set only to those values that the holder may legitimately assign as the owner of an object.


This privilege identifies its holder as part of the trusted computer base. Some trusted protected subsystems are granted this privilege. This privilege is required to call the LogonUser function.


Required to read unsolicited input from a terminal device.


Required to create a machine account.

Many of these should be familiar to NT administrators as the "user rights" seen in User Manager, and now in the MMC, as in Figure 3.7. Windows 2000 gives administrators GUI access to most of these privileges.

Figure 3.7 User rights and privileges are synonymous.

Taking a look at the WINWORD.EXE process—this time using the PVIEW.EXE tool that ships with the Windows NT 4 Resource Kit—you see the privileges that are enabled and the ones that are disabled (see Figure 3.8). Note that the enabled groups are the same as you saw in the exercise earlier. (Scrolling down reveals the rest of them.)

Figure 3.8 The PVIEW.EXE tool in the Windows NT 4 Resource Kit shows more token information.


Yes, I am using a tool from the Windows NT 4.0 Resource Kit to explore my Windows 2000 machine. The PVIEW.EXE tool is not included in the pre-release build of the Windows 2000 Resource Kit that I have. Because the structure of the token has not changed between NT 4 and Windows 2000, the NT 4 Resource Kit does the trick in this case. However, be careful when mixing versions of system-level tools.

Some privileges are very powerful, so Microsoft put a two-tier mechanism in place for using the rights associated with privileges. When attempting a privileged action, not only must the privileges be held by the client, but they must also be enabled.

Say I am attempting to call the LogonUser() Win32 function, which requires the SE_TCB_NAME privilege. My administrator granted my account (or a group to which I belong) the SE_TCB_NAME privilege using User Manager or another tool. However, this privilege (and all others) are disabled by default. Thus, my program does the following:

  1. Call OpenThreadToken() to get a handle to my primary (or impersonation) token.

  2. Call AdjustTokenPrivileges() to enable the necessary privileges, in this case SE_TCB_NAME.

  3. Do the call to LogonUser().

  4. Call AdjustTokenPrivileges() to disable the privilege.

SYSTEM Context

Many system processes run under a special access token called SYSTEM. Analyzing the SYSTEM token as I did my own token earlier, you see the following:

kd> !token e10007d0
TOKEN e10007d0 Flags: 9 Source *SYSTEM* AuthentId (0, 3e7)
  Type:          Primary (IN USE)
  Token ID:        3ea
  ParentToken ID:     0
  Modified ID:       (0, 3e9)
  TokenFlags:       0x9
  SidCount:        4
  Sids:          e1000950
  RestrictedSidCount:   0
  RestrictedSids:     0
  PrivilegeCount:     21
  Privileges:       e1000854

kd> dd e1000950
e1000950 e1000970 00000000 e100097c 0000000e
e1000960 e100098c 00000007 e1000998 00000007
kd> !sid e1000970
SID is: S-1-5-18
kd> !sid e100097c
SID is: S-1-5-32-544
kd> !sid e100098c
SID is: S-1-1-0
kd> !sid e1000998

Table 3.9 lists the SIDs that are contained within the SYSTEM token.

Table 3.9 SIDs Contained in the SYSTEM Token




Unique to the SYSTEM context


Local built-in domain administrators group


World (everyone)


Authenticated user—all non-anonymous users

The SYSTEM token contains local administrator rights to the computer and pretty much nothing else.


The fact that the SYSTEM token has no network rights is important. Many administrators scratch their heads wondering why the neat little batch script works fine when they run it themselves but fails when run as a scheduled job by the scheduler service. The answer most often is that the script needs network rights (for example, it contains a NET command or NetBIOS path), and because the scheduler runs under SYSTEM context, the network access fails.

Note that because the SYSTEM token is a member of the local administrators group, processes running under this context can enable any privilege it might need.


Okay, back to tokens. As you saw, every process has a primary token that contains the security context of the user account associated with the process. By default, the system uses the primary token when a thread of the process interacts with a securable object.

However, a thread can impersonate a client account. This is a powerful feature that allows the thread to interact with securable objects using a client's security context. A thread impersonating a client has both a primary token and an impersonation token.

Impersonation is commonly used in server processes that handle requests from the network. For example, the Windows 2000 server service provides RPC support and file, print, and named pipe sharing. A quick look with PVIEW.EXE shows that the process is indeed running with the SYSTEM token. In fact, most server processes run with SYSTEM context. You'll note that the server service actually shares a binary with SERVICES.EXE, as several system services do. See Chapter 5, "Services," for more information about this technique.

Great, so the server service has local administrator power. But what happens when a client that has less than administrator privileges accesses a file share on this machine? Simple, the server service impersonates the client before attempting to access the resources. If the client does not have sufficient privilege, then neither will the server (while impersonating) and thus the access will fail. When the server service is done handling this client's requests, it can revert back to its primary (non-impersonated) token.

As another example, the Internet Information Server (IIS) process also runs under the SYSTEM context. However, administrators familiar with IIS will recognize the special IIS anonymous account (IIS_domainname) that IIS uses for anonymous clients (HTTP and FTP). The general idea is that you assign that special account the rights that you want for folks accessing your server anonymously.

What happens under the hood is that the IIS worker thread that actually services the request impersonates the guest account before accessing resources. When the user's request has been filled, the thread reverts back to its primary token.

Impersonation is a powerful and elegant way to handle access checks. You can imagine the alternative: Each server process, running as SYSTEM, would have to manually check access for each user to each object that he requests. This would require a huge volume of repetitive and error-prone code in each server service.

Restricted Tokens

New in Windows 2000 is the ability to create restricted tokens. Restricted tokens, as you might expect, contain a subset of the SIDs and privileges of the original token. You create restricted tokens using the CreateRestrictedToken() function, which simply takes as arguments a handle to an existing token as well as a list of privileges and SIDs to remove.

Token Security

With all this talk about opening tokens, I'm sure you're wondering how tokens are secured. Tokens are just kernel objects and thus, like every other kernel object, are securable and have security descriptors attached. See the SDK for the access rights that apply to tokens, as well as how to adjust them.

Moving On

Now that you're familiar with the background structures and concepts, look at the components that make up the Windows 2000 security architecture.

  • + Share This
  • 🔖 Save To Your Account

Related Resources

There are currently no related titles. Please check back later.