Add backend support for shellcode generation using Donut

dependabot/npm_and_yarn/Src/WebController/UI/websocket-extensions-0.1.4
Grzegorz Rychlik 2019-11-25 16:30:33 +01:00
parent 08c3087aa7
commit 69fa74bae6
10 changed files with 389 additions and 22 deletions

View File

@ -0,0 +1,209 @@
using System;
using System.Runtime.InteropServices;
// https://github.com/TheWover/donut/blob/master/lib/donut.h
namespace MWR.C3.WebController.Comms
{
public class DonutLibrary
{
public enum ErrorCode
{
SUCCESS = 0,
FILE_NOT_FOUND = 1,
FILE_EMPTY = 2,
FILE_ACCESS = 3,
FILE_INVALID = 4,
NET_PARAMS = 5,
NO_MEMORY = 6,
INVALID_ARCH = 7,
INVALID_URL = 8,
URL_LENGTH = 9,
INVALID_PARAMETER = 10,
RANDOM = 11,
DLL_FUNCTION = 12,
ARCH_MISMATCH = 13,
DLL_PARAM = 14,
BYPASS_INVALID = 15,
NORELOC = 16,
INVALID_ENCODING = 17,
INVALID_ENGINE = 18,
COMPRESSION = 19,
INVALID_ENTROPY = 20,
}
// target architecture
public enum Architecture
{
ANY = -1, // just for vbs,js and xsl files
X86 = 1, // x86
X64 = 2, // AMD64
X84 = 3, // AMD64 + x86
}
// module type
public enum ModuleType
{
NET_DLL = 1, // .NET DLL. Requires class and method
NET_EXE = 2, // .NET EXE. Executes Main if no class and method provided
DLL = 3, // Unmanaged DLL, function is optional
EXE = 4, // Unmanaged EXE
VBS = 5, // VBScript
JS = 6, // JavaScript or JScript
}
// format type
public enum OutputFormat
{
BINARY = 1,
BASE64 = 2,
RUBY = 3,
C = 4,
PYTHON = 5,
POWERSHELL = 6,
CSHARP = 7,
HEX = 8,
}
// compression engine
public enum ComperssionEngine
{
NONE = 1,
LZNT1 = 2, // COMPRESSION_FORMAT_LZNT1
XPRESS = 3, // COMPRESSION_FORMAT_XPRESS
XPRESS_HUFF = 4, // COMPRESSION_FORMAT_XPRESS_HUFF
}
// entropy level
public enum EntropyLevel
{
NONE = 1, // don't use any entropy
RANDOM = 2, // use random names
DEFAULT = 3, // use random names + symmetric encryption
}
// misc options
public enum ExitOpt
{
EXIT_THREAD = 1, // return to the caller which calls RtlExitUserThread
EXIT_PROCESS = 2, // call RtlExitUserProcess to terminate host process
}
// instance type
public enum InstanceType
{
PIC = 1, // Self-contained
URL = 2, // Download from remote server
}
// AMSI/WLDP options
public enum AmsiWldpBypass
{
NONE = 1, // Disables bypassing AMSI/WDLP
ABORT = 2, // If bypassing AMSI/WLDP fails, the loader stops running
CONTINUE = 3, // If bypassing AMSI/WLDP fails, the loader continues running
}
public static class Constants
{
public const int DONUT_MAX_NAME = 256; // maximum length of string for domain, class, method and parameter names
public const int DONUT_MAX_DLL = 8; // maximum number of DLL supported by instance
public const int DONUT_MAX_URL = 256;
public const int DONUT_MAX_MODNAME = 8;
public const int DONUT_SIG_LEN = 8; // 64-bit string to verify decryption ok
public const int DONUT_VER_LEN = 32;
public const int DONUT_DOMAIN_LEN = 8;
}
[StructLayout(LayoutKind.Sequential, Pack = 0)]
public struct DonutConfig
{
public int arch;
public int bypass;
public int compress; // engine to use when compressing file via RtlCompressBuffer
public int entropy; // entropy/encryption level
public int fork; // fork/create a new thread for the loader
public int format; // output format for loader
public int exit_opt; // return to caller or invoke RtlExitUserProcess to terminate the host process
public int thread; // run entrypoint of unmanaged EXE as a thread. attempts to intercept calls to exit-related API
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string input;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string output;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string runtime;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string domain;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string cls;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string method;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string param;
public int ansi;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string url;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = Constants.DONUT_MAX_NAME)]
public string modname;
public int mod_type;
public int mod_len;
public IntPtr mod; // points to DONUT_MODULE
// DONUT_INSTANCE
public int inst_type; // DONUT_INSTANCE_PIC or DONUT_INSTANCE_URL
public int inst_len; // size of DONUT_INSTANCE
public IntPtr inst; // points to DONUT_INSTANCE
public int pic_len; // size of loader/shellcode
public IntPtr pic; // points to loader/shellcode
}
public static byte[] GenerateShellcode(DonutConfig config)
{
var buffer = DonutCreate(config);
DonutDelete(config);
return buffer;
}
// Use DllImport to import the Win32 MessageBox function.
[DllImport("donut")]
private static extern int DonutCreate(ref DonutConfig donutConfig);
public class DonutException : Exception
{
public DonutException(string message) : base(message) { }
}
[DllImport("donut")]
private static extern int DonutDelete(ref DonutConfig donutConfig);
[DllImport("donut")]
private static extern IntPtr DonutError(int errorcode);
static string GetDonutError(ErrorCode errorcode)
{
IntPtr ptr = DonutError((int)errorcode);
return Marshal.PtrToStringAnsi(ptr);
}
private static byte[] DonutCreate(DonutConfig config)
{
var createErrorCode = (ErrorCode)DonutCreate(ref config);
if (createErrorCode != ErrorCode.SUCCESS)
{
var errorMessage = GetDonutError(createErrorCode);
throw new DonutException($"Donut error: {errorMessage}");
}
var buffer = new byte[config.pic_len];
Marshal.Copy(config.pic, buffer, 0, config.pic_len);
return buffer;
}
private static void DonutDelete(DonutConfig config)
{
var delErrorCode = (ErrorCode)DonutDelete(ref config);
if (delErrorCode != ErrorCode.SUCCESS)
{
var errorMessage = GetDonutError(delErrorCode);
throw new DonutException($"Donut error: {errorMessage}");
}
}
}
}

View File

@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Options;
using MWR.C3.WebController.Models;
using MWR.C3.WebController.RandomExtentions;
namespace MWR.C3.WebController.Comms
{
public class DonutService : IDonutService
{
public DonutService(IOptions<DonutServiceOptions> options)
{
m_Options = options.Value;
m_TempPath = options.Value.Tempdir ??
Environment.GetEnvironmentVariable("TEMPDIR") ??
Environment.GetEnvironmentVariable("TEMP") ??
".";
}
private DonutServiceOptions m_Options;
private string m_TempPath;
public byte[] GenerateShellcode(byte[] payload, DonutRequest request, Build.Architecture arch, Build.BinaryType binaryType)
{
if (!m_Options.Enable)
throw new Exception("Donut service disabled");
// donut api requires files
var rand = new Random();
var tmpFilename = rand.NextString(16);
var tmpPayloadFile = Path.Combine(m_TempPath, tmpFilename + ".exe");
var tmpDonutFile = Path.Combine(m_TempPath, tmpFilename + ".donut");
WriteToFile(payload, tmpPayloadFile);
var config = new DonutLibrary.DonutConfig
{
arch = (int)(arch == Build.Architecture.X64 ? DonutLibrary.Architecture.X64 : DonutLibrary.Architecture.X86),
mod_type = (int)(binaryType == Build.BinaryType.Exe ? DonutLibrary.ModuleType.EXE : DonutLibrary.ModuleType.DLL),
format = (int)request.format,
compress = (int)request.compress,
entropy = (int)request.entropy,
inst_type = (int)DonutLibrary.InstanceType.PIC,
input = tmpPayloadFile,
output = tmpDonutFile,
bypass = (int)request.bypass,
inst_len = 0
};
try
{
DonutLibrary.GenerateShellcode(config);
return File.ReadAllBytes(tmpDonutFile);
}
finally
{
CleanupFile(tmpPayloadFile);
CleanupFile(tmpDonutFile);
}
}
private static void WriteToFile(byte[] payload, string filename)
{
using (var binWriter = new BinaryWriter(File.Open(filename, FileMode.Create)))
{
binWriter.Write(payload);
}
}
private static void CleanupFile(string filename)
{
if (File.Exists(filename))
File.Delete(filename);
}
}
}

View File

@ -0,0 +1,16 @@
using MWR.C3.WebController.Models;
using System.Collections.Generic;
namespace MWR.C3.WebController.Comms
{
public interface IDonutService
{
byte[] GenerateShellcode(byte[] payload, DonutRequest request, Build.Architecture arch, Build.BinaryType binaryType);
}
public class DonutServiceOptions
{
public bool Enable { get; set; }
public string Tempdir { get; set; }
}
}

View File

@ -47,8 +47,11 @@ namespace MWR.C3.WebController.Controllers
}
[HttpPost("customize")]
public async Task<IActionResult> Customize([FromBody]RelayBuildRequest request, [FromServices] ICustomizer customizer)
public async Task<IActionResult> Customize([FromBody]RelayBuildRequest request, [FromServices] ICustomizer customizer, [FromServices] IDonutService donut)
{
if (request is null)
return BadRequest("Failed to read RelayBuildRequest");
RelayBuild newBuild = null;
System.Action cleanupBuild = () =>
{
@ -73,8 +76,10 @@ namespace MWR.C3.WebController.Controllers
context.SaveChanges();
}
output = await customizer.CustomizeNodeRelay(newBuild);
if (request.Donut != null)
output = donut.GenerateShellcode(output, request.Donut, request.Architecture, request.Type);
var buildName = String.IsNullOrEmpty(newBuild.Name) ? "" : $"_{newBuild.Name}";
return File(output, "application/octet-stream", $"Relay_{newBuild.Arch.ToString().ToLower()}_{newBuild.BuildId:x}{buildName}.{newBuild.Type.ToString().ToLower()}");
return File(output, "application/octet-stream", $"Relay_{newBuild.Arch.ToString().ToLower()}_{newBuild.BuildId:x}{buildName}.{GetBuildExtention(request)}");
}
catch (TimeoutException e)
{
@ -95,7 +100,7 @@ namespace MWR.C3.WebController.Controllers
catch (Exception e)
{
cleanupBuild();
return StatusCode(StatusCodes.Status500InternalServerError , $"Unknown error. {e.Message}");
return StatusCode(StatusCodes.Status500InternalServerError, $"Unknown error. {e.Message}");
}
}
@ -117,5 +122,22 @@ namespace MWR.C3.WebController.Controllers
ParentGatwayAgentId = gatewayAgentId,
};
}
private string GetBuildExtention(RelayBuildRequest request)
{
switch (request.Donut?.format)
{
case DonutLibrary.OutputFormat.BINARY: return "bin";
case DonutLibrary.OutputFormat.BASE64: return "b64";
case DonutLibrary.OutputFormat.RUBY: return "rb";
case DonutLibrary.OutputFormat.C: return "c";
case DonutLibrary.OutputFormat.PYTHON: return "py";
case DonutLibrary.OutputFormat.POWERSHELL: return "ps1";
case DonutLibrary.OutputFormat.CSHARP: return "cs";
case DonutLibrary.OutputFormat.HEX: return "hex";
case null: return request.Type.ToString().ToLower();
default: throw new ArgumentException("Unrecognized output format");
}
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Linq;
namespace MWR.C3.WebController.RandomExtentions
{
@ -24,5 +25,12 @@ namespace MWR.C3.WebController.RandomExtentions
self.NextBytes(buffer);
return BitConverter.ToUInt64(buffer);
}
public static string NextString(this Random self, int length)
{
const string chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return new string(Enumerable.Repeat(chars, length)
.Select(s => s[self.Next(s.Length)]).ToArray());
}
}
}

View File

@ -22,5 +22,34 @@ namespace MWR.C3.WebController.Models
public string Name { get; set; }
public JArray StartupCommands { get; set; }
public DonutRequest Donut { get; set; }
}
public class DonutRequest
{
public DonutRequest()
{
format = DonutLibrary.OutputFormat.BINARY;
compress = DonutLibrary.ComperssionEngine.NONE;
entropy = DonutLibrary.EntropyLevel.DEFAULT;
exitOpt = DonutLibrary.ExitOpt.EXIT_THREAD;
bypass = DonutLibrary.AmsiWldpBypass.NONE;
}
[JsonConverter(typeof(StringEnumConverter))]
public DonutLibrary.OutputFormat format { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DonutLibrary.ComperssionEngine compress { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DonutLibrary.EntropyLevel entropy { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DonutLibrary.ExitOpt exitOpt { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public DonutLibrary.AmsiWldpBypass bypass { get; set; }
}
}

View File

@ -11,7 +11,6 @@
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}

View File

@ -35,6 +35,8 @@ namespace MWR.C3.WebController
{
c.SwaggerDoc("v1", new Info { Title = "C3 API", Version = "v1" });
});
services.Configure<DonutServiceOptions>(Configuration.GetSection("Donut"));
services.AddTransient<IDonutService, DonutService>();
services.AddTransient<ICustomizer, Customizer>();
services.AddScoped<GatewayResponseProcessor>();
services.AddDbContext<C3WebAPIContext>(options => options.UseSqlite("Data Source=C3API.db"));

View File

@ -45,6 +45,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DonutRuntimes" Version="0.9.3-7ae213b.1" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.0" />

View File

@ -1,22 +1,25 @@
{
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
"Logging": {
"IncludeScopes": false,
"Debug": {
"LogLevel": {
"Default": "Warning"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
"Customizer": {
"payloadTemplateDir": "../Bin/"
},
"ApiBridge": {
"Host": "127.0.0.1",
"Port": 2323
},
"Donut": {
"enable": true
}
},
"Customizer": {
"payloadTemplateDir": "../Bin/"
},
"ApiBridge": {
"Host": "127.0.0.1",
"Port": 2323
}
}