Tuesday, 23 February 2010

Faking a Strong Name

Introduction

Once upon a time a strong name was just that - strong. If you'd referenced an assembly with a strong name within your application you could be sure that that's the assembly you were going to get a run-time. Well, even that was never true. An assembly re-direct could cause you to be delivered a different version but it would always have to have the same name and public key token, and hence must have originated from the same source. They key thing is that no-one could slip your application a fake assembly and pass it off as the real thing.

Well, it turns out that now they can. You simply place your fake assembly in the GAC folder within which the real version of the assembly would logically reside. The name of the folder is used to establish its version and public key token. Don't believe me? Read on...

Scenario

The scenario is based upon a situation I encountered a few years ago. ThirdPartySecurity contains a class called DirectoryServices which is essentially a wrapper around System.DirectoryServices - it allows the caller to easily performs some look-ups against Active Directory ('who are the members of this group', that kind of thing). ConsoleApplication has a reference to ThirdPartySecurity and simply acts as a test harness. MockDirectoryServices is exactly that - a mocked-up implementation of System.DirectoryServices. It is not referenced by anything.

Solution Explorer view of FakingAStrongName.sln

He's the source code for the third-party DirectoryServices in its entirety. This is the actual source code of the assembly which was performing this job on a project I worked on. The code is not mine. I did not write it. I've left the completely alone (despite the urge to perform some major re-factoring), with the exception that I changed the namespace to protect the innocent and have changed the catch block in GetUsersInGroup such that it re-throws an exception rather than logging and swallowing it.

using System;
using System.DirectoryServices;

namespace ThirdPartySecurity
{
    /// <summary>
    /// Summary description for ActiveDirectory.
    /// </summary>
    public class DirectoryServices
    {
        public DirectoryServices()
        {
        }

        #region public methods

        /// <summary>
        /// Takes an Active Directory group name and returns an array of user names that
        /// belong to that single group.
        /// </summary>
        /// <param name="groupName"></param>
        /// <returns></returns>
        /// <remarks>If there are no groups return a null array.  If there are no users in the group, return an empty array.</remarks>
        public string[] GetUsersInGroup (string groupName)
        {
            //If no Users in the group, return an empty array.
            string [] result = new string[0];

            try
            {
                DirectorySearcher ds = new DirectorySearcher();
                ds.SearchRoot = new DirectoryEntry();   // start searching from local domain

                if (groupName.IndexOf("\\") > 0)
                    groupName = groupName.Substring(groupName.IndexOf("\\")+1); //Take out the Domain 

                ds.Filter = BuildLDAPFilter("group",groupName);

                SearchResultCollection src = ds.FindAll();
                DirectoryEntry objGroupEntry;

                int i = 0;
                if (src.Count == 1) //Must match to a single group
                {
                    foreach(SearchResult objResult in src) //for the 1st and only group
                    {
                        objGroupEntry = objResult.GetDirectoryEntry();
                        string[] members = new string[objGroupEntry.Properties["member"].Count];
                        foreach(object objMember in objGroupEntry.Properties["member"])
                        {
                            string memberDetails = objMember.ToString();
                            //strip out CN= from start of string and get name up to first comma delimiter
                            members[i] = (memberDetails.Substring(3,memberDetails.IndexOf(",")-3));
                            i++;
                        }
                        result = members;
                    }
                }
                else //If no groups return a Null Array
                {
                    result = null;
                }

                src.Dispose();
                ds.Dispose();
            }
            catch (System.Exception ex)
            {
                // NOTE:the original version of this catch block logged the exception via the
                //      Enterprise Library and then swallowed it; we'll simply re-throw it
                //Framework.EnterpriseLibrary.Log(String.Format("GetUsersInGroup(\"{0}\")failed (Active Directory is probably unavailable); the following exception hasbeen silently ignored.\n\n{1}", groupName, ex),Framework.Severity.Warning);
                throw;
            }

            return result;
        }


        /// <summary>
        /// Takes a user name in an Active Directory store and returns the display name for that user.
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public string GetDisplayName (string userName)
        {
            return GetPropertyFromUser(userName, "displayname");
        }


        /// <summary>
        /// /// Takes a user name in an Active Directory store and returns the email address for that user.
        /// </summary>
        /// <param name="userName"></param>
        /// <returns></returns>
        public string GetEmailAddress (string userName)
        {
            return GetPropertyFromUser(userName, "mail");
        }

        #endregion

        #region private methods

        /// <summary>
        /// Forms a filter string for the search in LDAP Format
        /// </summary>
        /// <param name="objectCategory"></param>
        /// <param name="filter"></param>
        /// <returns></returns>
        private string BuildLDAPFilter(string objectCategory, string filter)
        {
            String result;
            result = String.Format("(&(objectCategory={0})(name={1}))", objectCategory, filter);
            return result;
        }


        /// <summary>
        /// /// Takes a user name in an Active Directory store and returns a property specified by the caller for that user.
        /// </summary>
        /// <param name="userName">The user to search for the given <b>propertyName</b></param>
        /// <param name="propertyName">The property of the <b>userName</b> to be returned.</param>
        /// <returns></returns>
        /// <remarks>If the user is not found returns null, if the property does not exist return empty string</remarks>
        private string GetPropertyFromUser(string userName, string propertyName)
        {
            string result = null;

            DirectorySearcher ds = new DirectorySearcher();
            ds.SearchRoot = new DirectoryEntry();   // start searching from local domain
            ds.Filter = BuildLDAPFilter("user",userName);
            SearchResultCollection src = ds.FindAll();

            //TODO: Reject the search if more than 1 user returned
            if (src.Count == 1)
            {
                result = string.Empty; //If the property does not exist return an empty string
                foreach (SearchResult srcResult in src)
                    result = srcResult.Properties[propertyName][0].ToString();
            }

            return result;
        }


        #endregion
    }
}

Problem

The problem I had on this project was that I didn't have access to Active Directory when I was developing off-line on my laptop. Of course, the DirectoryServices class above could be modified in many ways to work around this: the most obvious way would be to have it implement an IDirectoryServices interface which could also be implemented by a new class called MockDirectoryServices, with some entry in the configuration file defining which implementation would be used at runtime. But that would rely upon having access to the source code, and I'm trying to show you a technique which will work even if you only have access to the assembly. (Hence why I called this assembly ThirdPartySecurity - to help us imagine that the source code is unavailable).

I can demonstrate the problem via the simple ConsoleApplication which makes use of ThirdPartySecurity to enumerate the members of a specific group.

using System;

namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[]args)
        {
            try
            {
                (new Program()).Run();
            }
            catch (System.Exception ex)
            {
                Console.WriteLine(ex);
            }

            // ask our AppDomain what DirectoryServices-related assemblies are loaded
            Console.WriteLine("\nDirectoryServices-related assemblies loaded into the current AppDomain are:");
            foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
            {
                if (assembly.GetName().Name.Contains("DirectoryServices"))
                {
                    Console.WriteLine("\t{0} (from {1})", assembly.FullName,assembly.CodeBase);
                }
            }

            Console.WriteLine("\nPress ENTER to exit");
            Console.ReadLine();
        }

        void Run()
        {
            // create an instance of the third-party DirectoryServices wrapper
            ThirdPartySecurity.DirectoryServices directoryServices = new ThirdPartySecurity.DirectoryServices();

            // define which group we'll be seeking members of
            string groupName = "MYDOMAIN\\MyGroup";
            Console.WriteLine("The members of the group \"{0}\"are:", groupName);

            // ask the third-party DirectoryServices wrapper for the members of the specified group
            string[] memberNames = directoryServices.GetUsersInGroup(groupName);

            // output the results
            foreach (string memberName in memberNames)
            {
                Console.WriteLine("\t{0}", memberName);
            }
        }
    }
}

Obviously ConsoleApplication has a reference to ThirdPartySecurity and ThirdPartySecurity has a reference to System.DirectoryServices. The reason I included code to display assemblies within the AppDomain containing the text 'DirectoryServices' in their name will become clear later on.

If I run this code on my laptop, which is currently disconnected and has no access to Active Directory, I get the following output. I've highlighted key items within the output.

The members of the group "MYDOMAIN\MyGroup"are:
System.Runtime.InteropServices.COMException (0x8007054B): The specified domain e
ither does not exist or could not be contacted.

   at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
   at System.DirectoryServices.DirectoryEntry.Bind()
   at System.DirectoryServices.DirectoryEntry.get_AdsObject()
   at System.DirectoryServices.PropertyValueCollection.PopulateList()
   at System.DirectoryServices.PropertyValueCollection..ctor(DirectoryEntry entr
y, String propertyName)
   at System.DirectoryServices.PropertyCollection.get_Item(String propertyName)
   at System.DirectoryServices.DirectoryEntry.Bind(Boolean throwIfFail)
   at System.DirectoryServices.DirectoryEntry.Bind()
   at System.DirectoryServices.DirectoryEntry.get_AdsObject()
   at System.DirectoryServices.DirectorySearcher.FindAll(Boolean findMoreThanOne
)
   at System.DirectoryServices.DirectorySearcher.FindAll()
   at ThirdPartySecurity.DirectoryServices.GetUsersInGroup(String groupName) in
C:\Users\Ian.Picknell\Documents\Blog\Strong Names\Faking a Strong Name\ThirdPart
ySecurity\DirectoryServices.cs:line 72
   at ConsoleApplication.Program.Run() in C:\Users\Ian.Picknell\Documents\Blog\S
trong Names\Faking a Strong Name\ConsoleApplication\Program.cs:line 42
   at ConsoleApplication.Program.Main(String[] args) in C:\Users\Ian.Picknell\Do
cuments\Blog\Strong Names\Faking a Strong Name\ConsoleApplication\Program.cs:lin
e 11

DirectoryServices-related assemblies loaded into the current AppDomain are:
        System.DirectoryServices, Version=2.0.0.0, Culture=neutral, PublicKeyTok
en=b03f5f7f11d50a3a (from file:///C:/Windows/assembly/GAC_MSIL/System.DirectoryS
ervices/2.0.0.0__b03f5f7f11d50a3a/System.DirectoryServices.dll)

Press ENTER to exit

Solution

The solution is to use a mock implementation of System.DirectoryServices and then persuade ThirdPartySecurity to use that instead - despite the fact that it holds a reference to the strong-named real System.DirectoryServices. I highlighted the version number and public key tokens in the output above because they'll change in a moment.

Let's take a look at the mocked-up System.DirectoryServices. You'll note that I've had to make sure that the methods exist on the same classes that they would in the real System.DirectoryServices. For example, when ThirdPartySecurity calls Dispose on the real DirectorySearcher it's actually calling the Dispose method on System.ComponentModel.Component because that's what the real DirectorySearcher inherits from. So our fake version does too. By contrast, the real PropertyCollection has no base class - it handles all the collection logic itself; so our mock version needs to ensure that properties like Count are available directly on the PropertyCollection class and are not simply inherited from the base Dictionary.

namespace System.DirectoryServices
{
    public class DirectorySearcher : System.ComponentModel.Component
    {
        public DirectoryEntry SearchRoot { get; set; }
        public string Filter { get; set; }
 
        public SearchResultCollection FindAll()
        {
            SearchResultCollection searchResultCollection = new SearchResultCollection();
            SearchResult searchResult = new SearchResult();
            searchResultCollection.Add(searchResult);
            return searchResultCollection;
        }
    }

    public class DirectoryEntry
    {
        private PropertyCollection _properties;

        public PropertyCollection Properties
        {
            get { return _properties; }
            set { _properties = value; }
        }
    }

    public class SearchResult
    {
        public DirectoryEntry GetDirectoryEntry()
        {
            DirectoryEntry directoryEntry = new DirectoryEntry();
            directoryEntry.Properties = new PropertyCollection();
            PropertyValueCollection members = new PropertyValueCollection();
            ((System.Collections.IList)members).Add(@"CN=MYDOMAIN\ian.picknell,otherstuff");
            ((System.Collections.IList)members).Add(@"CN=MYDOMAIN\jane.armstrong,otherstuff");
            directoryEntry.Properties["member"] = members;
            return directoryEntry;
        }
    }

    public class SearchResultCollection : System.Collections.ArrayList, IDisposable
    {
        public void Dispose()
        {
        }
    }

    public class PropertyCollection : System.Collections.Generic.Dictionary<string, PropertyValueCollection>
    {
        new public int Count
        {
            get { return base.Count; }
        }

        new public PropertyValueCollection this[string key]
        {
            get { return base[key]; }
            set { base[key] = value; }
        }
    }

    public class PropertyValueCollection : System.Collections.CollectionBase
    {
    }
}

We need to make sure this is compiled to an assembly named System.DirectoryServices via the Assembly Name property on the Application tab of Project Properties. We also need to make sure that it has a strong name - I just assigned it a new key pair via the Signing tab of Project Properties.

Okay, so how to be persuade ThirdPartySecurity to use this mock implementation of System.DirectoryServices rather than the real thing?

Magic

The first thing we need to do is add a configuration file for ConsoleApplication to redirect requests for System.DirectoryServices from version 2.0.0.0 to some other version - I'll choose 2.1.0.0.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="System.DirectoryServices"
                          publicKeyToken="b03f5f7f11d50a3a"
                          culture="neutral" />
        <bindingRedirect oldVersion="2.0.0.0"
                         newVersion="2.1.0.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

This just uses a standard assembly re-direction. As I mentioned earlier, such re-directions only allow the version number to be changed - you can't change the assembly name or public key token.

To persuade .NET that our MockDirectoryServices is actually the real System.DirectoryServices simply create the appropriate folder in the GAC and drop it in there:

> MD %SystemRoot%\assembly\GAC_MSIL\System.DirectoryServices\2.1.0.0__b03f5f7f11d50a3a
> COPY System.DirectoryServices.dll %SystemRoot%\assembly\GAC_MSIL\System.DirectoryServices\2.1.0.0__b03f5f7f11d50a3a

So we're copying our System.DirectoryServices (actually generated from the MockDirectoryServices project), which has a version number of 1.0.0.0 (because we didn't change it) and some random public key token and dropping it in the folder which should contain version 2.1.0.0 of System.DirectoryServices, signed with Microsoft's key pair (which has a public key token of b03f5f7f11d50a3a). In other words, we're lying.

When we run ConsoleApplication this time, we get a much more healthy result - even with no Active Directory in sight. Again, I've highlighted some interesting elements in the output.

The members of the group "MYDOMAIN\MyGroup"are:
        MYDOMAIN\ian.picknell
        MYDOMAIN\jane.armstrong

DirectoryServices-related assemblies loaded into the current AppDomain are:
        System.DirectoryServices, Version=1.0.0.0, Culture=neutral, PublicKeyTok
en=7c6ae5d5059e81d9 (from file:///C:/Windows/assembly/GAC_MSIL/System.DirectoryS
ervices/2.1.0.0__b03f5f7f11d50a3a/System.DirectoryServices.dll)

Press ENTER to exit

You'll note that our AppDomain knows that it's dealing with version 1.0.0.0 of an assembly which has a public key token of eeab6744580125d7, which is clearly not the version ThirdPartySecurity references. But it doesn't seem to care. It was loaded from a folder within the GAC where version 2.1.0.0 with a public key token of b03f5f7f11d50a3a was expected to exist, and that seems good enough for it.

So, if you ever find yourself needing to provide a mock implementation of an assembly which has a strong name, now you know how.

See Also

No comments:

Post a Comment