SILENTTRINITY (byt3bl33d3r, 2018) is a recently released post-exploitation agent powered by IronPython and C#. This blog post will delve into how it works and techniques for detection.
.NET has become a significant component in the offensive security world, due to its vast functionalities and capabilities, ease to develop in, and default presence on modern Windows platforms. It is beginning to take the place of PowerShell – used for similar reasons originally – due to the increasing ability of blue teams to detect malicious PowerShell.
IronPython is essentially Python that is tightly coupled with the .NET framework. This effectively means that threat actors are able to leverage the power of .NET libraries with simple Python scripting and arguably more easily develop cross-platform implants in Python, utilizing IronPython on Windows platforms.
Dynamic Language Runtime
.NET languages are well known for being static binding (variable types and methods are bound at compile time), whereas Python is an interpreted scripting language. As such, how is it possible to execute Python code within a .NET environment? The answer to this lies in the dynamic language runtime (DLR). The image below shows an architecture view of the DLR (Microsoft 2017).
The DLR is a runtime environment that allows dynamic languages like Python to run on .NET and be eventually compiled into its corresponding common intermediate language (CIL) format. One of the DLR components that made this possible is “Dynamic Object Interoperability”.
A key nature of Python is it not having to have its variable type declared at compile time. The variable type resolution is instead performed at run-time, which is different to how standard .NET languages, such as C#, operate. To solve this, the DLR provides a data type known as “Dynamic”. Variables assigned with the “Dynamic” data type are resolved at run-time instead of compile time, thereby allowing Python to keep its dynamic nature and still be able to run within the .NET environment.
In SILENTTRINITY, the IronPython code is embedded and executed within a C# object and, by extension, the .NET environment. This means that it would first get compiled to CIL code, subsequently becoming native code through the use of the JIT engine. This also means that it is possible to inspect IronPython behavior through the use of .NET ETW providers. We have blogged about this in more detail previously (part 1 and part 2).
How SILENTTRINITY works
SILENTTRINITY is made up of a C2 server running Python and a C# implant that is executed on the victim’s machine through the use dynamic .NET assembly loading via reflection. The gif below shows a high level overview of how SILENTTRINITY works. We will use the msbuild stager mechanism as the example.
1.An xml file containing an embedded serialized C# implant is first dropped onto the victim.
2.Upon execution though MsBuild.exe, the C# implant is deserialized and loaded into memory using reflection.
3.Once in memory, the C# implant connects back to SILENTTRINITY C2 server and downloads a zip file, named staged.zip, into memory. The zip file is effectively made up of IronPython’s .NET assembly and a Python script, named stage.py.
4.Subsequently, the .NET assembly is loaded into memory through the use of .NET’s reflection library. Once the assemblies are loaded, the C# implant is able to call upon the IronPython engine to execute stage.py.
5.The C2 server is now able to push Python coded modules whenever a request is made by the C# implant.
Now that we have a basic idea of how SILENTTRINITY works, let’s look at how we can go about detecting it. We will start by making use of .NET ETW providers via the Python script we released to trace the behavior as seen through the behavior of the underlying use of the CLR.
Dynamic Assembly Loading
C# implant is actually a C# Class of the “ST” type and an instance of it is created dynamically during runtime. This can be achieved with .NET’s “Reflection” and “Activator” libraries as seen in the code snippet below.
We can thereby attempt to trace for the presence of such function calls with our Python script as seen in the image below.
As shown, we can observe JIT-Inlining failures of both “GetType” and “CreateInstance” functions, hence indicating that these functions have been executed at least once. However, this is quite generic on its own. We do, however, also see in-memory assembly loads related to the instance creation and the SILENTTRINITY assembly itself:
Before the C# implant is able to use the python engine to execute python code, it must first import a set of assemblies at runtime namely:
This can be accomplished with a call-back method by utilizing the “ResolveAssembly” event (Richter, 2010). The code snippet below shows how this is achieved.
Once the assemblies are properly loaded, the C# implant would be able to utilize the IronPython engine to execute Python code as shown in the code snippet below.
Since the loading of the aforementioned DLLs are a crucial part of SILENTTRINITY, we can attempt to hunt for evidence of these assembly loads as well:
In this instance, we can observe the DLLs being loaded through both the “DomainModuleDCStart_V1” and “ModuleDCStart_V2” events, thus indicating the presence of IronPython. They are also loaded in-memory, without a file backing. However, it is important to note that the presence of these assemblies is not indicative of any malicious behavior on its own. A legitimate .NET application that utilizes the IronPython engine would load these, though it may be more likely to load them from disk. However, in most corporate environments, it’s unlikely that there would be many applications making use of IronPython legitimately and therefore any legitimate applications could probably be baselined fairly easily.
Another important distinction from other IronPython usage is that the assemblies involved here are used for the embedded use of IronPython that SILENTTRINITY uses. If IronPython was used instead as a standalone python interpreter for running python scripts which calls upon the .NET framework, the ipy.exe standalone interpreter would be seen in use.
SILENTTRINITY contains a few modules that can aid a threat actor during a post exploitation phase. After the C# implant is able to load the IronPython runtime engine, it would be able to read and execute modules sent by the C2 server. This is where it becomes interesting because these post-exploitation modules can be written in Python.
We will take a look at one of these modules, SafetyKatz. This is much like GhostPack module of the same name, which dumps the process memory of lsass and then loads Mimikatz in-memory to extract credentials. The code snippet below shows the implementation of SafetyKatz in Python:
As mentioned in the Dynamic Language Runtime section, it is possible to detect this through the use of .NET ETW providers to track JIT and Interop events.
As expected, we are able to observe events that are of concern for instance interop events of VirtualAlloc and MinDumpWrite function calls from the SharpSploit module.
Other than looking at .NET ETW events, we can also trace for suspicious process activities. Since we are using the msbuild stager in this example, we obviously see msbuild process execution events.
Since MSBuild is responsible for executing the xml file, we can hunt for signs of xml execution that were performed by MSBuild. Most MSBuild events follow a common format in terms or parent and arguments and so across an enterprise it is possible to baseline normal activity and spot deviations.
We can also hunt for high outbound connections that originates from MSBuild. The C# implant has to regularly connect back to the C2 server to check for pending jobs as shown in the code below.
As such, even if there are no pending jobs, the C# implant would still have to communicate with the C2 periodically or until the MSBuild process is terminated. Thus, we can hunt for high outbound traffic as seen in the two images below.
As defenders we always aim to have visibility of threat actors at every stage of a cyber killchain. This also holds true when we are identifying indicators of compromise (IOCs). This post has presented several IOCs to detect the presence of SILENTTRINITY. Some of the IOCs, such as detecting IronPython assembly loading, are low fidelity indicators on their own but when you combine all the activity the behavior of SILENTRINITY becomes increasingly specific, which aids hunting for it.
Defenders often have to draw a hypothesis during an investigation and having several different IOCs can often strengthen the hypothesis. For instance, if we were to detect the dynamic assembly loading, combined with IronPython assembly loads, combined with XML arguments to MSBuild.exe and regular network connections, then we have much increased evidence of compromise, even before a malicious module like Safetykatz is run.
We would like to extend our gratitude towards byt3bl33d3r for creating such an impressive tool. Check it out if you haven’t done so already.
- byt3bl33d3r (2018, Oct 14) SilentTrinity, a post exploitation agent. Retrieved from https://github.com/byt3bl33d3r/SILENTTRINITY
- Microsoft (2017, Mar 30) Dynamic Language Runtime Overview. Retrieved from https://docs.microsoft.com/en-us/dotnet/framework/reflection-and-codedom/dynamic-language-runtime-overview
- Jeffery Richter (2010, Feb 3) Excerpt #2 from CLR via C#, Third Edition. Retrieved from https://blogs.msdn.microsoft.com/microsoft_press/2010/02/03/jeffrey-richter-excerpt-2-from-clr-via-c-third-edition/