blank Skip to main content

3 Ways to Get a COM Server Process ID

To create new products, it’s important to have standardized and reliable components. For example, home builders build houses using bricks of a standard size. In programming, we have development standards. One of these standards is the Component Object Model (COM), created by Microsoft.

COM effectively solves the problem of code reuse, but the implementation of some of its functions isn’t clear. For example, when developing data leak protection systems, you may need to get a COM server process ID (PID) to check how processes handle sensitive data. The documentation from Microsoft doesn’t provide an explicit way to do this, so we decided to share our experience. In this article, we explain how COM servers work and show three different ways to get a COM server’s PID.

This guide will be useful both for those wishing to learn more about the infrastructure of COM servers as well as for specialists facing issues getting PIDs in practice.

What is COM?

Code reusability is a priority in software development. Usually, software is developed using a specific programming language and can only be used effectively if other components are developed using the same language. COM provides developers with constituent modules, which must work in a variety of environments. 

As a platform-independent, object-oriented technology standard, the Component Object Model enables the development of binary components. Subsequently, these components can be used both locally and in a distributed network environment. The main purpose of COM is to provide means by which objects and components written in various programming languages can interoperate with no changes to the executable code. According to Microsoft’s documentation, objects that provide services to clients are called COM servers. Services are represented as implementations of COM interfaces, which can be called by any client that can get a pointer to one of the interfaces on the server object. 

The ability to interact transparently with objects is built in to COM by design. An object can run in the same process, on the same machine, or on another machine, but there will always be a single programming model for it, regardless of its type. This feature is called location transparency.

Location transparency means that for COM users, it doesn’t matter where the COM server is located. For a user, it all works the same. But in case the COM server is in another process and the user needs its PID, location transparency by design doesn’t allow the user to get it. Let’s look at how we can deal with this problem. 

Related services

Remote Access and Management Solutions

Why do you need to get a process ID?

Getting a process ID is a rather specific task. Nevertheless, there are some cases where it’s necessary. For example, a PID is needed for system monitoring tools, which should provide information about processes running within the system, including data on processes with parent–child relationships. COM servers don’t provide this information, although it would be useful to know which process initiated the server’s creation. Moreover, there’s no easy or obvious way to hook such processes, as there are no parent–child relationships between them and the original process. The way out is to track the creation of COM servers using the methods we’ll introduce below. 

In addition, obtaining a PID is particularly important for all kinds of Data Loss Prevention (DLP) systems. In a DLP system, it’s necessary to control how processes handle sensitive documentation. Usually, this is done by hooking the process. Sometimes, processes generate child processes or COM processes, and they also need to be controlled to prevent data leakage. To hook a COM process, you need to know its PID. This is exactly the problem we encountered while working on one of our projects. 

In one of our projects, we used an application that uses WinAPI hooks for functions like ReadFile, WriteFile, CreateFile and so on to handle encrypted files. Files are stored encrypted on the disk, and when the app interacts with a file, it decrypts or encrypts the contents on the fly:



Figure 1. File encryption and decryption process

The application doesn’t know that the file is encrypted and uses a simple API to interact with it. Sometimes, the app launches child processes that also work with the encrypted files. This situation can be handled by hooking the CreateProcess or CreateProcessAsUser functions and retrieving the PID of child processes from the return value. The PID is enough to hook the process. Here’s how it works:



Figure 2. Process of hooking using a PID

Sometimes, an application launches COM processes that also work with encrypted files. There’s no easy or obvious way to hook such processes, as there are no parent–child relationships between them and the original process. Let’s see how to get a COM server process ID from the very basics.

Read also:
3 Effective DLL Injection Techniques for Setting API Hooks

Basic principles of working with COM

Before you get PIDs, let’s quickly review some basics of COM technology to better understand what the process ID of the COM server is and how COM servers work from the inside.

How to get a COM interface

In order to get the PID of a COM server, we must first get the COM interface. There are three main functions to get COM interfaces:

In general, the process of getting a COM interface looks like this:



Figure 3. Process for getting a COM interface

  1. Client calls the CoCreateInstanceEx function causing COM to delegate the activation request to its local Service Control Manager (SCM).
  2. The local SCM looks into the local registry under [HKCRCLSIDCLSIDinprocServer32] for an in-process server that implements this COM class. If it finds an in-process server, it returns the in-process server’s path to COM. COM then loads the in-process server.
  3. If the SCM can’t find an in-process server that implements the requested COM class, it looks into the SCM cache to see whether the requested COM class’s class factory has been registered by an already running local server.
  4. If the SCM can’t find a server in the cache, it looks for a local server path under [HKCRCLSIDCLSIDLocalServer32] and spawns the local server, which registers the server’s supported class factories.
  5. If the SCM can’t find a local server, it looks for the RemoteServerName entry under [HKCRAppIDAppIDRemoteServerName]. The local SCM then contacts the remote SCM and asks the remote SCM to handle the activation request. The remote SCM will, following steps 3 through 5, try to spawn a remote process that supports the requested factory.

COM operates with objects through interface pointers. 


However, there’s no way to know if the created object is in the same process or in another, and there’s also no API to get the PID of the process that hosts the COM object. To better understand how to get a COM server process ID, let’s explore how COM communicates with remote objects.  

How COM servers communicate with remote objects

Clients access remote COM objects through special proxies that the COM runtime provides to achieve location transparency. Microsoft extends and utilizes its existing Remote Procedure Call (RPC) technology to allow remote objects to communicate. Proxies marshal all parameters and interfaces to the remote stubs that unmarshal them and call a remote object’s methods. You can read more about inter-object communication in the Microsoft documentation.

To get a PID, we need to understand how proxies marshal COM interfaces and what fields the marshaled representation has. COM uses a special OBJREF structure to represent a marshaled interface. Here’s an example of such an interface:

typedef struct tagOBJREF  {
    unsigned long signature;         
    unsigned long flags;             
    GUID        iid;               
    union      {
        struct          {
            STDOBJREF     std;     
            DUALSTRINGARRAY saResAddr;
        } u_standard;  
        struct          {
            STDOBJREF     std;     
            CLSID         clsid;   
            DUALSTRINGARRAY saResAddr;
        } u_handler;          
        struct          {
            CLSID         clsid;   
            unsigned long   cbExtension;
            unsigned long   size;    
            byte *pData;
        } u_custom;
        struct          {
            STDOBJREF     std;
            unsigned long   Signature1;
            DUALSTRINGARRAY saResAddr;
            unsigned long   nElms;
            unsigned long   Signature2;
            DATAELEMENT   ElmArray;
        } u_extended; 
    } u_objref;  

There are four different formats for an OBJREF, which are specified by definitions of the u_objref field. Also, the STDOBJREF structure is an important part of the marshaled interface representation. Here is an example of this structure:

typedef unsigned __int64 OXID;
typedef unsigned __int64 OID;
typedef GUID           IPID;
typedef struct tagSTDOBJREF  {
    unsigned long flags;
    unsigned long cPublicRefs;
    OXID oxid;
    OID  oid;
    IPID ipid;

STDOBJREF contains fields important for marshaling and finding the remote object and interface: object exporter identifier (OXID), object identifier (OID), and interface pointer identifier (IPID). We will use these fields in two of the three methods for getting a PID described below, so let’s take a closer look at them:

  • OXID. This is a 64-bit value assigned to an apartment that exports an interface or marshals an interface for a remote client. It is unique within a given machine.
  • OID. This is a 64-bit value assigned to a stub manager, which can be described as a fake client on the object side. It is unique within a particular apartment.
  • IPID. This is a 128-bit value which represents a unique interface pointer ID to identify an interface stub.

Now, let’s go directly to the methods of obtaining PIDs. To be concise, we have omitted the error handling step in our tutorials.

Read also:
Hooking COM Objects: Intercepting Calls to COM Interfaces

Getting a COM server PID

There are three basic methods of getting the PID of a COM server:


Let’s take a closer look at each of them.

Get a COM server PID from the IPID

Note: Error handling is omitted for brevity.

The first method is getting the PID from the IPID. To illustrate this method, we’ll write a small app that instructs COM to launch a separate process that hosts a COM object:

#include <atlbase.h>
#include <windows.h>
int main()
    ⁄⁄ Initializes the COM library in the current thread and identifies the concurrency model as a single-thread apartment.
    CComPtr<iunknown> excelInterface;
        OLESTR("Excel.Application"), ⁄⁄ Launch Microsoft Excel app
        NULL,                      ⁄⁄ No aggregation
        CLSCTX_LOCAL_SERVER        ⁄⁄ Create an object in another process

The Excel application has launched. Using Process Explorer from Sysinternals tools, we can see that it was launched by svchost.exe, which tells us that it was launched by the COM runtime:



Figure 4. Excel application launched by the COM runtime

But having only the IUnknown interface, we can’t say if an object is local or remote. Let’s marshal the interface to see the fields we need:

#include <atlbase.h>
#include <windows.h>
int main()
    CComPtr<istream> marshalStream;
    CreateStreamOnHGlobal(NULL, TRUE, &marshalStream);
        marshalStream,   ⁄⁄ Where to write the marshaled interface
        IID_IUnknown,  ⁄⁄ ID of the marshaled interface
        excelInterface,  ⁄⁄ The interface to be marshaled
        MSHCTX_INPROC,   ⁄⁄ Unmarshaling will be done in the same process
        NULL,          ⁄⁄ Reserved and must be NULL
        MSHLFLAGS_NORMAL ⁄⁄ The data packet produced by the marshaling process will be unmarshaled in the destination process
    HGLOBAL memoryHandleFromStream = NULL;
    GetHGlobalFromStream(marshalStream, &memoryHandleFromStream);
    LPOBJREF objef = reinterpret_cast <LPOBJREF> (GlobalLock(memoryHandleFromStream));

Then we use a debugger to see the contents of the IPID structure:

Contents of the IPID structure


Figure 5. Contents of the IPID structure

First two bytes of Data2 are the PID of the remote process.

The code to get the PID from OBJREF looks like this:

#include <atlbase.h>
#include <windows.h>
int main()
    ⁄⁄ Valid OBJREF has this field = 0x574f454d
    ⁄⁄ https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/fe6c5e46-adf8-4e34-a8de-3f756c875f31
    if (objef && objef->signature == OBJREF_SIGNATURE)
        IPID ipid;
        if (objef->flags == OBJREF_STANDARD)
            ipid = objef->u_objref.u_standard.std.ipid;
        else if (objef->flags == OBJREF_HANDLER)
            ipid = objef->u_objref.u_handler.std.ipid;
        else if (objef->flags == OBJREF_EXTENDED)
            ipid = objef->u_objref.u_extended.std.ipid;
        DWORD pid = 0;
        if (GetCOMServerPID(ipid, &pid))
            std::cout << "PID: " << pid << std::endl;

To extract the PID, we use the GetCOMServerPID function:

BOOL GetCOMServerPID(__in IPID ipid, __out DWORD* pid)
    static const int COM_SERVER_PID_OFFSET = 4;
    *pid = *reinterpret_cast<LPWORD>(
        (reinterpret_cast<LPBYTE>(&ipid) + COM_SERVER_PID_OFFSET)
    ⁄⁄ IPID contains only 16-bit for PID, and if the PID > 0xffff, then it's clapped to 0xffff
    return *pid != 0xffff;

If the PID is less than 65535, we will get it after executing this function. If it’s greater, we need to use another method.

Get the COM server PID from the OXID Resolver

Note: Error handling is omitted for brevity.

Every COM machine runs a special manager service called Object Resolver, also called OXID Resolver. It runs on every machine that supports COM and performs two important functions:

  • Stores the remote procedure call (RPC) string bindings that are necessary to connect with remote objects and provides RPC string bindings to local clients.
  • Sends ping messages to remote objects for which the local machine has clients and receives ping messages for objects running on the local machine.

Basically, OXID Resolver stores information about the COM server (addresses, ports, etc.). We can retrieve this information from the OXID Resolver by calling the ResolveOxid method:

* [idempotent] *error_status_t ResolveOxid(
* [in] *handle_t hRpc,
* [in] *⁄ OXID *pOxid,
* [in] *unsigned short cRequestedProtseqs,
* [size_is][ref][in] *unsigned short arRequestedProtseqs[  ],
* [ref][out] *⁄ DUALSTRINGARRAY **ppdsaOxidBindings,
* [ref][out] *⁄ IPID *pipidRemUnknown,
* [ref][out] *⁄ DWORD *pAuthnHint);

First, we should retrieve the RPC binding to connect to the OXID Resolver. We can establish a connection with the OXID Resolver via transmission control protocol (TCP) port 135:


Here’s an example of this:

⁄⁄ OXID Resolver server listens to TCP port 135
⁄⁄ https://docs.microsoft.com/en-us/troubleshoot/windows-server/networking/service-overview-and-network-port-requirements
RPC_WSTR OXIDResolverStringBinding = 0;
⁄⁄ Make OXID Resolver authenticate without a password
RpcBindingSetOption(OXIDResolverBinding, RPC_C_OPT_BINDING_NONCAUSAL, 1);
RPC_SECURITY_QOS securityQualityOfServiceSettings;
securityQualityOfServiceSettings.Version         = 1;
securityQualityOfServiceSettings.Capabilities    = RPC_C_QOS_CAPABILITIES_MUTUAL_AUTH;
securityQualityOfServiceSettings.IdentityTracking  = RPC_C_QOS_IDENTITY_STATIC;
securityQualityOfServiceSettings.ImpersonationType = RPC_C_IMP_LEVEL_IMPERSONATE;
    RPC_WSTR(L"NT Authority\\NetworkService"),

Now, we can use the OXID Resolver to get the server’s string bindings. String bindings are similar to logical addresses. 


Let’s call the ResolveOxid method to get string bindings of the server:

unsigned short requestedProtocols[] = { TCP_PROTOCOL_ID };
IPID            remoteUnknownIPID     = GUID_NULL;
DWORD           authHint              = 0;

We requested the TCP addresses, but the server may not support any networking. It seems like Microsoft Excel doesn’t use any TCP connections. In this case, the request should cause an error, but it doesn’t. To figure out why there’s no error, let’s look in Process Explorer from Sysinternals tools to see what TCP/IP connections Microsoft Excel uses after the execution of the code above:

pasted image 0 2


Figure 6. TCP/IP connections

As you can see, Microsoft Excel has two open ports. 


Object Resolver makes Microsoft Excel open a TCP port and returns us that binding. Now it’s easy to get the process that uses that port with the GetTcpTable2 function or the PowerShell command:

Get-NetTCPConnection | where Localport -eq 24043 | select Localport,OwningProcess
Localport OwningProcess
--------- -------------
    24043       12144
    24043       12144

This method is the most correct way to get the PID of a COM server. By getting the PID from the OXID resolver, we use a documented API and follow the same steps as the COM runtime during the connection of remote objects.

Get a COM server PID from the ALPC port 

Note: Error handling is omitted for brevity.

The previous two methods used the OBJREF interface to find the PID. This method, on the other hand, takes a rather different approach, as it uses Advanced Local Procedure Call (ALPC). COM employs this procedure to communicate between objects on the same machine. 


With API Monitor tracing the RpcBindingFromStringBinding function, we can see string binding inside the CoMarshalInterface function that the client gets the RPC binding from:



Figure 7. String binding inside of CoMarshalInterface

Let’s look at opened handles of the COM server using Process Explorer from Sysinternals tools:



Figure 8. Opened handles of a COM server

We can see that the COM server uses the same ALPC Port that the client passes to the RpcBindingFromStringBinding function. In order to find the COM server’s PID, we should:

Note: Make sure that the RpcBindingFromStringBinding function is called by the COM runtime and not by a plain RPC call. The RpcBindingFromStringBinding function is used as an element of the COM communication implementation, and if we check this function in the API monitor and we are interested in COM, we have to make sure that it’s called by the COM runtime.

The most challenging part of this method is finding a process that uses the handle value, as there’s no documented way to do that. We have to explore the ntdll.dll functions and structures to get information about handles and all running processes. First, we need to get some macros and definitions, as most ntdll.dll constants and structures are not exposed in public headers:

#define NT_SUCCESS(x) ((x) >= 0)
#define STATUS_INFO_LENGTH_MISMATCH   0xc0000004
#define SystemHandleInformation       16
#define SystemExtendedHandleInformation 0x40
#define ObjectBasicInformation        0
#define ObjectNameInformation         1
#define ObjectTypeInformation         2
typedef NTSTATUS(NTAPI *NtQuerySystemInformationType)(
    ULONG SystemInformationClass,
    PVOID SystemInformation,
    ULONG SystemInformationLength,
    PULONG ReturnLength
typedef NTSTATUS(NTAPI *NtDuplicateObjectType)(
    HANDLE SourceProcessHandle,
    HANDLE SourceHandle,
    HANDLE TargetProcessHandle,
    PHANDLE TargetHandle,
    ACCESS_MASK DesiredAccess,
    ULONG Attributes,
    ULONG Options
typedef NTSTATUS(NTAPI *NtQueryObjectType)(
    HANDLE ObjectHandle,
    ULONG ObjectInformationClass,
    PVOID ObjectInformation,
    ULONG ObjectInformationLength,
    PULONG ReturnLength
typedef NTSTATUS(NTAPI *NtCloseType)(
    HANDLE ObjectHandle
typedef struct _SYSTEM_HANDLE
    ULONG ProcessId;
    BYTE ObjectTypeNumber;
    BYTE Flags;
    USHORT Handle;
    PVOID Object;
    ACCESS_MASK GrantedAccess;
    PVOID Object;
    ULONG_PTR  ProcessId;
    ULONG_PTR  Handle;
    ULONG GrantedAccess;
    USHORT CreatorBackTraceIndex;
    USHORT ObjectTypeIndex;
    ULONG HandleAttributes;
    ULONG Reserved;
    ULONG HandleCount;
    SYSTEM_HANDLE Handles[1];
    ULONG HandleCount;
    ULONG reserved;
#ifdef _M_X64
    PVOID reserved2;
typedef enum _POOL_TYPE
    ULONG TotalNumberOfObjects;
    ULONG TotalNumberOfHandles;
    ULONG TotalPagedPoolUsage;
    ULONG TotalNonPagedPoolUsage;
    ULONG TotalNamePoolUsage;
    ULONG TotalHandleTableUsage;
    ULONG HighWaterNumberOfObjects;
    ULONG HighWaterNumberOfHandles;
    ULONG HighWaterPagedPoolUsage;
    ULONG HighWaterNonPagedPoolUsage;
    ULONG HighWaterNamePoolUsage;
    ULONG HighWaterHandleTableUsage;
    ULONG InvalidAttributes;
    GENERIC_MAPPING GenericMapping;
    ULONG ValidAccess;
    BOOLEAN SecurityRequired;
    BOOLEAN MaintainHandleCount;
    USHORT MaintainTypeList;
    POOL_TYPE PoolType;
    ULONG PagedPoolUsage;
    ULONG NonPagedPoolUsage;

To get a PID, we need to call a function that prints all opened ALPC ports and corresponding PIDs to the standard output:

#include "ntdefines.h"
NtQuerySystemInformationType NtQuerySystemInformation = reinterpret_cast<NtQuerySystemInformationType>(
        GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQuerySystemInformation"));
NtDuplicateObjectType NtDuplicateObject = reinterpret_cast<NtDuplicateObjectType>(
        GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtDuplicateObject"));
NtQueryObjectType NtQueryObject = reinterpret_cast<NtQueryObjectType>(
        GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryObject"));
void GetALPCPorts()
    std::vector<BYTE> infoStorage                     = std::vector<BYTE>(INITIAL_SYSTEM_INFORMATION_LENGTH);
    ⁄⁄ NtQuerySystemInformation won't give us the correct buffer size, so we double the buffer size until the operation is successful 
    while (NtQuerySystemInformation(SystemHandleInformation, infoStorage.data(), infoStorage.size(), NULL) == STATUS_INFO_LENGTH_MISMATCH)
        infoStorage.resize(infoStorage.size() * 2);
    PSYSTEM_HANDLE_INFORMATION handleInfo = reinterpret_cast<psystem_handle_information>(infoStorage.data());
    for(ULONG i = 0; i < handleInfo->HandleCount; ++i)
        ⁄⁄ Skip if the type isn't an ALPC port. Note: This value might be different on other systems. This was tested on 64-bit Windows 10.
        if (handleInfo->Handles[i].ObjectTypeNumber != 0x2e)
        HANDLE process = OpenProcess(PROCESS_DUP_HANDLE, FALSE, handleInfo->Handles[i].ProcessId);
        ⁄⁄ Duplicate the handle so we can query it
        HANDLE dublicatedHandle;
        NtDuplicateObject(process, reinterpret_cast<HANDLE>(handleInfo->Handles[i].Handle),
            GetCurrentProcess(), &dublicatedHandle, 0, 0, 0);
        ⁄⁄ Let's suppose it's enough space for storing the name
        std::vector<BYTE> objectName(8192);
        ⁄⁄ Try to query the name
        NtQueryObject(dublicatedHandle, ObjectNameInformation,
            objectName.data(), objectName.size(), NULL);
        POBJECT_NAME_INFORMATION objectNameInformation = reinterpret_cast<pobject_name_information>(objectName.data());
        if (objectNameInformation->Name.Buffer != NULL)
            std::wcout << "ALPC Port: " << std::wstring(objectNameInformation->Name.Buffer, objectNameInformation->Name.Lengthsizeof(wchar_t))
                << " PID: " << handleInfo->Handles[i].ProcessId << std::endl;
int main()
    return 0;

As we can see, here’s the needed process. Now we just need to filter it by the known ALPC port name:



Figure 9. COM process ID found through the ALPC port

This approach is the most time-consuming because it involves examining each process in the system and the name of each handle. 

Related services

Outsource Software Development in C/C++


This article describes three different ways to get a process ID of a COM server. This value can be extremely important if you’re faced with a task that involves creating parent–child processes in system monitoring tools and checking DLP systems. 

  • Get COM server PID from IPID. The fastest way of getting COM servers process ID is from the IPID struct, but it can provide only values less than 65535. Also, it relies on the undocumented structure of the IPID field in the marshaled form.
  • Get COM server PID from OXID resolver. Getting a PID from the OXID Resolver can be considered the most correct way of getting a COM server’s PID because it uses a documented API and performs the same steps as the COM runtime does when connecting to remote objects.
  • Get COM server pid from alpc port. Getting COM server’s PID through the ALPC port requires using an undocumented API from ntdll.dll. Note that it may take quite a lot of time, as this approach involves looking into each process on the system and into each handle’s name.

Apriorit specialists have in-depth knowledge of engineering cybersecurity solutions and are always ready to help you with that. If you have a challenging cybersecurity project in mind, feel free to contact us.

Tell us about your project

Send us a request for proposal! We’ll get back to you with details and estimations.

By clicking Send you give consent to processing your data

Book an Exploratory Call

Do not have any specific task for us in mind but our skills seem interesting?

Get a quick Apriorit intro to better understand our team capabilities.

Book time slot

Contact us