Update, October 2026 — the idea holds, the code can be much shorter.
The core insight of this post is still correct: never address built-in accounts or groups by name. Names are localised and can be renamed; SIDs cannot. What has changed is how much code that takes.
- No PInvoke needed.
ConvertSidToStringSidis unnecessary —UserPrincipal.Sidalready hands you aSecurityIdentifier, and for raw bytesnew SecurityIdentifier(bytes, 0).Valuedoes the conversion.- For groups, skip names entirely. The Administrators group has the fixed SID
S-1-5-32-544on every machine, so you can add members without ever knowing what the group is called in Polish or Japanese:using System.DirectoryServices.AccountManagement; using var ctx = new PrincipalContext(ContextType.Machine); var admins = GroupPrincipal.FindByIdentity(ctx, IdentityType.Sid, "S-1-5-32-544");
- For the built-in Administrator account, the RID-500 rule from below still applies. Without PInvoke and without
DirectoryEntry:using var ctx = new PrincipalContext(ContextType.Machine); using var searcher = new PrincipalSearcher(new UserPrincipal(ctx)); var admin = searcher.FindAll() .OfType<UserPrincipal>() .FirstOrDefault(u => u.Sid.Value.StartsWith("S-1-5-21-") && u.Sid.Value.EndsWith("-500"));
- On .NET 5 and later,
System.DirectoryServices.AccountManagementis a NuGet package and Windows-only.- Since Windows Vista the built-in Administrator account is disabled by default. If your code depends on it existing and being usable, check that assumption before anything else.
The text from here on is the 2013 original, unchanged.
For a project I needed to create a new group and add the Administrators account to it. In general, this is easy to achieve:
public static bool AddUserToGroup(string userId, string groupName)
{
try
{
using (PrincipalContext pc = new PrincipalContext(ContextType.Machine, Environment.MachineName))
{
GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, groupName);
group.Members.Add(pc, IdentityType.Name, userId);
group.Save();
return true;
}
}
catch (Exception ex)
{
// Logging
return false;
}
}This code works fine if you call this method with “Administrator”.
Two problems
But there are two problems with this approach:
- The Administrator account is localised by Microsoft.
- The Administrator account can be renamed for security reasons.
After some googl… aeh binging I found the Microsoft support article How to deal with localized and renamed user and group names (KB 157234). The article itself is from 2006 and the source code they show there is from 1996!! And in C++… But I wanted to have the solution in .NET — so I continued.
What about iterating over all local users and finding the Administrator? Iterating is easy, but how do you know that you have a handle to the Administrator account? Turns out, Microsoft has a definition for that:
Well-known SIDs
The SID of accounts on Windows has a well-known schema for a specified list of accounts — this is documented in Well-known security identifiers in Windows operating systems (KB 243330).
As an example, “Everyone” has the SID S-1-1-0. With this info, we can get the localised string of “Everyone” easily:
string everyone = new System.Security.Principal.SecurityIdentifier("S-1-1-0")
.Translate(typeof(System.Security.Principal.NTAccount)).ToString();There is even an enum for these predefined entities: System.Security.Principal.WellKnownSidType! So why not use:
string aSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).ToString();
string adminLocalized = new System.Security.Principal.SecurityIdentifier(aSID)
.Translate(typeof(System.Security.Principal.NTAccount)).ToString();If you look closely, the enum says BuiltinAdministratorsSid. Yes, that’s an “s” in the middle and means this is not the Administrator account, but the Administrators group! This is stated wrong also in the documentation of Microsoft.
So the result of this code is BUILTIN\Administrators.
Since I called my previous method AddUserToGroup with this, I ended up having the Administrators GROUP inside my own local Windows group!
This should not even be supported by a non-domain-joined machine — according to Microsoft (KB 974815). You cannot achieve this via the UI — so I deleted the group and researched further ;)
According to this Stack Overflow post, the SID of the Administrator has the following pattern: S-1-5-(some other SID parts)-500. That’s also the reason why there is no enum — the SID is changing from machine to machine…
On a hunt for the Administrator account
So we can iterate over all local users and find the one that has this pattern — and even optimise a bit and only iterate over all users inside the Administrators group (since we have found this one by accident :)
public static string GetLocalizedAdministratorsAccountName()
{
DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName);
string adminsSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).ToString();
string localizedAdmin = new System.Security.Principal.SecurityIdentifier(adminsSID)
.Translate(typeof(System.Security.Principal.NTAccount)).ToString();
localizedAdmin = localizedAdmin.Replace(@"BUILTIN\", "");
DirectoryEntry admGroup = localMachine.Children.Find(localizedAdmin, "group");
object adminmembers = admGroup.Invoke("members", null);
// Retrieve each user name.
foreach (object groupMember in (IEnumerable)adminmembers)
{
DirectoryEntry member = new DirectoryEntry(groupMember);
string sidAsText = GetTextualSID(member);
if (IsAdminSid(sidAsText))
{
return member.Name;
}
}
return "";
}This method iterates over all users in the Administrators group and checks if each adheres to the pattern, using this simple helper method:
private static bool IsAdminSid(string sid)
{
return sid.StartsWith("S-1-5") && sid.EndsWith("-500");
}But before we have the SID as a string, we need to do some PInvoke stuff to get the byte array converted correctly:
[DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool ConvertSidToStringSid(IntPtr pSid, out string strSid);
private static string GetTextualSID(DirectoryEntry objGroup)
{
string sSID = string.Empty;
byte[] SID = objGroup.Properties["objectSID"].Value as byte[];
IntPtr sidPtr = Marshal.AllocHGlobal(SID.Length);
System.Runtime.InteropServices.Marshal.Copy(SID, 0, sidPtr, SID.Length);
ConvertSidToStringSid((IntPtr)sidPtr, out sSID);
System.Runtime.InteropServices.Marshal.FreeHGlobal(sidPtr);
return sSID;
}So having all these pieces together, I was finally able to identify the local Administrator account… a bit more effort than I would have estimated ;)
