Execution-Time Security Issues

Having compiled and deployed your assemblies to a target machine, the next step, of course, is to run your code within the Common Language Runtime. A lot of steps occur “under the covers” when you run your HelloWorld.exe managed executable. In this section, we're going to walk through the process by which managed code contained within an assembly is loaded, evaluated by security policy, Just-In-Time compiled, type-safety verified, and finally allowed to execute. The overall process of developing, deploying, and executing managed code is depicted graphically in Figure 12.1. We have discussed the Development and Deployment boxes previously and will focus solely on Execution in this section.

Figure 12.1. High-level diagram of the process of developing, deploying, and executing managed code.


The diagram in Figure 12.1 shows how an individual assembly is loaded and executed within the Runtime, but there is a key initial step that must occur before loading any assemblies. Every managed application is run on top of the Runtime within the context of a host. The host is the trusted piece of code that is responsible for launching the Runtime, specifying the conditions under which the Runtime (and thus managed code within the Runtime) will execute, and controlling the transition to managed code execution. The .NET Framework includes a shell host launching executables from the command line, a host that plugs into Internet Explorer that allows managed objects to run within a semitrusted browser context, and the ASP.NET host for Web applications. After the host has initialized the Runtime, the assembly containing the entry point for the application must be loaded and then control can be transferred to that entry point to begin executing the application. (In the case of shell-launched C# executables, this entry point is the Main method defined in your application.)

Loading an Assembly

Referring to Figure 12.1, the first step that occurs when loading an assembly into the Runtime is to locate the desired assembly. Typically, assemblies are located on disk or downloaded over the network, but it is also possible for an assembly to be “loaded” dynamically from a byte array. In any case, after the bytes constituting the assembly are located, they are handed to the Runtime's Assembly Loader. The Assembly Loader parses the contents of the assembly and creates the data structures that represent the contents of the assembly to the Runtime. Control then passes to the Policy Manager.

NOTE

When the Assembly Loader is asked to resolve a reference to an assembly, the reference may be either a simple reference, consisting of just the “friendly name” of an assembly, or a “strong” reference that uses the cryptographic strong name of the referenced assembly. Strong references only successfully resolve if the target assembly has a cryptographically valid strong name (that is, it was signed with the private key corresponding to the public key in the strong name and has not been tampered with since being signed). For performance reasons, assemblies loaded from the Global Assembly Cache are strong name verified only when they are inserted into the GAC; the Runtime depends on the underlying operating system to keep the contents of the GAC secure.

The Assembly Loader is also responsible for resolving file references within a single assembly. An assembly always consists of at least a single file, but that file can contain references to subordinate files that together constitute a single assembly. Such file references are contained within the “assembly manifest” that is stored in the first file in the assembly (the file that is externally referenced by other assemblies). Every file reference contained within the manifest includes the cryptographic hash of the contents of the referenced file. When a file reference needs to be resolved, the Assembly Loader finds the secondary file, computes its hash value, compares that hash value to the value stored in the manifest, and (assuming the match) loads the subordinate file. If the hash values do not match, the subordinate file has been tampered with after the assembly was linked together and the load of the subordinate file fails. (More information on cryptographic hashes and their uses may be found in Chapter 30, “Using Cryptography with the .NET Framework: The Basics.”)


Resolving Policy for an Assembly

The Policy Manager is a core component of the Runtime security system. Its job is to decide what permissions should be granted, in accordance with the policy specification, to every single assembly loaded by the Runtime. Before any managed code from an assembly is executed, the Policy Manager has determined whether the code should be allowed to run at all and, if it is allowed to run, the set of rights with which it will run. Figure 12.2 provides a high-level view of the operation of the Policy Manager.

Figure 12.2. High-level diagram of the Policy Manager.


There are three distinct inputs to the Policy Manager:

  • The current security policy

  • The evidence that is known about the assembly

  • The set of permission requests, if any, that were made in assembly-level metadata declarations by the assembly author

The security policy is the driving document; it is the specification that describes in detail what rights are granted to an assembly. As described in Chapter 8, the security policy in effect for a particular application domain at any point in time consists of four policy levels:

  • Enterprise-wide level

  • Machine-wide level

  • User-specific level

  • An optional, application domain–specific level

Each policy level consists of a tree of code groups and membership conditions, and it is against this tree that the evidence is evaluated. Each policy level is evaluated independently to arrive at a set of permissions that the level would grant to the assembly. The intersection of these level grants creates the maximal set of permissions that can be granted by the Policy Manager.

The second input to the Policy Manager is the evidence, the set of facts that are known about the assembly. As noted in Chapter 5, “Evidence: Knowing Where Code Comes From,” there are two types of evidence that are provided to the policy system—implicitly trusted evidence and initially untrusted evidence. Implicitly trusted evidence consists of facts that the Policy Manager assumes to be true either because the Policy Manager computed those facts itself or because the facts were supplied by the trusted host that initialized the Runtime (“host-provided evidence”). (An example of the former type of implicitly trusted evidence is the presence of a cryptographically valid strong name or Authenticode signature. The URI from which an assembly was loaded is an example of the latter type of implicitly trusted evidence.) Initially untrusted evidence consists of facts that were embedded in the assembly (“assembly provided evidence”) at development time by the code author; they must be independently verified before being used or believed.

NOTE

The default security policy that is installed by the .NET Framework never considers initially untrusted evidence, but the facility is present as an extension point. For example, third-party certifications of an assembly might be carried as assembly provided evidence.


The third and final input to the Policy Manager is the set of permission requests made by the assembly. These declarations provide hints to the Policy Manager concerning

  • The minimum set of permissions that the assembly must be granted to function properly

  • The set of permissions that the assembly would like to be granted but are not strictly necessary for minimal operation

  • The set of permissions that the assembly never wants to be granted by the policy system

After computing the maximal set of permissions that can be granted to the assembly in accordance with the policy levels, the Policy Manager can reduce that set based on the contents of the permission requests. More details on permission request syntax and semantics can be found in Chapter 29.

NOTE

Permission requests never increase the set of permissions granted to an assembly beyond the maximal set determined by the policy levels.


After the Policy Manager has determined the in-effect security policy, the set of applicable evidence, and the set of permission requests, it can compute the actual set of permissions to be associated with the assembly. Recall that permissions are granted by the policy system on an assembly-wide basis; all code within an assembly is granted the same set of rights when that assembly is loaded into an application domain. After the set of granted permissions has been computed by the Policy Manager, that set is associated with the Runtime-internal objects that represent the assembly, and individual classes contained within the assembly can be accessed.

Before leaving the Policy Manager, we should mention that there are two security conditions enforced by the Policy Manager that could cause the load of the assembly to fail at this point and never proceed to the Class Loader. The first condition concerns the assembly's set of minimum permission requests. If an assembly contains any minimum permission requests, these requests must be satisfied by the grant set that is output by the policy system for processing of the assembly to continue. If the minimum request set is not a subset of the resulting grant set, a PolicyException is immediately thrown. The second condition that is checked before proceeding is that the assembly has been granted the right to execute code. “The right to run code” on top of the Runtime is represented by an instance of the SecurityPermission class (specifically, SecurityPermission(SecurityPermissionFlag.Execution)). Under default policy, the Policy Manager will check that the set of permissions granted to an assembly contains at least an instance of this flavor of SecurityPermission. If the right to run code is not granted for some reason, that also generates a PolicyException and no further processing of the assembly occurs.

Loading Classes from an Assembly

Assuming that the assembly's minimum requests have been satisfied and that the assembly is indeed granted the right to run code, control passes from the Policy Manager to the Class Loader. Classes are retrieved lazily from the containing assembly; if you access a single class from an assembly containing a hundred classes, only that single class is touched by the Class Loader. The Class Loader is responsible for laying out in memory the method tables and data structures associated with the class and verifying access (visibility) rules for classes and interfaces. After the class data structures have been properly initialized, we are ready to access individual methods defined within the class.

Just-In-Time Verification and Compilation of Methods

Referring back to Figure 12.1, after the Class Loader has finished its work, we are ready to verify the MSIL container within the assembly and generate native code from it. This is the job of the Just-In-Time compiler and type-safety verifier (also known as the JIT compiler/verifier). As with loading classes from an assembly, methods within a class are JIT verified and compiled lazily on an as-demanded basis. When a method is called for the first time within the process, the MSIL for the method is checked for compliance with the published type-safety rules and then (assuming it passes) converted into native code. The type-safety verification process is described in more detail in Chapter 11, “Verification and Validation: The Backbone of .NET Framework Security.”

The JIT compiler/verifier also plays a direct role in the evaluation of class- and method-level declarative security actions. Recall that there are three types of declarative permission demands, represented by the SecurityAction enumeration values Demand, LinkDemand, and InheritanceDemand. (Examples of how to use these types of security declarations are provided in Chapter 25.) Inheritance demands, which control the right to subclass a class, and link-time demands, which restrict the right to bind to a method, are checked and enforced by the JIT as the class is being compiled. Failure to satisfy an inheritance or link-time demand will result in the generation of a SecurityException. Runtime demands (SecurityAction.Demand) are converted by the JIT compiler/verifier into a native code wrapper around the body of the method that is protected by the demand. Every call to the method must first pass through the wrapper, satisfying the security demand it represents, before entering the body of the method.

After a method has been successfully processed by the JIT compiler/verifier and converted to native code, it is ready to be executed. As the method executes, references to unprocessed methods, classes, and assemblies can occur. These references will cause the Runtime to recursively call the JIT compiler/verifier, Class Loader, or Assembly Loader and Policy Manager as necessary. These operations happen implicitly, “under the covers,” as the managed code within the application executes.

Execution-Time Permission Enforcement

So far, everything that we have described in this section concerning the operation of the execution engine is largely transparent to the code author and the user.

Assembly references are resolved, assemblies are loaded, policy evaluation occurs, classes are laid out, and MSIL is verified and converted into native code, all without any external indication or control. (Of course, in the event that an error occurs at any stage in this processing pipeline, such as the failure of an inheritance demand in the JIT compiler/verifier, an exception will be generated and program execution will not proceed normally.) However, essentially all of the security processing that occurs from assembly load through JIT compilation/verification really exists primarily to set up the execution environment for execution-time permission enforcement.

Execution-time permission enforcement, the final security-related component in Figure 12.1, is the raison d'être of the .NET Framework security system. All of the evidence gathering, policy specification, and evaluation that has occurred up to this point was performed simply so that we can associate a permission grant set with each assembly at execution time. We need that association between assemblies and their grant sets so that we can properly perform and evaluate stack-walking security demands. As a program runs within the Runtime, security demands will generally occur as a result of one of the following three actions:

  • An explicit call to a permission's Demand() method

  • An explicit call to a method protected by a declarative security attribute specifying a demand

  • Implicitly as part of a platform invoke or COM interop call, because all calls to native code through these mechanisms are automatically protected by security checks

Recall from Chapter 7, “Walking the Stack,” that the goal of a stack-walking permission check is to verify that the demanded permission is within the policy grants of all the code in the call chain. For every piece of code on the call stack above the method performing the check, the Runtime security system ensures that the code has been granted the particular demanded permission by the Policy Manager. Whenever a method within a secured assembly is about to perform a potentially dangerous action, the method must first perform a security demand to check that the action is permitted by policy.

NOTE

As a practical matter, note that because the Policy Manager assigns grant sets to assemblies, a sequence of successive frames on the stack corresponding to code loaded from the same assembly will have the same set of granted permissions. Consequently, the security system needs only check permission grants at every assembly transition on the stack, not every method transition. Keep this behavior in mind when deciding how you want to organize your code into various assemblies; a poor design that includes a very chatty interface across two assemblies can slow down the performance of the security system.


..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.140.108