Skip to content
This repository was archived by the owner on Feb 28, 2024. It is now read-only.

Commit b73969b

Browse files
committed
Fix compatibility hash spoofing when plugins are present. Resolves #58.
1 parent 1c01173 commit b73969b

File tree

4 files changed

+153
-41
lines changed

4 files changed

+153
-41
lines changed

NeosModLoader/AssemblyLoader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ internal static class AssemblyLoader
4848
Assembly assembly;
4949
try
5050
{
51-
Logger.MsgInternal( $"load assembly {filename} with sha256hash: {Util.GenerateSHA256(filepath)}");
51+
Logger.MsgInternal($"load assembly {filename} with sha256hash: {Util.GenerateSHA256(filepath)}");
5252
assembly = Assembly.LoadFile(filepath);
5353
}
5454
catch (Exception e)

NeosModLoader/ModLoader.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace NeosModLoader
1010
{
1111
public class ModLoader
1212
{
13-
internal const string VERSION_CONSTANT = "1.11.3";
13+
internal const string VERSION_CONSTANT = "1.12.0";
1414
/// <summary>
1515
/// NeosModLoader's version
1616
/// </summary>

NeosModLoader/NeosVersionReset.cs

Lines changed: 142 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
using BaseX;
12
using FrooxEngine;
23
using HarmonyLib;
34
using System;
45
using System.Collections.Generic;
6+
using System.ComponentModel;
57
using System.IO;
8+
using System.Linq;
69
using System.Reflection;
710
using System.Reflection.Emit;
811
using System.Runtime.CompilerServices;
@@ -12,65 +15,159 @@ namespace NeosModLoader
1215
{
1316
internal class NeosVersionReset
1417
{
18+
// used when AdvertiseVersion == true
19+
private const string NEOS_MOD_LOADER = "NeosModLoader.dll";
20+
1521
internal static void Initialize()
1622
{
1723
ModLoaderConfiguration config = ModLoaderConfiguration.Get();
1824
Engine engine = Engine.Current;
1925

2026
List<string> extraAssemblies = Engine.ExtraAssemblies;
2127
string assemblyFilename = Path.GetFileName(Assembly.GetExecutingAssembly().Location);
22-
bool nmlPresent = extraAssemblies.Contains(assemblyFilename);
28+
bool nmlPresent = extraAssemblies.Remove(assemblyFilename);
2329

2430
if (!nmlPresent)
2531
{
2632
throw new Exception($"Assertion failed: Engine.ExtraAssemblies did not contain \"{assemblyFilename}\"");
2733
}
2834

29-
bool otherPluginsPresent = extraAssemblies.Count > 1;
30-
bool shouldSpoofCompatibility = !otherPluginsPresent || config.Unsafe;
31-
bool shouldSpoofVersion = !config.AdvertiseVersion && shouldSpoofCompatibility;
35+
// get all PostX'd assemblies. This is useful, as plugins can't NOT be PostX'd.
36+
Assembly[] postxedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
37+
.Where(IsPostXProcessed)
38+
.ToArray();
3239

33-
if (shouldSpoofVersion)
34-
{
35-
// we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value
36-
// this is to prevent the case where a player simply doesn't know their version string is wrong
37-
extraAssemblies.Clear();
38-
if (!SpoofVersionString(engine))
40+
string potentialPlugins = postxedAssemblies
41+
.Select(a => Path.GetFileName(a.Location))
42+
.Join(delimiter: ", ");
43+
44+
Logger.DebugFuncInternal(() => $"Found {postxedAssemblies.Length} potential plugins: {potentialPlugins}");
45+
46+
HashSet<Assembly> expectedPostXAssemblies = GetExpectedPostXAssemblies();
47+
48+
// attempt to map the PostX'd assemblies to Neos's plugin list
49+
Dictionary<string, Assembly> plugins = new Dictionary<string, Assembly>(postxedAssemblies.Length);
50+
Assembly[] unmatchedAssemblies = postxedAssemblies
51+
.Where(assembly =>
3952
{
40-
Logger.WarnInternal("Version string spoofing failed");
41-
return;
42-
}
53+
string filename = Path.GetFileName(assembly.Location);
54+
if (extraAssemblies.Contains(filename))
55+
{
56+
// okay, the assembly's filename is in the plugin list. It's probably a plugin.
57+
plugins.Add(filename, assembly);
58+
return false;
59+
}
60+
else
61+
{
62+
// remove certain expected assemblies from the "unmatchedAssemblies" naughty list
63+
return !expectedPostXAssemblies.Contains(assembly);
64+
}
65+
})
66+
.ToArray();
67+
68+
string actualPlugins = plugins.Keys.Join(delimiter: ", ");
69+
Logger.DebugFuncInternal(() => $"Found {plugins.Count} actual plugins: {actualPlugins}");
70+
71+
// warn about the assemblies we couldn't map to plugins
72+
foreach (Assembly assembly in unmatchedAssemblies)
73+
{
74+
Logger.WarnInternal($"Unexpected PostX'd assembly: \"{assembly.Location}\". If this is a plugin, then my plugin-detection code is faulty.");
4375
}
44-
else
76+
77+
// warn about the plugins we couldn't map to assemblies
78+
HashSet<string> unmatchedPlugins = new(extraAssemblies);
79+
unmatchedPlugins.ExceptWith(plugins.Keys); // remove all matched plugins
80+
foreach (string plugin in unmatchedPlugins)
4581
{
46-
Logger.MsgInternal("Version string not being spoofed due to config.");
82+
Logger.ErrorInternal($"Unmatched plugin: \"{plugin}\". NML could not find the assembly for this plugin, therefore NML cannot properly calculate the compatibility hash.");
4783
}
4884

49-
if (shouldSpoofCompatibility)
85+
// flags used later to determine how to spoof
86+
bool includePluginsInHash = true;
87+
88+
// if unsafe is true, we should pretend there are no plugins and spoof everything
89+
if (config.Unsafe)
5090
{
51-
if (!SpoofCompatibilityHash(engine))
91+
if (!config.AdvertiseVersion)
5292
{
53-
Logger.WarnInternal("Compatibility hash spoofing failed");
54-
return;
93+
extraAssemblies.Clear();
5594
}
95+
includePluginsInHash = false;
96+
Logger.WarnInternal("Unsafe mode is enabled! Not that you had a warranty, but now it's DOUBLE void!");
5697
}
57-
else
98+
// else if unmatched plugins are present, we should not spoof anything
99+
else if (unmatchedPlugins.Count != 0)
58100
{
59-
Logger.WarnInternal("Version spoofing was not performed due to another plugin being present! Either remove unknown plugins or enable NeosModLoader's unsafe mode.");
101+
Logger.ErrorInternal("Version spoofing was not performed due to some plugins having missing assemblies.");
102+
return;
103+
}
104+
// else we should spoof normally
105+
106+
107+
// get plugin assemblies sorted in the same order Neos sorted them.
108+
List<Assembly> sortedPlugins = extraAssemblies
109+
.Select(path => plugins[path])
110+
.ToList();
111+
112+
if (config.AdvertiseVersion)
113+
{
114+
// put NML back in the version string
115+
Logger.MsgInternal($"Adding {NEOS_MOD_LOADER} to version string because you have AdvertiseVersion set to true.");
116+
extraAssemblies.Insert(0, NEOS_MOD_LOADER);
117+
}
118+
119+
// we intentionally attempt to set the version string first, so if it fails the compatibilty hash is left on the original value
120+
// this is to prevent the case where a player simply doesn't know their version string is wrong
121+
if (!SpoofVersionString(engine))
122+
{
123+
Logger.WarnInternal("Version string spoofing failed");
124+
return;
125+
}
126+
127+
if (!SpoofCompatibilityHash(engine, sortedPlugins, includePluginsInHash))
128+
{
129+
Logger.WarnInternal("Compatibility hash spoofing failed");
60130
return;
61131
}
62132

63133
Logger.MsgInternal("Compatibility hash spoofing succeeded");
64134
}
65135

66-
private static bool SpoofCompatibilityHash(Engine engine)
136+
private static bool IsPostXProcessed(Assembly assembly)
137+
{
138+
return assembly.Modules // in practice there will only be one module, and it will have the dll's name
139+
.SelectMany(module => module.GetCustomAttributes<DescriptionAttribute>())
140+
.Where(IsPostXProcessedAttribute)
141+
.Any();
142+
}
143+
144+
private static bool IsPostXProcessedAttribute(DescriptionAttribute descriptionAttribute)
145+
{
146+
return descriptionAttribute.Description == "POSTX_PROCESSED";
147+
}
148+
149+
// get all the non-plugin PostX'd assemblies we expect to exist
150+
private static HashSet<Assembly> GetExpectedPostXAssemblies()
151+
{
152+
List<Assembly?> list = new()
153+
{
154+
Type.GetType("FrooxEngine.IComponent, FrooxEngine")?.Assembly,
155+
Type.GetType("BusinessX.NeosClassroom, BusinessX")?.Assembly,
156+
Assembly.GetExecutingAssembly(),
157+
};
158+
return list
159+
.Where(assembly => assembly != null)
160+
.ToHashSet()!;
161+
}
162+
163+
private static bool SpoofCompatibilityHash(Engine engine, List<Assembly> plugins, bool includePluginsInHash)
67164
{
68165
string vanillaCompatibilityHash;
69166
int? vanillaProtocolVersionMaybe = GetVanillaProtocolVersion();
70167
if (vanillaProtocolVersionMaybe is int vanillaProtocolVersion)
71168
{
72169
Logger.DebugFuncInternal(() => $"Vanilla protocol version is {vanillaProtocolVersion}");
73-
vanillaCompatibilityHash = CalculateCompatibilityHash(vanillaProtocolVersion);
170+
vanillaCompatibilityHash = CalculateCompatibilityHash(vanillaProtocolVersion, plugins, includePluginsInHash);
74171
return SetCompatibilityHash(engine, vanillaCompatibilityHash);
75172
}
76173
else
@@ -80,10 +177,21 @@ private static bool SpoofCompatibilityHash(Engine engine)
80177
}
81178
}
82179

83-
private static string CalculateCompatibilityHash(int ProtocolVersion)
180+
private static string CalculateCompatibilityHash(int ProtocolVersion, List<Assembly> plugins, bool includePluginsInHash)
84181
{
85182
using MD5CryptoServiceProvider cryptoServiceProvider = new();
86-
byte[] hash = cryptoServiceProvider.ComputeHash(new MemoryStream(BitConverter.GetBytes(ProtocolVersion)));
183+
ConcatenatedStream inputStream = new();
184+
inputStream.EnqueueStream(new MemoryStream(BitConverter.GetBytes(ProtocolVersion)));
185+
if (includePluginsInHash)
186+
{
187+
foreach (Assembly plugin in plugins)
188+
{
189+
FileStream fileStream = File.OpenRead(plugin.Location);
190+
fileStream.Seek(375L, SeekOrigin.Current);
191+
inputStream.EnqueueStream(fileStream);
192+
}
193+
}
194+
byte[] hash = cryptoServiceProvider.ComputeHash(inputStream);
87195
return Convert.ToBase64String(hash);
88196
}
89197

@@ -109,20 +217,18 @@ private static bool SetCompatibilityHash(Engine engine, string Target)
109217

110218
private static bool SpoofVersionString(Engine engine)
111219
{
112-
// calculate correct version string
113-
string target = Engine.VersionNumber;
220+
string cachedVersion = engine.VersionString;
114221

115-
if (!engine.VersionString.Equals(target))
222+
FieldInfo field = AccessTools.DeclaredField(engine.GetType(), "_versionString");
223+
if (field == null)
116224
{
117-
FieldInfo field = AccessTools.DeclaredField(engine.GetType(), "_versionString");
118-
if (field == null)
119-
{
120-
Logger.WarnInternal("Unable to write Engine._versionString");
121-
return false;
122-
}
123-
Logger.DebugFuncInternal(() => $"Changing version string from {engine.VersionString} to {target}");
124-
field.SetValue(engine, target);
225+
Logger.WarnInternal("Unable to write Engine._versionString");
226+
return false;
125227
}
228+
// null the cached value
229+
field.SetValue(engine, null);
230+
231+
Logger.DebugFuncInternal(() => $"Changing version string from {cachedVersion} to {engine.VersionString}");
126232
return true;
127233
}
128234

NeosModLoader/Util.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
using System;
2-
using System.IO;
2+
using System.Collections.Generic;
33
using System.Diagnostics;
4+
using System.IO;
45
using System.Reflection;
6+
using System.Security.Cryptography;
57
using System.Threading;
68
using System.Threading.Tasks;
7-
using System.Security.Cryptography;
89

910
namespace NeosModLoader
1011
{
@@ -74,7 +75,7 @@ private static bool IsCompletedSuccessfully(this Task t)
7475
{
7576
return t.IsCompleted && !t.IsFaulted && !t.IsCanceled;
7677
}
77-
78+
7879
//credit to delta for this method https://github.com/XDelta/
7980
internal static string GenerateSHA256(string filepath)
8081
{
@@ -84,5 +85,10 @@ internal static string GenerateSHA256(string filepath)
8485
return BitConverter.ToString(hash).Replace("-", "");
8586
}
8687

88+
internal static HashSet<T> ToHashSet<T>(this IEnumerable<T> source, IEqualityComparer<T>? comparer = null)
89+
{
90+
return new HashSet<T>(source, comparer);
91+
}
92+
8793
}
8894
}

0 commit comments

Comments
 (0)