Can I achieve type forwarding in .NET without modifying the "from" assembly?

183 views Asked by At

The Problem:

We are trying to move an application from .NET Framework to .NET 8. The application needs to consume some third-party .NET Framework assemblies, one of which uses the Framework-only class System.Security.Cryptography.ProtectedMemory.

I'm wondering if there's any way for me to provide an implementation of that class that I can cause to get picked up when the third-party assembly tries to load the .NET Framework version, for which it won't find a type forward from .NET 8.

This sounds horribly insecure to me, so I suspect it's not possible. But we're in a real pickle if it can't be done.

1

There are 1 answers

0
adv12 On

While I didn't find any way to achieve type forwarding at runtime (probably for good reason), I did come across a GitHub issue where a developer was trying to replace an inbox DLL with a modified build of it, just for his application.

To sum up the GitHub conversation, you can override a system DLL in your .NET application by creating your own version with the same name and public key but a higher version number. You then include it in your C# project as an assembly reference or NuGet package. This adds your custom DLL to the app's .deps.json file, causing it to be loaded in preference to the lower-versioned DLL that ships with .NET.

In my case, I wanted to add two missing type forwards to System.Security.dll. Rather than clone the entire .NET runtime repo to build a custom version from source, I used dnlib to modify the inbox assembly directly. I have a GitHub repo with a full demonstration of this technique and how to incorporate it into a working .NET 8 app, but here's the code for the app that creates the modified shim DLL:

  using System.Reflection;
  using dnlib.DotNet;
  using TypeAttributes = dnlib.DotNet.TypeAttributes;

  string stringPath = typeof(string).Assembly.Location;
  string? dirPath = Path.GetDirectoryName(stringPath);
  if (dirPath == null)
  {
     return;
  }

  string securityPath = Path.Combine(dirPath, "System.Security.dll");

  var mod = ModuleDefMD.Load(securityPath);
  var classesMod = ModuleDefMD.Load(typeof(System.Security.Cryptography.ProtectedMemory).Assembly.Location);
  string ns = "System.Security.Cryptography";
  var mpsTypeDef = classesMod.Types.First(t => t.Namespace == ns && t.Name == "MemoryProtectionScope");
  var pmTypeDef = classesMod.Types.First(t => t.Namespace == ns && t.Name == "ProtectedMemory");

  var impl = new AssemblyRefUser(classesMod.Assembly);
  mod.ExportedTypes.Add(new ExportedTypeUser(classesMod, mpsTypeDef.Rid, ns, "MemoryProtectionScope", TypeAttributes.Forwarder, impl));
  mod.ExportedTypes.Add(new ExportedTypeUser(classesMod, pmTypeDef.Rid, ns, "ProtectedMemory", TypeAttributes.Forwarder, impl));
  mod.Assembly.Version = new Version(5, 0, 0, 0);

  string thisLocation = Assembly.GetExecutingAssembly().Location;
  string solutionPath = thisLocation.Substring(0, thisLocation.IndexOf(@"ShimBuilder\", StringComparison.InvariantCultureIgnoreCase));
  mod.Write(Path.Combine(solutionPath, "System.Security.dll"));

I didn't have to sign the DLL because the original already had a public key, but if you need to rebuild a .NET DLL, you can use public signing.

The missing puzzle piece for me is getting this all to work in a C++/CLI application, but that probably isn't relevant for most readers of this answer.