如何保护我的私有函数免受反射执行的影响?

16

9
访问修饰符的作用是防止您犯错误,而非提供安全功能。 - Doc Brown
3
反射是.NET中的蜜獾。 - TrueWill
2
这个问题提供了更多信息:https://dev59.com/u3I95IYBdhLWcg3w8iv1 注意:访问修饰符不是安全特性。 - Tim Schmelter
2
请查看 ReflectionPermissionAttribute 类。 - Hans Passant
2
@Hans 实际上,大多数常规 .NET 代码都在完全信任的情况下运行,所以没有进行检查(如果我没记错的话)。 - Marc Gravell
显示剩余9条评论
10个回答

32

如果有人目前能够在您的私有方法上使用反射,那么他们已经拥有足够的访问权限来绕过您设置的任何其他障碍。运行时减少信任可能是一种选择,但这只是为了防止插件等东西获得过多的访问权限 - 它不会阻止具有(例如)管理员访问权限的用户提升访问权限。

如果您不希望代码运行,请不要将其放在恶意用户物理接触范围内; 将其保存在网络服务或类似地方。用户可以直接或间接地使用任何可用的代码,甚至可以对其进行反编译(如果需要还可以进行反混淆)。您可以使用一些技巧来阻止他们(例如通过堆栈跟踪检查调用者),但这不会阻止决心很强的人。


27
这是一个晚回答,但我认为它是一个更新,因为所有的答案都是在2015年中旬.NET 4.6发布之前编写的,该版本引入了一个名为DisablePrivateReflection的新程序集属性。
只需在您的AssemblyInfo.cs中标记该属性,即可禁用对该程序集的私有成员的反射。
例如:
namespace DisablingPrivateReflection
{
    public class Singleton
    {
        private Singleton()
        {

        }
    }
}

在您的AssemblyInfo.cs中添加此行:
[assembly: DisablePrivateReflection]

然后在引用上述程序集的客户端程序集中,此代码将在运行时失败:

var singleton = Activator.CreateInstance(typeof(Singleton), true);

抛出的异常类型为MethodAccessException,并带有以下信息:

方法“Program.Main(System.String[])”尝试访问方法“DisablingPrivateReflection.Singleton..ctor()”失败。


当我将这个与ConfuserEx一起使用时,由于某种原因,我的exe会崩溃,并抛出“System.MethodAccessException”异常。 - mrid
很高兴了解到这个,我认为它可以成为混淆策略的一部分,但正如文档中所指出的那样,不能完全依赖它。https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.disableprivatereflectionattribute?redirectedfrom=MSDN&view=netframework-4.7.2 - DvS

23
如何保护我的私有函数免受反射执行的影响?
您可以更改安全策略,以便在运行代码时不允许进行“私有反射”。当然,这只会影响到的电脑。如果您想影响其他人的电脑,则需要发送电子邮件给他们的机器管理员并要求管理员更改用户的安全策略,以便不允许进行“私有反射”。那是拥有机器和其运行网络的人;显然,您没有能力更改您不拥有的网络上的安全设置。
当然,比私有反射权限更强大的权限也必须受到限制。例如,“拒绝私有反射权限,但授予更改安全策略的权限”并没有任何用处。用户随后可以更改安全策略,将私有反射权限重新授予自己。
您还需要限制访问磁盘的能力。能够访问磁盘的人可以简单地从程序集中读取代码,将私有元数据更改为公共,并正常加载程序集。
因此,您的任务是说服世界上所有机器管理员不允许其用户访问自己的磁盘。我怀疑您会失败;我建议您找到一种不同的方法来保护您的函数免受滥用。

9

访问修饰符并非安全机制。

如果您可以防止函数被通过反射调用,用户仍然可以反编译程序,提取该函数,将其放入新的程序集中并执行它。


3
using System;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Generic;
using System.Linq;

class Person
{
    private string SayInternalSecure()
    {
         if (!PrivacyHelper.IsInvocationAllowed<Person>())
            throw new Exception("you can't invoke this private method");
         return "Internal Secure";
    }

    private string SayInternal()
    {
         return "Internal";
    }

    public string SaySomething()
    {
         return "Hi " + this.SayInternal();
    }

    public string SaySomethingSecure()
    {
        return "Hi " + this.SayInternalSecure();
    }

    public void BeingCalledBy()
    {
            Console.WriteLine("I'm being called by: " + new StackTrace().GetFrame(1).GetMethod().Name);
    }
}

public class MethodBaseComparer : IEqualityComparer<MethodBase> 
{
     private string GetMethodIdentifier(MethodBase mb)
     {
      return mb.Name + ":" + String.Join(";", mb.GetParameters().Select(paramInfo=>paramInfo.Name).ToArray());
     }

     public bool Equals(MethodBase m1, MethodBase m2) 
     {
        //we need something more here, comparing just by name is not enough, need to take parameters into account
        return this.GetMethodIdentifier(m1) == this.GetMethodIdentifier(m2);
     }

     public int GetHashCode(MethodBase mb) 
     {
             return this.GetMethodIdentifier(mb).GetHashCode();
     }
}

class PrivacyHelper
{
static Dictionary<Type, MethodBase[]> cache = new Dictionary<Type, MethodBase[]>();
public static bool IsInvocationAllowed<T>()
{
    Type curType = typeof(T);
    if (!cache.ContainsKey(curType))
    {
        cache[curType] = curType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).ToArray();
    }
    //StackTrace.GetFrame returns a MethodBase, not a MethodInfo, that's why we're falling back to MethodBody
    MethodBase invoker = new StackTrace().GetFrame(2).GetMethod();
    return cache[curType].Contains(invoker, new MethodBaseComparer());
}
}

public class App
{
public static void CheckCaller()
{
    Person p = new Person();

    Console.WriteLine("- calling via delegate");
    Action action = p.BeingCalledBy;
    action();

    Console.WriteLine("- calling via reflection");
    MethodInfo method = typeof(Person).GetMethod("BeingCalledBy", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    Console.WriteLine(method.Invoke(p, null));

    Console.WriteLine("- calling via delegate again");
    action = (Action)(Delegate.CreateDelegate(typeof(Action), p, method));
    action();
}

public static void Main()
{
    Console.WriteLine("Press key to run");
    Console.ReadLine();

    CheckCaller();

    Person p = new Person();
    Console.WriteLine(p.SaySomething());
    Console.WriteLine(p.SaySomethingSecure());

    MethodInfo privateMethod = typeof(Person).GetMethod("SayInternal", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    Console.WriteLine("invoking private method via Reflection:");
    Console.WriteLine(privateMethod.Invoke(p, null));

    Console.WriteLine("----------------------");

    privateMethod = typeof(Person).GetMethod("SayInternalSecure", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    Console.WriteLine("invoking secured private method via Reflection:");
    try
    {
        Console.WriteLine(privateMethod.Invoke(p, null));
    }
    catch(Exception ex)
    {
        Console.WriteLine(ex.Message);
    }
}
}

1

没有办法做到那样。为什么你想防止执行你的私有函数?通常,如果有人使用反射,他知道自己在做什么。


我的私有方法在外部属性进行多次计算后被内部调用。我不希望有人激活该函数并绕过程序的正常流程。 - Royi Namir
6
@RoyiNamir 为什么你关心那种情况?如果其他开发者决定这样做并引起问题,那就是他的问题,不是你的问题。把你的代码看作一台机器,你把这台机器交给别人去使用。那个人打开机器并开始搞乱其中的运作,你不能阻止他这样做,你所能做的就是贴上标签说这将使保修无效。如果他们损坏了它,那就是因为他们忽略了你的警告,自己承担后果,而不是你。 - Ben Robinson
这里的主要用例与安全无关,而是代码可维护性... 在一个大型的单体系统中,你需要在你的有界上下文中建立坚固的墙壁,以便每个模块可以独立于其他模块进行开发,而不会破坏其他模块... 如果另一个模块中的开发人员使用反射来调用我未公开的模块中的方法, 而我随后删除了该方法,则另一个模块现在就被破坏了。通过设立这些强有力的/不可违犯的边界,开发团队可以彼此独立地移动,而不必担心出现破坏事故。 - Deven Phillips

1

你不能这样做,修饰符只是为了让开发人员拥有适当的封装。运行时可能会感到一切都处于同一级别。

反射机制通常被应用程序使用,需要调用一些预配置方法(旧 ORM)或显示它们(IDE)。如果这种机制无法做到这一点,那将非常困难。


1
这取决于运行时的程度,例如Silverlight在比完整的.NET更紧密的沙盒中运行,并且默认情况下可用性较少。 - Marc Gravell

1

虽然我完全同意访问修饰符不是安全功能的想法,但为了编程的缘故,我思考了一下这个问题,并得出了一个简单而不太有用的机制,使用反射来对抗反射 :-)

请注意,这只是一个愚蠢的概念证明,我没有考虑泛型方法,如果要使用泛型方法,需要进行更改...

这个想法是,在你想要保护免受“非法”调用的每个私有方法的开头,你只需通过反射检查你是否从该类的另一个方法中调用,而不是从外部调用。所以你会使用: new StackTrace().GetFrame(1).GetMethod(); 来获取你的调用者的MethodBase,并将其与你的类的MethodInfos列表进行比较。

你可以将它添加到一个帮助类中(无论如何,你都需要一个IEqualityComparer来比较MethodBases...

一个问题是,你也会阻止一些正确的调用,比如通过委托或通过反射从你的另一个方法中调用... 所以你在使用它时应该小心。

你可以在这里找到我的实现here


0

我使用eazfuscator,将方法变成虚拟方法似乎可以很好地隐藏方法。

然后,在我想要保护的方法中,我会在发布模式下更改代码,以便生成空指针并执行任务,而代码没有调用堆栈。

没有太多的开发人员能够在不进行调试的情况下处理您的生产代码...

#if !DEBUG
    #pragma warning disable CS8618 
    
    [System.Diagnostics.DebuggerStepThrough, System.Diagnostics.DebuggerNonUserCode]
    [System.ComponentModel.Browsable(false)]
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
#endif

namespace MyApp.Infrastructure.LicenseManager
{
    [Obfuscation(Feature = "apply to member * when method or constructor: virtualization", Exclude = false)]
    internal sealed class LicenseManager(ISomeClass parameter, ILoggerFactory? factory = null)
    {
       //hide the name space for reverse engineering the naming scheme
       _logger = factory?.CreateLogger("MyApp.LicenseManager") 

    #if !DEBUG
        if (System.Diagnostics.Debugger.IsAttached)
        {
            //send computer information for license violation and prosecution then crash
            System.Threading.ThreadPool.UnsafeQueueUserWorkItem(SystemCrasher.SendAbuseAndCrash);
            
            //send a log entry, of don't if you wish to give no warning
           _logger?.LogWarning(Walter.BOM.LogEvents.FireWall_Warning, "Debugging will cause the developer to generate null pointer exceptions in administration context");
           return;
           
           //all class initiolizers will stay null
         }
    #endif

        _myClass= parameter;

}

#if !DEBUG
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
#endif

如果你的代码被共享 DLL(比如 NuGet 包中的代码)所使用,那么上述方法将会让你在同事中不受欢迎。你需要一种“调试”友好的方法,使用方法代理结合混淆可能是对你有帮助的技巧。
[Obfuscation(Feature = "virtualization", Exclude = false)]
private void MyMethod(int parameter)
{
    MyMethodProxy();
    //do some code
}

private void MyMethodProxy([CallerMemberName] string? calledBy = null)
{
    if (!string.Equals(nameof(MyMethod), calledBy, StringComparison.Ordinal))
    {
        throw new LordOfTheRingsException("You shall not pass!!");
    }
}

通过使用NameOf方法,您可以在混淆器的重命名方案中幸存非可打印字符。
我使用了其他一些选项,但这会限制您自动化和使用面向方面的框架,如postsharp
希望它有所帮助输入代码

-1

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