Showing posts with label Security. Show all posts
Showing posts with label Security. Show all posts

Finding the parent of a process in C#

If you want to find the parent process of a Windows process from .NET, there are a couple of alternatives. None of these alternatives is very straightforward, because this functionality is not a built into the System.Diagnostics.Process class. Well ... at least not by default. You can of course inject it via an extension method -say ParentProcess()- to enable calls like the following:

            // Find my own parent.
            Process myProcess = Process.GetCurrentProcess();
            Console.WriteLine(myProcess.ParentProcess().ProcessName);
 
            // Create a child, and find its parent.
            Process p = Process.Start("notepad.exe");
            Console.WriteLine(p.ParentProcess().ProcessName);


This code displays the parent of a test program, then starts a Notepad instance and requests its parent. The resulting console looks like this (with and without a debugger):

 

I've seen C# implementations that read performance counters or call WMI queries to get the parent process id, but I assume that the following extension method will outperform these:

//-----------------------------------------------------------------------
// <copyright file="ProcessExtensions.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the ProcessExtensions class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
 
    /// <summary>
    /// Extension Methods for the System.Diagnostics.Process Class.
    /// </summary>
    public static class ProcessExtensions
    {
        /// <summary>
        /// Returns the Parent Process of a Process
        /// </summary>
        /// <param name="process">The Windows Process.</param>
        /// <returns>The Parent Process of the Process.</returns>
        public static Process ParentProcess(this Process process)
        {
            int parentPid = 0;
            int processPid = process.Id;
            uint TH32CS_SNAPPROCESS = 2;
 
            // Take snapshot of processes
            IntPtr hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
 
            if (hSnapshot == IntPtr.Zero)
            {
                return null;
            }
 
            PROCESSENTRY32 procInfo = new PROCESSENTRY32();
 
            procInfo.dwSize = (uint)Marshal.SizeOf(typeof(PROCESSENTRY32));
 
            // Read first
            if (Process32First(hSnapshot, ref procInfo) == false)
            {
                return null;
            }
 
            // Loop through the snapshot
            do
            {
                // If it's me, then ask for my parent.
                if (processPid == procInfo.th32ProcessID)
                {
                    parentPid = (int)procInfo.th32ParentProcessID;
                }
            }
            while (parentPid == 0 && Process32Next(hSnapshot, ref procInfo)); // Read next
 
            if (parentPid > 0)
            {
                return Process.GetProcessById(parentPid);
            }
            else
            {
                return null;
            }
        }
 
        /// <summary>
        /// Takes a snapshot of the specified processes, as well as the heaps, 
        /// modules, and threads used by these processes.
        /// </summary>
        /// <param name="dwFlags">
        /// The portions of the system to be included in the snapshot.
        /// </param>
        /// <param name="th32ProcessID">
        /// The process identifier of the process to be included in the snapshot.
        /// </param>
        /// <returns>
        /// If the function succeeds, it returns an open handle to the specified snapshot.
        /// If the function fails, it returns INVALID_HANDLE_VALUE.
        /// </returns>
        [DllImport("kernel32.dll", SetLastError = true)]
        private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
 
        /// <summary>
        /// Retrieves information about the first process encountered in a system snapshot.
        /// </summary>
        /// <param name="hSnapshot">A handle to the snapshot.</param>
        /// <param name="lppe">A pointer to a PROCESSENTRY32 structure.</param>
        /// <returns>
        /// Returns TRUE if the first entry of the process list has been copied to the buffer.
        /// Returns FALSE otherwise.
        /// </returns>
        [DllImport("kernel32.dll")]
        private static extern bool Process32First(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
 
        /// <summary>
        /// Retrieves information about the next process recorded in a system snapshot.
        /// </summary>
        /// <param name="hSnapshot">A handle to the snapshot.</param>
        /// <param name="lppe">A pointer to a PROCESSENTRY32 structure.</param>
        /// <returns>
        /// Returns TRUE if the next entry of the process list has been copied to the buffer.
        /// Returns FALSE otherwise.</returns>
        [DllImport("kernel32.dll")]
        private static extern bool Process32Next(IntPtr hSnapshot, ref PROCESSENTRY32 lppe);
 
        /// <summary>
        /// Describes an entry from a list of the processes residing 
        /// in the system address space when a snapshot was taken.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        private struct PROCESSENTRY32
        {
            public uint dwSize;
            public uint cntUsage;
            public uint th32ProcessID;
            public IntPtr th32DefaultHeapID;
            public uint th32ModuleID;
            public uint cntThreads;
            public uint th32ParentProcessID;
            public int pcPriClassBase;
            public uint dwFlags;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string szExeFile;
        }
    }
}

Finding the owner of a Process in C#

The System.Diagnostics.Process class provides lots of information about any running process, but unfortunately not its owner's identity. We need to invoke unmanaged code to get that. Every process executed on behalf of a user has a copy of its access token. That token is returned by the OpenProcessToken function, and can then be used in a WindowsIdentity constructor. I created a wrapper around this functionality as an extension method to the Process class. This allows you to get the associated Windows Identity of a Process by simply calling aProcess.WindowsIdentity.

Here's how you use the new method:

            ////
            // 'WindowsIdentity' Extension Method Demo: 
            //   Enumerate all running processes with their associated Windows Identity
            ////
 
            foreach (var p in Process.GetProcesses())
            {
                string processName;
 
                try
                {
                    processName = p.WindowsIdentity().Name;
 
                }
                catch (Exception ex)
                {
 
                    processName = ex.Message; // Probably "Access is denied"
                }
 
                Console.WriteLine(p.ProcessName + " (" + processName + ")");
            }


Here's the corresponding class:

//-----------------------------------------------------------------------
// <copyright file="ProcessExtensions.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the ProcessExtensions class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Security.Principal;
 
    /// <summary>
    /// Extension Methods for the System.Diagnostics.Process Class.
    /// </summary>
    public static class ProcessExtensions
    {
        /// <summary>
        /// Required to query an access token.
        /// </summary>
        private static uint TOKEN_QUERY = 0x0008;
 
        /// <summary>
        /// Returns the WindowsIdentity associated to a Process
        /// </summary>
        /// <param name="process">The Windows Process.</param>
        /// <returns>The WindowsIdentity of the Process.</returns>
        /// <remarks>Be prepared for 'Access Denied' Exceptions</remarks>
        public static WindowsIdentity WindowsIdentity(this Process process)
        {
            IntPtr ph = IntPtr.Zero;
            WindowsIdentity wi = null;
            try
            {
                OpenProcessToken(process.Handle, TOKEN_QUERY, out ph);
                wi = new WindowsIdentity(ph);
            }
            catch (Exception)
            {
                throw;
            }
            finally
            {
                if (ph != IntPtr.Zero)
                {
                    CloseHandle(ph);
                }
            }
 
            return wi;
        }
 
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool CloseHandle(IntPtr hObject);
    }
}

Loading a User Profile from C#

Lightweight Impersonation using the LogonUser function may be nice for accessing a file or a database. It changes the credentials, but not the profile associated with the process. If you want access the impersonated account's mapped network drives, printers, environment variables and special folders (e.g. DeskTop, My Documents, and Application Data Folders) then you need to load its profile.

Changing the profile associated with a process is not obvious. The win32 APIs contain functions to do that: theoretically you could -after the impersonation for the current process started- respectively call ImpersonateLoggedOnUser to put the security context on the thread, LoadUserProfile to load the User Hive and CreateEnvironmentBlock to get the environment variables for the specified user. Unfortunately you need to have an unrealistic set of permissions to make this work (other than to a specific account for a Windows Service, I would be reluctant to assign 'Administrator' as well as 'Part of the Operating System' privileges).

So when you want to load a specific account's user profile, it makes probably more sense to simply start another process under that account. The ProcessStartInfo structure -used as parameter of .NET's Process.Start() method- has all the necessary attributes to make sure the account's profile is loaded.

Here's some sample code; it's the Main method of a console application that restarts itself under another account:

    ////
    // Process Restart Demo: Restart process under another account
    ////
 
    Console.Write("User Name: ");
    Console.WriteLine(WindowsIdentity.GetCurrent().Name);
    Console.Write("Application Directory: ");
    Console.WriteLine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData));
 
    // Create Impersonation Object
    Impersonation impersonation = new Impersonation("YourDomain", "Test123", "Test123");
 
    // Ask for Restart
    Console.ForegroundColor = ConsoleColor.Cyan;
    Console.Write("\nReady to restart (Y/N): ");
    ConsoleKeyInfo key = Console.ReadKey();
    if ((key.KeyChar == 'y') || (key.KeyChar == 'Y'))
    {
        // Start Process
        try
        {
            impersonation.StartProcess(Assembly.GetExecutingAssembly().Location);
        }
        catch (Exception ex)
        {
            // Oops
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("\n\n" + ex.Message);
            Console.ReadKey();
        }
    }


Here's the full Impersonation class:

//-----------------------------------------------------------------------
// <copyright file="Impersonation.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the Impersonation class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Runtime.InteropServices;
    using System.Security;
    using System.Security.Principal;
 
    /// <summary>
    /// Facilitates impersonation of a Windows User.
    /// </summary>
    public class Impersonation
    {
        /// <summary>
        /// The User Id.
        /// </summary>
        private string userName = string.Empty;
 
        /// <summary>
        /// The Domain.
        /// </summary>
        private string domain = string.Empty;
 
        /// <summary>
        /// The Password.
        /// </summary>
        private SecureString password;
 
        /// <summary>
        /// Windows Token.
        /// </summary>
        private IntPtr tokenHandle = new IntPtr(0);
 
        /// <summary>
        /// The impersonated User.
        /// </summary>
        private WindowsImpersonationContext impersonatedUser;
 
        /// <summary>
        /// Initializes a new instance of the Impersonation class.
        /// </summary>
        /// <param name="domainName">Domain name of the impersonated user.</param>
        /// <param name="userName">Name of the impersonated user.</param>
        /// <param name="password">Password of the impersonated user.</param>
        /// <remarks>
        /// Uses the unmanaged LogonUser function to get the user token for
        /// the specified user, domain, and password.
        /// </remarks>
        public Impersonation(string domainName, string userName, string password)
        {
            // Fill private field for later use
            this.domain = domainName;
            this.userName = userName;
            this.password = new SecureString();
            char[] passwordChars = password.ToCharArray();
            foreach (char c in passwordChars)
            {
                this.password.AppendChar(c);
            }
 
            // Use the standard logon provider.
            const int LOGON32_PROVIDER_DEFAULT = 0;
 
            // Create a primary token.
            const int LOGON32_LOGON_INTERACTIVE = 2;
 
            this.tokenHandle = IntPtr.Zero;
 
            // Call LogonUser to obtain a handle to an access token.
            bool returnValue = LogonUser(
                                userName,
                                domainName,
                                password,
                                LOGON32_LOGON_INTERACTIVE,
                                LOGON32_PROVIDER_DEFAULT,
                                ref this.tokenHandle);
 
            if (false == returnValue)
            {
                // Something went wrong.
                int ret = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(ret);
            }
        }
 
        /// <summary>
        /// Starts the impersonation.
        /// </summary>
        public void Impersonate()
        {
            // Create Identity.
            WindowsIdentity newId = new WindowsIdentity(this.tokenHandle);
 
            // Start impersonating.
            this.impersonatedUser = newId.Impersonate();
        }
 
        /// <summary>
        /// Stops the impersonation and releases security token.
        /// </summary>
        public void Revert()
        {
            // Stop impersonating.
            if (this.impersonatedUser != null)
            {
                this.impersonatedUser.Undo();
            }
 
            // Release the token.
            if (this.tokenHandle != IntPtr.Zero)
            {
                CloseHandle(this.tokenHandle);
            }
        }
 
        /// <summary>
        /// Spawns a process under a specific account.
        /// </summary>
        /// <param name="fullPath">Full path to the executable.</param>
        /// <remarks>There's no need to call 'Impersonate' first.</remarks>
        public void StartProcess(string fullPath)
        {
            ProcessStartInfo info = new ProcessStartInfo();
 
            // File properties
            info.FileName = Path.GetFileName(fullPath);
            info.WorkingDirectory = Path.GetDirectoryName(fullPath);
 
            // User properties
            info.Domain = this.domain;
            info.UserName = this.userName;
            info.Password = this.password;
            info.LoadUserProfile = true; // Optional, but makes sense ...
            info.UseShellExecute = false; // Must be 'false' when 'UserName' is filled
 
            // Kick Off
            Process.Start(info);
        }
 
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);
 
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(
                string lpszUsername,
                string lpszDomain,
                string lpszPassword,
                int dwLogonType,
                int dwLogonProvider,
                ref IntPtr phToken);
    }
}

Windows Impersonation in C#

Every now and then an application component needs to impersonate another Windows user. Running a chunk of .NET code under a specific Windows account requires calls to some unmanaged functions. It makes sense to wrap this functionality in a separate class or in extension methods. I went for a wrapper class with Impersonate and Revert methods. This is how you use it:

    // Get Current Identity
    Console.WriteLine(WindowsIdentity.GetCurrent().Name); // --> Your current Account
 
    // Create Impersonation Object
    Impersonation impersonation = new Impersonation("YourDomain", "Test123", "Test123");
 
    // Start Impersonation
    impersonation.Impersonate();
    Console.WriteLine(WindowsIdentity.GetCurrent().Name); // --> Test123
 
    // Stop Impersonation
    impersonation.Revert();
    Console.WriteLine(WindowsIdentity.GetCurrent().Name); // --> Your current Account again


And here's the whole class definition:

//-----------------------------------------------------------------------
// <copyright file="Impersonation.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the Impersonation class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Runtime.InteropServices;
    using System.Security.Permissions;
    using System.Security.Principal;
 
    /// <summary>
    /// Facilitates impersonation of a Windows User.
    /// </summary>
    public class Impersonation
    {
        /// <summary>
        /// Windows Token.
        /// </summary>
        private IntPtr tokenHandle = new IntPtr(0);
 
        /// <summary>
        /// The impersonated User.
        /// </summary>
        private WindowsImpersonationContext impersonatedUser;
 
        /// <summary>
        /// Initializes a new instance of the Impersonation class.
        /// </summary>
        /// <param name="domainName">Domain name of the impersonated user.</param>
        /// <param name="userName">Name of the impersonated user.</param>
        /// <param name="password">Password of the impersonated user.</param>
        /// <remarks>
        /// Uses the unmanaged LogonUser function to get the user token for
        /// the specified user, domain, and password.
        /// </remarks>
        public Impersonation(string domainName, string userName, string password)
        {
            // Use the standard logon provider.
            const int LOGON32_PROVIDER_DEFAULT = 0;
 
            // Create a primary token.
            const int LOGON32_LOGON_INTERACTIVE = 2;
 
            this.tokenHandle = IntPtr.Zero;
 
            // Call LogonUser to obtain a handle to an access token.
            bool returnValue = LogonUser(
                                userName,
                                domainName,
                                password,
                                LOGON32_LOGON_INTERACTIVE,
                                LOGON32_PROVIDER_DEFAULT,
                                ref this.tokenHandle);
 
            if (false == returnValue)
            {
                // Something went wrong.
                int ret = Marshal.GetLastWin32Error();
                throw new System.ComponentModel.Win32Exception(ret);
            }
        }
 
        /// <summary>
        /// Starts the impersonation.
        /// </summary>
        public void Impersonate()
        {
            // Create Identity.
            WindowsIdentity newId = new WindowsIdentity(this.tokenHandle);
 
            // Start impersonating.
            this.impersonatedUser = newId.Impersonate();
        }
 
        /// <summary>
        /// Stops the impersonation and releases security token.
        /// </summary>
        public void Revert()
        {
            // Stop impersonating.
            if (this.impersonatedUser != null)
            {
                this.impersonatedUser.Undo();
            }
 
            // Release the token.
            if (this.tokenHandle != IntPtr.Zero)
            {
                CloseHandle(this.tokenHandle);
            }
        }
 
        [DllImport("advapi32.dll", SetLastError = true)]
        private static extern bool LogonUser(
                string lpszUsername,
                string lpszDomain,
                string lpszPassword,
                int dwLogonType,
                int dwLogonProvider,
                ref IntPtr phToken);
 
        [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
        private static extern bool CloseHandle(IntPtr handle);
    }
}

Querying LDAP from C#

Here's a class that will bring you up to speed in querying LDAP servers from C#. It allows you to configure some common settings (root path, and account information for Impersonation) and facilitates some common actions, like verifying if a user or a group exists, enumerating all users and groups, enumerating all group members, and validating a userid/password combination.

Here's how you might use the LDAPHelper:

// Point to Root [Optional]
LDAPHelper.RootPath = @"LDAP://myLDAPServer";
 
// Impersonate [Optional]
LDAPHelper.UserName = "aUserId";
LDAPHelper.Password = "aPassword";
 
// Typical Method Calls, with COMException Catch
try
{
    Console.WriteLine(LDAPHelper.UserExists("Diederik"));
    Console.WriteLine(LDAPHelper.GroupExists("Bloggers"));
}
catch (COMException ex)
{
    Console.WriteLine(ex.ErrorCode + "\t" + ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}


Here's the whole helper class:

//-----------------------------------------------------------------------
// <copyright file="LDAPHelper.cs" company="DockOfTheBay">
//     http://www.dotbay.be
// </copyright>
// <summary>Defines the LDAPHelper class.</summary>
//-----------------------------------------------------------------------
 
namespace DockOfTheBay
{
    using System;
    using System.Collections.Generic;
    using System.DirectoryServices;
 
    /// <summary>
    /// Facilitates commonly used actions against Active Directory.
    /// </summary>
    /// <remarks>
    /// Typical COMExceptions that may be returned are:
    ///     -2147016646: LDAP Server Down
    ///     -2147023570: Login failure
    ///     -2147463168: Bad Path Name 
    /// </remarks>
    public static class LDAPHelper
    {
        /// <summary>
        /// Initializes static members of the LDAPHelper class.
        /// </summary>
        static LDAPHelper()
        {
            RootPath = @"LDAP:\\";
        }
 
        /// <summary>
        /// Gets or sets the Path to the Active Directory server [Optional].
        /// </summary>
        /// <value>The Path to the Active Directory server.</value>
        /// <remarks>The value "LDAP:\\" will be used by default.</remarks>
        public static string RootPath { get; set; }
 
        /// <summary>
        /// Gets or sets the UserName to be used for authentication [Optional].
        /// </summary>
        /// <value>The UserName to be used for authentication [Optional].</value>
        /// <remarks>
        /// When the code is running on a remote machine, set this value to 
        /// impersonate a user of the domain that you're querying.
        /// </remarks>
        public static string UserName { get; set; }
 
        /// <summary>
        /// Gets or sets the PassWord to be used for authentication [Optional].
        /// </summary>
        /// <value>The PassWord to be used for authentication [Optional].</value>
        /// <remarks>
        /// When the code is running on a remote machine, set this value to 
        /// impersonate a user of the domain that you're querying.
        /// </remarks>
        public static string Password { get; set; }
 
        /// <summary>
        /// Checks if a full object path is valid.
        /// </summary>
        /// <param name="fullObjectPath">The full object path.</param>
        /// <returns>True if the path is valid.</returns>
        /// <remarks>Impersonation is not possible here.</remarks>
        public static bool Exists(string fullObjectPath)
        {
            bool found = false;
            if (DirectoryEntry.Exists(fullObjectPath))
            {
                found = true;
            }
 
            return found;
        }
 
        /// <summary>
        /// Verifies if a User exists.
        /// </summary>
        /// <param name="userName">The UserName to verify.</param>
        /// <returns>True if the UserName exists in Active Directory, false if not.</returns>
        public static bool UserExists(string userName)
        {
            using (DirectorySearcher searcher = GetDirectorySearcher())
            {
                searcher.Filter = "(&(ObjectClass=user)(sAMAccountName=" + userName.Substring(userName.IndexOf('\\') + 1) + "))";
 
                // for performance reasons only request needed properties
                searcher.PropertiesToLoad.AddRange(new string[] { "sAMAccountName" });
 
                SearchResult result = searcher.FindOne();
 
                return result != null;
            }
        }
 
        /// <summary>
        /// Verifies if a Group exists.
        /// </summary>
        /// <param name="groupName">The GroupName to verify.</param>
        /// <returns>True if the GroupName exists in Active Directory, false if not.</returns>
        public static bool GroupExists(string groupName)
        {
            using (DirectorySearcher searcher = GetDirectorySearcher())
            {
                searcher.Filter = "(&(objectClass=Group)(sAMAccountName=" + groupName + "))";
 
                // for performance reasons only request needed properties
                searcher.PropertiesToLoad.AddRange(new string[] { "sAMAccountName" });
 
                SearchResult result = searcher.FindOne();
 
                return result != null;
            }
        }
 
        /// <summary>
        /// Returns all stored UserIds.
        /// </summary>
        /// <returns>A list of all UserIds.</returns>
        public static List<string> GetAllUserNames()
        {
            DirectoryEntry de = GetDirectoryEntry();
            List<string> result = new List<string>();
            using (DirectorySearcher srch = new DirectorySearcher(de, "(objectClass=user)"))
            {
                SearchResultCollection results = srch.FindAll();
 
                foreach (SearchResult item in results)
                {
                    result.Add(item.Properties["sAMAccountName"][0].ToString());
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Returns all stored GroupNames.
        /// </summary>
        /// <returns>A list of all GroupNames.</returns>
        public static List<string> GetAllGroupNames()
        {
            DirectoryEntry de = GetDirectoryEntry();
            List<string> result = new List<string>();
            using (DirectorySearcher srch = new DirectorySearcher(de, "(objectClass=Group)"))
            {
                SearchResultCollection results = srch.FindAll();
 
                foreach (SearchResult item in results)
                {
                    result.Add(item.Properties["sAMAccountName"][0].ToString());
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Returns all UserIds in a Group.
        /// </summary>
        /// <param name="groupName">The Group.</param>
        /// <returns>A list of all UserIds in the Group.</returns>
        public static List<string> GetAllGroupMembers(string groupName)
        {
            List<string> result = new List<string>();
            DirectorySearcher searcher = GetDirectorySearcher();
            searcher.Filter = "(CN=" + groupName + ")";
            SearchResultCollection groups = searcher.FindAll();
            foreach (SearchResult group in groups)
            {
                ResultPropertyCollection props = group.Properties;
                foreach (object member in props["member"])
                {
                    DirectoryEntry memberEntry = GetDirectoryEntry();
                    memberEntry.Path = RootPath + @"/" + member;
                    PropertyCollection userProps = memberEntry.Properties;
                    object userName = userProps["sAMAccountName"].Value;
                    if (null != userName)
                    {
                        result.Add(userName.ToString());
                    }
                }
            }
 
            return result;
        }
 
        /// <summary>
        /// Validates a UserId / Password combination.
        /// </summary>
        /// <param name="userName">The userid.</param>
        /// <param name="password">The password.</param>
        /// <returns>True if the user can be authenticated, False if not.</returns>
        public static bool IsValidUser(string userName, string password)
        {
            bool authenticated = false;
            try
            {
                DirectoryEntry entry = new DirectoryEntry(RootPath, userName, password);
 
                object nativeObject = entry.NativeObject;
                authenticated = true;
            }
            catch (Exception)
            {
                // not authenticated  
            }
 
            return authenticated;
        }
 
        /// <summary>
        /// Returns a properly configured DirectoryEntry.
        /// </summary>
        /// <returns>A properly configured DirectoryEntry.</returns>
        private static DirectoryEntry GetDirectoryEntry()
        {
            if (string.IsNullOrEmpty(UserName))
            {
                // No Impersonation
                return new DirectoryEntry(RootPath);
            }
            else
            {
                // Impersonation
                return new DirectoryEntry(RootPath, UserName, Password);
            }
        }
 
        /// <summary>
        /// Returns a properly configured DirectorySearcher.
        /// </summary>
        /// <returns>A properly configured DirectorySearcher.</returns>
        private static DirectorySearcher GetDirectorySearcher()
        {
            return new DirectorySearcher(GetDirectoryEntry());
        }
    }
}