diff --git a/README.md b/README.md index f9f079c..6209959 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,9 @@ SendHashes(UserHash[] userHashes) | bofnet_jobkill *job_id* | Dump any pending console buffer from the background job then kill it. Warning, can cause deadlocks when terminating a thread that have transitioned into native code | | bofnet_boo *booscript.boo* | Compile and execute Boo script in seperate temporary AppDomain | | bofnet_vfs_add *local_path* *vfs_filename* *content_type* | Add a file from the operator machine and store inside the BOFNET VFS | +| bofnet_executeassembly *assembly_name* [*args*] | Execute a standard .NET assembly calling the entry point, supplying optional arguments. Blocks Beacon until completion. | +| bofnet_jobassembly *assembly_name* [*args*] | Execute a standard .NET assembly calling the entry point, supplying optional arguments. Runs as a background job (two threads). | +| bofnet_patchexit | Re-patch .NET's Environment.Exit() to prevent exit. Performed by default during `bofnet_init` but useful if DLLs are unhooked later. | ## Built-in BOFS @@ -213,4 +216,4 @@ Once the steps are complete, the `build\dist` folder should contain the artifact ## References * https://modexp.wordpress.com/2019/05/10/dotnet-loader-shellcode/ - CLR creation using native raw COM interfaces -* https://gist.github.com/sysenter-eip/1a985a224c67aa78f62be83f190b6e86 - Great trick for declaring BOF imports +* https://gist.github.com/sysenter-eip/1a985a224c67aa78f62be83f190b6e86 - Great trick for declaring BOF imports \ No newline at end of file diff --git a/bofnet.cna b/bofnet.cna index c5bfcce..e0064ac 100644 --- a/bofnet.cna +++ b/bofnet.cna @@ -11,7 +11,9 @@ beacon_command_register("bofnet_jobstatus", "Dump the console buffer of an activ beacon_command_register("bofnet_jobkill", "Kills a running jobs thread (warning, could leave leaked resources/sockets behind", "Synopsis: bofnet_jobkill jobid\nKills a running jobs thread (warning, could leave leaked resources/sockets behind\n"); beacon_command_register("bofnet_boo", "Runs a Boo script in a temporary AppDomain which is then unloaded", "Synopsis: bofnet_boo filename.boo\nRuns a Boo script in a temporary AppDomain which is then unloaded\n"); beacon_command_register("bofnet_vfs_add", "Uploads a file to the in memory VFS storage", "Synopsis: bofnet_vfs_add local_path filename content_type\Uploads a file to the in memory VFS store\n"); - +beacon_command_register("bofnet_executeassembly", "Execute a standard .NET assembly calling the entry point (blocks until completion)", "Synopsis: bofnet_executeassembly assembly_name arg1 arg2 ...\nExecute a standard .NET assembly calling the entry point and passing all arguments supplied (blocks until completion)\n"); +beacon_command_register("bofnet_jobassembly", "Execute a standard .NET assembly calling the entry point (as a background job)", "Synopsis: bofnet_jobassembly assembly_name arg1 arg2 ...\nExecute a standard .NET assembly calling the entry point and passing all arguments supplied (as a background job)\n"); +beacon_command_register("bofnet_patchexit", "Re-patch .NET's Environment.Exit() to prevent exit", "Synopsis: bofnet_patchexit \nRe-patch .NET's Environment.Exit() to prevent exit"); @@ -82,6 +84,7 @@ alias bofnet_init { btask($1, "Initializing BOFNET"); beacon_inline_execute($1, $bofnetNative, "go", "BOFNET.Bofs.Initializer\x00".$bofnetRuntime); + beacon_inline_execute($1, $bofnetNative, "go", "BOFNET.Bofs.PatchEnvironmentExit\x00"); } alias bofnet_shutdown { @@ -212,6 +215,50 @@ alias bofnet_boo { bofnet_execute_raw($1, "BOFNET.Bofs.Boo.BooRunner", $args); } +alias bofnet_executeassembly { + + $bofnetNative = loadBOFNativeRuntime($1); + if($bofnetNative != $null){ + return; + } + $bofArguments = "\x00"; + + @argParts = sublist(@_,2); + if(size(@argParts) > 0){ + $bofArguments = " ".addQuotes(@argParts)."\x00"; + } + + btask($1, "Attempting to start .NET assembly in blocking mode"); + beacon_inline_execute($1, $bofnetNative, "go", "BOFNET.Bofs.ExecuteAssembly ".$2.$bofArguments); +} + +alias bofnet_jobassembly { + + $bofnetNative = loadBOFNativeRuntime($1); + if($bofnetNative != $null){ + return; + } + $bofArguments = "\x00"; + + @argParts = sublist(@_,2); + if(size(@argParts) > 0){ + $bofArguments = " ".addQuotes(@argParts)."\x00"; + } + + btask($1, "Attempting to start .NET assembly as a job"); + beacon_inline_execute($1, $bofnetNative, "go", "BOFNET.Bofs.Jobs.JobRunnerAssembly ".$2.$bofArguments); +} + +alias bofnet_patchexit { + $bofnetNative = loadBOFNativeRuntime($1); + if($bofnetNative != $null){ + return; + } + + btask($1, "Re-patching .NET Environment.Exit()"); + beacon_inline_execute($1, $bofnetNative, "go", "BOFNET.Bofs.PatchEnvironmentExit\x00"); +} + alias bofnet_vfs_add{ local('$fileData $args'); diff --git a/managed/BOFNET/BOFNET.csproj b/managed/BOFNET/BOFNET.csproj index 080d927..e28eb5c 100644 --- a/managed/BOFNET/BOFNET.csproj +++ b/managed/BOFNET/BOFNET.csproj @@ -45,6 +45,7 @@ AnyCPU + true @@ -56,6 +57,14 @@ false true + + + true + + + + true + diff --git a/managed/BOFNET/BeaconConsoleWriter.cs b/managed/BOFNET/BeaconConsoleWriter.cs index d637780..4361c88 100644 --- a/managed/BOFNET/BeaconConsoleWriter.cs +++ b/managed/BOFNET/BeaconConsoleWriter.cs @@ -32,12 +32,53 @@ public override void Write(byte[] buffer, int offset, int count) { public override void Flush() { base.Flush(); - + if (Position > 0 && beaconCallbackWriter != null && ownerThread == Thread.CurrentThread) { byte[] data = new byte[Position]; Seek(0, SeekOrigin.Begin); Read(data, 0, data.Length); - beaconCallbackWriter(OutputTypes.CALLBACK_OUTPUT_UTF8, data, data.Length); + + bool currentSeekHit = false; + int stringIndex = 0; + for (int index = 0; index < (data.Length - 2); index++) + { + if (currentSeekHit != true) + { + /* + * Logical to check for sequential null values that go beyond our buffer length. + * Due to representation of null bytes in a UTF-8 callback, this can otherwise lead to "phantom" output which is the flushing of an allocated MemoryStream object. + * + * As such, a "beacon operator" may receive multiple instances of "received output" from the callback. + * We implement our own needle here, assuming in both UTF-8 and UTF-16 arrays that sequential null bytes represent the termination of any string. + * + * This prevents both pollution of output, and cleanliness of logs. + * All instances of the base class are still properly flushed at the end of our operations with a call the the base class' Dispose method. + */ + if ((data[index] == (byte)0x00) && (data[index+1] == (byte)0x00)) + { + stringIndex = index + 1; + currentSeekHit = true; + } + else + { + stringIndex += 1; + } + } + } + + if (currentSeekHit != true) + { + beaconCallbackWriter(OutputTypes.CALLBACK_OUTPUT_UTF8, data, data.Length); + } + else + { + if (stringIndex >= 2) + { + beaconCallbackWriter(OutputTypes.CALLBACK_OUTPUT_UTF8, data, stringIndex); + } + } + + // Regardless of output, seek to the beginning of the MemoryStream Seek(0, SeekOrigin.Begin); } } diff --git a/managed/BOFNET/BeaconJob.cs b/managed/BOFNET/BeaconJob.cs index ca32289..15fa4aa 100644 --- a/managed/BOFNET/BeaconJob.cs +++ b/managed/BOFNET/BeaconJob.cs @@ -1,5 +1,7 @@  using System; +using System.IO; +using System.Text; using System.Threading; namespace BOFNET { @@ -11,8 +13,16 @@ public class BeaconJob { public BeaconJobWriter BeaconConsole { get; private set; } - public BeaconJob(BeaconObject bo, string[] args, BeaconJobWriter beaconTaskWriter) { + public string StandardAssembly { get; private set; } + string StandardAssemblyEntryPointType { get; set; } + + volatile static ProducerConsumerStream MemoryStreamPC = new ProducerConsumerStream(); + + volatile static bool RunThread; + + public BeaconJob(BeaconObject bo, string[] args, BeaconJobWriter beaconTaskWriter, string standardAssembly = null) { + StandardAssembly = standardAssembly; BeaconConsole = beaconTaskWriter; BeaconObject = bo; Thread = new Thread(new ParameterizedThreadStart(this.DoTask)); @@ -22,15 +32,145 @@ public BeaconJob(BeaconObject bo, string[] args, BeaconJobWriter beaconTaskWrite private void DoTask(object args) { try { if (args is string[] stringArgs) { - BeaconObject.Go(stringArgs); + if (StandardAssembly != null) { + // Redirect stdout to MemoryStream + StreamWriter memoryStreamWriter = new StreamWriter(MemoryStreamPC); + memoryStreamWriter.AutoFlush = true; + Console.SetOut(memoryStreamWriter); + Console.SetError(memoryStreamWriter); + + // Start thread to check MemoryStream to send data to Beacon + RunThread = true; + Thread runtimeWriteLine = new Thread(() => RuntimeWriteLine(BeaconConsole)); + runtimeWriteLine.Start(); + + // Run main program passing original arguments + object[] mainArguments = new[] { stringArgs }; + StandardAssemblyEntryPointType = Runtime.LoadedAssemblies[StandardAssembly].Assembly.EntryPoint.DeclaringType.ToString(); + Runtime.LoadedAssemblies[StandardAssembly].Assembly.EntryPoint.Invoke(null, mainArguments); + + // Trigger safe exit of thread, ensuring MemoryStream is emptied too + RunThread = false; + runtimeWriteLine.Join(); + } + else { + BeaconObject.Go(stringArgs); + } } - }catch(Exception e) { + } catch(Exception e) { BeaconConsole.WriteLine($"Job execution failed with exception:\n{e}"); } } public override string ToString() { - return $"Type: {BeaconObject.GetType().Name}, Id: {Thread.ManagedThreadId}, Active: {Thread.IsAlive}, Console Data: {BeaconConsole.HasData}"; + return $"Type: {(StandardAssembly != null ? StandardAssemblyEntryPointType : BeaconObject.GetType().FullName).ToString()}, Id: {Thread.ManagedThreadId}, Standard Assembly: {(StandardAssembly != null ? true : false).ToString()}, Active: {Thread.IsAlive}, Console Data: {BeaconConsole.HasData}"; + } + + public static void RuntimeWriteLine(BeaconJobWriter beaconconsole) { + bool LastCheck = false; + while (RunThread == true || LastCheck == true) { + int offsetWritten = 0; + int currentCycleMemoryStreamLength = Convert.ToInt32(MemoryStreamPC.Length); + if (currentCycleMemoryStreamLength > offsetWritten) { + try { + var byteArrayRaw = new byte[currentCycleMemoryStreamLength]; + int count = MemoryStreamPC.Read(byteArrayRaw, offsetWritten, currentCycleMemoryStreamLength); + + 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 = currentCycleMemoryStreamLength; + } + if (lastNewLine > 0) { + var byteArrayToLastNewline = new byte[lastNewLine]; + Buffer.BlockCopy(byteArrayRaw, 0, byteArrayToLastNewline, 0, lastNewLine); + beaconconsole.WriteLine(Encoding.ASCII.GetString(byteArrayToLastNewline)); + offsetWritten = offsetWritten + lastNewLine; + } + } + } + catch (Exception e) { + beaconconsole.WriteLine($"[!] BOFNET threw an exception when returning captured console output: {e}"); + } + } + 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; + } + } } } -} +} \ No newline at end of file diff --git a/managed/BOFNET/Bofs/ExecuteAssembly.cs b/managed/BOFNET/Bofs/ExecuteAssembly.cs new file mode 100644 index 0000000..29c9dd7 --- /dev/null +++ b/managed/BOFNET/Bofs/ExecuteAssembly.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace BOFNET.Bofs { + public class ExecuteAssembly : BeaconObject { + public ExecuteAssembly(BeaconApi api) : base(api) { } + + public override void Go(string[] args) { + if (args.Length == 0) { + BeaconConsole.WriteLine("[!] Cannot continue execution, no .NET assembly specified to run"); + return; + } + + if (Runtime.Jobs.Values.Where(p => p.StandardAssembly != null && p.Thread.IsAlive).Count() > 0) { + BeaconConsole.WriteLine("[!] Cannot continue execution, unable to execute while a bofnet_job_assembly instance is active"); + return; + } + + if (Runtime.LoadedAssemblies.ContainsKey(args[0])) { + // Redirect stdout to MemoryStream + var memStream = new MemoryStream(); + var memStreamWriter = new StreamWriter(memStream); + memStreamWriter.AutoFlush = true; + Console.SetOut(memStreamWriter); + Console.SetError(memStreamWriter); + + // Call entry point + Assembly assembly = Runtime.LoadedAssemblies[args[0]].Assembly; + object[] mainArguments = new[] { args.Skip(1).ToArray() }; + object execute = assembly.EntryPoint.Invoke(null, mainArguments); + + // Write MemoryStream to Beacon output + BeaconConsole.WriteLine(Encoding.ASCII.GetString(memStream.ToArray())); + } + else { + BeaconConsole.WriteLine("[!] Cannot continue execution, specified .NET assembly not loaded"); + } + } + } +} \ No newline at end of file diff --git a/managed/BOFNET/Bofs/Initializer.cs b/managed/BOFNET/Bofs/Initializer.cs index 0a4710c..7759893 100644 --- a/managed/BOFNET/Bofs/Initializer.cs +++ b/managed/BOFNET/Bofs/Initializer.cs @@ -8,7 +8,7 @@ public Initializer(BeaconApi api) : base(api) { } public override void Go(byte[] assemblyData) { Runtime.RegisterRuntimeAssembly(assemblyData); - BeaconConsole.WriteLine($"[+] BOFNET Runtime Initalized, assembly size {assemblyData.Length}, .NET Runtime Version: {Environment.Version} in AppDomain {AppDomain.CurrentDomain.FriendlyName}"); + BeaconConsole.WriteLine($"[+] BOFNET Runtime Initalized, assembly size {assemblyData.Length}, .NET Runtime Version: {Environment.Version} in AppDomain {AppDomain.CurrentDomain.FriendlyName}"); } } } diff --git a/managed/BOFNET/Bofs/Jobs/JobRunnerAssembly.cs b/managed/BOFNET/Bofs/Jobs/JobRunnerAssembly.cs new file mode 100644 index 0000000..7f7175b --- /dev/null +++ b/managed/BOFNET/Bofs/Jobs/JobRunnerAssembly.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace BOFNET.Bofs.Jobs { + public class JobRunnerAssembly : BeaconObject { + public JobRunnerAssembly(BeaconApi api) : base(api) { + } + + public AppDomain LoadAssemblyInAppDomain(string appDomain, byte[] data, int len) { + return null; + } + + public override void Go(string[] args) { + if (args.Length == 0) { + BeaconConsole.WriteLine("[!] Cannot continue execution, no .NET assembly specified to run"); + return; + } + + if (Runtime.Jobs.Values.Where(p => p.StandardAssembly != null && p.Thread.IsAlive).Count() > 0) { + BeaconConsole.WriteLine("[!] Cannot continue execution, multiple instances of bofnet_job_assembly cannot run in parallel"); + return; + } + + if (Runtime.LoadedAssemblies.ContainsKey(args[0])) { + BeaconJobWriter btw = new BeaconJobWriter(); + BeaconJob beaconJob = new BeaconJob(null, args.Skip(1).ToArray(), btw, args[0]); + + Runtime.Jobs[beaconJob.Thread.ManagedThreadId] = beaconJob; + BeaconConsole.WriteLine($"[+] Started Task {args[0]} with job id {beaconJob.Thread.ManagedThreadId}"); + } + else { + BeaconConsole.WriteLine("[!] Cannot continue execution, specified .NET assembly not loaded"); + } + } + } +} diff --git a/managed/BOFNET/Bofs/ListAssemblies.cs b/managed/BOFNET/Bofs/ListAssemblies.cs index 2183bf7..3193718 100644 --- a/managed/BOFNET/Bofs/ListAssemblies.cs +++ b/managed/BOFNET/Bofs/ListAssemblies.cs @@ -11,7 +11,7 @@ public ListAssemblies(BeaconApi api) : base(api) {} public override void Go(string[] _) { foreach(KeyValuePair assembly in Runtime.LoadedAssemblies) { BeaconConsole.WriteLine($"{assembly.Key}: {assembly.Value.Assembly.FullName}"); - } + } } } } diff --git a/managed/BOFNET/Bofs/PatchEnvironmentExit.cs b/managed/BOFNET/Bofs/PatchEnvironmentExit.cs new file mode 100644 index 0000000..ab9873a --- /dev/null +++ b/managed/BOFNET/Bofs/PatchEnvironmentExit.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace BOFNET.Bofs { + public class PatchEnvironmentExit : BeaconObject { + public PatchEnvironmentExit(BeaconApi api) : base(api) { } + public override void Go(string[] args) { + if (Runtime.PatchEnvironmentExit()) { + BeaconConsole.WriteLine($"[+] Environment.Exit() patched successfully"); + } + else { + BeaconConsole.WriteLine($"[!] Environment.Exit() patched failed"); + } + } + } +} diff --git a/managed/BOFNET/Runtime.cs b/managed/BOFNET/Runtime.cs index 5ad1a91..91fd368 100644 --- a/managed/BOFNET/Runtime.cs +++ b/managed/BOFNET/Runtime.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; @@ -23,10 +24,45 @@ public class AssemblyInfo { static bool firstInit = true; + [StructLayout(LayoutKind.Sequential)] + public struct MEMORY_BASIC_INFORMATION + { + public IntPtr BaseAddress; + public IntPtr AllocationBase; + public uint AllocationProtect; + public IntPtr RegionSize; + public uint State; + public uint Protect; + public uint Type; + } + + public enum AllocationProtect : uint + { + PAGE_EXECUTE = 0x00000010, + PAGE_EXECUTE_READ = 0x00000020, + PAGE_EXECUTE_READWRITE = 0x00000040, + PAGE_EXECUTE_WRITECOPY = 0x00000080, + PAGE_NOACCESS = 0x00000001, + PAGE_READONLY = 0x00000002, + PAGE_READWRITE = 0x00000004, + PAGE_WRITECOPY = 0x00000008, + PAGE_GUARD = 0x00000100, + PAGE_NOCACHE = 0x00000200, + PAGE_WRITECOMBINE = 0x00000400 + } + [DllImport("shell32.dll", SetLastError = true)] static extern IntPtr CommandLineToArgvW( [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, + UIntPtr dwSize, uint flNewProtect, out uint lpflOldProtect); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern int VirtualQueryEx(IntPtr hProcess, IntPtr lpAddress, + out MEMORY_BASIC_INFORMATION lpBuffer, uint dwLength); + private static Type FindType(string name) { //Try to get based on fully qualified name first @@ -80,11 +116,10 @@ public static Assembly LoadAssembly(byte[] assemblyData) { public static BeaconObject CreateBeaconObject(string bofName, BeaconOutputWriter bow, InitialiseChildBOFNETAppDomain initialiseChildBOFNETAppDomain, BeaconUseToken beaconUseToken, BeaconRevertToken beaconRevertToken, BeaconCallbackWriter beaconCallbackWriter) { Type bofType = FindType(bofName); - - if (bofType == null) { + if (bofType == null) + { throw new TypeLoadException($"[!] Failed to find type {bofName} within BOFNET AppDomain, have you loaded the containing assembly yet?"); } - BeaconObject bo = (BeaconObject)Activator.CreateInstance(bofType, new object[] { new DefaultBeaconApi(bow, initialiseChildBOFNETAppDomain, beaconUseToken, beaconRevertToken, beaconCallbackWriter) }); return bo; } @@ -173,5 +208,29 @@ public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEvent return null; } } + + public static bool PatchEnvironmentExit() + { + // Credit Peter Winter-Smith @ MDSec: https://www.mdsec.co.uk/2020/08/massaging-your-clr-preventing-environment-exit-in-in-process-net-assemblies/ + var methods = new List(typeof(Environment).GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)); + var exitMethod = methods.Find((MethodInfo mi) => mi.Name == "Exit"); + RuntimeHelpers.PrepareMethod(exitMethod.MethodHandle); + var exitMethodPtr = exitMethod.MethodHandle.GetFunctionPointer(); + unsafe { + IntPtr target = exitMethod.MethodHandle.GetFunctionPointer(); + MEMORY_BASIC_INFORMATION mbi; + if (VirtualQueryEx((IntPtr)(-1), target, out mbi, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) != 0) { + if (mbi.Protect == (uint)AllocationProtect.PAGE_EXECUTE_READ) { + uint flOldProtect; + if (VirtualProtectEx((IntPtr)(-1), (IntPtr)target, (UIntPtr)1, (uint)AllocationProtect.PAGE_EXECUTE_READWRITE, out flOldProtect)) { + *(byte*)target = 0xc3; // ret + VirtualProtectEx((IntPtr)(-1), (IntPtr)target, (UIntPtr)1, flOldProtect, out flOldProtect); + return true; + } + } + } + } + return false; + } } }