18 May 2021
tldr; Check out our repo which has multiple F# injection routines, evasion techniques, and an unmanaged F# loader - https://github.com/FortyNorthSecurity/What-The-F
In late 2020, a client challenged us to establish outbound command and control (C2) from within their internal environment. They provided a virtual system to test from, and we figured we'd get C2 up-and-running in a couple hours or so. It took us three days to establish C2 comms.
So what happened, what failed, and what finally worked? This customer really locked down their environment with a solid application allowing listing/application control deployment and configuration. All documented LOLBINs were blocked (including old versions), binary modifications were blocked, but standard Microsoft signed binaries could execute. After struggling for a couple days, we remembered about this blog post from Red Canary about F#.
After spending a little time reading the post, we found Vincent Yu's repo which had a sample shellcode injection routine written in F#. After modifying the shellcode injection code to work with our infrastructure, we transferred the script and required F# dlls onto the virtual machine. We executed our F# script using fsi.exe (F#'s scripting console) and we successfully established C2 comms. Why did this work in the locked-down environment? Both fsc.exe and fsi.exe are digitally signed by Microsoft.
We decided to spend some time building out different F# scripts for use on red team assessments. This led us to publicly release our work via our What-The-F Github repo.
F# is not installed by default on Windows systems, but we still have options to execute F# assemblies on target hosts. Let's look at a couple ways to do this.
First, you can bring the required files and drop them into the directory that you want to run your code from. The required files are:
While we generally try to avoid dropping files to disk, all of these files are digitally signed by Microsoft. After you drop these files, call fsi.exe and pass in the path to an F# scripting file (like the one mentioned above in @vysecurity's repo).
With the F# compiler, you have the option of making standalone binaries with an extra compilation flag (--standalone). The benefit of this method is that it will aggregate all required resources for your assembly and ensure they are all placed into a single binary without any external dependencies. The drawback to this method is you're generally producing binaries that are > 1.5MB in size. This can work if you really want to drop it to disk, but using it in a tool like Cobalt Strike won't work due to size limitations.
Speaking of that, let's talk F# and Cobalt Strike.
If using Cobalt Strike (or really any other C2 framework which utilizes a fork & run methodology for post-exploitation jobs), you can still use F# executables with Cobalt Strike's execute-assembly functionality. However, there are some catches.
You need to make the FSharp.Core.dll accessible to the child process that is being created. There are two different ways to solve this.
Your fork & run child process is what Cobalt Strike injects its post-exploitation job into. If you are executing F# code via execute-assembly, you will need to drop the FSharp.Core.dll into the same directory that your child process resides. When your child process runs, it will find the dll within the same directory and your code will run. This could potentially require administrative permissions, but it is dependent upon the process location that is being used for post-exploitation.
This step requires admin permissions, but results in a much easier way to run F# code. You can register FSharp.Core.dll with gacutil so that it is added to the Global Assembly Cache. When this step is taken, you can chose any post-exploitation child process and not worry about adding FSharp.Core.dll into the same directory because the victim system will be able to find it in the GAC.
*Here's a useful tutorial on registering a DLL in the GAC on a machine that does not have Visual Studio installed.
Our ultimate goal was to execute F# within an unmanaged process, in a similar manner to C# and execute-assembly. We found an existing project called HostingCLR, which bootstrapped a CLR and loaded managed C# code from unmanaged code. We made several multiple modifications to the code base to support F#; however, we still had to drop the FSharp.Core.dll to the same directory as the executable.
But then we found a blog post from Jean Maes which walked us through a way to resolve external DLL dependency errors by embedding the required DLLs in our unmanaged code loader. After some trial and error, we implemented this technique by embedding the FSharp.Core.dll within the unmanaged executable and created a truly standalone loader for F#.
The end result is functionally equivalent to Cobalt Strike's execute-assembly method; however, at the moment this is just a Proof-of-Concept and requires the user to take two steps beyond what execute-assembly requires. First, users must embed their assembly manually into the HostingCLR loader and second, upload that compiled binary onto the target environment.
To make this method truly equivalent to execute-assembly, we need to port it over into a beacon object file. Once that is created (looking for collaborators to work on that btw), you'll be able to execute a F# assembly in Cobalt Strike exactly how C# executes.
What the F is a collection of scripts that we found helpful on various engagements. We have three different sections of code for you to review:
Prior to release, FortyNorth Security contacted Red Canary, and Matt Graeber was happy to review the UnmanagedFSharp project and provide recommendations for detection (thank you both Matt and Red Canary for taking the time to provide your recommendations). The recommendations are as follows:
helloworld.exe
or FSharp.Core.dll
.helloworld.exe
loaded in memory. Carbon Black has a great post highlighting the optics available to vendors and customers when these events are collected.helloworld.exe
and FSharp.Core.dll
and subsequent execution.clr.dll
and mscoree.dll
module loads.We hope to see more use cases of F# due to its ability to run on Windows. If you have any questions about F#, don't hesitate to reach out and contact us!
By Chris Truncer & Joe Leon