From b4ce2fdc597b33d4c4db3595055119ebcad14baf Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Sat, 4 Jul 2026 16:13:48 +0200 Subject: [PATCH 1/2] feature: symbol map reuse for consistent re-obfuscation (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements InputSymbolMap (upstream mkaring#168 / yck1509#680) so re-obfuscation produces the same obfuscated names across builds — needed to patch already-deployed obfuscated assemblies without replacing every module. - ConfuserProject gains an inputSymbolMap attribute (Save/Load/Clone + XSD). - NameService loads that map on construction and pre-populates the name maps, so the existing reuse path in ObfuscateName returns the previous obfuscated name for any original that appears in the map. Missing files / parse errors log a warning and fall back to fresh names. - When an input map is in use, names for all rename modes are recorded (not just Decodable/Sequential), so the exported symbols.map stays complete and chainable. - CLI --map flag sets InputSymbolMap for non-project runs. Behaviour is unchanged when no map is supplied (hasInputMap gates all new paths). Test: obfuscate twice with DIFFERENT seeds — with the input map the names match, and a no-map control with the same second seed produces different names, proving reuse is what causes the consistency. Existing renamer tests unaffected. --- Confuser.CLI/Program.cs | 8 ++ Confuser.Core/Project/ConfuserPrj.xsd | 1 + Confuser.Core/Project/ConfuserProject.cs | 20 +++++ Confuser.Renamer/NameService.cs | 60 +++++++++++++- Confuser2.sln | 15 ++++ .../SymbolMapReuse.Test.csproj | 13 +++ .../SymbolMapReuse.Test/SymbolMapReuseTest.cs | 79 +++++++++++++++++++ 7 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 Tests/SymbolMapReuse.Test/SymbolMapReuse.Test.csproj create mode 100644 Tests/SymbolMapReuse.Test/SymbolMapReuseTest.cs diff --git a/Confuser.CLI/Program.cs b/Confuser.CLI/Program.cs index 9d3323c02..b8929c45d 100644 --- a/Confuser.CLI/Program.cs +++ b/Confuser.CLI/Program.cs @@ -28,6 +28,7 @@ static int Main(string[] args) { bool quiet = false; bool dumpRequested = false; string dumpPath = null; + string inputMap = null; int verbosity = 0; string outDir = null; string snKeyPath = null; @@ -65,6 +66,9 @@ static int Main(string[] args) { }, { "dump:", "write a diagnostic report (optionally to the given file).", value => { dumpRequested = true; if (!string.IsNullOrEmpty(value)) dumpPath = value; } + }, { + "map=", "reuse names from a previous symbol map for consistent re-obfuscation.", + value => { inputMap = value; } } }; @@ -147,6 +151,9 @@ static int Main(string[] args) { parameters.Project = proj; } + if (inputMap != null && parameters.Project != null) + parameters.Project.InputSymbolMap = inputMap; + int retVal = RunProject(parameters, quiet, verbosity, dumpRequested, dumpPath); if (NeedPause() && !noPause) { @@ -284,6 +291,7 @@ static void PrintUsage() { WriteLine(" -v|verbose : increase verbosity (-v debug, -vv trace)."); WriteLine(" -q|quiet : only show warnings and errors."); WriteLine(" -dump : write a diagnostic report (-dump= for a custom path)."); + WriteLine(" -map : reuse names from a previous symbol map for consistent re-obfuscation."); } static void WriteLineWithColor(ConsoleColor color, string txt) { diff --git a/Confuser.Core/Project/ConfuserPrj.xsd b/Confuser.Core/Project/ConfuserPrj.xsd index 65c04f6d7..e99703b12 100644 --- a/Confuser.Core/Project/ConfuserPrj.xsd +++ b/Confuser.Core/Project/ConfuserPrj.xsd @@ -75,6 +75,7 @@ + diff --git a/Confuser.Core/Project/ConfuserProject.cs b/Confuser.Core/Project/ConfuserProject.cs index 3c0886a8e..75d2f5361 100644 --- a/Confuser.Core/Project/ConfuserProject.cs +++ b/Confuser.Core/Project/ConfuserProject.cs @@ -528,6 +528,14 @@ public ConfuserProject() { /// true if debug symbols are generated; otherwise, false. public bool Debug { get; set; } + /// + /// Gets or sets the path to a symbol map from a previous obfuscation run. When set, the + /// renamer reuses the obfuscated names recorded in that map, so re-obfuscation produces + /// consistent names across builds (e.g. to patch already-deployed obfuscated assemblies). + /// + /// The path to the input symbol map, or null to generate fresh names. + public string InputSymbolMap { get; set; } + /// /// Gets or sets the output directory. /// @@ -594,6 +602,12 @@ public XmlDocument Save() { elem.Attributes.Append(debugAttr); } + if (InputSymbolMap != null) { + XmlAttribute mapAttr = xmlDoc.CreateAttribute("inputSymbolMap"); + mapAttr.Value = InputSymbolMap; + elem.Attributes.Append(mapAttr); + } + foreach (Rule i in Rules) elem.AppendChild(i.Save(xmlDoc)); @@ -655,6 +669,11 @@ public void Load(XmlDocument doc, string baseDirRoot = null) { else Debug = false; + if (docElem.Attributes["inputSymbolMap"] != null) + InputSymbolMap = docElem.Attributes["inputSymbolMap"].Value.NullIfEmpty(); + else + InputSymbolMap = null; + Packer = null; Clear(); ProbePaths.Clear(); @@ -724,6 +743,7 @@ public ConfuserProject Clone() { var ret = new ConfuserProject(); ret.Seed = Seed; ret.Debug = Debug; + ret.InputSymbolMap = InputSymbolMap; ret.OutputDirectory = OutputDirectory; ret.BaseDirectory = BaseDirectory; ret.Packer = Packer == null ? null : Packer.Clone(); diff --git a/Confuser.Renamer/NameService.cs b/Confuser.Renamer/NameService.cs index ae5a481ec..69bb5aeac 100644 --- a/Confuser.Renamer/NameService.cs +++ b/Confuser.Renamer/NameService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using Confuser.Core; @@ -8,6 +9,7 @@ using Confuser.Renamer.Analyzers; using Confuser.Renamer.Properties; using dnlib.DotNet; +using Microsoft.Extensions.Logging; namespace Confuser.Renamer { public interface INameService { @@ -67,6 +69,7 @@ internal class NameService : INameService { readonly Dictionary _originalToObfuscatedNameMap = new Dictionary(); readonly Dictionary _obfuscatedToOriginalNameMap = new Dictionary(); readonly Dictionary _prefixesMap = new Dictionary(); + readonly bool hasInputMap; internal ReversibleRenamer reversibleRenamer; public NameService(ConfuserContext context) { @@ -74,6 +77,7 @@ public NameService(ConfuserContext context) { storage = new VTableStorage(context.Logger); random = context.Registry.GetService().GetRandomGenerator(NameProtection._FullId); nameSeed = random.NextBytes(20); + hasInputMap = LoadInputSymbolMap(); Renamers = new List { new InterReferenceAnalyzer(), @@ -89,6 +93,57 @@ public NameService(ConfuserContext context) { public IList Renamers { get; private set; } + /// + /// Loads the project's input symbol map (if any) so previously assigned obfuscated names + /// are reused, giving consistent names across re-obfuscation runs. The file uses the same + /// "{obfuscated}\t{original}" format as the exported symbols.map. + /// + /// true if a non-empty map was loaded; otherwise false. + bool LoadInputSymbolMap() { + var mapPath = context.Project.InputSymbolMap; + if (string.IsNullOrEmpty(mapPath)) + return false; + + try { + if (!Path.IsPathRooted(mapPath)) + mapPath = Path.Combine(context.BaseDirectory, mapPath); + + if (!File.Exists(mapPath)) { + context.Logger.LogWarning("Input symbol map not found: '{0}'. Names will be generated fresh.", mapPath); + return false; + } + + int count = 0; + foreach (var line in File.ReadAllLines(mapPath)) { + if (string.IsNullOrWhiteSpace(line)) + continue; + + int tab = line.IndexOf('\t'); + if (tab <= 0) + continue; + + var obfuscated = line.Substring(0, tab); + var original = line.Substring(tab + 1); + + // The map is "{obfuscated}\t{original}"; reuse looks up original -> obfuscated. + if (!_obfuscatedToOriginalNameMap.ContainsKey(obfuscated)) + _obfuscatedToOriginalNameMap[obfuscated] = original; + if (!_originalToObfuscatedNameMap.ContainsKey(original)) { + _originalToObfuscatedNameMap[original] = obfuscated; + count++; + } + } + + context.Logger.LogInformation( + "Loaded {0} name mappings from input symbol map for consistent re-obfuscation.", count); + return count > 0; + } + catch (Exception ex) { + context.Logger.LogWarning(ex, "Failed to load input symbol map. Names will be generated fresh."); + return false; + } + } + public VTableStorage GetVTables() { return storage; } @@ -255,7 +310,10 @@ public string ObfuscateName(string format, string name, RenameMode mode, bool pr hash = Utils.SHA1(hash); } - if (mode == RenameMode.Decodable || mode == RenameMode.Sequential) { + // Decodable/Sequential always record the mapping (they need it to be reversible). + // When an input symbol map is in use, record every mode so newly-generated names are + // reused on the next run and the exported map stays complete and chainable. + if (mode == RenameMode.Decodable || mode == RenameMode.Sequential || hasInputMap) { _obfuscatedToOriginalNameMap.Add(newName, name); _originalToObfuscatedNameMap.Add(name, newName); } diff --git a/Confuser2.sln b/Confuser2.sln index 47acc3a6c..bec181e0c 100644 --- a/Confuser2.sln +++ b/Confuser2.sln @@ -211,6 +211,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Confuser.Analyzers.Test", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AntiDebug.Test", "Tests\AntiDebug.Test\AntiDebug.Test.csproj", "{47197200-B8CB-400A-B1BD-84975FEC8C28}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SymbolMapReuse.Test", "Tests\SymbolMapReuse.Test\SymbolMapReuse.Test.csproj", "{591069EF-617C-4284-A968-5DB55BE1E1EF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1385,6 +1387,18 @@ Global {47197200-B8CB-400A-B1BD-84975FEC8C28}.Release|x64.Build.0 = Release|Any CPU {47197200-B8CB-400A-B1BD-84975FEC8C28}.Release|x86.ActiveCfg = Release|Any CPU {47197200-B8CB-400A-B1BD-84975FEC8C28}.Release|x86.Build.0 = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|x64.ActiveCfg = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|x64.Build.0 = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|x86.ActiveCfg = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Debug|x86.Build.0 = Debug|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|Any CPU.Build.0 = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|x64.ActiveCfg = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|x64.Build.0 = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|x86.ActiveCfg = Release|Any CPU + {591069EF-617C-4284-A968-5DB55BE1E1EF}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1478,6 +1492,7 @@ Global {4458415A-0F5E-4136-B723-7A67955D6047} = {356BDB31-853E-43BB-8F9A-D8AC08F69EBB} {1B22CAAE-FC4A-478D-BD68-D29A3081F938} = {356BDB31-853E-43BB-8F9A-D8AC08F69EBB} {47197200-B8CB-400A-B1BD-84975FEC8C28} = {356BDB31-853E-43BB-8F9A-D8AC08F69EBB} + {591069EF-617C-4284-A968-5DB55BE1E1EF} = {356BDB31-853E-43BB-8F9A-D8AC08F69EBB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0D937D9E-E04B-4A68-B639-D4260473A388} diff --git a/Tests/SymbolMapReuse.Test/SymbolMapReuse.Test.csproj b/Tests/SymbolMapReuse.Test/SymbolMapReuse.Test.csproj new file mode 100644 index 000000000..48ef32c65 --- /dev/null +++ b/Tests/SymbolMapReuse.Test/SymbolMapReuse.Test.csproj @@ -0,0 +1,13 @@ + + + + net462 + false + + + + + + + + diff --git a/Tests/SymbolMapReuse.Test/SymbolMapReuseTest.cs b/Tests/SymbolMapReuse.Test/SymbolMapReuseTest.cs new file mode 100644 index 000000000..2cdc35889 --- /dev/null +++ b/Tests/SymbolMapReuse.Test/SymbolMapReuseTest.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Confuser.Core; +using Confuser.Core.Project; +using Confuser.UnitTest; +using Xunit; +using Xunit.Abstractions; + +namespace SymbolMapReuse.Test { + public sealed class SymbolMapReuseTest { + readonly ITestOutputHelper output; + + public SymbolMapReuseTest(ITestOutputHelper output) => this.output = output; + + // Obfuscation names are deterministic per seed, so two runs with different seeds normally + // produce different names. With InputSymbolMap set to a previous run's symbols.map, the + // renamer must reuse those names — so re-obfuscation stays consistent across builds even + // when the seed (or code) changes. This is the core guarantee of issue #26. + [Fact] + [Trait("Category", "Renamer")] + [Trait("Issue", "https://github.com/mcpolo99/ConfuserExx/issues/26")] + public async Task InputSymbolMap_ReusesNames_AcrossDifferentSeeds() { + var baseDir = Environment.CurrentDirectory; + const string subject = "AntiTamper.exe"; + Assert.True(File.Exists(Path.Combine(baseDir, subject)), $"Test subject {subject} not found in {baseDir}"); + + // Run 1 — fresh names with seed A. + var map1 = await ObfuscateAndReadMap(baseDir, subject, seed: "SeedAAAAAAAA", inputMap: null, suffix: "-map-run1"); + Assert.NotEmpty(map1); + + // Run 2 — DIFFERENT seed, but feed run 1's map back in: names must be reused. + var run1MapPath = Path.Combine(baseDir, "obf-map-run1", "symbols.map"); + var map2 = await ObfuscateAndReadMap(baseDir, subject, seed: "SeedBBBBBBBB", inputMap: run1MapPath, suffix: "-map-run2"); + Assert.Equal(map1, map2); + + // Control — different seed, NO input map: names must differ (proving reuse caused the match). + var map3 = await ObfuscateAndReadMap(baseDir, subject, seed: "SeedBBBBBBBB", inputMap: null, suffix: "-map-run3"); + Assert.NotEqual(map1, map3); + } + + async Task> ObfuscateAndReadMap(string baseDir, string subject, string seed, string inputMap, string suffix) { + var outputDir = Path.Combine(baseDir, "obf" + suffix); + if (Directory.Exists(outputDir)) + Directory.Delete(outputDir, true); + + var proj = new ConfuserProject { + BaseDirectory = baseDir, + OutputDirectory = outputDir, + Seed = seed, + InputSymbolMap = inputMap + }; + proj.Add(new ProjectModule { Path = subject }); + + var rule = new Rule(); + // Decodable mode records every rename in symbols.map, giving a stable map to compare. + rule.Add(new SettingItem("rename") { { "mode", "decodable" } }); + proj.Rules.Add(rule); + + var logger = new XunitLogger(output); + await ConfuserEngine.Run(new ConfuserParameters { + Project = proj, + Logger = logger, + ProgressReporter = logger + }); + + var mapPath = Path.Combine(outputDir, "symbols.map"); + Assert.True(File.Exists(mapPath), $"symbols.map should be produced in {outputDir}"); + + // Return the mapping lines sorted, so comparison is order-insensitive. + return File.ReadAllLines(mapPath) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .OrderBy(line => line, StringComparer.Ordinal) + .ToList(); + } + } +} From a11dd4d8aebed2ad33a9bf48addb8d0c35184751 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Sat, 4 Jul 2026 16:28:33 +0200 Subject: [PATCH 2/2] feature: add Input Symbol Map field to GUI advanced settings (#26) Completes the #26 GUI follow-up. The project's Advanced Settings dialog now has an 'Input Symbol Map' field (text box with file drag-drop + a browse button) bound to ProjectVM.InputSymbolMap, so users can select a previous symbols.map from the GUI for consistent re-obfuscation. Round-trips through the existing project save/load. --- ConfuserEx/ViewModel/Project/ProjectVM.cs | 5 +++ ConfuserEx/Views/ProjectTabAdvancedView.xaml | 31 +++++++++++++++---- .../Views/ProjectTabAdvancedView.xaml.cs | 7 +++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/ConfuserEx/ViewModel/Project/ProjectVM.cs b/ConfuserEx/ViewModel/Project/ProjectVM.cs index 2063a166c..fe849d730 100644 --- a/ConfuserEx/ViewModel/Project/ProjectVM.cs +++ b/ConfuserEx/ViewModel/Project/ProjectVM.cs @@ -57,6 +57,11 @@ public bool Debug { set { SetProperty(proj.Debug != value, val => proj.Debug = val, value, "Debug"); } } + public string InputSymbolMap { + get { return proj.InputSymbolMap; } + set { SetProperty(proj.InputSymbolMap != value, val => proj.InputSymbolMap = val, value, "InputSymbolMap"); } + } + public string BaseDirectory { get { return proj.BaseDirectory; } set { SetProperty(proj.BaseDirectory != value, val => proj.BaseDirectory = val, value, "BaseDirectory"); } diff --git a/ConfuserEx/Views/ProjectTabAdvancedView.xaml b/ConfuserEx/Views/ProjectTabAdvancedView.xaml index bfb94b0a5..38e5b5413 100644 --- a/ConfuserEx/Views/ProjectTabAdvancedView.xaml +++ b/ConfuserEx/Views/ProjectTabAdvancedView.xaml @@ -12,15 +12,34 @@ + -