From 6c921f0efc272fe531dba74f90540c7fd1c27644 Mon Sep 17 00:00:00 2001 From: DobriyKaban Date: Mon, 19 Oct 2026 03:30:13 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=9C=D0=B5=D1=82=D0=B0=D0=BB=D1=81=D0=B5?= =?UTF-8?q?=D0=B9=D0=B4=D0=B6=20VS=20=D0=98=D0=BD=D1=82=D0=B5=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Metasage: У нас детектить нечем :( - Чит-Девы: Возьми яйтсо Членти = жирный уебан Читеров = не банить Античит = обходить Мать Металсейджа = посадить на диету --- Marsey.Tests/Hidesey/HideseyTest.cs | 79 +++++++++++ Marsey/Stealthsey/Hidesey.cs | 209 ++++++++++++++++++++++++++++ Marsey/Stealthsey/HideseyPatches.cs | 74 ++++++++++ 3 files changed, 362 insertions(+) diff --git a/Marsey.Tests/Hidesey/HideseyTest.cs b/Marsey.Tests/Hidesey/HideseyTest.cs index 049a3c3..54cd3bc 100644 --- a/Marsey.Tests/Hidesey/HideseyTest.cs +++ b/Marsey.Tests/Hidesey/HideseyTest.cs @@ -102,4 +102,83 @@ public void Hidesey_ManifestHidesCvars() Assert.That(filteredCvars, Does.Contain("normal_cvar")); Assert.That(filteredCvars, Does.Not.Contain("stealth_cvar"), "Cvar wasn't hidden by the manifest."); } + + [Test] + public void Hidesey_HiddenDefinedTypes() + { + List allDefinedTypes = new List(); + + using (Hidesey.ForceEngineView()) + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly == Assembly.GetExecutingAssembly()) continue; + + try + { + allDefinedTypes.AddRange(assembly.DefinedTypes); + } + catch (ReflectionTypeLoadException e) + { + allDefinedTypes.AddRange(e.Types.Where(t => t != null).Select(t => t.GetTypeInfo())); + } + catch + { + } + } + } + + List hiddenTypeNamespaces = new List { "Marsey", "Harmony" }; + + TypeInfo[] filteredTypes = allDefinedTypes.Where(type => + hiddenTypeNamespaces.Any(forbidden => type.Namespace?.StartsWith(forbidden) ?? false) + ).ToArray(); + + // Assert + Assert.That(filteredTypes, Is.Empty, "Forbidden defined types were found in the engine view domain."); + } + + [Test] + public void Hidesey_HiddenExceptionStackTrace() + { + Exception ex; + try + { + throw new InvalidOperationException("Test exception from Marsey test"); + } + catch (Exception caught) + { + ex = caught; + } + + string? stackTrace; + using (Hidesey.ForceEngineView()) + { + stackTrace = ex.StackTrace; + } + + // Assert + if (!string.IsNullOrEmpty(stackTrace)) + { + Assert.That(stackTrace, Does.Not.Contain("0Harmony")); + Assert.That(stackTrace, Does.Not.Contain("HarmonyLib")); + Assert.That(stackTrace, Does.Not.Contain("Marsey")); + } + } + + [Test] + public void Hidesey_HiddenTypeAssembly() + { + Type hiddenType = typeof(Hidesey); + Assembly? asm; + + using (Hidesey.ForceEngineView()) + { + asm = hiddenType.Assembly; + } + + // Assert: under engine view, Type.Assembly should be spoofed to a surrogate assembly, not Marsey + Assert.That(asm, Is.Not.Null); + Assert.That(asm.GetName().Name, Is.Not.EqualTo("Marsey"), "Type.Assembly was not spoofed under engine view."); + } } \ No newline at end of file diff --git a/Marsey/Stealthsey/Hidesey.cs b/Marsey/Stealthsey/Hidesey.cs index 50efc05..eedbbd4 100644 --- a/Marsey/Stealthsey/Hidesey.cs +++ b/Marsey/Stealthsey/Hidesey.cs @@ -32,6 +32,7 @@ public static partial class Hidesey private static bool _initialized; private static bool _caching; [ThreadStatic] private static bool _inFromHidden; + [ThreadStatic] private static bool _inLyingTypeAssembly; // Fast-path cache for FromHidden: assemblies known to be engine/system, skip stack walk on these private static readonly HashSet _knownEngineAsms = new(); @@ -234,6 +235,15 @@ private static void Perjurize() // Closes Environment.StackTrace leak (whitelisted string property). PerjurizeStackTrace(); + + // Closes Exception.StackTrace leak. + PerjurizeExceptionStackTrace(); + + // Closes Assembly.DefinedTypes leak. + PerjurizeDefinedTypes(); + + // Closes Type.Assembly (GetType().Assembly) leak. + PerjurizeTypeAssembly(); } private static void PerjurizeTryLooseGetType() @@ -407,6 +417,137 @@ private static void PerjurizeStackTrace() } } + private static void PerjurizeExceptionStackTrace() + { + MethodInfo? postfix = typeof(HideseyPatches).GetMethod(nameof(HideseyPatches.LieExceptionStackTrace), + BindingFlags.Public | BindingFlags.Static); + if (postfix == null) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, "HideseyPatches.LieExceptionStackTrace not found"); + return; + } + + MethodInfo? target = typeof(Exception).GetProperty("StackTrace")?.GetGetMethod(); + if (target == null) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, "Exception.StackTrace getter not found"); + return; + } + + try + { + var harm = HarmonyManager.GetHarmony(); + var hm = new HarmonyMethod(postfix) + { + before = Array.Empty(), + after = Array.Empty(), + priority = Priority.Normal, + }; + harm.Patch(target, postfix: hm); + MarseyLogger.Log(MarseyLogger.LogType.INFO, "Exception.StackTrace hook installed"); + } + catch (Exception ex) + { + MarseyLogger.Log(MarseyLogger.LogType.FATL, $"Exception.StackTrace hook FAILED: {ex.Message}"); + } + } + + private static void PerjurizeDefinedTypes() + { + MethodInfo? postfix = typeof(HideseyPatches).GetMethod(nameof(HideseyPatches.LieDefinedTypes), + BindingFlags.Public | BindingFlags.Static); + if (postfix == null) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, "HideseyPatches.LieDefinedTypes not found"); + return; + } + + var targets = new List(); + var baseGetter = typeof(Assembly).GetProperty("DefinedTypes")?.GetGetMethod(); + if (baseGetter != null && !baseGetter.IsAbstract) targets.Add(baseGetter); + + var runtimeAsm = Assembly.GetExecutingAssembly().GetType(); + var runtimeGetter = runtimeAsm.GetProperty("DefinedTypes", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)?.GetGetMethod() + ?? runtimeAsm.GetProperty("DefinedTypes")?.GetGetMethod(); + if (runtimeGetter != null && !runtimeGetter.IsAbstract && !targets.Contains(runtimeGetter)) + targets.Add(runtimeGetter); + + var harm = HarmonyManager.GetHarmony(); + int hooked = 0; + foreach (var target in targets) + { + try + { + var hm = new HarmonyMethod(postfix) + { + before = Array.Empty(), + after = Array.Empty(), + priority = Priority.Normal, + }; + harm.Patch(target, postfix: hm); + MarseyLogger.Log(MarseyLogger.LogType.INFO, $"{target.DeclaringType?.Name}.DefinedTypes hook installed"); + hooked++; + } + catch (Exception ex) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, $"DefinedTypes hook on {target.DeclaringType?.Name} failed: {ex.Message}"); + } + } + + if (hooked == 0) + { + MarseyLogger.Log(MarseyLogger.LogType.FATL, "PerjurizeDefinedTypes: no DefinedTypes getter targets hooked"); + } + } + + private static void PerjurizeTypeAssembly() + { + MethodInfo? postfix = typeof(HideseyPatches).GetMethod(nameof(HideseyPatches.LieTypeAssembly), + BindingFlags.Public | BindingFlags.Static); + if (postfix == null) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, "HideseyPatches.LieTypeAssembly not found"); + return; + } + + var targets = new List(); + var baseGetter = typeof(Type).GetProperty("Assembly")?.GetGetMethod(); + if (baseGetter != null && !baseGetter.IsAbstract) targets.Add(baseGetter); + + var runtimeType = typeof(object).GetType(); + var runtimeGetter = runtimeType.GetProperty("Assembly", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)?.GetGetMethod() + ?? runtimeType.GetProperty("Assembly")?.GetGetMethod(); + if (runtimeGetter != null && !runtimeGetter.IsAbstract && !targets.Contains(runtimeGetter)) + targets.Add(runtimeGetter); + + var harm = HarmonyManager.GetHarmony(); + int hooked = 0; + foreach (var target in targets) + { + try + { + var hm = new HarmonyMethod(postfix) + { + before = Array.Empty(), + after = Array.Empty(), + priority = Priority.Normal, + }; + harm.Patch(target, postfix: hm); + MarseyLogger.Log(MarseyLogger.LogType.INFO, $"{target.DeclaringType?.Name}.Assembly hook installed"); + hooked++; + } + catch (Exception ex) + { + MarseyLogger.Log(MarseyLogger.LogType.WARN, $"Assembly hook on {target.DeclaringType?.Name} failed: {ex.Message}"); + } + } + + if (hooked == 0) + { + MarseyLogger.Log(MarseyLogger.LogType.FATL, "PerjurizeTypeAssembly: no Type.Assembly targets hooked"); + } + } + /// /// Postfix on Robust's ReflectionManager. /// Must run AFTER engine load — Robust.Shared isn't in AppDomain during Perjurize(). @@ -719,6 +860,74 @@ public static string LyingStackTrace(string original) return sb.ToString(); } + public static IEnumerable LyingDefinedTypes(IEnumerable original) + { + if (FromHidden()) return original; + return original.Where(t => !IsHiddenType(t)); + } + + private static bool IsInternalReflectionCaller() + { + if (IsEngineViewForced()) return false; + + var trace = new StackTrace(2, false); + var frames = trace.GetFrames(); + if (frames == null) return false; + + for (int i = 0; i < Math.Min(10, frames.Length); i++) + { + var method = frames[i].GetMethod(); + var declType = method?.DeclaringType; + if (declType == null) continue; + + var asmName = declType.Assembly?.GetName().Name ?? ""; + if (asmName.Contains("Harmony") || asmName.Contains("MonoMod") || asmName.Contains("Mono.Cecil")) + return true; + + if (declType.FullName != null && + (declType.FullName.StartsWith("System.Reflection.") || + declType.FullName.StartsWith("System.RuntimeMethodHandle") || + declType.FullName.StartsWith("System.RuntimeType"))) + { + return true; + } + } + + return false; + } + + /// + /// Filter for Type.Assembly getter — returns a legitimate surrogate (Robust.Client) for hidden types. + /// + public static Assembly LyingTypeAssembly(Assembly original, Type instance) + { + if (_inLyingTypeAssembly) return original; + if (FromHidden()) return original; + + _inLyingTypeAssembly = true; + try + { + if (IsInternalReflectionCaller()) + return original; + + if (IsHiddenAsm(original) || IsHiddenType(instance)) + { + var robust = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Robust.Client"); + if (robust != null && !robust.IsDynamic) + return robust; + + return typeof(object).Assembly; + } + + return original; + } + finally + { + _inLyingTypeAssembly = false; + } + } + /// /// Expose the hidden assemblies list to other Marsey-context code (e.g. SourceDumper). /// diff --git a/Marsey/Stealthsey/HideseyPatches.cs b/Marsey/Stealthsey/HideseyPatches.cs index 76ba5a8..e368944 100644 --- a/Marsey/Stealthsey/HideseyPatches.cs +++ b/Marsey/Stealthsey/HideseyPatches.cs @@ -54,6 +54,21 @@ public static partial class HideseyPatches public static string? LastContentFindFilesCaller { get; private set; } public static List ContentFindFilesBlockedNames { get; } = new(); + /// + /// Diagnostic: Assembly.DefinedTypes filter count. + /// + public static int DefinedTypesFilteredCount { get; private set; } + + /// + /// Diagnostic: Exception.StackTrace filter count. + /// + public static int ExceptionStackTraceFilteredCount { get; private set; } + + /// + /// Diagnostic: Type.Assembly spoof count. + /// + public static int TypeAssemblySpoofedCount { get; private set; } + public static void Lie(ref T __result) { __result = __result switch @@ -65,6 +80,7 @@ public static void Lie(ref T __result) assemblyLoadContextEnumerable), AssemblyName[] assemblyNames => (T)(object)Hidesey.LyingReference(assemblyNames), Type[] types => (T)(object)Hidesey.LyingTyper(types), + IEnumerable typeInfos => (T)(object)Hidesey.LyingDefinedTypes(typeInfos), _ => throw new InvalidOperationException("Unsupported type for LiePatch") }; } @@ -83,6 +99,22 @@ public static void LieFindAllTypes(ref IEnumerable __result) TryCaptureFindAllTypes(filtered); } + /// + /// Postfix on Assembly.DefinedTypes getter. + /// Filters out TypeInfo instances declared in hidden assemblies/types. + /// + public static void LieDefinedTypes(ref IEnumerable __result) + { + if (__result == null) return; + var original = __result; + var filtered = Hidesey.LyingDefinedTypes(__result); + __result = filtered; + if (!ReferenceEquals(original, filtered)) + { + DefinedTypesFilteredCount++; + } + } + public static void LieGetType(ref Type? __result, object[] __args) { var originalResult = __result; @@ -186,6 +218,48 @@ public static void LieStackTrace(ref string __result) } } + /// + /// Postfix on Exception.StackTrace getter. + /// Filters out lines that reference hidden assemblies or patches. + /// + public static void LieExceptionStackTrace(ref string? __result) + { + if (string.IsNullOrEmpty(__result)) return; + + var original = __result; + __result = Hidesey.LyingStackTrace(__result); + + if (!ReferenceEquals(original, __result) && original != __result) + { + try + { + ExceptionStackTraceFilteredCount++; + } + catch { } + } + } + + /// + /// Postfix on Type.Assembly getter. + /// Returns surrogate assembly (e.g. Robust.Client) if type is hidden. + /// + public static void LieTypeAssembly(ref Assembly __result, Type __instance) + { + if (__result == null || __instance == null) return; + + var original = __result; + __result = Hidesey.LyingTypeAssembly(__result, __instance); + + if (!ReferenceEquals(original, __result)) + { + try + { + TypeAssemblySpoofedCount++; + } + catch { } + } + } + /// /// Postfix on IModLoader.LoadedModules getter. /// Returns enumerable of Assembly (concrete return type matters in postfix). From a18ce3f603160baee5d71b9d1597392cad13732c Mon Sep 17 00:00:00 2001 From: DobriyKaban Date: Mon, 19 Oct 2026 03:51:17 +0300 Subject: [PATCH 2/4] =?UTF-8?q?Metalsage=20VS=20=D0=97=D0=B4=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D1=81=D0=BC=D1=8B=D1=81=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Marsey.Tests/Hidesey/HideseyTest.cs | 120 ++++++++++++++++--------- Marsey/Stealthsey/Hidesey.cs | 45 +++++++++- Marsey/Stealthsey/HideseyPatches.cs | 34 +++---- Marsey/Stealthsey/HideseyRegistries.cs | 42 +++++---- 4 files changed, 155 insertions(+), 86 deletions(-) diff --git a/Marsey.Tests/Hidesey/HideseyTest.cs b/Marsey.Tests/Hidesey/HideseyTest.cs index 54cd3bc..b3d6611 100644 --- a/Marsey.Tests/Hidesey/HideseyTest.cs +++ b/Marsey.Tests/Hidesey/HideseyTest.cs @@ -106,63 +106,86 @@ public void Hidesey_ManifestHidesCvars() [Test] public void Hidesey_HiddenDefinedTypes() { - List allDefinedTypes = new List(); + // 1. Obtain hidden assembly reference in normal view (before Type.Assembly spoofing kicks in) + Assembly marseyAssembly = typeof(Hidesey).Assembly; + var normalTypes = marseyAssembly.DefinedTypes.ToList(); + Assert.That(normalTypes, Does.Contain(typeof(Hidesey).GetTypeInfo()), + "Hidden caller should see all defined types in Marsey assembly."); + + // 2. Under engine view, DefinedTypes on the hidden assembly must be lazy and filter out hidden types using (Hidesey.ForceEngineView()) { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly == Assembly.GetExecutingAssembly()) continue; - - try - { - allDefinedTypes.AddRange(assembly.DefinedTypes); - } - catch (ReflectionTypeLoadException e) - { - allDefinedTypes.AddRange(e.Types.Where(t => t != null).Select(t => t.GetTypeInfo())); - } - catch - { - } - } + int countBefore = HideseyPatches.DefinedTypesFilteredCount; + + // Getter call: should NOT evaluate or filter yet (demonstrating laziness) + var lazyEnumerable = marseyAssembly.DefinedTypes; + Assert.That(HideseyPatches.DefinedTypesFilteredCount, Is.EqualTo(countBefore), + "DefinedTypes must be lazy and not filter during getter invocation."); + + // Materialization: now it filters and increments the counter + var filteredTypes = lazyEnumerable.ToList(); + Assert.That(filteredTypes, Does.Not.Contain(typeof(Hidesey).GetTypeInfo()), + "Hidden type must be filtered out under engine view."); + Assert.That(HideseyPatches.DefinedTypesFilteredCount, Is.GreaterThan(countBefore), + "DefinedTypesFilteredCount must count the actual filtered types."); + + // 3. Non-hidden assembly (CoreLib) must remain intact + var stringTypes = typeof(string).Assembly.DefinedTypes; + Assert.That(stringTypes, Does.Contain(typeof(string).GetTypeInfo()), + "Non-hidden assembly types must not be filtered."); } - - List hiddenTypeNamespaces = new List { "Marsey", "Harmony" }; - - TypeInfo[] filteredTypes = allDefinedTypes.Where(type => - hiddenTypeNamespaces.Any(forbidden => type.Namespace?.StartsWith(forbidden) ?? false) - ).ToArray(); - - // Assert - Assert.That(filteredTypes, Is.Empty, "Forbidden defined types were found in the engine view domain."); } [Test] public void Hidesey_HiddenExceptionStackTrace() { + string mixedTrace = " at Robust.Shared.GameObjects.EntitySystem.Update()\n" + + " at HarmonyLib.PatchFunctions.UpdateWrapper()\n" + + " at Marsey.Stealthsey.Hidesey.DoSomething()\n" + + " at Robust.Client.BaseClient.Main()"; + + // 1. Normal view (hidden caller) leaves stack trace unmodified + string normalTrace = Hidesey.LyingStackTrace(mixedTrace); + Assert.That(normalTrace, Is.EqualTo(mixedTrace), + "Stack trace should remain unmodified for hidden callers."); + + // 2. Under engine view, hidden frames (Harmony, Marsey) are stripped + using (Hidesey.ForceEngineView()) + { + Hidesey.ClearStackTraceCache(); + + string sanitized1 = Hidesey.LyingStackTrace(mixedTrace); + Assert.That(sanitized1, Does.Not.Contain("HarmonyLib")); + Assert.That(sanitized1, Does.Not.Contain("Marsey")); + Assert.That(sanitized1, Does.Contain("Robust.Shared.GameObjects.EntitySystem.Update()")); + Assert.That(sanitized1, Does.Contain("Robust.Client.BaseClient.Main()")); + + // 3. Test caching: subsequent call with same input should return the cached instance + string sanitized2 = Hidesey.LyingStackTrace(mixedTrace); + Assert.That(ReferenceEquals(sanitized1, sanitized2), Is.True, + "LyingStackTrace must return cached string reference on repeat calls."); + } + + // 4. Test actual Exception.StackTrace hook under engine view Exception ex; try { - throw new InvalidOperationException("Test exception from Marsey test"); + throw new InvalidOperationException("Test exception for stack trace verification"); } catch (Exception caught) { ex = caught; } - string? stackTrace; using (Hidesey.ForceEngineView()) { - stackTrace = ex.StackTrace; - } - - // Assert - if (!string.IsNullOrEmpty(stackTrace)) - { - Assert.That(stackTrace, Does.Not.Contain("0Harmony")); - Assert.That(stackTrace, Does.Not.Contain("HarmonyLib")); - Assert.That(stackTrace, Does.Not.Contain("Marsey")); + string? stackTrace = ex.StackTrace; + if (!string.IsNullOrEmpty(stackTrace)) + { + Assert.That(stackTrace, Does.Not.Contain("0Harmony")); + Assert.That(stackTrace, Does.Not.Contain("HarmonyLib")); + } } } @@ -170,15 +193,26 @@ public void Hidesey_HiddenExceptionStackTrace() public void Hidesey_HiddenTypeAssembly() { Type hiddenType = typeof(Hidesey); - Assembly? asm; + // 1. Normal view: caller is hidden, returns real assembly + Assert.That(hiddenType.Assembly.GetName().Name, Is.EqualTo("Marsey"), + "Hidden caller should see real assembly name."); + + // 2. Engine view: Type.Assembly should be spoofed and counter incremented using (Hidesey.ForceEngineView()) { - asm = hiddenType.Assembly; + int spoofCountBefore = HideseyPatches.TypeAssemblySpoofedCount; + Assembly spoofed = hiddenType.Assembly; + + Assert.That(spoofed, Is.Not.Null); + Assert.That(spoofed.GetName().Name, Is.Not.EqualTo("Marsey"), + "Type.Assembly was not spoofed under engine view."); + Assert.That(HideseyPatches.TypeAssemblySpoofedCount, Is.GreaterThan(spoofCountBefore), + "TypeAssemblySpoofedCount should increment on spoofing."); + + // 3. Non-hidden type: remains unchanged + Assert.That(typeof(string).Assembly, Is.EqualTo(typeof(object).Assembly), + "Non-hidden type's assembly must not be spoofed."); } - - // Assert: under engine view, Type.Assembly should be spoofed to a surrogate assembly, not Marsey - Assert.That(asm, Is.Not.Null); - Assert.That(asm.GetName().Name, Is.Not.EqualTo("Marsey"), "Type.Assembly was not spoofed under engine view."); } } \ No newline at end of file diff --git a/Marsey/Stealthsey/Hidesey.cs b/Marsey/Stealthsey/Hidesey.cs index eedbbd4..1b0bb72 100644 --- a/Marsey/Stealthsey/Hidesey.cs +++ b/Marsey/Stealthsey/Hidesey.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Diagnostics; using System.Reflection; using System.Runtime.Loader; @@ -33,6 +34,7 @@ public static partial class Hidesey private static bool _caching; [ThreadStatic] private static bool _inFromHidden; [ThreadStatic] private static bool _inLyingTypeAssembly; + [ThreadStatic] internal static bool InInternalInspection; // Fast-path cache for FromHidden: assemblies known to be engine/system, skip stack walk on these private static readonly HashSet _knownEngineAsms = new(); @@ -48,6 +50,10 @@ public static partial class Hidesey "HarmonySharedState", }; + // Cache for sanitized stack trace strings to avoid repeated split/allocations + private static readonly ConcurrentDictionary _stackTraceCache = new(); + private const int MaxStackTraceCacheSize = 512; + public static void Initialize() { if (_initialized) @@ -842,12 +848,16 @@ public static string LyingLocation(string original, Assembly instance) /// Delegates the per-line decision to Hidesey.StackLineMentionsHidden (see HideseyRegistries), /// which sources hidden names dynamically from the hidden-assembly list + infra keywords + /// dynamic Harmony asm names + manifest StackNames. + /// Uses thread-safe cache to avoid repeated split and string allocations. /// public static string LyingStackTrace(string original) { if (FromHidden()) return original; if (string.IsNullOrEmpty(original)) return original; + if (_stackTraceCache.TryGetValue(original, out var cached)) + return cached; + var sb = new System.Text.StringBuilder(original.Length); bool hadAny = false; foreach (var line in original.Split('\n')) @@ -857,13 +867,43 @@ public static string LyingStackTrace(string original) sb.Append(line); hadAny = true; } - return sb.ToString(); + + var result = sb.ToString(); + if (_stackTraceCache.Count < MaxStackTraceCacheSize) + { + _stackTraceCache.TryAdd(original, result); + } + + return result; } + public static void ClearStackTraceCache() => _stackTraceCache.Clear(); + + /// + /// Filter for Assembly.DefinedTypes getter. + /// Lazily streams TypeInfo elements via yield return, filtering out types belonging to hidden assemblies. + /// Accurately increments HideseyPatches.DefinedTypesFilteredCount per filtered type as the enumerable is consumed. + /// public static IEnumerable LyingDefinedTypes(IEnumerable original) { + if (original == null) return Enumerable.Empty(); if (FromHidden()) return original; - return original.Where(t => !IsHiddenType(t)); + + return FilterIterator(original); + + static IEnumerable FilterIterator(IEnumerable source) + { + foreach (var t in source) + { + if (t == null) continue; + if (IsHiddenType(t)) + { + HideseyPatches.DefinedTypesFilteredCount++; + continue; + } + yield return t; + } + } } private static bool IsInternalReflectionCaller() @@ -902,6 +942,7 @@ private static bool IsInternalReflectionCaller() public static Assembly LyingTypeAssembly(Assembly original, Type instance) { if (_inLyingTypeAssembly) return original; + if (InInternalInspection) return original; if (FromHidden()) return original; _inLyingTypeAssembly = true; diff --git a/Marsey/Stealthsey/HideseyPatches.cs b/Marsey/Stealthsey/HideseyPatches.cs index e368944..84646c6 100644 --- a/Marsey/Stealthsey/HideseyPatches.cs +++ b/Marsey/Stealthsey/HideseyPatches.cs @@ -57,17 +57,17 @@ public static partial class HideseyPatches /// /// Diagnostic: Assembly.DefinedTypes filter count. /// - public static int DefinedTypesFilteredCount { get; private set; } + public static int DefinedTypesFilteredCount { get; internal set; } /// /// Diagnostic: Exception.StackTrace filter count. /// - public static int ExceptionStackTraceFilteredCount { get; private set; } + public static int ExceptionStackTraceFilteredCount { get; internal set; } /// /// Diagnostic: Type.Assembly spoof count. /// - public static int TypeAssemblySpoofedCount { get; private set; } + public static int TypeAssemblySpoofedCount { get; internal set; } public static void Lie(ref T __result) { @@ -106,13 +106,7 @@ public static void LieFindAllTypes(ref IEnumerable __result) public static void LieDefinedTypes(ref IEnumerable __result) { if (__result == null) return; - var original = __result; - var filtered = Hidesey.LyingDefinedTypes(__result); - __result = filtered; - if (!ReferenceEquals(original, filtered)) - { - DefinedTypesFilteredCount++; - } + __result = Hidesey.LyingDefinedTypes(__result); } public static void LieGetType(ref Type? __result, object[] __args) @@ -122,9 +116,9 @@ public static void LieGetType(ref Type? __result, object[] __args) if (originalResult != null && __result == null && __args.Length > 0 && __args[0] is string probe) { + TypeGetTypeBlockedCount++; try { - TypeGetTypeBlockedCount++; LastBlockedTypeGetTypeProbe = probe; LastBlockedTypeGetTypeCaller = FindFirstBusinessCaller(); if (BlockedTypeGetTypeProbes.Count < 100) @@ -157,9 +151,9 @@ public static void LieTryLooseGetType(ref bool __result, ref Type? __1, string? __1 = null; __result = false; + TypeGetTypeBlockedCount++; try { - TypeGetTypeBlockedCount++; LastBlockedTypeGetTypeProbe = __0 ?? "?"; LastBlockedTypeGetTypeCaller = FindFirstBusinessCaller(); if (BlockedTypeGetTypeProbes.Count < 100) @@ -182,9 +176,9 @@ public static void LieLocation(ref string __result, Assembly __instance) if (!ReferenceEquals(originalResult, __result)) { + LocationBlockedCount++; try { - LocationBlockedCount++; LastBlockedLocationCaller = FindFirstBusinessCaller(); if (BlockedLocationProbes.Count < 100) { @@ -209,9 +203,9 @@ public static void LieStackTrace(ref string __result) if (!ReferenceEquals(original, __result) && original != __result) { + StackTraceFilteredCount++; try { - StackTraceFilteredCount++; LastFilteredStackTraceCaller = FindFirstBusinessCaller(); } catch { } @@ -231,11 +225,7 @@ public static void LieExceptionStackTrace(ref string? __result) if (!ReferenceEquals(original, __result) && original != __result) { - try - { - ExceptionStackTraceFilteredCount++; - } - catch { } + ExceptionStackTraceFilteredCount++; } } @@ -252,11 +242,7 @@ public static void LieTypeAssembly(ref Assembly __result, Type __instance) if (!ReferenceEquals(original, __result)) { - try - { - TypeAssemblySpoofedCount++; - } - catch { } + TypeAssemblySpoofedCount++; } } diff --git a/Marsey/Stealthsey/HideseyRegistries.cs b/Marsey/Stealthsey/HideseyRegistries.cs index d1776c6..6f39ac3 100644 --- a/Marsey/Stealthsey/HideseyRegistries.cs +++ b/Marsey/Stealthsey/HideseyRegistries.cs @@ -146,29 +146,37 @@ public static void HideFromStackTrace(params string[] names) public static bool IsHiddenType(Type t) { if (t == null) return false; - if (IsHiddenAsm(t.Assembly)) return true; // whole-assembly hiding (existing semantics) - if (!_anyHiddenTypeNames) return false; // lock-free fast path: nothing registered + Hidesey.InInternalInspection = true; + try + { + if (IsHiddenAsm(t.Assembly)) return true; // whole-assembly hiding (existing semantics) + if (!_anyHiddenTypeNames) return false; // lock-free fast path: nothing registered - var fn = t.FullName; - if (fn == null) return false; + var fn = t.FullName; + if (fn == null) return false; - bool inComps, inOther; - lock (_hideRegLock) - { - inOther = _hiddenSystems.Contains(fn) || _hiddenIoc.Contains(fn); - inComps = !inOther && _hiddenComps.Contains(fn); - } + bool inComps, inOther; + lock (_hideRegLock) + { + inOther = _hiddenSystems.Contains(fn) || _hiddenIoc.Contains(fn); + inComps = !inOther && _hiddenComps.Contains(fn); + } - if (inOther) return true; - if (!inComps) return false; + if (inOther) return true; + if (!inComps) return false; - // Component category — networked guard (reflection + logging done outside the lock). - if (IsNetworkedComponent(t)) + // Component category — networked guard (reflection + logging done outside the lock). + if (IsNetworkedComponent(t)) + { + WarnNetworkedRefusal(fn); + return false; // leave VISIBLE — hiding would break net hash + } + return true; + } + finally { - WarnNetworkedRefusal(fn); - return false; // leave VISIBLE — hiding would break net hash + Hidesey.InInternalInspection = false; } - return true; } /// From 5d89669a9e75d9692a1e37392a2229033de806af Mon Sep 17 00:00:00 2001 From: DobriyKaban Date: Mon, 19 Oct 2026 04:21:34 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=9C=D0=BE=D0=B5=D0=BC=20=D0=9C=D0=B5?= =?UTF-8?q?=D1=82=D0=B0=D0=BB=D1=81=D0=B5=D0=B9=D0=B4=D0=B6=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Marsey.Tests/Hidesey/HideseyTest.cs | 32 --------- Marsey/Stealthsey/Hidesey.cs | 99 +++++--------------------- Marsey/Stealthsey/HideseyPatches.cs | 16 ----- Marsey/Stealthsey/HideseyRegistries.cs | 6 +- 4 files changed, 22 insertions(+), 131 deletions(-) diff --git a/Marsey.Tests/Hidesey/HideseyTest.cs b/Marsey.Tests/Hidesey/HideseyTest.cs index b3d6611..554b9c9 100644 --- a/Marsey.Tests/Hidesey/HideseyTest.cs +++ b/Marsey.Tests/Hidesey/HideseyTest.cs @@ -103,39 +103,7 @@ public void Hidesey_ManifestHidesCvars() Assert.That(filteredCvars, Does.Not.Contain("stealth_cvar"), "Cvar wasn't hidden by the manifest."); } - [Test] - public void Hidesey_HiddenDefinedTypes() - { - // 1. Obtain hidden assembly reference in normal view (before Type.Assembly spoofing kicks in) - Assembly marseyAssembly = typeof(Hidesey).Assembly; - var normalTypes = marseyAssembly.DefinedTypes.ToList(); - Assert.That(normalTypes, Does.Contain(typeof(Hidesey).GetTypeInfo()), - "Hidden caller should see all defined types in Marsey assembly."); - - // 2. Under engine view, DefinedTypes on the hidden assembly must be lazy and filter out hidden types - using (Hidesey.ForceEngineView()) - { - int countBefore = HideseyPatches.DefinedTypesFilteredCount; - - // Getter call: should NOT evaluate or filter yet (demonstrating laziness) - var lazyEnumerable = marseyAssembly.DefinedTypes; - Assert.That(HideseyPatches.DefinedTypesFilteredCount, Is.EqualTo(countBefore), - "DefinedTypes must be lazy and not filter during getter invocation."); - - // Materialization: now it filters and increments the counter - var filteredTypes = lazyEnumerable.ToList(); - Assert.That(filteredTypes, Does.Not.Contain(typeof(Hidesey).GetTypeInfo()), - "Hidden type must be filtered out under engine view."); - Assert.That(HideseyPatches.DefinedTypesFilteredCount, Is.GreaterThan(countBefore), - "DefinedTypesFilteredCount must count the actual filtered types."); - - // 3. Non-hidden assembly (CoreLib) must remain intact - var stringTypes = typeof(string).Assembly.DefinedTypes; - Assert.That(stringTypes, Does.Contain(typeof(string).GetTypeInfo()), - "Non-hidden assembly types must not be filtered."); - } - } [Test] public void Hidesey_HiddenExceptionStackTrace() diff --git a/Marsey/Stealthsey/Hidesey.cs b/Marsey/Stealthsey/Hidesey.cs index 1b0bb72..bf51f75 100644 --- a/Marsey/Stealthsey/Hidesey.cs +++ b/Marsey/Stealthsey/Hidesey.cs @@ -53,6 +53,7 @@ public static partial class Hidesey // Cache for sanitized stack trace strings to avoid repeated split/allocations private static readonly ConcurrentDictionary _stackTraceCache = new(); private const int MaxStackTraceCacheSize = 512; + private static Assembly? _cachedRobustClientAsm; public static void Initialize() { @@ -138,6 +139,7 @@ private static void Hide(Assembly marsey) { Facade.Cloak(marsey); _hideseys.Add(marsey); + ClearStackTraceCache(); } [HideLevelRequirement(HideLevel.Normal)] @@ -245,9 +247,6 @@ private static void Perjurize() // Closes Exception.StackTrace leak. PerjurizeExceptionStackTrace(); - // Closes Assembly.DefinedTypes leak. - PerjurizeDefinedTypes(); - // Closes Type.Assembly (GetType().Assembly) leak. PerjurizeTypeAssembly(); } @@ -458,53 +457,6 @@ private static void PerjurizeExceptionStackTrace() } } - private static void PerjurizeDefinedTypes() - { - MethodInfo? postfix = typeof(HideseyPatches).GetMethod(nameof(HideseyPatches.LieDefinedTypes), - BindingFlags.Public | BindingFlags.Static); - if (postfix == null) - { - MarseyLogger.Log(MarseyLogger.LogType.WARN, "HideseyPatches.LieDefinedTypes not found"); - return; - } - - var targets = new List(); - var baseGetter = typeof(Assembly).GetProperty("DefinedTypes")?.GetGetMethod(); - if (baseGetter != null && !baseGetter.IsAbstract) targets.Add(baseGetter); - - var runtimeAsm = Assembly.GetExecutingAssembly().GetType(); - var runtimeGetter = runtimeAsm.GetProperty("DefinedTypes", BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)?.GetGetMethod() - ?? runtimeAsm.GetProperty("DefinedTypes")?.GetGetMethod(); - if (runtimeGetter != null && !runtimeGetter.IsAbstract && !targets.Contains(runtimeGetter)) - targets.Add(runtimeGetter); - - var harm = HarmonyManager.GetHarmony(); - int hooked = 0; - foreach (var target in targets) - { - try - { - var hm = new HarmonyMethod(postfix) - { - before = Array.Empty(), - after = Array.Empty(), - priority = Priority.Normal, - }; - harm.Patch(target, postfix: hm); - MarseyLogger.Log(MarseyLogger.LogType.INFO, $"{target.DeclaringType?.Name}.DefinedTypes hook installed"); - hooked++; - } - catch (Exception ex) - { - MarseyLogger.Log(MarseyLogger.LogType.WARN, $"DefinedTypes hook on {target.DeclaringType?.Name} failed: {ex.Message}"); - } - } - - if (hooked == 0) - { - MarseyLogger.Log(MarseyLogger.LogType.FATL, "PerjurizeDefinedTypes: no DefinedTypes getter targets hooked"); - } - } private static void PerjurizeTypeAssembly() { @@ -860,14 +812,22 @@ public static string LyingStackTrace(string original) var sb = new System.Text.StringBuilder(original.Length); bool hadAny = false; + bool removedAny = false; foreach (var line in original.Split('\n')) { - if (StackLineMentionsHidden(line)) continue; + if (StackLineMentionsHidden(line)) + { + removedAny = true; + continue; + } if (hadAny) sb.Append('\n'); sb.Append(line); hadAny = true; } + if (!removedAny) + return original; + var result = sb.ToString(); if (_stackTraceCache.Count < MaxStackTraceCacheSize) { @@ -879,32 +839,6 @@ public static string LyingStackTrace(string original) public static void ClearStackTraceCache() => _stackTraceCache.Clear(); - /// - /// Filter for Assembly.DefinedTypes getter. - /// Lazily streams TypeInfo elements via yield return, filtering out types belonging to hidden assemblies. - /// Accurately increments HideseyPatches.DefinedTypesFilteredCount per filtered type as the enumerable is consumed. - /// - public static IEnumerable LyingDefinedTypes(IEnumerable original) - { - if (original == null) return Enumerable.Empty(); - if (FromHidden()) return original; - - return FilterIterator(original); - - static IEnumerable FilterIterator(IEnumerable source) - { - foreach (var t in source) - { - if (t == null) continue; - if (IsHiddenType(t)) - { - HideseyPatches.DefinedTypesFilteredCount++; - continue; - } - yield return t; - } - } - } private static bool IsInternalReflectionCaller() { @@ -953,10 +887,13 @@ public static Assembly LyingTypeAssembly(Assembly original, Type instance) if (IsHiddenAsm(original) || IsHiddenType(instance)) { - var robust = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == "Robust.Client"); - if (robust != null && !robust.IsDynamic) - return robust; + if (_cachedRobustClientAsm == null || _cachedRobustClientAsm.IsDynamic) + { + _cachedRobustClientAsm = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.GetName().Name == "Robust.Client" && !a.IsDynamic); + } + if (_cachedRobustClientAsm != null) + return _cachedRobustClientAsm; return typeof(object).Assembly; } diff --git a/Marsey/Stealthsey/HideseyPatches.cs b/Marsey/Stealthsey/HideseyPatches.cs index 84646c6..febc431 100644 --- a/Marsey/Stealthsey/HideseyPatches.cs +++ b/Marsey/Stealthsey/HideseyPatches.cs @@ -54,11 +54,6 @@ public static partial class HideseyPatches public static string? LastContentFindFilesCaller { get; private set; } public static List ContentFindFilesBlockedNames { get; } = new(); - /// - /// Diagnostic: Assembly.DefinedTypes filter count. - /// - public static int DefinedTypesFilteredCount { get; internal set; } - /// /// Diagnostic: Exception.StackTrace filter count. /// @@ -80,7 +75,6 @@ public static void Lie(ref T __result) assemblyLoadContextEnumerable), AssemblyName[] assemblyNames => (T)(object)Hidesey.LyingReference(assemblyNames), Type[] types => (T)(object)Hidesey.LyingTyper(types), - IEnumerable typeInfos => (T)(object)Hidesey.LyingDefinedTypes(typeInfos), _ => throw new InvalidOperationException("Unsupported type for LiePatch") }; } @@ -99,16 +93,6 @@ public static void LieFindAllTypes(ref IEnumerable __result) TryCaptureFindAllTypes(filtered); } - /// - /// Postfix on Assembly.DefinedTypes getter. - /// Filters out TypeInfo instances declared in hidden assemblies/types. - /// - public static void LieDefinedTypes(ref IEnumerable __result) - { - if (__result == null) return; - __result = Hidesey.LyingDefinedTypes(__result); - } - public static void LieGetType(ref Type? __result, object[] __args) { var originalResult = __result; diff --git a/Marsey/Stealthsey/HideseyRegistries.cs b/Marsey/Stealthsey/HideseyRegistries.cs index 6f39ac3..1f2f2d8 100644 --- a/Marsey/Stealthsey/HideseyRegistries.cs +++ b/Marsey/Stealthsey/HideseyRegistries.cs @@ -87,6 +87,7 @@ public static void Apply(HideManifest manifest) MergeInto(_stackHiddenExtra, manifest.StackNames); _anyHiddenTypeNames = _hiddenSystems.Count > 0 || _hiddenComps.Count > 0 || _hiddenIoc.Count > 0; + ClearStackTraceCache(); } MarseyLogger.Log(MarseyLogger.LogType.DEBG, @@ -146,6 +147,7 @@ public static void HideFromStackTrace(params string[] names) public static bool IsHiddenType(Type t) { if (t == null) return false; + var prev = Hidesey.InInternalInspection; Hidesey.InInternalInspection = true; try { @@ -175,7 +177,7 @@ public static bool IsHiddenType(Type t) } finally { - Hidesey.InInternalInspection = false; + Hidesey.InInternalInspection = prev; } } @@ -206,7 +208,7 @@ public static IEnumerable LyingCvars(IEnumerable original) { bool hidden; lock (_hideRegLock) hidden = name != null && _hiddenCvars.Contains(name); - if (!hidden) result.Add(name); + if (!hidden && name != null) result.Add(name); } return result; } From 91d769cce773891605ef05d7b887d1748f7741ff Mon Sep 17 00:00:00 2001 From: DobriyKaban <165943472+DobriyKaban@users.noreply.github.com> Date: Mon, 19 Oct 2026 04:49:24 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=A5=D1=83=D0=BB=D0=B8=20=D0=B2=D1=8B=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B1=D1=8B=D0=BB=D0=B8=20=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=B8=D1=82=D1=8C=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E?= =?UTF-8?q?=20=D0=BE=D0=BF=D0=B5=D0=B7=D0=B4=D0=BE=D0=BB=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Launcher.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Launcher.props b/Launcher.props index a37c4ea..271e298 100644 --- a/Launcher.props +++ b/Launcher.props @@ -10,7 +10,7 @@ 5. publish-release.yml GitHub Actions workflow --> net10.0 - 0.38.0.50 + 0.38.0.52 true