As offensive toolsets continue to move towards using C# as the language of choice for post-exploitation, I thought it’d be useful to think about some of the operational challenges associated with using C# offensively, especially as compared with PowerShell. PowerShell has many operational and convenience benefits for offensive operators that we lose when moving to C#. However, stealth should almost always take precedence over convenience during red team operations. With that being said, we always want our toolset to be as flexible and convenient as possible, while staying below the bar of detection.
In this post, I’ll try to document some operational challenges with using C# and offer solutions to help combat these challenges. Examples in this post will utilize SharpSploit, a .NET post-exploitation library I recently developed (and introduced in my last post). My goal, however, is to document in a way that is applicable to any C# library or project.
Additionally, I’ll introduce SharpGen, a new project I built to combat some of these operational challenges.
When releasing SharpSploit, I made the decision to publish as a library, as opposed to a standalone executable. I did this with some hesitation, as I knew it could cause some operational pain points, but as I continue to develop on C# toolsets, I’m further convinced that it was the right decision. Formatting toolsets as libraries allows them to be utilized together, and helps regain a bit of the flexibility lost in the transition from PowerShell.
However, publishing as a library also adds an operational challenge that you won’t have to deal with for the majority of other C# toolsets, how do I actually use this DLL? There are several options! And as promised in my previous post, I’ll document several options here.
The easiest, and most obvious, method of invoking SharpSploit methods is to create a new console application, add a reference to the compiled SharpSploit.dll
, write any custom code that utilizes SharpSploit, and compile. This will result in a compiled .exe
file that you should be able to execute from the command line. However, an important caveat with this approach is that the resulting .exe
expects the SharpSploit.dll
to be present on the target system.
DLL references are used during compilation to embed metadata about the .dll
into the .exe
so that the .exe
can search for the DLL on a system at runtime. This approach will be successful if you plan to copy the .exe
and the SharpSploit.dll
to disk on the target system, but will not be successful through a method such as Cobalt Strike’s execute-assembly
command, which does not write the .exe
or it’s references to disk on the target system.
So how can we combat this missing DLL conundrum? I document four methods below: ILMerge, Costura, Reflection, and DotNetToJScript.
ILMerge is an open source, static linker for .NET assemblies. As the name implies, it merges multiple .NET assemblies into a single output assembly. To do so, ILMerge actually modifies the merged assemblies by stripping out the metadata of the merged assemblies, and creates an entirely new assembly with its own metadata and identity. If the first assembly in the list provided to ILMerge contains an EntryPoint, the EntryPoint is taken as the EntryPoint of the merged assembly. An “EntryPoint” is what makes a console application a console application, and not just a library. The EntryPoint is often the “Main” function that you may be familiar with.
I’ll walkthrough a quick example of how to use ILMerge. First, you’ll create a new console application that references SharpSploit, and write your custom code that uses it:
You’ll build this application, which results in a .exe
, SharpSploitDriver.exe
in this case, and the SharpSploit.dll
. Now we can use ILMerge to merge these two assemblies into a SharpSploit.exe
. In this case, ILMerge.exe
is already on my Path. You’ll also need to make sure you delete any generated .pdb
files prior to merging.
This generated SharpSploit.exe
is a self-contained executable that will not expect a SharpSploit.dll
on the target system. ILMerge can also be automated as a part of the build process with some configuration. If you choose to go this route, ILMerge documents how to do this in the README.
There’s also some interesting forensic implications of this merge. If we open SharpSploit.exe
in DNSpy:
We see that SharpSploit
is not actually included as a reference, but the SharpSploit namespaces are embedded as modules in the SharpSploit.exe
assembly. We can compare this with SharpSploitDriver.exe
in DNSpy:
In this case, SharpSploit is included as a reference, but not as a module. I’m not sure that one or the other is forensically “stealthier”, although references can result in “ImageLoad” events (to use the Sysmon nomenclature) that could be detected. Either way, I think it’s important to be aware of potential forensic artifacts.
To be clear, this process is more than just merging files together, but actually merging assemblies. This process is partially destructive and could have an impact on how the application executes. For instance, if the functionality depends on the assembly’s name:
Costura is another open source project with a similar goal to ILMerge. However, the method used is slightly different. Costura adds DLL references as “embedded resources” to the console application. Embedded resources are commonly used for miscellaneous files needed by an application, such as images, videos, or other files.
Costura embeds reference DLLs as embedded resources, and appends a callback to the AppDomain’s AssemblyResolve
event that attempts to load assemblies from the application’s embedded resources! This affects the way the system will resolve assembly loads for the application, and allows us to load assemblies from arbitrary locations, such as an application’s embedded resources.
This trick originates from a 2010 blog post by Jeffrey Richter, in which he demonstrates the process for registering a callback for the AssemblyResolve
event.
I’ll walkthrough a quick example of how to use Costura. You’ll start out just like last time by creating a console application, adding a reference to the SharpSploit.dll
, and writing any custom C# code that utilizes SharpSploit. You’ll also have to add a reference to Costura.Fody
. This can be installed as a Nuget package by right-clicking on References and selecting Manage Nuget Packages
:
A “gotcha” to keep in mind with Costura, is that the most recent versions have deprecated support for .NET Framework v3.5. For offensive operations, .NET Framework v3.5 is usually what you will want to be working with. Be sure to install Costura v1.6.2 to work with .NET Framework v3.5 assemblies. After installing ILMerge, you’ll see that a FodyWeavers.xml
file has been created. This is the Costura configuration, which by default will embed all referenced assemblies as resources. Now when we recompile, it results in a self-contained SharpSploitDriver.exe
executable:
There are some interesting forensic implications of Costura-generated binaries as well. When opening SharpSploitDriver.exe
in DNSpy:
You’ll see that not only will SharpSploitDriver.exe
maintain its reference to SharpSploit
, it also includes: a reference to Costura
, several embedded references: costura.sharpsploit.dll.compressed
, costura.sharpsploit.pdb
, and costura.system.management.automation.dll
, as well as a Costura
module.
So how do ILMerge and Costura compare? Well, you’ll find an interesting comment on the previously mentioned blog post from the author of ILMerge:
Despite that comment, I think there are use cases for both solutions. In fact, for stealth and forensic reasons, I’d probably recommend ILMerge over Costura unless ILMerge impacts how your specific application executes. For SharpSploit specifically, I have not noticed any issues using ILMerge.
Reflection can be used to execute .NET assemblies that do not contain an EntryPoint (i.e. a DLL). The System.Reflection
namespace can be used to load .NET assemblies, invoke methods, and much more. So we can use .NET reflection to load the SharpSploit assembly and invoke methods.
One method for utilizing reflection is using PowerShell. For example, we could invoke SharpSploit methods like this:
Or we could load it from a hosted location, and then load the assembly and invoke methods by utilizing reflection:
And of course, anything we can do from PowerShell, we can do from C#:
For that example, you’ll want to remember to compile as a console application. However, you will not need to worry about adding any references, as SharpSploit is loaded through reflection, not through the typically assembly resolving process. Though it is worth noting that Costura’s “AssemblyResolve” technique itself uses reflection, by utilizing the System.Reflection.Assembly.Load()
method.
Reflection is an interesting vector for execution, and can serve as a useful “download cradle” to save on executable size and avoid reference complications. For more information on reflection, I’d recommend checking out Microsoft’s documentation.
DotNetToJScript is an open source tool written by James Forshaw that creates JScript or VBScript files that load a given .NET assembly. However, there are a few limiting factors that make this option less practical for use with SharpSploit. It’s still possible, but will require some customization!
The first thing you’ll notice is that DotNetToJScript does not work nicely with large assemblies, and by default, SharpSploit is a large assembly:
Embedded Mimikatz binaries cause SharpSploit to be large. Luckily, it’s easy to choose not to embed the binaries at compile time, if you don’t need them. Just comment out a couple lines in the SharpSploit.csproj
file, like so:
After making this change, you’ll just have to recompile SharpSploit
and copy over the new SharpSploit.dll
to the DotNetToJScript folder.
The next thing you’ll find is that DotNetToJScript does not work well with static
classes or methods:
Most of SharpSploit’s useful methods are static, including ShellExecute
. To use this method with DotNetToJScript, you’ll have to edit the method in SharpSploit by removing the static
classifier on the ShellExecute
method, recompiling, and copying the new SharpSploit.dll
to the DotNetToJScript folder. And, finally, it should succeed:
This method is interesting for one-off executions or launching an agent, but it requires us to drop a JScript or VBScript file to disk on the target system, which is unnecessary if you already have an agent on the system.
So far, I’ve documented four potential options for creating a self-contained executable and/or script that can invoke SharpSploit methods: Costura, ILMerge, Reflection, and JScript/VBScript via DotNetToJScript. Let’s quickly compare and contrast these methods and when they might be useful:
execute-assembly
command and similar methods. I’ve already compared and contrasted these methods earlier, and the conclusion was that ILMerge is usually, but not always, the right option.The real downside to the Costura/ILMerge methods, at least as described so far, is convenience. Not only do you have to compile the SharpSploit library, you also have to create an additional console application that references the library, implement either ILMerge or Costura configurations, and compile.
At first, this doesn’t seem like too much additional work to ask for. However, you would need to do all of this every time you want to invoke any SharpSploit method. As an operator, you really want to be able to quickly invoke consecutive SharpSploit methods, and this is a real hindrance to the operational effectiveness of the project.
So how can we solve the operational challenge of convenience?
Since the release of SharpSploit a little over a month ago, two open source projects have already attempted to combat the operational challenge of convenience. SharpSploitConsole and SharpAttack have both sought to solve this problem in similar, but different ways. They both serve as a singular console application that can interface with many different methods included in SharpSploit. SharpSploitConsole leverages Costura, while SharpAttack leverages ILMerge.
Both of these applications accept arguments as command line parameters that specify SharpSploit methods and parameters to be executed. These projects allow us to compile a console application just once, and have access to a majority of the functionality of SharpSploit without having to constantly recompile new console applications. This is a huge win for convenience, however, I think there is a downside in flexibility with these approaches.
For example, let’s say you wanted to use SharpSploit to enumerate all Domain computers, and find the local administrators for these computers. In a custom console application with SharpSploit, you’d do something like this:
With SharpSploitConsole or SharpAttack, you’d likely have to run GetDomainComputers()
, parse out the results as text, and run GetNetLocalGroupMembers()
on each of the computer names.
Or let’s say that we want to run some custom C# code as an alternative user, runas.exe
-style. In a custom console application with SharpSploit, you’d do something like this:
With SharpSploitConsole or SharpAttack, I’m not sure this would be possible, as the custom C# code would need to be compiled and loaded as an assembly.
An additional caveat with these approaches, which is really the fault of SharpSploit itself, is that they don’t work out of the box with Cobalt Strike’s execute-assembly
. You must strip out the embedded Mimikatz PE binaries prior to use with execute-assembly
, as Cobalt Strike has an upper size limit of 1 MB for assemblies. It’s a real bummer to not be able to utilize Mimikatz from within SharpSploit over Cobalt Strike. Luckily, I’ve made some changes in SharpSploit v1.1 that will allow it to happen, which we will discuss later in the post.
With these approaches, we gain convenience at the cost of the loss of power of using SharpSploit as a library in customizable ways. We introduce the operational challenge of flexibility.
There are key operational differences in using a compiled language like C# as opposed to an interpreted language like PowerShell. We lose quite a bit of flexibility making this change. With a scripting language, we are able to quickly edit scripts on the fly without worrying about the extra step of compilation. Without PowerShell, we lose the power of the pipeline and the ability to combine toolsets or quickly filter or format output with built-in cmdlets, such as Select-Object
or Format-List
.
With C#, there’s no native way to send the output of one tool over the pipeline as input to another tool. There’s no native way to make a small edit to a compiled executable. To help bring back a bit of this flexibility, I’ve written a tool called SharpGen, which is described throughout the remainder of the post.
To attempt to combat the operational challenge of flexibility, I’ve created a .NET Core console application called SharpGen. SharpGen utilizes the Roslyn C# compiler to quickly cross-compile .NET Framework console applications or libraries. .NET Core allows SharpGen to be cross-platform, permitting operators to utilize SharpGen from whatever OS they prefer.
Remember, the challenge of convenience was caused by having to constantly create new console applications in Visual Studio, add references, embed the references using Costura or ILMerge, etc. SharpGen solves this challenge by making it as quick as a single command to create a new console application, and comes with some additional benefits.
The most basic usage of SharpGen would be to provide SharpGen an output filename and a C# one-liner that you’d like to execute. SharpGen will generate a .NET Framework console application that will execute the one-liner. For example:
This example generates example.exe
, a .NET Framework console application that executes the Mimikatz sekurlsa::logonpasswords
module and writes the output to the screen.
The C# one-liner should always be specified as the final, unnamed command line argument when using SharpGen. However, you can alternatively specify a source file to read from. You may require some logic that does not fit nicely into a single line, or perhaps you are having trouble with escaping quotes on the command line. SharpGen supports reading from a file with the --source-file
command line argument.
Or, you can specify a source file with a pre-defined class, complete with a Main function:
Those are the very basics of the tool. The complete command line usage information is included below:
We’ll dive into some details on how SharpGen works under the hood and additional usages in the following sections.
To understand how SharpGen works, let’s take a quick look at the directory structure of the project:
You’ll want to pay particularly close attention to the Source
, References
, and Resources
directories, as these drive SharpGen’s core functionality. All source code placed under the Source
folder will be compiled as source into a single assembly. Because it is compiled as source, there is no need to worry about combining assemblies or embedding them as resources. By default, SharpSploit source code is included in the Source
directory, for easy compilation against SharpSploit. However, SharpGen is built in a way so that any source libraries could be dropped into this folder and included.
For instance, we could drop in the GhostPack SharpWMI
source code into the Source
folder (with a few minor modifications) and compile against it:
This allows us to invoke SharpWMI
and SharpSploit methods from a single assembly! The ability to drop in other libraries and quickly compile against a combination of libraries might be my favorite feature of SharpGen. I did have to make a few minor modifications to SharpWMI
to enable this to work. I would love to see new offensive C# toolsets formatted as libraries to allow this sort of tool combination by default, without customizations.
Assembly references can be configured under the References
directory. References can be applied to net35 or net40 assemblies, by being placed in the corresponding directory and configured in the references.yml
configuration file. This references.yml
configuration comes with sane defaults, but you may want some customization. For instance, if you add additional source code that expects an additional reference, you will need to add this reference in the configuration.
Or, let’s say you know that you won’t need a particular reference. For instance, you know you won’t need to execute any PowerShell and the endpoint you are on will detect a System.Management.Automation.dll
ImageLoad event. SharpSploit includes a reference to System.Management.Automation.dll
by default, but can be removed if you don’t plan on using the SharpSploit.Execution.Shell.PowerShellExecute()
method. In this scenario, you can simply disable the System.Management.Automation.dll
reference in the references.yml
configuration by setting Enabled: false
:
Make sure to also disable the Net40
reference if you plan on targeting .NET framework v4.0!
If you plan to use SharpGen to create assemblies for use with Cobalt Strike’s execute-assembly
command (which it was specifically designed to do), you’ll want to pay attention to the Resources
directory. The main limiting factor of the execute-assembly
command is the 1MB upper size limit. SharpSploit embeds both x86 and x64 Mimikatz binaries by default that pushes it over the 1MB limit. You have a few options to combat this limiting factor:
resources.yml
configuration file, which will drastically reduce your binary size. To do so, simply switch the resource configuration to Enabled: false
:2. If you do plan to use use Mimikatz methods, do you really need both the x86 and x64 copy of Mimikatz? If not, we can embed only the resource of the platform we need. There are two ways to do this:
2a. You can filter resources based on platform through command line arguments. This should get you just below the 1MB limit:
2b. You can also simply disable the unneeded resource in the resources.yml
configuration file like we did earlier:
3. To further reduce binary size, you can embed compressed Mimikatz resources instead of the default resources, which is supported and handled by SharpSploit. These are compressed using the built-in System.IO.Compression
library. This compression is not efficient enough to allow you to embed both the x64 and x86 resources and stay under the 1MB limit, but still significantly reduces binary size. This can be done using the resources.yml
configuration file by enabling compressed resources and disabling uncompressed resources:
4. The more robust way to use Mimikatz and embed both the x64 and x86 resources, while staying underneath the 1MB limit, is by using ConfuserEx resource protection, which uses a much more efficient compression algorithm. We’ll discuss this in the Advanced Usage section.
SharpGen supports the use of ConfuserEx, an open-source protector for .NET applications. The original ConfuserEx that I had been familiar with is available here. I was pretty excited to find that ConfuserEx has been forked and maintained in a new location, as the originally project has been abandoned. I was even more excited when I realized that the new ConfuserEx supports .NET Core! This allows us to automate protection and obfuscation of our binaries from within SharpGen, cross-compiled for .NET Framework!
SharpGen includes a default ConfuserEx project file, confuse.cr
that can be used and/or customized with additional ConfuserEx rules, and can be utilized through command line arguments:
The default confuse.cr
file includes only a single rule that enables resource protection. ConfuserEx resource protection performs encryption and LZMA compression on the embedded resources. LZMA compression is a much more efficient compression algorithm than the System.IO.Compression
compression performed on the powerkatz*.dll.comp
files included in SharpGen. This more efficient compression allows us to embed both of the Mimikatz binaries into a compiled binary and still come in under the 1MB Cobalt Strike execute-assembly
limit! An important caveat is that you should embed the non-compressed version of the resources when using this technique, as compression of previously compressed files does not work nearly as well.
In addition to resource protection, the confuse.cr
project file can be modified with additional ConfuserEx rules. For ease of use, the default contains many additional rules that can be uncommented to enable:
For further information on available ConfuserEx protections, I’ll point you to the ConfuserEx Wiki documentation.
Another feature of SharpGen to be aware of, is that SharpGen attempts to optimize your source code by removing unused types. It does this to reduce the final binary size, but also for stealth. There’s no reason your assembly should contain Mimikatz and PE loading source code if it doesn’t need it! This becomes much more useful if you are adding many libraries underneath the Source
folder, as you might not need to reference all of them for each compilation.
SharpGen is fairly transparent about the optimizations made during compilation and will always print the original source and the optimized source code while compiling.
This optimization appears to work well, but of course there’s always the risk of breaking things when automating source code modifications. So if you run into issues with this optimization, you can always disable it with the --no-optimization
command line argument (be aware that this could increase your binary size!):
Coincidentally, a similar tool with some key differences, was released just last week called SharpCompile, that I would encourage everyone to look into as well. I think the coolest aspect of SharpCompile is that includes an aggressor script that handles all of the compilation in the background, so you never have to leave the Cobalt Strike interface!
I would love to add a similar feature to SharpGen that would handle all of the compilation in the background, and prevent users from ever having to leave the Cobalt Strike interface to generate new assemblies. So look for that to be added to SharpGen here in the near future.
The use of offensive C# is exciting, but also comes with a set of operational challenges, particularly when formatting toolsets as libraries. Personally, I’d love to see additional open-source toolsets published as a libraries with an optional front-end console application interface, which would give us the best of both worlds.
Solutions to these operational challenges have to select a method of execution while balancing the needs for convenience and flexibility. SharpGen is my solution to this balancing act, and I hope that others find it useful. But other viable solutions such as SharpSploitConsole, SharpAttack, and SharpCompile do exist, and I am sure that others will emerge. I encourage others to think about these challenges to pick the right tool for the right use case.
SharpGen utilizes several open source libraries that I’d like to credit:
Microsoft.CodeAnalysis.CSharp
, written by Microsoft.McMaster.Extensions.CommandLineUtils
library, written by Nate McMaster.I’d also like to acknowledge some of the other open source projects mentioned throughout this post:
csc.exe
.And finally, some additional resources I’d recommend checking out: