Freigeben über


More on 2.0 changes: Delegates Security

=================================

The text below is provided "AS IS", without any responsibilities attached to it. It represents author's personal opinion and knowledge, and does not necessarily reflect recommended best practices of Microsoft. Author does not assume any responsibility caused by the use of the following information.

=================================

If you create a Delegate around a function protected by security Demand, and then somebody invokes that Delegate, the following Security checks will happen in that simple scenario:

1. At Delegate creation time, LinkDemands on method passed to delegate constructor [“Target”] are evaluated against grant set of creating assembly [“Creator”]. If they fail, Security exception is thrown.
2. At Delegate creation time, various pieces of Security-related Assembly identity of the Creator are captured and stored in the Delegate.
3. At Delegate invocation time:
3.1. Creator's identity is evaluated to produce the grant set of the Creator.
3.2. Permission set demanded by the Target is compared to Creator's grant set. If it's found to be not a subset of that grant, Security Exception is thrown.

3.3. Normal Demand against Delegate caller is initiated. If it fails, Security exception is thrown, too.

These Security checks can be viewed in a simpler form: when an assembly creates a delegate to a method in another assembly, this is treated as though it created the delegate to a method in its own assembly which in turn called out to the other assembly. This guarantees that the code creating the delegate is always seen in any Security stack walks that take place while executing code called through the delegate.

Steps 2, 3.1, and 3.2 are marked with bold green because they are new as compared to .NET 1.0 and 1.1. Before 2.0, there was no Security check against Delegate Creator performed at invocation time.

Why this change has been made? The short answer is: to enable easy implementation of secure coding practices around Delegates. And if you want the longer story, here it is with some extra details.

The model which existed in 1.x times permitted untrusted code to create Delegates around virtually any code, and then to inject them into other fully trusted code, staying at the same time outside of the Security checks. At some cases, this is dangerous, since it potentially allows low-trusted code to trick higher trusted one into calling other highly trusted methods which it did not intend to invoke. While this usually does not cause Security holes per se if proper mitigating techniques are used, it certainly makes difficult to implement good Security solution in scenarios where Delegates are involved.

Consider the following pattern [note that it will work only in 1.0 or 1.1] which shows how easy it was to accidentally introduce Security issue in 1.x if some specific guidelines [explained below] were not followed:

1. SystemLib is a highly trusted library exposing methods that could be potentially dangerous if all callers are allowed to invoke them. It could be one of existing .NET APIs, or some third party plug-in which performs low-level system actions, etc. Full Trust is required to call methods of this library; for that reason, they are protected by proper Demands:

public class SystemLib
{

// It has the Declarative form of protection:
[method:PermissionSetAttribute(SecurityAction.Demand, Unrestricted = true)]
public static void DoDangerousThing()
{

// Restarts the computer, or erases system files, etc.
}
}

Then you, as a developer, define in your code the following highly trusted classes:

2.1. Assembly B is fully trusted but accepting delegates from the low-trust side. This does not represent ideal Security practices, but sometimes is needed:

public delegate void BDelegate();

public class B
{
public event BDelegate mOnButtonClick;

 // This is the method called by other classes to notify B that it's time to execute its’ delegates:
public void RunEVentDelegates()
{

// We can even add an extra Security check to this place, just to ensure
// that only fully trusted callers invoke mOnButtonClick() code:
(new PermissionSet(PermissionState.Unrestricted)).Demand();
if (null != mOnButtonClick) mOnButtonClick();
}
}

2.2. Assembly C is simply the caller of B, and it is highly trusted, too. This can be the code of yours, or of somebody else, doesn’t matter.

public class C
{
public static void PingB(B pB)
{
pB.RunEventDelegates();
}
}

At this point, everything looks safe. System method DoDangerousThing has Security Demand on it, so only fully trusted callers can invoke it. RunEventDelegates is protected by Demand, too. It seems like nobody without Full Trust grant can do anything bad. Unfortunately, that’s not enough. A smart attacker can create a code which will trick one part of your [highly trusted] code to call into another fully trusted code -- by manipulating Delegates:

Evil assembly has low trust. It wants to invoke the method SystemLib::DoDangerousThing(), but can't since it does not have required permissions. So it takes different approach: it creates a delegate around desired System code, passes it for storage to highly trusted class B and then waits 'till highly trusted class C fires the event that would invoke the delegate.

public class Evil
{
public static void Main()
{

// Here we create B explicitly for demo purposes. In real life it is possible that
// an existing instance of B will be accessible for Evil:
B mB = new B();
// Then Evil "infects" class B with the system method:
mB.mOnButtonClick += new BDelegate(L.DoDangerousThing);
}

}

Evil’s job is done at this point. Now it just wait’s ‘till something happens what will make C to call B.RunEventDelegates, which in turn will call into SystemLib.DoDangerousThing(). At the moment of the call, Security Demand will be initiated. However, it will check only B and C classes, not the Evil one, and the call will succeed, resulting in computer restart, or in whatever DoDangerousThing was programmed to do.

This looks scary, and may represent one of the common coding mistakes in 1.x .NET. What can you do to ensure your code doesn’t fall a victim of Delegates injection attack described above?

In order to exploit the aforementioned scenario, Evil code needs to enjoy the coincidence of three quite unlikely conditions. If you deprive it of one [or better all] of them, it won’t succeed. What are those conditions?

First, Evil needs to be able to link to dangerous System method, in order to pass it to the Delegate constructor. So if DoDangerousThing is protected by a LinkDemand, Evil will not be able to access it directly and very likely won't be able to access it at all. This protection corresponds to Step 1 of Security checks outlined at the beginning of this post.

OK, this may work if you own SystemLib class and can modify it accordingly, but what if not?

The second and very important is that Evil code needs to find a publicly exposed Event with the signature exactly matching the signature of the System function. In the example above that was easy: DoDangerousThing was "void (void)" function, as well as mOnButtonClick event. If you change the signature of your Event and BDelegate to accept and/or return the instance of privately defined class, it will never match any system’s code function. Therefore, such function won’t be able to be passed as an argument for Delegate creation, and attack will fail.

Overall, if you work with highly trusted publicly accessible Delegates/Events in 1.x, it is critical that their signatures are unique, so they can’t accept anything which it not explicitly designed to match them.

This rule applies to .NET 1.x itself. A big effort was made to ensure that all publicly exposed events of powerful .NET code have unique signatures. As an example, take a look at the AssemblyLoad Event of System.AppDomain class. Its’ signature is

public delegate void AssemblyLoadEventHandler(
object sender,
AssemblyLoadEventArgs args
);

-- which is quite impossible to wrap around something like void File.Delete(string).

Finally, Evil code needs the Event to be accessible, too. So if you can put a Demand or LinkDemand on the Events of your class, it should prevent low-trusted code from adding Delegates to them.

The above seems like enough protection. Then why Secure Delegates were introduced?

The primary reason was the relaxation of Delegate signatures matching rules. In 2.0, you can create a Delegate over method with signature not strictly the same as the Delegate’s one. Some restrictions [which I won’t list here as being not really an expert in the area] still apply there, but the resulting effect is that now everyone can create Delegates over much broader set of targets than before. This significantly reduces the protection offered by rule #2. To compensate on that, Delegates Security was added to the game, so people are still able to create secure and robust Delegates solutions in easy and straightforward manner. Thus now, when every Delegate Creator is automatically put on the Security stack and checked against target method Security requirements, you don’t need to worry about injection attack. .NET will do the checks.

As a final note please keep in mind that if Delegate creator and caller live in the same assembly no Security checks are applied against Delegate creator at calling time. This is reasonable, since in this situation Creator most likely can invoke the target method directly without bothering with Delegates.