As part of improving security for the Internet as a whole, it is required that the information security industry identify vulnerable code and provide documentation so that developers can build safer code. Safer code means fewer exploits, and safer endpoints. In this post, I will cover an attack on loading assemblies in .NET.
In the .NET programming languages, applications are able to dynamically load code bases referred to as Assemblies. Assemblies can be executables (EXEs), or dynamic libraries (DLLs). These assemblies can be loaded from a resource, file, or even from a simple byte array. Once loaded, the assemblies can be inspected by a number of function calls on the Assembly object. The code inside the assemblies can also be called, making this functionality very useful for implementing modular interfaces in .NET.
For a simple example of loading an assembly and inspecting it, let's take a look at an example from OWASP (before we notified them of the issue with the code), which is usually a reasonable source for secure code.
Vulnerable code example located previously on OWASP wiki. Updated code sample can be found here.
This code sample loads the assembly with the Assembly.LoadFile function which creates an Assembly object in memory which represents the .NET assembly. Once this assembly is loaded, each module in the assembly is checked for classes and methods. Clearly, this code sample has no intention of executing code from the assembly.
In order to enable developers to migrate to .NET slowly over time, Microsoft allows for what are called Mixed Assemblies. These are assemblies that include both managed .NET code as well as unmanaged C/C++ code. More documentation about these assemblies can be found in the MSDN documentation.
In order to support DLLs being converted to mixed assemblies, the DllMain function in DLLs is supported in mixed assemblies. This function is normally used as the entry point of execution for DLLs, which is called when the DLL is loaded and unloaded in order to allocate and free resources needed by other functions made available. More information about DllMain can be found on MSDN.
In order to exploit assembly loading, an attacker could create a malicious mixed assembly. This mixed assembly would include its malicious code in its DllMain function. In order to exemplify this, I have created a very simple mixed assembly which creates a popup in its DllMain.
Simple proof of concept code which creates a popup notification when it can execute code. Text version here.
When this is compiled with the /clr flag, it can be loaded as a mixed assembly.
In order to exploit the code from OWASP, all we need to do is attempt to load the mixed assembly with it. During the call to Assembly.LoadFile, the DllMain from the mixed assembly is called, allowing for our proof of concept code to be executed.
The exploit has succeeded and is displaying the popup.
The code being executed is not limited to a MessageBox, but could be used to execute any code with the same permissions as the application loading it. For instance, a simple call to CreateThread could allow for a payload to be executed without blocking the loading application making for a stealthy attack.
When dealing with untrusted binaries, the best method to avoid exploitation is to avoid loading the assembly as much as possible. It is dangrous to load untrusted assemblies, and reasonable precaution needs to be taken. If it is at all possible to not load untrusted assemblies, avoid loading them. Alternative methods to loading assemblies are specific to the intention of loading them. In order to avoid writing a novel, I will cover the specific case of this OWASP code.
In order to avoid exploitation from this mixed assembly attack, we only need to change one line in the OWASP code. We can replace the Assembly.LoadFile function with Assembly.ReflectionOnlyLoadFrom. This allows us to read the assembly into memory and read information about the assembly without executing code. While this method will avoid this specific method of exploitation, it may be possible to otherwise exploit it. As .NET becomes more and more open source, it will be easier to determine how safe certain methods are.
Once we do this change, and execute, we can see what we would normally expect from loading an assembly with this code.
The mitigation method proves effective and the exploit fails.
While managed code solutions can be more secure than unmanaged ones, it does not make them impervious to exploitation. Much like any other application, .NET applications need to be audited for security risks, whether they are memory corruption based, or in this case, based on a potentially unclear design choice. It should be noted that loading any untrusted code/data in any application needs to be treated with extreme care. This can be very dangerous, and is a diverse attack surface.