From 1cb58f342f1fd141d3db7a6c5dad0a194db8ed49 Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Fri, 12 Jun 2026 21:18:41 +0300 Subject: [PATCH 1/7] add EnvFileViewer tool --- tools/EnvFileViewer/EnvFileViewer.slnx | 3 + .../EnvFileViewer/EnvFileModel.cs | 24 ++++ .../EnvFileViewer/EnvFileParser.cs | 135 ++++++++++++++++++ .../EnvFileViewer/EnvFileViewer.csproj | 10 ++ tools/EnvFileViewer/EnvFileViewer/Program.cs | 21 +++ 5 files changed, 193 insertions(+) create mode 100644 tools/EnvFileViewer/EnvFileViewer.slnx create mode 100644 tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs create mode 100644 tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs create mode 100644 tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj create mode 100644 tools/EnvFileViewer/EnvFileViewer/Program.cs diff --git a/tools/EnvFileViewer/EnvFileViewer.slnx b/tools/EnvFileViewer/EnvFileViewer.slnx new file mode 100644 index 0000000..8054afc --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer.slnx @@ -0,0 +1,3 @@ + + + diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs new file mode 100644 index 0000000..174fb15 --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs @@ -0,0 +1,24 @@ +namespace EnvFileViewer; + +public class EnvFileModel +{ + public bool SizeValid { get; set; } + public bool HeaderValid { get; set; } + public bool FooterValid { get; set; } + public bool SignatureValid { get; set; } + + public int ModuleCount { get; set; } + public uint KeySeed { get; set; } + + public VersionInfo Version { get; set; } + public FeatureFlags Flags { get; set; } = new FeatureFlags(); +} + +public class FeatureFlags +{ + public bool ModuleCheckEnabled { get; set; } + public bool DebugCheckEnabled { get; set; } + public bool TimestampCheckEnabled { get; set; } +} + +public record VersionInfo(int Major, int Minor, int Patch); \ No newline at end of file diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs new file mode 100644 index 0000000..1b91848 --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs @@ -0,0 +1,135 @@ +using System.Security.Cryptography; + +namespace EnvFileViewer; + +internal class EnvFileParser +{ + private const int VersionMajorOffset = 10; + private const int VersionMinorOffset = 11; + private const int VersionPatchOffset = 12; + private const byte VersionMajorXorValue = 0x4A; + private const byte VersionMinorXorValue = 0xC8; + private const byte VersionPatchXorValue = 0x1C; + + private const int ModuleCountOffset = 8; + private const byte ModuleCountXorValue = 0xA7; + + private const int KeySeedOffset = 16 * 1024; + private const uint KeySeedXorValue = 0x3B527B1C; + + private const int FeatureFlagsOffset = 60; + private const byte FeatureFlagsXorValue = 0x5C; + private const byte ModuleCheckFlag = 0x01; + private const byte DebugCheckFlag = 0x02; + private const byte TimestampCheckFlag = 0x04; + + + public static EnvFileModel Parse(string filePath) + { + var content = System.IO.File.ReadAllBytes(filePath); + + var version = GetVersion(content); + + var model = new EnvFileModel + { + SizeValid = ValidateSize(content), + HeaderValid = ValidateHeader(content), + FooterValid = ValidateFooter(content), + SignatureValid = ValidateSignature(content), + Version = GetVersion(content), + ModuleCount = GetModuleCount(content), + KeySeed = GetKeySeed(content), + Flags = GetFeatureFlags(content) + }; + return model; + } + + private static bool ValidateSize(byte[] content) + { + return content.Length == 20 * 1024; + } + + private static bool ValidateHeader(byte[] content) + { + return content.Length >= 4 && + content[0] == 0x31 && + content[1] == 0x9A && + content[2] == 0xFF && + content[3] == 0xA2; + } + + private static bool ValidateFooter(byte[] content) + { + return content.Length >= 4 && + content[^4] == 0x44 && + content[^3] == 0x5A && + content[^2] == 0x4D && + content[^1] == 0xAA; + } + + private static bool ValidateSignature(byte[] content) + { + byte[] public_key = + [ + 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A, + 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03, 0x42, 0x00, 0x04, 0x5E, 0xD9, 0xCB, 0x24, 0x9E, + 0xC0, 0x43, 0x16, 0x04, 0x26, 0xCC, 0xCC, 0xE2, 0x2F, 0xE5, 0x31, 0x0D, 0x06, 0x62, 0x7D, 0xE1, + 0x8A, 0x06, 0x83, 0xEF, 0x0B, 0x4E, 0xFA, 0xEB, 0x2C, 0x7F, 0xE1, 0x69, 0xFC, 0x4B, 0x13, 0x08, + 0xD3, 0x0F, 0xC1, 0x97, 0x52, 0x99, 0x98, 0x36, 0x5D, 0x87, 0x7C, 0x32, 0x0D, 0x44, 0x2F, 0x13, + 0xEC, 0xB4, 0xE3, 0xB3, 0x66, 0x84, 0x61, 0x63, 0xF2, 0xBE, 0xF3 + ]; + + // generate a new ECDSA key and load the public key from public_key + ECDsa ecdsa = ECDsa.Create(); + ecdsa.ImportSubjectPublicKeyInfo(public_key, out _); + //ecdsa.ImportECPrivateKey(PrivateKey, out _); + + //using var signingKey = ECDsa.Create(); + //signingKey.ImportECPrivateKey(EnvFileKeyProvider.PrivateKey, out _); + //var signature = signingKey.SignData(new ReadOnlySpan(data, 0, 18 * 1024), hashAlgorithm); + //Array.Copy(signature, 0, data, 18 * 1024, signature.Length); + + // validate the signature using the public key + var hashAlgorithm = HashAlgorithmName.SHA256; + return ecdsa.VerifyData(new ReadOnlySpan(content, 0, 18 * 1024), new ReadOnlySpan(content, 18 * 1024, 64), hashAlgorithm); + } + + private static VersionInfo GetVersion(byte[] content) + { + int major = content[VersionMajorOffset] ^ VersionMajorXorValue; + int minor = content[VersionMinorOffset] ^ VersionMinorXorValue; + int patch = content[VersionPatchOffset] ^ VersionPatchXorValue; + return new VersionInfo(major, minor, patch); + } + + private static byte GetModuleCount(byte[] content) + { + return (byte)(content[ModuleCountOffset] ^ ModuleCountXorValue); + } + + private static uint GetKeySeed(byte[] content) + { + if (content.Length < KeySeedOffset + 4) + return 0; + uint result = + (uint)(content[KeySeedOffset + 0] << 24 | + content[KeySeedOffset + 1] << 16 | + content[KeySeedOffset + 2] << 8 | + content[KeySeedOffset + 3]); + + return result ^ KeySeedXorValue; + } + + private static FeatureFlags GetFeatureFlags(byte[] content) + { + if (content.Length < FeatureFlagsOffset + 1) + return new FeatureFlags(); + byte flagsByte = (byte)(content[FeatureFlagsOffset] ^ FeatureFlagsXorValue); + return new FeatureFlags + { + ModuleCheckEnabled = (flagsByte & ModuleCheckFlag) != 0, + DebugCheckEnabled = (flagsByte & DebugCheckFlag) != 0, + TimestampCheckEnabled = (flagsByte & TimestampCheckFlag) != 0 + }; + } +} diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj b/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj new file mode 100644 index 0000000..ed9781c --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj @@ -0,0 +1,10 @@ + + + + Exe + net10.0 + enable + enable + + + diff --git a/tools/EnvFileViewer/EnvFileViewer/Program.cs b/tools/EnvFileViewer/EnvFileViewer/Program.cs new file mode 100644 index 0000000..5ae97a5 --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer/Program.cs @@ -0,0 +1,21 @@ +using EnvFileViewer; + +var noWait = args.Contains("--no-wait"); +var filePath = args.FirstOrDefault(a => a != "--no-wait"); + +if (string.IsNullOrWhiteSpace(filePath)) +{ + Console.WriteLine("Usage: EnvFileViewer [--no-wait]"); + return; +} + +var result = EnvFileParser.Parse(filePath); +var resultText = System.Text.Json.JsonSerializer.Serialize(result, new System.Text.Json.JsonSerializerOptions { WriteIndented = true }); + +Console.WriteLine(resultText); + +if (!noWait) +{ + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(true); +} From ec707fb544b13ce94b39fee00914d23058167044 Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Mon, 15 Jun 2026 12:45:53 +0300 Subject: [PATCH 2/7] include ModuleCheckInfo --- .../EnvFileViewer/EnvFileModel.cs | 25 +++++++++----- .../EnvFileViewer/EnvFileParser.cs | 34 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs index 174fb15..1ee3a44 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs @@ -1,4 +1,6 @@ -namespace EnvFileViewer; +using System.Text.Json.Serialization; + +namespace EnvFileViewer; public class EnvFileModel { @@ -11,14 +13,19 @@ public class EnvFileModel public uint KeySeed { get; set; } public VersionInfo Version { get; set; } - public FeatureFlags Flags { get; set; } = new FeatureFlags(); + public FeatureFlags Flags { get; set; } = new FeatureFlags(false, false, false); + public ModuleCheckInfo ModuleCheckInfo { get; set; } = new ModuleCheckInfo(ModuleCheckType.AtLeast, 0); } -public class FeatureFlags -{ - public bool ModuleCheckEnabled { get; set; } - public bool DebugCheckEnabled { get; set; } - public bool TimestampCheckEnabled { get; set; } -} +public record FeatureFlags(bool ModuleCheckEnabled, bool DebugCheckEnabled, bool TimestampCheckEnabled); -public record VersionInfo(int Major, int Minor, int Patch); \ No newline at end of file +public record ModuleCheckInfo(ModuleCheckType CheckType, byte CheckValue); + +public record VersionInfo(int Major, int Minor, int Patch); + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ModuleCheckType : byte +{ + All = 1, + AtLeast = 2 +} \ No newline at end of file diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs index 1b91848..f4f7d26 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs @@ -23,6 +23,10 @@ internal class EnvFileParser private const byte DebugCheckFlag = 0x02; private const byte TimestampCheckFlag = 0x04; + private const int MatchTypeOffset = 25; + private const byte MatchTypeXorValue = 0x9F; + private const int MatchValueOffset = 29; + private const byte MatchValueXorValue = 0xD3; public static EnvFileModel Parse(string filePath) { @@ -39,7 +43,8 @@ public static EnvFileModel Parse(string filePath) Version = GetVersion(content), ModuleCount = GetModuleCount(content), KeySeed = GetKeySeed(content), - Flags = GetFeatureFlags(content) + Flags = GetFeatureFlags(content), + ModuleCheckInfo = GetModuleCheckInfo(content), }; return model; } @@ -79,15 +84,8 @@ private static bool ValidateSignature(byte[] content) 0xEC, 0xB4, 0xE3, 0xB3, 0x66, 0x84, 0x61, 0x63, 0xF2, 0xBE, 0xF3 ]; - // generate a new ECDSA key and load the public key from public_key ECDsa ecdsa = ECDsa.Create(); ecdsa.ImportSubjectPublicKeyInfo(public_key, out _); - //ecdsa.ImportECPrivateKey(PrivateKey, out _); - - //using var signingKey = ECDsa.Create(); - //signingKey.ImportECPrivateKey(EnvFileKeyProvider.PrivateKey, out _); - //var signature = signingKey.SignData(new ReadOnlySpan(data, 0, 18 * 1024), hashAlgorithm); - //Array.Copy(signature, 0, data, 18 * 1024, signature.Length); // validate the signature using the public key var hashAlgorithm = HashAlgorithmName.SHA256; @@ -123,13 +121,19 @@ private static uint GetKeySeed(byte[] content) private static FeatureFlags GetFeatureFlags(byte[] content) { if (content.Length < FeatureFlagsOffset + 1) - return new FeatureFlags(); + return new FeatureFlags(false, false, false); byte flagsByte = (byte)(content[FeatureFlagsOffset] ^ FeatureFlagsXorValue); - return new FeatureFlags - { - ModuleCheckEnabled = (flagsByte & ModuleCheckFlag) != 0, - DebugCheckEnabled = (flagsByte & DebugCheckFlag) != 0, - TimestampCheckEnabled = (flagsByte & TimestampCheckFlag) != 0 - }; + return new FeatureFlags((flagsByte & ModuleCheckFlag) != 0, (flagsByte & DebugCheckFlag) != 0, (flagsByte & TimestampCheckFlag) != 0); + } + + private static ModuleCheckInfo GetModuleCheckInfo(byte[] content) + { + if (content.Length < MatchValueOffset + 1) + return new ModuleCheckInfo(ModuleCheckType.AtLeast, 0); + byte checkTypeByte = (byte)(content[MatchTypeOffset] ^ MatchTypeXorValue); + byte checkValueByte = (byte)(content[MatchValueOffset] ^ MatchValueXorValue); + var checkType = (ModuleCheckType)checkTypeByte; + + return new ModuleCheckInfo(checkType, checkValueByte); } } From b02f3ddf516b13d671f02b6c1037a2b4269ce370 Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Wed, 17 Jun 2026 15:37:32 +0300 Subject: [PATCH 3/7] Add TimestampUtc --- .../EnvFileViewer/EnvFileModel.cs | 1 + .../EnvFileViewer/EnvFileParser.cs | 30 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs index 1ee3a44..35c4c44 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs @@ -15,6 +15,7 @@ public class EnvFileModel public VersionInfo Version { get; set; } public FeatureFlags Flags { get; set; } = new FeatureFlags(false, false, false); public ModuleCheckInfo ModuleCheckInfo { get; set; } = new ModuleCheckInfo(ModuleCheckType.AtLeast, 0); + public DateTime? TimestampUtc { get; set; } } public record FeatureFlags(bool ModuleCheckEnabled, bool DebugCheckEnabled, bool TimestampCheckEnabled); diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs index f4f7d26..f544b5c 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs @@ -1,4 +1,5 @@ -using System.Security.Cryptography; +using System; +using System.Security.Cryptography; namespace EnvFileViewer; @@ -28,6 +29,9 @@ internal class EnvFileParser private const int MatchValueOffset = 29; private const byte MatchValueXorValue = 0xD3; + private const int TimestampOffset = 30; + private const long TimestampXorValue = 0x0A5DFB499C6830CD; + public static EnvFileModel Parse(string filePath) { var content = System.IO.File.ReadAllBytes(filePath); @@ -45,6 +49,7 @@ public static EnvFileModel Parse(string filePath) KeySeed = GetKeySeed(content), Flags = GetFeatureFlags(content), ModuleCheckInfo = GetModuleCheckInfo(content), + TimestampUtc = GetTimestampUtc(content) }; return model; } @@ -118,6 +123,29 @@ private static uint GetKeySeed(byte[] content) return result ^ KeySeedXorValue; } + private static DateTime? GetTimestampUtc(byte[] content) + { + if (content.Length < TimestampOffset + 8) + return null; + + try + { + var slice = new byte[8]; + Array.Copy(content, TimestampOffset, slice, 0, 8); + + if (BitConverter.IsLittleEndian) + Array.Reverse(slice); + + long timestamp = BitConverter.ToInt64(slice, 0); + timestamp ^= TimestampXorValue; + return DateTimeOffset.FromUnixTimeSeconds(timestamp).UtcDateTime; + } + catch + { + return null; + } + } + private static FeatureFlags GetFeatureFlags(byte[] content) { if (content.Length < FeatureFlagsOffset + 1) From fb5456892ab043872c6ba7bfa37de87513f0f2d6 Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Wed, 1 Jul 2026 20:42:41 +0300 Subject: [PATCH 4/7] EnvFileViewer - validate modules --- tools/EnvFileViewer/EnvFileViewer.slnx | 2 + .../EnvFileViewer/EnvFileModel.cs | 32 --- .../EnvFileViewer/EnvFileViewer.csproj | 4 + tools/EnvFileViewer/EnvFileViewer/Program.cs | 2 +- tools/EnvFileViewer/EnvLib/EnvFileBuilder.cs | 206 ++++++++++++++++++ .../EnvLib/EnvFileKeyProvider.cs | 35 +++ tools/EnvFileViewer/EnvLib/EnvFileModel.cs | 55 +++++ .../EnvFileParser.cs | 102 ++++++++- tools/EnvFileViewer/EnvLib/EnvLib.csproj | 9 + tools/EnvFileViewer/EnvLib/Helper.cs | 72 ++++++ .../EnvFileViewer/EnvLib/SourceModuleInfo.cs | 8 + tools/EnvFileViewer/HashFile/HashFile.csproj | 14 ++ tools/EnvFileViewer/HashFile/Program.cs | 75 +++++++ 13 files changed, 574 insertions(+), 42 deletions(-) delete mode 100644 tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs create mode 100644 tools/EnvFileViewer/EnvLib/EnvFileBuilder.cs create mode 100644 tools/EnvFileViewer/EnvLib/EnvFileKeyProvider.cs create mode 100644 tools/EnvFileViewer/EnvLib/EnvFileModel.cs rename tools/EnvFileViewer/{EnvFileViewer => EnvLib}/EnvFileParser.cs (60%) create mode 100644 tools/EnvFileViewer/EnvLib/EnvLib.csproj create mode 100644 tools/EnvFileViewer/EnvLib/Helper.cs create mode 100644 tools/EnvFileViewer/EnvLib/SourceModuleInfo.cs create mode 100644 tools/EnvFileViewer/HashFile/HashFile.csproj create mode 100644 tools/EnvFileViewer/HashFile/Program.cs diff --git a/tools/EnvFileViewer/EnvFileViewer.slnx b/tools/EnvFileViewer/EnvFileViewer.slnx index 8054afc..83193be 100644 --- a/tools/EnvFileViewer/EnvFileViewer.slnx +++ b/tools/EnvFileViewer/EnvFileViewer.slnx @@ -1,3 +1,5 @@ + + diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs b/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs deleted file mode 100644 index 35c4c44..0000000 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileModel.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Json.Serialization; - -namespace EnvFileViewer; - -public class EnvFileModel -{ - public bool SizeValid { get; set; } - public bool HeaderValid { get; set; } - public bool FooterValid { get; set; } - public bool SignatureValid { get; set; } - - public int ModuleCount { get; set; } - public uint KeySeed { get; set; } - - public VersionInfo Version { get; set; } - public FeatureFlags Flags { get; set; } = new FeatureFlags(false, false, false); - public ModuleCheckInfo ModuleCheckInfo { get; set; } = new ModuleCheckInfo(ModuleCheckType.AtLeast, 0); - public DateTime? TimestampUtc { get; set; } -} - -public record FeatureFlags(bool ModuleCheckEnabled, bool DebugCheckEnabled, bool TimestampCheckEnabled); - -public record ModuleCheckInfo(ModuleCheckType CheckType, byte CheckValue); - -public record VersionInfo(int Major, int Minor, int Patch); - -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum ModuleCheckType : byte -{ - All = 1, - AtLeast = 2 -} \ No newline at end of file diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj b/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj index ed9781c..3ec00e6 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj +++ b/tools/EnvFileViewer/EnvFileViewer/EnvFileViewer.csproj @@ -7,4 +7,8 @@ enable + + + + diff --git a/tools/EnvFileViewer/EnvFileViewer/Program.cs b/tools/EnvFileViewer/EnvFileViewer/Program.cs index 5ae97a5..8447a7b 100644 --- a/tools/EnvFileViewer/EnvFileViewer/Program.cs +++ b/tools/EnvFileViewer/EnvFileViewer/Program.cs @@ -1,4 +1,4 @@ -using EnvFileViewer; +using EnvLib; var noWait = args.Contains("--no-wait"); var filePath = args.FirstOrDefault(a => a != "--no-wait"); diff --git a/tools/EnvFileViewer/EnvLib/EnvFileBuilder.cs b/tools/EnvFileViewer/EnvLib/EnvFileBuilder.cs new file mode 100644 index 0000000..ea9c662 --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/EnvFileBuilder.cs @@ -0,0 +1,206 @@ +using System.Security.Cryptography; + +namespace EnvLib +{ + public class EnvFileBuilder + { + public const string EnvFileName = "environment0f617e02.bin"; + + private const int ModuleCountOffset = 8; + private const byte ModuleCountXorValue = 0xA7; + private const byte MatchTypeXorValue = 0x9F; + private const byte MatchValueXorValue = 0xD3; + private const int MaxModuleCount = 255; + private const int MixSeedOffset = 20; + private const int MatchTypeOffset = 25; + private const int MatchValueOffset = 29; + private const int FeatureFlagsOffset = 60; + private const int ModuleDataOffset = 64; + private const int KeySeedOffset = 16 * 1024; + private const byte FeatureFlagsXorValue = 0x5C; + private const int HashLength = 32; + private const int VersionMajorOffset = 10; + private const int VersionMinorOffset = 11; + private const int VersionPatchOffset = 12; + private const byte VersionMajorXorValue = 0x4A; + private const byte VersionMinorXorValue = 0xC8; + private const byte VersionPatchXorValue = 0x1C; + private const uint KeySeedXorValue = 0x3B527B1C; + private const int TimestampOffset = 30; + private const long TimestampXorValue = 0x0A5DFB499C6830CD; + + byte[] data = new byte[20 * 1024]; + + + void SetHeaderAndFooter() + { + data[0] = 0x31; + data[1] = 0x9A; + data[2] = 0xFF; + data[3] = 0xA2; + + data[data.Length - 4] = 0x44; + data[data.Length - 3] = 0x5A; + data[data.Length - 2] = 0x4D; + data[data.Length - 1] = 0xAA; + } + + public EnvFileBuilder FillData(SourceModuleInfo[] modules) + { +#if NET5_0_OR_GREATER + RandomNumberGenerator.Fill(data); +#else + using (var rng = RandomNumberGenerator.Create()) + { + rng.GetBytes(data); + } +#endif + SetHeaderAndFooter(); + SetModuleData(modules); + SetMatchTypeInternal(ModuleCheckType.All, 0); + SetFeatureFlags(new FeatureFlags(false, false, false, false, false, false)); + SetKeySeed(0); + SetFileVersion(new VersionInfo(0, 0, 0)); + + return this; + } + + public EnvFileBuilder SetKeySeed(uint keySeed) + { + uint obfuscatedSeed = keySeed ^ KeySeedXorValue; + data[KeySeedOffset + 0] = (byte)(obfuscatedSeed >> 24); + data[KeySeedOffset + 1] = (byte)(obfuscatedSeed >> 16); + data[KeySeedOffset + 2] = (byte)(obfuscatedSeed >> 8); + data[KeySeedOffset + 3] = (byte)(obfuscatedSeed >> 0); + return this; + } + + private EnvFileBuilder AddSignature() + { +#if NET5_0_OR_GREATER + using var signingKey = ECDsa.Create(); + signingKey.ImportECPrivateKey(EnvFileKeyProvider.PrivateKey, out _); + var hashAlgorithm = HashAlgorithmName.SHA256; + var signature = signingKey.SignData(new ReadOnlySpan(data, 0, 18 * 1024), hashAlgorithm); + Array.Copy(signature, 0, data, 18 * 1024, signature.Length); +#else + // ECPrivateKey (RFC 5915) DER for P-256: D at offset 7, X at offset 57, Y at offset 89 + var privateKeyBytes = EnvFileKeyProvider.PrivateKey; + byte[] d = new byte[32], x = new byte[32], y = new byte[32]; + Array.Copy(privateKeyBytes, 7, d, 0, 32); + Array.Copy(privateKeyBytes, 57, x, 0, 32); + Array.Copy(privateKeyBytes, 89, y, 0, 32); + + // BCRYPT_ECCPRIVATE_BLOB: magic (4) + keySize (4) + X (32) + Y (32) + D (32) + byte[] blob = new byte[104]; + blob[0] = 0x45; blob[1] = 0x43; blob[2] = 0x53; blob[3] = 0x32; // BCRYPT_ECDSA_PRIVATE_P256_MAGIC + blob[4] = 32; // key size in bytes + Array.Copy(x, 0, blob, 8, 32); + Array.Copy(y, 0, blob, 40, 32); + Array.Copy(d, 0, blob, 72, 32); + + using (var cngKey = CngKey.Import(blob, CngKeyBlobFormat.EccPrivateBlob)) + using (var signingKey = new ECDsaCng(cngKey)) + { + var signature = signingKey.SignData(data, 0, 18 * 1024, HashAlgorithmName.SHA256); + Array.Copy(signature, 0, data, 18 * 1024, signature.Length); + } +#endif + return this; + } + + public EnvFileBuilder SetMatchType(ModuleCheckType matchType, byte matchValue = 0) + { + SetMatchTypeInternal(matchType, matchValue); + + return this; + } + + private void SetMatchTypeInternal(ModuleCheckType matchType, byte matchValue) + { + data[MatchTypeOffset] = (byte)((byte)matchType ^ MatchTypeXorValue); + if (matchType == ModuleCheckType.AtLeast) + { + data[MatchValueOffset] = (byte)(matchValue ^ MatchValueXorValue); + } + } + + public EnvFileBuilder SetFeatureFlags(FeatureFlags featureFlags) + { + var featureFlagsByte = (byte)((featureFlags.ModuleCheckEnabled ? 1 : 0) | + (featureFlags.DebugCheckEnabled ? 2 : 0) | + (featureFlags.TimestampCheckEnabled ? 4 : 0)); + data[FeatureFlagsOffset] = (byte)(featureFlagsByte ^ FeatureFlagsXorValue); + return this; + } + + public EnvFileBuilder SetFileVersion(VersionInfo versionInfo) + { + data[VersionMajorOffset] = (byte)(versionInfo.Major ^ VersionMajorXorValue); + data[VersionMinorOffset] = (byte)(versionInfo.Minor ^ VersionMinorXorValue); + data[VersionPatchOffset] = (byte)(versionInfo.Patch ^ VersionPatchXorValue); + return this; + } + + public EnvFileBuilder SetModuleHashSeed(uint seed) + { + byte[] seedBytes = BitConverter.GetBytes(seed); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(seedBytes); + } + Array.Copy(seedBytes, 0, data, MixSeedOffset, seedBytes.Length); + return this; + } + + public EnvFileBuilder SaveToFile(string fullPath) + { + SetTimestamp(); + AddSignature(); + File.WriteAllBytes(fullPath, data); + + return this; + } + + public void SetModuleData(SourceModuleInfo[] modules) + { + if (modules.Length > MaxModuleCount) + throw new ArgumentException("Too many modules"); + data[ModuleCountOffset] = (byte)(modules.Length ^ ModuleCountXorValue); + int offset = ModuleDataOffset; + byte[] temp = new byte[32]; + foreach (var module in modules) + { + Array.Copy(module.FileNameHash, 0, data, offset, HashLength); + offset += HashLength; + Array.Copy(module.ContentHash, 0, data, offset, HashLength); + offset += HashLength; + } + } + + private void SetTimestamp() + { + long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + // convert to bytes using big-endian format and XOR with the obfuscation value + byte[] timestampBytes = BitConverter.GetBytes(timestamp ^ TimestampXorValue); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(timestampBytes); + } + Array.Copy(timestampBytes, 0, data, TimestampOffset, timestampBytes.Length); + } + + static void Mix(byte[] data, uint seed) + { + uint s = seed; + for (int i = 0; i < data.Length; i++) + { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + data[i] ^= (byte)(s & 0xFF); + } + } + } + +} diff --git a/tools/EnvFileViewer/EnvLib/EnvFileKeyProvider.cs b/tools/EnvFileViewer/EnvLib/EnvFileKeyProvider.cs new file mode 100644 index 0000000..de30aa6 --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/EnvFileKeyProvider.cs @@ -0,0 +1,35 @@ +namespace EnvLib +{ + internal class EnvFileKeyProvider + { + static byte[] privateKey = new byte[] { + 0x74, 0xD4, 0x95, 0x97, 0xB6, 0x01, 0x5D, 0xFB, 0xC3, 0x98, 0x53, 0x89, 0x62, 0x97, 0x4A, 0x4A, + 0x68, 0xC3, 0xBF, 0xFF, 0xFF, 0x82, 0x5D, 0x1C, 0x9F, 0xEA, 0x96, 0xD5, 0xD7, 0xBA, 0x16, 0x49, + 0xF1, 0x7F, 0xA5, 0x03, 0xAA, 0x5A, 0x7F, 0xE8, 0x29, 0x09, 0x50, 0x61, 0xC5, 0x3D, 0xE1, 0xCA, + 0xA3, 0x22, 0xD2, 0xD0, 0x1C, 0xE8, 0xB3, 0x0A, 0xBF, 0x7C, 0x7E, 0x23, 0x5D, 0xCB, 0x5D, 0x5C, + 0xCA, 0x46, 0x05, 0x5E, 0xCB, 0x37, 0xDF, 0xA0, 0xE6, 0x29, 0x56, 0xE6, 0x74, 0xD7, 0xB8, 0xEA, + 0x14, 0xD2, 0x3D, 0xB1, 0xB4, 0x09, 0x7D, 0x64, 0xF0, 0x74, 0x7F, 0xA0, 0x60, 0xB3, 0x40, 0xFD, + 0x5B, 0x41, 0xC5, 0xA9, 0x8D, 0xC1, 0xB1, 0x37, 0x89, 0x20, 0x75, 0x7A, 0x5D, 0xD0, 0x20, 0x17, + 0x01, 0xF3, 0xEC, 0xD3, 0x9B, 0x7E, 0xD3, 0xA4, 0xE0, 0x44, 0xA3, 0x97, 0x96, 0xB7, 0x05, 0x7D, + 0x5C, 0x89, 0x90, 0x20, 0x04, 0x98, 0x67, 0x4B, 0x1D, 0xD1, 0x7F, 0x81, 0x87, 0xDE, 0x66, 0xB5, + 0x90, 0xED, 0x09, 0x3A, 0x90, 0x06, 0xCA, 0xA5, 0x48, 0x6D, 0x4F, 0x40, 0x6C, 0xA1, 0x56, 0xC6, + 0x48, 0x23, 0x0F, 0x58, 0x4B, 0x43, 0x75, 0x2F, 0xF7, 0xA0, 0x23, 0xD5, 0x71, 0x58, 0xEB, 0xF1, + 0x0A, 0xBB, 0x22, 0xA7, 0xE8, 0x79, 0x55, 0x9D, 0x1F, 0xDC, 0x42, 0x23, 0x92, 0x07, 0xD5, 0xF0, + 0x45, 0xD7, 0x24, 0x50, 0x84, 0x09, 0x36, 0x32, 0xEC, 0x97, 0x3D, 0x36, 0xFF, 0x4E, 0xE2, 0x51, + 0x1B, 0x11, 0x1D, 0x83, 0xEB, 0x73, 0xBB, 0x93, 0xF2, 0x9A, 0xD6, 0x97, 0x30, 0x15, 0xF7, 0xEC, + 0xB0, 0xF5, 0x12, 0x78, 0x3E, 0x72, 0xC3, 0xCC, 0xA3, 0xE2, 0x40, 0x8A, 0x57, 0xFA, 0x1D, 0x21, + 0x1A, 0x13 }; + + private static byte[] GetPrivateKey() + { + var result = new byte[privateKey.Length / 2]; + for (int i = 0; i < result.Length; i++) + { + result[i] = (byte)(privateKey[i] ^ privateKey[i + result.Length]); + } + return result; + } + + public static byte[] PrivateKey { get; } = GetPrivateKey(); + } +} diff --git a/tools/EnvFileViewer/EnvLib/EnvFileModel.cs b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs new file mode 100644 index 0000000..43f0aed --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs @@ -0,0 +1,55 @@ +using System.Text.Json.Serialization; + +namespace EnvLib; + +public class EnvFileModel +{ + public bool SizeValid { get; set; } + public bool HeaderValid { get; set; } + public bool FooterValid { get; set; } + public bool SignatureValid { get; set; } + + public int ModuleCount { get; set; } + public uint KeySeed { get; set; } + + public VersionInfo Version { get; set; } + public FeatureFlags Flags { get; set; } = new FeatureFlags(false, false, false, false, false, false); + public ModuleCheckInfo ModuleCheckInfo { get; set; } = new ModuleCheckInfo(ModuleCheckType.AtLeast, 0); + public DateTime? TimestampUtc { get; set; } + public uint ModuleHashSeed { get; internal set; } + public List Modules { get; set; } = new List(); +} + +public record FeatureFlags(bool ModuleCheckEnabled, bool DebugCheckEnabled, bool TimestampCheckEnabled, + bool EntryPointCheckEnabled, bool EnvCheckEnabled, bool HookCheckEnabled); + +public record ModuleCheckInfo(ModuleCheckType CheckType, byte CheckValue); + +public record VersionInfo(int Major, int Minor, int Patch); + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum ModuleCheckType : byte +{ + All = 1, + AtLeast = 2 +} + +public class ModuleInfo +{ + [JsonIgnore] + public byte[] FileNameHash { get; } + [JsonIgnore] + public byte[] ContentHash { get; } + + public bool FileExists { get; set; } + public bool ContentMatches { get; set; } + + + public string FileNameHashHex => BitConverter.ToString(FileNameHash).Replace("-", "").ToLowerInvariant(); + public string ContentHashHex => BitConverter.ToString(ContentHash).Replace("-", "").ToLowerInvariant(); + public ModuleInfo(byte[] fileNameHash, byte[] contentHash) + { + FileNameHash = fileNameHash; + ContentHash = contentHash; + } +} \ No newline at end of file diff --git a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs similarity index 60% rename from tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs rename to tools/EnvFileViewer/EnvLib/EnvFileParser.cs index f544b5c..d31a689 100644 --- a/tools/EnvFileViewer/EnvFileViewer/EnvFileParser.cs +++ b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs @@ -1,9 +1,9 @@ using System; using System.Security.Cryptography; -namespace EnvFileViewer; +namespace EnvLib; -internal class EnvFileParser +public class EnvFileParser { private const int VersionMajorOffset = 10; private const int VersionMinorOffset = 11; @@ -20,9 +20,12 @@ internal class EnvFileParser private const int FeatureFlagsOffset = 60; private const byte FeatureFlagsXorValue = 0x5C; - private const byte ModuleCheckFlag = 0x01; - private const byte DebugCheckFlag = 0x02; - private const byte TimestampCheckFlag = 0x04; + private const byte ModuleCheckFlag = 1 << 0; + private const byte DebugCheckFlag = 1 << 1; + private const byte TimestampCheckFlag = 1 << 2; + private const byte EntryPointCheckFlag = 1 << 3; + private const byte EnvCheckFlag = 1 << 4; + private const byte HookCheckFlag = 1 << 5; private const int MatchTypeOffset = 25; private const byte MatchTypeXorValue = 0x9F; @@ -32,11 +35,19 @@ internal class EnvFileParser private const int TimestampOffset = 30; private const long TimestampXorValue = 0x0A5DFB499C6830CD; + private const int MixSeedOffset = 20; + + private const int ModuleDataOffset = 64; + public static EnvFileModel Parse(string filePath) { var content = System.IO.File.ReadAllBytes(filePath); + var folder = System.IO.Path.GetDirectoryName(filePath); var version = GetVersion(content); + var moduleCount = GetModuleCount(content); + + var hashSeed = GetModuleHashSeedGetModuleHashSeed(content); var model = new EnvFileModel { @@ -44,9 +55,11 @@ public static EnvFileModel Parse(string filePath) HeaderValid = ValidateHeader(content), FooterValid = ValidateFooter(content), SignatureValid = ValidateSignature(content), - Version = GetVersion(content), - ModuleCount = GetModuleCount(content), + Version = version, + ModuleCount = moduleCount, + Modules = GetModules(folder, hashSeed, content, moduleCount), KeySeed = GetKeySeed(content), + ModuleHashSeed = hashSeed, Flags = GetFeatureFlags(content), ModuleCheckInfo = GetModuleCheckInfo(content), TimestampUtc = GetTimestampUtc(content) @@ -123,6 +136,18 @@ private static uint GetKeySeed(byte[] content) return result ^ KeySeedXorValue; } + private static uint GetModuleHashSeedGetModuleHashSeed(byte[] content) + { + if (content.Length < MixSeedOffset + 4) + return 0; + uint result = + (uint)(content[MixSeedOffset + 0] << 24 | + content[MixSeedOffset + 1] << 16 | + content[MixSeedOffset + 2] << 8 | + content[MixSeedOffset + 3]); + return result; + } + private static DateTime? GetTimestampUtc(byte[] content) { if (content.Length < TimestampOffset + 8) @@ -149,9 +174,15 @@ private static uint GetKeySeed(byte[] content) private static FeatureFlags GetFeatureFlags(byte[] content) { if (content.Length < FeatureFlagsOffset + 1) - return new FeatureFlags(false, false, false); + return new FeatureFlags(false, false, false, false, false, false); byte flagsByte = (byte)(content[FeatureFlagsOffset] ^ FeatureFlagsXorValue); - return new FeatureFlags((flagsByte & ModuleCheckFlag) != 0, (flagsByte & DebugCheckFlag) != 0, (flagsByte & TimestampCheckFlag) != 0); + return new FeatureFlags( + (flagsByte & ModuleCheckFlag) != 0, + (flagsByte & DebugCheckFlag) != 0, + (flagsByte & TimestampCheckFlag) != 0, + (flagsByte & EntryPointCheckFlag) != 0, + (flagsByte & EnvCheckFlag) != 0, + (flagsByte & HookCheckFlag) != 0); } private static ModuleCheckInfo GetModuleCheckInfo(byte[] content) @@ -164,4 +195,57 @@ private static ModuleCheckInfo GetModuleCheckInfo(byte[] content) return new ModuleCheckInfo(checkType, checkValueByte); } + + private static List GetModules(string folder, uint seed, byte[] content, int moduleCount) + { + var modules = new List(); + int offset = ModuleDataOffset; + for (int i = 0; i < moduleCount; i++) + { + if (offset + 64 > content.Length) + break; + byte[] fileNameHash = new byte[32]; + byte[] contentHash = new byte[32]; + Array.Copy(content, offset, fileNameHash, 0, 32); + Array.Copy(content, offset + 32, contentHash, 0, 32); + modules.Add(new ModuleInfo(fileNameHash, contentHash)); + offset += 64; + } + + FillModuleStatus(folder, seed, modules); + + return modules; + } + + private static void FillModuleStatus(string folder, uint seed, IEnumerable modules) + { + var files = Helper.GetModuleFiles(folder); + List<(byte[] fileNameHash, byte[] contentHash)> moduleHashes = new List<(byte[], byte[])>(); + foreach (var module in files) + { + var fileName = Path.GetFileName(module)?.ToLowerInvariant(); + if (fileName == null) + { + continue; + } + var fileNameHash = Helper.GetStringSHA256Hash(fileName, seed); + var contentHash = Helper.GetSHA256Hash(module, seed); + moduleHashes.Add((fileNameHash, contentHash)); + } + + foreach(var module in modules) + { + var matchingModule = moduleHashes.FirstOrDefault(m => m.fileNameHash.SequenceEqual(module.FileNameHash)); + if (matchingModule.fileNameHash != null) + { + module.FileExists = true; + module.ContentMatches = matchingModule.contentHash.SequenceEqual(module.ContentHash); + } + else + { + module.FileExists = false; + module.ContentMatches = false; + } + } + } } diff --git a/tools/EnvFileViewer/EnvLib/EnvLib.csproj b/tools/EnvFileViewer/EnvLib/EnvLib.csproj new file mode 100644 index 0000000..b760144 --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/EnvLib.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/tools/EnvFileViewer/EnvLib/Helper.cs b/tools/EnvFileViewer/EnvLib/Helper.cs new file mode 100644 index 0000000..b26e565 --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/Helper.cs @@ -0,0 +1,72 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace EnvLib; + +internal class Helper +{ + public static string[] GetModuleFiles(string outputPath) + { + var files = Directory.GetFiles(outputPath, "*", SearchOption.TopDirectoryOnly) + .Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) + || f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + || IsAotExecutable(f) + ) + .ToArray(); + + return files; + } + + // On Linux and macOS, AoT-compiled executables have no file extension but carry the execute bit. + private static bool IsAotExecutable(string filePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + return false; + + if (!string.IsNullOrEmpty(Path.GetExtension(filePath))) + return false; + + try + { + var mode = File.GetUnixFileMode(filePath); + return (mode & (UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute)) != 0; + } + catch + { + return false; + } + } + + public static byte[] GetSHA256Hash(string filePath, uint seed) + { + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + using (var fileStream = File.OpenRead(filePath)) + { + var hash = sha256.ComputeHash(fileStream); + Mix(hash, seed); + return hash; + } + } + + public static byte[] GetStringSHA256Hash(string input, uint seed) + { + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(input)); + Mix(hash, seed); + return hash; + } + } + + static void Mix(byte[] data, uint seed) + { + uint s = seed; + for (int i = 0; i < data.Length; i++) + { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + data[i] ^= (byte)(s & 0xFF); + } + } +} diff --git a/tools/EnvFileViewer/EnvLib/SourceModuleInfo.cs b/tools/EnvFileViewer/EnvLib/SourceModuleInfo.cs new file mode 100644 index 0000000..96ef24a --- /dev/null +++ b/tools/EnvFileViewer/EnvLib/SourceModuleInfo.cs @@ -0,0 +1,8 @@ +namespace EnvLib; + +public class SourceModuleInfo +{ + public string FilePath { get; set; } + public byte[] FileNameHash { get; set; } + public byte[] ContentHash { get; set; } +} \ No newline at end of file diff --git a/tools/EnvFileViewer/HashFile/HashFile.csproj b/tools/EnvFileViewer/HashFile/HashFile.csproj new file mode 100644 index 0000000..3ec00e6 --- /dev/null +++ b/tools/EnvFileViewer/HashFile/HashFile.csproj @@ -0,0 +1,14 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + diff --git a/tools/EnvFileViewer/HashFile/Program.cs b/tools/EnvFileViewer/HashFile/Program.cs new file mode 100644 index 0000000..a61422b --- /dev/null +++ b/tools/EnvFileViewer/HashFile/Program.cs @@ -0,0 +1,75 @@ +using EnvLib; +using System.ComponentModel; + +const string EnvFileName = "environment0f617e02.bin"; + +var noWait = args.Contains("--no-wait"); +var filePath = args.FirstOrDefault(a => a != "--no-wait"); + +if (string.IsNullOrWhiteSpace(filePath)) +{ + Console.WriteLine("Usage: HashFile [--no-wait]"); + return; +} + +var loweredFileName = Path.GetFileName(filePath).ToLowerInvariant(); + + +byte[] loweredFileNameHash = null; +using (var sha256 = System.Security.Cryptography.SHA256.Create()) +{ + loweredFileNameHash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(loweredFileName)); +} + +var loweredFileNameHashString = BitConverter.ToString(loweredFileNameHash).Replace("-", "").ToLowerInvariant(); +Console.WriteLine($"SHA256 hash of lowered file name {loweredFileName}: {loweredFileNameHashString}"); + +var envFilePath = Path.Combine(Path.GetDirectoryName(filePath), EnvFileName); +byte[] hash = null; + +// Hash the content of filePath using SHA256 +using (var sha256 = System.Security.Cryptography.SHA256.Create()) +using (var fileStream = File.OpenRead(filePath)) +{ + hash = sha256.ComputeHash(fileStream); +} +var hashString = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); +Console.WriteLine($"SHA256 hash of file content: {hashString}"); + +if (File.Exists(envFilePath)) +{ + var env = EnvFileParser.Parse(envFilePath); + + var mixedFileNameHash = (byte[])loweredFileNameHash.Clone(); + Mix(mixedFileNameHash, env.ModuleHashSeed); + var mixedFileNameHashString = BitConverter.ToString(mixedFileNameHash).Replace("-", "").ToLowerInvariant(); + Console.WriteLine($"Mixed SHA256 hash of lowered file name {loweredFileName}: {mixedFileNameHashString}"); + + var mixedContentHash = (byte[])hash.Clone(); + Mix(mixedContentHash, env.ModuleHashSeed); + var mixedHashString = BitConverter.ToString(mixedContentHash).Replace("-", "").ToLowerInvariant(); + Console.WriteLine($"Mixed SHA256 hash of file content: {mixedHashString}"); + + var module = env.Modules.FirstOrDefault(m => m.FileNameHash.SequenceEqual(mixedFileNameHash)); + Console.WriteLine("Env file contains module name hash: " + (module != null ? "Yes" : "No")); + var moduleContentMatch = module != null && module.ContentHash.SequenceEqual(mixedContentHash); + Console.WriteLine("Env file content hash matches: " + (moduleContentMatch ? "Yes" : "No")); +} + +if (!noWait) +{ + Console.WriteLine("Press any key to exit..."); + Console.ReadKey(true); +} + +static void Mix(byte[] data, uint seed) +{ + uint s = seed; + for (int i = 0; i < data.Length; i++) + { + s ^= s << 13; + s ^= s >> 17; + s ^= s << 5; + data[i] ^= (byte)(s & 0xFF); + } +} \ No newline at end of file From 786e6f59e33351ebc5ae876981d614a0dc70af0e Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Thu, 2 Jul 2026 13:10:50 +0300 Subject: [PATCH 5/7] update Readme for EnvFileViewer --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 60b1232..a16ddee 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ This repository houses a collection of test sample projects designed to demonstr The repository contains these folders: - **`DotnetSDK`** — Integration test samples for all supported .NET versions, used as part of the smoke testing suite - **`feature-tests`** — Isolated samples for testing specific functionalities +- **`tools`** - Internal tools used for testing and debugging obfuscated applications ## 🧪 Feature Tests This section lists the available feature-test solutions, each designed to test specific functionalities in isolation: @@ -15,6 +16,13 @@ This section lists the available feature-test solutions, each designed to test s - **MauiAppforChecks** — MAUI App for testing RASP Root check or other checks - **SimpleChecks** — .NET 10 and .NET Framework console samples for testing Debug Check and other RASP checks +## 🧰 Tools +Contains internal tools used in Dotfuscator development process, including testing and debugging. +- **EnvFileViewer** - Reads the environment file (`environment0f617e02.bin`) and displays data in JSON format + - Usage: drag-and-drop a file on `EnvFileViewer.exe` or run `EnvFileViewer.exe ...full-path\environment0f617e02.bin` + - By default, waits for a key press before exit + - Allows using `--no-wait` command line argument to avoid waiting for a key press. + ## 👥 Audience This repository is primarily intended for the technical team responsible for developing, testing, and maintaining Dotfuscator. The samples provide practical, code-based scenarios to aid in product validation, feature development, and understanding Dotfuscator's behavior across various application types. From 6965d5e80c70a067633a12cd3a906d5fe54cfc7b Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Thu, 2 Jul 2026 14:15:53 +0300 Subject: [PATCH 6/7] add --register and --unregister command line arguments --- README.md | 4 +- .../EnvFileViewer/FileAssociationManager.cs | 73 +++++++++++++++++++ tools/EnvFileViewer/EnvFileViewer/Program.cs | 25 ++++++- 3 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs diff --git a/README.md b/README.md index a16ddee..90ef69a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,9 @@ This section lists the available feature-test solutions, each designed to test s ## 🧰 Tools Contains internal tools used in Dotfuscator development process, including testing and debugging. - **EnvFileViewer** - Reads the environment file (`environment0f617e02.bin`) and displays data in JSON format - - Usage: drag-and-drop a file on `EnvFileViewer.exe` or run `EnvFileViewer.exe ...full-path\environment0f617e02.bin` + - Usage: run as Administrator `EnvFileViewer.exe --register` to register `Preview` action for `.bin` files + - Unregister Preview action: run as Administrator `EnvFileViewer.exe --unregister` + - Preview a file: drag-and-drop a file on `EnvFileViewer.exe` or run `EnvFileViewer.exe ...full-path\environment0f617e02.bin` - By default, waits for a key press before exit - Allows using `--no-wait` command line argument to avoid waiting for a key press. diff --git a/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs b/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs new file mode 100644 index 0000000..fa17dd4 --- /dev/null +++ b/tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs @@ -0,0 +1,73 @@ +using Microsoft.Win32; + +namespace EnvFileViewer; + +public static class FileAssociationManager +{ + private const string Extension = ".bin"; + private const string ProgId = "binfile"; + private const string Verb = "Preview"; + + /// + /// Registers the "Preview" context menu action for *.bin files. + /// + /// Full path to your application executable. + public static void RegisterPreviewAction(string applicationPath) + { + try + { + // 1. Ensure .bin points to our ProgID (binfile) + using (RegistryKey extKey = Registry.ClassesRoot.CreateSubKey(Extension)) + { + extKey.SetValue("", ProgId); + } + + // 2. Create the shell verb structure: binfile\shell\Preview\command + string verbKeyPath = $@"{ProgId}\shell\{Verb}"; + using (RegistryKey verbKey = Registry.ClassesRoot.CreateSubKey(verbKeyPath)) + { + verbKey.SetValue("", Verb); // The text shown in the context menu + } + + using (RegistryKey commandKey = Registry.ClassesRoot.CreateSubKey($@"{verbKeyPath}\command")) + { + // Format: "C:\Path\To\App.exe" "%1" + commandKey.SetValue("", $"\"{applicationPath}\" \"%1\""); + } + + Console.WriteLine("Successfully registered 'Preview' action."); + } + catch (UnauthorizedAccessException) + { + Console.WriteLine("Error: You must run this application as an Administrator."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred during registration: {ex.Message}"); + } + } + + /// + /// De-registers the "Preview" context menu action. + /// + public static void UnregisterPreviewAction() + { + try + { + string verbKeyPath = $@"{ProgId}\shell\{Verb}"; + + // Delete the Preview key and all its subkeys (like 'command') + Registry.ClassesRoot.DeleteSubKeyTree(verbKeyPath, throwOnMissingSubKey: false); + + Console.WriteLine("Successfully de-registered 'Preview' action."); + } + catch (UnauthorizedAccessException) + { + Console.WriteLine("Error: You must run this application as an Administrator."); + } + catch (Exception ex) + { + Console.WriteLine($"An error occurred during de-registration: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/tools/EnvFileViewer/EnvFileViewer/Program.cs b/tools/EnvFileViewer/EnvFileViewer/Program.cs index 8447a7b..904e860 100644 --- a/tools/EnvFileViewer/EnvFileViewer/Program.cs +++ b/tools/EnvFileViewer/EnvFileViewer/Program.cs @@ -1,4 +1,25 @@ -using EnvLib; +using EnvFileViewer; +using EnvLib; + +if (args.Contains("--register")) +{ + var applicationPath = System.Reflection.Assembly.GetExecutingAssembly().Location; + if (applicationPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + var exePath = Path.Combine(Path.GetDirectoryName(applicationPath) ?? string.Empty, Path.GetFileNameWithoutExtension(applicationPath) + ".exe"); + if (File.Exists(exePath)) + { + applicationPath = exePath; + } + } + FileAssociationManager.RegisterPreviewAction(applicationPath); + return; +} +if (args.Contains("--unregister")) +{ + FileAssociationManager.UnregisterPreviewAction(); + return; +} var noWait = args.Contains("--no-wait"); var filePath = args.FirstOrDefault(a => a != "--no-wait"); @@ -6,6 +27,8 @@ if (string.IsNullOrWhiteSpace(filePath)) { Console.WriteLine("Usage: EnvFileViewer [--no-wait]"); + Console.WriteLine(" EnvFileViewer --register Register the application as a preview handler for .bin files"); + Console.WriteLine(" EnvFileViewer --unregister Unregister the application as a preview handler for .bin files"); return; } From bcf91e29894c213794f0b00ef131efb1ead58565 Mon Sep 17 00:00:00 2001 From: Dragos Cucu Date: Fri, 10 Jul 2026 13:08:35 +0300 Subject: [PATCH 7/7] Adjust EnvFileViewer to use module relative path, include FileName in output --- tools/EnvFileViewer/EnvLib/EnvFileModel.cs | 1 + tools/EnvFileViewer/EnvLib/EnvFileParser.cs | 47 +++++++++++++++++---- tools/EnvFileViewer/EnvLib/Helper.cs | 31 +++++++++++--- 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/tools/EnvFileViewer/EnvLib/EnvFileModel.cs b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs index 43f0aed..701bd97 100644 --- a/tools/EnvFileViewer/EnvLib/EnvFileModel.cs +++ b/tools/EnvFileViewer/EnvLib/EnvFileModel.cs @@ -43,6 +43,7 @@ public class ModuleInfo public bool FileExists { get; set; } public bool ContentMatches { get; set; } + public string? FileName { get; set; } public string FileNameHashHex => BitConverter.ToString(FileNameHash).Replace("-", "").ToLowerInvariant(); diff --git a/tools/EnvFileViewer/EnvLib/EnvFileParser.cs b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs index d31a689..2d4583e 100644 --- a/tools/EnvFileViewer/EnvLib/EnvFileParser.cs +++ b/tools/EnvFileViewer/EnvLib/EnvFileParser.cs @@ -220,26 +220,28 @@ private static List GetModules(string folder, uint seed, byte[] cont private static void FillModuleStatus(string folder, uint seed, IEnumerable modules) { var files = Helper.GetModuleFiles(folder); - List<(byte[] fileNameHash, byte[] contentHash)> moduleHashes = new List<(byte[], byte[])>(); + List<(byte[] FileNameHash, byte[] ContentHash, string FileName)> moduleHashes = new List<(byte[] FileNameHash, byte[] ContentHash, string FileName)>(); foreach (var module in files) { - var fileName = Path.GetFileName(module)?.ToLowerInvariant(); - if (fileName == null) + var rawRelativePath = Path.GetRelativePath(folder, module).Replace("\\", "/"); + var relativeFilePath = GetModuleRelativePath(module, folder); + if (relativeFilePath == null) { continue; } - var fileNameHash = Helper.GetStringSHA256Hash(fileName, seed); + var relativePathHash = Helper.GetStringSHA256Hash(relativeFilePath, seed); var contentHash = Helper.GetSHA256Hash(module, seed); - moduleHashes.Add((fileNameHash, contentHash)); + moduleHashes.Add((relativePathHash, contentHash, rawRelativePath)); } foreach(var module in modules) { - var matchingModule = moduleHashes.FirstOrDefault(m => m.fileNameHash.SequenceEqual(module.FileNameHash)); - if (matchingModule.fileNameHash != null) + var matchingModule = moduleHashes.FirstOrDefault(m => m.FileNameHash.SequenceEqual(module.FileNameHash)); + if (matchingModule.FileNameHash != null) { module.FileExists = true; - module.ContentMatches = matchingModule.contentHash.SequenceEqual(module.ContentHash); + module.FileName = matchingModule.FileName; + module.ContentMatches = matchingModule.ContentHash.SequenceEqual(module.ContentHash); } else { @@ -248,4 +250,33 @@ private static void FillModuleStatus(string folder, uint seed, IEnumerable basePathNormalized.Length && + fullPathNormalized.Substring(0, basePathNormalized.Length).Equals(basePathNormalized, StringComparison.OrdinalIgnoreCase)) + { + relativePath = fullPathNormalized.Substring(basePathNormalized.Length); + } + else + { + // Fallback: if fullPath is not under basePath, use filename + relativePath = Path.GetFileName(fullPathNormalized); + } + + // Replace backslashes with forward slashes + relativePath = relativePath.Replace('\\', '/'); + + // Convert to lowercase + return relativePath.ToLower(); + } } diff --git a/tools/EnvFileViewer/EnvLib/Helper.cs b/tools/EnvFileViewer/EnvLib/Helper.cs index b26e565..49865d5 100644 --- a/tools/EnvFileViewer/EnvLib/Helper.cs +++ b/tools/EnvFileViewer/EnvLib/Helper.cs @@ -7,11 +7,32 @@ internal class Helper { public static string[] GetModuleFiles(string outputPath) { - var files = Directory.GetFiles(outputPath, "*", SearchOption.TopDirectoryOnly) - .Where(f => f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) - || f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) - || IsAotExecutable(f) - ) + string runtimesPath = Path.Combine(outputPath, "runtimes") + Path.DirectorySeparatorChar; + var files = Directory.GetFiles(outputPath, "*", SearchOption.AllDirectories) + .Where(f => + { + // Exclude files from runtimes subfolder + if (f.StartsWith(runtimesPath, StringComparison.OrdinalIgnoreCase)) + return false; + + var isDll = f.EndsWith(".dll", StringComparison.OrdinalIgnoreCase); + var isExe = f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase); + + // .exe files only in top directory (direct children of outputPath) + if (isExe) + return Path.GetDirectoryName(f) == outputPath; + + // .dll files and AoT executables allowed anywhere (except runtimes) + if (isDll) + return true; + +#if NET6_0_OR_GREATER + if (IsAotExecutable(f)) + return true; +#endif + + return false; + }) .ToArray(); return files;