diff --git a/data/headers/windows/c_payload_util/beacon.h b/data/headers/windows/c_payload_util/beacon.h new file mode 100644 index 000000000000..d656a3d29e76 --- /dev/null +++ b/data/headers/windows/c_payload_util/beacon.h @@ -0,0 +1,69 @@ +/* + * Beacon Object Files (BOF) + * ------------------------- + * A Beacon Object File is a light-weight post exploitation tool that runs + * with Beacon's inline-execute command. + * + * Additional BOF resources are available here: + * - https://github.com/Cobalt-Strike/bof_template + * + * Cobalt Strike 4.x + * ChangeLog: + * 1/25/2022: updated for 4.5 + */ + +/* data API */ +typedef struct { + char * original; /* the original buffer [so we can free it] */ + char * buffer; /* current pointer into our buffer */ + int length; /* remaining length of data */ + int size; /* total size of this buffer */ +} datap; + +DECLSPEC_IMPORT void BeaconDataParse(datap * parser, char * buffer, int size); +DECLSPEC_IMPORT char * BeaconDataPtr(datap * parser, int size); +DECLSPEC_IMPORT int BeaconDataInt(datap * parser); +DECLSPEC_IMPORT short BeaconDataShort(datap * parser); +DECLSPEC_IMPORT int BeaconDataLength(datap * parser); +DECLSPEC_IMPORT char * BeaconDataExtract(datap * parser, int * size); + +/* format API */ +typedef struct { + char * original; /* the original buffer [so we can free it] */ + char * buffer; /* current pointer into our buffer */ + int length; /* remaining length of data */ + int size; /* total size of this buffer */ +} formatp; + +DECLSPEC_IMPORT void BeaconFormatAlloc(formatp * format, int maxsz); +DECLSPEC_IMPORT void BeaconFormatReset(formatp * format); +DECLSPEC_IMPORT void BeaconFormatAppend(formatp * format, char * text, int len); +DECLSPEC_IMPORT void BeaconFormatPrintf(formatp * format, char * fmt, ...); +DECLSPEC_IMPORT char * BeaconFormatToString(formatp * format, int * size); +DECLSPEC_IMPORT void BeaconFormatFree(formatp * format); +DECLSPEC_IMPORT void BeaconFormatInt(formatp * format, int value); + +/* Output Functions */ +#define CALLBACK_OUTPUT 0x0 +#define CALLBACK_OUTPUT_OEM 0x1e +#define CALLBACK_OUTPUT_UTF8 0x20 +#define CALLBACK_ERROR 0x0d + +DECLSPEC_IMPORT void BeaconOutput(int type, char * data, int len); +DECLSPEC_IMPORT void BeaconPrintf(int type, char * fmt, ...); + + +/* Token Functions */ +DECLSPEC_IMPORT BOOL BeaconUseToken(HANDLE token); +DECLSPEC_IMPORT void BeaconRevertToken(); +DECLSPEC_IMPORT BOOL BeaconIsAdmin(); + +/* Spawn+Inject Functions */ +DECLSPEC_IMPORT void BeaconGetSpawnTo(BOOL x86, char * buffer, int length); +DECLSPEC_IMPORT void BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len); +DECLSPEC_IMPORT void BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len); +DECLSPEC_IMPORT BOOL BeaconSpawnTemporaryProcess(BOOL x86, BOOL ignoreToken, STARTUPINFO * si, PROCESS_INFORMATION * pInfo); +DECLSPEC_IMPORT void BeaconCleanupProcess(PROCESS_INFORMATION * pInfo); + +/* Utility Functions */ +DECLSPEC_IMPORT BOOL toWideChar(char * src, wchar_t * dst, int max); diff --git a/lib/rex/post/meterpreter/extensions/bofloader/bofloader.rb b/lib/rex/post/meterpreter/extensions/bofloader/bofloader.rb index bdacbc321c65..5855cebd0e92 100644 --- a/lib/rex/post/meterpreter/extensions/bofloader/bofloader.rb +++ b/lib/rex/post/meterpreter/extensions/bofloader/bofloader.rb @@ -6,162 +6,166 @@ require 'set' module Rex -module Post -module Meterpreter -module Extensions -module Bofloader - -### -# -# Bofloader extension - Executes a beacon object file in -# the current meterpreter session. -# -# Kevin Haubris (@kev169) -# Kevin Clark (@GuhnooPlusLinux) -# TrustedSec (@TrustedSec) -# -### - -class BofPack - # Code referenced from: https://github.com/trustedsec/COFFLoader/blob/main/beacon_generate.py - # Emulates the native Cobalt Strike bof_pack() function. - # Documented here: https://hstechdocs.helpsystems.com/manuals/cobaltstrike/current/userguide/content/topics_aggressor-scripts/as-resources_functions.htm#bof_pack - # - # Type Description Unpack With (C) - # --------|---------------------------------------|------------------------------ - # b | binary data | BeaconDataExtract - # i | 4-byte integer | BeaconDataInt - # s | 2-byte short integer | BeaconDataShort - # z | zero-terminated+encoded string | BeaconDataExtract - # Z | zero-terminated wide-char string | (wchar_t *)BeaconDataExtract - - def initialize() - @buffer = '' - @size = 0 - end - - def addshort(short) - @buffer << [short.to_i].pack(" 'bofloader', + 'ext' => self + }, + ] + ) + end + + def execute(bof_data, args_format: nil, args: nil, entry: 'go') + request = Packet.create_request(COMMAND_ID_BOFLOADER_EXECUTE) + + # Pack up beacon object file data and arguments into one single binary blob + # Hardcode the entrypoint to "go" (CobaltStrike approved) + bof = BofPack.new + packed_args = bof.bof_pack(args_format, args) + + # Send the meterpreter TLV packet and get the output back + request.add_tlv(TLV_TYPE_BOFLOADER_EXECUTE_BUFFER, bof_data) + request.add_tlv(TLV_TYPE_BOFLOADER_EXECUTE_BUFFER_ENTRY, entry) + request.add_tlv(TLV_TYPE_BOFLOADER_EXECUTE_ARGUMENTS, packed_args) + response = client.send_request(request) + return response.get_tlv_value(TLV_TYPE_BOFLOADER_EXECUTE_RESULT) + end + + end + end end end - - # return the packed bof_string - return finalize_buffer() end - - def coff_pack_pack(entrypoint, coff_data, argument_data) - # Create packed data containing: - # functionname | coff_data | args_data - # which can be passed directly to the LoadAndRun() function - fmt_pack = "zbb" # string, binary, binary - return bof_pack(fmt_pack, [entrypoint, coff_data, argument_data]) - end - end - -class Bofloader < Extension - - def self.extension_id - EXTENSION_ID_BOFLOADER - end - - # Typical extension initialization routine. - # - # @param client (see Extension#initialize) - def initialize(client) - super(client, 'bofloader') - - client.register_extension_aliases( - [ - { - 'name' => 'bofloader', - 'ext' => self - }, - ]) - - end - - def execute(bof_data, args_format: nil, args: nil, entry: 'go') - request = Packet.create_request(COMMAND_ID_BOFLOADER_EXECUTE) - - # Pack up beacon object file data and arguments into one single binary blob - # Hardcode the entrypoint to "go" (CobaltStrike approved) - bof = BofPack.new - packed_args = bof.bof_pack(args_format, args) - packed_coff_data = bof.coff_pack_pack(entry, bof_data, packed_args) - - # Send the meterpreter TLV packet and get the output back - request.add_tlv(TLV_TYPE_BOFLOADER_EXECUTE_BUFFER, packed_coff_data) - response = client.send_request(request) - return response.get_tlv_value(TLV_TYPE_BOFLOADER_EXECUTE_RESULT) - end - -end - -end; end; end; end; end diff --git a/lib/rex/post/meterpreter/extensions/bofloader/command_ids.rb b/lib/rex/post/meterpreter/extensions/bofloader/command_ids.rb index 031c0c4e3395..c14a49812de1 100644 --- a/lib/rex/post/meterpreter/extensions/bofloader/command_ids.rb +++ b/lib/rex/post/meterpreter/extensions/bofloader/command_ids.rb @@ -1,18 +1,17 @@ # -*- coding: binary -*- -module Rex -module Post -module Meterpreter -module Extensions -module Bofloader - -# ID for the extension (needs to be a multiple of 1000) -EXTENSION_ID_BOFLOADER = 18000 -# Associated command ids -COMMAND_ID_BOFLOADER_EXECUTE = EXTENSION_ID_BOFLOADER + 1 +module Rex + module Post + module Meterpreter + module Extensions + module Bofloader + # ID for the extension (needs to be a multiple of 1000) + EXTENSION_ID_BOFLOADER = 18000 -end -end -end -end + # Associated command ids + COMMAND_ID_BOFLOADER_EXECUTE = EXTENSION_ID_BOFLOADER + 1 + end + end + end + end end diff --git a/lib/rex/post/meterpreter/extensions/bofloader/tlv.rb b/lib/rex/post/meterpreter/extensions/bofloader/tlv.rb index f4763631fd4a..0fd47feed3f0 100644 --- a/lib/rex/post/meterpreter/extensions/bofloader/tlv.rb +++ b/lib/rex/post/meterpreter/extensions/bofloader/tlv.rb @@ -1,15 +1,16 @@ # -*- coding: binary -*- -module Rex -module Post -module Meterpreter -module Extensions -module Bofloader - -TLV_TYPE_BOFLOADER_EXECUTE_BUFFER = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 100) -TLV_TYPE_BOFLOADER_EXECUTE_RESULT = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 101) -end -end -end -end +module Rex + module Post + module Meterpreter + module Extensions + module Bofloader + TLV_TYPE_BOFLOADER_EXECUTE_BUFFER = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 100) + TLV_TYPE_BOFLOADER_EXECUTE_BUFFER_ENTRY = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 101) + TLV_TYPE_BOFLOADER_EXECUTE_ARGUMENTS = TLV_META_TYPE_RAW | (TLV_EXTENSIONS + 102) + TLV_TYPE_BOFLOADER_EXECUTE_RESULT = TLV_META_TYPE_STRING | (TLV_EXTENSIONS + 103) + end + end + end + end end diff --git a/lib/rex/post/meterpreter/ui/console/command_dispatcher/bofloader.rb b/lib/rex/post/meterpreter/ui/console/command_dispatcher/bofloader.rb index bdbe924f0d18..e9ca6d45bd40 100644 --- a/lib/rex/post/meterpreter/ui/console/command_dispatcher/bofloader.rb +++ b/lib/rex/post/meterpreter/ui/console/command_dispatcher/bofloader.rb @@ -1,138 +1,218 @@ # -*- coding: binary -*- + require 'rex/post/meterpreter' module Rex -module Post -module Meterpreter -module Ui - -### -# -# Bofloader extension - load and execute bof files -# -### -class Console::CommandDispatcher::Bofloader - - Klass = Console::CommandDispatcher::Bofloader - - include Console::CommandDispatcher - - # - # Name for this dispatcher - # - def name - 'Beacon Object File Loader' - end - - DEFAULT_ENTRY = 'go' - - @@bof_cmd_opts = Rex::Parser::Arguments.new( - ['-h', '--help'] => [ false, "Help Banner" ], - ['-e', '--entry'] => [ true, "The entry point (default: #{DEFAULT_ENTRY})." ], - ['-f', '--format-string'] => [ true, "bof_pack compatible format-string. Choose combination of: b, i, s, z, Z" ], - ) - - # TODO: Properly parse arguments (positional and named switches) - - # - # List of supported commands. - # - def commands - { - 'execute_bof' => 'Execute an arbitrary BOF file', - } - end - - def cmd_execute_bof_help - print_line('Usage: execute_bof [arguments [arguments]] --format-string [format-string]') - print_line("Example: execute_bof /root/dir.x64.o C:\\ 0 --format-string Zs") - print_line(@@bof_cmd_opts.usage) - end - - # Tab complete the first argument as a file on the local filesystem - def cmd_execute_bof_tabs(str, words) - return tab_complete_filenames(str, words) if words.length == 1 - fmt = { - '-e' => [ true ], - '--entry' => [ true ], - '-f' => [ true ], - '--format-string' => [ true ], - } - tab_complete_generic(fmt, str, words) - end - - def cmd_execute_bof(*args) - if args.length == 0 || args.include?('-h') || args.include?('--help') - cmd_bof_cmd_help - return false - end - - bof_args = nil - bof_args_format = nil - bof_cmdline = [] - bof_filename = nil - bof_json_filename = nil - entry = DEFAULT_ENTRY - - @@bof_cmd_opts.parse(args) { |opt, idx, val| - case opt - when '-f', '--format-string' - bof_args_format = val - when '-e', '--entry' - entry = val - when nil - bof_cmdline << val - end - } - - bof_filename = bof_cmdline[0] - - unless ::File.file?(bof_filename) && ::File.readable?(bof_filename) - print_error("Unreadable file: #{bof_filename}") - return - end - - if bof_args_format - if bof_args_format.length != bof_cmdline.length - 1 - print_error("Format string length must be the same as argument length: fstring:#{bof_args_format.length}, args:#{bof_cmdline.length - 1}") - return + module Post + module Meterpreter + module Ui + ### + # + # Bofloader extension - load and execute bof files + # + ### + class Console::CommandDispatcher::Bofloader + + Klass = Console::CommandDispatcher::Bofloader + + include Console::CommandDispatcher + + # + # Name for this dispatcher + # + def name + 'Beacon Object File Loader' + end + + DEFAULT_ENTRY = 'go'.freeze + + @@execute_bof_opts = Rex::Parser::Arguments.new( + ['-h', '--help'] => [ false, 'Help Banner' ], + ['-c', '--compile'] => [ true, 'Compile the input file (requires mingw).' ], + ['-e', '--entry'] => [ true, "The entry point (default: #{DEFAULT_ENTRY})." ], + ['-f', '--format-string'] => [ true, 'bof_pack compatible format-string. Choose combination of: b, i, s, z, Z' ] + ) + + # TODO: Properly parse arguments (positional and named switches) + + # + # List of supported commands. + # + def commands + { + 'execute_bof' => 'Execute an arbitrary BOF file' + } + end + + def cmd_execute_bof_help + print_line('Usage: execute_bof [arguments [arguments]] --format-string [format-string]') + print_line('Example: execute_bof /root/dir.x64.o C:\\ 0 --format-string Zs') + print_line(@@bof_cmd_opts.usage) + end + + # Tab complete the first argument as a file on the local filesystem + def cmd_execute_bof_tabs(str, words) + return tab_complete_filenames(str, words) if words.length == 1 + + fmt = { + '-c' => [ nil ], + '--compile' => [ nil ], + '-e' => [ true ], + '--entry' => [ true ], + '-f' => [ true ], + '--format-string' => [ true ] + } + tab_complete_generic(fmt, str, words) + end + + def cmd_execute_bof(*args) + if args.empty? || args.include?('-h') || args.include?('--help') + cmd_execute_bof_help + return false + end + + bof_args = nil + bof_args_format = nil + bof_cmdline = [] + entry = DEFAULT_ENTRY + compile = false + + @@execute_bof_opts.parse(args) do |opt, _idx, val| + case opt + when '-c', '--compile' + compile = true + when '-f', '--format-string' + bof_args_format = val + when '-e', '--entry' + entry = val + when nil + bof_cmdline << val + end + end + + bof_filename = bof_cmdline[0] + + unless ::File.file?(bof_filename) && ::File.readable?(bof_filename) + print_error("Unreadable file: #{bof_filename}") + return + end + + if bof_args_format + if bof_args_format.length != bof_cmdline.length - 1 + print_error("Format string length must be the same as argument length: fstring:#{bof_args_format.length}, args:#{bof_cmdline.length - 1}") + return + end + bof_args = bof_cmdline[1..] + elsif bof_cmdline.length > 1 + print_error('Arguments detected and no format string specified.') + return + else + print_status('No argument format specified, executing bof with no arguments.') + end + + if compile + bof_data = compile_c(bof_filename) + return unless bof_data + else + bof_data = ::File.binread(bof_filename) + end + + # loading all data will hang on invalid files like DLLs, so only parse the 20-byte header at first + parsed = Metasm::COFF.decode_header(bof_data[0...20]) + bof_arch = { # map of metasm to metasploit architectures + 'AMD64' => ARCH_X64, + 'I386' => ARCH_X86 + }.fetch(parsed.header.machine, nil) + + unless bof_arch + print_error('Unable to determine the file architecture.') + return + end + unless bof_arch == client.arch + print_error("The file architecture is incompatible with the current session (file: #{bof_arch} session: #{client.arch})") + return + end + + parsed = Metasm::COFF.decode(bof_data) + unless (executable_symbols = get_executable_symbols(parsed)).include?(entry) + print_error("The specified entry point was not found: #{entry}") + print_error("Available symbols: #{executable_symbols.join(', ')}") + return + end + + begin + output = client.bofloader.execute(bof_data, args_format: bof_args_format, args: bof_args, entry: entry) + rescue Rex::Post::Meterpreter::Extensions::Bofloader::BofPackingError => e + print_error("Error processing the specified arguments: #{e.message}") + return + end + + if output.nil? + print_status('No output returned from bof') + else + print_line(output) + end + end + + private + + def compile_c(source) + if client.arch == ARCH_X86 + mingw = Metasploit::Framework::Compiler::Mingw::X86.new + elsif client.arch == ARCH_X64 + mingw = Metasploit::Framework::Compiler::Mingw::X64.new + else + print_error("Unsupported client architecture: #{client.arch}") + return + end + + unless mingw.class.available? + print_error("#{mingw.mingw_bin} is unavailable, can not compile source code") + return + end + + ::Dir::Tmpname.create([::File.basename(source, '.c'), '.o']) do |destination| + output, status = Open3.capture2e(mingw.mingw_bin, '-c', source, '-I', Metasploit::Framework::Compiler::Mingw::INCLUDE_DIR, '-o', destination) + unless status.exitstatus == 0 + print_error("Compilation exited with error code: #{status.exitstatus}") + print_line(output) unless output.blank? + return + end + + bof_data = ::File.binread(destination) + ::File.delete(destination) + return bof_data + end + end + + def get_executable_symbols(coff) + executable_symbols = [] + coff.symbols.each do |sym| + next unless sym + next unless sym.sec_nr.is_a? Integer + + section = coff.sections[sym.sec_nr - 1] + next unless section + + next if section.name == sym.name + next unless section.characteristics.include?('MEM_EXECUTE') + next unless section.characteristics.include?('CONTAINS_CODE') + + # see: https://github.com/trustedsec/COFFLoader/blob/24da168356bd20438a4e66ef3261c5012344d362/COFFLoader.c#L182-L189 + if client.arch == ARCH_X64 + executable_symbols << sym.name + else + next unless sym.name.start_with?('_') + + executable_symbols << sym.name[1..] + end + end + + executable_symbols + end + + end end - bof_args = bof_cmdline[1..] - elsif bof_cmdline.length > 1 - print_error('Arguments detected and no format string specified.') - return - else - print_status('No argument format specified executing bof with no arguments.') - end - - bof_data = ::File.binread(bof_filename) - parsed = Metasm::COFF.decode_header(bof_data[0...20]) - bof_arch = { # map of metasm to metasploit architectures - 'AMD64' => ARCH_X64, - 'I386' => ARCH_X86 - }.fetch(parsed.header.machine, nil) - - unless bof_arch - print_error('Unable to determine the file architecture.') - return - end - unless bof_arch == client.arch - print_error("The file architecture is incompatible with the current session (file: #{bof_arch} session: #{client.arch})") - return - end - - output = client.bofloader.execute(bof_data, args_format: bof_args_format, args: bof_args, entry: entry) - if output.nil? - print_status("No output returned from bof") - else - print_line(output) end - end - -end - -end -end -end end