How to deal with localised or renamed Administrator accounts in .NET and c#

For a project I needed to create a new group and add the Administrators account to it. 

In general, this is easy to achieve: 

   1:  public static bool AddUserToGroup(string userId, string groupName)
   2:  {
   3:      try
   4:      {
   5:          using (PrincipalContext pc = new PrincipalContext(ContextType.Machine, Environment.MachineName))
   6:          {
   7:              GroupPrincipal group = GroupPrincipal.FindByIdentity(pc, groupName);
   8:              group.Members.Add(pc, IdentityType.Name, userId);
   9:              group.Save();
  10:              return true;
  11:          }
  12:      }
  13:      catch(Exception ex)
  14:      {
  15:          // Logging
  16:          return false; 
  17:      }
  18:  }

This code works fine if you call this method with "Administrator".


Two problems

But there are two problems with this approach: 

  1. The Administrator account is localised by Microsoft. They state this here and show all localised Versions
  2. The Administrator account can be renamed for security reasons


After some googl.. aeh binging i found this Microsoft support page: How to deal with localized and renamed user and group names. The article itself is from 2006 and the sourcecode, 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 find 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 have a well-known schema for a specified list of accounts - this is documented here: Well-known security identifiers in Windows operating systems

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: 

   1:  string aSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).ToString();
   2:  string adminLocalized = new System.Security.Principal.SecurityIdentifier(aSID).Translate(typeof(System.Security.Principal.NTAccount)).ToString();

If you look closely, the enum says BuiltinAdministratorsSid. Yes, thats 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 results 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! 

A local group within a local group on Windows 8.1

A local group within a local group on Windows 8.1

This should not be even supported by a non-domain joined machine - according to Microsoft.  You cannot achieve this via the UI - so i deleted the Group and researched further ;)


According to this Stackoverflow post, the SID Administrator has the following pattern: 

 S-1-5-(some other SID parts)-500. Thats 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 optimize a bit and only iterate over all users inside the Administrators group (since we have found this one by accident :)


   1:  public static string GetLocalizedAdministratorsAccountName()
   2:  {
   3:      DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName);
   4:      string adminsSID = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null).ToString();
   6:      string localizedAdmin = new System.Security.Principal.SecurityIdentifier(adminsSID).Translate(typeof(System.Security.Principal.NTAccount)).ToString();
   8:      localizedAdmin = localizedAdmin.Replace(@"BUILTIN\", "");
  10:      DirectoryEntry admGroup = localMachine.Children.Find(localizedAdmin, "group");
  11:      object adminmembers = admGroup.Invoke("members", null);
  13:      DirectoryEntry userGroup = localMachine.Children.Find("users", "group");
  14:      object usermembers = userGroup.Invoke("members", null);
  16:      //Retrieve each user name.
  17:      foreach (object groupMember in (IEnumerable)adminmembers)
  18:      {
  19:          DirectoryEntry member = new DirectoryEntry(groupMember);
  21:          string sidAsText = GetTextualSID(member);
  22:          if (IsAdminSid(sidAsText))
  23:          {
  24:              return member.Name;
  25:          }
  26:      }
  27:      return "";
  28:  }

This method iterates over all users in the Administrators group and checks, if it adheres to the pattern, using this simple helper method: 

   1:  private static bool IsAdminSid(string sid)
   2:  {
   3:      return sid.StartsWith("S-1-5") && sid.EndsWith("-500");
   4:  }

But before we have the SID as a string, we need to do some PInvoke stuff, to get the byte array converted correctly.  

   1:  [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)]
   2:  static extern bool ConvertSidToStringSid(IntPtr pSid, out string strSid);
   4:  private static string GetTextualSID(DirectoryEntry objGroup)
   5:  {
   6:      string sSID = string.Empty;
   7:      byte[] SID = objGroup.Properties["objectSID"].Value as byte[];
   8:      IntPtr sidPtr = Marshal.AllocHGlobal(SID.Length);
   9:      sSID = "";
  10:      System.Runtime.InteropServices.Marshal.Copy(SID, 0, sidPtr, SID.Length);
  11:      ConvertSidToStringSid((IntPtr)sidPtr, out sSID);
  12:      System.Runtime.InteropServices.Marshal.FreeHGlobal(sidPtr);
  13:      return sSID;
  15:  }

So having all these pieces together, i was finally able to identify the local Administrator account ... a bit more effort that i would have estimated ;) 

Posted on September 12, 2013 and filed under development.