July 21, 2026
Userland ALPC Enumeration: Dynamic & PPL-Aware Approach
Walking through ALPC from userland: avoiding hardcoded object indices, resolving query hangs, handling PPL boundries & verifying via…

By Talha Nazeef Ahmed
6 min read
Most tools that enumerate Windows ALPC ports silently skip the ones they can't open, which happen to be the most interesting. This is how I built a userland enumerator that doesn't, and the fun journey regarding the enumerator.
1. ALPC Introduction:
Windows enforces a concept of virtual memory which is basically that any process in userland can't interact with the other process's memory directly. This is useful as a security feature as if some process gets corrupt, it will not directly impact the memory or if there is some malicious process, it will not affect the other process directly. Previously, I used the word directly and this is because windows have to come up with some way to actually make processes interact. That is where comes IPC's called Inter Process Communications and ALPC is one of the form of an IPC. So, ALPC (Advanced Local Procedure Call) is a communication channel that works locally so its very fast and mediated by the kernel itself.
2. Native API:
Windows had documented the win API's for developers but for ALPC enumeration, which is highly undocumented, there was the need to use the native API's. Basically the API's are in the ntdll.dll and there are some public headers available that include native API calls but not by microsoft itself. The methodology I used was dynamic resolution: make a blueprint of the call and then at runtime ask windows to get the dll and within the dll the specific native call.
To get the address for any native API, we first need the address for the library so here we are using GetModuleHandleA function to pass the string of the library and it will give us the correct address for that library at runtime.
This is the function that will query the native API address at runtime from windows using GetProcAddress.
Now here we passed the string and address of the library to dynamically get the actual address of NtQuerySystemInformation call.
Now, we will cast the original resolved address into the blueprint we made earlier and with that we could use that call later in our code with precision.
3. Resolving ALPC object indices dynamically:
The strategy for the enumeration was simple, call the kernel for objects with query information and then filter down those results to get all ALPC objects. But there was the catch like how to know the object type index of ALPC ports. As, the index gets resolved by the boot loader and depends on its sequence so it will differ from computer to computer so we can't hardcode any index. To solve this problem, we should first make a throwaway ALPC, ask kernel to give the index for that port at runtime so we know for sure that the index is correct and we will use that index to filter down the objects to only ALPC objects.
Here we did the double call procedure to get the accurate length of the structure via return length.
4. Hang Issues:
We successfully have a vector containing only ALPC port objects, now what we need is to query their name and display the information. But that last step contains multiple hurdles, the first one is the hang issue. When we will use NtQueryObject call to resolve name, we could face the infinite hang as sometimes, the port object is in the wait state, this wait state is like a sleep state in which the server is ready and waiting for the incoming message. When we will use our call, kernel will stage our call behind the queue and till the server doesn't free itself from that wait state, our call will stay behind and sometimes it could stay there for infinite time till we kill our process. So, to mitigate this issue, we should use a worker thread that will query the name.
Here we create a worker thread, CreateThread expects a specific structure in the parameters but due to the functionality of the enumerator we had to first pass our context structure in the expected structure and then pass that container to CreateThread function in a loop on every ALPC entry.
5. Handling the hang with a bounded wait / timeout on the worker thread:
We are passing every entry in a loop to a worker thread to resolve the name, lets say if any entry faces the hang. Now to resolve name, we go through 3 processes: 1)Get the permission to duplicate the handle for that process 2)Duplicate the handle 3)Query name from kernel on that handle. Now when the hang occurs we don't know where the worker thread got the hang and from where our main thread should continue. To solve this we should ask the kernel itself through status codes to get the state of our worker thread and then perform functionality according to it.
6. PPL Protected and SeDebugPrivilege:
The setup is ready and execution now holds no flaws but upon execution we will face the limitations of userland boundaries. The first boundary was most of the ports didn't allow to let our process resolve their name. This was because our process runs with a lower privilege and when we ask OpenProcess with PROCESS_DUP_HANDLE permission, it denies our process and our worker thread ends at step 1 without getting the name. Its not a bug but a limitation. To resolve this, we first ask the kernel to give our process SeDebugPrivilege and when we get that privilege, we will not face the hard block by most processes. That is why running this tool as administrator is necessary. The second boundary is the PPL protection. Now, this protection is not a normal protection that could be bypassed using some privilege as it is enforced by the kernel itself and is signed by the process owners. To get around this mitigation, we don't call OpenProcess with duplication permission but ask the process to give our process the PROCESS_QUERY_LIMITED_INFORMATION. This permission is not a big ask and any process could allow this even the PPL protected ones. Once we get this permission we don't rely on process name but process path and with that we could get alot of idea about the process through the binary it is running. Also we could get the information regarding the type and signer of that process.
7. Use cases for the enumerator:
The main purpose of making this enumerator was to first poke around the userland boundaries. Also, the ALPC is one of the highly undocumented variable from Microsoft so it was fun getting around ways. Now there are some tools available that perform the same enumeration purposes but the main thing is that they are not userland but run from kernel mode. Also, it is a simple C++ program that could be integrated into any of the other tool to enhance the purpose and efficiency. For defensive purposes it could be used too like if there is a malicious living process and is using ALPC and we saw that it is very easy to create an ALPC object, so it could deny the AV its name or could spoof any name and also the path of its binary, but it can't spoof the signer and type. It will return 0, 0 while enforcing PPL restrictions and that is the sign of the anomaly.
So, below is the github link of the repository for anyone who wants to play around with it and contains verification logs. If you came this far, I hope you enjoyed this writeup, cheers.
GitHub - talha-nazeef-ahmed/ALPC-Enumerator: A Windows userland tool to enumerate and classify ALPC… A Windows userland tool to enumerate and classify ALPC ports, including PPL-protected processes. …