Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 405
ElektroStudios Avatar asked Jan 08 '17 00:01

ElektroStudios


People also ask

What is the method to load assembly given its file name and its path?

LoadFrom(String) Loads an assembly given its file name or path.

When assembly will load on AppDomain?

If an assembly is loaded into the same AppDomain, then the class can be instantiated in the usual way. But if an assembly is loaded into a different AppDomain then it can be instantiated using reflection. Another way is an interface.

What is the method to load assembly by name?

Loads an assembly given its AssemblyName. The assembly is loaded into the domain of the caller using the supplied evidence. Loads the assembly with a common object file format (COFF)-based image containing an emitted assembly. The assembly is loaded into the application domain of the caller.

Which event allows you to intervene and manually load an assembly that the CLR Cannot find?

AssemblyResolve Event (System)


1 Answers

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

like image 54
squill25 Avatar answered Nov 15 '22 18:11

squill25