How to programatically get the Enforce Password History group policy setting?
Asked Answered
B

1

2

How can i programatically get the Enforce Password History group policy setting?

Research Effort

You can find the Group Policy option under:

**Computer Configuration\Windows Settings\Security Settings\Account Policies\Password Policy**

Enforce password history

This security setting determines the number of unique new passwords that have to be associated with a user account before an old password can be reused. The value must be between 0 and 24 passwords.

This policy enables administrators to enhance security by ensuring that old passwords are not reused continually.

Like all group policy options, it is stored in the registry. Unfortunately it is stored in an undocumented registry location:

HKEY_LOCAL_MACHINE\SAM\SAM\Domains\Account\F

In an undocumented binary blob format.

There is a WMI class RSOP_SecuritySettingBoolean, but without any context, it's just a name hanging out there - which i have no idea how to read through COM/native.

NetValidatePasswordPolicy

Windows does provide an NetValidatePasswordPolicy API that will allow it to validate your password for things like:

  • too many bad attempts
  • account lockout
  • lockout automatic reset
  • minimum password age
  • maximum password age
  • password reuse

and it will do all these things following the group policy in effect on the computer. And it all works great, except for password history.

The function requires you to pass a list of password hashes, e.g.:

  • $2a$14$mACnM5lzNigHMaf7O1py1OLCBgGL4tYUF0N/4rS9CwDsI7ytwL4D6
  • $2a$14$mACnM5lzNigHMaf7O1py1O3vlf6.BA8k8x3IoJ.Tq3IB/2e7g61Km
  • $2a$12$.TtQJ4Jr6isd4Hp.mVfZeuh6Gws4rOQ/vdBczhDx.19NFK0Y84Dle
  • $2a$12$Ro0CUfOqk6cXEKf3dyaM7OhSCvnwM9s4wIX9JeLapehKK5YdLxKcm

as well as the hash of the new candidate password, e.g.:

  • $2a$10$1JsBs47iuMNsV166PKV.u.56hlT5/tRe9V5t5FIdfA0axpDSQuNN

This will cause NetValidatePasswordPolicy to check if your current hash matches any of the existing hashes. Unfortunately, the entire concept of checking previous password hashes only works when you use a weak password algorithm (such as PBKDF2 that Windows uses).

Because i use a modern, expensive, salted, password hashing algorithm (BCrypt/SCrypt), a hash cannot be deterministically generated from a password alone - i can't simply check the old list. I would have to rehash the user's new password against every previously stored salt.

  • not only is this an expensive operation; potentially taking 12 seconds to check all 24 stored hashes
  • i don't know when to stop, because i don't know the Password History group policy value (e.g. i would be checking all 24 stored hashes, when i only need to check zero).

I have considered calling NetValidatePasswordPolicy with a dummy list of 24 stored password hashes, e.g.:

  • a
  • b
  • c
  • d
  • ...
  • v
  • w
  • x

The API will then tell me that it password has did not match any in the n history. But the API is also designed to return to you what you have to keep. It might then return:

  • $2a$10$1JsBs47iuMNsV166PKV.u.56hlT5/tRe9V5t5FIdfA0axpDSQuNN
  • a
  • b
  • c

And from that i might be able to infer that the password history length is four.

But i've not gotten there yet.

I'm three days into this, and losing patience.

  • why did Microsoft obfuscate the group policy?
  • why did Microsoft not allow people to read it?
  • why is it undocumented?
  • how do i get it all the same?
Barbabas answered 22/6, 2015 at 21:13 Comment(4)
FWIW, it probably wasn't intentionally obfuscated, it's just that storing a binary blob is simpler. You might be able to read this information using IGroupPolicyObject::OpenLocalMachineGPO but I'm not sure how that works.Clichy
@HarryJohnston Group Policy Object, neato! "Minimum supported client: Windows Vista" Bah!Barbabas
Vista is the minimum supported client for everything. :-) That doesn't always mean that the interface isn't actually available in XP or earlier, so it's worth double-checking. (But if it really isn't, you can always use the binary blob format when you're running on XP. It's not like XP is ever going to change!)Clichy
@HarryJohnston I definitely thought about reading it; but users do not have read access to the registry keyBarbabas
B
1

Turns out that using RSOP_SecuritySettingBoolean (resultant set of policy) is a bad idea; as it only works on domain-joined machines.

Querying Active Directory likewise only works for computers joined to a domain; and on works for users who have the ability to query the domain controller (which is something that can be un-granted).

The real solution is to use NetUserModalsGet, which can return you structures like:

struct USER_MODALS_INFO_0
{
    DWORD usrmod0_min_passwd_len;
    DWORD usrmod0_max_passwd_age;
    DWORD usrmod0_min_passwd_age
    DWORD usrmod0_force_logoff; 
    DWORD usrmod0_password_hist_len; //Specifies the length of password history maintained. 
          //A new password cannot match any of the previous usrmod0_password_hist_len passwords. 
          //Valid values for this element are zero through DEF_MAX_PWHIST.
}

and

struct USER_MODALS_INFO_3 
{
   DWORD usrmod3_lockout_duration;
   DWORD usrmod3_lockout_observation_window;
   DWORD usrmod3_lockout_threshold;
}

The sample code would be:

Int32 GetPasswordHistoryLength()
{
   PUSER_MODALS_INFO_0 info0;

   NET_API_STATUS res = NetUserModalsGet(nil, 0,  out info0);

   if (res <> NERR_Success)
      RaiseWin32Error(res);
   try
   {
      return info0.usrmod0_password_hist_len;
   }
   finally
   {
      NetApiBufferFree(info0);
   }
}

The documentation says the max value is DEF_MAX_PWHIST, which is defined in Lmaccess.h as:

//
// user modals related defaults
//

#define MAX_PASSWD_LEN      PWLEN
#define DEF_MIN_PWLEN       6
#define DEF_PWUNIQUENESS    5
#define DEF_MAX_PWHIST      8

#define DEF_MAX_PWAGE       TIMEQ_FOREVER               // forever
#define DEF_MIN_PWAGE       (unsigned long) 0L          // 0 days
#define DEF_FORCE_LOGOFF    (unsigned long) 0xffffffff  // never
#define DEF_MAX_BADPW       0                           // no limit
#define ONE_DAY             (unsigned long) 01*24*3600  // 01 day

But that's not true. The policy allows a user to set a maximum of 24.

Note: Any code released into public domain. No attribution required.

Barbabas answered 23/6, 2015 at 14:28 Comment(1)
I would guess that eight was the maximum for LAN Manager, i.e., if you are setting the value rather than reading it, it is the largest number you can safely use without first figuring out which OS you're talking to.Clichy

© 2022 - 2024 — McMap. All rights reserved.