Changing Local Administrator Passwords — ...

Changing Local Administrator Passwords — A Delphi Implementation

Sep 25, 2025

image

Changing Local Administrator Passwords — A Delphi Implementation

Problem Definition

When setting up computers with Windows NT-based operating systems, a local administrator account is created with unlimited privileges on that machine. For computers intended to be part of a domain, IT staff typically set the same password for this account across multiple machines — and unfortunately, these passwords are often not very complex.

If someone gains physical access to a workstation, the administrator password can be easily compromised, leading to serious security implications. The network administrator’s job is to establish sufficiently complex passwords for these accounts and change them periodically.

With dozens of computers in a domain, this can be time-consuming. With hundreds of computers, especially when they’re geographically distributed, automation becomes essential.

Let’s define what our utility program should accomplish — essentially creating a simple workflow:

  • Retrieve a list of computer names in the domain (possibly filtered by specific criteria)

  • Connect to each computer in the list and change the password

For better usability, we need proper error handling and should log the results to a database.

Understanding ADSI

After determining what needs to be done, the question becomes how to implement it. Initially, I considered using WMI technology, but after brief research, decided to use ADSI instead.

ADSI stands for Active Directory Service Interfaces. Microsoft created a set of COM interfaces designed to access various directory services.

A directory service is a distributed system that provides methods for locating and utilizing various types of network resources.

The ADSI object model is based on COM objects. Client programs manage objects through interfaces. The following table lists the fundamental ADSI elements:

Interface Description IADs Used for object identification. As the fundamental interface supported by all ADSI objects, it provides access to object metadata, including the object’s description in the Active Directory schema. IADsContainer Used for retrieving and managing objects. All ADSI container objects require this interface to access and manipulate objects within the container. IADsPropertyList Used for working with object properties.

Complex ADSI objects may support additional interfaces.

Initial VBScript Approach

The first implementation was done in VBScript, which makes sense — you can simply visit Microsoft’s website and download ready-made scripts, then modify them slightly for your needs. Additionally, VB code is very concise and easy to understand.

Here’s an example of creating a computer list from a domain located in a specific organizational unit in Active Directory:

Set objDictionary = CreateObject("Scripting.Dictionary")
strDomain = "LDAP://ou=Test, ou=Mine, dc=mydomain, dc=com"
Set objDomain = GetObject(strDomain)
objDomain.Filter = Array("computer")
i = 0
For Each objComputer In objDomain
    objDictionary.Add i, Mid(objComputer.Name,4)
    i = i + 1
Next

To access the directory namespace, you need to connect to the appropriate ADSI object:

Set objDomain = GetObject(strDomain)

strDomain is the binding string. The first part determines which directory service we're accessing:

Service Description “LDAP://” Directory service based on LDAP protocol (including Active Directory) “WinNT://” Directory service in Windows NT 4.0 networks or Windows XP/2000 workstations

The second part of the binding string determines the object’s location in the directory.

LDAP Binding Examples:

WinNT Binding Examples:

  • WinNT://

  • WinNT:///

  • WinNT:///,

We set a filter to select computer objects:

objDomain.Filter = Array("computer")

Then iterate through the collection elements.

The main drawback of this implementation (in my opinion) is slow performance. Processing ~150 workstations and changing their passwords took about an hour.

The primary delays occur during binding operations, especially with large timeouts when attempting to bind to powered-off or non-existent computers (or when access is denied). The solution is implementing multithreading, which led me to abandon VBScript.

Delphi Implementation

The task was implemented in Delphi 6 SP2. During development, I discovered that the necessary functions weren’t described in the standard library. I’ll provide descriptions of all required functions throughout this article.

First, let’s establish a connection to AD using the ADsGetObject function:

HRESULT ADsGetObject(
    LPWSTR lpszPathName,
    REFIID riid,
    VOID** ppObject
);

Parameters:

  • lpszPathName - binding string

  • riid - interface identifier

  • ppObject - pointer to interface pointer returned by function

This function is equivalent to VB’s GetObject function. It takes a binding string and returns a pointer to the requested interface. Binding occurs in the security context of the calling thread, using ADS_SECURE_AUTHENTICATION options. If you need to specify a particular user, use ADsOpenObject instead.

Here’s an example using ADsGetObject to bind with AD:

interface
Uses ActiveDs_TLB;
function ADsGetObject(lpszPathName: WideString; const riid: TGUID; 
    out ppObject: Pointer): HRESULT; stdcall;implementation
function ADsGetObject; external 'activeds.dll';procedure TForm1.Test;
var 
    hr: HResult;
    objDomain: Pointer;
begin
    hr := ADsGetObject('LDAP://ou=test, ou=mine, dc=mydomain, dc=com', 
        IID_IADsContainer, objDomain);
    if Failed(hr) then Exit;
end;

To compile this example, you need to import the Activeds.tlb type library.

Note: When working with ADsGetObject, I occasionally encountered situations where attempting to read object properties resulted in the error "The directory property cannot be found in cache." Unfortunately, this was quite some time ago and I can't reproduce the situation. However, the error existed and was resolved by using ADsOpenObject:

function ADsOpenObject(lpszPathName: WideString; lpszUserName: WideString; 
    lpszPassword: WideString; dwReserved: DWORD; const riid: TGUID; 
    out ppObject: Pointer): HRESULT; stdcall;
function ADsOpenObject; external 'activeds.dll';

In these examples, we’re trying to get a reference to the IID_IADsContainer interface, which is used to obtain collections of ADSI objects.

After obtaining a reference to the container, we need to iterate through its objects and read their names. This requires two additional functions: AdsBuildEnumerator and ADsEnumerateNext.

AdsBuildEnumerator creates an Enumerator object for a specific ADSI container object:

function ADsBuildEnumerator(pADsContainerL: IADsContainer; 
    ppEnumVariant: PIEnumVARIANT): HRESULT; stdcall;

ADsEnumerateNext allows moving the pointer through collection elements:

function ADsEnumerateNext(pEnumVariant: IEnumVARIANT; cElements: ULONG; 
    pvar: POleVariant; pcElementsFetched: PULONG): HRESULT; stdcall;

Here’s a complete example demonstrating how to get a list of domain computers from AD:

procedure TForm1.Button1Click(Sender: TObject);
var 
    objDomain: Pointer;
    objChild: Pointer;
    hr: HResult;
    s: String;
    iArr: OleVariant;
    iEnum: IEnumVARIANT;
    iFetch: ULONG;
begin
    ListBox1.Clear;
    hr := ADsGetObject('LDAP://ou=test, ou=mine, dc=domain, dc=com', 
        IID_IADsContainer, objDomain);
    if Failed(hr) then Exit;
    
    hr := ADsBuildEnumerator(IADsContainer(objDomain), @iEnum);
    if Failed(hr) then Exit;
    
    hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
    while (S_OK = hr) and (1 = iFetch) do
    begin
        hr := IDispatch(iArr).QueryInterface(IADs, objChild);
        if Failed(hr) then Exit;
        
        if AnsiLowerCase(IAds(objChild).Class_) = 'computer' then
        begin
            s := IAds(objChild).Name;
            System.Delete(s, 1, 3);
            ListBox1.Items.Add(s);
        end;
        
        iArr := null;
        hr := ADsEnumerateNext(iEnum, 1, @iArr, @iFetch);
    end;
end;

Changing the Password

Now for the actual password change. We form a binding string to access the “Administrator” object. The object class is “user” and it’s located on workstation “Computer01”:

iPath := 'WinNT://' + NameWs + '/Administrator,user';

And the implementation:

procedure ChangePassword;
var 
    objUser: Pointer;
    hr: HResult;
    iPath: String;
begin
    iPath := 'WinNT://Computer01/Administrator,user';
    hr := ADsGetObject(iPath, IID_IADsUser, objUser);
    if hr <> S_OK then Exit;
    
    IADsUser(objUser).SetPassword('newpassword123');
end;

Error Handling

When ADSI function calls fail, they return error codes in the standard COM object manner. Error codes fall into four groups:

  • Universal COM error codes

  • Universal ADSI error codes

  • Win32 error codes for ADSI

  • LDAP error codes for ADSI

Additionally, some interfaces provide additional error information that can be obtained using ADsGetLastError:

function ADsGetLastError(lpError: LPDWORD; lpErrorBuf: LPWSTR; 
    dwErrorBufLen: DWORD; lpNameBuf: LPWSTR; 
    dwNameBufLen: DWORD): HRESULT; stdcall;

This comprehensive approach allows for robust password management across domain computers, providing both the functionality needed and proper error handling for enterprise environments.

Conclusion

This Delphi implementation provides a much faster and more flexible solution than the initial VBScript approach, while maintaining the power of ADSI for Active Directory operations. The multithreading capabilities of Delphi make it particularly suitable for managing large numbers of computers across distributed networks.

References

  • Microsoft Developer Network (MSDN)

  • “Windows Administration with WMI and WMIC”

  • Various security documentation and Active Directory resources

Enjoy this post?

Buy DelphiFan Forum a coffee

More from DelphiFan Forum