11 min read
Some of you love it and some of you hate it, but at this point it should come as no surprise that .NET tradecraft is here to stay a little longer than anticipated. The .NET framework is an integral part of Microsoft’s operating system with the most recent release of .NET being .NET core. Core is the cross-platform successor to the .NET Framework that brings .NET to Linux and macOS as well. This now makes .NET more popular than ever for post exploitation tradecraft among adversaries and red teams. This blog will dive into a new Beacon Object File (BOF) that allows operators to execute .NET assemblies in process via Cobalt Strike versus the traditional built-in execute-assembly module, which uses the fork and run technique.
Cobalt Strike, a popular adversary simulation software, recognized the trend of red teams moving away from PowerShell tooling in favor of C# due to the increase in detection capability for PowerShell, and in 2018 with Cobalt Strike version 3.11 introduced the execute-assembly module. This allowed operators to take advantage of the power of post-exploitation .NET assemblies by executing them in memory without having the added risk of dropping those tools to disk. While the capability of loading .NET assemblies in memory via unmanaged code wasn’t new or unknown at the time of release, I would say Cobalt Strike brought the capability to the mainstream and helped continue to fuel the popularity of .NET for post-exploitation tradecraft.
Cobalt Strike’s execute-assembly module uses the fork and run technique, which is to spawn a new sacrificial process, inject your post-exploitation malicious code into that new process, execute your malicious code and when finished, kill the new process. This has both its benefits and its drawbacks. The benefit to the fork and run method is that execution occurs outside our Beacon implant process. This means that if something in our post-exploitation action goes wrong or gets caught, there is a much greater chance of our implant surviving. To simplify, it really helps with overall implant stability. However, due to security vendors catching on to this fork and run behavior it has now added what Cobalt Strike admits, an OPSEC expensive pattern.
As of version 4.1 released in June of 2020, Cobalt Strike introduced a new feature to try and help address this issue with the introduction of Beacon Object Files (BOFs). BOFs allow operators to avoid the well-known execution patterns as described above or other OPSEC failures such as using cmd.exe/powershell.exe by executing object files in memory within the same process as our beacon implant. While I won’t be going into the inner workings of BOFs, here are a few blog posts I found insightful:
If you read the above blogs, we should now see that BOFs weren’t exactly the saving grace we hoped for and if you were dreaming of re-writing all those awesome .NET tools and turning them into BOFs, those dreams have now been crushed. Sorry. Hope however is not lost as there are, in my opinion, some great things BOFs can offer, and I have recently had a lot of fun (and some frustration too) pushing the limits of what can be done with them. First, was by creating CredBandit which performs a complete in memory dump of a process such as LSASS and sends it back through your existing Beacon communication channel. Today I am releasing InlineExecute-Assembly, which can be used to execute .NET assemblies inside your beacon process with no modification to your favorite .NET tooling. Let’s dive into why I wrote the BOF, some of its key features, caveats, and how it could be useful when conducting adversary simulations/red teams.
Industry newsletter
Stay up to date on the most important—and intriguing—industry trends on AI, automation, data and beyond with the Think newsletter. See the IBM Privacy Statement.
Your subscription will be delivered in English. You will find an unsubscribe link in every newsletter. You can manage your subscriptions or unsubscribe here. Refer to our IBM Privacy Statement for more information.
The reason behind building InlineExecute-Assembly is pretty simple. I wanted a way for our adversary simulation team to execute .NET assemblies in process to avoid some of those OPSEC pitfalls talked about above when using Cobalt Strike to operate in mature environments. I also needed the tool to not tax our team with extra development time by having to make modifications to most of our current .NET tooling. It also needed to be stable. Well, as stable as a complex BOF can be, since the last thing we want is to lose one of our few Beacons into the environment. Basically, it should work as smoothly for the operator as Cobalt Strike’s execute-assembly module as possible.
I know, kind of obvious. We wouldn’t get too far without it, am I right! All joking aside, the intricacies of how the CLR works and what goes on in-depth could be a blog post unto itself, so we are going to review what the BOF uses at a very high level when loading the CLR via unmanaged code.
Loading the CLR
As shown in the simplified screenshot above, the main steps the BOF will take to load the CLR are as follows:
So now the CLR is initialized but there is still a little bit more that needs to happen before we actually get to executing our favorite .NET assemblies. We need to create our AppDomain instance that is what Microsoft explains as “an isolated environment where applications execute”. In other words, this will be used to load and execute our post exploitation .NET assemblies.
AppDomain being created and assembly being loaded/executed
As shown in the simplified screenshot above, the main steps the BOF will take to load and invoke our .NET assembly are as follows:
Hopefully you now have a high-level understanding of .NET execution via unmanaged code, but this still doesn’t get us anywhere near having an operationally sound tool, so we will look at some features that were implemented in the BOF to take it from meh to totes legit.
You are probably wondering why this is important. Well, if you are like me and value your time, you don’t want to spend it modifying pretty much every .NET assembly out there so that its entry point returns a string back with all your data that would normally just be piped to console standard output, do you? Figured as much. In order to avoid that, what we need to do is redirect our standard output to either a named pipe or a mail slot, read the output after it has been written, and then revert it back to its original state. This way we can run our unmodified assemblies just like we would from cmd.exe or powershell.exe. Now, before we go over any code I do need to give thanks to @N4k3dTurtl3 and their blog post about in process execute assembly and mail slots. This is originally what got me down the path of implementing this technique into my own private C implant when it first came out and many months later I ported that same functionality to a BOF. Ok, now that props have been given let’s look at a simplified example of how this would be achieved by redirecting stdout to a named pipe below:
Redirecting console standard output to named pipe and reverting back
Remember when loading the CLR via ICLRMetaHost ->GetRuntime we had to specify what version of the .NET framework we need? Remember how that depends on what version our .NET assembly was compiled with? It wouldn’t be much fun to have to manually specify which version is needed each time, would it? Lucky for us, @b4rtik implemented a cool function to handle this in their execute-assembly module for the Metasploit framework that we can easily implement into our own tooling shown below:
Function that reads our .NET assembly and helps determine what .NET version we need when loading the CLR
Essentially what this function does is when passed our assembly bytes, it will read through those bytes and look for the hex values of 76 34 2E 30 2E 33 30 33 31 39, which when converted to ASCII is v4.0.30319. Hopefully that looks familiar. If that value is found when reading the assembly, the function returns 1 or true, and if it isn’t found it returns 0 or false. We can use this to easily determine what version to load with whether 1/true or 0/false comes back as shown in the code example below:
If/else statement to set .NET version variable
We definitely couldn’t talk .NET offensive tradecraft and not talk about AMSI. While we won’t go in depth about what AMSI is and all the ways it can be bypassed as this has been covered many times over, we will talk a little about why patching AMSI may be necessary depending on what you decide to execute via the BOF. For example, if you decide to run Seatbelt without any obfuscation you will quickly notice that you didn’t get any output back and your beacon is dead. Yes, D.E.A.D. dead. This is because AMSI caught your assembly, determined it was malicious, and shut you down like a house party making too much noise. Not ideal, right? Now we have two good options here when it comes to AMSI, we can either obfuscate our .NET tooling via something like ConfuserX or Invisibility Cloak or we can disable AMSI using a variety of techniques. In our case, we will be using one by RastaMouse, which is to patch the amsi.dll in memory so it returns E_INVALIDARG and makes the scan result 0. Which as pointed out in their blog post, is usually interpreted as AMSI_RESULT_CLEAN. Let’s look at a simplified version of the code for an x64 process below:
In memory patching of AmsiScanBuffer
As you can see in the screenshot above, we simply do the following:
By implementing this into our tool we should now be able to run the default version of Seatbelt.exe using the –amsi flag to bypass AMSI detection as shown below:
InlineExecute-Assemby AMSI bypass example
Fortunately for defenders, there is more than just AMSI to help out when it comes to picking up malicious .NET tradecraft by using ETW. Unfortunately like AMSI, this too can be fairly easy for adversaries to bypass and @xpn did some really awesome research into how this could be done. Let’s look at a simplified example of how you could patch ETW to completely disable it below:
In memory patching of EtwEventWrite
As you can see from the screenshot above, the steps are pretty much identical to how we patched AMSI, so I won’t go over the steps for this one. You can see a before and after screenshot of running the –etw flag below:
Using Process Hacker to view PowerShell.exe properties before running inlineExecute-Assembly with –etw flag
Running inline-Execute-Assembly using the –etw flag
Using Process Hacker to view the same PowerShell.exe properties after running inlineExecute-Assembly
By default, the AppDomain, Named Pipe or Mail Slot created uses the default value “totesLegit”. These values can be changed to better blend in within the environment you are testing by either changing them in the provided aggressor script or via command line flags on the fly. An example of changing them via the command line can be shown below:
InlineExecute-Assembly example using unique AppDomain Name and unique named pipe name
Example of unique AppDomain name ChangedMe
Example of unique named pipe LookAtMe
Example of AppDomain being removed after successful execution is finished
Example of named pipe being removed after successful execution is finished
This section will pretty much be a repeat of what I mention in the GitHub repository, but I felt it was important to reiterate a few things you need to keep in mind when using this tool:
Below are some defensive considerations: