STUBborn: Activate and call DCOM objects without proxy
In the last years, the Local RPC (LRPC) & ALPC have been the subject of scrutiny by some Windows internal enthusiasts and vulnerability researchers.
In this article, we will go a step further to explore what can be done about LocalServer DCOM objects, how to instantiate them and directly connect to their interfaces without using the COM proxy clients normally rely on!
This will give us an excuse to explore some COM internals, understand part of the combase DLL & write more fun python code !
1 The Goal
In this article, our goal will be to interact directly with a DCOM object in a remote process of the local computer by using a custom RPC client.
Where does this challenge come from? When working on our Windows forensic collect agent (french link)1 I sometimes encounter new forensics artifacts or information that may be a challenge to retrieve. In this case, I use it as an excuse to dive in some Windows Internals and explore new things.
I once encountered a COM server able to give me some sweet forensics information and whose proxy was only available as a 64-bit DLL, my goal at this time was to be able to query a specific method of a COM interface from a 32-bit process. Our forensic agent being a unique 32-bit process that crosses the heaven gate when needed.
The final goal being to use it in production, I added another requirement: to let the real COM machinery do the works when possible to limit the changes required to handle Windows version from Windows Server 2003 to Windows 11.
This article relies on some basic knowledge of COM2; understanding the concepts of IID, CLSID, CoCreateInstance, IUnknown and IUnknown::QueryInterface would enhance comprehension. As always, I can only recommend having a look at James Forshaw’s COM in Sixty Seconds!3 presentation (or at least the first half).
1.1 Setup
1.1.1 IExaDemo
To replicate this need, we will use a toy COM LocalServer that I implemented and open-sourced
here: https://github.com/ExaTrack/COM_IExaDemo. The
complete information about compiling and installing both binaries (IExaDemo_server_64.exe
& IExaDemo_proxy_64.dll
) is
in the
repos README.md.
In our case we will compile both the COM server and proxy DLL for 64-bit and try to access it from a 32-bit Python interpreter.
The main interface we want to interact with is IExaDemo
whose IDL is given here:
[
uuid (45786100-1111-2222-3333-445500000001),
version(1.0),
] interface IExaDemo : IUnknown
{
HRESULT add(
[in] unsigned int x,
[in] unsigned int y,
[out] unsigned int *res
);
HRESULT print(
[in][string] wchar_t* msg);
};
// IDL description of the server CLSID
[
uuid(45786100-4343-4343-4343-434343434343),
version(1.0)
]
coclass ExaDemoSrv
{
interface IExaDemo;
}
So, without the proxy DLL, we will try to add two numbers and get the result back. If you are curious about the add()
implementation, the code is here.
In our setup, the CLSID of our server is 45786100-4343-4343-4343-434343434343
and the IID we want to interact with
is 45786100-1111-2222-3333-445500000001
aka IID_IExaDemo
.
1.1.2 PythonForWindows
For this project, I will exclusively use Python3.11
and PythonForWindows
, a library of my creation that I use for my
works on Windows.
Most of my discoveries have already been committed to the project, so some explanations and code will directly reference it.
A simple python client, using the proxy DLL and the full COM machinery is available in the COM_IExaDemo
repos
as client.py
.
The important bits are explained here:
import ctypes
import windows.com
import windows.generated_def as gdef
# General PFW definition for the COM class, normally generated from the IDL c output
class IExaDemo(gdef.COMInterface):
IID = gdef.generate_IID(0x45786100, 0x1111, 0x2222, 0x33, 0x33, 0x44, 0x55, 0x00, 0x00, 0x00, 0x01, name="IExaDemo",
strid="45786100-1111-2222-3333-445500000001")
[ctypes interface definition]
EXA_DEMO_SRV_CLSID = "45786100-4343-4343-4343-434343434343"
windows.com.init() # CoInitialize
iunk = gdef.IUnknown()
print("CreateInstance {0}".format(EXA_DEMO_SRV_CLSID))
# Wrapper around CoCreateInstance which is more permissiv with GUID & strings
windows.com.create_instance(EXA_DEMO_SRV_CLSID, iunk, gdef.IUnknown.IID)
print(" OK: Got an IUnknown: {0}".format(iunk))
print("QueryInterface: IExaDemo.IID: {0}".format(IExaDemo.IID))
# A wrapper around IUnknown.QueryInterface that can be read as
# iunk.QueryInterface(IExaDemo.IID, IExaDemo())
iexademo = iunk.query(IExaDemo)
print(" OK: {0}".format(iexademo))
print("Adding stuff:")
result = gdef.DWORD()
iexademo.add(0x41414141, 0x01010101, result)
print(" iexademo.add(0x41414141, 0x01010101) == {0:#x}".format(result.value))
The output of this script would be
PS> py -3.11 .\client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x1dc36053750>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
OK: <IExaDemo at 0x1dc360539d0>
Adding stuff:
iexademo.add(0x41414141, 0x01010101) == 0x42424242
Whereas the ExaDemo_server_64.exe
console output should end with:
CALL:IExaDemoImplem_QueryInterface
[IExaDemoImplem_QueryInterface] * riid: {45786100-1111-2222-3333-445500000001} !
[IExaDemoImplem_QueryInterface] * Asking for IID_IExaDemo: I CAN DO THAT !
[...]
CALL:IExaDemoImplem_add
[IExaDemoImplem_add] 0x41414141 + 0x1010101 = 0x42424242
1.2 The problem
First, let’s take a look at how a typical COM client behaves when a proxy DLL is missing.
To do so, we will execute client.py
with a 32-bit python, bitness in which the proxy DLL is not installed.
PS> py -3.11-32 .\client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x1b12440>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
Traceback (most recent call last):
File "COM_IExaDemo\client.py", line 34, in <module>
iexademo = iunk.query(IExaDemo)
^^^^^^^^^^^^^^^^^^^^
File "pythonforwindows\windows\generated_def\interfaces.py", line 62, in query
self.QueryInterface(interface_iid, target)
File "pythonforwindows\windows\generated_def\interfaces.py", line 28, in _default_errcheck
ctypes._check_HRESULT(result)
OSError: [WinError -2147221164] Class not registered
The client fails, and what’s more interesting is that the fail does not occur at the instance creation of the IUnknown
but only when we query for an interface handled by our proxy: IID_IExaDemo
.
In WinDBG we can see that the error occurs in the following stack:
0:000> k
# ChildEBP RetAddr
...
02 (Inline) -------- combase!CStdMarshal::GetPSFactory+0xa9 [onecore\com\combase\dcomrem\marshal.cxx @ 6664]
03 00beecd8 772f369b combase!CStdMarshal::CreateProxy+0x1da [onecore\com\combase\dcomrem\marshal.cxx @ 6745]
...
09 (Inline) -------- combase!CStdMarshal::QueryRemoteInterfaces+0x71 [onecore\com\combase\dcomrem\marshal.cxx @ 5759]
0b 00beeef8 73834018 combase!CStdIdentity::CInternalUnk::QueryInterface+0x1f0 [onecore\com\combase\dcomrem\stdid.cxx @ 428]
This demonstrates that our proxy is actually searched for and loaded when querying a specific interface, rather than at instantiation time.
It means that we may be able to delegate some of the heavy lifting to the COM runtime. What we can do is let COM perform the instance creation for us, and then connect to the COM server and its created object.
With the setup done, and the problem defined, let’s begin our exploration.
2 STUBBorn
2.1 State of the art
At this time, a lot have been already done about RPC, ORPC & DCOM.
The first place to look for information is the MS-DCOM open specification4 which is a BIG document with a lot of very precise information about DCOM.
I can also cite this Airbus article5
as a reliable source of information about the OXIDResolver
from a network point of view. This gave us a preview of what to expect.
Next, I obviously need to cite the various works of James Forshaw. I have in
mind this article about Relaying DCOM Authentication6
and
the andbox-attacksurface-analysis-tools GitHub repos7.
This gives us more information about what to expect in terms of OBJREF
and interface marshaling.
Of course, it’s hard to make any Windows Internal’s state of the art without citing Alex Ionescu, this time for
the hazmat5 GitHub repos8 which provides various
information about the RPC interfaces offered by RPCSS
and the ILocalObjectExporter
.
Lastly, I can cite my previous work on RPC&ALPC9 and the RPC client integrated in PythonForWindows10 that was improved for this research.
2.2 What are we looking for ?
Based on the state of the art, we can already establish a shopping list of what we need to find in order to manually
interact with the remote IExaDemo
and IExaDemo_server_64.exe
.
We need to find an RPC endpoint in IExaDemo_server_64.exe
, based on previous research10 we can assess
that the endpoint will be an ALPC Port. We could list the handles of the process, but that would be cheating 😁. We need
to upgrade our knowledge of RPC to ORPC (Object Remote Procedure Call).
The Microsoft documentation about ORPC Calls11 also tells us we will need an IPID 12 that
represents a given interface for a remote object.
Shopping List:
- ALPC Endpoint
- IPID
- ORPC capabilities
2.3 What we won’t do
The first step of my research was to analyze the ALPC-RPC interactions of a working client and the various ALPC-RPC endpoints it interacts with during instantiation, interface queries & ORPC calls.
By analyzing the calls to NtAlpcCreatePort
and NtAlpcSendWaitReceive
with a custom debugger, the following image can
be drawn:
[ALPC-CONNECT] <\RPC Control\epmapper>
[RPC-BIND]: \RPC Control\epmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (name=ILocalObjectExporter)
...
[RPC-CALL]: \RPC Control\epmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (method 0) (name=Connect)
...
[RPC-CALL]: \RPC Control\epmapper (<RPC_IF_ID "E60C73E6-88F9-11CF-9AF1-0020AF6E72F4" (2, 0)>) (method 6) (name=ServerAllocateOXIDAndOIDs)
[RPC-BIND]: \RPC Control\epmapper (<RPC_IF_ID "00000136-0000-0000-C000-000000000046" (0, 0)>) (name=ISCMLocalActivator)
[RPC-CALL]: \RPC Control\epmapper (<RPC_IF_ID "00000136-0000-0000-C000-000000000046" (0, 0)>) (method 4) (name=CreateInstance)
[ALPC-CONNECT] <\RPC Control\OLE1C6126F89B815E927A6AF617D45E>
[RPC-BIND]: \RPC Control\OLE1C6126F89B815E927A6AF617D45E (<RPC_IF_ID "00000134-0000-0000-C000-000000000046" (0, 0)>) (name=)
On this information only, we can confirm:
- That DCOM use ALPC
- That DCOM indeed use
ILocalObjectExporter
13 that is served by the endpoint\RPC Control\epmapper
- Knowing that
ISCMLocalActivator
is indeed a DCOM object, this gives us some insight about ORPC sharing a lot of logic with RPC bind/call logic- The bind happening on the interface IID of the DCOM interface target.
All this, give us exactly what we WON'T DO :
- Manually connect to the
epmapper
- bind to
ILocalObjectExporter
and callConnect
- bind to
ISCMLocalActivator
and callCreateInstance
Although this would be a promising research angle and would likely work at the end, I have multiple reasons to approach things
differently.
First of all, the prototype depicted
by hazmat514
for ILocalObjectExporter.Connect
shows that the crafting of the [in]
parameters and the parsing of [out]
it would
be tedious.
The parameters let us also guess that this interface has most likely evolved multiple times since Windows XP
.
This last point is of importance, because my final goal is to be able to use STUBborn
in
our forensic collect agent (french link)1 to target
some DCOM endpoints with some valuable forensic information. So I want to have a code that will work across the widest
range of Windows versions with minimal modifications. Thus this rejoin our previously stated goal of letting the COM runtime do the heavy lifting of object creation for us.
Lastly, this is a good reason to explore the inner workings of combase.dll
!
2.4 Combase.dll
combase.dll
is the DLL implementing a big part of the COM logic and whose content and capabilities are quite
interesting to explore.
As far as I know, this DLL is loaded in any COM client or server, especially due to the fact that it implements
important functions such as CoCreateInstance
or CoRegisterClassObject
. In older version of Windows, this logic could
be found in ole32.dll
.
2.4.1 gInternalClassObjects
A well-known behavior of COM is that the COM servers are registered in the registry and that CLSID are looked up
in HKEY_CLASSES_ROOT\CLSID
for the server path. A less well-known behavior is that combase.dll
implements some class
objects as hardcoded CLSID that bypass the registry and are directly implemented in combase
.
This is represented as an array of (CLSID
, CreateInstance_Method
, Flags
) which can be found
in combase!gInternalClassObjects
:
CLSID | CreateInstance function |
---|---|
combase!CLSID_StdEvent |
combase!CStdEventCF_CreateInstance |
combase!CLSID_ManualResetEvent |
combase!CManualResetEventCF_CreateInstance |
combase!CLSID_SynchronizeContainer |
combase!CSynchronizeContainerCF_CreateInstance |
combase!CLSID_StdGlobalInterfaceTable |
combase!CGIPTableCF_CreateInstance |
combase!CLSID_DCOMAccessControl |
combase!CAccessControlCF_CreateInstance |
combase!CLSID_ErrorObject |
combase!CErrorObjectCF_CreateInstance |
combase!GUID_00000346_0000_0000_c000_000000000046 |
combase!CComCatalogCF_CreateInstance |
combase!CLSID_RpcHelper |
combase!CRpcHelperCF_CreateInstance |
combase!CLSID_ObjectContext |
combase!CObjectContextCF_CreateInstance |
combase!CLSID_InProcFreeMarshaler |
combase!CFreeThreadedMarshalerCF_CreateInstance |
combase!CLSID_ActivationPropertiesIn |
combase!CActivationPropertiesInCF_CreateInstance |
combase!CLSID_ActivationPropertiesOut |
combase!CActivationPropertiesOutCF_CreateInstance |
combase!CLSID_InprocActpropsUnmarshaller |
combase!CInprocActpropsUnmarshallerCF_CreateInstance |
combase!CLSID_ComActivator |
combase!CComActivatorCF_CreateInstance |
combase!CLSID_AddrControl |
combase!CAddrControlCF_CreateInstance |
combase!CLSID_LocalMachineNames |
combase!CLocalMachineNamesCF_CreateInstance |
combase!CLSID_GlobalOptions |
combase!CGlobalOptionsCF_CreateInstance |
combase!CLSID_ContextSwitcher |
combase!CContextSwitcherCF_CreateInstance |
combase!CLSID_RestrictedErrorObject |
combase!CRestrictedErrorObjectCF_CreateInstance |
combase!CLSID_RegisterSuspendNotify |
combase!CSuspendMonitorCF_CreateInstance |
combase!CLSID_MachineGlobalObjectTable |
combase!CMgotCF_CreateInstance |
combase!CLSID_ActivationCapabilities |
combase!CActivationCapabilitiesCF_CreateInstance |
This gives us a list of CLSID to explore, instantiate and test. Some of these are already used and known, for example,
the CLSID_ComActivator
1718 that is used under the hood
of combase!CoCreateInstance
.
The question is, which class do we want to explore for our objective? By exploring around object creation and ALPC
transmission of data over ntdll!NtAlpcSendWaitReceivePort
we can encounter the following type of call stack:
:000> kc
# Call Site
00 ntdll!NtAlpcSendWaitReceivePort
[...]
07 combase!ServerAllocateOXIDAndOIDs
08 combase!CRpcResolver::ServerRegisterOXID
[...]
11 combase!CRpcResolver::CreateInstance
12 combase!CClientContextActivator::CreateInstance
13 combase!ActivationPropertiesIn::DelegateCreateInstance
14 combase!ICoCreateInstanceEx
15 combase!CComActivator::DoCreateInstance
16 combase!CoCreateInstanceEx
17 combase!CoCreateInstance
The object ActivationPropertiesIn
, present under the known CComActivator
, seems to participate to the instance creation
and is accessible via the gInternalClassObjects
thus seems a very good candidate for further exploration.
Another good indicator is
the MS-DCOM IRemoteSCMActivator:: RemoteGetClassObject page19
that indicates the function takes an ActivationPropertiesIn
as a parameter. Promising!
2.4.2 ActivationPropertiesIn
The CLSID_ActivationPropertiesIn
indeed allow us to instantiate an object in combase that present (at least) the
following interfaces:
- IActivationProperties
- IActivationPropertyIn
- IInitActivationPropertiesIn
- IPrivActivationPropertiesIn
- IActivationStageInfo
Each interface allows to initialize some important part of the ActivationPropertiesIn
structure to trigger a
custom CreateInstance
.
This step represents the minimal code identified to have a successful CoCreateInstance
emulation.
IActivationPropertiesIn.AddRequestedIIDs(1, IID)
allow filling the requested IID (riid
)IInitActivationPropertiesIn.SetClassInfo(IComClassInfo)
allow filling the requested CLSID (rclsid
)IInitActivationPropertiesIn.SetClsctx(CLSCTX_LOCAL_SERVER)
allow filling the server context (dwClsContext
)IActivationStageInfo.SetStageAndIndex(CLIENT_CONTEXT_STAGE, 0)
tell we are a client activator (and prevent aCatastrophic failure
error)IPrivActivationPropertiesIn.DelegateCreateInstance([out] IActivationPropertiesOut)
trigger the actualCreateInstance
The IComClassInfo
that need to be passed to IInitActivationPropertiesIn.SetClassInfo
can be obtained via two
methods:
- Implementing a custom
IComClassInfo
- The only method that seems to be called is
IComClassInfo.GetConfiguredClsid
which need to return the CLSID we ask for
- The only method that seems to be called is
- Via the combase object with CLSID
GUID_00000346_0000_0000_c000_000000000046
corresponding to theCComCatalog
- that implements
IComCatalog.GetClassInfo
taking a CLSID and returning aIComClassInfo
- that implements
The question that may be asked is: what is the advantage of using a ActivationPropertiesIn
for instance creation compared to CoCreateInstance
?
The response is the return value, this method returns an ActivationPropertiesOut
which is a trove of information!
2.4.3 ActivationPropertiesOut
The object returned by IPrivActivationPropertiesIn.DelegateCreateInstance()
is an ActivationPropertiesOut
which
present (at least) the following interfaces:
The first interesting function is IScmReplyInfo.GetResolverInfo()
that allows us to retrieve a structure containing
information about the remote COM server in
a PRIV_RESOLVER_INFO.
2.4.3.1 PRIV_RESOLVER_INFO
As far as I know, this structure is the only thing that changed in different Windows version. The old
version PRIV_RESOLVER_INFO
can be traced back from Windows XP
up to Windows Server 2016
(at least on some install ISO).
The new version
of PRIV_RESOLVER_INFO
has been found on my test computer (10.0.22631.4317
) as well as on an ISO install of Windows Server 2019
.
If someone finds the exact update/version in which the definition change, I would be glad to get the information! For the rest of the article, I will use the latest version.
This structure contains a lot of useful information to accomplish our task, let’s have a look at
what IScmReplyInfo.GetResolverInfo()
returns us.
>>> resolver_info
<windows.generated_def.winstructs._PRIV_RESOLVER_INFO object at 0x000001F2BA31AFD0>
>>> windows.utils.sprint(resolver_info)
struct.OxidServer -> 0x47b7459170ebf6dd
struct.pServerORBindings -> NULL
struct.OxidInfo.dwTid -> 0x2f94
struct.OxidInfo.dwPid -> 0x56fc
struct.OxidInfo.dwAuthnHint -> 0x5
struct.OxidInfo.dcomVersion.MajorVersion -> 0x5
struct.OxidInfo.dcomVersion.MinorVersion -> 0x7
struct.OxidInfo.containerVersion.version -> 0x3
struct.OxidInfo.containerVersion.capabilityFlags -> 0x0
struct.OxidInfo.containerVersion.extensions -> NULL
struct.OxidInfo.ipidRemUnknown -> <GUID "00009400-56FC-2F94-4A97-D41AAB84F489">
struct.OxidInfo.dwFlags -> 0x4000000
struct.OxidInfo.psa<deref>.wNumEntries -> 0x3d
struct.OxidInfo.psa<deref>.wSecurityOffset -> 0x2b
struct.OxidInfo.psa<deref>.aStringArray -> <windows.generated_def.winstructs.c_ushort_Array_1 object at 0x000002215903F050>
struct.OxidInfo.guidProcessIdentifier -> <GUID "36A580E5-366E-4E9D-81E7-3FAC865D240A">
struct.OxidInfo.processHostId -> 0x0
struct.OxidInfo.clientDependencyBehavior -> 0x0
struct.OxidInfo.packageFullName -> NULL
struct.OxidInfo.userSid -> NULL
struct.OxidInfo.appcontainerSid -> NULL
struct.OxidInfo.primaryOxid -> 0xc81a618f9842fdf3
struct.OxidInfo.primaryIpidRemUnknown -> <GUID "00004C01-56FC-FFFF-2994-EED6240E08FE">
struct.LocalMidOfRemote -> 0x57d71877bf3b4fe2
struct.FoundInROT -> 0x0
The main information is priv_resolver_info.OxidInfo.psa
which is a pointer to
a tagDUALSTRINGARRAY20
and necessitate some parsing to interpret.
>>> resolver_info.OxidInfo.psa[0].bidings
['ncalrpc:[OLE85D5A9520394209954DA2E2595EF]']
This gives us the ALPC Port exposed by our COM server IExaDemo_server_64.exe
! The real path of the port can be
inferred as \RPC Control\OLE85D5A9520394209954DA2E2595EF
.
With this information we can directly connect to it via ALPC and start direct interaction:
>>> client = windows.rpc.RPCClient(r"\RPC Control\OLE85D5A9520394209954DA2E2595EF")
>>> iid = client.bind("45786100-1111-2222-3333-445500000001", (0, 0))
>>> client.call(iid, 0, b"Hello !")
Traceback (most recent call last):
[...]
ValueError: RPC Response error 2147549457 (RPC_E_INVALID_HEADER(0x80010111))
One step done, let’s update the shopping list and continue to try to understand how to make direct call!
Shopping List:
- ALPC Endpoint
- IPID
- ORPC capabilities
2.4.3.2 IPrivActivationPropertiesOut
The other interesting interface offered by our ActivationPropertiesOut
is IPrivActivationPropertiesOut
. More
precisely
the IPrivActivationPropertiesOut.GetMarshalledResults()
method.
This method allows to retrieve the interfaces pointers instantiated
during IPrivActivationPropertiesIn.DelegateCreateInstance()
as MInterfacePointer
21.
The documentation tells us
that MInterfacePointer
21
are just a way to described a varying size array containing
an OBJREF
22,
probably for NDR
encoding. OBJREF
22
are a well-known structure for people working on COM/DCOM and have been used and described by James Forshaw
in Windows Exploitation Tricks: Relaying DCOM Authentication6
for example.
But what contains our OBJREF
in our case?
# Call to IPrivActivationPropertiesOut.GetMarshalledResults
>>> propout_as_priv.GetMarshalledResults(nb_interface, iids, results, interfaces)
0
# Explore the interface
>>> interfaces[0].contents.objref.flags
1
>>> interfaces[0].contents.objref.flags == gdef.OBJREF_STANDARD
True
>>> interfaces[0].contents.objref.std
<windows.generated_def.winstructs.tagSTDOBJREF object at 0x000002B3C0723E50>
>>> windows.utils.sprint(interfaces[0].contents.objref.std)
struct.flags -> 0x0
struct.cPublicRefs -> 0x5
struct.oxid -> 0x6a74afc5319c3f21
struct.oid -> 0x9883dc2f214bf1a4
struct.ipid -> <GUID "00001006-4A14-4DF4-173F-4799F71C9689">
What we retrieve is
an IPID
12
for our interface. It represents the interface in the remote COM server and is used by the client to
make ORPC Calls11.
This is the missing piece that will allow us to perform a direct call to our object.
Shopping List:
- ALPC Endpoint
- IPID
- ORPC capabilities
2.5 LORPC
Armed with the following elements, we have most of the tools and information needed to directly talk
to IExaDemo_server_64.exe
:
- An ALPC Port of
IExaDemo_server_64.exe
- An IPID for
IExaDemo
- A lot of documentation to read from MS-DCOM4
- Some background on Local RPC & ALPC
Most of the information needed to transform an RPC client to a ORPC one is found in the MS-DCOM: ORPC Calls11 & MS-DCOM: ORPC Invocations23 pages.
As seen before, we can connect to the ALPC Port and bind to the IID
we want like a normal Local RPC call.
The only not documented things to add are:
- The
RPC_HEADER.Flag
must have the value1
setup to make it a RPC call - The
IPID
must be filled as theRPC_HEADER
last value, which I namedALPC_RPC_CALL.orpc_ipid
inPythonForWindows
- The
ORPCTHIS
must have itsflags
set toORPCF_LOCAL(1)
andversion
to(5, 7)
- In our case of Local ORPC, another structure
combase!_LOCALTHIS
is present between theORPCTHIS
and method arguments- The only required field are:
LOCALTHIS.callTraceActivity
with a random GUIDLOCALTHIS.dwClientThread
- The only required field are:
- NDR marshaling of the parameters does not change
The improvements made
to PythonForWindows LRPC client
can be
found here.
Note that the layout of combase!_LOCALTHIS
is also subject to multiple changes accross versions. Currently only the last version of the structure is present in PythonForWindows
Based on that we can write the following code to ask our IExaDemo
to add 0x41414141 + 0x01010101
:
>>> client = windows.rpc.RPCClient(target_alpc_server)
>>> iid = client.bind("45786100-1111-2222-3333-445500000001", (0, 0))
>>> ipid = interfaces[0][0].objref.std.ipid
>>> data = client.call(iid, 3, b"\x41\x41\x41\x41\x01\x01\x01\x01", ipid=ipid)
>>> data
b'\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00BBBB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
The call worked, and we can see we are near the end of the tunnel as the result 0x42424242
AKA BBBB
can be seen in
the response buffer.
In the meantime, IExaDemo_server_64.exe
printed [IExaDemoImplem_add] 0x41414141 + 0x1010101 = 0x42424242
which is
also a good indicator.
Just like the ORPC Calls, the response contains a ORPCTHAT
and LOCALTHAT
, some light parsing and we have our result:
>>> addstream = ndr.NdrStream(addrep)
>>> orpcthat = gdef.ORPCTHAT32.from_buffer_copy(bytearray(addstream.read(ctypes.sizeof(gdef.ORPCTHAT32))))
>>> localthat = gdef.LOCALTHAT32.from_buffer_copy(bytearray(addstream.read(ctypes.sizeof(gdef.LOCALTHAT32))))
>>> result = addstream.partial_unpack("<I")[0]
>>> print(hex(result))
0x42424242
>>> hex(result)
'0x42424242'
2.6 STUBborn_client.py
The IExaDemo repository include a full demonstration of this technique
in stubborn_client.py
24
The code performs the following actions:
- Instanciate a
IComCatalog
- Retrieve a
IComClassInfo
onCLSID_ExaDemoSrv
- Instanciate an
ActivationPropertiesIn
and query the needed interfaces- Fill the various required information thought the interfaces documented in this article
- call
IPrivActivationPropertiesIn.DelegateCreateInstance()
to retrieve anActivationPropertiesOut
- Query the difference interfaces needed for the information extraction
- Use
IScmReplyInfo.GetResolverInfo()
to retrieve aPRIV_RESOLVER_INFO
- Extract the COM Server ALPC endpoint from this structure
- Use
IPrivActivationPropertiesOut.GetMarshalledResults()
to retrieve our interfaceIPID
- Use
PythonForWindows
ORPC client to callIExaDemo.add(0x41414141, 0x01010101)
- Parse the results and check that the out parameters is equals to
0x42424242
- Verify that a
iexademo_proxy
DLL was never loaded in the process
With our goal in mind, we can also check that our 32-bit python (where the proxy DLL is not registered) can successfully call our interface.
# Real COM client.py still does not work :(
PS COM_IExaDemo> py -3.11-32 .\client.py
CreateInstance 45786100-4343-4343-4343-434343434343
OK: Got an IUnknown: <IUnknown at 0x20d2440>
QueryInterface: IExaDemo.IID: 45786100-1111-2222-3333-445500000001
Traceback (most recent call last):
File "client.py", line 34, in <module>
iexademo = iunk.query(IExaDemo)
^^^^^^^^^^^^^^^^^^^^
File "pythonforwindows\windows\generated_def\interfaces.py", line 62, in query
self.QueryInterface(interface_iid, target)
File "rojets\pythonforwindows\windows\generated_def\interfaces.py", line 28, in _default_errcheck
ctypes._check_HRESULT(result)
OSError: [WinError -2147221164] Class not registered
# Stubborn client successfuly makes the call!
PS COM_IExaDemo> py -3.11-32 .\stubborn_client.py
Addition is OK : 0x42424242 !
Proxy DLL was never loaded !
With this, we have everything we need to target random DCOM servers with a custom ORPC Client!
3 Going further?
To complete the “COM tour”, I also explored some more well-known mechanism with this new method.
3.1 GetClassObject
If you want to interact with the IClassFactory.GetClassObject()
method and manually
implement CreateInstance
25,
the IPrivActivationPropertiesIn
also implements DelegateGetClassObject()
. In this case you can ask for
an IID_IClassFactory
and retrieve the ALPC Endpoint & IPID
as done previously.
You can use then use the ORPC client to call IClassFactory.CreateInstance()
manually and parse the results. Contrary to
the real COM function, the remote version of IClassFactory.CreateInstance()
only takes an IID as parameters.
The response will contain a MInterfacePointer
with your newly created object!
remfactory = client.bind(gdef.IClassFactory.IID, (0, 0))
# 3 -> CreateInstance
params = bytearray(IID_IExaDemo) # The remote version of CreateInstance() only take an IID
createi_resp = client.call(remfactory, 3, params, ipid=ifactory_objref.std.ipid)
createi_stream = ndr.NdrStream(createi_resp)
xorpcthat = gdef.ORPCTHAT32.from_buffer_copy(createi_stream.read(ctypes.sizeof(gdef.ORPCTHAT32)))
xlocalthat = gdef.LOCALTHAT32.from_buffer_copy(createi_stream.read(ctypes.sizeof(gdef.LOCALTHAT32)))
createi_stream.read(4) # Manual NDR Parsing: UNIQU
createi_stream.read(4) # Manual NDR Parsing: Conformant size
tmpbuffer = ctypes.create_string_buffer(createi_stream.data)
mifptr = gdef.MInterfacePointer.from_buffer(tmpbuffer) # Parse tmpbuffer as a MInterfacePointer and extract the objref
obj_ipid = mifptr.objref.std.ipid
3.2 RemQueryInterface
You can also make a call to QueryInterface
on remote interface. For this, the normal ORPC call with a method 0
cannot be used.
The IRemUnknown
26
interface
and RemQueryInterface
27
function must be used:
- The
IPID
for this interface can be found in theIScmReplyInfo
:PRIV_RESOLVER_INFO.OxidInfo.ipidRemUnknown
- This function takes the IPID you want to call
QueryInterface
on and a list ofIID
to query - The response is
a
REMQIRESULT
28 which contains aSTDOBJREF
Examples with very bad NDR parsing code:
# iunk_ipid is a IPID on a IUnknown of our ExaDemoSrv Object.
remunk = client.bind(gdef.IRemUnknown.IID, (0, 0))
target_id = target_id = IID_IExaDemo
params = bytearray(iunk_ipid)[:] + struct.pack("<I", 12) + struct.pack("<I", 1) + struct.pack("<I", 1) + bytearray(target_id)
rem_queryi_response = client.call(remunk, 3, params, ipid=ipidRemUnknown) # Works !
# Parse reponse to RemoteQueryInterface
stream = ndr.NdrStream(rem_queryi_response)
orpcthat = gdef.ORPCTHAT32.from_buffer_copy(stream.read(ctypes.sizeof(gdef.ORPCTHAT32)))
localthat = gdef.LOCALTHAT32.from_buffer_copy(stream.read(ctypes.sizeof(gdef.LOCALTHAT32)))
sream = stream.read(8)
assert sream == b"\x00\x00\x02\x00\x01\x00\x00\x00", repr(sream)
reminterface = gdef.REMQIRESULT.from_buffer_copy(stream.data)
new_ipid = reminterface.std.ipid
This code also shows some nice potential for further investigations, as a
call client.call(remunk, 3, params, ipid=ipid)
with another IPID (like iunk_ipid
) will crash the server with a NULL
DEREF ;)
4 Conclusion
By exploring combase.dll
, understanding the internal COM classes it exposes and playing with them we found a way to
access some information COM normally abstract and try to hide from us. With this information and
the MS-DCOM open specification4
we were able to instantiate a COM object and interact with remote COM interface while bypassing parts of the COM runtime
logic.
This code has been tested on a Windows 10.0.22631.4317
, a POC have been adapted on Windows 7 (6.1.7601.0
) by using _PRIV_RESOLVER_INFO_LEGACY
although changes in the layout of the RPC-CALL buffer (LOCALTHIS
) were also necessary.
This technique and the capabilities it offers may open new avenues in the exploration of Windows Internals to better understand things, develop new tools and find fun bugs.
For ExaTrack, it will allow us to improve our forensic collection agent and give us more chance to catch the bad guys on Windows ;)
I will gladly accept any feedback you have on the article and the code shared in https://github.com/ExaTrack/COM_IExaDemo and https://github.com/hakril/PythonForWindows. If you have any question or remarks, feel free to contact us !
Footnotes & references
-
https://learn.microsoft.com/en-us/windows/win32/com/com-technical-overview ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/4a893f3d-bd29-48cd-9f43-d9777a4415b0 ↩︎ ↩︎ ↩︎
-
https://www.cyber.airbus.com/the-oxid-resolver-part-1-remote-enumeration-of-network-interfaces-without-any-authentication/ ↩︎
-
https://googleprojectzero.blogspot.com/2021/10/windows-exploitation-tricks-relaying.html ↩︎ ↩︎
-
https://github.com/googleprojectzero/sandbox-attacksurface-analysis-tools ↩︎
-
https://hakril.net/slides/A_view_into_ALPC_RPC_pacsec_2017.pdf ↩︎
-
https://github.com/hakril/PythonForWindows?tab=readme-ov-file#alpc-rpc ↩︎ ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/db1d5ce1-a783-4f3d-854c-dc44308e78fb ↩︎ ↩︎ ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/ba4c4d80-ef81-49b4-848f-9714d72b5c01#gt_74540339-daab-46ea-a8f9-fe8fca3b150c ↩︎ ↩︎
-
https://github.com/ionescu007/hazmat5/blob/main/lclor.idl#L186 ↩︎
-
https://gist.github.com/enigma0x3/2e549345e7f0ac88fad130e2444bb702#file-rpc_dump_rs5-txt-L1107 ↩︎ ↩︎
-
https://github.com/samba-team/samba/blob/master/librpc/idl/dcom.idl#L133 ↩︎
-
https://github.com/tongzx/nt5src/blob/daad8a087a4e75422ec96b7911f1df4669989611/Source/XPSP1/NT/com/ole32/idl/internal/objsrv.idl#L81 ↩︎
-
: https://cicada-8.medium.com/process-injection-is-dead-long-live-ihxhelppaneserver-af8f20431b5d ↩︎
-
https://github.com/antonioCoco/RemotePotato0/blob/main/RemotePotato0.cpp#L268 ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/c5917c4f-aaf5-46de-8667-bad7e495abf9 ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/8ad7d21d-5c34-4649-9bc7-5be6fe568245 ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/21781a97-cb45-4655-82b0-02c4a1584603 ↩︎ ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/fe6c5e46-adf8-4e34-a8de-3f756c875f31 ↩︎ ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/98c08086-d94a-443c-b7c6-167e82652885 ↩︎
-
https://github.com/ExaTrack/COM_IExaDemo/blob/master/stubborn_client.py ↩︎
-
https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance#remarks ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/7f621d16-8448-4f9a-9567-793845db2bc7 ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/11fd5e3a-f5ef-41cc-b943-45217efdb054 ↩︎
-
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dcom/1d6a8a54-b115-4148-815a-af0258931948 ↩︎