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.
Contents
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.
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:
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:
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.
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:
- Client calls the CoCreateInstanceEx function causing COM to delegate the activation request to its local Service Control Manager (SCM).
- 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.
- 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.
- 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.
- 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;
} OBJREF, *LPOBJREF;
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;
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.
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.
CoInitialize(NULL);
CComPtr<iunknown> excelInterface;
excelInterface.CoCreateInstance(
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:
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);
CoMarshalInterface(
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:
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;
RpcStringBindingComposeW(
NULL,
RPC_WSTR(L"ncacn_ip_tcp"),
RPC_WSTR(L"127.0.0.1"),
RPC_WSTR(L"135"),
NULL,
&OXIDResolverStringBinding
);
RPC_BINDING_HANDLE OXIDResolverBinding = 0;
RpcBindingFromStringBindingW(
OXIDResolverStringBinding,
&OXIDResolverBinding
);
โโ 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;
RpcBindingSetAuthInfoExW(
OXIDResolverBinding,
RPC_WSTR(L"NT Authority\\NetworkService"),
RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
RPC_C_AUTHN_WINNT,
NULL,
RPC_C_AUTHZ_NONE,
&securityQualityOfServiceSettings
);
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 };
LPDUALSTRINGARRAY COMServerStringBindings = NULL;
IPID remoteUnknownIPID = GUID_NULL;
DWORD authHint = 0;
ResolveOxid(
OXIDResolverBinding,
&oxid,
_countof(requestedProtocols),
requestedProtocols,
&COMServerStringBindings,
&remoteUnknownIPID,
&authHint
);
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:
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:
Letโs look at opened handles of the COM server using Process Explorer from Sysinternals tools:
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:
-
- Hook the RpcBindingFromStringBinding function
- Wait until the “ncalrpc:[OLE*]” string comes to the call
- Call the RpcStringBindingParse function to get an endpoint part (OLE*)
- Find a process that uses that ALPC Port
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;
} SYSTEM_HANDLE, *PSYSTEM_HANDLE;
typedef struct SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX
{
PVOID Object;
ULONG_PTR ProcessId;
ULONG_PTR Handle;
ULONG GrantedAccess;
USHORT CreatorBackTraceIndex;
USHORT ObjectTypeIndex;
ULONG HandleAttributes;
ULONG Reserved;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX, *PSYSTEM_HANDLE_TABLE_ENTRY_INFO_EX;
typedef struct _SYSTEM_HANDLE_INFORMATION
{
ULONG HandleCount;
SYSTEM_HANDLE Handles[1];
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
typedef struct _SYSTEM_HANDLE_INFORMATION_EX
{
ULONG HandleCount;
ULONG reserved;
#ifdef _M_X64
PVOID reserved2;
#endif
SYSTEM_HANDLE_TABLE_ENTRY_INFO_EX Handles[1];
} _SYSTEM_HANDLE_INFORMATION_EX, *P_SYSTEM_HANDLE_INFORMATION_EX;
typedef enum _POOL_TYPE
{
NonPagedPool,
PagedPool,
NonPagedPoolMustSucceed,
DontUseThisType,
NonPagedPoolCacheAligned,
PagedPoolCacheAligned,
NonPagedPoolCacheAlignedMustS
} POOL_TYPE, *PPOOL_TYPE;
typedef struct _OBJECT_TYPE_INFORMATION
{
UNICODE_STRING Name;
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;
} OBJECT_TYPE_INFORMATION, *POBJECT_TYPE_INFORMATION;
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()
{
const ULONG INITIAL_SYSTEM_INFORMATION_LENGTH = 0x10000;
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)
{
continue;
}
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);
CloseHandle(process);
โโ 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);
CloseHandle(dublicatedHandle);
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.Length โ sizeof(wchar_t))
<< " PID: " << handleInfo->Handles[i].ProcessId << std::endl;
}
}
}
int main()
{
GetALPCPorts();
return 0;
}
</pobject_name_information></psystem_handle_information>
As we can see, hereโs the needed process. Now we just need to filter it by the known ALPC port name:
This approach is the most time-consuming because it involves examining each process in the system and the name of each handle.
Conclusion
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.