Skip to content

Commit a6b2ce5

Browse files
sunnamed434claude
andcommitted
Add plugin support: load custom protections from a plugins directory (#227)
Drop plugin DLLs (custom IProtection implementations) into a `plugins` directory and BitMono discovers and runs them like the built-ins. External plugin dependencies are resolved via a single AppDomain.AssemblyResolve handler, which is the only mechanism that works across every BitMono target (net462, netstandard2.0/2.1, net6-net10) since AssemblyLoadContext is absent on the older ones. - PluginLoader + PluginProbing in BitMono.Shared, hooked from AddProtections - The resolver prefers already-loaded host assemblies so plugins bind to the host's BitMono.API (avoids the duplicate-contract trap), then probes the plugin directories; the hook is AppDomain-global and attached once - Plugins live at the plugins root or one folder deep; nested folders (e.g. a libs/ of NuGet deps) are resolved lazily by the handler - Type discovery now uses IProtection.IsAssignableFrom (warns on a shadowed BitMono.API), skips open generics, and instantiates each protection defensively so one broken plugin can't abort the run - PluginsDirectoryName setting in obfuscation.json (default "plugins") - Unit tests for the probing logic and developer docs (developers/plugins) Closes #227 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 8635dc9 commit a6b2ce5

10 files changed

Lines changed: 583 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ Read the **[docs][bitmono_docs]** to read protection, functionality, and more.
8484
* AntiDe4dot
8585
* AntiILdasm
8686
* and you can integrate existing/make own feature ;)
87+
* **Plugins** - drop your own protections in a `plugins` folder, no rebuild required ([guide](https://bitmono.readthedocs.io/en/latest/developers/plugins.html))
8788

8889
## Documentation
8990

docs/source/developers/plugins.rst

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
Plugins
2+
=======
3+
4+
BitMono can load **plugins** - external assemblies that contribute your own protections - without
5+
rebuilding BitMono. Drop a plugin DLL into the plugins directory and BitMono discovers and runs the
6+
protections inside it, exactly like the built-in ones. (`#227 <https://github.com/sunnamed434/BitMono/issues/227>`_)
7+
8+
.. warning::
9+
10+
Plugins are loaded into the BitMono process with full trust - .NET cannot sandbox them. Only drop
11+
in plugins from sources you trust, just as you would with any other executable.
12+
13+
14+
Where plugins live
15+
------------------
16+
17+
By default BitMono scans a ``plugins`` directory next to the BitMono executable. You can change the
18+
directory name (or point it at an absolute path) with ``PluginsDirectoryName`` in ``obfuscation.json``:
19+
20+
.. code-block:: json
21+
22+
{
23+
"PluginsDirectoryName": "plugins"
24+
}
25+
26+
Two layouts are supported:
27+
28+
- **Flat** - drop the DLL straight into the directory: ``plugins/MyProtections.dll``.
29+
- **Per-plugin folder** - give each plugin its own folder: ``plugins/MyProtections/MyProtections.dll``.
30+
Put any extra dependencies in a nested folder (for example ``plugins/MyProtections/libs/``); they are
31+
resolved on demand and are not themselves treated as plugins.
32+
33+
If the directory does not exist, BitMono simply skips plugin loading.
34+
35+
36+
Writing a plugin
37+
----------------
38+
39+
A plugin is an ordinary class library that references ``BitMono.API`` (and usually ``BitMono.Core`` for
40+
the ``Protection`` base class) and exposes one or more public protections. Writing a protection is the
41+
same as :doc:`creating a built-in one <first-protection>`:
42+
43+
.. code-block:: csharp
44+
45+
using BitMono.Core;
46+
using BitMono.Core.Attributes;
47+
using BitMono.Shared.DependencyInjection;
48+
using BitMono.Shared.Logging;
49+
50+
[ProtectionName("HelloPlugin")]
51+
public class HelloPlugin : Protection
52+
{
53+
private readonly ILogger _logger;
54+
55+
public HelloPlugin(IBitMonoServiceProvider serviceProvider) : base(serviceProvider)
56+
{
57+
_logger = serviceProvider.GetRequiredService<ILogger>().ForContext<HelloPlugin>();
58+
}
59+
60+
public override Task ExecuteAsync()
61+
{
62+
_logger.Information("Hello from a BitMono plugin!");
63+
return Task.CompletedTask;
64+
}
65+
}
66+
67+
.. note::
68+
69+
The container builds your protection by calling its **first** constructor and resolving each
70+
parameter. Take ``IBitMonoServiceProvider`` (as above) to reach BitMono's services. If a parameter
71+
cannot be resolved the protection is skipped and the error is logged - it never aborts the run.
72+
73+
74+
.. _plugins-reference-the-host:
75+
76+
Reference BitMono, don't ship it
77+
--------------------------------
78+
79+
Reference the BitMono assemblies with ``Private="false"`` so they are **not** copied next to your
80+
plugin. Your plugin must bind to the *same* ``BitMono.API`` the host is running; if you ship your own
81+
copy, your protection implements a *different* ``IProtection`` type and BitMono will ignore it (and warn
82+
you in the log).
83+
84+
.. code-block:: xml
85+
86+
<ItemGroup>
87+
<Reference Include="BitMono.API">
88+
<HintPath>path\to\BitMono.API.dll</HintPath>
89+
<Private>false</Private>
90+
</Reference>
91+
<Reference Include="BitMono.Core">
92+
<HintPath>path\to\BitMono.Core.dll</HintPath>
93+
<Private>false</Private>
94+
</Reference>
95+
<Reference Include="BitMono.Shared">
96+
<HintPath>path\to\BitMono.Shared.dll</HintPath>
97+
<Private>false</Private>
98+
</Reference>
99+
</ItemGroup>
100+
101+
102+
External dependencies
103+
---------------------
104+
105+
If your plugin uses external libraries (NuGet packages, your own helpers), ship those DLLs alongside the
106+
plugin - either next to it or in a nested folder such as ``libs``. BitMono installs an
107+
``AppDomain.AssemblyResolve`` handler that probes the plugin directories and loads dependencies on
108+
demand, so they don't have to sit in BitMono's own folder.
109+
110+
A typical per-plugin layout::
111+
112+
plugins/
113+
MyProtections/
114+
MyProtections.dll <- your plugin
115+
libs/
116+
SomeNuGetPackage.dll <- resolved automatically when needed
117+
118+
119+
Enabling a plugin protection
120+
----------------------------
121+
122+
Plugin protections are configured exactly like the built-in ones - by name (the class name, or the
123+
``[ProtectionName("...")]`` value). Add them to ``protections.json``:
124+
125+
.. code-block:: json
126+
127+
{
128+
"Protections": [
129+
{
130+
"Name": "HelloPlugin",
131+
"Enabled": true
132+
}
133+
]
134+
}
135+
136+
or pass them on the command line:
137+
138+
.. code-block:: bash
139+
140+
BitMono.CLI -f MyApp.dll -p HelloPlugin
141+
142+
Execution order follows the configuration order, the same as built-in protections. See
143+
:doc:`obfuscation-execution-order` for details.

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Table of Contents:
6060
:name: sec-developers
6161

6262
developers/first-protection
63+
developers/plugins
6364
developers/obfuscation-execution-order
6465
developers/which-base-protection-select
6566
developers/protection-runtime-moniker

src/BitMono.Host/Extensions/ContainerExtensions.cs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using BitMono.API.Protections;
22
using BitMono.Shared.DependencyInjection;
3+
using BitMono.Shared.Logging;
4+
using BitMono.Shared.Models;
5+
using BitMono.Shared.Plugins;
36
using System;
47
using System.Collections.Generic;
58
using System.Diagnostics.CodeAnalysis;
@@ -18,9 +21,10 @@ public static class BitMonoContainerExtensions
1821
{
1922
private const string ProtectionsFileName = "BitMono.Protections.dll";
2023
private const string UnityFileName = "BitMono.Unity.dll";
24+
private const string DefaultPluginsDirectoryName = "plugins";
2125

2226
/// <summary>
23-
/// Loads and registers protection assemblies.
27+
/// Loads and registers protection assemblies, including any drop-in plugins (see #227).
2428
/// </summary>
2529
/// <param name="container">Container to register protections in</param>
2630
/// <param name="file">Optional path to protections DLL</param>
@@ -38,6 +42,12 @@ public static Container AddProtections(this Container container, string? file =
3842
Assembly.Load(unityRawData);
3943
}
4044

45+
var logger = container.GetService(typeof(ILogger)) as ILogger;
46+
47+
// Load drop-in plugins before scanning so their protections are discovered alongside the built-ins.
48+
LoadPlugins(container, logger);
49+
50+
var scanLogger = logger?.ForContext(typeof(BitMonoContainerExtensions));
4151
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
4252

4353
// Collect all protection types (excluding IPhaseProtection)
@@ -52,23 +62,37 @@ public static Container AddProtections(this Container container, string? file =
5262
catch (ReflectionTypeLoadException ex)
5363
{
5464
types = ex.Types.Where(t => t != null).ToArray()!;
65+
scanLogger?.Warning("Some types in '{0}' could not be loaded: {1}",
66+
assembly.GetName().Name ?? assembly.FullName ?? "?",
67+
string.Join("; ", ex.LoaderExceptions.Where(e => e != null).Select(e => e!.Message).Distinct()));
5568
}
5669

5770
foreach (var type in types)
5871
{
59-
if (!type.IsClass || type.IsAbstract || !type.IsPublic)
72+
// Open generic definitions can't be instantiated by the container, so skip them.
73+
if (!type.IsClass || type.IsAbstract || !type.IsPublic || type.IsGenericTypeDefinition)
6074
continue;
6175

62-
// Skip IPhaseProtection, only register IProtection
63-
if (type.GetInterface(nameof(IPhaseProtection)) != null)
76+
// IPhaseProtection extends IProtection, so it MUST be checked first - phase protections
77+
// are driven by their pipeline, not registered as standalone protections.
78+
if (typeof(IPhaseProtection).IsAssignableFrom(type))
6479
continue;
6580

66-
if (type.GetInterface(nameof(IProtection)) != null)
81+
if (typeof(IProtection).IsAssignableFrom(type))
6782
{
6883
protectionTypes.Add(type);
6984
// Register the concrete type
7085
container.Register(type, type).AsSingleton();
7186
}
87+
else if (type.GetInterfaces().Any(i => i.Name == nameof(IProtection)))
88+
{
89+
// Implements an interface *named* IProtection but not BitMono's contract - almost always
90+
// a plugin shipping its own copy of BitMono.API, which would be silently ignored (#227).
91+
scanLogger?.Warning(
92+
"Type '{0}' in '{1}' looks like a protection but references a different BitMono.API and " +
93+
"will be ignored. Reference BitMono.API with Private=\"false\" and don't ship it beside your plugin.",
94+
type.FullName ?? type.Name, type.Assembly.GetName().Name ?? "?");
95+
}
7296
}
7397
}
7498

@@ -78,15 +102,43 @@ public static Container AddProtections(this Container container, string? file =
78102
var list = new List<IProtection>();
79103
foreach (var protType in protectionTypes)
80104
{
81-
var instance = container.GetService(protType);
82-
if (instance is IProtection protection)
105+
try
106+
{
107+
var instance = container.GetService(protType);
108+
if (instance is IProtection protection)
109+
{
110+
list.Add(protection);
111+
}
112+
}
113+
catch (Exception ex)
83114
{
84-
list.Add(protection);
115+
// A broken plugin protection must not abort the whole run - log and skip it (#227).
116+
scanLogger?.Error(ex,
117+
"Failed to instantiate protection '{0}'. Its constructor parameters must be resolvable by the container.",
118+
protType.FullName ?? protType.Name);
85119
}
86120
}
87121
return list;
88122
}).AsSingleton();
89123

90124
return container;
91125
}
126+
127+
private static void LoadPlugins(Container container, ILogger? logger)
128+
{
129+
// No logger means the host wasn't fully set up; plugin loading relies on logging for diagnostics.
130+
if (logger == null)
131+
return;
132+
133+
var obfuscationSettings = container.GetService(typeof(ObfuscationSettings)) as ObfuscationSettings;
134+
var directoryName = obfuscationSettings?.PluginsDirectoryName;
135+
if (string.IsNullOrWhiteSpace(directoryName))
136+
directoryName = DefaultPluginsDirectoryName;
137+
138+
var pluginsDirectory = Path.IsPathRooted(directoryName)
139+
? directoryName!
140+
: Path.Combine(AppContext.BaseDirectory, directoryName!);
141+
142+
new PluginLoader(pluginsDirectory, logger).LoadPlugins();
143+
}
92144
}

src/BitMono.Host/obfuscation.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@
2424
// Name of the output directory
2525
"OutputDirectoryName": "output",
2626

27+
// Directory scanned for drop-in plugins (custom protections). Drop a plugin DLL here (or in a
28+
// subfolder with its dependencies) and BitMono loads it on start. Relative paths are resolved
29+
// against BitMono's base directory; absolute paths are used as-is. See the Plugins docs.
30+
"PluginsDirectoryName": "plugins",
31+
2732
// Outputs information about protections (Enabled protections, disabled, deprecated, unknown, etc)
2833
// Set to true indicates whether enabled
2934
"NotifyProtections": true,

src/BitMono.Shared/Models/ObfuscationSettings.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ public class ObfuscationSettings
77
public bool ForceObfuscation { get; set; }
88
public string ReferencesDirectoryName { get; set; }
99
public string OutputDirectoryName { get; set; }
10+
11+
// Directory scanned for drop-in plugin assemblies that contain custom protections. Relative paths
12+
// are resolved against BitMono's base directory; absolute paths are used as-is. See #227.
13+
public string PluginsDirectoryName { get; set; } = "plugins";
1014
public bool NotifyProtections { get; set; }
1115
public bool Tips { get; set; } = true;
1216
public bool WpfBamlRewrite { get; set; } = true;

0 commit comments

Comments
 (0)