From 88f5b31c67434002bdb4df366e2669b0bb2261a6 Mon Sep 17 00:00:00 2001 From: RandomCrocodile Date: Sat, 4 Jul 2026 17:04:17 +0200 Subject: [PATCH] feature: make watermark opt-in and configurable (#69) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the unique-obfuscation-identity work (#69). The 'ConfusedByAttribute' watermark was force-inserted into every obfuscated assembly and carried the ConfuserEx version — an identifiable fingerprint that de4dot and AV heuristics match. It is now opt-in: - ConfuserEngine no longer force-inserts the watermark rule, so default output carries no fingerprint attribute unless is set. - WatermarkingPhase reads two parameters: 'text' (attribute value, defaults to the version) and 'attributeName' (attribute type name, defaults to ConfusedByAttribute), so the watermark can be branded or disguised. - Documented the previously-undocumented protection in docs/protections.md. Behaviour change: assemblies are no longer watermarked by default. Re-enable per project with the watermark protection. Test: Watermark.Test verifies no fingerprint attribute when not requested, and the custom text/name are applied when enabled. --- Confuser.Core/ConfuserEngine.cs | 7 ++- Confuser.Core/WatermarkingProtection.cs | 13 +++-- Confuser2.sln | 15 ++++++ Tests/Watermark.Test/Watermark.Test.csproj | 13 +++++ Tests/Watermark.Test/WatermarkTest.cs | 58 ++++++++++++++++++++++ docs/protections.md | 25 ++++++++++ 6 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 Tests/Watermark.Test/Watermark.Test.csproj create mode 100644 Tests/Watermark.Test/WatermarkTest.cs diff --git a/Confuser.Core/ConfuserEngine.cs b/Confuser.Core/ConfuserEngine.cs index 68ac9c482..91594f9cf 100644 --- a/Confuser.Core/ConfuserEngine.cs +++ b/Confuser.Core/ConfuserEngine.cs @@ -90,10 +90,9 @@ static void RunInternal(ConfuserParameters parameters, CancellationToken token) bool ok = false; try { - // Enable watermarking by default - context.Project.Rules.Insert(0, new Rule { - new SettingItem(WatermarkingProtection._Id) - }); + // Watermarking is opt-in (issue #69): the "watermark" protection is no longer + // force-enabled, so obfuscated output carries no ConfuserEx fingerprint attribute + // unless a project explicitly requests it via . var asmResolver = new ConfuserAssemblyResolver { EnableTypeDefCache = true }; asmResolver.DefaultModuleContext = new ModuleContext(asmResolver); diff --git a/Confuser.Core/WatermarkingProtection.cs b/Confuser.Core/WatermarkingProtection.cs index 7fff32205..b1d1aeddb 100644 --- a/Confuser.Core/WatermarkingProtection.cs +++ b/Confuser.Core/WatermarkingProtection.cs @@ -48,10 +48,17 @@ protected internal override void Execute(ConfuserContext context, ProtectionPara context.Logger.LogDebug("Watermarking..."); foreach (var module in parameters.Targets.OfType()) { + // Both are configurable so users can brand (or disguise) the watermark instead of + // carrying the identifiable default ConfuserEx fingerprint. + var attributeName = parameters.GetParameter(context, module, "attributeName", "ConfusedByAttribute"); + if (string.IsNullOrEmpty(attributeName)) + attributeName = "ConfusedByAttribute"; + var text = parameters.GetParameter(context, module, "text", ConfuserEngine.Version); + var attrRef = module.CorLibTypes.GetTypeRef("System", "Attribute"); - var attrType = module.FindNormal("ConfusedByAttribute"); + var attrType = module.FindNormal(attributeName); if (attrType == null) { - attrType = new TypeDefUser("", "ConfusedByAttribute", attrRef); + attrType = new TypeDefUser("", attributeName, attrRef); module.Types.Add(attrType); marker.Mark(attrType, Parent); } @@ -75,7 +82,7 @@ protected internal override void Execute(ConfuserContext context, ProtectionPara } var attr = new CustomAttribute(ctor); - attr.ConstructorArguments.Add(new CAArgument(module.CorLibTypes.String, ConfuserEngine.Version)); + attr.ConstructorArguments.Add(new CAArgument(module.CorLibTypes.String, text)); module.CustomAttributes.Add(attr); } diff --git a/Confuser2.sln b/Confuser2.sln index bec181e0c..9dc5bf919 100644 --- a/Confuser2.sln +++ b/Confuser2.sln @@ -213,6 +213,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AntiDebug.Test", "Tests\Ant EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SymbolMapReuse.Test", "Tests\SymbolMapReuse.Test\SymbolMapReuse.Test.csproj", "{591069EF-617C-4284-A968-5DB55BE1E1EF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Watermark.Test", "Tests\Watermark.Test\Watermark.Test.csproj", "{0C937593-1C74-4576-B79F-D99642612484}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1399,6 +1401,18 @@ Global {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 + {0C937593-1C74-4576-B79F-D99642612484}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Debug|x64.ActiveCfg = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Debug|x64.Build.0 = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Debug|x86.ActiveCfg = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Debug|x86.Build.0 = Debug|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|Any CPU.Build.0 = Release|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|x64.ActiveCfg = Release|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|x64.Build.0 = Release|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|x86.ActiveCfg = Release|Any CPU + {0C937593-1C74-4576-B79F-D99642612484}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1493,6 +1507,7 @@ Global {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} + {0C937593-1C74-4576-B79F-D99642612484} = {356BDB31-853E-43BB-8F9A-D8AC08F69EBB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0D937D9E-E04B-4A68-B639-D4260473A388} diff --git a/Tests/Watermark.Test/Watermark.Test.csproj b/Tests/Watermark.Test/Watermark.Test.csproj new file mode 100644 index 000000000..48ef32c65 --- /dev/null +++ b/Tests/Watermark.Test/Watermark.Test.csproj @@ -0,0 +1,13 @@ + + + + net462 + false + + + + + + + + diff --git a/Tests/Watermark.Test/WatermarkTest.cs b/Tests/Watermark.Test/WatermarkTest.cs new file mode 100644 index 000000000..f83f5f440 --- /dev/null +++ b/Tests/Watermark.Test/WatermarkTest.cs @@ -0,0 +1,58 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Confuser.Core; +using Confuser.Core.Project; +using Confuser.UnitTest; +using dnlib.DotNet; +using Xunit; +using Xunit.Abstractions; + +namespace Watermark.Test { + public sealed class WatermarkTest : TestBase { + public WatermarkTest(ITestOutputHelper outputHelper) : base(outputHelper) { } + + // Issue #69: the watermark is opt-in. An obfuscation that does not request it must produce + // output with no ConfusedByAttribute fingerprint. + [Fact] + [Trait("Category", "Protection")] + [Trait("Issue", "https://github.com/mcpolo99/ConfuserExx/issues/69")] + public Task Watermark_NotRequested_ProducesNoFingerprintAttribute() => + Run("AntiTamper.exe", + new[] { "This is a test." }, + new SettingItem("rename"), + "_wm_off", + postProcessAction: outputPath => { + using (var module = ModuleDefMD.Load(Path.Combine(outputPath, "AntiTamper.exe"))) { + Assert.DoesNotContain(module.CustomAttributes, a => a.TypeFullName == "ConfusedByAttribute"); + Assert.DoesNotContain(module.GetTypes(), t => t.Name == "ConfusedByAttribute"); + } + return Task.CompletedTask; + }); + + // When explicitly enabled with custom text and attribute name, the watermark must use them + // (so it can be branded or disguised instead of the default ConfuserEx fingerprint). + [Fact] + [Trait("Category", "Protection")] + [Trait("Issue", "https://github.com/mcpolo99/ConfuserExx/issues/69")] + public Task Watermark_CustomTextAndName_AppliesConfiguredAttribute() => + Run("AntiTamper.exe", + new[] { "This is a test." }, + new SettingItem("watermark") { + { "text", "MyCorp Security" }, + { "attributeName", "SecurityStampAttribute" } + }, + "_wm_custom", + postProcessAction: outputPath => { + using (var module = ModuleDefMD.Load(Path.Combine(outputPath, "AntiTamper.exe"))) { + var attr = module.CustomAttributes.FirstOrDefault(a => a.TypeFullName == "SecurityStampAttribute"); + Assert.NotNull(attr); + Assert.Equal("MyCorp Security", attr.ConstructorArguments[0].Value?.ToString()); + + // The default fingerprint name must not appear. + Assert.DoesNotContain(module.CustomAttributes, a => a.TypeFullName == "ConfusedByAttribute"); + } + return Task.CompletedTask; + }); + } +} diff --git a/docs/protections.md b/docs/protections.md index 3851215cd..48665fd9d 100644 --- a/docs/protections.md +++ b/docs/protections.md @@ -277,6 +277,31 @@ Compresses the entire output assembly and wraps it in a native stub that decompr ``` +--- + +### Watermark + +**ID:** `watermark` + +Adds a small custom attribute to the assembly marking that it was protected. It is **opt-in** — +obfuscated output carries no watermark unless this protection is explicitly enabled, so the +default output has no identifiable ConfuserEx fingerprint. + +**Options:** + +| Name | Values | Default | Description | +|------|--------|---------|-------------| +| `text` | any string | ConfuserEx version | The value stored in the watermark attribute. | +| `attributeName` | any identifier | `ConfusedByAttribute` | The name of the injected attribute type. | + +```xml + + + + + +``` + ## Combining Protections Protections stack. You can start from a preset and add/remove individual protections: