An access token helps track users as they log on and determine what operations the user is allowed to perform. Here's an AccessToken class in Managed C++, which is used to create a Security Explorer that displays a standard tree/list view, where the tree contains composite or collection entities, and the list contains the values or attributes.
September 01, 2003
URL:http://www.drdobbs.com/security/win32-security-in-managed-c/184416685
Download the code for this issue
Surviving the Win32 Security API
Strings in Managed C++
Managed C++ Hasn't Got Any Friends!
Although .NET provides its own security model (in the System::Security namespace) for CLR security, it doesn't address the lurking behemoth that is the Win32 security subsystem. For those who have programmed with Win32 security, you'll know that while it's conceptually straightforward, it's a real bear to program. For example, when writing a program that requires certain rights or privileges to work, it can often be a very intensive process to determine what privileges one has, or what rights are required.
In this article, I'll demonstrate how this coding headache can be simplified as I develop a .NET library, in Managed C++, that encapsulates the programming details in a simple object model. Along the way, we'll have a quick refresher on Win32 security, demonstrate some techniques for simplifying coding with the Win32 security APIs, and learn some of the restrictions and gotchas of Managed C++.
The article will be based on the first version of the .NET Win32 Security component I'm developing, and will describe the implementation of the AccessToken class and supporting types. It will also show how this object model can be utilized in a UI client, Security Explorer, written in C#.
Win32 security is a big topic, which would require many articles to do it justice. (For those who want to learn more, I recommend Nik Okuntseff's book Windows NT Security as a good place to start.) I'm going to restrict myself in this article to a discussion of access tokens. An access token is created for each user when the user logs on, and persists for the duration of that user's session. Every process that the user executes receives a copy of that access token, which is then used by the security subsystem to determine what operations the process (i.e., the user) is allowed to perform.
An access token contains, among other things, the following items:
The SynSoft::Security::AccessToken class is defined, in Managed C++, as shown in Listing 1. The first thing you'll note is that it derives from the interface System::IDisposable. This interface marks the class as implementing the Dispose pattern, which means that the class can be used in a C# using expression (providing Resource Acquisition Is Initialization, albeit user-requested rather than class-mandated) and also is amenable to generalized closure: One simply dynamically casts (using dynamic_cast in C++ or as in C#) to IDisposable, then calls Dispose() on a (nonnull) resultant reference. AccessToken, like any class that owns/manages a native resource (in this case the access token) provides this facility so that well-behaved clients can cause it to relinquish its resources in a timely fashion, rather than waiting for garbage collection. Furthermore, it is my practice to provide a Close() method whenever implementing IDisposable, which performs the same task as Dispose(). You'll see this in other classes in this library.
Next, it is clear that one cannot construct an instance of an AccessToken. This makes sense, since we cannot create an access token in Win32 land we can only open one via the Win32 functions OpenProcessToken() and OpenThreadToken(). AccessToken has two corresponding static methods that do exactly the same job, and then return an AccessToken instance representing the (successfully opened) token. So we can open a token and we can close/dispose of it. Now what we're interested in are its attributes.
The current version of this library contains read-only components. (Create/edit functionality is planned in a future release; visit http:// synsoft.org/dotnet.html to obtain updates.) We can access, as .NET properties, the user and primary group, the groups, the privileges, the default DACL, and a variety of other attributes associated with an access token. The types returned (e.g., SID, GroupList, PrivilegeList) are also classes in the same namespace (and assembly). Most of these classes are also required to implement the IDisposable pattern; this is one of the hassles of .NET's not supporting C++'s deterministic destructors. Consider the GroupList class (Listing 2). This class does not contain any unmanaged resources. In fact, its only member is an ArrayList. However, because the list is populated with Group instances in the GroupList constructor, GroupList must implement the IDisposable interface, and in its Dispose() method must iterate through the list ArrayList does not implement IDisposable and call Dispose() on each contained object (in this case, Group instance). This stuff is tedious, and also carries a bit of a catch. Sometimes a class will implement IDisposable but will make its Dispose() method nonpublic; this is known as Explicit Interface Member Implementation. Hence, you need to cast the contained elements to IDisposable before attempting to call Dispose(). Because this is a common scenario, I've used the .netSTL (.netSTL is a subproject of STLSoft; http://dotnetstl.org/) algorithm dispose_contents<>() (shown in Listing 3, with its supporting function dispose_set_null<>()).
The picture doesn't end there, alas. Group also does not contain any unmanaged resources, but it has an instance of class SID, which does a Win32 SID blob, in fact (see Listing 4). Thus, we had to add Dispose() (and Close()) functionality to at least two other types because a composed type requires it. Of course, you may think why bother, the Win32 SID is only a blob of memory (albeit from the native rather than the managed heap)? Well, as a rule, I don't feel comfortable having any unmanaged resources hanging around for an indefinite time, and one might ponder the case where the resource is not a block of memory but a file handle. This is a fundamental problem of garbage-collected systems; sure, they handle memory fine (often better than deterministically managed systems), but what about all the other resource types (files, pipes, and heaven-forefend, synchronization objects!)?
Having had my C++-biased rant, let's see what's good about this library. Looking into the implementation of many of the property accessor methods of AccessToken (Listing 5), we can see one nice feature. Since all managed types must be held by pointer, the implementation dilemma common in object models do I use composition to reduce memory usage and increase execution speed, or use pimpl to reduce memory usage and increase execution and compilation speed? is moot. We have to use pointers, and it's often best to choose lazy evaluation. This may or may not be the "best" thing from a pure programming perspective, but it simplifies the design, coding, and maintenance of modules, so it has a lot going for it. A side effect is that it will help to ameliorate the dangling unmanaged resource issue; in cases where they are not used, they're not created, and so not dangling.
So how do we get information out of an access token? GetTokenInformation(), of course. This very powerful function is prototyped as follows:
BOOL WINAPI GetTokenInformation (
HANDLE TokenHandle,
TOKEN_INFORMATION_CLASS TokenInfClass,
LPVOID TokenInformation,
DWORD TokenInfLength,
PDWORD ReturnLength);
You pass a token handle, the type of information you want (a member of the TOKEN_INFORMATION_CLASS enumeration), a destination buffer and its length (in bytes), and an address to receive the number of bytes of information retrieved. Seems straightforward, but like most Win32 Security API functions, it can be tricky. Some of the access token information is of a fixed size, and so one simply passes the appropriate parameters, as can be seen in the implementation of AccessToken::get_Type(). In most cases, however, the information is of variable length, so one must call GetTokenInformation() as described in the sidebar titled "Surviving the Win32 Security API." This approach is repetitive and error-prone what if there is an exception thrown between the allocation and deallocation of the resource? A lot of careful boilerplate is required (see AccessToken::_get_groups() in Listing 5; this implementation is chosen because both TokenGroups and TokenRestrictedSids return information in a TOKEN_GROUPS structure). Mercifully, there is a simple and elegant solution in the form of the WinSTL (another STLSoft subproject; http://winstl.org/) component token_information<> (see Listing 6). You parameterize the template using the appropriate TOKEN_INFORMATION_CLASS enumeration member, and the traits mechanism automatically deduces the correct data structure type and instantiates a class that wraps the allocation, acting as a smart pointer for the requisite type. The get_User() and get_Privileges() methods in Listing 5 demonstrate how much this simplifies matters. (Note that the template is flexible in allowing you to specify an exception policy. The default is stlsoft::null_exception, which does not throw, in which case the implementation ensures that GetLastError() will reflect the failing condition, rather than being overwritten during deallocation of the memory buffer.)
The difficulty of processing the information returned depends on the type of information retrieved. Retrieving SECURITY_IMPERSONATION_LEVEL, which is an enumeration (as is TOKEN_TYPE), is very simple. Just retrieve it and convert it to the appropriate managed enumeration type. Note: It's convenient, but somewhat unsafe, that Managed C++ lets you cast, in the form of a conversion constructor, from a C-type to a managed enumeration type; see AccessToken::get_Type(). Although I've borrowed the values for the managed enumerations TokenType, TokenImpersonationLevel, and SidType from their native equivalents, and am confident I've transcribed them correctly, what if I got it wrong?)
For some variable sized entities, it is also reasonably easy to process the retrieved information. Retrieving groups or privileges returns counted arrays of SID_AND_ATTRIBUTES and LUID_AND_ATTRIBUTES, respectively, so one need simply walk the array, as in the GroupList constructor (Listing 2).
The retrieval of the default DACL from an access token is an altogether more complex matter. As well as retrieving a variable length structure, the items within the structure are polymorphic (can be one of the three ACE types), and are themselves of variable length. Nasty.
The answer comes from a combination of another useful class from WinSTL the acl_sequence<> template (not shown; available from http://winstl.org/downloads.html) and a hacky assumption. Since all the ACE structures have the same essential structure (which can't be a coincidence I bet the designers of the Win32 Security API decided in this one small way to give themselves a break), we can treat them as sharing a pseudotype (wherein ACE_HEADER) to treat them all equally. Listing 7 shows the solution in the ACL constructor. acl_sequence<> takes care of defining the range [begin, end), and ensuring the appropriate pointer-arithmetic when moving from one ACE to the next. Inside the loop, we switch on the header type, and then call the appropriate ACE constructor. AccessAllowedACE, AccessDeniedACE, and SystemAuditACE share a common base class, ACE, which handles the retrieval of the ACE structure contents into the class in a generalized (based on their common layouts) fashion. As I said, it's a hack, but since it would be impossible to change the sizes of the extant ACE types without breaking every Win32 program out there that uses security, I think we can say it is an informed hack and sleep soundly.
That's pretty much all the ugly stuff in the library; the rest is very straightforward and I'll leave it to you to download the archive and look through it. Let's see how we can use it. The Security Explorer program is shown in Figure 1. As you can see, it has a standard tree/list format, where the tree contains composite or collection entities, and the list contains the values or attributes. This first version allows exploration of the process access token. The root-most node, when selected, displays the user, owner, primary group, and the like, and then the five subnodes show the contents of each of their collections.
The Security Explorer works by opening the access token for the process, by calling AccessToken::OpenProcessToken. It stores this along with an identifying type in an instance of a NodeItem type, which is then stored as the Tag value (LPARAM in Win32 API parlance) of the tree node. The collection properties (Groups, Privileges, etc.) are similarly stored in subnodes. When the user clicks on a node, the handler loads the stored object (and its type), and processes it according to the type. It's not particularly elegant a more professional solution would be to use polymorphic display-handler classes but it serves to exercise the security object model. I'm already finding this useful, since it's far and away the quickest mechanism I have to tell me what privileges I have, and whether they're enabled, on a given system.
This article has given a very brief introduction to the Win32 security subsystem, discussing in some detail the contents of access tokens and how to read them. Hopefully, this has whetted your appetite and shown you that, in combination with suitable helper code, manipulating the security API is not as scary as you might have thought. We all avoid it like the plague as a rule, but when you need to secure your application objects, you have no choice but to dive in. Now, perhaps, you'll have a bit of background info to help.
We've also discussed some of the implications of development in Managed C++, and highlighted some of the issues you encounter with this new language. Finally, I hope the point is not lost that we've tamed a daunting C API by applying some very modern C++ (STL) techniques. Over this, we've layered a Managed C++ object model, which was then used in a C# application. How's that for integration?
Applied .NET Framework Programming. Jeffrey Richter, Microsoft Press, 2002.
Windows NT Security. Nik Okuntseff, R & D Books, 1997.
STLSoft is an open-source organization applying STL techniques to a variety of technologies and operating systems. It is located at http://stlsoft.org/. WinSTL and .netSTL are subprojects, and are located at http://winstl.org/ and http://dotnetstl.org/, respectively.
SynSoft is the nonprofit imprint of my company, Synesis Software, and is located at http://synsoft.org/. It provides D, .NET, Java, Perl, and Python code/components free for use without royalty.w::d
namespace SynSoft { namespace Security { /// Represents a security AccessToken public __gc class AccessToken : public IDisposable { private: typedef void *HANDLE; private: /// Create an instance managing the given security handle AccessToken(HANDLE hToken); /// Finaliser ~AccessToken(); // IDisposable public: /// Provides facility for void Close(); void Dispose(); // Creation public: /// Open the access token for the current process static AccessToken *OpenProcessToken(); /// Open the access token for the current thread static AccessToken *OpenThreadToken(); // Attributes public: /// The token's user account __property SID *get_User(); /// The groups accounts associated with the access token __property GroupList *get_Groups(); /// Privileges __property PrivilegeList *get_Privileges(); /// The security identifier of the owner __property SID *get_Owner(); /// The primary groups of any objects created with the access token __property SID *get_PrimaryGroup(); /// Default DACL __property ACL *get_DefaultDACL(); /// Source __property TokenSource *get_Source(); /// Type __property TokenType get_Type(); /// Impersonation Level __property TokenImpersonationLevel get_ImpersonationLevel(); /// Statistics __property TokenStatistics *get_Statistics(); /// Restricted SIDs __property GroupList *get_RestrictedSIDs(); /// Session Id __property Int32 get_SessionId(); /// Sandbox Inert __property bool get_SandboxInert(); // Standard members public: // Returns a string representation of the AccessToken String *ToString(); // Implementation private: GroupList *_get_groups(TOKEN_INFORMATION_CLASS tic); // Members private: HANDLE m_hToken; SID *m_user; GroupList *m_groups; PrivilegeList *m_privileges; SID *m_owner; SID *m_primaryGroup; ACL *m_defaultDACL; TokenSource *m_source; TokenStatistics *m_statistics; GroupList *m_restrictedSIDs; }; } }
namespace SynSoft { namespace Security { // GroupList.h /// Represents a list of groups public __gc class GroupList : public IEnumerable , public IDisposable { public: GroupList(int cGroups, SID_AND_ATTRIBUTES *groups); // IDisposable public: void Close(); void Dispose(); // IEnumerable public: IEnumerator *GetEnumerator(); // Members private: ArrayList *m_groups; }; // GroupList.cpp GroupList::GroupList(int cGroups, SID_AND_ATTRIBUTES *groups) : m_groups(new ArrayList(cGroups)) { for(int i = 0; i < cGroups; ++i) { m_groups->Add(new Group(new SID(groups[i].Sid), groups[i].Attributes)); } } void GroupList::Close() { if(0 != m_groups) { dispose_contents(m_groups); m_groups->Clear(); m_groups = 0; } } } }
/* ///////////////////////////////////////////////////////////// * ... * * Extract from dotnetstl_dispose_functions.h * * Copyright (C) 2003, Synesis Software Pty Ltd. * (Licensed under the Synesis Software Standard Source License: * http://www.synesis.com.au/licenses/ssssl.html) * * ... * ////////////////////////////////////////////////////////// */ namespace dotnetstl { /// Disposes the managed type, and resets the pointer template <ds_typename_param_k T> inline void dispose_set_null(T *&pt) { if(0 != pt) { System::IDisposable *disposable = pt; disposable->Dispose(); pt = 0; } } /// Disposes all the items in a container template <ds_typename_param_k C> inline void dispose_contents(C *pc) { for(int i = 0, count = pc->get_Count(); i < count; ++i) { System::IDisposable *o = dynamic_cast<System::IDisposable*>(pc->get_Item(i)); dispose_set_null(o); } } } // namespace dotnetstl
namespace SynSoft { namespace Security { // SID.h public __gc class SID : public IDisposable { public: SID(void *psid); ~SID(); // IDisposable public: void Close(); void Dispose(); // Properties public: // Returns a string representation of the SID String *ToString(); // The type of the SID __property SidType get_Type(); // The name of the user associated with the SID __property String *get_UserName(); // The name of the domain associated with the SID __property String *get_DomainName(); // Members private: void *m_psid; SidType m_type; String *m_userName; String *m_domainName; String *m_stringForm; }; // SID.cpp SID::SID(void *psid) : m_psid(Sec_SidDup(psid)) , m_type(SidType::Unknown) , m_userName(0) , m_domainName(0) , m_stringForm(0) { // Now we get the user and domain names if(0 == psid) { throw new SecurityException(E_INVALIDARG, "NULL SID"); } else if(!::IsValidSid(psid)) { throw new SecurityException(::GetLastError(), "Invalid SID"); } else { . . . } } SID::~SID() { if(0 != m_psid) { Sec_Free(m_psid); } } void SID::Close() { if(0 != m_psid) { Sec_Free(m_psid); m_psid = 0; } } void SID::Dispose() { Close(); } . . . } }
SID *AccessToken::get_User() { if(0 == m_user) { token_information<TokenUser> user(m_hToken); if(!user) { throw new SecurityException(::GetLastError()); } else { m_user = new SID(user->User.Sid); } } return m_user; } GroupList *AccessToken::get_Groups() { if(0 == m_groups) { m_groups = _get_groups(TokenGroups); } return m_groups; } GroupList *AccessToken::get_RestrictedSIDs() { if(0 == m_restrictedSIDs) { m_restrictedSIDs = _get_groups(TokenRestrictedSids); } return m_restrictedSIDs; } PrivilegeList *AccessToken::get_Privileges() { if(0 == m_privileges) { winstl::token_information<TokenPrivileges> privileges(m_hToken); if(!privileges) { throw new SecurityException(::GetLastError()); } else { m_privileges = new PrivilegeList(privileges->PrivilegeCount, privileges->Privileges); } return m_privileges; } return m_privileges; } TokenType AccessToken::get_Type() { TOKEN_TYPE tt; DWORD chRequired; if(!::GetTokenInformation(m_hToken, ::TokenType, &tt, sizeof(tt), &chRequired)) { throw new SecurityException(::GetLastError(), "Could not elicit type from token"); } return TokenType(tt); } GroupList *AccessToken::_get_groups(TOKEN_INFORMATION_CLASS tic) { GroupList *grouplist; DWORD cbRequired; DWORD dwErr; stlsoft_assert(tic == TokenGroups || tic == TokenRestrictedSids); ::GetTokenInformation(m_hToken, tic, NULL, 0, &cbRequired); dwErr = ::GetLastError(); if(ERROR_INSUFFICIENT_BUFFER != dwErr) { throw new SecurityException(dwErr); } else { TOKEN_GROUPS *groups = static_cast<TOKEN_GROUPS*>(Sec_Alloc(cbRequired)); if(!::GetTokenInformation(m_hToken, tic, groups, cbRequired, &cbRequired)) { throw new SecurityException(::GetLastError()); } else { try { grouplist = new GroupList(groups->GroupCount, groups->Groups); } catch(Exception *x) { Sec_Free(groups); throw x; } } } return grouplist; }
/* ///////////////////////////////////////////////////////////// * ... * * Extract from winstl_token_information.h * * Copyright (C) 2003, Synesis Software Pty Ltd. * (Licensed under the Synesis Software Standard Source License: * http://www.synesis.com.au/licenses/ssssl.html) * * ... * ////////////////////////////////////////////////////////// */ namespace winstl { template <TOKEN_INFORMATION_CLASS C> struct token_information_traits; template <> struct token_information_traits<TokenUser> { typedef TOKEN_USER data_type; }; ... // More specialisations of token_information_traits template< TOKEN_INFORMATION_CLASS C , ws_typename_param_k X = stlsoft::null_exception , ws_typename_param_k D = token_information_traits<C>::data_type , ws_typename_param_k A = processheap_allocator<ss_byte_t> > class token_information { public: typedef token_information<C, X, D, A> class_type; typedef token_information_traits<C> traits_type; typedef X exception_thrower_type; typedef D data_type; typedef A allocator_type; // Construction public: /// Constructs an instance from the given access token ws_explicit_k token_information(HANDLE hToken) : m_data(0) { DWORD cbRequired; DWORD dwError; ::GetTokenInformation(hToken, C, NULL, 0, &cbRequired); dwError = ::GetLastError(); if(ERROR_INSUFFICIENT_BUFFER != dwError) { // Report error exception_thrower_type()(dwError); } else { data_type *data = (data_type*)allocator_type().allocate(cbRequired); if(NULL == data) { // Report error exception_thrower_type()(ERROR_NOT_ENOUGH_MEMORY); // Set the last error, in case the client code is not // using exception reporting ::SetLastError(ERROR_NOT_ENOUGH_MEMORY); } else { if(!::GetTokenInformation(hToken, C, data, cbRequired, &cbRequired)) { // Scope the last error, in case the client code is not // using exception reporting dwError = ::GetLastError(); allocator_type().deallocate((ss_byte_t*)data); // Report error ::SetLastError(dwError); exception_thrower_type()(dwError); } else { // Success m_data = data; ::SetLastError(ERROR_SUCCESS); } } } } ~token_information() { allocator_type().deallocate((ss_byte_t*)m_data); } // Conversion public: operator data_type *(); operator data_type const *() const; data_type *operator ->(); data_type const *operator ->() const; ws_bool_t operator !() const; /// Members private: data_type *m_data; /// Not to be implemented private: token_information(token_information const &); token_information &operator =(token_information const &); }; } // namespace winstl
// from ACL.h namespace SynSoft { namespace Security { public __gc class ACL : public IEnumerable , public IDisposable { public: ACL(PACL pacl); // IDisposable public: void Close(); void Dispose(); // IEnumerable public: IEnumerator* GetEnumerator(); // Properties public: __property Int32 get_Count(); String *ToString(); // Members private: UInt32 m_revision; ArrayList *m_aces; }; } } // GroupList.cpp ACL::ACL(PACL pacl) : m_aces(new ArrayList(pacl->AceCount)) , m_revision(pacl->AclRevision) { using winstl::acl_sequence; acl_sequence aces(pacl); acl_sequence::const_iterator begin = aces.begin(); acl_sequence::const_iterator end = aces.end(); for(; begin != end; ++begin) { PACE_HEADER header = *begin; switch(header->AceType) { case ACCESS_ALLOWED_ACE_TYPE: m_aces->Add(new AccessAllowedACE(header)); break; case ACCESS_DENIED_ACE_TYPE: m_aces->Add(new AccessDeniedACE(header)); break; case SYSTEM_AUDIT_ACE_TYPE: m_aces->Add(new SystemAuditACE(header)); break; } } }
If there are two things you should remember above all others when working with the Win32 Security API, they are:
1. Nearly everything is variable length. Not only does this mean you should make no assumptions about being able to iterate through composite/collection types in an integral fashion, but you should also never assume that things are of a predeterminable size.
2. The common paradigm is Size-Allocate-Retrieve, as we saw with GetTokenInformation(). You call a function with a null destination pointer and give an initial size of 0. It will return into your size parameter the required size (and set the last error to ERROR_INSUFFICIENT_BUFFER). You allocate an appropriately sized buffer and call again, and the function will fill your buffer with the desired information.
M.W.
Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.