§ CM

AppDomainManager Hijacking Hides in a Signed Binary. The Tell Is One Line of XML That Kills ETW

A Microsoft-signed binary launches from a user-writable directory, the .NET runtime reads a config file sitting next to it, and attacker-controlled managed code runs inside that trusted process before the application’s own Main() ever executes. No CreateRemoteThread, no reflective loader, no shellcode in a private RX region for your EDR’s memory scanner to trip over. The execution path is a documented .NET Framework feature working exactly as designed — and it’s a .NET Framework / CLR 4.x behavior specifically; modern .NET (6+) dropped the multi-AppDomain model and these config knobs, so a fully-migrated shop has a smaller surface (though Windows still ships plenty of Framework binaries, as below). That’s the uncomfortable part of AppDomainManager hijacking (MITRE T1574.014): there’s no vulnerability to patch, and the host process stays Microsoft-signed the whole way through.

It moved from red-team curiosity to documented APT tradecraft over the last couple of years. China-nexus Earth Baxia leaned on it against government and telecom targets across APAC after popping GeoServer. More recently, Unit 42 tied it to the Iran-nexus group it tracks as Screening Serpens (overlapping with UNC1549 / Smoke Sandstorm), running recruitment-themed lures into aerospace, defense manufacturing, and telecom. Same primitive, two unrelated intrusion sets, both reaching for it because it sails past the controls most shops actually have turned on.

And here’s the detail that should change how you hunt it: the config file that redirects assembly loading can, in the same breath, tell the CLR to stop emitting ETW. The technique doesn’t just hide the payload. It blinds the sensor you’d use to find the payload.

The mechanism, and why the signed binary doesn’t help you

When a .NET Framework process starts, the CLR checks for an application domain manager before it hands control to the program. You can point it at a custom managed assembly through an app.exe.config file dropped alongside the executable, or through the APPDOMAIN_MANAGER_ASM and APPDOMAIN_MANAGER_TYPE environment variables. Set those, and your AppDomainManager type’s constructor runs first, inside the host process, with the host’s identity and signature.

The config-file variant is the one you’ll see in intrusions, because it’s self-contained and survives a reboot if the binary gets launched by a scheduled task. The relevant elements are appDomainManagerAssembly and appDomainManagerType under <runtime>. That alone is enough to get code execution in a trusted context. Screening Serpens went further and stacked evasion directives into the same file (condensed for illustration — publisherPolicy actually nests under assemblyBinding/dependentAssembly, not directly under <runtime>):

<runtime>
  <appDomainManagerAssembly value="Evil, Version=1.0.0.0, ..." />
  <appDomainManagerType value="Evil.DomainManager" />
  <etwEnable enabled="false" />
  <bypassTrustedAppStrongNames enabled="true" />
  <publisherPolicy apply="no" />
</runtime>

Read etwEnable enabled="false" and understand what it does. Microsoft-Windows-DotNETRuntime is the ETW provider that surfaces JIT events, assembly loads, exceptions — the managed-code telemetry that Defender for Endpoint, CrowdStrike, and the rest consume to reason about what a .NET process is doing. Turn it off in the config and that process goes dark to that data source from the instant it starts. The EDR still sees the kernel-level stuff (process create, image load, network), but the rich .NET layer it would use to say “this signed binary is doing something weird” is gone. One caveat worth stating so you don’t overtrust the gap in the other direction: a sensor that instruments managed code through the CLR Profiling API or AMSI rather than ETW isn’t governed by this switch, so “kills ETW” isn’t “kills all managed visibility.” ETW is the common path, though, and for most stacks this is a real blind spot. The other two lines don’t “allow” the unsigned assembly to load — in a full-trust desktop process that happens anyway — they smooth the binding: bypassTrustedAppStrongNames relaxes strong-name verification — though in a full-trust desktop process that bypass has been the default since .NET 3.5 SP1, so this line usually restates the default rather than opening a new hole — and publisherPolicy apply="no" makes the runtime ignore GAC publisher-policy version redirects, so binding doesn’t throw on the way in.

The remote-loading capability is the part that should worry your network team. The assembly reference can resolve over HTTP or UNC, which means the trusted process reaches out and pulls the payload from outside. On modern .NET Framework (CLR 4.x), loading an assembly from an HTTP URL or other non-Intranet zone is gated behind <loadFromRemoteSources enabled="true">, so expect that directive when the payload lives off-box over HTTP — and add it to your hunt strings. UNC is murkier: since .NET Framework 4.5, assemblies on Local Intranet network shares run in full trust by default and don’t need the directive, so its absence doesn’t clear a share-hosted payload. Either way, that’s a SC boundary problem wearing a SI costume: you’ve got a Microsoft-signed binary making an egress call to fetch executable content, and if your egress monitoring only flags unsigned or unknown processes, it waves this one straight through.

Why your AppLocker/WDAC policy doesn’t catch it

This is the question that comes up first, so deal with it up front. Application control built around “only signed, allowlisted executables run” does nothing here, because the executable is signed and allowlisted. It’s setup.exe, or some renamed Microsoft binary — Screening Serpens used a legitimate Microsoft setup binary renamed to SoftwareLicencing.exe and update.exe. The thing AppLocker is supposed to block is the thing you explicitly allowed. AppLocker does have DLL rule collections that would scope the malicious assembly — but they’re off by default and demand an allow rule for every DLL every allowed app loads, an operational cost Microsoft itself flags, so almost nobody enables them.

WDAC (App Control for Business) can shut this down through its Dynamic Code Security option, which extends policy to the libraries .NET loads from external or remote sources so the malicious assembly itself has to satisfy policy — blocks surface as Code Integrity event ID 3114. In practice almost nobody runs it, because the rule-tuning cost is brutal and one missed binding redirect bricks a line-of-business app. Some highly locked-down enclaves may already enforce it; in a flat commercial AD forest with a thousand .NET LOB apps, you don’t, and you’re not going to stand it up this quarter to close one technique. So the realistic answer is detection, not prevention.

What the detection actually looks like in the SIEM

Splunk ships a relevant rule, “Windows Potential AppDomainManager Hijack Artifacts Creation.” Its logic is worth understanding because it tells you where the easy detection ends. It keys on Sysmon EventID 11 (FileCreate) and fires when a single process drops the trio in one directory: an .exe, a matching .exe.config with the same base name, and at least one .dll, with a minimum of three files, in a set of suspicious paths (C:\Windows\Fonts, Users\Public, Windows\Debug, PerfLogs, the usual writable-but-weird list).

That’s a fine starting point and it’ll catch the lazy version. It will also miss two things that matter. It misses the environment-variable method entirely — no config file is dropped, so there’s no FileCreate trio to correlate. And it misses the case where the attacker stages in a directory you didn’t put on the suspicious-paths list, which, once they’ve watched one defender publish the path list, they will.

The bigger problem is volume and false positives if you loosen it. The instinct is to alert on any *.exe.config creation. Do not. ClickOnce deployments, MSBuild output directories, NuGet restore, and roughly every enterprise .NET installer write .exe.config files constantly, and the overwhelming majority contain nothing but <bindingRedirect> assembly-version plumbing. In a dev-heavy shop, a bare config-creation rule will bury the SOC on day one. Your build agents alone will generate thousands a day.

So the first round of tuning is content, not filename. The discriminator is what’s inside the config. appDomainManagerAssembly and appDomainManagerType are rare in legitimate enterprise software. etwEnable enabled="false" is rarer still — I can’t think of a legitimate production reason to disable runtime ETW in a shipped app’s config, and that single string is about as close to a high-fidelity signature as this technique offers. Hunt the XML.

Sysmon FileCreate doesn’t give you file content, so the content hunt lives in your EDR’s live-query or a scheduled osquery sweep. The shape:

-- scheduled hunt: .config files referencing AppDomainManager or disabling ETW
SELECT path, directory FROM file
WHERE path LIKE 'C:\Users\%\%.config'
   OR path LIKE 'C:\ProgramData\%.config';
-- then grep matched files for: appDomainManagerAssembly, appDomainManagerType, etwEnable, loadFromRemoteSources

Run the grep through your EDR’s file-content capability against user-writable trees on a cadence. Mind the mechanics, though, because they differ by vendor: CrowdStrike’s RTR can search file content directly, while Defender for Endpoint’s advanced hunting surfaces file events, not contents — so there you’ll lean on Live Response to pull the file, or an agent-side scan to read the XML, rather than a Kusto query. Either way the noise here is genuinely low, because you’ve filtered to a handful of strings that don’t show up in <bindingRedirect> boilerplate.

For the environment-variable variant, the signal is APPDOMAIN_MANAGER_ASM or APPDOMAIN_MANAGER_TYPE appearing in a process’s environment block. Those variables have no business existing in a production session. If your EDR captures process environment (CrowdStrike does in places; Defender’s coverage is spottier and you may not get the env block at all, so don’t assume it’s there), alert on the variable name itself. Zero legitimate baseline, near-zero false positives. Don’t forget the persistent path either: those variable names can be planted as machine- or user-scoped environment variables in the registry — HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment or the per-user Environment key — so every newly spawned process inherits them without a config file ever touching disk. Monitor those keys for the APPDOMAIN_MANAGER_* names; it’s both a hunt signal and the persistence mechanism behind the env-var variant.

There’s a subtler hunt that pays off once you’ve baselined: the absence of DotNETRuntime ETW events from a process you know is .NET. If a signed .NET binary executes and your collector logs no managed-runtime ETW for that PID, either ETW got switched off or the collector dropped events. Both are worth a look. This one is hard to operationalize cleanly because ETW event loss under load is real and your time skew between hosts will make the correlation window fuzzy, so treat it as a hunt hypothesis, not a paging alert.

Where the false positives actually come from

Three sources, in rough order of how much they’ll hurt. ClickOnce and other legitimate .NET deployment frameworks that write .exe.config next to a freshly downloaded binary in a user profile — this is the big one (the ClickOnce application cache under %LOCALAPPDATA%\Apps\2.0\ is the canonical noise source, and it’s worth carving out by path), and it’s why the file-creation trio rule needs the staging-path scoping to stay sane. Build and CI directories on developer endpoints and build agents, which produce the exe+config+dll trio thousands of times a day as a matter of course; carve those hosts out by asset group early or you’ll never see signal through the noise. And a long tail of older in-house .NET apps that ship genuinely unusual config files because someone in 2014 needed a binding redirect and a custom assembly resolver — rare, annoying, and the reason your allowlist needs an exception process rather than a hard suppression.

One environment assumption changes the whole calculus: .NET Framework versus modern .NET. The multi-AppDomain model and these config knobs are a .NET Framework (CLR 4.x) thing. .NET Core and .NET 5+ collapsed to a single AppDomain and don’t honor the classic appDomainManager config the same way, so a shop that’s fully migrated to self-contained .NET 8 has a smaller surface. Don’t relax, though. Windows ships with a large population of Framework binaries, and plenty of signed Microsoft and third-party tools on a standard build are still Framework. The technique needs exactly one suitable signed .NET Framework executable in a writable location, and your image has more than one.

Control mapping

Control Why it applies
SI-4 The whole game is detection — file-content hunts for the config markers, env-var monitoring, ETW-gap analysis
SI-7 File integrity monitoring on .exe.config in app and user-writable directories; the modified config is the integrity violation
AU-12 The etwEnable="false" directive is a direct attack on audit-record generation; treat ETW suppression as an AU event, not just an SI one
CM-7 WDAC / App Control Dynamic Code Security is the real preventive control, with the cost caveat above; least-functionality on which signed .NET binaries can run from writable paths
SC-7 Remote assembly resolution over HTTP/UNC from a signed process is an egress-monitoring case your boundary controls probably wave through

The mapping people get wrong is filing this purely under SI and stopping there. The ETW kill is an AU integrity problem. If an adversary can switch off your audit source from a config file before your sensor wakes up, the gap isn’t just “we missed a detection” — it’s that the audit record was never generated. That distinction matters when you’re writing it up for the ISSO, because the remediation for an SI gap (tune the detection) is not the remediation for an AU gap (make audit generation resistant to in-process suppression, which for .NET Framework largely means out-of-band telemetry the target process can’t disable).

Hunt the XML, watch for the variable names, and treat a .NET process that emits no managed-runtime ETW as a question, not a comfort. The signed binary was never the anomaly. The config file is.

Sources