Load a .NET assembly from the application's resources and run It from memory, but without terminating the main/host application

走远了吗. 提交于 2019-12-05 01:29:28
squill25

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().

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.

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.

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.

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