Securing the loading of dynamic code
on 18 October, 2016
Background
Reflection in computer science is when a programming language has the ability to inspect and modify itself at runtime. Arguably, it has been around in a crude form since the beginning of programming itself where programmers could find ways to create and execute new code at runtime. Built-in support for reflection now exists in a host of modern languages including Java, C#, Python, R, Go, PHP, Ruby and Objective-C to name a few.
Reflection can be used to inspect code at runtime to ascertain which methods are available and what arguments they take. This is the feature that is used extensively in testing and mocking of test objects.
Another use case for reflection is to dynamically load new functionality at runtime. This can be done by loading additional assemblies into memory and then calling their methods – all without having to restart or upgrade the executable.
While reflection can offer incredible functionality and fluidity with regards to code, it can also introduce serious security flaws if not implemented correctly.
The basics
While most modern programming languages support reflection, in this article all examples will be in C#. However, the procedures and logic can be carried across to most other languages with minor alterations.
The following example shows the basics of loading an assembly reflectively in C#:
//Load the assembly into memory so that it can be manipulated
Assembly assembly = Assembly.LoadFrom("MyNewFunction.dll");
//Get the type of the assembly
Type type = assembly.GetType("Namespace.Class");
//Gets the entry method of the assembly
MethodInfo methodInfo = type.GetMethod("EntryMethod");
//Create an instance of the reflected class
object classInstance = Activator.CreateInstance(type);
//Create an array arguments to pass to the entry method
object[] parameters = {"ExampleArg"};
//Launch the method
methodInfo.Invoke(classInstance, new object[] {parameters});
Using the above code snippet, it is possible to create a code base that can be changed on the fly without affecting existing code around it. The following example highlights these features by creating a simple timer application.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Reflection
{
class Program
{
static void Main(string[] args)
{
List<SimpleTimer> timers = new List<SimpleTimer>();
string contentDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "content");
while (true)
{
foreach (string file in Directory.GetFiles(contentDirectory))
{
if (timers.FirstOrDefault(x => x.location == file) == null)
{
SimpleTimer simpleTimer = new SimpleTimer(file);
simpleTimer.Start();
timers.Add(simpleTimer);
}
}
List<SimpleTimer> timersToRemove = new List<SimpleTimer>();
foreach (SimpleTimer timer in timers)
{
if (!File.Exists(timer.location))
{
timer.Stop();
timersToRemove.Add(timer);
}
}
foreach (SimpleTimer removeTimer in timersToRemove)
{
timers.Remove(removeTimer);
}
Thread.Sleep(10000);
}
}
}
public class SimpleTimer
{
public string location;
private Thread thread;
private object classInstance;
private Type type;
public SimpleTimer(string location)
{
this.location = location;
Assembly assembly = Assembly.LoadFrom(location);
this.type = assembly.GetType("CommonNamespace.ClassName");
MethodInfo startMethod = type.GetMethod("Start");
this.classInstance = Activator.CreateInstance(type);
thread = new Thread(() => startMethod.Invoke(classInstance, null));
}
public void Start()
{
thread.Start();
}
public void Stop()
{
MethodInfo stopMethod = type.GetMethod("Stop");
stopMethod.Invoke(classInstance, null);
thread.Join();
}
}
}
The above code snippet monitors a directory for timer assemblies. Each assembly that is found is added to a list of timers and started using reflection. If the assembly is subsequently removed from the directory, the timer is terminated and removed from the running timers list. Adding a new timer can now be done by compiling the following code as a DLL and then adding it to the content directory:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CommonNamespace
{
public class ClassName
{
public bool running = true;
public void Start()
{
while(running)
{
Console.WriteLine("Running");
Thread.Sleep(1000);
}
}
public void Stop()
{
running = false;
}
}
}
This is a very simple example of the functionality of reflection and its implementation but serves to highlight the key components of flexibility and dynamism.
While code such as this does offer a large degree of flexibility, it is also liable to load assemblies that it was not intended to run. Due to the fact that there are no controls in place to limit which assemblies are run, any assembly that is placed in the content directory will be executed. This problem can be incredibly damaging if the parent process is running as a user with elevated privileges; as all executed assemblies have the same privileges as their parent. By utilising this vulnerability an attacker could use this functionality to escalate privileges. This problem is compounded if the assemblies are downloaded via insecure means.
Securing reflection
In order to mitigate these vulnerabilities, it is critical to ensure that all assemblies are obtained from trusted sources and have not been tampered with before they are loaded. Once again all example code is in C# but the principles are universal.
Hashing
The first method of securing a reflectively loaded assembly is to check the assembly against a hash before loading the assembly. A cryptographic hash allows for a snapshot of what the assembly should look like and allows you to compare it to the actual assembly when it is loaded. An important part of using this approach is that the source of the hash can be verified and is trusted. This can be achieved in a variety of ways, the most common of which is to download the hash over an encrypted connection from an authenticated source.
Once the hash has been retrieved from the trusted source, it can then be used to verify the assembly. This can be done in two ways.
Method 1 – Precheck
If the intention of checking the assembly’s hash is to ensure that the assembly was not tampered with in transit, then using the precheck method is acceptable. This method works by obtaining a hash from a trustworthy source and comparing it to the hash of the assembly prior to loading the assembly.
byte[] validHash = DownloadAuthenticatedHash();
byte[] assemblyHash = SHA256.Create("MyNewFunction.dll").Hash;
if(validHash.SequenceEqual(assemblyHash)){
Assembly assembly = Assembly.LoadFrom("MyNewFunction.dll");
}
If there is the potential for the assembly to be modified on disk before it is loaded then this method does not protect against a time of check, time of use (TOCTOU) vulnerability. If an attacker monitors for file access to MyNewFunction.dll and swaps the file out directly after the hash check, then this method does not provide sufficient protection.
Method 2 – On load
In order to address the TOCTOU vulnerability from the precheck hash method, the loading and hashing of the assembly needs to be an atomic function. C# provides functionality to do this by providing an overload to the Assembly.LoadFrom method.
Assembly assembly = Assembly.LoadFrom("MyNewFunction.dll", validHash, AssemblyHashAlgorythm.SHA1);
This method takes the file hash and hashing algorithm as arguments in addition to the assembly name. While loading the assembly, the hash is calculated which is then compared to the supplied hash value to ensure that the assembly has not been tampered with. This ensures that the loading and hash checking of the assembly is one atomic function and thus mitigates the TOCTOU vulnerability. It was interesting to note that using any other hashing algorithm other than SHA1 and MD5 in the Assembly.LoadFrom method did not work correctly under .NET 4.6. Making use of SHA1 is not ideal but is the best that can be done using built-in .NET libraries. The bug report for this issue can be found at https://connect.microsoft.com/VisualStudio/feedback/details/2807090.
Certificates
Another option for authenticating an assembly prior to loading it is to sign the assembly with a certificate and then check it when loading the assembly. A code signing certificate can either be bought or one can be generated and self-signed. There are several guides available online to create and use code signing certificates. The following shows an example of how to check that an Assembly’s was signed by a particular certificate authority:
//Load the assemblies certificate
X509Certificate assemblyCertificate = X509Certificate.CreateFromCertFile("MyAssembly.dll");
//Convert the certificate to an X509Certificate2
X509Certificate2 theCertificate = new X509Certificate2(assemblyCertificate);
//Create a chain to check the assemblies certificate against
X509Chain chain = new X509Chain();
//Do not check if the certificate has been revoked
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
//Load the public key of the CA used to sign the assembly
chain.ChainPolicy.ExtraStore.Add(new X509Certificate2(ByteArrayCertificate));
//Allow certificates that are not present in the windows key store
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
//Build the chain from the given information
if (chain.Build(assemblyCertificate))
{
//Compare the thumbprints of the certificate authorities to ensure that the assembly is valid
if (chain.ChainElements[chain.ChainElements.Count - 1].Certificate.Thumbprint == chain.ChainPolicy.ExtraStore[0].Thumbprint)
{
//Load the assembly
Assembly assembly = Assembly.LoadFrom("MyAssembly.dll")
}
}
By checking that the assembly has been signed by a particular certificate authority (or exact certificate) improves on the hash checking model. While a hash needs to be updated for every change to an assembly, a certificate does not. In addition to this, authenticating an assembly’s certificate guarantees the origin of the assembly without having to trust the connection that was used to download it. Note that in this example however, the checking and loading of the assembly are not an atomic function. This means that it is vulnerable to the same TOCTOU vulnerability as the precheck hash method.
File permissions
The last method for securing the loading of new assemblies is to ensure that they cannot be overwritten by an unprivileged user to escalate to a higher privilege level.
File permissions can be used to restrict access to assemblies on disk. Locking the file permissions on the assembly files to only be accessible to the required user will prevent a lower privileged user from modifying the assemblies before they are executed. By preventing unauthorised users from modifying or creating assembly files this mitigates the potential for a privilege escalation scenario.
Conclusions
While using reflection can be inherently dangerous, there are mitigations that can be applied to prevent an attacker from abusing it to insert malicious code. As discussed in this article, there are three main ways of ensuring the validity of an assembly, namely: hashing, signing and file permissions. Hashing ensures that the assembly has not been modified in transit, signing ensures the origin of an assembly can be verified and file permissions prevent unauthorised users from modifying a stored assembly. Using a combination of the described methods it is possible to make reflectively loading an assembly a safe operation.