Skip to content

AMSI / antivirus blocks startup: embedded MCPPollingEngine.ps1 flagged as malicious when run via Invoke-Expression #50

Description

@bigminer

Summary

On startup, the module fails to load with an AMSI ("Antimalware Scan Interface") detection. PowerShell's antimalware provider flags the embedded MCPPollingEngine.ps1 script as malicious at execution time, so the polling engine never starts and the MCP server is unusable. No tools are exposed to the client.

This is distinct from #46 (which was WDAC/Device Guard blocking the unsigned PowerShell.MCP.Proxy.exe on disk). Here nothing is quarantined on disk — the block happens in memory, when the embedded .ps1 resource is run through Invoke-Expression.

Error

WARNING: [PowerShell.MCP] Failed to start: At line:1 char:1
+ # MCPPollingEngine.ps1
+ ~~~~~~~~~~~~~~~~~~~~~~
This script contains malicious content and has been blocked by your antivirus software.

Note the caret points at line 1 of the embedded script (# MCPPollingEngine.ps1), and the message is the standard text PowerShell surfaces when AMSI returns AMSI_RESULT_DETECTED for a script buffer.

Root cause analysis

The module ships three PowerShell scripts as embedded managed resources inside PowerShell.MCP.dll:

  • PowerShell.MCP.Resources.MCPPollingEngine.ps1
  • PowerShell.MCP.Resources.MCPCleanup.ps1
  • PowerShell.MCP.Resources.MCPLocationProvider.ps1

At load time these are read via Assembly.GetManifestResourceStream(...) and executed as a dynamic string through Invoke-Expression / PowerShell.AddScript(...) (the same Invoke-Expression $cleanupScript pattern is visible in PowerShell.MCP.psm1's OnRemove handler, and the matching Failed to start: warning string and LoadScript/AddScript symbols are present in the DLL).

When a script is executed from a dynamically-built string, AMSI scans the in-memory script buffer before execution. A persistent polling loop that reads instructions from an external channel (a named pipe) and executes arbitrary strings is exactly the behavioral shape AV/EDR heuristics associate with fileless PowerShell backdoors/downloaders — so the AMSI provider (Microsoft Defender or a third-party AV's AMSI provider) returns a detection and PowerShell refuses to run the buffer.

Evidence this is an in-memory AMSI block, not a disk detection

  • There is no MCPPollingEngine.ps1 file on disk in the installed module directory — it only exists as an embedded resource string inside the DLL.
  • Get-MpThreatDetection shows no entry referencing the module — consistent with an AMSI execution-time block (which is not necessarily written to file-based threat history) rather than a quarantined file.
  • The parse/caret location (At line:1 char:1) is the first line of the embedded script content, not a path on disk.

Impact

  • The MCP server does not start at all — there is no degraded/managed-only fallback; the polling engine is the core loop.
  • This will affect anyone on a machine with an aggressive AV/EDR AMSI provider, which is common on enterprise-managed Windows workstations. It can also appear intermittently as AV definition updates change heuristics.

Environment

  • OS: Windows 11 Pro, build 10.0.26200
  • PowerShell: 7.6.0 (Core)
  • PowerShell.MCP: 1.10.0 (...\Documents\PowerShell\Modules\PowerShell.MCP\1.10.0)
  • MCP client: Claude Code

Suggested directions

These are options for maintainers to weigh, not a prescription:

  1. Avoid Invoke-Expression of dynamic strings where possible. AMSI is far more likely to flag iex/AddScript(<string>) than a script file dot-sourced or a function exported from the (signed) assembly. Loading the polling engine as compiled cmdlets/methods in the DLL, or dot-sourcing a real .ps1 shipped on disk, sidesteps the dynamic-string heuristic.
  2. Authenticode-sign the embedded scripts / the script content. AMSI honors trust for signed content; a signed script buffer is much less likely to be heuristically blocked. (This also complements the binary-signing request in PowerShell.MCP.Proxy.exe is unsigned — blocked by Windows Defender Application Control (WDAC/Device Guard) #46.)
  3. Reduce the "backdoor-shaped" signature of the polling loop if feasible (naming, structure), since behavioral heuristics key off the read-external-input-then-execute pattern.
  4. Document a supported workaround. Today the only mitigations are user-side and fragile: an AV path/process exclusion (doesn't help for in-memory AMSI on some providers), or an IT allowlist. A documented, supported path would help enterprise users.

Happy to provide additional diagnostics (AMSI event logs, Get-MpThreatDetection output, DLL resource dump) if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions