In this second part of our LDAP Firewall blog series, we will present our process of researching LDAP and reverse-engineering the Windows LDAP service. We hope that such write-ups will help other researchers to create their own defensive tools and to contribute code to our open-source repositories.
Be sure to check out the original blog post which introduces the tool and explains the basics of LDAP.
In Windows, the LDAP functionality is handled by 2 separate DLLs - Wldap32.dll, which contains the client-side code and is present on all Windows hosts, and ntdsai.dll, which contains the server-side code and is only available on machines with the LDAP feature installed (usually by adding the Active Directory Domain Services server role).
To provide functionality on the server side, ntdsai.dll is loaded into the LSASS process, which in turn listens on the relevant network ports (389 LDAP / 636 LDAPS).
The ntdsai.dll is loaded by the lsass.exe process (viewed in Process Explorer)
The lsass.exe process listening on port 389 (viewed in TCPView)
While the client-side code for LDAP is well documented, no such documentation is available for the server-side implementation, and we were unable to find any public research or reverse-engineering efforts of this code online.
Our virtual lab environment included a Windows 11 client and a Windows Server 2019 machine with the Domain Controller role (later we added a few more servers with different versions of Windows installed, as there are slight differences in the LDAP codebase between Windows Server editions).
On the client side, we used the Ldp tool to initiate LDAP requests towards our Domain Controller. Since LSASS is a protected process, we also had to set up kernel debugging and use WinDbg on the client machine, connecting remotely over a virtual serial port to the server being debugged.
On the server side, apart from setting up the remote debugging, we also used Ghidra to disassemble and reverse-engineer the ntdsai.dll module.
Heading into the LDAP server-side module research, we got two lucky breaks:
- The public Microsoft symbol server has debug symbols available for ntdsai.dll
- The LDAP server-side functionality is conveniently implemented as separate functions for each LDAP operation type (each a method of a class named LDAP_CONN)
When a request reaches the LDAP server, the LDAP_CONN::ProcessRequestEx method handles the request, and in turn, calls a function corresponding to the requested operation. When that operation-specific function gets called, it receives pointers to relevant data structures as function parameters. These include:
- LDAP_CONN - contains information about LDAP connection, including the network socket
- _THSTATE - contains information about the current token, including the user SID
- LDAPMsg - contains the LDAP operation parameters
ProcessRequestEx calling relevant operation functions with data structures pointers (view from Ghidra decompiler)
LDAP Message Structures
To better understand the different data structures, we used WinDbg and set breakpoints for when the different operation methods are called.
Let’s use SearchRequest as an example. With Ghidra’s disassembly view, we can see that the pointer to the LDAP Message structure (parameter #4) is located on the stack with an offset of 0x28.
Location on the stack of the LDAPMsg pointer (view in Ghidra)
We could theoretically follow the code path to figure out what each data structure contains, but sometimes it’s easier to just get your hands dirty. To do this, we use the LDP tool to send a Search operation request to our server.
Sending an LDAP Search operation with Ldp
Once we reach our breakpoint on the server, we can follow the pointer to view the actual LDAP Message data structure in memory. By looking at the memory display in WinDbg, we can see some recognizable patterns. As an example, at struct offset + 0x10 we see an integer value followed by a pointer, which suggests a string structure (the integer represents the size of the string, and the pointer points to the actual char array). If we display the string, we see it matches the DN element of our search request.
LDAPMsg pointer address (view in WinDbg)
LDAP Message structure (view in WinDbg)
Following this procedure, we sent a bunch of LDAP operations and mapped out the different LDAP Message structures, eventually having enough information to define all of the relevant structs needed for the LDAP Firewall. You can find all mapped-out LDAP structures in our repository.
Note: we only mapped the struct fields relevant to our research, and thus some of the fields remained unknown (there are the byte array variables named “dummy”).
LDAPSearchMessage struct (as defined in the LDAPFW code)
The LDAP Firewall Architecture
After performing the initial reverse engineering and getting a grasp on the protocol, we needed to decide how to implement a software tool that would allow us to control LDAP operations. Our considerations were:
- Using a native programming language (C++) without requiring the user to install extra dependencies
- Using function hooking instead of capturing the TCP traffic, as the latter is more complex, would require a device driver installation, and would not support encrypted traffic (LDAPS)
- Using an established hooking library (Microsoft Detours) that is mature, well-tested, and easy to use
- Using the JSON format (with the jsoncpp library) for the firewall configuration, as it is convenient to use by both human and machine
As the LDAP service functionality is performed in a process outside of our control, our easiest option is to directly hook the relevant functions in the ntdsai.dll module. We do this by injecting the LDAP Firewall DLL (ldapFW.dll) into the LSASS process (using the CreateRemoteThread technique), and then intercepting the LDAP function calls with the Microsoft Detours library.
Windows LDAP architecture (including the LDAP Firewall)
Once installed, the LDAP Firewall detours the different LDAP operation methods that are called from LDAP_CONN::ProcessRequestEx. When this happens, instead of directly calling the corresponding function, ProcessRequestEx hands the execution over to LDAP Firewall (to learn how the interception works, see the overview in the Detours wiki).
Once we have control, LDAP Firewall first parses the request (based on the structs we created earlier). Next, the request is compared to the configured firewall rules. This happens by iterating over the rules (loaded from the config.json file) until hitting a rule that matches the request parameters (or defaulting to an allow rule if no matches are found). If the rule specifies that the request should be allowed, the original LDAP operation function is called; otherwise, it is dropped, and the execution is handed back to ntdsai.dll. Finally, an event is logged in the Event Viewer (if applicable).
LDAP Firewall process
For any questions or feedback, we invite you to join our Zero Labs slack channel, and we would love to hear from you on how you use LDAP Firewall for research, threat hunting, or cyber defense purposes.