Ver Fonte

复查 2.99.0.1:setup 黑名单没堵住静默更新

- 2.99.0.1 只给 setup 加了 12 键黑名单
- slientupdate 仍是 0 click,HasUpdatePermission 恒 true
MiRinChan há 19 horas atrás
pai
commit
91754f10a6

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+# 本地归档:旧分析、旧样本、旧报告
+/archive/
+
+# PCL 本体:发布包与解包后的 exe
+/PCL*.zip
+/Plain Craft Launcher 2.exe

+ 2 - 0
README.md

@@ -1,6 +1,8 @@
 1. [向龙腾猫跃的道歉公告](#my-apology)
 2. [PCL PoC 与研究报告](#report)
 
+[2026-10-25 更新 2.99.0.1 删掉了吗?没有——见复查报告](analysis/2.99.0.1/README.md)
+
 # 重申为什么我的观点倾向于 PCL 有一个后门。
 
 鉴于所谓其他软件的撑腰“开团”,我想在这里作一些评论。

+ 273 - 0
analysis/2.99.0.1/README.md

@@ -0,0 +1,273 @@
+# PCL 2.99.0.1 复查(2026-10-25)
+
+> 写:DeepSeek Harness aka deepseek-flash
+
+## 结论
+
+没删。
+
+2.99.0.1 跟远程执行有关的改动只有一个:给 `notice.json` 的 `setup` 加了个 12 个键的
+黑名单,把 `LaunchAdvanceRun` 这些直接堵了。`slientupdate` 那条静默更新链还是老样子,
+0 click:
+
+- `HasUpdatePermission => true`(`ModSecret.cs:402`)
+- `slientupdate` 直接 `UpdateStart(true)`(1513–1530)
+- 下载地址来自未签名的 `ServerConfig`(1017–1037)
+- 没有哈希,没有签名,zip 解压覆盖 `PCL\Plain Craft Launcher 2.exe`(198–211)
+- 静默时只置 `IsUpdateWaitingRestart`,退出时 `UpdateRestart(false)` 启动新 exe
+  (`FormMain.cs:1968`、`ModSecret.cs:1084`)
+
+按报告原来的前提——能控制公告 CDN 的内容,或者做 DNS/TLS 中间人——还是能零点击换掉
+启动器本体。
+
+| | 2.99.0.0 | 2.99.0.1 |
+|---|---|---|
+| 启动拉 `notice.cfg` / `notice.json` / `ServerConfig.json` | 有 | 有 |
+| 配置内容签名 / 更新包哈希 | 无 | 无 |
+| `setup` 写设置 | 任意键 | 12 键黑名单 |
+| `setup.LaunchAdvanceRun` → `cmd /c` | 可写 | 被拦 |
+| `slientupdate` → 任意 zip → 替换自身 | 有 | 有 |
+| `HasUpdatePermission` | `=> true` | `=> true` |
+
+## 样本
+
+用户给的 `PCL 正式版 2.99.0.1.zip`,SHA-256
+`4d239a62ed66fb9ebcf4633fd881eb098e9a7e4f296f0e414b58aa2f1f7f7830`。里面一个
+`Plain Craft Launcher 2.exe`,6,013,952 字节,SHA-256
+`0db34e19d9eeb6d496d20fabe786ec16bbd36ba6b0ccdc8d29cb7e909c114397`。PE 的
+`Security Directory` 还是 0,没签名。
+
+官方仓库 `Hex-Dragon/PCL2` 的发布提交是 `93ede5d`(`2.99.0.1 正式版`)。这个提交里的
+`最新正式版.zip` 和用户给的 zip 逐字节一样,解出来的 exe 也一样。不是二次打包。
+
+## 反编译对了一下
+
+两个 exe 都用 `ilspycmd 9.1 -p` 反编译,逐文件 diff:
+
+```text
+.cs     变了 19 / 164
+.baml   变了 51
+其它    2 个(PCL.Resources.resx、Plain Craft Launcher 2.csproj)
+```
+
+19 个 `.cs` 里大部分是版本号 413 → 415、更新日志、删 MC 下载入口和 UI。跟远程执行有关
+的只有 `ModSecret.cs` 的 `setup` 黑名单。这次只是静态 diff,没在 Windows VM 上重跑 PoC。
+
+## 2.99.0.1 堵了什么
+
+```csharp
+// PCL/ModSecret.cs:1495-1510
+case "setup":
+{
+    JObject val5 = (JObject)val4.Value;
+    string[] source = new string[12]
+    {
+        "LaunchAdvanceRun", "LaunchAdvanceJvm", "LaunchAdvanceGame", "UiCustomNet",
+        "HintCustomCommand", "LaunchArgumentJavaAll", "LaunchArgumentJavaSelect",
+        "SystemSystemCache", "LaunchFolderSelect", "LaunchFolders",
+        "ToolDownloadCert", "CacheAuthServerServer"
+    };
+    foreach (KeyValuePair<string, JToken> item4 in val5)
+    {
+        if (source.Contains(item4.Key))
+        {
+            throw new Exception("被禁止的设置项:" + item4.Key);
+        }
+        Settings.Set(item4.Key, item4.Value.ToString());
+    }
+    break;
+}
+```
+
+几个细节:
+
+- 键名精确匹配。碰到黑名单里的键就抛,循环中断,后面的键不写了;前面已经写进去的键
+  不回滚。
+- 异常被每个 action 外面的 `catch` 吃掉,日志里只留一句“执行 PCL 服务器公告动作失败”。
+- `Settings.Entries` 是 `Dictionary<string, Setting>`(`ToDictionary(e => e.Key)`,默认
+  ordinal),大小写变体在 `Settings.Set` 里也找不到,绕不过去。
+- 黑名单只有 12 个键,`setup` 还是能写别的键。这是黑名单,不是把 `setup` 关了。
+
+2.99.0.0 报告里的方法一,`LaunchAdvanceRun` 就在黑名单第一位,所以在 2.99.0.1 上没了。
+
+## 没动的:slientupdate
+
+从拉到执行,链子是这样的:
+
+1. 启动拉 `notice.cfg`:
+
+   ```csharp
+   // PCL/ModSecret.cs:1324-1325
+   text3 = ModNet.NetRequestByClientRetry(
+       "https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}");
+   num3 = (int)Math.Round(ModBase.Val(StringExtensions.Split(text3, "|", false)[0]));
+   ```
+
+2. `notice.cfg` 第五段是配置版本,比本地大就重下 `ServerConfig.json`:
+
+   ```csharp
+   // PCL/ModSecret.cs:1378-1383
+   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, (Encoding)null, (Type)null);
+   }
+   ```
+
+3. 拉 `notice.json`,然后按里面的 `actions` 干活:
+
+   ```csharp
+   // PCL/ModSecret.cs:1403-1404
+   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, (Encoding)null);
+   ```
+
+4. `slientupdate` 不用确认,因为权限判断写死成 `true`:
+
+   ```csharp
+   // PCL/ModSecret.cs:402
+   private static bool HasUpdatePermission => true;
+
+   // PCL/ModSecret.cs:1513-1530
+   case "update":
+   case "slientupdate":
+   {
+       _ = (JObject)val4.Value;
+       bool flag3 = Operators.CompareString(val4.Name, "slientupdate", false) == 0;
+       if (!HasUpdatePermission)
+       {
+           if (!flag3) { InputPotatoCode(IsUpdating: true); }
+           if (!HasUpdatePermission) { break; }
+       }
+       UpdateStart(flag3);
+       break;
+   }
+   ```
+
+5. `UpdateStart` 从 `ServerConfig` 拿下载地址,扩展名直接从 URL 里取:
+
+   ```csharp
+   // PCL/ModSecret.cs:1017-1037
+   JObject val = (JObject)ServerConfig["Update"][(object)ModBase.BuildTypes.Release.ToString()];
+   if (val.ContainsKey("PatchUrls") && StringExtensions.StartsWithF(UpdateMark, "Empty", false))
+       _0024VB_0024Local_Urls = val["PatchUrls"].ToObject<List<string>>();
+   else if (val.ContainsKey("DownloadUrls"))
+       _0024VB_0024Local_Urls = val["DownloadUrls"].ToObject<List<string>>();
+
+   _0024VB_0024Local_Extension = "." + PathUtils.GetExtension(
+       StringExtensions.BeforeLast(_0024VB_0024Local_Urls.First(), "{CDN}", false));
+   _0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}",
+       ModMain.RequestTaskTempFolder(), "2.99.0.1", 50, _0024VB_0024Local_Extension);
+
+   Task.Output = new ModNet.NetFile[1]
+   {
+       new ModNet.NetFile(_0024VB_0024Local_Urls, _0024VB_0024Local_LocalPath)
+   }.ToList();
+   ```
+
+   `ModNet.NetFile` 只有 URL 和本地路径,没有哈希,没有签名。
+
+6. 解压覆盖,静默时只记一个“等重启”:
+
+   ```csharp
+   // PCL/ModSecret.cs:198-211
+   if (Operators.CompareString(_0024VB_0024Local_Extension, ".zip", false) == 0)
+   {
+       FileUtils.ExtractToDirectory(_0024VB_0024Local_LocalPath, Paths.Base + "PCL\\",
+           default(CancellationToken), (ProgressProvider)null);
+   }
+   else
+   {
+       MsDelta.Apply(ModBase.PathExe, _0024VB_0024Local_LocalPath,
+           $"{Paths.Base}PCL\\Plain Craft Launcher 2.exe");
+   }
+   if (_0024STATIC_0024UpdateStart_00240212151280E91E_0024IsSilentUpdate)
+   {
+       IsUpdateWaitingRestart = true;
+       return;
+   }
+   ```
+
+7. 用户正常退出,启动覆盖后的 exe:
+
+   ```csharp
+   // PCL/FormMain.cs:1968
+   if (ModSecret.IsUpdateWaitingRestart)
+   {
+       ModSecret.UpdateRestart(TriggerRestartAndByEnd: false);
+   }
+
+   // PCL/ModSecret.cs:1084-1097
+   public static void UpdateRestart(bool TriggerRestartAndByEnd)
+   {
+       try
+       {
+           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);
+           }
+       }
+       catch (Win32Exception ex)
+       {
+           // 被拦截时提示用户手动替换
+       }
+   }
+   ```
+
+全程没有 Authenticode、SHA-256、内容签名。`{CDN}` 那个 `SecretCdnSign` 还是硬编码密钥
+`PCL2Server` 的 MD5 查询串(`ModSecret.cs:428`),只管 URL,不管响应内容。
+
+## 对现有 PoC
+
+- `update` 模式还能用;
+- `launch` 模式会被黑名单拦。
+
+## 复现
+
+```bash
+git clone https://github.com/Hex-Dragon/PCL2.git upstream
+cd upstream
+
+# 2.99.0.0
+git checkout 6b9a50d
+unzip -q 最新正式版.zip -d ../2990
+ilspycmd -p -o ../decompiled-2990 ../2990/"Plain Craft Launcher 2.exe"
+
+# 2.99.0.1
+git checkout 93ede5d
+unzip -q 最新正式版.zip -d ../2991
+ilspycmd -p -o ../decompiled-2991 ../2991/"Plain Craft Launcher 2.exe"
+
+cd ..
+diff -rq decompiled-2990 decompiled-2991 | grep '\.cs '
+diff -u decompiled-2990/PCL/ModSecret.cs decompiled-2991/PCL/ModSecret.cs
+```
+
+## 文件
+
+```text
+analysis/2.99.0.1/
+├── README.md                       本文件
+├── diff/
+│   ├── ModSecret.diff              ModSecret.cs 的完整 unified diff
+│   ├── other-modules.diff          其余 18 个 .cs 的 diff
+│   ├── changed-files.txt           72 个差异文件清单
+│   └── summary.txt                 差异计数
+└── evidence/
+    ├── hashes.txt                  zip / exe / 官方包哈希
+    ├── provenance.md               来源与比对过程
+    └── snippets.md                 2.99.0.1 关键代码片段
+```

+ 103 - 0
analysis/2.99.0.1/diff/ModSecret.diff

@@ -0,0 +1,103 @@
+--- a/PCL/ModSecret.cs (2.99.0.0)
++++ b/PCL/ModSecret.cs (2.99.0.1)
+@@ -172,7 +172,7 @@
+ 			}
+ 			_0024VB_0024Local_Urls = _0024VB_0024Local_Urls.Select([SpecialName] (string u) => ModMain.ArgumentReplace(u, WebUtility.HtmlEncode)).ToList();
+ 			_0024VB_0024Local_Extension = "." + PathUtils.GetExtension(StringExtensions.BeforeLast(_0024VB_0024Local_Urls.First(), "{CDN}", false));
+-			_0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}", ModMain.RequestTaskTempFolder(), "2.99.0.0", 50, _0024VB_0024Local_Extension);
++			_0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}", ModMain.RequestTaskTempFolder(), "2.99.0.1", 50, _0024VB_0024Local_Extension);
+ 			Logger.Info(string.Format("更新开始,静默:{0},扩展名:{1},下载目标:{2},URL:{3}", _0024STATIC_0024UpdateStart_00240212151280E91E_0024IsSilentUpdate, _0024VB_0024Local_Extension, _0024VB_0024Local_LocalPath, EnumerableExtensions.Join((IEnumerable)_0024VB_0024Local_Urls, ",")), (LogBehavior)0, "E:\\Projects\\PCL\\本体代码\\Plain Craft Launcher 2\\Modules\\ModSecret.vb");
+ 			Task.Output = new ModNet.NetFile[1]
+ 			{
+@@ -458,16 +458,16 @@
+ 			}
+ 			else if (SimulateBrowserHeaders)
+ 			{
+-				((HttpHeaders)Req.Headers).Add("User-Agent", string.Format("PCL2/{0}.{1} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36", "2.99.0.0", 50));
++				((HttpHeaders)Req.Headers).Add("User-Agent", string.Format("PCL2/{0}.{1} Mozilla/5.0 AppleWebKit/537.36 Chrome/63.0.3239.132 Safari/537.36", "2.99.0.1", 50));
+ 			}
+ 			else
+ 			{
+-				((HttpHeaders)Req.Headers).Add("User-Agent", string.Format("PCL2/{0}.{1}", "2.99.0.0", 50));
++				((HttpHeaders)Req.Headers).Add("User-Agent", string.Format("PCL2/{0}.{1}", "2.99.0.1", 50));
+ 			}
+ 		}
+ 		if (!SimulateBrowserHeaders)
+ 		{
+-			Req.Headers.Referrer = new Uri($"http://{413}.pcl2.server/");
++			Req.Headers.Referrer = new Uri($"http://{415}.pcl2.server/");
+ 		}
+ 		if (Url.Contains("api.curseforge.com") || Url.Contains("forgecdn.net"))
+ 		{
+@@ -921,7 +921,7 @@
+ 				}
+ 				else if (flag == true)
+ 				{
+-					ModMain.Hint(string.Format("已经是最新{0} {1},不需要更新啦,可以直接使用!", "正式版", "2.99.0.0"), ModMain.HintType.Green);
++					ModMain.Hint(string.Format("已经是最新{0} {1},不需要更新啦,可以直接使用!", "正式版", "2.99.0.1"), ModMain.HintType.Green);
+ 				}
+ 				else
+ 				{
+@@ -1034,7 +1034,7 @@
+ 					{
+ 						CS_0024_003C_003E8__locals28._0024VB_0024Local_Urls = CS_0024_003C_003E8__locals28._0024VB_0024Local_Urls.Select([SpecialName] (string u) => ModMain.ArgumentReplace(u, WebUtility.HtmlEncode)).ToList();
+ 						CS_0024_003C_003E8__locals28._0024VB_0024Local_Extension = "." + PathUtils.GetExtension(StringExtensions.BeforeLast(CS_0024_003C_003E8__locals28._0024VB_0024Local_Urls.First(), "{CDN}", false));
+-						CS_0024_003C_003E8__locals28._0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}", ModMain.RequestTaskTempFolder(), "2.99.0.0", 50, CS_0024_003C_003E8__locals28._0024VB_0024Local_Extension);
++						CS_0024_003C_003E8__locals28._0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}", ModMain.RequestTaskTempFolder(), "2.99.0.1", 50, CS_0024_003C_003E8__locals28._0024VB_0024Local_Extension);
+ 						Logger.Info(string.Format("更新开始,静默:{0},扩展名:{1},下载目标:{2},URL:{3}", _0024STATIC_0024UpdateStart_00240212151280E91E_0024IsSilentUpdate, CS_0024_003C_003E8__locals28._0024VB_0024Local_Extension, CS_0024_003C_003E8__locals28._0024VB_0024Local_LocalPath, EnumerableExtensions.Join((IEnumerable)CS_0024_003C_003E8__locals28._0024VB_0024Local_Urls, ",")), (LogBehavior)0, "E:\\Projects\\PCL\\本体代码\\Plain Craft Launcher 2\\Modules\\ModSecret.vb");
+ 						Task.Output = new ModNet.NetFile[1]
+ 						{
+@@ -1271,7 +1271,7 @@
+ 		//IL_045c: Expected O, but got Unknown
+ 		//IL_08b1: Unknown result type (might be due to invalid IL or missing references)
+ 		//IL_08b8: Expected O, but got Unknown
+-		//IL_0903: Unknown result type (might be due to invalid IL or missing references)
++		//IL_0996: Unknown result type (might be due to invalid IL or missing references)
+ 		//IL_04b5: Unknown result type (might be due to invalid IL or missing references)
+ 		//IL_04bc: Expected O, but got Unknown
+ 		//IL_05e0: Unknown result type (might be due to invalid IL or missing references)
+@@ -1334,10 +1334,10 @@
+ 				else
+ 				{
+ 					Logger.Info($"本地公告编号:{num2},服务器公告:{text3},无需更新", (LogBehavior)0, "E:\\Projects\\PCL\\本体代码\\Plain Craft Launcher 2\\Modules\\ModSecret.vb");
+-					if (Settings.Get<int>("SystemHighestSavedBetaVersionReg") < 413 || !FileUtils.Exists(ModBase.PathTemp + "Latest.exe"))
++					if (Settings.Get<int>("SystemHighestSavedBetaVersionReg") < 415 || !FileUtils.Exists(ModBase.PathTemp + "Latest.exe"))
+ 					{
+-						Settings.Set("SystemHighestSavedBetaVersionReg", 413);
+-						Logger.Info($"复制自身为最新正式版:版本号升高到 {413}", (LogBehavior)0, "E:\\Projects\\PCL\\本体代码\\Plain Craft Launcher 2\\Modules\\ModSecret.vb");
++						Settings.Set("SystemHighestSavedBetaVersionReg", 415);
++						Logger.Info($"复制自身为最新正式版:版本号升高到 {415}", (LogBehavior)0, "E:\\Projects\\PCL\\本体代码\\Plain Craft Launcher 2\\Modules\\ModSecret.vb");
+ 						try
+ 						{
+ 							FileUtils.Copy(ModBase.PathExe, ModBase.PathTemp + "Latest.exe");
+@@ -1495,8 +1495,17 @@
+ 							case "setup":
+ 							{
+ 								JObject val5 = (JObject)val4.Value;
++								string[] source = new string[12]
++								{
++									"LaunchAdvanceRun", "LaunchAdvanceJvm", "LaunchAdvanceGame", "UiCustomNet", "HintCustomCommand", "LaunchArgumentJavaAll", "LaunchArgumentJavaSelect", "SystemSystemCache", "LaunchFolderSelect", "LaunchFolders",
++									"ToolDownloadCert", "CacheAuthServerServer"
++								};
+ 								foreach (KeyValuePair<string, JToken> item4 in val5)
+ 								{
++									if (source.Contains(item4.Key))
++									{
++										throw new Exception("被禁止的设置项:" + item4.Key);
++									}
+ 									Settings.Set(item4.Key, item4.Value.ToString());
+ 								}
+ 								break;
+@@ -1571,10 +1580,10 @@
+ 					result = (double)Settings.Get<int>("SystemCount") >= ModBase.Val(Value);
+ 					break;
+ 				case "versioncode <=":
+-					result = 413.0 <= ModBase.Val(Value);
++					result = 415.0 <= ModBase.Val(Value);
+ 					break;
+ 				case "versioncode >=":
+-					result = 413.0 >= ModBase.Val(Value);
++					result = 415.0 >= ModBase.Val(Value);
+ 					break;
+ 				case "versionbranch <=":
+ 					result = ModBase.Val(50) <= ModBase.Val(Value);

+ 72 - 0
analysis/2.99.0.1/diff/changed-files.txt

@@ -0,0 +1,72 @@
+PCL.Resources.resx
+PCL/Application.cs
+PCL/FormMain.cs
+PCL/ModBase.cs
+PCL/ModLaunch.cs
+PCL/ModMain.cs
+PCL/ModMinecraft.cs
+PCL/ModModpack.cs
+PCL/ModSecret.cs
+PCL/PageDownloadInstall.cs
+PCL/PageDownloadLeft.cs
+PCL/PageInstanceModDisabled.cs
+PCL/PageLaunchLeft.cs
+PCL/PageLinkMain.cs
+PCL/PageOtherAbout.cs
+PCL/PageResource.cs
+PCL/PageSelectLeft.cs
+PCL/PageSelectRight.cs
+PCL/PageSetupSystem.cs
+Plain Craft Launcher 2.csproj
+Properties/AssemblyInfo.cs
+application.baml
+controls/myextratextbutton.baml
+controls/myhint.baml
+controls/mymsg/mymsginput.baml
+controls/mymsg/mymsgselect.baml
+controls/mymsg/mymsgtext.baml
+controls/mysearchbox.baml
+formmain.baml
+pages/pagedownload/pagedownloadinstall.baml
+pages/pagedownload/pagedownloadleft.baml
+pages/pagedownload/resource/myresourceitem.baml
+pages/pagedownload/resource/pagedownloadcompdetail.baml
+pages/pagedownload/resource/pagedownloaddatapack.baml
+pages/pagedownload/resource/pagedownloadmod.baml
+pages/pagedownload/resource/pagedownloadpack.baml
+pages/pagedownload/resource/pagedownloadresourcepack.baml
+pages/pagedownload/resource/pagedownloadshader.baml
+pages/pagedownload/resource/pageresource.baml
+pages/pageinstance/mylocalmoditem.baml
+pages/pageinstance/pageinstanceexport.baml
+pages/pageinstance/pageinstanceleft.baml
+pages/pageinstance/pageinstancemod.baml
+pages/pageinstance/pageinstancemoddisabled.baml
+pages/pageinstance/pageinstanceoverall.baml
+pages/pageinstance/pageinstancesetup.baml
+pages/pagelaunch/mymsglogin.baml
+pages/pagelaunch/myskin.baml
+pages/pagelaunch/pagelaunchleft.baml
+pages/pagelaunch/pagelaunchright.baml
+pages/pagelaunch/pageloginauth.baml
+pages/pagelaunch/pageloginauthskin.baml
+pages/pagelaunch/pageloginlegacy.baml
+pages/pagelaunch/pageloginms.baml
+pages/pagelaunch/pageloginmsskin.baml
+pages/pagelaunch/pageloginnide.baml
+pages/pagelaunch/pageloginnideskin.baml
+pages/pagelink/pagelinkmain.baml
+pages/pageother/pageotherabout.baml
+pages/pageother/pageotherhelp.baml
+pages/pageother/pageotherhelpdetail.baml
+pages/pageother/pageotherleft.baml
+pages/pageother/pageothertest.baml
+pages/pageselectleft.baml
+pages/pageselectright.baml
+pages/pagesetup/pagesetuplaunch.baml
+pages/pagesetup/pagesetupleft.baml
+pages/pagesetup/pagesetuplink.baml
+pages/pagesetup/pagesetupsystem.baml
+pages/pagesetup/pagesetupui.baml
+pages/pagespeedleft.baml
+pages/pagespeedright.baml

Diff do ficheiro suprimidas por serem muito extensas
+ 430 - 0
analysis/2.99.0.1/diff/other-modules.diff


+ 3 - 0
analysis/2.99.0.1/diff/summary.txt

@@ -0,0 +1,3 @@
+.cs changed: 19
+.baml changed: 51
+other changed: 2

+ 16 - 0
analysis/2.99.0.1/evidence/hashes.txt

@@ -0,0 +1,16 @@
+# 2.99.0.1 复查样本
+
+## 新版
+
+```
+4d239a62ed66fb9ebcf4633fd881eb098e9a7e4f296f0e414b58aa2f1f7f7830  PCL 正式版 2.99.0.1.zip
+0db34e19d9eeb6d496d20fabe786ec16bbd36ba6b0ccdc8d29cb7e909c114397  Plain Craft Launcher 2.exe (2.99.0.1)
+4d239a62ed66fb9ebcf4633fd881eb098e9a7e4f296f0e414b58aa2f1f7f7830  upstream/最新正式版.zip (Hex-Dragon/PCL2 93ede5d)
+```
+
+## 旧版(对照)
+
+```
+6b6e1260cb5a1d6a303855f1c60cab3d0bc1aebba7e40a964180188b271e7386  Plain Craft Launcher 2.exe (2.99.0.0)
+01877f06df84cbab376c286a633e76ef7fca0bb469e26df97f3b09b5abd503bc  upstream/最新正式版.zip (Hex-Dragon/PCL2 6b9a50d)
+```

+ 64 - 0
analysis/2.99.0.1/evidence/provenance.md

@@ -0,0 +1,64 @@
+# 2.99.0.1 来源与比对
+
+## 用户样本
+
+```text
+$ unzip -l "PCL 正式版 2.99.0.1.zip"
+ 6013952  10-25-2026 00:26   Plain Craft Launcher 2.exe
+
+$ sha256sum "PCL 正式版 2.99.0.1.zip"
+4d239a62ed66fb9ebcf4633fd881eb098e9a7e4f296f0e414b58aa2f1f7f7830
+
+$ sha256sum "Plain Craft Launcher 2.exe"
+0db34e19d9eeb6d496d20fabe786ec16bbd36ba6b0ccdc8d29cb7e909c114397
+
+$ file "Plain Craft Launcher 2.exe"
+PE32 executable for MS Windows 6.00 (GUI), Intel i386 Mono/.Net assembly, 3 sections
+```
+
+PE `Security Directory`:RVA = 0,size = 0,即没有 Authenticode 签名。
+
+## 官方仓库
+
+```text
+$ git clone --depth 5 https://github.com/Hex-Dragon/PCL2.git upstream
+$ git -C upstream log --oneline -3
+93ede5d 2.99.0.1 正式版
+e3d1cff 2.99.0.1
+6b9a50d 2.99.0.0 正式版
+
+$ sha256sum upstream/最新正式版.zip
+4d239a62ed66fb9ebcf4633fd881eb098e9a7e4f296f0e414b58aa2f1f7f7830
+
+$ unzip -l upstream/最新正式版.zip
+ 6013952  10-25-2026 00:26   Plain Craft Launcher 2.exe
+```
+
+## 比对
+
+```text
+$ unzip -q upstream/最新正式版.zip -d official
+$ sha256sum official/"Plain Craft Launcher 2.exe"
+0db34e19d9eeb6d496d20fabe786ec16bbd36ba6b0ccdc8d29cb7e909c114397
+
+$ cmp official/"Plain Craft Launcher 2.exe" "Plain Craft Launcher 2.exe"
+# 无输出,逐字节一致
+```
+
+结论:用户提供的 2.99.0.1 zip 与官方仓库 `93ede5d`(`2.99.0.1 正式版`)中的发布包
+逐字节一致,exe 亦一致,不是第三方二次打包。
+
+## 反编译与 diff
+
+```text
+$ ilspycmd -p -o decompiled-2991 "Plain Craft Launcher 2.exe"
+$ diff -rq decompiled-2990 decompiled-2991
+# .cs 19 个不同,.baml 51 个不同,另有 PCL.Resources.resx 和 csproj
+```
+
+`e3d1cff` 的公开源码改动(不含二进制 `ModSecret`):
+
+- 版本号 2.99.0.0 / 413 → 2.99.0.1 / 415;
+- 移除 MC 下载入口相关 UI 与报错;
+- `PageOtherAbout` 等 UI 调整;
+- `ModSecret.vb` 在公开仓库中仍是删减版,不含公告/更新逻辑。

+ 212 - 0
analysis/2.99.0.1/evidence/snippets.md

@@ -0,0 +1,212 @@
+# 2.99.0.1 关键代码片段
+
+反编译:`ilspycmd 9.1 -p -o decompiled "Plain Craft Launcher 2.exe"`
+行号对应 2.99.0.1 反编译结果。
+
+## 1. 启动时拉取配置
+
+```csharp
+// PCL/ModSecret.cs:382
+public static ModLoader.LoaderTask<int, int> ServerLoader =
+    new ModLoader.LoaderTask<int, int>("PCL 配置更新", ServerSub, null, ThreadPriority.BelowNormal);
+
+// PCL/ModSecret.cs:1324-1325
+text3 = ModNet.NetRequestByClientRetry(
+    "https://pcl2-server-1253424809.file.myqcloud.com/notice.cfg{CDN}");
+num3 = (int)Math.Round(ModBase.Val(StringExtensions.Split(text3, "|", false)[0]));
+```
+
+## 2. 按 notice.cfg 的版本号刷新 ServerConfig
+
+```csharp
+// PCL/ModSecret.cs:1378-1383
+double num4 = ModBase.Val(StringExtensions.Split(text3, "|", false)[4]);
+if ((double)num < num4)
+{
+    Logger.Info($"本地配置版本号:{num},服务器配置版本号:{num4},需要更新", ...);
+    ModNet.NetDownloadByClient(
+        "https://pcl2-server-1253424809.file.myqcloud.com/ServerConfig.json{CDN}", text);
+    ServerConfig = (JObject)FileUtils.ReadAsJson(text, (Encoding)null, (Type)null);
+}
+```
+
+## 3. 拉取并缓存 notice.json
+
+```csharp
+// PCL/ModSecret.cs:1403-1404
+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, (Encoding)null);
+```
+
+## 4. setup 黑名单
+
+```csharp
+// PCL/ModSecret.cs:1495-1510
+case "setup":
+{
+    JObject val5 = (JObject)val4.Value;
+    string[] source = new string[12]
+    {
+        "LaunchAdvanceRun", "LaunchAdvanceJvm", "LaunchAdvanceGame", "UiCustomNet",
+        "HintCustomCommand", "LaunchArgumentJavaAll", "LaunchArgumentJavaSelect",
+        "SystemSystemCache", "LaunchFolderSelect", "LaunchFolders",
+        "ToolDownloadCert", "CacheAuthServerServer"
+    };
+    foreach (KeyValuePair<string, JToken> item4 in val5)
+    {
+        if (source.Contains(item4.Key))
+        {
+            throw new Exception("被禁止的设置项:" + item4.Key);
+        }
+        Settings.Set(item4.Key, item4.Value.ToString());
+    }
+    break;
+}
+```
+
+`Settings.Entries` 的构造是 `... .ToDictionary(e => e.Key)`,即默认
+`Dictionary<string, Setting>` comparer,键名大小写敏感。
+
+## 5. 静默更新入口与恒真权限
+
+```csharp
+// PCL/ModSecret.cs:402
+private static bool HasUpdatePermission => true;
+
+// PCL/ModSecret.cs:1513-1530
+case "update":
+case "slientupdate":
+{
+    _ = (JObject)val4.Value;
+    bool flag3 = Operators.CompareString(val4.Name, "slientupdate", false) == 0;
+    if (!HasUpdatePermission)
+    {
+        if (!flag3)
+        {
+            InputPotatoCode(IsUpdating: true);
+        }
+        if (!HasUpdatePermission)
+        {
+            break;
+        }
+    }
+    UpdateStart(flag3);
+    break;
+}
+```
+
+## 6. 更新 URL 与下载
+
+```csharp
+// PCL/ModSecret.cs:1017-1037
+JObject val = (JObject)ServerConfig["Update"][(object)ModBase.BuildTypes.Release.ToString()];
+if (val.ContainsKey("PatchUrls") && StringExtensions.StartsWithF(UpdateMark, "Empty", false))
+{
+    _0024VB_0024Local_Urls = val["PatchUrls"].ToObject<List<string>>();
+}
+else if (val.ContainsKey("DownloadUrls"))
+{
+    _0024VB_0024Local_Urls = val["DownloadUrls"].ToObject<List<string>>();
+}
+// ...
+_0024VB_0024Local_Extension = "." + PathUtils.GetExtension(
+    StringExtensions.BeforeLast(_0024VB_0024Local_Urls.First(), "{CDN}", false));
+_0024VB_0024Local_LocalPath = string.Format("{0}{1}.{2}{3}",
+    ModMain.RequestTaskTempFolder(), "2.99.0.1", 50, _0024VB_0024Local_Extension);
+
+Task.Output = new ModNet.NetFile[1]
+{
+    new ModNet.NetFile(_0024VB_0024Local_Urls, _0024VB_0024Local_LocalPath)
+}.ToList();
+```
+
+`ModNet.NetFile` 的构造参数只有 URL 列表与本地路径,没有哈希/签名。
+
+## 7. 安装:解压覆盖,静默等待重启
+
+```csharp
+// PCL/ModSecret.cs:198-211
+if (Operators.CompareString(_0024VB_0024Local_Extension, ".zip", false) == 0)
+{
+    Logger.Info($"解压更新文件:{_0024VB_0024Local_LocalPath}", ...);
+    FileUtils.ExtractToDirectory(_0024VB_0024Local_LocalPath, Paths.Base + "PCL\\",
+        default(CancellationToken), (ProgressProvider)null);
+}
+else
+{
+    Logger.Info($"应用补丁文件:...", ...);
+    MsDelta.Apply(ModBase.PathExe, _0024VB_0024Local_LocalPath,
+        $"{Paths.Base}PCL\\Plain Craft Launcher 2.exe");
+}
+if (_0024STATIC_0024UpdateStart_00240212151280E91E_0024IsSilentUpdate)
+{
+    IsUpdateWaitingRestart = true;
+    return;
+}
+```
+
+## 8. 退出时执行新的 exe
+
+```csharp
+// PCL/FormMain.cs:1968
+if (ModSecret.IsUpdateWaitingRestart)
+{
+    ModSecret.UpdateRestart(TriggerRestartAndByEnd: false);
+}
+
+// PCL/ModSecret.cs:1084-1097
+public static void UpdateRestart(bool TriggerRestartAndByEnd)
+{
+    try
+    {
+        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}";
+        Logger.Info($"更新程序启动,参数:{text}", ...);
+        ModBase.StartProcess(new ProcessStartInfo(fileName)
+        {
+            WindowStyle = ProcessWindowStyle.Hidden,
+            CreateNoWindow = true,
+            Arguments = text
+        });
+        if (TriggerRestartAndByEnd)
+        {
+            ModMain.FrmMain.EndProgram(SendWarning: false);
+            Logger.Info("已由于更新强制结束程序", ...);
+        }
+    }
+    catch (Win32Exception ex) { ... }
+}
+```
+
+## 9. CDN 签名(不是内容签名)
+
+```csharp
+// PCL/ModSecret.cs:428-445
+internal static string SecretCdnSign(string UrlWithMark)
+{
+    if (ModBase.ApplicationStartTick < 1)
+    {
+        return UrlWithMark;
+    }
+    if (!StringExtensions.EndsWithF(UrlWithMark, "{CDN}", false))
+    {
+        return UrlWithMark;
+    }
+    string text = UrlWithMark.Replace("{CDN}", "").Replace(" ", "%20");
+    string text2 = StringExtensions.EnsureLength(ModBase.RandomInteger(0, 2147483645).ToString("x"), '0', 8);
+    string text3 = Conversions.ToString(ModBase.GetUnixTimestampUtc());
+    string text4 = text.Substring(checked(StringExtensions.IndexOfF(text, "://", false) + 3));
+    text4 = text4.Substring(StringExtensions.IndexOfF(text4, "/", false));
+    string text5 = CryptographyUtils.ComputeHash(EnumerableExtensions.Join((IEnumerable)new string[5]
+    {
+        text4, text3, text2, "0", "PCL2Server"
+    }, "-"), (HashMethod)0);
+    return text + (text.Contains("?") ? "&" : "?") + "sign=" +
+        EnumerableExtensions.Join((IEnumerable)new string[4] { text3, text2, "0", text5 }, "-");
+}
+```
+
+密钥 `PCL2Server` 硬编码在客户端,MD5 只覆盖 URL 路径与时间戳,不覆盖响应内容。

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff