How to programmatically check the "Password must meet complexity requirements" group policy setting?
Asked Answered
E

1

16

Window has five group policy settings related to password security:

  • Enforce password history
  • Maximum password age
  • Minimum password age
  • Minimum password length
  • Password must meet complexity requirements
  • Store passwords using reversible encryption

enter image description here

I know how to use NetUserModalsGet to read most of these items. But it doesn't support checking if password complexity requirement is enabled:

  • Enforce password history: usrmod0_password_hist_len
  • Maximum password age: usrmod0_max_passwd_age
  • Minimum password age: usrmod0_min_passwd_age
  • Minimum password length: usrmod0_min_passwd_len
  • Password must meet complexity requirements: ?
  • Store passwords using reversible encryption:

I also know that WMI's RSOP ("Resultant set of policy") is unsuitable, as it only works on a domain. And i'm certainly not going to crawling through an undocumented binary blob (i.e. i want the supported way).

Note: I don't care about the "Store passwords using reversible encryption" group policy setting.

Bonus

You can also use the NetUserModalsGet API to retrieve the Account Lockout Policy settings:

  • Account lockout duration: usrmod3_lockout_duration
  • Account lockout threshold: usrmod3_lockout_threshold
  • Reset account lockout counter after: usrmod3_lockout_observation_window

Thus rounding out all the password related group policy options; except for "must meet complexity requirements".

For completeness, assume a non-domain joined machine (i.e. no AD server to query, no RSOP to query, etc).

Environment answered 16/7, 2015 at 21:27 Comment(2)
Why does the other solution given in the linked stackoverflow post ,dumping to an INI via secedit and parsing the INI, not work for you? It does not require a domain and seems much nicer than binary blob parsing.Omnifarious
@ssnobody Even with the problems of having to write a file some place, and how it's not possible to wait for secedit to launch, or know when it'sdone, and how it requires administrative privileges when the user's won't have it, or how it might be called a lot, it would still remain a horrible hack of an idea that i would never to admit to creating or ever let it see the light of day. I'm looking for the correct way to do it.Environment
C
24

This is accessible using SAM (Security Account Manager) APIs.

This API (served by SAMLIB.DLL) is not directly documented (no header, no SDK), but the "protocol" to use it is documented here: [MS-SAMR]: Security Account Manager (SAM) Remote Protocol (Client-to-Server), you "just" have to remove the r in described SamrXXXX methods.

The ones in question here are SamQueryInformationDomain (and associated SamSetInformationDomain) which will get you a DOMAIN_PASSWORD_INFORMATION structure

typedef struct _DOMAIN_PASSWORD_INFORMATION {
   unsigned short MinPasswordLength;
   unsigned short PasswordHistoryLength;
   unsigned long PasswordProperties;
   OLD_LARGE_INTEGER MaxPasswordAge;
   OLD_LARGE_INTEGER MinPasswordAge;
 } DOMAIN_PASSWORD_INFORMATION,

The PasswordProperties member can contain DOMAIN_PASSWORD_COMPLEX flag:

DOMAIN_PASSWORD_COMPLEX
0x00000001
The server enforces password complexity policy. See section 3.1.1.7.2 for details of the password policy. 

I've provided some C# samples to check this.

First one dumps the policy for all domains served by the current machine's SAM server:

        using (SamServer server = new SamServer(null, SamServer.SERVER_ACCESS_MASK.SAM_SERVER_ENUMERATE_DOMAINS | SamServer.SERVER_ACCESS_MASK.SAM_SERVER_LOOKUP_DOMAIN))
        {
            foreach (string domain in server.EnumerateDomains())
            {
                Console.WriteLine("domain: " + domain);

                var sid = server.GetDomainSid(domain);
                Console.WriteLine(" sid: " + sid);

                var pi = server.GetDomainPasswordInformation(sid);
                Console.WriteLine(" MaxPasswordAge: " + pi.MaxPasswordAge);
                Console.WriteLine(" MinPasswordAge: " + pi.MinPasswordAge);
                Console.WriteLine(" MinPasswordLength: " + pi.MinPasswordLength);
                Console.WriteLine(" PasswordHistoryLength: " + pi.PasswordHistoryLength);
                Console.WriteLine(" PasswordProperties: " + pi.PasswordProperties);
            }
        }

Second one reads and updates the policy for the current machine's domain:

        using (SamServer server = new SamServer(null, SamServer.SERVER_ACCESS_MASK.SAM_SERVER_ALL_ACCESS))
        {
            var sid = server.GetDomainSid(Environment.MachineName);
            var pi = server.GetDomainPasswordInformation(sid);

            // remove password complexity
            pi.PasswordProperties &= ~SamServer.PASSWORD_PROPERTIES.DOMAIN_PASSWORD_COMPLEX;
            server.SetDomainPasswordInformation(sid, pi);
        }

This is the SamServer utility:

public sealed class SamServer : IDisposable
{
    private IntPtr _handle;

    public SamServer(string name, SERVER_ACCESS_MASK access)
    {
        Name = name;
        Check(SamConnect(new UNICODE_STRING(name), out _handle, access, IntPtr.Zero));
    }

    public string Name { get; }

    public void Dispose()
    {
        if (_handle != IntPtr.Zero)
        {
            SamCloseHandle(_handle);
            _handle = IntPtr.Zero;
        }
    }

    public void SetDomainPasswordInformation(SecurityIdentifier domainSid, DOMAIN_PASSWORD_INFORMATION passwordInformation)
    {
        if (domainSid == null)
            throw new ArgumentNullException(nameof(domainSid));

        var sid = new byte[domainSid.BinaryLength];
        domainSid.GetBinaryForm(sid, 0);

        Check(SamOpenDomain(_handle, DOMAIN_ACCESS_MASK.DOMAIN_WRITE_PASSWORD_PARAMS, sid, out IntPtr domain));
        IntPtr info = Marshal.AllocHGlobal(Marshal.SizeOf(passwordInformation));
        Marshal.StructureToPtr(passwordInformation, info, false);
        try
        {
            Check(SamSetInformationDomain(domain, DOMAIN_INFORMATION_CLASS.DomainPasswordInformation, info));
        }
        finally
        {
            Marshal.FreeHGlobal(info);
            SamCloseHandle(domain);
        }
    }

    public DOMAIN_PASSWORD_INFORMATION GetDomainPasswordInformation(SecurityIdentifier domainSid)
    {
        if (domainSid == null)
            throw new ArgumentNullException(nameof(domainSid));

        var sid = new byte[domainSid.BinaryLength];
        domainSid.GetBinaryForm(sid, 0);

        Check(SamOpenDomain(_handle, DOMAIN_ACCESS_MASK.DOMAIN_READ_PASSWORD_PARAMETERS, sid, out IntPtr domain));
        var info = IntPtr.Zero;
        try
        {
            Check(SamQueryInformationDomain(domain, DOMAIN_INFORMATION_CLASS.DomainPasswordInformation, out info));
            return (DOMAIN_PASSWORD_INFORMATION)Marshal.PtrToStructure(info, typeof(DOMAIN_PASSWORD_INFORMATION));
        }
        finally
        {
            SamFreeMemory(info);
            SamCloseHandle(domain);
        }
    }

    public SecurityIdentifier GetDomainSid(string domain)
    {
        if (domain == null)
            throw new ArgumentNullException(nameof(domain));

        Check(SamLookupDomainInSamServer(_handle, new UNICODE_STRING(domain), out IntPtr sid));
        return new SecurityIdentifier(sid);
    }

    public IEnumerable<string> EnumerateDomains()
    {
        int cookie = 0;
        while (true)
        {
            var status = SamEnumerateDomainsInSamServer(_handle, ref cookie, out IntPtr info, 1, out int count);
            if (status != NTSTATUS.STATUS_SUCCESS && status != NTSTATUS.STATUS_MORE_ENTRIES)
                Check(status);

            if (count == 0)
                break;

            var us = (UNICODE_STRING)Marshal.PtrToStructure(info + IntPtr.Size, typeof(UNICODE_STRING));
            SamFreeMemory(info);
            yield return us.ToString();
            us.Buffer = IntPtr.Zero; // we don't own this one
        }
    }

    private enum DOMAIN_INFORMATION_CLASS
    {
        DomainPasswordInformation = 1,
    }

    [Flags]
    public enum PASSWORD_PROPERTIES
    {
        DOMAIN_PASSWORD_COMPLEX = 0x00000001,
        DOMAIN_PASSWORD_NO_ANON_CHANGE = 0x00000002,
        DOMAIN_PASSWORD_NO_CLEAR_CHANGE = 0x00000004,
        DOMAIN_LOCKOUT_ADMINS = 0x00000008,
        DOMAIN_PASSWORD_STORE_CLEARTEXT = 0x00000010,
        DOMAIN_REFUSE_PASSWORD_CHANGE = 0x00000020,
    }

    [Flags]
    private enum DOMAIN_ACCESS_MASK
    {
        DOMAIN_READ_PASSWORD_PARAMETERS = 0x00000001,
        DOMAIN_WRITE_PASSWORD_PARAMS = 0x00000002,
        DOMAIN_READ_OTHER_PARAMETERS = 0x00000004,
        DOMAIN_WRITE_OTHER_PARAMETERS = 0x00000008,
        DOMAIN_CREATE_USER = 0x00000010,
        DOMAIN_CREATE_GROUP = 0x00000020,
        DOMAIN_CREATE_ALIAS = 0x00000040,
        DOMAIN_GET_ALIAS_MEMBERSHIP = 0x00000080,
        DOMAIN_LIST_ACCOUNTS = 0x00000100,
        DOMAIN_LOOKUP = 0x00000200,
        DOMAIN_ADMINISTER_SERVER = 0x00000400,
        DOMAIN_ALL_ACCESS = 0x000F07FF,
        DOMAIN_READ = 0x00020084,
        DOMAIN_WRITE = 0x0002047A,
        DOMAIN_EXECUTE = 0x00020301
    }

    [Flags]
    public enum SERVER_ACCESS_MASK
    {
        SAM_SERVER_CONNECT = 0x00000001,
        SAM_SERVER_SHUTDOWN = 0x00000002,
        SAM_SERVER_INITIALIZE = 0x00000004,
        SAM_SERVER_CREATE_DOMAIN = 0x00000008,
        SAM_SERVER_ENUMERATE_DOMAINS = 0x00000010,
        SAM_SERVER_LOOKUP_DOMAIN = 0x00000020,
        SAM_SERVER_ALL_ACCESS = 0x000F003F,
        SAM_SERVER_READ = 0x00020010,
        SAM_SERVER_WRITE = 0x0002000E,
        SAM_SERVER_EXECUTE = 0x00020021
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct DOMAIN_PASSWORD_INFORMATION
    {
        public short MinPasswordLength;
        public short PasswordHistoryLength;
        public PASSWORD_PROPERTIES PasswordProperties;
        private long _maxPasswordAge;
        private long _minPasswordAge;

        public TimeSpan MaxPasswordAge
        {
            get
            {
                return -new TimeSpan(_maxPasswordAge);
            }
            set
            {
                _maxPasswordAge = value.Ticks;
            }
        }

        public TimeSpan MinPasswordAge
        {
            get
            {
                return -new TimeSpan(_minPasswordAge);
            }
            set
            {
                _minPasswordAge = value.Ticks;
            }
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    private class UNICODE_STRING : IDisposable
    {
        public ushort Length;
        public ushort MaximumLength;
        public IntPtr Buffer;

        public UNICODE_STRING()
            : this(null)
        {
        }

        public UNICODE_STRING(string s)
        {
            if (s != null)
            {
                Length = (ushort)(s.Length * 2);
                MaximumLength = (ushort)(Length + 2);
                Buffer = Marshal.StringToHGlobalUni(s);
            }
        }

        public override string ToString() => Buffer != IntPtr.Zero ? Marshal.PtrToStringUni(Buffer) : null;

        protected virtual void Dispose(bool disposing)
        {
            if (Buffer != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(Buffer);
                Buffer = IntPtr.Zero;
            }
        }

        ~UNICODE_STRING() => Dispose(false);

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
    }

    private static void Check(NTSTATUS err)
    {
        if (err == NTSTATUS.STATUS_SUCCESS)
            return;

        throw new Win32Exception("Error " + err + " (0x" + ((int)err).ToString("X8") + ")");
    }

    private enum NTSTATUS
    {
        STATUS_SUCCESS = 0x0,
        STATUS_MORE_ENTRIES = 0x105,
        STATUS_INVALID_HANDLE = unchecked((int)0xC0000008),
        STATUS_INVALID_PARAMETER = unchecked((int)0xC000000D),
        STATUS_ACCESS_DENIED = unchecked((int)0xC0000022),
        STATUS_OBJECT_TYPE_MISMATCH = unchecked((int)0xC0000024),
        STATUS_NO_SUCH_DOMAIN = unchecked((int)0xC00000DF),
    }

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamConnect(UNICODE_STRING ServerName, out IntPtr ServerHandle, SERVER_ACCESS_MASK DesiredAccess, IntPtr ObjectAttributes);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamCloseHandle(IntPtr ServerHandle);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamFreeMemory(IntPtr Handle);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamOpenDomain(IntPtr ServerHandle, DOMAIN_ACCESS_MASK DesiredAccess, byte[] DomainId, out IntPtr DomainHandle);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamLookupDomainInSamServer(IntPtr ServerHandle, UNICODE_STRING name, out IntPtr DomainId);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamQueryInformationDomain(IntPtr DomainHandle, DOMAIN_INFORMATION_CLASS DomainInformationClass, out IntPtr Buffer);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamSetInformationDomain(IntPtr DomainHandle, DOMAIN_INFORMATION_CLASS DomainInformationClass, IntPtr Buffer);

    [DllImport("samlib.dll", CharSet = CharSet.Unicode)]
    private static extern NTSTATUS SamEnumerateDomainsInSamServer(IntPtr ServerHandle, ref int EnumerationContext, out IntPtr EnumerationBuffer, int PreferedMaximumLength, out int CountReturned);
}
Cavort answered 31/7, 2015 at 14:10 Comment(11)
Does this code actually work for you? It was a good starting point, but for me, it crashed on the line Check(SamConnect(new UNICODE_STRING(name), out _handle, access, IntPtr.Zero));. By comparison with e.g., github.com/MichaelGrafnetter/DSInternals, I determined that the name parameter was meant to be passed by ref.Ropy
In addition, I found that the structure returned by SamEnumerateDomainsInSamServer didn't seem to be right and it was crashing on the Marshal.PtrToStructure line. Also, the count parameter for SamEnumerateDomainsInSamServer is being ignored, and you should probably be iterating the enumerated domains. My edit had made those changes and worked for me. What happened for you?Ropy
Yes it works when compiled with x64 and fails for x86, which is a sign of an issue in p/invoke declaration. I've updated my answer and tested it for both architectures. count is ignored because we ask for one, so we don't get more than one, but anyway when you free info, it frees the whole thing, including UNICODE_STRINGs inside. There were indeed some leaks with UNICODE_STRING buffers are fixed as well now. Remember none of this is officially documented. The PDF describes a protocol, not an API. In fact what SamEnumerateDomainsInSamServer returns dos not match 100% what's in the PDF.Cavort
That makes sense, my app is compiled for x86. I'll give your updated version a try laterRopy
Thanks @Simon Mourier for explaining the concept of SAM APIs. There is no helpful documentation to explain how the SAM functions can be used in Visual Code. I am trying to write a small C program to use SamQueryInformationDomain and other SAM functions. I have googled and it seems they are defined in ntsam.h? But when I tried to include it in Visual Code, it would not get recognised. Why?Insolence
@Insolence - Microsoft doesn't provide any official .h, so you'll have to copy the necessary header files that you found on the internet over to your project.Cavort
@SimonMourier Not able to find header files .h for these APIs SamQueryInformationDomainInvite
@Invite - read the text: "This API (served by SAMLIB.DLL) is not directly documented (no header, no SDK)"Cavort
@SimonMourier any c++example code snippet for using SamQueryInformationDomain would be great helpInvite
@Invite - sure. You can do it, or ask another question.Cavort
@SimonMourier Posted new question hereInvite

© 2022 - 2024 — McMap. All rights reserved.