Load a .NET assembly from the application's re

2020-08-23 06:04发布

问题:

INTRODUCTION


I'm using the next C# code example shared by David Heffernan' for loading a .NET assembly from the application's resources and run It from memory:

Assembly a = Assembly.Load(bytes);
MethodInfo method = a.EntryPoint;
if (method != null)
    method.Invoke(a.CreateInstance(method.Name), null);

Here I just share an adaptation in VB.NET that I am using too:

Public Shared Sub Execute(ByVal resource As Byte(), ByVal parameters As Object())

    Dim ass As Assembly = Assembly.Load(resource)
    Dim method As MethodInfo = ass.EntryPoint

    If (method IsNot Nothing) Then
        Dim instance As Object = ass.CreateInstance(method.Name)
        method.Invoke(instance, parameters)
        If (instance IsNot Nothing) AndAlso (instance.GetType().GetInterfaces.Contains(GetType(IDisposable))) Then
            DirectCast(instance, IDisposable).Dispose()
        End If
        instance = Nothing
        method = Nothing
        ass = Nothing

    Else
        Throw New EntryPointNotFoundException("Entrypoint not found in the specified resource. Are you sure it is a .NET assembly?")

    End If

End Sub

PROBLEM


The problem is that if the executed assembly has an application exit instruction, then it also terminates my main/host application too. For example:

ConsoleApplication1.exe compiled from this source-code:

Module Module1
    Sub Main()
        Environment.Exit(0)
    End Sub
End Module

When I add ConsoleApplication1.exe to the application resources, and then I load it and run it with the Assembly.Load methodology, it also terminates my application because the call to Environment.Exit.

QUESTION


How can I prevent this, without modifying the source code of the executed assembly?.

Maybe I could do somehting like associate a kind of exit event handler to the executed assembly to handle/ignore it properly?. What are my options at this point?.

PS: For me no matter if the given solution is written in C# or VB.NET.

Please note two things, the first is that my intention is to resolve this issue in an automated/abstracted way, I mean the final result should just need to call the "Execute" method passing the resource and the arguments and don't worry about the rest; and secondly, I want the executed assembly to be ran synchronuslly, not async... in case of that could matter for a possible solution.

回答1:

Update: My first solution doesn't work for assemblies contained in the resources of a program like OP asked; instead it loads it from the disk. The solution for loading from a byte array will follow (in progress). Note that the following points apply to both solutions:

  • As the Environment.Exit() method throws an exception because of lack of permissions, execution of the method will not continue after it is encountered.

  • You're going to need all the permissions that your Main method needs, but you can find those quickly by just typing in "Permission" in intellisense, or by checking the SecurityException's TargetSite property (which is an instance of MethodBase and will tell you which method failed).

  • If another method in your Main needs the UnmanagedCode permission, you're out of luck, at least using this solution.

  • Note that I found that the UnmanagedCode permission was the one that Environment.Exit() needs purely by trial and error.

Solution 1: When the assembly is on the disk

Okay, here's what I've found so far, bear with me. We're going to create a sandboxed AppDomain:

AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// This is where the main executable resides. For more info on this, see "Remarks" in
// https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1

PermissionSet permission = new PermissionSet(PermissionState.None);
// Permissions of the AppDomain/what it can do

permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode));
// All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit()
// BUT the assembly needs SecurityPermissionFlag.Execution to be run;
// otherwise you'll get an exception.

permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
permission.AddPermission(new UIPermission(PermissionState.Unrestricted));
// the above two are for Console.WriteLine() to run, which is what I had in the Main method

var assembly = Assembly.LoadFile(exePath); // path to ConsoleApplication1.exe

var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain

try
{
    domain.ExecuteAssemblyByName(assembly.GetName(), new string[] { });
}
// The SecurityException is thrown by Environment.Exit() not being able to run
catch (SecurityException e) when (e.TargetSite == typeof(Environment).GetMethod("Exit"))
{
    Console.WriteLine("Tried to run Exit");
}
catch (SecurityException e)
{
    // Some other action in your method needs SecurityPermissionFlag.UnmanagedCode to run,
    // or the PermissionSet is missing some other permission
}
catch
{
    Console.WriteLine("Something else failed in ConsoleApplication1.exe's main...");
}

Solution 2: When the assembly is a byte array

Warning: cancerous solution follows.

When changing my solution to load a byte array, OP and I discovered a weird exception file not found exception: even if you pass in a byte array to Assembly.Load(), domain.ExecuteAssemblyByName() still searches the disk for the assembly, for some weird reason. Apparently we weren't the only ones with the issue: Loading Byte Array Assembly.

First, we have a Helper class:

public class Helper : MarshalByRefObject
{
    public void LoadAssembly(Byte[] data)
    {
        var a = Assembly.Load(data);
        a.EntryPoint.Invoke(null, null);
    }
}

which as you can see, loads the assembly using Assembly.Load() and calls it's entry point. This is the code we'll be loading into the AppDomain:

AppDomainSetup adSetup = new AppDomainSetup();
adSetup.ApplicationBase = AppDomain.CurrentDomain.BaseDirectory;
// This is where the main executable resides. For more info on this, see "Remarks" in
// https://msdn.microsoft.com/en-us/library/system.appdomainsetup.applicationbase(v=vs.110).aspx#Anchor_1

PermissionSet permission = new PermissionSet(PermissionState.None);
// Permissions of the AppDomain/what it can do

permission.AddPermission(new SecurityPermission(SecurityPermissionFlag.AllFlags & ~SecurityPermissionFlag.UnmanagedCode));
// All SecurityPermission flags EXCEPT UnmanagedCode, which is required by Environment.Exit()
// BUT the assembly needs SecurityPermissionFlag.Execution to be run;
// otherwise you'll get an exception.

permission.AddPermission(new FileIOPermission(PermissionState.Unrestricted));
permission.AddPermission(new UIPermission(PermissionState.Unrestricted));
// the above two are for Console.WriteLine() to run, which is what I had in the Main method

var domain = AppDomain.CreateDomain("SomeGenericName", null, adSetup, permission, null); // sandboxed AppDomain

try
{
    Helper helper = (Helper)domain.CreateInstanceAndUnwrap(typeof(Helper).Assembly.FullName, typeof(Helper).FullName);
    // create an instance of Helper in the new AppDomain
    helper.LoadAssembly(bytes); // bytes is the in-memory assembly
}
catch (TargetInvocationException e) when (e.InnerException.GetType() == typeof(SecurityException))
{
    Console.WriteLine("some kind of permissions issue here");
}
catch (Exception e)
{
    Console.WriteLine("Something else failed in ConsoleApplication1.exe's main... " + e.Message);
}

Note that in the second solution, the SecurityException becomes a TargetInvocationException with it's InnerException property being a SecurityException. Unfortunately, this means that you cannot use e.TargetSite to see which method threw the exception.

Conclusion/Things to keep in mind

This solution isn't perfect. It would be much better to somehow go through the IL of the method and artificially remove the call to Environment.Exit().



回答2:

All credits go to Kirill Osenkov - MSFT

I can successfully load the assembly into another AppDomain and call its entry point. Environment.Exit always shuts down the hosting process.
Workaround for this would be, to return an int from Main of the loaded console application. Zero for success and other numbers for errors.

Instead of this:

Module Module1
    Sub Main()
        // your code
        Environment.Exit(0)
    End Sub
End Module

write: (I hope this is valid VB.NET :-))

Module Module1
 Function Main() As Integer
    // your code
    Return 0 // 0 == no error
 End Function
End Module

Demo - C#

class Program
{
    static void Main(string[] args)
    {
        Launcher.Start(@"C:\Users\path\to\your\console\app.exe");
    }
}    

public class Launcher : MarshalByRefObject
{
    public static void Start(string pathToAssembly)
    {
        TextWriter originalConsoleOutput = Console.Out;
        StringWriter writer = new StringWriter();
        Console.SetOut(writer);

        AppDomain appDomain = AppDomain.CreateDomain("Loading Domain");
        Launcher program = (Launcher)appDomain.CreateInstanceAndUnwrap(
            typeof(Launcher).Assembly.FullName,
            typeof(Launcher).FullName);

        program.Execute(pathToAssembly);
        AppDomain.Unload(appDomain);

        Console.SetOut(originalConsoleOutput);
        string result = writer.ToString();
        Console.WriteLine(result);
    }

    /// <summary>
    /// This gets executed in the temporary appdomain.
    /// No error handling to simplify demo.
    /// </summary>
    public void Execute(string pathToAssembly)
    {
        // load the bytes and run Main() using reflection
        // working with bytes is useful if the assembly doesn't come from disk
        byte[] bytes = File.ReadAllBytes(pathToAssembly); //"Program.exe"
        Assembly assembly = Assembly.Load(bytes);
        MethodInfo main = assembly.EntryPoint;
        main.Invoke(null, new object[] { null });
    }
}

Also to note:

Also, note that if you use LoadFrom you'll likely get a FileNotFound exception because the Assembly resolver will attempt to find the assembly you're loading in the GAC or the current application's bin folder. Use LoadFile to load an arbitrary assembly file instead--but note that if you do this you'll need to load any dependencies yourself.



回答3:

There is only one way to do this. You have to dynamically instrument all the code that the assembly is going to get executed. This boils down to intercepting system calls. There is no easy way to do this. Note that this does not require modifying the source code.

Why can't the .NET security system do this? While the system could have provided you with a security permission that you can use to control calls to Environment.Exit, that would not really solve the problem. The assembly could still call into unmanaged code. Other answers have pointed out that this can be done by creating an AppDomain and revoking SecurityPermissionFlag.UnmanagedCode. Indeed, this works, but you indicated in the comments that you to enable the assembly to call unmanaged code.

That's it if you want to run the code in the same process.You could also run the code in another process, but then you have to do interprocess communication.



回答4:

More AppDomain code could help in finding a solution. Code can be found at LoadUnload

The small applications included in the project LoadUnload contain AppDomain code that you maybe able to adapt in your solution.