|
|||||||||||
Preventing Elevation of Privilege Attacks Using the .NET Security Actions
In the last article we discussed the fundamental programmer actions of the Code Access Security System: Security Actions. In our discussion, we did not go into detail about two of the actions, LinkDemand and Assert. Both of these actions require a more detailed discussion because if they are not handled properly they open a major security hole that could be exploited with a “luring” or “elevation of privilege” attack.
The Elevation of Privilege Attack
This security hole is the “elevation of privilege attack” or the “luring attack”. According to the Principle of Least Privilege we only give an assembly the minimum privileges it needs. What makes life complicated is that an assembly with minimum privileges (called a “partially trusted assembly” or an “untrusted assembly”) will at some point make a call into an assembly that has more privileges than it has, or even into a fully-trusted assembly. At that point, the trusted code could perform, on behalf of the untrusted code, an action (like erasing your hard drive, or sending off your client’s credit card numbers) that they would not be allowed to do on their own.
Let’s assume that you have created an assembly, log.dll, that reads and writes information about software errors in your application to a log file. Within log.dll you have specified that all callers should have been granted the Full Trust Permission Set in order to use log.dll and access the log file (for details about the Full Trust Permission Set see last month’s article). Another assembly in your application, app.dll uses log.dll to analyze the error information in the log file. If a malicious third party could read this error information, it could use it to wreck havoc with, or steal information from, your application. An elevation of privilege attack occurs when a third, partially trusted assembly, called evil.dll, which was accidentally downloaded from Hacker.com, is able to call app.dll and use it to interact with log.dll.
How do you as a developer prevent elevation of privilege attacks? How can you predict all the possible callers of your trusted assembly? Take heart, because if you follow a few basic guidelines, you can virtually eliminate the elevation of privilege attack. First, let us review how the .NET security system evaluates permissions.
A set of security permissions is assigned to an assembly based on their identifying characteristics, or evidence. These characteristics include such things as the location from which the assembly was executed, or the cryptographic strong name of the assembly. When an assembly is loaded, the evidence is presented to the Common Language Runtime (CLR). Using this evidence, the CLR consults the security policies to determine the final set of permissions granted to the assembly. This policy is based on the security rules set by the administrators for the enterprise, the individual machines and users, as well as by the application host. This process determines the relative level of trust that any given assembly has. The last month’s article had a more detailed discussion of this with illustrative examples.
Demands
The previous paragraph describes how the rights associated with an assembly are assigned. At runtime these rights have to be checked to determine if a particular piece of code should be allowed to execute. In other words, is the code trusted enough? Evil.dll is not trusted. What prevents it from calling app.dll, or even log.dll itself that are trusted? Clearly, to enforce trust, all the assemblies that have code on the current call-stack have to be checked for trust.
Trust is enforced at runtime primarily through the three demand security actions of the Code Access Security System listed in the Demand Actions Table. As discussed in the previous article, you normally guard against this type of attack by having log.dll use the Demand security action.
The Demand operation invokes a full stack-walk at runtime, in which every frame of the call-stack is examined for the demanded permission or permission set. During a stack-walk, the flow of your application is completely halted for the duration of the check. This causes a slight performance hit, but the Demand action is by far the safest, and should be the one that you generally use. Most of the time, however, this performance hit is not noticeable to human perception. In fact, if you have ever run managed code at all, you have probably experienced several stack-walks without even knowing it!
Failure to have the specified permission causes a SecurityException to be thrown. You (or the callers of your code) can easily catch this exception by placing code in a try/catch block. You can gracefully handle the failure in the manner of your choosing. Hence, untrusted code cannot get more trusted code to work on its behalf.
Note that this stack walk starts at the stack frame above the frame where the demand is made. Suppose code in assembly “a” calls a method in assembly “b”. If method “b” makes no further method calls and makes a demand, only assembly “a” is evaluated. If the method in “b” makes a method call “c” (irrespective of whether or not “c” is in the same assembly) and within “c” a demand is made, both “a” and “b” would be evaluated for trust.
Most of the time this check is made by the framework class libraries. As discussed in the previous article, sometimes your code makes the demand to make error recovery easier. The one time you must make the demand, as in the log.dll example, is if you are writing code for an object that has its own unique set of security requirements.
The Assert
The Demand action can only prevent an elevation of privilege attack if it is able to examine every frame on the call stack. If you use the Assert action, a stack-walk for a particular permission is halted without error if an assert for the same permission is encountered. In this situation, any callers above the frame where the assert occurs will not be examined at all, and there is no way to be sure that such callers have been granted the permissions that you require.
To illustrate the use of Assert and Demand Security actions, let’s assume that log.dll has the following demand for full trust placed on all classes in the library:
[PermissionSet(SecurityAction.Demand, Name="FullTrust")]
When app.dll tries to use a class in log.dll, it had better have the Full Trust Permission Set; otherwise, an exception is generated, and app.dll is prevented from using log.dll. The same requirement applies to all direct and indirect callers of log.dll such as app.dll or evil.dll.
However, let’s assume that app.dll decided to assert permission for all of its callers as demonstrated in the following code:
[PermissionSet(SecurityAction.Assert, Name="FullTrust")]
In this situation, evil.dll from hacker.com could call app.dll and indirectly call log.dll without ever being granted full trust because the stack walk is halted when the frame containing the assert is encountered. Hence, the trust level of evil.dll is never evaluated. As you can imagine, this has the potential to be very dangerous.
You can only assert permissions your assembly already has. App.dll could make the assertion because it is fully trusted. Evil.dll itself would get a SecurityException if it tried to make such an assert.
The Assert security action is used in situations where you need to provide a safe version of code to callers you otherwise would not trust. The ConsoleAssert sample illustrates this (see Listing 1 below). Version 1 of the .NET framework does not provide any way to change the background color or text color of a console application. If a program needs to do this, it must use the Platform Invoke (PInvoke) facility to make unmanaged calls into Kernel32.dll. An assembly with such code would require UnmanagedCodePermission because the PInvoke facility automatically performs a demand for this permission. You rarely want to grant UnmanagedCodePermission to an assembly. If you do, the assembly could use it to circumvent the entire .NET security system by making calls directly into the underlying operating system! For example, by default code, from the Internet or Local Internet does not have this privilege. Follow the instructions that accompany the sample to set up the ConsoleAssert’s main program to be partially trusted.
You can, however, build a library (such as AssertLib in the sample) that has the permission to call unmanaged code. Because this assembly has the unmanaged code permission by default when it is executed from a local drive, it can assert it as in the sample code below. When the demand is made for the unmanaged code permission the CLR will not thrown an exception. If you comment out the Assert, the code will fail because all the callers on stack will not have the unmanaged code permission.
Why is this code safe? Notice that this library does not expose sensitive resources. You can't mount an attack by changing the color of console elements. In this case, it is safe to go ahead and do an assert for permission to interoperate with unmanaged code. After the assert is made, partially trusted code can call the library without being granted permission to interoperate with unmanaged code. The Assert expires once the method has executed so no other methods are affected.
public enum STDOut { STD_INPUT_HANDLE = -10, STD_OUTPUT_HANDL = -11, STD_ERROR_HANDLE = -12 } public enum ColorFlag { FOREGROUND_BLUE = 0x0001, FOREGROUND_GREEN = 0x0002, FOREGROUND_RED = 0x0004, FOREGROUND_INTENSITY = 0x0008, BACKGROUND_BLUE = 0x0010, BACKGROUND_GREEN = 0x0020, BACKGROUND_RED = 0x0040, BACKGROUND_INTENSITY = 0x0080 } [SecurityPermissionAttribute(SecurityAction.Assert, UnmanagedCode = true)] public class ConsoleColor { [DllImport("Kernel32.dll")] private static extern bool SetConsoleTextAttribute (int hConsoleOutput, long wAttributes);
[DllImport("Kernel32.dll")] private static extern int GetStdHandle(STDOut nStdHandle);
public static bool SetColor(long ColorFlagVal) { int handle = GetStdHandle( STDOut.STD_OUTPUT_HANDL); return SetConsoleTextAttribute(handle, ColorFlagVal); } }
Suppose you want to allow access to sensitive resources to partially trusted code? This is the problem the developers of the .NET Framework File IO classes had to solve. If they just asserted the unmanaged code permission then code could modify files it had no right to. What that code does, however, is demand the appropriate FileIO permissions before making the assert. Only if that demand succeeds (i.e. the calling code has the appropriate rights) will the framework code assert the unmanaged code permission and make the Win32 call. If you find yourself in a similar situation, you should adopt a similar strategy.
Assert actions, however, can be used by sloppy developers to ensure that their code always has access to protected resources even in situations where their code would not be granted permission to use those resources. Such a developer only needs to install a fully trusted library that asserts permissions for his or her partially trusted code. As a rule, you should never use Assert to bypass local security. If your customers trust your assemblies, they will assign them enough permission to do what they need.
LinkDemand
The LinkDemand action does not cause the CLR to do a full stack walk and therefore is not always a reliable way to protect against elevation of privilege attacks. It causes a check of only your assembly’s immediate caller when linking occurs at JIT time. Hence it offers a slight performance advantage over the Demand action. The LinkDemand action was designed for situations when the following conditions are true: 1) The performance of your application so important that you can't afford the overhead of the full stack walk associated with a regular demand, 2) You are sure that all callers will take responsibility for extending the LinkDemand to all of their callers.
Although you gain a little speed, you loose the ability to make sure that every caller on the stack has the permission that you require. If the immediate caller does not have the required permission, then a LinkDemand will cause a PolicyException to be thrown at JIT time. Because this exception occurs before your code executes and outside the scope of your code, there is no way for you or your callers to catch this exception. Since you cannot catch this exception, your users cannot get a good user interface or explanation of what went wrong. This is one of their main drawbacks. See the side bar for a full explanation of this problem.
When you use a link demand, the only way to prevent an elevation of privilege attack is to make sure that all callers duplicate the link demand on their level. If they don't, then partially trusted callers will be able to call your code simply by calling your callers (i.e. a method in evil.dll calls a method in app.dll). Although you might be able to ensure your code always extends the LinkDemand, you can’t guarantee that anyone else’s code will.
Consider the following class from the included sample that uses a LinkDemand to protect a method from partially trusted callers. Although the InvokeMethod of SimpleClass uses a LinkDemand for the FullTrust permission set, the InvokeNoLinkDemand method does not. It is possible for a partially trusted application to call the InvokeNoLinkDemand method and not generate an exception. If a partially trusted application calls the Invoke method an exception will be generated.
public class SimpleClass { // Do a linkDemand for full trust. [PermissionSet(SecurityAction.LinkDemand, Name="FullTrust")] public string Invoke() {
return InvokeNoLinkDemand(); }
// Return a string indicating that the library // was called. public string InvokeNoLinkDemand() {
return "You have invoked LinkDemandLib.dll"; } }
The following event handlers of the Windows Form application respond to click events of two buttons. One code path calls the method protected by the LinkDemand, while the other does not. Because methods are JITed on an as needed basis, the btnInvoke_Click method will not get JITed if it is not called, causing the LinkDemand in SimpleClass.Invoke never be made.
private void btnInvoke_Click(object sender, System.EventArgs e) { tbDisplay.Text = new SimpleClass().Invoke(); }
private void btnInvoke2_Click(object sender, System.EventArgs e) { tbDisplay.Text = new SimpleClass().InvokeNoLinkDemand(); }
To experience this behavior, we need to make sure that the Windows Form application does not receive the FullTrust Permission Set. You can make sure that it doesn’t by refusing a permission explicitly in code. When you add the following code to the AssemblyInfo file of your project, your application will not be granted FileIOPermission and won’t receive full trust.
using System.Security.Permissions;
[assembly: FileIOPermission( SecurityAction.RequestRefuse, Unrestricted = true)]
Try running the LinkDemand sample. When you click on the “Invoke Without LinkDemand” button, the library is accessed and a message is displayed to the text box. However, when you click the “Invoke linkDemand” button, an exception is generated. Even if you add a try/catch block to the btnInvoke_Click method, the exception generated by the link demand will still manifest. You should also try commenting out the previous code that explicitly refuses FileIOPermission. After you do so and execute the application from a local drive, the program will pass the LinkDemand and no exception will be generated.
Using a LinkDemand to Prevent Elevation of Privilege
As the second LinkDemand sample illustrates, you can successfully use a LinkDemand to prevent an elevation of privilege attack when you use a permission that represents a strong name used by you or your company. In this situation, only assemblies that have the strong name will be able to call your code. Because all callers have to be from you or your company, you can eliminate your code’s use by partially trusted assemblies or assemblies that you have no knowledge of.
Before you sign your files with a strong name, you need to create a new asymmetric key-pair. Of course, your company might already have a key, in which case, you will want to use it. The sn.exe tool that ships with the .NET Framework SDK allows you to easily generate a key if you don’t already have one. The following command will generate a new cryptographic key and save it to file called key.snk in the current directory.
sn -k key.snk
Next, you need to sign the assemblies in your project with the key you just created. You can either use the Al.exe tool, or you can use an attribute that points to the .snk file. For this demonstration, we will use the attribute. Go ahead and add the following attribute to the AssemblyInfo.cs file for all projects in your solution.
[assembly: AssemblyKeyFile ("..\\..\\..\\key.snk")]
Notice that visual studio adds this attribute automatically to every new project, so all you really have to do is fill in the path to your .snk file. By default, the AssemblyKeyFileAttribute looks for the .snk file in the directory that contains your assembly, so you will need to point the attribute to the proper directory if you put your .snk file somewhere else. The example places the .snk file in the root of the solution folder.
You also need to specify the version number of your assembly. By default, Visual Studio adds the AssemblyVersionAttribute to every new project. For this example, we will slightly modify the default setting and specify the following version number. If you don’t do this, then the minor version number will change whenever you recompile.
[assembly: AssemblyVersion("1.0.0.0")]
After you supply the previous attributes, all you need to do is compile you project and it will automatically be signed with a strong name.
Now that the assemblies are signed, you can go ahead and do a LinkDemand for the strong name. The StrongNameIdentityPermission accepts the public key, name, and version of a strongly named assembly as parameters and allows you to perform demands and requests against a specified strong name. You can either use imperative syntax with the StrongNameIdentityPermision class or use declarative syntax with the StrongNameIdentityPermissionAttribue class. Both classes achieve the same end-result, but the StrongNameIdentityPermissionAttribue class is a little easier to use. It has the advantage that it can be applied to entire classes (excluding public fields) with one declaration.
Before you apply the attribute to your code, you need to know the actual value for the public key. Using the sn.exe tool, you can display the public key of an assembly or a .snk file. The –tp flag extracts the key from a .snk file that only contains a public key while the –Tp flag extracts it from a strongly named assembly as demonstrated by the following commands. Note that the .snk file we created earlier to sign the assembly contains both a public and private key. See the sidebar for the details about how to create a new .snk file that contains only a public key.
sn -tp pubkey.snk sn –Tp LinkDemandLib.dll
When you run one of the previous commands, your public key will be displayed to the command console in a hexadecimal value. Just copy the key from the command console and paste it in as the value of the StrongNameIdentityPermissionAttribute’s PublicKey property as shown in the following code. Note that you can also specify a version number using the Version property and an assembly name using the Name property. If you omit the Name or Version property, the runtime will allow assemblies with any version or name to link to your library as long as they are signed with the correct key. Unfortunately, the Version property does not accept wild-card characters, so you always have to provide the exact version number.
using System; using System.Security.Permissions;
namespace LinkDemandLib { [StrongNameIdentityPermissionAttribute( SecurityAction.LinkDemand, PublicKey = "0024000004800 0009400000006020000002400005253413100040000010001000b feee407eb3d2a5aa14f1e2ef40727c78e86a3f95baa455dcd1953 51e04ac8bcfc657f6759f4a599799868864edd610f465bc0a1f76 5f4e5d57a954ebbc23517ce9b443302c14463a5b29d734f098980 bedd05a627271cfe1f4208d63fb6fff92723310e475cbf8746cc0 7f2d34ace97085de3281295a2f83eb9f78f6291fb3", Version = "1.0.0.0")] public class SimpleClass { . . . } }
After you apply the demand, only code that is signed with the specified strong name will be allowed to use your code (see the provided sample code). In this manner, you can eliminate the elevation of privilege attack without having to set of a complete stack walk.
Some General Advice.
If you want to extend the use of your code to a wide variety of unknown callers and you can’t guarantee that they will always extend the same LinkDemand, then you should use a Demand. In fact, the Demand action should be the one that you use unless you have a clear, specific reason to do otherwise.. In most applications, the stack walk associated with a Demand will not noticeably impact the performance of your application.
There are some situations, however, where a LinkDemand is needed. If you are designing a library that needs to access a protected resource several thousand times in a row, protecting the resource with the Demand action might produce a noticeable performance hit. For example, if log.dll sets off a full stack walk 200 times a second, for the lifetime of your application, it might cause things to lag. Using a LinkDemand would only produce a single check at link time, and might allow an application that uses log.dll to be more preferment. If you decide to use a LinkDemand, you need to take the following precautions to prevent an elevation of privilege attack: 1) make sure every caller on the stack duplicates the same LinkDemand on its own level, or 2) limit callers to those with only the safe identities that you specify as we illustrated in the sample.
Finally, never use the Assert action to bypass the local policy settings. Never assert permissions required by another developer’s assembly unless you are 100% sure that it will not expose a security vulnerability. The Assert action was designed to expose only safe resources to partially trusted code.
As a developer of managed code, you should get used to the idea of protecting against the elevation of privilege attack. Doing so will help create the safest and most reliable computing environment for you and your customers
Michael Stiefel is the principal of Reliable Software, Inc. where he does training and consulting in Microsoft technologies for companies ranging from startups to the Fortune 500. He is the co-author of “Application Development Using C# and .NET” published by Prentice-Hall. Go to www.reliablesoftware.com for more information about his development work and the training courses he offers.
Stephen McCloskey is a Programmer/Writer for Microsoft Corporation. He has worked on Version 1.0 and 1.1 of the .NET Framework, where he wrote security-related documentation and samples for the .NET Framework SDK and MSDN. He continues to write security-related material for future Microsoft products. |
||||||||||||
All Content (c) 2000 - 2014 Reliable Software, Inc. All rights reserved. |