SID. Vicious?

Some activities on any operating system fall into that category of “should be extraordinarily simple, and yet is full of the sort of pitfalls that cause headaches, confusion and (at least in my case) bouts of cursing and ranting”. 

My favourite of the moment is a simple security task: authenticating credentials provided by the user to ensure they are valid; and detecting programmatically if an authenticated user is an administrator. The fun-inducing caveat: this code has to work on Windows 2000, XP and Vista.

I’ll give a few code examples in this article. A couple of caveats: firstly, none but the last will be the complete and correct solution, so quick cutting and pasting may be inadvisable. Secondly, the code is C# and will assume the existence of a class called NativeSecurityApis in which all the relevant native API declarations are located. For those needing to create such a class, I recommend the pinvoke.net website.

So, let’s pretend we know nothing about this task, and type appropriate phrases into Google. Most of the code samples and advice one will find explain how to check if the current user is an administrator. In .NET this is triviality itself:

        public static bool IsUserAdmin()
        {
            return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
        }

Which is splendid. Two problems here: we are checking the current user, not the user whose user and password we have to hand; and, this code doesn’t work on Windows Vista. We’ll come back to that later.

The Simple Approach

Leaving aside the latter problem for the moment, let’s focus on the former problem. Given a username and password, how do we authenticate that user?

Google again provides. We call the LogonUser() API. This takes a user name, domain, and password, and returns a handle to a token representing that user; or, if the login failed, an error. Super. Then we have a choice: we can either temporarily “become” that user via impersonation, and then use the above to check whether “we” are an administrator, and then revert back to being ourselves; or we use the WindowsIdentity constructor which takes the sort of token we have just acquired, and check that user. Impersonation is a useful thing to know about, but strictly speaking isn’t relevant here. So we should take the second route.

Slight obstacles: firstly, how do we robustly turn a user-supplied username and password into the triple required by LogonUser: user name, domain, password. Secondly, LogonUser is not provided by .NET, so we have to PInvoke our way to the underlying API. Thirdly, it’s badly implemented on Windows 2000, such that you have to more or less be SYSTEM in order for it to ever work, and is therefore useless for this purpose.

Let’s leave the latter two aside (again) for the moment, and press on. So let’s imagine we’ve asked the user for a username and password. The user may have supplied a username in one of these formats:

“UserName” (unqualified; local user name)
“DOMAINUserName” (old style)
“UserName@DOMAIN” (so called “UPN format”)

Can we just supply all of these these to LogonUser, with a null domain string, and have it work? I’ll give you a clue: no. So, which of these can we just supply these to LogonUser? Well, number three is fine. As the API documentation suggests, UPN fomat is accepted if you specify a doman of null. The others aren’t supported. What, you say, supplying “UserName” on its own is not allowed? That’s right. In the world of Windows Security, the domain for the local machine can be expressed as “.”, but not as null. And in the second case, you have to manually strip the domain out yourself and pass it in as a separate parameter, as LogonUser isn’t bright enough to do this for you.

Another incidental bit of fun is that if you are, as I was, doing all this in an installer which also has to install a Windows Service using .NET’s AssemblyInstaller classes, you’d better steer clear of UPN format, as usernames not in “DOMAINUserName” format will cause exceptions courtesy of tht component. Fun!

So either way, time to write some highly trivial but irritating code to split out a (username,password) tuple into a (username,domain,password) tuple.

        public static string GetDomain(ref string userName, bool includeUPNFormat)
        {
            string[] domainAndUser = userName.Split(new char[] { ‘\’ });
            if (domainAndUser.Length > 2)
                throw new ArgumentException(“Username format incorrect.”);

            string domain = null;
            if (domainAndUser.Length == 2)
            {
                domain = domainAndUser[0];
                userName = domainAndUser[1];
            }
            else if (includeUPNFormat)
            {
                domainAndUser = userName.Split(new char[] { ‘@’ });
                if (domainAndUser.Length > 2)
                    throw new ArgumentException(“Username format incorrect”);

                if (domainAndUser.Length == 2)
                {
                    domain = domainAndUser[1];
                    userName = domainAndUser[0];
                }
            }

            if (domain == null)
                domain = “.”;

            return domain;
        }

That little bit of fun over with, we can now call LogonUser(). If the credentials are valid we get a token back. If they’re not, we get an error. Super.

Then it only remains to check whether the user corresponding to our nice token is an administrator, as already described; and our job is done. 

        public static bool IsUserAdmin(string userName, string password)
        {
            string name = userName;
            string domain = GetDomain(ref name, true);

            return IsUserAdmin(name, domain, password);
        }

        public static bool IsUserAdmin(string userName, string domain, string password)
        {
            IntPtr hToken;
            if (NativeSecurityApis.LogonUser(userName, domain, password, NativeSecurityApis.LOGON32_LOGON_INTERACTIVE, NativeSecurityApis.LOGON32_PROVIDER_DEFAULT, out hToken))
            {
                try
                {
                    return new WindowsPrincipal(new WindowsIdentity(hToken)).IsInRole(WindowsBuiltInRole.Administrator);
                }
                finally
                {
                    NativeSecurityApis.CloseHandle(hToken);
                }
            }
            else
            {
                    throw new Win32Exception(); // last error
            }
        }
 

Coping with Windows 2000

Now as noted previously, if your code has to work on Win2K, then your headaches aren’t quite over. On Win2K, LogonUser is implemented via the low level API call LsaLogonUser, the user owning the calling process is required to have a grubby privilege called SeTcbPrivilege, otherwise known as “Act as part of the operating system”. This is because LsaLogonUser allows some rather nasty things to be done other than just checking credentials. Very few Windows processes (those running as SYSTEM) are likely to have this privilege; everyone else doesn’, for obvious security reasons, and so therefore can’t call LogonUser.

Microsoft’s suggested workaround is to use SSPI authentication instead of calling LogonUser. This is a very protracted client/server authentication approach designed for authenticating users remotely (such as over a SQL Server connection which uses “Windows Authentication”). For once, it’s quite a lot of grubby native code to call, but in .NET 2.0, the lovely NegotiateStream class was added which is capable of executing the right manoevres for us.

One may ask why, if SSPI authentication works on Windows 2000 and above, we shouldn’t just always use it instead of LogonUser? Well, one argument is because of its particular behaviour w.r.t. the “Guest” account on Windows XP and above. As noted in http://support.microsoft.com/default.aspx?scid=kb;EN-US;180548 SSPI may always try and logon as “Guest”, or indeed do no authentication at all and just claim to have logged in, depending on registry settings. This would make our attempt to validate user credentials rather academic. SSPI should therefore be a fallback approach, not a replacement for calling LogonUser where we can.

Now I’ve seen a code example bandied around the internet which purports to be “the” way to get this to work, but I found it had several problems. Firstly it wasn’t reliably callable more than once; especially if an authentication attempt actually failed. If you’re calling from an application which has any kind of UI, this is problematic. Secondly, its use of asynchronous calls meant that it had a nasty race condition which made itself particularly manifest on successive authentication attempts. Here’s a fixed up version which doesn’t exhibit these problems:

    internal class Win32SSPI
    {
        private static readonly TcpListener tcpListener = new TcpListener(IPAddress.Loopback, 0);

        static Win32SSPI()
        {
            tcpListener.Start();
        }

        /// <summary>
        /// Logon user using SSPI authentication.
        /// </summary>
        /// <param id=”userName””>The username (without domain qualifications, so no DOMAINuser or user@DOMAIN here)</param>
        /// <param id=”domain””>The domain name (or “.”)</param>
        /// <param id=”password””>The password.</param>
        /// <returns>A valid WindowsPricipal. Throws an exception on failure.</returns>
        public static WindowsPrincipal LogonUser(string userName, string domain, string password)
        {
            try
            {
                // need a full duplex stream – loopback is easiest way to get that
                WindowsIdentity id = null;
                AutoResetEvent waitEvent = new AutoResetEvent(false);
                IAsyncResult result = tcpListener.BeginAcceptTcpClient(delegate(IAsyncResult asyncResult)
                {
                    try
                    {
                        using (NegotiateStream serverSide = new NegotiateStream(
                            tcpListener.EndAcceptTcpClient(asyncResult).GetStream()))
                        {
                            serverSide.AuthenticateAsServer(CredentialCache.DefaultNetworkCredentials,
                                                             ProtectionLevel.None, TokenImpersonationLevel.Impersonation);
                            id = (WindowsIdentity)serverSide.RemoteIdentity;

                        }
                    }
                    catch (Exception)
                    {
                        // ack.
                    }
                    finally
                    {
                        waitEvent.Set();
                    }
                }, null);

                using (NegotiateStream clientSide = new NegotiateStream(new TcpClient(“localhost”,
                                                                                         ((IPEndPoint)tcpListener.LocalEndpoint).Port).GetStream()))
                {
                    clientSide.AuthenticateAsClient(new NetworkCredential(userName, password, domain),
                                                     “”, ProtectionLevel.None, TokenImpersonationLevel.Impersonation);
                }

                waitEvent.WaitOne();
                waitEvent.Close();

                if (id != null)
                    return new WindowsPrincipal(id);

                throw new LogonException(“Cannot authenticate user using SSPI.”);
            }
            catch (Exception ex)
            {
                throw new LogonException(“Cannot authenticate user using SSPI.”, ex);
            }
        }
     }

So, now we can adjust our IsUserAdmin() function to try this approach if LogonUser fails. Since it will fail on Windows 2000, this gives us a working fallback position without having to write any “what OS version is this” type code (which is notoriously a bad idea).

         public static bool IsUserAdmin(string userName, string domain, string password)
        {
            IntPtr hToken;
            if (NativeSecurityApis.LogonUser(userName, domain, password, NativeSecurityApis.LOGON32_LOGON_INTERACTIVE, NativeSecurityApis.LOGON32_PROVIDER_DEFAULT, out hToken))
            {
                try
                {
                    return new WindowsPrincipal(new WindowsIdentity(hToken)).IsInRole(WindowsBuiltInRole.Administrator);
                }
                finally
                {
                    NativeSecurityApis.CloseHandle(hToken);
                }
            }
            else
            {
                try
                {
                    WindowsPrincipal principal = Win32SSPI.LogonUser(userName, domain, password);
                    return prinicipal.IsInRole(WindowsBuiltInRole.Administrator);
                }
                catch (Exception ex)
                {
                    throw new LogonException(ex.Message, ex);
                }
            }
        }

UAC, one can log Coping with Windows Vista

And now we come to the real fun. Windows Vista introduced UAC (User Account Control) for reasons which are outside the scope of this article. Under UAC, one can log in as a user who is, in theory, an administrator, but not actually have administrator privileges until they are absolutely required (because some application is about to do something which needs them). Only then is the user “elevated” to having full administrative rights.

This has a consequence for our authentication code: it no longer works. On Vista,  calling WindowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator) will return false unless the user is currently elevated to proper administrator status. If we’ve just come from a LogonUser call, they won’t be so elevated; nor do we really want to attempt to elevate them (even if this were feasible, which it really isn’t). So how do we check if the user is, in theory, an administrator, despite the fact that they aren’t currently administrating?

Google provides a bunch of code snippets resembling the following:

                string sddlAdmin = “S-1-5-32-544”;  //Sid of administrators group
                IdentityReference adminSid = new SecurityIdentifier(sddlAdmin);
                if (principal.Identity is WindowsIdentity &&
                     ((WindowsIdentity)principal.Identity).Groups.Contains(adminSid))
                {
                    return true;
                }
 

Which is excellent, except for the fact that this doesn’t have a snowball’s chance in hell of working either. This code is simply a protracted way of calling the same APIs as WindowsPrincipal.IsInRole(). Doomed to failure.

There are native APIs which look like they should help: IsUserAdmin(), previously an undocumented and unsupported internal API, is now a documented but very-likely-to-become-obsolete public API. Likewise the CheckTokenMembership() API which can check if a user is an administrator, as follows:

         public static bool IsImpersonationTokenUserAdmin(IntPtr hToken)
        {
            bool success;
            IntPtr pNTAuthority = Marshal.AllocHGlobal(Marshal.SizeOf(NativeSecurityApis.SID_IDENTIFIER_AUTHORITY.SECURITY_NT_AUTHORITY));
            Marshal.StructureToPtr(NativeSecurityApis.SID_IDENTIFIER_AUTHORITY.SECURITY_NT_AUTHORITY, pNTAuthority, false);

            try
            {
                IntPtr pSidAdministratorsGroup;

                success = NativeSecurityApis.AllocateAndInitializeSid(
                        pNTAuthority,
                        2,
                        NativeSecurityApis.SECURITY_BUILTIN_DOMAIN_RID,
                        NativeSecurityApis.DOMAIN_ALIAS_RID_ADMINS,
                        0, 0, 0, 0, 0, 0,
                        out pSidAdministratorsGroup);
                try
                {
                    if (success)
                    {
                        if (!NativeSecurityApis.CheckTokenMembership(hToken, pSidAdministratorsGroup, out success))
                        {
                            success = false;
                        }
                    }
                }
                finally
                {
                    NativeSecurityApis.FreeSid(pSidAdministratorsGroup);
                }
            }
            finally
            {
                Marshal.FreeHGlobal(pNTAuthority);
            }

            return success;
        }

But this equally turns out to be damned all use. All routes are answering the same question: “is this user currently an administrator”. Not, as we want, “does this user have the theoretical capacity to be an administrator”. What to do?

After much painful searching, I can across this article: http://www.microsoft.com/technet/technetmag/issues/2007/06/ACL/default.aspx which explains the changes to the Windows security APIs in Vista. I’d been asking myself “what’s the difference between the token for an adminstrator under XP, and the token for an administrator under Vista?” And this article provides the answer. It all comes down to SIDs and attributes.

What’s a SID? Well, to quote from the Microsoft Knowledge base:

“A security identifier (SID) is a unique value of variable length that is used to identify a security principal or security group in Windows operating systems. Well-known SIDs are a group of SIDs that identify generic users or generic groups. Their values remain constant across all operating systems.” I’d add that SIDs have an obscure binary format, and a more readily readable string format.

How is this relevant to us? Well, here I’ll refer you to MSDN for a decent explanation – http://msdn2.microsoft.com/en-us/library/aa374862(VS.85).aspx – but briefly, a logged in user is assigned an access token, which contains a set of SIDs (security IDs) corresponding to the various groups of which that user is a member, and a set of privileges which the user has. Securable “things” such as files have access control lists (ACLs) which allow or deny various different SIDs access to the thing in question. When it needs to know if a user has permission on some object, Windows runs through these lists in tandem; as soon as it hits a relevant “deny” entry, or if it doesn’t find any “allow” entries, then access is denied; otherwise, it’s allowed.

SIDs are usually accompanied by flags (known as attributes) in a SID_AND_ATTRIBUTES structure. Generally the SIDs associated with a user’s access token are “positive” flags, if you like: they list groups of which that user is a member. Here, for example, is a dump of the SIDs I have when logged into my own XP machine, with their display names, SID strings, and corresponding attributes in text format:

“FOODomain Users”  S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxx Mandatory EnabledByDefault Enabled
“Everyone”  S-1-1-0 Mandatory EnabledByDefault Enabled
“MYMACHINEDebugger Users”  S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxx Mandatory EnabledByDefault Enabled
“BUILTINAdministrators”  S-1-5-32-544 Mandatory EnabledByDefault Enabled Owner
“BUILTINUsers”  S-1-5-32-545 Mandatory EnabledByDefault Enabled
“NT AUTHORITYINTERACTIVE”  S-1-5-4 Mandatory EnabledByDefault Enabled
“NT AUTHORITYAuthenticated Users”  S-1-5-11 Mandatory EnabledByDefault Enabled
“LOCAL”  S-1-2-0 Mandatory EnabledByDefault Enabled
“FOOSoftwareDeveloper”  S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxx Mandatory EnabledByDefault Enabled
“FOOUserOfVirtualMachinesInSomeWay”  S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxx Mandatory EnabledByDefault Enabled
“BARLargeAdministrativeCheese”  S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxx Mandatory EnabledByDefault Enabled

Where you see “xxxx”, replace with an arbitrary sequence of numbers particular to my current domain. 

So you can see I’m a user on a domain; I’m an administrator; I’ve been properly authenticated; I’m a member of a group of people who write software for a living; and I also log on to virtual machines, and can administer the BAR domain with impunity.

If you want to try this on your own machine, you can download the “whoami” tool which is part of the “Windows XP Service Pack 2 Support Tools” from Microsoft. It gives you all of the above except for the permission flags.

To see the same on Windows Vista, we can run the bult in “whoami” command, which produces rather similar output. The key difference is as follows:

“BUILTINAdministrators”  S-1-5-32-544 UseForDenyOnly 

This is the essential difference between an “un-elevated” administrator on Vista, and an administrator on 2000 and XP. Previously the administrator had the BUILTINAdministrators SID enabled. In Vista pre elevation, administrators have the BUILTINAdministrators SID but set to “deny only”. Hence the lack of actual administrative powers until elevated.

This proves to be about the only way we can tell an un-elevated administrator on Vista from a standard user, who will not exhibit the BUILTINAdministrators SID at all.

So we have something we can check on Vista. How do we go about it? Well, we end up rewriting our existing IsUserAdmin(username,domain,password) method as follows:

         public static bool IsUserAdmin(string userName, string domain, string password)
        {
            IntPtr hToken;
            if (NativeSecurityApis.LogonUser(userName, domain, password, NativeSecurityApis.LOGON32_LOGON_INTERACTIVE, NativeSecurityApis.LOGON32_PROVIDER_DEFAULT, out hToken))
            {
                try
                {
                    return IsUserAdmin(hToken);
                }
                finally
                {
                    NativeSecurityApis.CloseHandle(hToken);
                }
            }
            else
            {
                try
                {
                    WindowsPrincipal principal = Win32SSPI.LogonUser(userName, domain, password);
                    return IsUserAdmin(principal);
                }
                catch (Exception ex)
                {
                    throw new LogonException(ex.Message, ex);
                }
            }
        }

We’ll add another helper overload for the case where, courtesy of the SSPI authentication for Windows 2000, we have a WindowsPrincipal rather than a token:

        public static bool IsUserAdmin(WindowsPrincipal principal)
        {
            WindowsIdentity identity = principal.Identity as WindowsIdentity;
            return IsUserAdmin(identity.Token);
        }

 And we can then write the function underlying both of these overloads:

        public static bool IsUserAdmin(IntPtr hToken)
        {
            string adminSid = NativeSecurityApis.STRING_SID_BUILTIN_ADMINISTRATORS; // “S-1-5-32-544”
            IntPtr pTokenGroups = GetTokenGroups(hToken);
            try
            {
                foreach (SidAndAttributes sid in GetTokenGroupStringSids(pTokenGroups))
                {
                    if (StringComparer.InvariantCultureIgnoreCase.Compare(sid.SidText, adminSid) == 0)
                    {
                        if (sid.IsGroupEnabled /* what we’d normally expect */||
                             sid.IsGroupUseForDenyOnly /* Vista: present but is deny only */ )
                        {
                            return true;
                        }
                    }
                }

                return false; // no admin SID, or not enabled and not deny only.
            }
            finally
            {
                FreeTokenGroups(pTokenGroups);
            }
        }

 This function uses a lot of helper mojo which we haven’t defined yet, but demonstrates the basic algorithm. We acquire the set of user groups associated with the authenticated user’s token; we iterate through them looking for the BUILTINAdministrator SID; and we count the user as an administrator if the SID’s attributes are either “enabled” (2000, XP) or “deny only” (Vista).

To get the token’s groups, we employ the following methods. We call the GetTokenInformation() API which can pull out lots of interesting things about a user’s token.

        private static IntPtr GetTokenGroups(IntPtr hToken)
        {
            uint dwSize = 0;
            if (!NativeSecurityApis.GetTokenInformation(hToken, NativeSecurityApis.TOKEN_INFORMATION_CLASS.TokenGroups, IntPtr.Zero, dwSize, out dwSize))
            {
                if (Marshal.GetLastWin32Error() != NativeSecurityApis.ERROR_INSUFFICIENT_BUFFER)
                    throw new Win32Exception();
            }

            IntPtr pTokenGroups = Marshal.AllocHGlobal((int)dwSize);
            try
            {
                if (!NativeSecurityApis.GetTokenInformation(hToken, NativeSecurityApis.TOKEN_INFORMATION_CLASS.TokenGroups, pTokenGroups, dwSize, out dwSize))
                    throw new Win32Exception();

                return pTokenGroups;
            }
            catch (Exception)
            {
                Marshal.FreeHGlobal(pTokenGroups);
                throw;
            }
        }

        private static void FreeTokenGroups(IntPtr pTokenGroups)
        {
            Marshal.FreeHGlobal(pTokenGroups);
        }

Essentially we call GetTokenInformation() to find out how much memory we need to allocate for a copy of the token group information; then we allocate said memory and call the API again to fetch the information.

The token groups we’ve retrieved are defined in the platform SDK as follows:

So we have a memory block which contains a group count, then a SID for each group with an accompanying set of attribute flags. We can treat the contents of the SID as a black box, since all we need to do is to be able to compare these SIDs against the standard SID for BUILTINAdministrators; if we find a match, we then ensure that the SID’s attributes include either SE_GROUP_ENABLED or SE_GROUP_USE_FOR_DENY_ONLY.

A caveat in dealing with the above is that in the platform SDK these structures are not explicitly packed. The compiler will therefore align each field on the nearest n byte boundary where n is 4 on 32 bit systems and 8 on 64 bit systems. Consequently on 64 bit systems, their layout in memory equivalent to the following: 

Unfortunately there’s no magic we can insert into a C# structure definition to say “Align this structure in the same way it would be aligned in C++ by default”. So when reading this information I chose to just read it in an IntPtr/Int32 at a time using the Marshal class. One could equally define multiple versions of structures with different packing and switch between them based on platform/sizeof(IntPtr).

I created the following simple wrapper to hold a SID and its attributes:

    internal class SidAndAttributes
    {
        private readonly string m_SidText;
        private readonly int m_Attributes;

        public SidAndAttributes(IntPtr pSid, int attributes)
        {
            IntPtr pString;
            if (!NativeSecurityApis.ConvertSidToStringSid(pSid, out pString))
                throw new Win32Exception(); // last error

            m_SidText = Marshal.PtrToStringAuto(pString);
            NativeSecurityApis.LocalFree(pString);

            m_Attributes = attributes;
        }

        public string SidText
        {
            get { return m_SidText; }
        }

        public int Attributes
        {
            get { return m_Attributes; }
        }

        public bool IsGroupEnabled
        {
            get { return (m_Attributes & NativeSecurityApis.SE_GROUP_ENABLED) == NativeSecurityApis.SE_GROUP_ENABLED; }
        }

        public bool IsGroupUseForDenyOnly
        {
            get { return (m_Attributes & NativeSecurityApis.SE_GROUP_USE_FOR_DENY_ONLY) == NativeSecurityApis.SE_GROUP_USE_FOR_DENY_ONLY; }
        }
    }

 
And used the following method to traverse the TOKEN_GROUPS structure and read its array of SID_AND_ATTRIBUTES structures into an IEnumerable<SidAndAttributes>:

        private static IEnumerable<SidAndAttributes> GetTokenGroupStringSids(IntPtr pTokenGroups)
        {
            List<SidAndAttributes> list = new List<SidAndAttributes>();
            if (pTokenGroups == IntPtr.Zero)
                return list;

            int groupCount = Marshal.ReadInt32(pTokenGroups, 0);

            // read in SID_AND_ATTRIBUTES items.
            // Due to the way these are packed, sizeof(SID_AND_ATTRIBUTES) and sizeof(TOKEN_GROUPS) varies
            // depending on whether the platform is 32 or 64 bit.
            //
            long sizeof_Int32 = Marshal.SizeOf(typeof(Int32));
            long sizeof_IntPtr = Marshal.SizeOf(typeof(IntPtr));

            long offset = (long)pTokenGroups;
            offset += Marshal.SizeOf(typeof(Int32));

            if (Marshal.SizeOf(typeof(IntPtr)) != Marshal.SizeOf(typeof(Int32)))
                offset += sizeof_Int32; // extra padding on Win64

            for (int iGroup = 0; iGroup < groupCount; iGroup++)
            {
                IntPtr pSid = Marshal.ReadIntPtr((IntPtr)offset);
                offset += sizeof_IntPtr;

                int attributes = Marshal.ReadInt32((IntPtr)offset);
                offset += sizeof_Int32;

                if (Marshal.SizeOf(typeof(IntPtr)) != Marshal.SizeOf(typeof(Int32)))
                    offset += sizeof_Int32; // extra padding on Win64

                SidAndAttributes item = new SidAndAttributes(pSid, attributes);
                list.Add(item);
            }

            return list;
        }
    }
 

 And finally, we’re done.

Conclusion

I don’t see any reason why this entire task can’t be made courtesy of a single API call. On Windows XP, it requires a handfull of calls; on Windows 2000, a large sequence (hidden by C#, thankfully) of mandatory but rather tangential calls; on Windows Vista, less than a dozen API calls to check something which is in fact little more than an artifact of the implementation of UAC, there being no obvious other route to take. In any case, there is no reason why information on how to actually validate user credentials, and check whether that user account has administrator privileges, should be difficult to track down or comprehend. Hopefully this article somewhat addresses that issue.