What the F#*%

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.

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.

How to run F#

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.

Drop the Required Files

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:

  • fsi.exe
  • FSharp.Core.dll
  • FSharp.Compiler.Private.dll
  • FSharp.Compiler.Interactive.Settings.dll
  • Microsoft.Build.Utilities.Core.dll

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).

Standalone Binaries

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.

Cobalt Strike Usage

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.

Drop to Disk

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.

GAC Addition

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.

Unmanaged Code

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

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:

  • Shellcode Loaders - This directory contains multiple examples of loading shellcode into memory using various API calls.
  • Evasion - The same techniques that C# leverages to evade detection are also applicable to F#. These techniques are all already publicly documented and have been ported to F#.
  • UnmanagedFSharp - This is the modified HostingCLR project which can be used to execute F# code within an unmanaged process. This project also has FSharp.Core.dll embedded within it to negate the need to drop it on disk. The project specifically uses the open source version of FSharp.Core.dll.

Detection

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:

  • As expected, Sysmon event data doesn't reveal any events related to the in-memory loading of helloworld.exe or FSharp.Core.dll.
  • AMSI catches the entire contents of helloworld.exe loaded in memory. Carbon Black has a great post highlighting the optics available to vendors and customers when these events are collected.
  • .NET ETW events capture context related to the loading of helloworld.exe and FSharp.Core.dll and subsequent execution.
  • .NET and AMSI ETW optics offer a rich opportunity to detect in-memory loading and the presence of evasive FSharp. This post covers how the PoC code could be supplemented to evade ETW logging.
  • Even though obvious F# artifacts are not present in readily accessible event data, there are plenty of optics available related to the loading of .NET code - e.g. clr.dll and mscoree.dll module loads.
  • By hiding the loading of FSharp components, defenders should concern themselves less with detecting .NET language implementations (e.g. C#, VB.Net, JScript.Net, F#) and focus more on behaviors associated with .NET execution.

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