Binary serialization with dynamically loaded .Net assembly

巧了我就是萌 提交于 2019-12-22 04:09:05

问题


I serialized an instance of my class into a file (with BinaryFormatter)

After, in another project, I wanted to deserialize this file, but it did not work because my new project does not have the description of my old class. The .Deserialize() gets an exception

Unable to find assembly '*MyAssembly, Version=1.9.0.0, Culture=neutral, PublicKeyToken=null'.*".

But I have the .DLL of the assembly containing a description of the old class which I want to deserialize.

I don't want to add a reference a this DLL in the project (I want be able to deserialize a class of any kind of assembly...)

How can I inform the Serializer/Deserializer to use my dynamically loaded assembly?


回答1:


Assuming you are loading your assembly via Assembly.Load() or Assembly.LoadFrom(), then as explained in this answer to SerializationException for dynamically loaded Type by Chris Shain, you can use the AppDomain.AssemblyResolve event to load your dynamic assembly during deserialization. However, for security reasons you will want to prevent loading of entirely unexpected assemblies.

One possible implementation would be to introduce the following:

public class AssemblyResolver
{
    readonly string assemblyFullPath;
    readonly AssemblyName assemblyName;

    public AssemblyResolver(string assemblyName, string assemblyFullPath)
    {
        // You might want to validate here that assemblyPath really is an absolute not relative path.
        // See e.g. https://stackoverflow.com/questions/5565029/check-if-full-path-given
        this.assemblyFullPath = assemblyFullPath;
        this.assemblyName = new AssemblyName(assemblyName);
    }

    public ResolveEventHandler AssemblyResolve
    {
        get
        {
            return (o, a) =>
                {
                    var name = new AssemblyName(a.Name);
                    if (name.Name == assemblyName.Name) // Check only the name if you want to ignore version.  Otherwise you can just check string equality.
                        return Assembly.LoadFrom(assemblyFullPath);
                    return null;
                };
        }
    }
}

Then, somewhere in startup, add an appropriate ResolveEventHandler to AppDomain.CurrentDomain.AssemblyResolve e.g. as follows:

class Program
{
    const string assemblyFullPath = @"C:\Full-path-to-my-assembly\MyAssembly.dll";
    const string assemblyName = @"MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";

    static Program()
    {
        AppDomain.CurrentDomain.AssemblyResolve += new AssemblyResolver(assemblyName, assemblyFullPath).AssemblyResolve;
    }

This ResolveEventHandler checks to see whether the requested assembly has your dynamic assembly's name, and if so, loads the current version from the expected full path.

An alternative would be to write a custom SerializationBinder and attach it to BinaryFormatter.Binder. In BindToType (string assemblyName, string typeName) the binder would need to check for types belonging to your dynamic assembly, and bind to them appropriately. The trick here is dealing with situations in which your dynamically loaded types are nested in a generic from another assembly, e.g. a List<MyClass>. In that case assemblyName will be the name of the assembly of List<T> not MyClass. For details on how to do this see

  • How to create a SerializationBinder for the Binary Formatter that handles the moving of types from one assembly and namespace to another.
  • BinaryFormatter deserialize gives SerializationException

In comments @sgnsajgon asked, I wonder why I cannot deserialize stream the same way I would do when signed assembly is explicitly referenced in project - just formatter.Deserialize(stream) and nothing else.

While I don't know what Microsoft employees were thinking when they designed these classes (back in .Net 1.1), it might be because:

  • In the words of Eric Lippert, no one ever designed, specified, implemented, tested, documented and shipped that feature.

  • BinaryFormatter security is already somewhat of a dumpster fire, but automatically calling Assembly.Load() on any unexpected assembly name in a BinaryFormatter stream might make things even worse.

    By "dumpster fire" I mean that, out-of-the-box, BinaryFormatter will instantiate and populate the types specified in the input stream which might not be the types you are expecting. Thus you might do

    var instance = (MyClass)new BinaryFormatter().Deserialize(stream);
    

    But if the stream actually contains a serialized attack gadget such as TempFileCollection then the gadget will be created and populated and the attack will be effected.

    (For details on this sort of attack, see TypeNameHandling caution in Newtonsoft Json, External json vulnerable because of Json.Net TypeNameHandling auto?, How to configure Json.NET to create a vulnerable web API and Alvaro Muñoz & Oleksandr Mirosh's blackhat paper https://www.blackhat.com/docs/us-17/thursday/us-17-Munoz-Friday-The-13th-JSON-Attacks-wp.pdf. Those links specify how to modify Json.NET's configuration to enable such attacks; BinaryFormatter is vulnerable to them in default configuration.)

    Now if BinaryFormatter were automatically calling Assembly.Load() on unrecognized assembly names, apps using it might additionally become vulnerable to a DLL planting attack where attack types from attack DLLs would get unexpectedly loaded from some unexpected location rather than a secure location, further exacerbating the attack risk.

    (Incidentally, if you do choose to write your own SerializationBinder you can filter out unexpected types and/or known attack types, thus reducing the risk of attack gadget injection. This also can be harder than expected since BinaryFormatter streams often include serialized private or internal classes that you may not know to allow.)

As an aside, What are the deficiencies of the built-in BinaryFormatter based .Net serialization? gives a useful overview of other problems you might encounter using BinaryFormatter.




回答2:


Binary serialization has a no-nonsense attitude to DLL Hell. It records the exact assembly that contained the type when the data was serialized. And insists to find that exact assembly back when it deserializes the data. Only way to be sure that serialized data matches the type, taking any shortcuts will merely ensure you'll get exceptions when your lucky, garbage data when you are not. The odds that this will happen, sooner or later, are 100%.

So you'll need to completely scrap the idea that you can use a "dynamically loaded assembly" and get it to "deserialize a class of any kind", that's an illusion. You can spin the wheel of fortune and put a <bindingRedirect> in the app.exe.config file to force the CLR to use a different assembly version. Dealing with the mishaps are now your responsibility. Many programmers grab the opportunity, few come back from the experience without learning a new lesson. It has to be done to appreciate the consequences. So go ahead.




回答3:


First of all, some facts about binary serialization (skip them if you are interested only in the solutions):

  • The goal of binary serialization is to make a 'bitwise' copy of an object. This often involves serialization of private fields, which may change from version to version. This is not a problem if deserialization happens always in the same process as the serialization (typical use cases: deep cloning, undo/redo, etc.).
  • Therefore binary serialization is not recommended if the deserialization can occur in a different environment (including different platform, framework version, different versions of the assemblies or even the obfuscated version of the same assembly). If you know that any of these applies your case, then consider to use a text-based serialization by public members, such as XML or JSON serialization.
  • It seems that Microsoft started to abandon BinaryFormatter. Though it will be removed/marked as obsolete only in .NET 5 (will be able to be used as a package, though), there are many types also in .NET Core 2/3 that used to be serializable in the .NET Framework but are not serializable anymore in .NET Core (eg. Type, Encoding, MemoryStream, ResourceSet, delegates, etc.).

If you are still sure you want to solve the issue by using the BinaryFormatter you have the following options:

1. Simplest case: only the assembly version has changed

You can add a simple assemblyBinding to the app.config file. Just put the actual version in the newVersion attribute.

<runtime>
  <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
    <dependentAssembly>
      <assemblyIdentity name="MyAssembly" publicKeyToken="null" culture="neutral" />
      <bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
    </dependentAssembly>
  </assemblyBinding>
</runtime>

2. Assembly name and/or type name has also changed (or if you prefer programmatic solutions)

IFormatter implementations (thus also BinaryFormatter) have a Binder property. You can use it to control assembly/type name resolves:

internal class MyBinder : SerializationBinder
{
    public override Type BindToType(string assemblyName, string typeName)
    {
        // mapping the known old type to the new one
        if (assemblyName.StartsWith("MyAssembly, ") && typeName == "MyNamespace.MyOldType")
            return typeof(MyNewType);

        // for any other type returning null to apply the default resolving logic:
        return null;
    }
}

Usage:

var formatter = new BinaryFormatter { Binder = new MyBinder() };
return (MyNewType)formatter.Deserialize(myStream);

If you just need an assembly-version insensitive resolver you can use the WeakAssemblySerializationBinder.

3. The inner structure of the new type has also changed

Since the OP did not cover this case I will not go too deep into the details. TL;DR: In this case you need to set the IFormatter.SurrogateSelector property. You can use it along with the Binder property if both the type name and the inner layout has changed. In case you are interested there are some possible sub-cases cases at the Remarks section of the CustomSerializerSurrogateSelector class.


Final thoughts:

  • The error message in the question is a hint that using BinaryFormatter is maybe not the best choice for your goals. Use the solutions above only if you are sure you want to use binary serialization. Otherwise, you can try to use XML or JSON serialization instead, which serialize types basically by public members and do not store any assembly information.
  • If you want to use the binders/surrogate selectors I linked above you can download the libraries from NuGet. It actually contains also an alternative binary serializer (disclaimer: written by me). Though it supports many simple types and collections natively (so no assembly identity is stored in the serialization stream) for custom types you may face the same issue as emerged in the question.


来源:https://stackoverflow.com/questions/18881659/binary-serialization-with-dynamically-loaded-net-assembly

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!