Introduction to .Net security

23

Author: Jeffrey L. Vagle

.Net is a software framework from Microsoft that enables language-non-specific
software development, resulting in applications that can easily interoperate across platforms and networks. A question seasoned developers should ask at this point is, “What about security?” The good news is that .Net provides a broad range of security tools and functionality to the developer. However, improper application of these security measures can be far more dangerous to than not applying them at all.Central to the .Net Framework lies the Common Language Runtime (CLR), which is responsible for the management and execution of code within .Net. For those more familiar with Java, the CLR can be considered the rough equivalent of the Java Virtual Machine. Key responsibilities of the CLR include the conversion of language-abstraction code known as Intermediate Language (IL) to platform-native executable code, loading of required classes, just-in-time (JIT) compilation of methods, memory management (including garbage collection), and security enforcement.

CLR security enforcement measures includes code verification, which prevents programs from doing anything deemed dangerous, such as misallocation of memory with C++ programs (yet another language that can be used to create .Net programs), or the infamous buffer overflow. In addition, the CLR provides a sandbox (not unlike the Java VM concept of the same name) wherein developers and administrators may control the default behaviors of their programs, as well as the resources they are allowed to access. The .Net Framework uses a stack walk to enforce these security constraints. This is where the CLR runtime maintains a call stack for each execution thread, and when security functionality is called by the running code, the CLR walks back up the stack to ensure the permissions required are met.

Microsoft Intermediate Language (MSIL)

In order to abstract away language internals, and to help facilitate support for multiple languages, the .Net Framework uses an intermediate language — called the Microsoft Intermediate Language (MSIL) — as the basis for code to be managed and executed by the CLR. Similar in function to Java bytecode, MSIL is object-oriented and supports such features as data abstraction, inheritance, and polymorphism. MSIL can be considered a platform-neutral assembly language, with no hardware specific calls. Before execution, MSIL code is compiled to native code by the CLR.

One can view the MSIL code within a .Net executable by using the MSIL disassembler (ildasm) that comes with the .Net SDK. We’ll start with the following simple C# program, which we’ll call Test1.cs. (We will implement our examples using C# and the Microsoft .Net SDK tools, although the principles apply across any .Net language or implementation (such as the Mono Project).):

using
System;

namespace Test1
{
class Program
{
static void
Main(string[] args)

{

Console.WriteLine("Hello, world.");

}
}
}

In order to first compile this code into a .Net executable, we’ll use the C# compiler that comes with the Microsoft .Net SDK (csc):

csc Test1.cs

This will create the .Net executable file Test1.exe, which can be run just like any other Windows executable, only this code will be managed by the CLR at runtime.

To view the MSIL and metadata associated with our newly created executable, we’ll use the MSIL disassembler (ildasm) that comes with the .Net SDK:

ildasm Test1.exe /text

This command will dump the metadata and MSIL associated with our program:

// Microsoft (R) .Net Framework IL
Disassembler. Version 2.0.50215.44
// Copyright (C) Microsoft Corporation. All rights reserved.

// Metadata version: v2.0.50215
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89
)
// .zV.4..
.ver 2:0:0:0
}
.assembly Test1
{
.custom instance void
[mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32)
= ( 01 00 08 00 00 00 00 00 )
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module Test1.exe
// MVID: {21C537F6-8F24-4BCB-B8F4-96E9ADBE1E26}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// Image base: 0x02EA0000

// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit Test1.Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args)
cil managed
{
.entrypoint
// Code size 13
(0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr
"Hello, world."
IL_0006:
call void
[mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Program::Main

.method public hidebysig specialname rtspecialname
instance
void .ctor() cil managed
{
// Code size 7
(0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001:
call instance void
[mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method Program::.ctor

} // end of class Test1.Program
// Microsoft (R) .Net Framework IL Disassembler. Version
2.0.50215.44
// Copyright (C) Microsoft Corporation. All rights reserved.

// Metadata version: v2.0.50215
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89
)
// .zV.4
..
.ver 2:0:0:0
}
.assembly Test1
{
.custom instance void
[mscorlib]System.Runtime.CompilerServices.CompilationRel
axationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 )
.hash algorithm 0x00008004
.ver 0:0:0:0
}
.module Test1.exe
// MVID: {21C537F6-8F24-4BCB-B8F4-96E9ADBE1E26}
.imagebase 0x00400000
.file alignment 0x00000200
.stackreserve 0x00100000
.subsystem 0x0003 // WINDOWS_CUI
.corflags 0x00000001 // ILONLY
// Image base: 0x02EA0000

// =============== CLASS MEMBERS DECLARATION ===================

.class private auto ansi beforefieldinit Test1.Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args)
cil managed
{
.entrypoint
// Code size 13
(0xd)
.maxstack 8
IL_0000: nop
IL_0001: ldstr
"Hello, world."
IL_0006:
call void
[mscorlib]System.Console::WriteLine(string)
IL_000b: nop
IL_000c: ret
} // end of method Program::Main

.method public hidebysig specialname rtspecialname
instance
void .ctor() cil managed
{
// Code size 7
(0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001:
call instance void
[mscorlib]System.Object::.ctor()
IL_0006: ret
} // end of method Program::.ctor

} // end of class Test1.Program

This output is the sum total of a .Net assembly, which we’ll discuss in the next section.

Assemblies

.Net programs are built using assemblies, the atomic components within .Net which contains code and its associated metadata. .Net assemblies contain the following information:

  • Manifest: metadata that
    describes the assembly, including:
    • Assembly Name
    • Assembly Version
    • Strong Name Public Key
  • Type Metadata
  • MSIL Code
  • Resources (if any)

We can see from the disassembled example above that our simple program, once compiled, resulted in a valid .Net assembly.

Assemblies can be made up of one (single-file assembly) or more (multi-file assembly) files containing the four elements described above. In theory, .Net requires only the manifest for a legal assembly, but without code or resources, such an assembly would be fairly useless.

What makes assemblies interesting from a security point of view is their function as a security sandbox within the .Net Framework. Assemblies can either private or shared. Private assemblies are used by a specific application, whereas shared (or public) assemblies can be used by any application. The CLR does not strictly enforce versioning on private assemblies — that task is left up to the application’s developer through the use of explicit path references or configuration files. However, in the case of shared assemblies, the CLR ensures that the correct assemblies — that is, the assemblies that the application used when compiled — are loaded at runtime. This is an important security feature, as .Net requires all assemblies to be uniquely identifiable through a combination of the assembly’s public name, version, culture, public key, and digital signature. This code identity — or strong name — allows the .Net Framework not only to properly identify the correct assembly for use by applications (thus providing a workable solution to DLL hell), but also to strictly control who can access the code and prevent code tampering through the use of a cryptographic hash code.

.Net Framework security

Because .Net Framework supports highly distributed applications, its developers paid much attention to the design and application of security measures, to avoid some of the security pitfalls that have overcome networked, mobile applications in the past.

Two major security features of .Net are role-based security and code access security.

The basic concepts behind .Net’s role-based security system should be familiar to most developers who target multi-user platforms. The .Net Framework provides functionality which enables developers to represent a user’s identity, which is then tightly coupled to the code running on behalf of that user. This gives developers and administrators the ability to restrict or allow access to code based on the identity or role of that user.

.Net provides the concept of a principal, a combination of a user’s identity along with one or more roles associated with that user, to be used within authentication and authorization mechanisms. These mechanisms can be integrated either with existing user accounting systems, such as those provided by the operating system, or with custom user accounting mechanisms.

Figure 1: The .Net Principal

In order to maximize the flexibility of applications within the .Net Framework, the developer is given a powerful system that allows for fine-grained control over user access to managed code within .Net. This feature is known as code access security, and it provides access control based not on the user’s identity, but on the code, which is itself uniquely identified within the .Net Framework.

Code access security functionality is provided through three major components: evidence, permissions, and security policy.

Evidence, within the .Net security model, is basically the pedigree of the managed code within the framework: where the code comes from, how it was loaded, and the metadata associated with the code. The responsibility for evidence gathering is handled when the CLR loads an assembly. There are seven types of evidence that are gathered when an assembly is loaded:

  • Site
  • URL
  • Zone
  • Application directory
  • Strong name
  • Publisher
  • Hash

The site, URL, zone, and application directory evidence types describe where the code came from; strong name and publisher describe who the code came from; and hash describes the code itself, and is used for assuring assembly integrity and identity.

Permissions are used within the .Net Framework to give administrators and developers fine-grained control over the behavior of .Net managed code with maximum flexibility. The CLR uses permissions during assembly loading at runtime, where it assigns to each assembly a permission set that encodes the authority granted to that assembly by the CLR, such as access to a particular URL. Additionally, managed code within .Net can demand that any other code that calls it must have certain permissions associated with it. Any permissions required during runtime are evaluated by the CLR and enforcement is provided as needed.

Finally, as the name implies, security policy is a set of configurable rules that the CLR uses to make security decisions at runtime. Assemblies may be granted or denied permissions to perform certain tasks using security policies. The statements made in
security policy provide the link between evidence and permissions.

.Net breaks security policy up into four levels: enterprise, machine, user, and application domain. These levels are all configurable, with the enterprise, machine, and user levels modified through the .Net administrative tools. Application domains do not exist until runtime, so security policy at this level must be configured in the code itself.