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:
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:
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:
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:
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 Derived : Base
{
}
// Other code
Derived d = new Derived();
Base b = d; // Don't need to cast
A downcast must be performed explicitly:
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:
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:
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:
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:
{
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.