Skip to content
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -15,6 +16,15 @@ 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: 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.

## 👥 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.

Expand Down
73 changes: 73 additions & 0 deletions tools/EnvFileViewer/EnvFileViewer/FileAssociationManager.cs
Original file line number Diff line number Diff line change
@@ -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";

/// <summary>
/// Registers the "Preview" context menu action for *.bin files.
/// </summary>
/// <param name="applicationPath">Full path to your application executable.</param>
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}");
}
}

/// <summary>
/// De-registers the "Preview" context menu action.
/// </summary>
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}");
}
}
}
25 changes: 24 additions & 1 deletion tools/EnvFileViewer/EnvFileViewer/Program.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
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");

if (string.IsNullOrWhiteSpace(filePath))
{
Console.WriteLine("Usage: EnvFileViewer <file-path> [--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;
}

Expand Down
1 change: 1 addition & 0 deletions tools/EnvFileViewer/EnvLib/EnvFileModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
47 changes: 39 additions & 8 deletions tools/EnvFileViewer/EnvLib/EnvFileParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,26 +220,28 @@ private static List<ModuleInfo> GetModules(string folder, uint seed, byte[] cont
private static void FillModuleStatus(string folder, uint seed, IEnumerable<ModuleInfo> 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
{
Expand All @@ -248,4 +250,33 @@ private static void FillModuleStatus(string folder, uint seed, IEnumerable<Modul
}
}
}

private static string GetModuleRelativePath(string fullPath, string basePath)
{
var fullPathNormalized = Path.GetFullPath(fullPath);
var basePathNormalized = Path.GetFullPath(basePath);

// Ensure base path ends with separator for proper prefix matching
if (!basePathNormalized.EndsWith(Path.DirectorySeparatorChar.ToString(), StringComparison.InvariantCulture))
basePathNormalized += Path.DirectorySeparatorChar;

// Extract relative path by removing base path prefix
string relativePath;
if (fullPathNormalized.Length > 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();
}
}
31 changes: 26 additions & 5 deletions tools/EnvFileViewer/EnvLib/Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down