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)
"DOMAIN\UserName" (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 "DOMAIN\UserName"
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 name="userName">The
username (without domain qualifications, so no DOMAIN\user or user@DOMAIN here)</param>
/// <param name="domain">The domain
name (or ".")</param>
/// <param name="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:
"FOO\Domain Users" S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxx Mandatory EnabledByDefault
Enabled
"Everyone" S-1-1-0 Mandatory EnabledByDefault Enabled
"MYMACHINE\Debugger Users" S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxxx Mandatory
EnabledByDefault Enabled
"BUILTIN\Administrators" S-1-5-32-544 Mandatory EnabledByDefault Enabled Owner
"BUILTIN\Users" S-1-5-32-545 Mandatory EnabledByDefault Enabled
"NT AUTHORITY\INTERACTIVE" S-1-5-4 Mandatory EnabledByDefault Enabled
"NT AUTHORITY\Authenticated Users" S-1-5-11 Mandatory EnabledByDefault Enabled
"LOCAL" S-1-2-0 Mandatory EnabledByDefault Enabled
"FOO\SoftwareDeveloper" S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxx Mandatory
EnabledByDefault Enabled
"FOO\UserOfVirtualMachinesInSomeWay" S-1-5-21-xxxxxxxxx-xxxxxxxxx-xxxxxxxxx-xxx
Mandatory EnabledByDefault Enabled
"BAR\LargeAdministrativeCheese" 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:
"BUILTIN\Administrators" 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 BUILTIN\Administrators
SID enabled. In Vista pre elevation, administrators have the BUILTIN\Administrators
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 BUILTIN\Administrators 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 BUILTIN\Administrator
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:
typedef struct _TOKEN_GROUPS {
DWORD GroupCount;
SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY];
} TOKEN_GROUPS,
*PTOKEN_GROUPS;
typedef struct _SID_AND_ATTRIBUTES {
PSID Sid;
DWORD Attributes;
} SID_AND_ATTRIBUTES,
*PSID_AND_ATTRIBUTES;
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 BUILTIN\Administrators; 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:
#pragma pack(push,1)
typedef struct _TOKEN_GROUPS_64 {
DWORD GroupCount;
DWORD __Unused;
SID_AND_ATTRIBUTES_64 Groups[ANYSIZE_ARRAY];
} TOKEN_GROUPS,
*PTOKEN_GROUPS;
typedef struct _SID_AND_ATTRIBUTES_64 {
PSID Sid;
DWORD Attributes;
DWORD __Unused;
} SID_AND_ATTRIBUTES,
*PSID_AND_ATTRIBUTES;
#pragma pack(pop)
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.