.NET Security Workshop
Home About Workshops Articles Writing Talks Books Contact

1. Common Vulnerabilities

The .NET runtime and framework library have been designed to eliminate vulnerabilities that have been exposed in unmanaged code. In this part of the workshop I will go through some of the common exploits and show how .NET has removed the vulnerability.

1.1 Buffer Overruns

Perhaps the most common vulnerability is caused by buffer overruns and typically by string buffer overruns. In C and C++ buffers are allocated on the stack or the free store. If more data than the buffer's capacity is written to the buffer then the extra data will write over member after the buffer. If the buffer is allocated in the free store, then another object or buffer may be overwritten and hence corrupted. Such an object may contain a function pointer and this could be over written by the buffer overrun to point to another location and so when this function pointer is used then the wrong code will be called. If the buffer is allocated on the stack then the extra data will overwrite other variables on the stack, which includes automatic variables (variables declared at method scope). However, the stack also contains the stack frame which contains the return address. This means that when the function has completed the processor will return to this address. If an attacker can alter this address she can persuade the processor to run some code that she has provided.

.NET addresses this issue by managing the stack and the heap. Buffers are always allocated on the managed heap and access to all buffers are bounds checked. If any operation that copies data to a buffer exceeds the array's upper or lower bounds then an exception is thrown and the copy operation is not performed. All arrays are bounds checked including the buffers used by strings. For example, compile the following code:

using System;

class App
{
   static void Main(string[] args)
   {
      string[] s = new string[4];
      for (int idx = 0; idx < args.Length; idx++)
      {
         s[idx] = args[idx];
      }
   }
}

Run the process at the command line a few times, with various number of parameters. When you try five parameters you'll see that the following exception is thrown:

Unhandled Exception: System.IndexOutOfRangeException: Index was
outside the bounds of the array.
   at App.Main(String[] args)

1.2 Strings

.NET strings are immutable so you never need to allocate them directly. All of the .NET methods that manipulate strings will return a new string allocated by the String class. The String class ensures that the buffers it allocates are of the correct size. For more complex string buffer manipulations you use a StringBuilder object and you can indicate the initial capacity of the buffer it will use. However, this is just a suggestion and is used as an optimisation because the class always performs bounds checks and will reallocate the buffer if needed.

Another vulnerability is format string bugs. The C printf family of functions have a string that contains instructions about how the data is formatted to create the final string. However, this format string is used in a type unsafe manner. For example, the function does not make sure that the number of format items in the format string matches the number of parameters. It is bad programming practice to pass a user supplied string as the formatting parameter to printf functions, but it is frequently done. The problem with doing is this that an attacker can pass a string which contains format parameters for data that printf does not supply and hence this leads to the functioning accessing other data on the stack. The reason is that when one of the printf family of functions is called the parameters passed to it will be passed via the stack, so the function will extract the number of items according to the format string, the function assumes that the number of place holders in the format string matches the number of parameters passed to the function. If this is not the case the function will simply access other values on the stack and this means that printf can be used to display items on the stack that the developer did not intend to display. 

More worrying, the %n format parameter assumes that the corresponding parameter on the stack is a pointer to an integer and it writes the number of characters in the final formatted string to this memory location. By careful manipulation an attacker can use this to copy values into a memory location, and if that location is a function pointer (for example the function return address on the stack) the attacker could force the processor to run code that she has specified.

This highlights a problem that even .NET cannot protect you against: the user may pass a string with potentially dangerous data. Thus you must always validate the values inputted by the user.

.NET string formatting is carried out through the StringBuilder.AppendFormat method. The String.Format method, for example, calls AppendFormat. This method does not have a format item that will write cause data to be written to an arbitrary memory location, so the %n format parameter vulnerability is prevented. Furthermore, the parameters to AppendFormat are passed via an array and so the length of the array is known. The method counts the format items in the format string and ensures that there are at least as many parameters as there are format items.

Note that although .NET supports a variable number of arguments the only language to use it is Managed C++ and C++ can only call a Varargs method, it cannot write them. The other languages (for example C#) prefer to use the overloaded versions that take a [ParamArray] array argument. (But note that there is an undocumented keyword in C# called __arglist that allows you to write methods with a variable number of arguments.)

1.3 Overflows

Another vulnerability is arithmetic overflow. For example, in an unchecked environment, using 32-bit integers, the result of 0xffffffff + 1 is zero (plus an overflow). This is counter intuitive because adding something to a large number produces a small number. This is a particular problem if you perform arithmetic on mixed signed and unsigned types. 

.NET solves this issue in several ways. First, it is an error to mix signed and unsigned types, but you can override this by casting:

// unsigned 32-bit integer
int x = -1;
// signed 32-bit integer
uint y = x + 1;

This causes error CS0029, "Cannot implicitly convert type 'int' to 'uint'".

.NET Version 3.0
Version 8.00.50727.42 of the C# compiler gives a similar error text, but indicates that the error code is CS0266:
Cannot implicitly convert type 'int' to 'uint'. An explicit conversion exists (are you missing a cast?)

The developer can cast a type to remove this error. For example, in the following code an arithmetic operation is performed between signed and unsigned types that we know is safe for the values that are specified:

int x = 0;
uint y = (uint)x + 1;

This will compile. However, in most cases we do not know what the values will be and so we do not want an overflow to occur. C# guards against this with the checked keyword:

void func(int x)
{
   uint y;
   try
   {
      checked
      {
         y = (uint)x + 1;
      }
   }
   catch(OverflowException oe)
   { /* ... */ }
}

If an overflow condition occurs, the OverflowException will be thrown.

Note that C# has a corresponding keyword unchecked that turns off overflow checks. This is particularly useful if you are dealing with numbers that are bitmaps and you want to use the shift operators to alter the bitmap.

1.4 Casting

.NET has strict rules on casting. At runtime the CLR knows the type of an object and the type that you are trying to obtain. Casts can be performed implicitly between a reference to a derived type to a reference of its base type. For example:

class Base
{
}
class Derived : Base
{
}

// Other code
Derived d = new Derived();
Base b = d; // Don't need to cast

A downcast must be performed explicitly:

Base b = new Derived();    // Don't need to cast
Derived d1 = (Derived)b;   // Explicit cast, if this fails InvalidCastException is thrown
Derived d2 = b as Derived; // Explicit cast, if this fails d2 is null, with no exception

However, .NET will perform a runtime type check and if the reference being cast is to an object that is not of a type related to the resultant type then an InvalidCastException exception is thrown. C# also has the as operator which does not throw an exception if the cast fails, instead it returns a null reference and if this is used a NullReferenceException will be thrown.

1.5 Delegates

Another common vulnerability occurs through function pointers. Assigning a value to a C function pointer is an unsafe operation because there is no type checking between the type of the function pointer and the value that it is assigned to. This means that a function pointer can be assigned to any value and it can be invoked regardless of whether that value is valid. Normally, when a C function is called the compiler sets up the stack according to the type of the function pointer and the function takes its parameters from the stack. Typically you invoke a C function pointer through a pointer created from the function prototype and this prototype determines the format of the stack at invocation. If the actual memory address called is a function that has a different prototype then it will interpret the stack differently, possibly removing additional items from the stack. For example:

// Unmanaged C code
typedef void (*FUNC)(void);
void f(unsigned int i, unsigned int j, unsigned int k)
{
   // three parameters are taken from the stack
}
void g()
{
   FUNC fn = (FUNC)f;
   // stack is built assuming no parameters
   fn();
}

.NET addresses this problem through delegates. A delegate is a typed function pointer and it can only be initialised with a method of the same type. For example:

using System;
class App
{
   delegate void MyDelNoParams();
   static void Func2Params(int i, int j)
   {
   }
   static void Main()
   {
      MyDelNoParams d = new MyDelNoParams(Func2Params);
      d();
   }
}

.NET Version 3.0
C# version 2 allows you to do the following:

MyDelNoParams d = Func2Params;

This code will not compile and you'll get a compiler error of CS0123. The compiler checks that the function used to initialize the delegate has the same number of parameters and the same types. The compiler gets this information from the metadata of the delegate.

The delegate has a base class of Delegate, which means that you can pass a delegate to any method that has a Delegate parameter. However, if you cast the Delegate parameter so that you can invoke the delegate a runtime type check will occur. For example:

using System;

class App
{
   delegate void MyDelNoParams();
   delegate void MyDel2Params(int i, int j);
   static void Func2Params(int i, int j)
   {
   }
   static void CallDelegate(Delegate d)
   {
      MyDelNoParams d1 = (MyDelNoParams)d;
      d1();
   }
   static void Main()
   {
      MyDel2Params d = new MyDel2Params(Func2Params);
      CallDelegate(d);
   }
}

This code will compile, but when you run it you'll get an InvalidCastException exception because CallDelegate is passed a delegate of type MyDel2Params but it tries to cast the delegate to a different type (MyDelNoParams). An alternative is to call DynamicInvoke on the delegate:

static void CallDelegate(Delegate d)
{
   d.DynamicInvoke(null);
}

Again, this code will compile, but an exception is thrown at runtime. The parameter of DynamicInvoke is an array of the parameters to pass to the method that is invoked. Again, d is a delegate of type MyDel2Params and passing null indicates that no parameters are passed to the method. However, DynamicInvoke checks the number of parameters that the delegate takes against the number of parameters passed, so in this case this method will throw a TargetParameterCountException exception.

It is also worth pointing out that methods on .NET objects have a special calling convention called __clrcall. Managed C++ allows you to write mixed mode code, that is, code that has both managed and unmanaged types, and the compiler can mix x86 and IL in the same code module. In Managed C++ you can only use __clrcall methods to initialize a delegate.

1.6 Library Code and Access Tokens

One of the worst security issues is caused by running DLL code. When your process loads a DLL the library is loaded into the process memory space. This means that the DLL has full access to the memory that the process owns. Furthermore, by default it is run under the access token of the process so this means that when the DLL code accesses a secured object (for example, a file or a registry key) the access check will performed against the process's access token and any auditing will be logged against the principal that owns the access token. The DLL could be a pluggable library like an ActiveX control, so the user will innocently access the library and it will apparently perform some innocuous task while actually performing some evil deed that is audited to your account.

Most processes will need specific privileges to carry out their purpose, but it is unlikely that all DLLs used by the process will require all of these privileges. A thread can create a thread token and reduce the privileges of that token or it can impersonate another user with a lower privileged account, before calling library code. However, an impersonating thread can also revert back to its previous access token with a call to RevertToSelf, which is a call that the library code could also make.

.NET solves this through code access security. This assigns permissions according to the assembly that is executing and the assemblies that called it. As a developer you cannot use code access security to grant more permissions, you can indicate the permissions that the code in the assembly requires, you can also assert that code calling your code has specific permissions. It is the .NET runtime, through a security policy specified by the administrator, that determines the permissions that an assembly will get.

I hope that you enjoy this tutorial and value the knowledge that you will gain from it. I am always pleased to hear from people who use this tutorial (contact me). If you find this tutorial useful then please also email your comments to mvpga@microsoft.com.

Errata

If you see an error on this page, please contact me and I will fix the problem.

Page Two

This page is (c) 2007 Richard Grimes, all rights reserved