From a499577b7c9c6b16e30bff1a610940a1778722cd Mon Sep 17 00:00:00 2001 From: MrMaho Date: Sat, 3 Oct 2026 18:44:03 +0200 Subject: [PATCH] Harden the mod against hostile servers and players - PacketReader for all network data: lengths are checked against the real data (Mono allocated the claimed size first - a fake 2 GB length crashed the game) - FsmSync: remote players can no longer switch arbitrary FSMs - never PLAYER / Systems / MSCO objects, CLICK only on FSMs that wait for a click, bolt/assembly events only along a transition the FSM really has for that event - Time/weather (msco.world) and '* ' system-style chat only from the host in P2P (the dedicated server filters them itself); other msco.* ids are reserved Co-Authored-By: Claude Opus 5.5 --- FsmSync.cs | 34 ++++++++++++++++++++++++++++++++++ MSCO.csproj | 1 + MultiplayerManager.cs | 19 +++++++++++++------ NetSession.cs | 2 +- PacketReader.cs | 37 +++++++++++++++++++++++++++++++++++++ ServerBrowser.cs | 2 +- WorldAdmin.cs | 2 +- 7 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 PacketReader.cs diff --git a/FsmSync.cs b/FsmSync.cs index d862b1a..d285379 100644 --- a/FsmSync.cs +++ b/FsmSync.cs @@ -292,6 +292,11 @@ namespace MSCO if (Debug) MscoLog.Warning("[MSCO] << not found: " + msg.Path + " [" + msg.FsmName + "]"); return; } + if (!Allowed(fsm, msg)) + { + if (Debug) MscoLog.Warning("[MSCO] << refused: " + msg.EventName + " " + msg.Path + " [" + msg.FsmName + " -> " + msg.State + "]"); + return; + } if (msg.EventName == VehicleAdmin.InstallEvent) { VehicleAdmin.InstallRemote(fsm); return; } // Admin hat ein Teil eingebaut FsmState state = fsm.Fsm.GetState(msg.State); if (state == null || switchState == null) return; @@ -304,6 +309,35 @@ namespace MSCO finally { applying--; } } + /// + /// Darf ein Mitspieler diese FSM in diesen Zustand schalten? Sonst könnte ein manipulierter Client beliebige + /// Zustände bei allen anderen auslösen (z.B. Tod oder Speichern). Nie: der eigene Spieler (PLAYER), Systeme + /// (Tod, Speichern ...), MSCO selbst. Klicks nur auf FSMs, die auf einen Klick warten; Schrauben/Montage nur + /// über einen Übergang, den die FSM für genau dieses Event auch hat. + /// + static bool Allowed(PlayMakerFSM fsm, FsmEventMsg msg) + { + try + { + string root = fsm.transform.root.name; + if (root == "PLAYER" || root == "Systems" || root.StartsWith("MSCO_")) return false; + if (msg.EventName == VehicleAdmin.InstallEvent) return fsm.FsmName == "Assembly"; + if (msg.EventName == "CLICK") + { + foreach (FsmState s in fsm.FsmStates) if (ChecksInput(s)) return fsm.Fsm.GetState(msg.State) != null; + return false; + } + if (Array.IndexOf(SyncedEvents, msg.EventName) < 0) return false; + foreach (FsmState s in fsm.FsmStates) + foreach (FsmTransition t in s.Transitions) + if (t.EventName == msg.EventName && t.ToState == msg.State) return true; + foreach (FsmTransition t in fsm.Fsm.GlobalTransitions) + if (t.EventName == msg.EventName && t.ToState == msg.State) return true; + return false; + } + catch { return false; } + } + /// FSM in einen Zustand schalten, ohne dass das als eigene Aktion erkannt und gesendet wird. public static void SwitchStateSilently(PlayMakerFSM fsm, FsmState state) { diff --git a/MSCO.csproj b/MSCO.csproj index 4fe408c..b162b37 100644 --- a/MSCO.csproj +++ b/MSCO.csproj @@ -99,6 +99,7 @@ + diff --git a/MultiplayerManager.cs b/MultiplayerManager.cs index f0d1d5b..ab5ef6d 100644 --- a/MultiplayerManager.cs +++ b/MultiplayerManager.cs @@ -1731,7 +1731,13 @@ namespace MSCO string modId = r.ReadString(); int len = r.ReadInt32(); if (len < 0 || len > MaxModMessageBytes) return; - if (modId == WorldAdmin.MessageId) { WorldAdmin.Receive(r.ReadBytes(len)); return; } // Admin: Uhrzeit/Wetter + if (modId == WorldAdmin.MessageId) + { + // Admin: Uhrzeit/Wetter. Der dedizierte Server lässt das nur von Admins durch; im P2P-Spiel nur vom Host + if (dedicated || from == NetSession.HostId) WorldAdmin.Receive(r.ReadBytes(len)); + return; + } + if (modId.StartsWith("msco.")) return; // reserviert für MSCO selbst ModLoader.Message(modId, from, r.ReadBytes(len)); return; } @@ -1740,7 +1746,8 @@ namespace MSCO string text = r.ReadString(); PlayerInfo sender; if (dedicated && from == NetSession.HostId) overlays.AddChat("", text, true); // Server/Admin-Meldung - else if (text.StartsWith("* ")) overlays.AddChat("", text.Substring(2), true); + // "* ..." = Meldung (z.B. Admin-Aktion): auf dem dedizierten Server filtert der Server, im P2P-Spiel nur vom Host + else if (text.StartsWith("* ") && (dedicated || from == NetSession.HostId)) overlays.AddChat("", text.Substring(2), true); else overlays.AddChat(players.TryGetValue(from, out sender) ? sender.Name : "Player " + from, text); return; } @@ -2368,7 +2375,7 @@ namespace MSCO /// System.Collections.IEnumerator ApplyWorldSnapshot(byte[] snapshot) { - var r = new BinaryReader(new MemoryStream(snapshot)); + var r = new PacketReader(snapshot); if (r.ReadBoolean()) { @@ -2401,7 +2408,7 @@ namespace MSCO for (int i = 0; i < eventCount; i++) { byte[] payload = r.ReadBytes(r.ReadInt32()); - FsmSync.Apply(FsmEventMsg.Deserialize(new BinaryReader(new MemoryStream(payload)))); + FsmSync.Apply(FsmEventMsg.Deserialize(new PacketReader(payload))); if ((i + 1) % SnapshotEventsPerFrame == 0) yield return null; // nicht alles in einem Frame } @@ -2426,7 +2433,7 @@ namespace MSCO for (int i = 0; i < spawns; i++) { byte[] payload = r.ReadBytes(r.ReadInt32()); - try { if (ApplySpawn(new BinaryReader(new MemoryStream(payload)))) spawned++; } + try { if (ApplySpawn(new PacketReader(payload))) spawned++; } catch (System.Exception e) { MscoLog.Warning("Could not spawn a saved item: " + e.Message); } } } @@ -2439,7 +2446,7 @@ namespace MSCO for (int i = 0; i < appeared; i++) { byte[] payload = r.ReadBytes(r.ReadInt32()); - try { ApplyAppeared(new BinaryReader(new MemoryStream(payload))); } + try { ApplyAppeared(new PacketReader(payload)); } catch (System.Exception e) { MscoLog.Warning("Could not restore a bought item: " + e.Message); } } } diff --git a/NetSession.cs b/NetSession.cs index 891b6b1..b5aee3f 100644 --- a/NetSession.cs +++ b/NetSession.cs @@ -67,7 +67,7 @@ namespace MSCO public BinaryReader Reader() { - return new BinaryReader(new MemoryStream(Payload)); + return new PacketReader(Payload); } } diff --git a/PacketReader.cs b/PacketReader.cs new file mode 100644 index 0000000..006cc61 --- /dev/null +++ b/PacketReader.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace MSCO +{ + /// + /// BinaryReader für Daten aus dem Netz (Server, Mitspieler). Mono legt bei ReadBytes(n) und ReadString() vorab ein + /// Array bzw. einen StringBuilder in der angegebenen Länge an - eine gefälschte Länge (z.B. 2 Milliarden) ließ das + /// Spiel mit "Out of Memory" abstürzen. Hier wird jede Länge gegen die tatsächlich vorhandenen Bytes geprüft. + /// + public sealed class PacketReader : BinaryReader + { + public PacketReader(byte[] data) : base(new MemoryStream(data ?? new byte[0], false)) { } + + long Remaining { get { return BaseStream.Length - BaseStream.Position; } } + + public override byte[] ReadBytes(int count) + { + if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count); + return base.ReadBytes(count); + } + + public override char[] ReadChars(int count) + { + if (count < 0 || count > Remaining) throw new EndOfStreamException("Invalid length " + count); + return base.ReadChars(count); + } + + public override string ReadString() + { + long start = BaseStream.Position; + int length = Read7BitEncodedInt(); + if (length < 0 || length > Remaining) throw new EndOfStreamException("Invalid string length " + length); + BaseStream.Position = start; + return base.ReadString(); + } + } +} diff --git a/ServerBrowser.cs b/ServerBrowser.cs index 4fd6429..57f1c88 100644 --- a/ServerBrowser.cs +++ b/ServerBrowser.cs @@ -244,7 +244,7 @@ namespace MSCO byte[] payload = ReadExactly(stream, length - 1); info.Ping = (int)watch.ElapsedMilliseconds; - using (var r = new BinaryReader(new MemoryStream(payload))) + using (var r = new PacketReader(payload)) { info.Name = r.ReadString(); info.Description = r.ReadString(); diff --git a/WorldAdmin.cs b/WorldAdmin.cs index cdba227..eb692c7 100644 --- a/WorldAdmin.cs +++ b/WorldAdmin.cs @@ -204,7 +204,7 @@ namespace MSCO public static void Receive(byte[] data) { if (!MenuHelper.InGame || data == null || data.Length < 17) return; - using (var r = new BinaryReader(new MemoryStream(data))) + using (var r = new PacketReader(data)) { byte kind = r.ReadByte(); int value = r.ReadInt32();