site logo

Marico's space

我的开源安全扫描工具被 Windows Defender 标记为 Trojan

编程技术 2026-06-04 11:27:58 8

最近折腾了一个 MCP 服务器的安全扫描工具。结果上线第一周,Windows Defender 就直接把它当成了木马,默默从用户机器上删掉了。

它根本不是恶意软件。VirusTotal 上 71 个杀毒引擎里只有 1 个报毒——就微软一家。

这篇文章说说到底发生了什么、为什么 Go 编译的二进制文件特别容易踩这个坑,以及我是怎么解决的,免得大家跟我一样栽跟头。

产品背景

MCPSense 是一个 Go 语言写的命令行工具,用来扫描 MCP(Model Context Protocol,模型上下文协议)服务器配置中的安全漏洞。MCP 是 Claude、ChatGPT、Cursor 和 VS Code 连接 AI 代理到外部工具的协议。这些工具的配置文件里经常包含 shell 命令、API 密钥、包引用这些东西,很容易被攻击利用。

我做这个工具是为了检测命令注入、凭证泄露、提示词注入、路径穿越等 23 种漏洞类型。总共 27 项检查。用 Go 写的,用 GoReleaser 编译发布,通过 GitHub Releases 分发。

第一周还挺顺利的。40 多个独立克隆量,Reddit 上也有几个帖子在推。大家还真开始用了。

然后我在干净的 Windows 机器上测试了一下

我在一个全新的 Windows 笔记本上跑了自己的安装脚本。脚本从 GitHub Releases 下载预编译好的二进制文件,放到指定目录,然后显示:

Installed successfully.

但二进制文件其实没在那儿。Windows Defender 把它标记成了 Trojan:Win32/Bearfoos.B!ml,隔离,然后删掉了。全程静默的。我的安装脚本只检查了下载是否成功,没检查文件 3 秒后是否还在。所以它愉快地报了个"安装成功",而 Defender 正在后台把东西清干净。

那一刻我才意识到,那 40 多个早期用户里肯定有 Windows 用户,他们肯定也遇到了同样的问题。但没有一个人提 issue,没有一个人留言。他们看到安全工具弹出"检测到 Trojan",就直接走了。也没法怪他们。

为什么 Go 编译的二进制文件会这样

这不是 MCPSense 的问题,是 Go 在 Windows 上的问题。Go 的 GitHub 上有一个Issue,开发者们反映这个问题已经好几年了。

问题出在这儿。Defender 用机器学习模型配合基于签名的检测来识别恶意软件。ML 模型看的是二进制文件的特征:文件结构、导入表、熵值模式、编译产物。Go 编译出来的二进制文件有一种独特的签名,因为 Go 编译器的处理方式很特殊。它把什么东西都静态链接进一个单独的大文件,包含 Go 运行时,有独特的导入导出结构。

问题在于,这种模式恰好跟某些恶意软件加壳工具生成的特征有重叠。ML 模型看到一个未签名的可执行文件,具有这些特征,还是从网上下载的(这给了它"Web 标记",Mark of the Web),就会触发启发式检测。

关键细节:本地编译的二进制文件不会触发这个。如果你在自己的机器上跑 go install,Defender 不管你。文件内容完全一样,但没有 Mark of the Web,因为它不是下载的。这就是全部区别。同样的字节,不同的元数据,完全不同的 Defender 反应。

Hugo、Terraform、Syncthing 还有其他很多 Go 项目都遇到过这个。这不是新问题。只是我到用户已经被影响了才知道。

VirusTotal 的证据

我把 mcpsense.exe 上传到了 VirusTotal。结果是这样的:

71 个安全引擎里只有 1 个报毒。 就微软一家。卡巴斯基、Norton、Bitdefender、ESET、Malwarebytes、CrowdStrike,还有其他 64 个,都说没问题。

完整 VirusTotal 结果

当 70 个独立安全引擎都认为你的二进制文件是干净的时候,唯一报毒的那个还有已知的 Go 二进制文件误报历史,那基本可以确定它真的不是恶意软件。

我是怎么处理的

我没有只是提交一个误报工单然后干等着。那能最终修复检测结果,但修复不了当前的用户体验。所以我从多个角度一起处理。

1. 发布到 npm

这是最大的变化。MCPSense 现在可以通过这种方式安装:

npm install -g mcpsense

在 Linux 和 macOS 上,npm 包会下载预编译好的 Go 二进制文件。快,不需要装 Go。

在 Windows 上,它检测平台,然后从源码编译而不是下载二进制文件,跑的是 go install。这样生成的可执行文件是本地编译的,Defender 不会标记。用户永远看不到那个 Trojan 警告。

为什么选 npm?因为所有做 MCP 服务器开发的开发者都有 Node.js。他们在 MCP 配置里跑 npx 命令。Node 是最大公约数。

2. 重写了 Windows 安装脚本

旧的 install.ps1 就是下载一个二进制文件然后结束。如果 Defender 把二进制文件吃了,脚本报"成功"然后关掉终端。

新版本按顺序尝试三种安装方式:

  1. npm install -g mcpsense(如果 npm 可用)
  2. go install(如果 Go 可用)
  3. 直接下载二进制文件(兜底)

二进制文件下载完成后,会等 3 秒检查文件是否还在。如果 Defender 删掉了,脚本会告诉用户到底发生了什么,并显示手动操作的步骤。而且关键的是,它永远不会在没显示"按回车关闭"提示的情况下关掉终端窗口。用户现在真的能看到错误信息了。

3. 提交给微软做误报审核

微软有个提交门户,开发者可以在那里报告误报。我提交了二进制文件,附上了 VirusTotal 的证据和源代码链接。他们通常会在几个工作日内回复,从后续的病毒定义更新里移除这个检测。

4. 申请代码签名

SignPath Foundation 为开源项目提供免费的代码签名证书。审批通过后,GoReleaser 会生成带签名的 Windows 二进制文件,Defender 就信任了。这是长期解决方案,但需要几周时间处理。

给 Go(和 Rust)开发者发布到 Windows 的教训

如果你用 Go 或 Rust 做命令行工具,要发布给 Windows 用户,以下是我希望自己在发布前就知道的事情。

在干净的 Windows 机器上测试。 不是你那个已经白名单了一堆东西的开发机,是全新安装、默认 Defender 设置的那种。发布之前做,不是等 40 个人已经试过你的工具之后才做。

永远不要把二进制文件下载作为唯一的 Windows 安装路径。 要有源码编译的兜底方案。不管是通过 npm 包装、提供 go install 命令,还是别的什么,你需要一条不涉及下载预编译 .exe 的路径。

安装脚本需要在安装后验证二进制文件是否还在。 不是"下载成功了没",而是"3 秒后文件还在不在"。Defender 快但不是即时的。

永远不要让安装脚本失败时关掉终端。 通过 irm ... | iex 调用的 PowerShell 脚本会在退出时关闭窗口。如果你的脚本失败了然后退出了,用户什么都看不到。始终以 Read-Host "Press Enter to close" 结尾。

尽早申请代码签名。 SignPath Foundation 对开源是免费的。申请需要时间。项目启动时就启动这个流程,不要等到已经发布了才想起来。

主动提交误报。 不要等用户来抱怨。每次发布都把二进制文件上传到 VirusTotal。任何引擎报毒就立刻提交到对应厂商的误报门户。

讽刺之处

一个安全扫描工具被安全产品标记成恶意软件,这是最好的讽刺方式。但底层的问题是严肃的。Go 是 CLI 工具最流行的语言之一,而 Defender 在每台 Windows 机器上都有。这个摩擦影响了整个 Go 生态触达 Windows 用户的能力。

Go 团队在编译器层面没法解决这个问题,因为这是启发式检测,不是签名匹配。没有特定的字节序列可以改。唯一的真正解决方案是代码签名(需要花钱或者需要向基金会申请)或者根本不发布预编译二进制文件。

在那两个方案落地之前,源码编译是最安全的 Windows 分发路径。

试试 MCPSense

如果你在用 Cursor、Claude Desktop 或 VS Code 的 MCP 服务器,扫描一下你的配置:

npm install -g mcpsense
mcpsense scan ./mcp.json

27 项检查,覆盖安全、规范合规和工具质量。开源,MIT 许可证。

GitHub | npm | Website

原文链接:https://dev.to/fayzkk889/my-open-source-security-scanner-got-flagged-as-a-trojan-by-windows-defender-4bpd