从应用程序资源中加载.NET程序集,并从内存中运行它,但不终止主/宿主应用程序。

10

介绍


我将使用由David Heffernan分享的下一个C#代码示例,用于从应用程序资源中加载.NET程序集并从内存中运行它
Assembly a = Assembly.Load(bytes);
MethodInfo method = a.EntryPoint;
if (method != null)
    method.Invoke(a.CreateInstance(method.Name), null);

在这里,我只是分享了一个我自己正在使用的VB.NET适配器:

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

问题


问题在于,如果执行的程序包含应用程序退出指令,则它也会终止我的主/宿主应用程序。例如:
从这个源代码编译出的ConsoleApplication1.exe
Module Module1
    Sub Main()
        Environment.Exit(0)
    End Sub
End Module

当我将 ConsoleApplication1.exe 添加到应用程序资源中,然后使用 Assembly.Load 方法加载并运行它时,由于调用了 Environment.Exit,它也终止了我的应用程序。

问题


我如何在不修改执行的程序集源代码的情况下防止这种情况发生?也许我可以做一些类似于将一个退出事件处理程序与执行的程序集相关联以适当处理/忽略它的事情。此时我的选择是什么?请注意两件事,第一件事是我的意图是以自动化/抽象的方式解决这个问题,我的意思是最终结果应该只需要调用“Execute”方法传递资源和参数,并不用担心其他问题;第二,我希望执行的程序集同步运行,而不是异步运行……如果可能对可能的解决方案有影响,请注意这一点。PS:对我来说,无论给出的解决方案是用C#还是VB.NET编写的都无所谓。

1
你尝试过在另一个应用程序域中加载和执行它吗? - Legends
2
你为什么想要执行这样的控制台应用程序,你有特殊的原因吗? - Legends
你能把内存中的可执行文件写入磁盘(暂时)吗? - George Vovos
1
@Legends Jan 是的,我有一个很好的理由,但我认为如果我从我的角度给出解释与问题本身无关。无论如何:例如这种方式,包含的程序/资源对破解者更加安全; 如果我将应用程序暂时提取到磁盘上执行它,那么任何具有最基本知识的人都可以复制/窃取它。谢谢大家的评论! - ElektroStudios
1
@ElektroStudios暂时不用担心JobObjects,除非你决定使用单独进程,否则这是一个离题的问题。此外,我的建议只是其他解决方案的补充。我并不建议如何防止Environemnt.Exit和其他操作终止您的进程-我只是建议如何隔离影响。有很多情况下,即使在分离的AppDomain中发生(例如StackOverflowException),您基本上无法防止拆除整个进程。因此,最好的方法是将其隔离在单独的进程中,并能够发送消息。 - Jan
显示剩余11条评论
4个回答

7
更新: 我的第一个解决方案不适用于像OP所问包含在程序资源中的程序集;相反,它会从磁盘加载。将提供从字节数组加载的解决方案(正在进行中)。请注意,以下几点适用于这两个解决方案:
  • 由于缺乏权限,Environment.Exit()方法会引发异常,因此在遇到该方法后执行将不会继续。

  • 您需要您的Main方法所需的所有权限,但是您可以通过在Intellisense中输入“Permission”或检查SecurityExceptionTargetSite属性(它是MethodBase的一个实例,并告诉您哪个方法失败)来快速找到这些权限。

  • 如果Main中的另一个方法需要UnmanagedCode权限,则至少使用此解决方案时将无法使用该权限。

  • 请注意,我发现Environment.Exit()需要UnmanagedCode权限,纯粹是通过试错而发现的。

解决方案1:当程序集位于磁盘上时

好的,这是我目前找到的,耐心看完。我们将创建一个受控制的 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...");
}

解决方案2:当程序集是一个字节数组时

警告:下面的解决方案可能会导致问题。

当我将我的解决方案更改为加载字节数组时,我和OP发现了一个奇怪的异常文件未找到异常:即使您向Assembly.Load()传递一个字节数组,domain.ExecuteAssemblyByName()仍然会搜索磁盘上的程序集,出于某种奇怪的原因。 显然,我们不是唯一遇到这个问题的人:加载字节数组程序集

首先,我们有一个Helper类:

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

正如您所见,使用 Assembly.Load() 加载程序集并调用其入口点。这是我们将要加载到 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);
}

请注意,在第二种解决方案中,SecurityException 变成了一个带有 InnerException 属性的 TargetInvocationException,其中包含了 SecurityException。不幸的是,这意味着您不能使用 e.TargetSite 来查看引发异常的方法。
结论/需要记住的事情
这种解决方案并不完美。更好的方法是通过某种方式遍历方法的 IL 并人为地删除对 Environment.Exit() 的调用。

您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - ElektroStudios
@ElektroStudios 此外,您会发现我使用了 adSetup.ApplicationBase,这是我的解决方案运行所必需的,但您可以将其设置为您的主要构建/调试文件夹。 - squill25
我最初对adSetup.ApplicationBase的理解是错误的。最终,我只是将其设置为AppDomain.CurrentDomain.BaseDirectory,这是主exe所在的目录,但您不必担心,这只是技术细节。另外,您能否给我更多关于异常的细节?例如类型、Message属性、InnerException属性(如果有)等等? - squill25
是的,抱歉,我会提供信息的。异常消息如下:无法加载文件或程序集“ConsoleApplication1(版本信息在此处...)”或其某个依赖项。所定位的程序集清单定义与程序集引用不匹配。 我必须说,如果我只使用Assembly.Load()而不干扰应用程序域,那么我就不会遇到这种异常,一切都正常运行...但我的应用程序会关闭。 - ElektroStudios
@ElektroStudios:至于AppDomain.Load(),它只返回一个Assembly。它不应该抛出异常,但它与调用Assembly.Load()并没有什么不同。它仍然需要执行程序集(这就是问题所在)。 - Visual Vincent
显示剩余14条评论

4

所有鸣谢归功于Kirill Osenkov - MSFT

我可以成功地将程序集加载到另一个AppDomain中并调用其入口点。Environment.Exit 始终会关闭宿主进程
解决方法是,从已加载的控制台应用程序的 Main 方法返回一个 int。零表示成功,其他数字表示错误。

不要使用这种方式:

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

写:(我希望这是有效的VB.NET :-))

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

演示 - 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 });
    }
}

另外需要注意的是:

同时,请注意,如果您使用LoadFrom方法,可能会出现FileNotFound异常,因为程序集解析器将尝试在GAC或当前应用程序的bin文件夹中查找您正在加载的程序集。请改用LoadFile方法来加载任意程序集文件--但是请注意,如果您这样做,您需要自己加载所有依赖项。


是的,这就是我得到的解决方案...只是太晚了,我无法发布它。我可以确认它有效。 - squill25
不幸的是,这也不起作用... Environment.Exit 也会退出调用应用程序。 - Legends
是的,但是你只需要像我一样添加权限,你的解决方案就可以工作了。请查看我的解决方案。 - squill25

1

更多的AppDomain代码可以帮助找到解决方案。 代码可在LoadUnload中找到。

项目LoadUnload中包含的小型应用程序包含AppDomain代码,您可能可以在您的解决方案中进行适应。


1

只有一种方法可以做到这一点。您必须动态地检测将要执行的所有代码。这归结为拦截系统调用。没有简单的方法可以做到这一点。请注意,这不需要修改源代码。

为什么.NET安全系统不能做到这一点?虽然系统可以为您提供一个安全权限,您可以使用该权限来控制对Environment.Exit的调用,但这并不能真正解决问题。程序集仍然可以调用非托管代码。其他答案已经指出,这可以通过创建AppDomain并撤销SecurityPermissionFlag.UnmanagedCode来完成。确实,这很有效,但是您在评论中指出您想要使程序集能够调用非托管代码。

如果您想在同一个进程中运行代码,那就这样做。您也可以在另一个进程中运行代码,但是那样就必须进行进程间通信。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接